Jump to content

DrQuincy

Members
  • Posts

    246
  • Joined

  • Last visited

  • Days Won

    1

Posts posted by DrQuincy

  1. Just installed this and it is really good! Thanks @Robin S, I am abandoning my Process class from the other thread. ?

    menu.png.9de375c85478b2c6ab88365585761507.png

    One suggestion. Can you dynamically set permissions? If so, it might be nice to restrict the appearance of the menus to a permission. It doesn't really matter for my use case but thought it might be worth adding if it's easy enough.

    • Like 2
  2. Thanks, I wasn't aware of the $http variable. ?

    Looking through the functions I think possibly this is more suitable in this instance. https://processwire.com/api/ref/wire-http/send-status-header/ I'm sure HEAD work work — though I may be wrong!

    What I am wanting to do is send something other than a 200 status for the current request. It looks like $http->sendStatusHeader(405) would send the right header per my above exmaple. Is it possible to then show the error page template?

  3. Ooh, this looks great! Thanks Robin. I'm still on .165 at the moment so will have to upgrade. ?

    Out of interest is there a technical reason why it's limited to three menus? Also, do you think there will ever be support for the navJSON type links? I.e. Main > drop down item > third-level item.

  4. On 9/3/2021 at 12:30 PM, kongondo said:

    Great! I did the same and it refused to work. No errors, nothing. Nothing in the nav is getting picked up and nothing in the markup. How did you go about this? Thanks.

    Bear in mind each time you make a change to your .module file you need to refresh modules.

    Here's a working sample that should get you started.

    namespace ProcessWire;
    
    class ProcessSiteOptions extends Process {
    
    private $cmsPath = null;
    
    public static function getModuleInfo() {
    	
    	return [
    
    		'title'			=> __('Site options', __FILE__),        
    		'summary'		=> __('Custom site options', __FILE__),
    		'version' 		=> '0.0.1', 
    		'permanent'		=> false, 
    		'useNavJSON' 	=> true,
    		'permission'	=> 'page-edit',
    		'nav' 			=> [
    
    			[
    				
    				// URls relative to where this admin page is added
    				'url'		=> '../page/edit/?id=1023',
    				'label' 	=> 'Settings', // These labels are HTML entity encoded
    				'icon' 		=> 'cog'
    				
    			],
    			[
    				
    				'url' 		=> '../page/edit/?id=6601#ProcessPageEditChildren',
    				'label' 	=> 'Project filters', 
    				'icon' 		=> 'filter',
    				'navJSON' 	=> 'navJSON'
    				
    			]
    		
    		]
    
    	];
    	
    }
    
    
    public function ___execute() {
    	
    	return $this->render();
    	
    }
    	
    
    protected function render() {
    	
    	$html = '<p>Manage site options.</p><ul>';
    	
    	$sanitizer = wire('sanitizer');
    	
    	$nav = self::getModuleInfo()['nav'];
    	
    	foreach ($nav as $item) {
    		
    		$html .= '<li><a href="' . $sanitizer->entities($item['url']) . '">' . $sanitizer->entities($item['label']) . '</a></li>';
    		
    	}
    	
    	return $html . '</ul>';
    	
    }
    
    
    public function ___executeNavJSON($options = []) {
    	
    	$options = [];
    	
    	$options['list'] = [];
    	
    	// Unlike nav children above these can be dynamic
    	$options['list'] = self::getChildArray();
    	
    	return json_encode($options);
    	
    }
    
    
    private function getCMSPath() {
    	
    	// Caches the CMS path
    	return $this->cmsPath === null ? wire('pages')->get(2)->path : $this->cmsPath;
    	
    }
    
    
    private function getChildArray() {
    	
    	$page = wire('pages')->get('/');
    	
    	$sanitizer = wire('sanitizer');
    	
    	return [
    		
    		['url' => $this->getCMSPath() . 'foo-bar', 'label' => 'Arbitrary CMS link', 'icon' => 'smile-o'],
    		['url' => '/about/', 'label' => 'Arbitrary site link', 'icon' => 'user-o'],
    		['url' => $page->path, 'label' => $sanitizer->entities($page->title), 'icon' => 'home'] // Dynamic; these labels are not HTML entity encoded
    		
    	];
    	
    }

    A few points:

    • Don't put any comments before the namespace declaration as PW can't pick up on the module info properly
    • As @Robin S pointed out the first level of items are static, the rest (navJSON) can be dynamic

    Questions (just me being picky/trying to understand):

    • I don't know how you add third-level items, i.e. children on navJSON but I probably don't need these anyway
    • The labels on navJSON aren't HTML encoded but the labels on 'nav' are — why is this?  Is it so you can add HTML into the dynamic options? (strong, em) — just wondering why they are different
    • How can you make a link in 'nav' not do anything when clicked — i.e. just act as a parent for the child links (not a big deal, just curious)
    • How can you add a link (in either 'nav' or navJSON) that goes to an external site? I.e. to a different HTTP host
    • Like 1
  5. I know you can throw a 404 easily enough:

    throw new \ProcessWire\Wire404Exception(); 

    However, is there an easy way to throw other HTTP status codes that sends the appropriate HTTP header and loads the error page rather than the current template?

    For example, I have some pages where I want to restrict the request method to GET or POST only and therefore want to sent a 405 Method Not Allowed if the wrong method is requested. I don't just want to manually send the headers and exit() though — I want the error page to show and for ProcessWire to finish the request.

    Is this easy to do? I looked but couldn't find anything.

    Thanks.

    • Like 1
  6. I did a very quick text and it seems to work! ?

    I just created a new Process class based off one that was already in there and placed it in site/modules and added the nav, stripped back what I thought I didn't need and installed the module and it pretty much worked first time. It will take a bit of tweaking but the bit I was struggling with now works.

    Thanks again!

    • Like 1
  7. Thanks for the detailed response! ?

    I did think it was going to require an admin Process when looking at existing pages. From the looks of it then I could create my own Process class and add the nav there. My knowledge of PW isn't wide enough to know much about how this works but it looks like I should be able to figure it out based off the example you linked to.

    In this case it is fine to have it hard-coded in getModuleInfo(). It is just for some specific site options such as Settings, Config, Emails that are in the site tree. I just thought it'd be nice to have it in the top nav so it appears separate from everything else.

    Thanks also for the #ProcessPageEditChildren tip.

    Just one more question please: is it any easier to to add items to existing menus? For example, could I add some extra options to the Setup menu or does this require custom Process classes as well?

  8. I thought this would be really easy but can't figure it out.

    In the backend at the top you have Setup, Modules, etc that have drop downs with icons. You can see that these correspond to pages that use the admin template when looking under Admin in the site tree.

    What I want to do is add an entirely new menu with drop downs and icons that look exactly the same as this. I have a redirect template that I will use to send them to specific pages within the CMS — or even the front end.

    1. Can someone point me in the right direction for how to do this (if it is possible)? Including, if possible, setting menu icons. When I add the pages in the site tree the child pages don't show as a drop down.
    2. When you link to a CMS page is it possible to use a GET var in the URL to have the Children tab open instead of Content? In my drop down I want to link to sections that show all child pages.

    I hope that makes sense.

    Many thanks.

  9. I finally got round to try the language tools and they are great, as expected. I have a few basic questions please. Sorry there are a few here but I thought it better to do it in one thread. I have searched for the answers but couldn't find anything.

    1. How do you access a language-alternative field if the language has a dash in it? E.g. you can have language foo-bar but not $page->body_foo-bar; the field is not allowed (tried it without the dash and with an underscore when using the API to no avail)
    2. Is it possible to have the languages expanded by default? I.e. as though you have clicked the folder icon
    3. Is it possible to show the languages in a dropdown instead of tab for large numbers of languages? Just curious really. It's not often this would be used but I do have one site with 15+ languages that I may eventually port over.
    4. In Languages what are Live Search, Site Translation Files and Core Translation Files for? Is this irrelevant to the front end? It looks like it is for translating PHP files but just wanted to make sure it wasn't something I could make use of. I will only ever require English back-end and would store or translations in fields in the database.
    5. If you choose a non-default language in your profile in the back-end is there any way to know from the site tree that for a given page the name is not active? It doesn't appear so but I feel this is quite an important feature if managing lots of pages and languages.
    6. What is the rationale behind always searching the current language and the default language when using the API? There is a workaround here but just wondered as I wouldn't thought it better to only search the current language.
    7. Finally, let's say you have a latest news section on a site with five languages and you only wish to publish an article in one or two of those languages. Is that possible? From what I can gather so long as you are publishing in the default language you can simply uncheck the “Active” box for the languages you don't wish to have a page for. However, it looks like you must have the default one. I presume this is so it has all the fallbacks it needs. I guess it would be easy enough to throw a 404 for the default page.

    Thanks!

    • Like 1
  10. I have implemented this today and it seems to work well. I am storing the files in a protected folder and when I copy them there I am assigning a unique 64-bit token which is referenced via a repeater. This saves me having to manage files with the same name with a numerical post-fix.

    On the front-end I have a single secure-files template that uses URL segments to access the files.

    E.g. /secure-files/68af96520c980c0a/test.jpg and /secure-files/68af96520c980c0a/test.jpg?dl=1 to download. (reads from /my-private-folder/68af96520c980c0a.jpg)

    The only downsides are:

    1. If you upload a file and don't save it is technically in the public root (as mentioned above) and not deleted until the current page is accessed again
    2. You can’t view the files directly from the CMS as you can with standard Files/Images fields. Is there any field type that easily allows some arbitrary HTML based on current page field values so I could add links in?
  11. I can see there is a plugin that allows for you to store files outside the document root but it seems to be a while since it was updated and some users are reporting issues. I thought of a relatively simple way of doing this and wondered if anyone thought this approach was good — or if there was a better way.

    I am thinking I have a Files field and then a proxy field (probably a Repeater) that stores the file path, file name, description, etc. When a page is saved the files from the files Field are copied outside the document root and the data is added as new Repeater items. The Files field is then cleared.

    When proxy Repeater items are deleted the files are also deleted off the server. The user could also edit any additional fields on the repeater (i.e. meta data for the file); the path field though would not be editable.

    Then on the front end I can access then via their repeater ID. E.g. /secure-files/1067/ Or maybe even have a name of the file as a URL segment: /secure-files/1067/document.pdf (if that is possible).

    Would that work — or is there a simpler way?

    One thing that I wondered about is if you upload a file and then don't save the page. What seems to happen is the system purges it but only when you edit that page again. This is not a big deal for this particular use case but thinking about sites that may be dealing in documents with personal information on I am wondering if there is any bulletproof method of ensuring there are never any sensitive files left in the public folder.

    Thanks! ?

  12. Ah right, thanks for the clarification. ?

    Quote

    CSRF token is only reset if/when a reset was specifically requested while validating the token. Page save won't do this, but a custom CSRF check could do it.

    Just to be clear though, simply calling $session->CSRF->validate() will not reset the token, will it? That's all I'm doing, which I guess is a custom CSRF check.

    Thanks!

  13. Many thanks for the detailed reply. ?

    28 minutes ago, teppo said:

    It can also be invalidated if another CSRF check runs meanwhile, so technically user doing something else on the site that triggers a CSRF check could result in such an issue.

    Could this include simply saving a page in the CMS? How and why does this happen? It was my understanding CSRF tokens were basically just a random string tied to the session and so long as the value in the form and the value session match all is okay. If I understand what you're saying correctly this could get in the way and cause false negatives. I don't see why two legitimate requests couldn't happen concurrently. Is there any way to disable this?

    30 minutes ago, teppo said:

    In my experience an issue like this would most commonly be a result of something in the user's environment changing, i.e. IP or some other value used for session fingerprinting has changed and thus session gets invalidated. This behaviour can be tweaked via the $config->sessionFingerprint setting.

    I always have $config->sessionFingerprint = false in config.php because one of my colleague's IP changes all the time (several times a day) and kept getting logged out of the CMS. Does the CSRF token always become invalid on a IP change? If so, again this could be problematic. If an IP change does not affect this with $config->sessionFingerprint = false then it can't be this since, as I say, I always switch it off.

    I do wonder if it is to do with him having the front-end and back-end open at the same time.

    Thanks!

  14. I use PW's in-built CSRF for forms. So I add a hidden tag:

    <input type="hidden" name="<?= $session()->CSRF->getTokenName() ?>" value="<?= $session()->CSRF->getTokenValue() ?>" />

    And then when the form is sent I check:

    try {
    
        $session->CSRF->validate();
    
    } catch (\Exception $e) {
    
        // ...
    
    }

    It has always worked for me. I have a client saying that they got a CSRF error when they filled a form out (payment form; quite lengthy but can't imagine it taking more than five minutes). This person is not very technical so it is hard to get information out of them but I can only conclude they either took ages filling the form out or they were signed into the CMS and logged out after loading the form (the latter has happened to me before).

    My questions are:

    1. What causes the $session()->CSRF token to change? Is it directly tied to session_id() or can a change occur (assuming no manual regeneration in my code) without the session ID changing?
    2. Do you think it is advisable on longer forms that use CSRF to have a background AJAX call that runs a simple PHP script every few minutes that simply has session_start() in it (rather than create a more resource-heavy PW request) to keep the session alive?

    Thanks.

  15. Thanks for your reply.

    Weird, I replied last night and the reply has gone. Must not have submitted properly. ?

    I had a hook that was preventing pages that were pointed to via a Page Reference field from being unpublished. It worked fine but when you cloned a page it seems to flag a false positive and was regarded by the system as being linked to from every page.

    Anyway, I have since found a very simple fix: simply check when the page was created. If less than a few seconds old don't run the hook. This seems to work but I will test more thoroughly.

    $secondsAgoCreated = time() - $page->created;
    
    // Check it's older than 3 seconds; if a clone secondsAgoCreated seems to be 1
    if ($secondsAgoCreated > 3) {
    
    	// Run the hook
    
    }

     

  16. Exactly as the title says: can you know if a page was cloned in a Pages:saveReady hook? I know there is Pages::cloned but can't work out how to do something in cloned that is available in saveReady. I tried useing sessions but it didn't seem to work.

    So this would be a one-time thing per page. I.e. after the initial save it is no longer regarded as a cloned page.

    Does that make sense? Is there an easy way to do this?

    Thanks.

  17. I checked the docs and couldn't find an answer to this.

    If you define a hook such as Pages::saveReady it gets called when saving a page in the CMS. And also any validation gets checked.

    I notice though in your own code when you work with pages in the API, e.g. $page->save(), then validation checks don't take place. For example, if you had a number field and specified in its Input settings it has to be between 0 and 100 you could set it to 1000 via the API. Also, the Pages::saveReady doesn't get called. I have tested this with throwing an exception in a Pages::saveReady  and it doesn't seem to trigger when using $page->save() but prevents saving in the CMS.

    I assume this is intended behaviour as when you use the API you should be able to do whatever you want and it is up to you to perform the required checks before you save.

    My question is: when you run $page->save() or $pages->save($page), is there any way to make it run as though you were saving it in the CMS? I.e. throw an exception if validation fails and/or run relevant hooks.

    I hope that makes sense, thanks.

  18. Ah, perfect, thanks!

    So, this is what I've got and seems to work so far but I need to test it a bit more.

    public static function freezePages($selector) {
    	
    	// Make name and template read-only
    	\Processwire\wire('pages')->addHookAfter('ProcessPageEdit::buildForm', function(\Processwire\HookEvent $event) use($selector)  {
    		
    		$wrapper 	= $event->return;
    		$page 		= $event->object->getPage();
    		
    		if ($wrapper->has('_pw_page_name') === false || 
    			$wrapper->has('template')      === false || 
    			$wrapper->has('parent_id')     === false)  {
    			
    			return;
    			
    		}
    		
    		if ($page->matches($selector) === true) {
    
    			$inputState 							= \Processwire\Inputfield::collapsedNoLocked;
    			$labelPostfix 							= ' (page frozen, value not editable)';
    			
    			$wrapper->_pw_page_name->collapsed 		= $inputState;
    			$wrapper->_pw_page_name->label 	   	   .= $labelPostfix;
    			$wrapper->_pw_page_name->description 	= null; // Hide note about 09, etc
    			
    			$wrapper->template->collapsed			= $inputState;
    			$wrapper->template->label 	   		   .= $labelPostfix;
    			
    			$wrapper->parent_id->label 	   		   .= $labelPostfix;
    			$wrapper->parent_id->contentClass 		= 'parent-disabled'; // Setting to Inputfield::collapsedNoLocked shows the ID so instead just disable via a CSS class from custom CSS file
    			
    		}
    		
    	});
    
    	// Remove “Move” button
    	\Processwire\wire('pages')->addHookAfter('ProcessPageListActions::getActions', null, function(\Processwire\HookEvent $event) use($selector)  {
    		
    		$page = $event->arguments(0);
    		
    		$actions = $event->return;
    		
    		if ($page->matches($selector) === true) {
    			
    			unset($actions['move']);
    			
    		}
    		
    		$event->return = $actions;
    		
    	});
    
    }

    Usage is then like this:

    Hooks::freezePages('template=settings|site-map');

    Any matching pages cannot be moved or have their name, parent or template changed.

    Note the parent part is blocked via a CSS class so in my case I have some custom CSS included from admin.php:

    .parent-disabled {
        opacity: 0.5;
        pointer-events: none;
    }

    I would've used an inline style but couldn't find that option.

    • Like 2
  19. That's a much better idea, thanks @Robin S ?

    I have nearly got it working. My only issue is in the ProcessPageEdit::buildForm hook, how do I get the current page so I can match to my selector?

    I.e. how do I get to $page from here:

    \Processwire\wire('pages')->addHookAfter('ProcessPageEdit::buildForm', function(\Processwire\HookEvent $event) use($selector)  {
    			
    	$form = $event->arguments(0);
    
    	// ...

    $form is of type ProcessWire\InputfieldForm. How can I get to the $page being edited from that?

    Once I get this I can post the full working code.

    Thanks.

    • Like 1
  20. I have the following hook but it doesn't quite work. The idea is I can specify a selector and it prevents the client from moving matched pages and changing their name and template. I know there are already options that allow you to do some of this but I wanted to cover it in a single hook. It so the client doesn't break things by mistake and by fixing the name and location of something like /settings/ it means I can do $pages->get('/settings/') instead of $pages->get(1063).

    So I'd set the hook like this:

    Hooks::freezePages('template=settings|site-map|emails|contact|downloads');

    And then my function looks like this:

    	public static function freezePages($selector) {
    		
    		\Processwire\wire()->addHookAfter('Pages::saveReady, ProcessPageSort::execute', function(\Processwire\HookEvent $event) use($selector) {
    			
    			$page = $event->arguments(0);
    			
    			// When sorting pages this can be null so do nothing in that case
    			if ($page === null) {
    				
    				return;
    				
    			}
    			
    			// $page->created !== - filters out new pages as when you create them; only do this on pages alrady created
    			if ($page->created !== 0) {
    				
    				$page->of(false);
    				
    				if ($page->matches($selector) === true) {
    					
    					// Get previous values (see https://processwire.com/talk/topic/24216-how-to-get-old-page-values-in-a-processwire-page-save-hook-and-some-other-notes-on-save-hooks/)
    					$clone = clone($page);
    					\Processwire\wire('pages')->uncache($clone);
    					$origPage = \Processwire\wire('pages')->get($clone->id);
    					
    					// Debugging to see how any times it's called
    					\Processwire\wire('session')->warning($origPage->template . ', ' . $page->template);
    					
    					if ($origPage->name != $page->name || $origPage->template != $page->template) {
    						
    						\Processwire\wire('session')->warning('**' . $origPage->path. '** is frozen and cannot have its name or template changed', \Processwire\Notice::allowMarkdown);
    						
    						$page->name 		= $origPage->name;
    						$page->template		= $origPage->template;
    						
    					}
    
    					// Handles drag-and-drop
    					if ($origPage->path != $page->path) {
    						
    						throw new \Processwire\WireException($page->path . ' is frozen and cannot change its path');
    						
    					}
    					
    				}
    				
    			}
    			
    		});
    		
    	}

    It kind of works. It works when dragging and dropping in the page tree and it seems to prevent name changes. It only seems to be problematic when changing the template.

    If you remove all template references above (i.e. just check the name) it works fine. When you use the code as above and change the template it only seems to work if you change it to a template not matched by the original selector. It's almost like if you do anything with the template it gets called twice and on the second call the templates are the same. I know this because if you look at the debugging line it adds two warnings to the session if you change the template to something not in the original selector.

    Can any of you expert PWers spot an elementary mistake in my code? Is there something special I need to do if changing the template?

    I hope that makes sense — do ask if you need more context or information. I've been staring at it for ages and it's driving me mad!

    Thanks.

×
×
  • Create New...