ProcessWire 3.0.141 core updates

This week’s dev branch version brings you improvements to ProcessWire’s $input->cookie API variable, plus it adds the ability to modify system URLs and paths at runtime. This post also includes some examples to demonstrate just how useful this can be.

Expanded cookie support in core

Everyone is taking about cookies these days, they seem to be all the rage. Back in the 1990s, we never thought much about cookies, other than clearing them. But now, nearly every site I go to lately pops up a banner touting their use of cookies… even going so far as to ask me to click on something to make sure I know that they use them. Presumably because they are proud and I should be very impressed by their "cookie" technology. And certainly, I am; and I never delete my cookies anymore, now recognizing their value. These banners remind me of how my new refrigerator came with PolarXtraFresh™ and SuperFreshyChill™ stickers prominently stuck all over the front, so I would have to acknowledge and could appreciate its technology. Indeed, I was impressed enough to relocate them as bumper stickers on my car, and while I still don't know what they mean, I feel more confident and respected on the road. But with every site bragging about its use of cookies these days, it got me thinking that maybe ProcessWire’s cookie support wasn't as exciting as everyone makes it out to be in their cookie banners. So I went to work with a mission of making ProcessWire’s cookie support every bit as exciting as the perception, something maybe even approaching the level of EskimoCraft™ ice maker technology. So here it is…

PlatinumElvenKing™ cookie technology

You've always been able to read cookies in ProcessWire via the $input->cookie API variable. But if you wanted to set them from PHP, you had to use PHP’s somewhat cryptic setcookie() function. In ProcessWire 3.0.141, now you can set cookies as easily as you can get them. You can set cookies directly as a property of the $input->cookie variable, or you can use the set() method from it. The following two lines do the same thing, setting a cookie named "foo" with the value "bar":

$input->cookie->foo = 'bar';
$input->cookie->set('foo', 'bar'); 

Any later access to get the value of $input->cookie->foo will return the value "bar", at least until the cookie expires.

The way browsers work, cookies can only be set in the response headers, meaning, before any output starts. But ProcessWire’s cookies are smart enough to work where PHP’s setcookie() won't. If setting the cookie fails (like due to prior output) ProcessWire remembers it in the session instead, and sets it the response to the next web request (before any output).

Cookie options

If you've worked with cookies before, you might already know that there are a lot more settings involved in setting a cookie than just choosing a name and value for it. Things like when it expires and what can see it. So ProcessWire 3.0.141 also adds a $config->cookieOptions array that lets you specify all of this. It's kind of like the digital control panels on the front of fridges these days... something you'll probably look at once and maybe change one setting, and then never need again. It stores the values you want to use as your defaults. Here are the core defaults below that you may choose to modify by copying them into your /site/config.php. Though I think many will only choose to modify the "age" option:

$config->cookieOptions = [
  'age' => 604800,
  'path' => null,
  'domain' => null,
  'secure' => null,
  'httponly' => false,
  'fallback' => true
];

// or if you just want to change one setting:
$config->cookieOptions('age', 86400); // 1 day

Here's a description of all the cookie options:

  • age: Max age of cookies in seconds or 0 to expire with session (3600=1hr, 86400=1day, 604800=1week, 2592000=30days, etc.)

  • path: Cookie path/URL or null for PW installation’s root URL (default=null).

  • domain: Cookie domain: null for current hostname, true for all subdomains of current domain, domain.com for domain and all subdomains, www.domain.com for www subdomain.

  • secure: Transmit cookies only over secure HTTPS connection? (true, false, or null to auto-detect, substituting true for cookies set when HTTPS is active).

  • httponly: When true, cookie is http/server-side only and not visible to client-side JS code.

  • fallback: If set cookie fails (perhaps due to output already sent), attempt to set at beginning of next request? (default=true)

You can also specify any of these options as the 3rd argument to the $input->cookie->set() method, like this:

$input->cookie->set('foo', 'bar', [ 'age' => 3600 ]); 

Because "age" is the most likely property to change, you can substitute it was the 3rd argument (rather than an options array) if you prefer:

$input->cookie->set('foo', 'bar', 3600); 

To remove a cookie you can do this:

$input->cookie->remove('foo');

If you wanted to remove ALL cookies (other than those maintaining PW’s session) here's how:

$input->cookie->removeAll(); 

Using cookies vs. using session values

Most often we would would want to use sessions, as they are independent of user input. But cookies also have their own unique benefits, so there's plenty of reasons why you might choose to use cookies rather than session values. Here are the cases that come up most often for me in choosing cookies rather than sessions:

  • When you need to store/remember value(s) longer than a session would ever last (like days, weeks, months).

  • When you want to share the value between Javascript and ProcessWire.

  • When you’re not really using sessions (like when site is primarily delivered by ProCache).

Sessions are usually very short term relative to cookies, and you might want to remember something much longer than a Session would allow. For example, if your website represented 100 different home improvement stores (that sell refrigerators of course), and the user selected their "primary store location" for product availability, you'd want to remember that in a cookie for as long as the browser would let you. That way the user doesn't have to re-select their "primary store location" every time they visit the site and check on what products are available at their location. The same goes for all kinds of user conveniences and preferences that aid the user experience, ensuring the site doesn't cost the user time by making them do a lot of work every time they return.

For sites primarily delivered by cache (like ProCache), sessions may not be an option for most of the pages, because PHP and ProcessWire don't see the request unless it comes in as a POST request or has a query string to process. For sites like this, ProcessWire can be very useful for processing those POST or query-string requests to set cookie values, but the primary consumer of those cookie values ends up being Javascript.

When it comes to working with cookies on the Javascript side, I still love using the old but great jQuery cookie plugin. The PW core even comes with as slightly modified version of it here, and for sites where I'm using cookies, I usually have the jquery.cookie.js (or min.js) file as part of my scripts. The script may be 13 years old, but it still does exactly what I need, nothing more, nothing less. And ProcessWire’s $input->cookie is now setup to work just like it, and be just as simple to use.

Don’t forget that cookies are user input

As the $input->cookie name suggests, cookie values pass through input just like $input->get and $input->post. They may not be as easy for the user to manipulate, but like with any input, you should always assume they can be modified. Meaning, sanitize and validate anything you read from a cookie, before you use it for anything. Any of ProcessWire’s built in $sanitizer methods can be used on $input->cookie just like they can be used on $input->get and $input->post. For example:

$hello = $input->cookie->text('hello'); // sanitize as plain text
$email = $input->cookie->email('email'); // sanitize as email

When you set a value with $session->set(), you can assume it will remain unmodified, because it'll never pass through user input. But with cookies, just because you set a value with $input->cookie->set() doesn't mean it'll remain there unmodified. That value passes through the request headers of every access to your site, so it is in the realm of user input, unlike session values. That's an important distinction to keep in mind.

New hookable $templates->fileModified() method

Maybe this isn't as exciting as cookie and refrigerator technology, but I'll mention it for the few that might be interested. ProcessWire keeps track of every time that a template file is modified (up to once per request). So if you've made some change in /site/templates/basic-page.php (as an example), ProcessWire knows about it, and updates its internal modified timestamp for the template. In ProcessWire 3.0.141, this behavior is now hookable by adding a hook to the Templates::fileModified() method. The hook receives as an argument the Template object, which also has the filename as one of its properties.

$templates->addHook('fileModified', function($event) {
  $template = $event->arguments(0);
  $event->log->message("Detected change to $template->filename");
}); 

ProcessWire system URLs/paths now more customizable

ProcessWire 3.0.141 adds a few new methods to the $config API variable that enable you to modify various system paths and URLs at runtime. You want to do this rather than modifying $config->urls or $config->paths properties directly (something we've always advised against). Having these dedicated methods also enables us to provide the necessary documentation (and warnings) to accompany this kind of capability. These methods are designed to be called during ProcessWire's "init" or "ready" states, which (from code) can be accessed from /site/init.php or /site/ready.php, or from the equivalent autoload module methods. Here are the newly added methods that allow manipulation of system URLs and paths:

$config->setLocation('name', $dir); // changes both path and URL to $dir
$config->setPath('name', $path); // changes just path, leaves URL as-is
$config->setUrl('name', $url); // changes just URL, leaves path as-is

The name argument should be one of: cache, logs, files, tmp, or templates (or one of your own). These are all named system paths/URLs that you would access from $config->paths or $config->urls. For example, $config->urls->templates is a common one to use in your template files, like when rendering <script> tags for JS files or <link> tags for CSS files. The $dir, $path or $url arguments for these methods is of course the value that you are wanting to set them to. Paths/URLs relative to site root should omit the leading slash and retain a trailing slash. Though if you forget, it's also okay.

Of the 3 methods, I think the setUrl() and setLocation() are the two most likely to be used, though the setPath() also seems like a necessary part of the triad. To describe what these methods do, let's look at some examples…

Example 1: Changing the templates directory

The following example would be in /site/init.php or /site/ready.php (or equivalent module method). In this example we are changing the location (path and URL) of our /site/templates/ to use a new version of the files in /site/dev-templates/ so that we can test them out with user 'karen', while all other users on the site get our regular templates:

// change templates path and URL when user is 'karen'
if($user->name == 'karen') {
  $config->setLocation('templates', 'site/dev-templates/');
}

So now whenever user 'karen' browses the site, she is seeing a newer version of the site templates, while the rest of the site's users see the older version in /site/templates/.

Example 2: Changing the /site/assets/files/ URLs

Let’s say we created a symbolic link in our web root /tiedostot/ (Finnish for “files”) that points to /site/assets/files/. How you do this depends on the platform, but for anything unix or OS X based, you'd do it like this (assuming you are in your web root):

ln -s site/assets/files tiedostot

We want our file URLs to appear as /tiedostot/1234/img.jpg rather than /site/assets/files/1234/img.jpg. To do this, we change the URL for ProcessWire’s $config->urls->files to point to our new location (code in /site/ready.php):

if($page->template != 'admin') {
  $config->setUrl('files', 'tiedostot/');
}

Following that change, you'll notice that all of your file/image URLs are now coming from /tiedostot/1234/file.jpg rather than /site/assets/files/1234/file.jpg (where 1234 is the page ID).

Example 3: Changing site files to use a cookieless subdomain

Let’s say that we want to take advantage of the performance benefits of delivering our assets from a cookieless subdomain named files.mywebsite.com rather than delivering them from mywebsite.com/site/assets/files/. We also want to trim off the /site/assets/files/ part, since it will be redundant on our subdomain.

We'd first have to setup the subdomain and make sure that the server knows that accessing the files.mywebsite.com subdomain maps to the server path of /path/to/mywebsite.com/site/assets/files/ of the main site. How that's done depends on your hosting platform, but let's assume that part is already functional. To make it work in ProcessWire, we'd do the following from our /site/ready.php file:

if($page->template != 'admin') {
  $config->setUrl('files', 'https://files.mywebsite.com/');
}

Now all of your file/image references on the front-end of the site deliver from our cookieless subdomain. As a bonus, the same strategy also works for delivering your assets from a CDN, should you want to use one.

Security and stability considerations

This is important, so if planning to use these new functions, don't skip this part! ProcessWire’s .htaccess file contains protections for defined system URLs in several cases. So if you are re-mapping URLs recognized in the .htaccess file for anything in production use, you'll want to update your .htaccess file to recognize your new URLs too. When modifying URLs with $config->setUrl(), your .htaccess updates should add to what's already there rather than replacing existing rules. That's because the existing URLs will still work, so you'll still want them protected in addition your new ones.

While the core supports changing these URLs/paths, and it's working well in my testing, I'd still consider it experimental at this stage. It's also very possible that some modules don't support this... It just depends on whether the modules are using ProcessWire’s $config->urls and $config->paths or whether they've got any relative URLs/paths hardcoded. So while ProcessWire now lets you change these things at runtime, test it out thoroughly in a development environment before using in production.

That’s it for this week. Check out the ProcessWire Weekly this weekend and have a great weekend and week ahead!

Comments

  • HMCB

    HMCB

    • 5 years ago
    • 73

    Thanks Ryan. Love those cookies!

  • Joe Regan

    Joe Regan

    • 5 years ago
    • 61

    If $config->setUrl('files' is used to serve the assets from something like s3, while your php files are served from the web server, does it serve on both the front end api and the backend admin?

    • Ryan

      Ryan

      • 5 years ago
      • 82

      Hi Joe, ProcessWire needs file system access to files, so that it can read things like image dimensions, exif/meta data, and so on. So if you are going to use S3, then you'll want to do so using something like s3fs-fuse (or one of the variations), so that ProcessWire can still access the files directly, rather than just know the filename. I've also heard EFS is another way to accomplish this, though have no experience with that. If you take one of these routes, you can independently set your 'files' URL to the S3 URL, while your 'files' Path points to the mounted S3 location in the file system.

      If you want to offload the delivery of file assets, what I prefer instead (and think is more efficient) is to keep the files in their native location with PW, but to instead use a CDN (like Cloudfront) and change the files URL to point to the CDN URL. When it comes to the backend/admin, don't use the CDN and instead let the files deliver from their local/native URL. The new setUrl() method technically makes this possible. ProCache also has the ability to manage all of this CDN for you, without the need for any code.

 

Latest news

  • ProcessWire Weekly #547
    In the 547th issue of ProcessWire Weekly we're going to check out the latest core updates, introduce a couple of new modules, and more. Read on!
    Weekly.pw / 2 November 2024
  • Custom Fields Module
    This week we look at a new ProFields module named Custom Fields. This module provides a way to rapidly build out ProcessWire fields that contain any number of subfields/properties within them.
    Blog / 30 August 2024
  • Subscribe to weekly ProcessWire news

“ProcessWire is like a breath of fresh air. So powerful yet simple to build with and customise, and web editors love it too.” —Margaret Chatwin, Web developer