Jump to content

ren

Members
  • Posts

    24
  • Joined

  • Last visited

Posts posted by ren

  1. Upgraded a site from 3.0.98 to 3.0.123 and now all admin pages are timing out. The problem seems to be debug mode in combination with using WireCache for around 3,400 items. Confirmed on a fresh install, but would appreciate if anyone could try replicating before I file an issue...

    To replicate, in config.php:

    $config->debug = true;
    $config->debugTools = array(
    	'pages',
    	'api',
    	'session',
    	'modules',
    	'hooks',
    	'database',
    	'db',
    	'timers',
    	'user',
    	'input',
    	'cache',
    );

    Fill the cache with some dummy data:

    for ($i = 1; $i < 4000; $i++) {
    	$cache->save($i, $i);
    }

    Then try and view any admin page.

    The issue goes away if 'cache' is removed from $config->debugTools , or if the site is downgraded to 3.0.98.

    Happens with or without Tracy.

  2. There is already some information on how to do this but it's a bit scattered, so here's a quick tutorial...

    This is how $session->CSRF->hasValidToken() handles AJAX requests, by getting the token name and value from request headers:

    if($this->config->ajax && isset($_SERVER["HTTP_X_$tokenName"]) && $_SERVER["HTTP_X_$tokenName"] === $tokenValue) {
    	$valid = true;
    } else if($this->input->post($tokenName) === $tokenValue) {
    	$valid = true; 
    }

    So all we need to do is to pass the name and value to our JavaScript and set the right headers with our AJAX request.

    We could instead of setting headers put the name and value pair into the body of a POST request, as the second conditional doesn't care if the POST request is received via AJAX or not. But this limits the request to POST and the body data type to either multipart/form-data or application/x-www-form-urlencoded (because that's all that PHP's $_POST superglobal will handle). Admittedly this might be 99% of use cases, but for example if a GET request invokes significant processing then CSRF protection could be used to prevent a DOS attack on the endpoint. So, for the sake of a bit more flexibility, and to be more in keeping with what PW expects, we'll use request headers.

    I find it neater to write the token name and value into HTML and retrieve that using JavaScript DOM functions, instead of writing JavaScript with PHP ?. So somewhere, maybe in init.inc.php, get the token name and value:

    $csrfTokenName = $session->CSRF->getTokenName();
    $csrfTokenValue = $session->CSRF->getTokenValue();

    Then in a global footer template:

    <div class="js-csrf-token" style="display:none;" data-csrf-token-name="<?php echo $csrfTokenName; ?>" data-csrf-token-value="<?php echo $csrfTokenValue; ?>"></div>

    Now to retrieve the token in JavaScript:

    function getCsrfToken() {
    	const csrfTokenNode = document.getElementsByClassName('js-csrf-token')[0];
    
    	return {
    		name: csrfTokenNode.getAttribute('data-csrf-token-name'),
    		value: csrfTokenNode.getAttribute('data-csrf-token-value'),
    	}
    }

    Then in any AJAX request:

    const csrfToken = getCsrfToken();
    let xhr = new XMLHttpRequest();
    
    // ...
    // open the request and do some configuration
    // ...
    
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // tell the server and PW that it's an AJAX request
    xhr.setRequestHeader('X-' + csrfToken.name, csrfToken.value); // create a header with the token
    
    // ...
    // send the request
    // ...

    And that's all, in the endpoint just do...

    if ($session->CSRF->hasValidToken()) {
    	// works for AJAX!
    }

    This has used the default CSRF token created for every session, but to create and use a different token just specify an id:

    $csrfTokenName = $session->CSRF->getTokenName('myAjaxToken'); // this creates a token if one with the given id doesn't exist
    $csrfTokenValue = $session->CSRF->getTokenValue('myAjaxToken');

    and...

    if ($session->CSRF->hasValidToken('myAjaxToken') {
    // ...
    }

     

    • Like 9
    • Thanks 5
  3. 2 hours ago, teppo said:

    As a one-off example of putting Sanitizer to work, I'm currently working on a module in which I convert template names to pascal case class names, so recently added $sanitizer->pascalCase() came in handy there.

    Ooh, that sounds cool! ?

    2 hours ago, teppo said:

    not only does this provide an extra layer of safety, but it also allows me to display more helpful error messages to the end-user.

    Yeah, that makes sense.

    2 hours ago, teppo said:

    $sanitizer->truncate() is a good example of this

    Forgot about that lovely new addition! Looking forward to not writing my own functions ?

    2 hours ago, teppo said:

    Personally I'm also a firm believer in the "defence in depth" practice: can't have too many layers of protection

    Agreed that there's no harm in guarding against any potential changes.

    2 hours ago, teppo said:

    By the way, in your code you mention that Inputfields run Sanitizer methods. It's probably worth noting that Inputfields won't interfere while you're storing data over the API, and thus they won't prevent anything from being saved to the database either. Fieldtypes handle data storage, and Inputfield settings kick in when you edit that value in the Admin.

    AH! ?Right, so in the example code, $form->processInput applies the Inputfield sanitizers but $pages->add does not?

    Thanks for the wide-ranging examples!

    • Like 2
  4. If I were explaining why I love PW to another developer I'd say...

    1. Unopinionated
    2. Powerful stable API
    3. Security
    4. Extremely active, friendly, *knowledgeable* community.

    I feel like that covers the major pain points that CMS/CMF developers feel, particularly 1) and 2).

    PW has so many great points that it's tempting to want to cover them all. I kinda agree with others that maybe there are too many points, and too much content.

    • Like 2
  5. Love it!

    The only thing I'm not sure about is the emphasis on...

    Quote

    you can launch a ProcessWire site and then leave it for years without maintenance or updates [...] It’s that reliable and secure

    I feel that a low maintenance overhead is already strongly implied by reliability and security sections.

    I like to run updates if only to keep the CK Editor plugin up-to-date, which *does* have semi-regular security patches.

    It might not appeal to developers for whom updates are bread and butter.

    I'd rather see something like...

    Lower maintenance costs, because security, reliability, and a stable API resulting in painless upgrades. Yeah, I once did a major version PW upgrade on a fairly complex 30,000+ page site and *nothing* broke! Zero. Zilch. Nada. It brought tears to my eyes. A lack of breakage that is unheard of in CMS land. Compare with regular minor version breakage for some other CMS ?

    • Like 6
  6. Hi,

    This is kind of an open ended question, but I find I'm not using $santizer much because the PW API takes care of most sanitization, and I'm wondering if this is actually ok.

    Take for example a contact form which creates a page for each message and emails admin. Something like this...

     

    Edit: this is incorrect

                    // The page's Inputfields run the same sanitizers as processInput() above
                    // Optionally with stripTags enabled

    See the reply from @teppo

     

    $form = $modules->get('InputfieldForm');
    $form->action = $page->url;
    $fullNameField = $modules->get('InputfieldText');
    $fullNameField->name = 'fullName';
    $fullNameField->required = 1;
    $emailField = $modules->get('InputfieldEmail');
    $emailField->name = 'email';
    $emailField->required = 1;
    $messageField = $modules->get('InputfieldTextarea');
    $messageField->name = 'message';
    $messageField->required = 1;
    $form->append($nameField);
    $form->append($emailField);
    $form->append($messageField);
    $csrfTokenName = $session->CSRF->getTokenName();
    $csrfTokenValue = $session->CSRF->getTokenValue();
    
    $fullNameMarkup = '';
    $emailMarkup = '';
    $messageMarkup = '';
    
    if ($input->post->submit) {
    	// processInput assigns $input values to the form Inputfields by calling each Inputfields' setAttribute() method
    	// setAttribute() calls setAttributeValue() which does sanitization, e.g.
    	// InputfieldText runs $sanitizer->text with stripTags set to false by default
    	// InputfieldTextarea runs $sanitizer->textarea with stripTags set to false by default
    	// InputfieldEmail runs $sanitizer->email
    	$form->processInput($input->post);
    	
    	// Output needs to be entity encoded
    	$fullNameMarkup = $sanitizer->entities($form->fullName->value);
    	$emailMarkup = $sanitizer->entities($form->email->value);
    	$messageMarkup = $sanitizer->entities(nl2br($form->message->value));
    	
    	if (!$form->getErrors()) {
    		$adminEmail = sendEmailToAdmin($fullNameMarkup, $emailMarkup, $messageMarkup);
    		
    		if (!$adminEmail) {
    			return wireRenderFile($config->paths->templates . 'includes/contact/failure.inc.php', [ 'type' => 'send email' ]);
    		}
    		
    		try {
    			$createPage = $pages->add('contact', '/contacts/', [
    				// The page's Inputfields run the same sanitizers as processInput() above
    				// Optionally with stripTags enabled
    				'contact_full_name' => $form->fullName->value,
    				'contact_email' => $form->email->value,
    				'contact_message' => $form->message->value,
    			]);
    		} catch (WireException $e) {
    			return wireRenderFile($config->paths->templates . 'includes/contact/failure.inc.php', [ 'type' => 'create page', 'error' => $e ]);
    		}
    
    		return wireRenderFile($config->paths->templates . 'includes/contact/success.inc.php'
    	}
    }
    
    return wireRenderFile($config->paths->templates . 'includes/contact/form.inc.php', [
    	'fullNameMarkup' => $fullNameMarkup,
    	'emailMarkup' => $emailMarkup,
    	'messageMarkup' => $messageMarkup,
    	'csrfTokenName' => $csrfTokenName,
    	'csrfTokenValue' => $csrfTokenValue,
    	'errors' => $form->getErrors(),
    ]);

     

    So processInput() ensures the email address is valid, and all markup is entity encoded.

    Even stripping tags seems unnecessary since page fields always give entity encoded output unless output formatting is disabled.

    So I only really use $sanitizer->entities(), $sanitizer->selectorValue(), and occasionally $sanitizer->name();

    Am I missing something?

    How do you use $sanitizer?

  7. Hi folks, wondering if anyone's encountered this problem before?...

    1) Trash a page from the page tree (screenshot1.png)

    2) The page is removed from the page list (screenshot2.png)

    3) Close the page tree (screenshot3.png)

    4) Open the page tree, the page reappears (screenshot4.png)

    If I do a browser refresh the page permanently disappears, so it seems the problem is with the AJAX admin.

    Replicated with:

    • Clean installations of ProcessWire v 3.0.98, 3.0.96 and 3.0.34
    • With and without AdminKitUI
    • No additional modules
    • Basic profile
    • Linux Chrome 67.0.3396.99
    • Linux Firefox 61.0.1
    • PHP 5
    • PHP 7
    • Restrictive and fully permissive file permissions

    Sometimes it takes adding and deleting a few pages for the problem to appear.

    There are no console errors.

    Thanks

    screenshot1.png

    screenshot2.png

    screenshot3.png

    screenshot4.png

  8. Clarification: by 'setup costs' I mean any initial costs associated with getting to grips with things, i.e. the codebase, server and deployment strategy etc.

  9. Hi folks,

    My department is planning to move up to five of our employer's websites in-house and onto ProcessWire from a CMS which has not seen much development in the last few years. They're B2B magazine websites and we feel that ProcessWire would be a great fit. But there are concerns over support. I'm the only server side programmer, so what would happen were I to leave or be hit by the proverbial bus? :lol:

    So we're looking for some kind of insurance. Thinking along the lines of a support contract, covering maybe 2 hours of support time per month. With a commitment to become more involved should the need arise, so providing more support if needed to someone who takes over my role, or in the 'bus' type emergency, be willing to take over full support duties for a limited period of time.

    The 2 or so hours would be used for providing advice if I were to really get stuck. But I'm happy using the forums for help, and I've built a prototype of one of the sites without huge difficulty (go ProcessWire!), so under normal circumstances they probably won't be used.

    The five sites are all quite large (the prototype site has 40 templates and 95 fields), consisting of news sections, a jobs section, a business directory, reader polls and social media integration. Some of the sites allow membership for the business directory, so companies can login and manage their listing and upload press releases, via a custom dashboard (implemented as a Process module).

    All code will be commented and held in a git repo.

    We're located in the UK. Email support would be sufficient I think.

    If you're interested, please PM me with a rough estimate of costs for:

    • The ~ 2 hours per month standard support.
    • Full 'emergency' support of all 5 websites.
    • Any one-off setup costs.

    And/or suggest an alternative pricing structure / proposal.

    Again at this stage we're just looking for a rough idea of costs and who would be available, so ballpark figures would be fine :-)

    If you could also provide a link to your website please.

    Please let me know if you would like any further information.

    Thanks :-)

    • Like 2
  10. 10 hours ago, Robin S said:

    Not sure but perhaps because you are not checking the process property of $this->page, i.e. instead of this...

    
    || $this->wire("process") == 'ProcessLogin'

    ...try...

    
    || $this->page->process == 'ProcessLogin'

     

    Doh. Yes, that works :-) Prefer this to matching urlSegment I think.

    Thanks!

  11. Hi @Robin S,

    Thanks, Tracy Debugger is looovely. I did as you said, and put db calls to log both cases (redirect and no redirect):

    // do not redirect if page matches:
    if($this->page->template != "admin" // any non-admin page 
        || $this->page->is($this->redirectPage) // the dashboard page (prevent infinite loop)
        || $this->page->parent->is('/admin/login/') // various attempts to allow logging out
        || $this->wire("process") == 'ProcessLogin'
        || strpos($this->page->url, $this->wire('config')->urls->admin . 'login/logout') !== false
    ) {
    	bd($this->page, 'no redirect page');
    	return;
    }
    
    bd($this->page, 'redirect page');

    When I tried to logout it logged both cases, with 'redirect page' first:

    data protected => array (3) title => "Login" (5) process => "ProcessLogin" (12) urlSegment => "logout" (6)

    ... for some reason 'ProcessLogin' is sailing through the conditional.

    Followed by 'no redirect page':

    data protected => array (2) title => "Dashboard" (9) process => "ProcessDashboard" (16)
    The first log gave me the idea to match the urlSegment, which works a charm!
    

    So if it's of any use to anyone, the working code is:

        if($this->page->template != "admin" || $this->page->is($this->redirectPage) || $this->input->urlSegment1 == 'logout') {
          return;
        }
    
        // find roles set in module configuration and create array
        $roles = explode(',', $this->userRoles);
    
        // for each user in module config check to see if current user
        foreach ($roles as $key => $val) {
    
          // get a role from the roles array
          $roles[$key] = trim($roles[$key]);
    
          // if current user found with role then redirect
          if($this->user->hasRole($roles[$key])){
    
            // redirect session to page stored in config
            $this->session->redirect($this->redirectPage);
    
            // code should never get here but this is a simple fallback
            break;
          }
        }

    Thanks Robin, you've been a great help, and I'm sure to be using Tracy Debugger again :)

    • Like 1
  12. Hi all,

    I'm creating a website for a magazine publisher. This will include a supplier section with press releases attached to company profiles. The idea is that companies can register and manage their profile and press releases (CRUD) via a dashboard.

    The dashboard is a Process module. Ben Byford's RedirectAdminPages module is being used to lock out the rest of the backend, with a few modifications.

    The following code should redirect every admin page to the dashboard page but still allowing logging out. However logging out just redirects to the dashboard page:

        // do not redirect if page matches:
        if($this->page->template != "admin" // any non-admin page 
    	|| $this->page->is($this->redirectPage) // the dashboard page (prevent infinite loop)
    	|| $this->page->parent->is('/admin/login/') // various attempts to allow logging out
    	|| $this->wire("process") == 'ProcessLogin'
    	|| strpos($this->page->url, $this->wire('config')->urls->admin . 'login/logout') !== false
    	) {
          return;
        }
    
        // find roles set in module configuration and create array
        $roles = explode(',', $this->userRoles);
    
        // for each user in module config check to see if current user
        foreach ($roles as $key => $val) {
    
          // get a role from the roles array
          $roles[$key] = trim($roles[$key]);
    
          // if current user found with role then redirect
          if($this->user->hasRole($roles[$key])){
    
            // redirect session to page stored in config
            $this->session->redirect($this->redirectPage);
    
            // code should never get here but this is a simple fallback
            break;
          }
        }
      }
    }

    I'm surprised that this URL matching doesn't work:

    || strpos($this->page->url, $this->wire('config')->urls->admin . 'login/logout') !== false

    Because this does work for allowing page edit:

    || strpos($this->page->url, $this->wire('config')->urls->admin . 'page/edit') !== false

    Any ideas?

    That issue aside, as a learning process I'm going to re-implement the dashboard as front-end pages. I guess that the advantage of using the ProcessWire back-end is most of the functionality is already there, you don't need to create forms and handle the processing, create / import css etc. So it'll be interesting to see how much of a difference this makes, and how much control each approach provides. I'd love to have some thoughts and feedback from those of you who've tried both methods?

    PS this will be my third website created with ProcessWire and it's already been a lot of fun, and as a CMS it 'just feels right', so a big thanks to Ryan and everyone who has contributed.

    Thanks :)

    • Like 1
×
×
  • Create New...