We use Incapsula as a reverse proxy for some of our sites, and recently ran into an issue with what appears to be a wilful violation of the HTTP specification by Incapsula.

Incapsula’s documentation says the following about how it handles Cache-Control headers:

Cache-Control - if it's set to no-cache / private / must-revalidate / proxy-revalidate/ no-store – then it'll prevent caching by Incapsula, unless site Acceleration Mode is set to Aggressive.

Unfortunately this isn’t the case. The following response headers from an origin server:

    Cache-Control: max-age=86400, private
    Content-Encoding: gzip
    Content-Length: 2431
    Content-Type: text/html; charset=utf-8
    ...

Get converted into this by Incapsula’s proxy:

    Cache-Control: max-age=86398, public
    Content-Encoding: gzip
    Content-Length: 2431
    Content-Type: text/html; charset=utf-8
    ...

This is a problem because what was meant to be a private resource has been cached and made public by Incapsula.

We contacted Incapsula to report this issue. This was the explanation we received:

private: Indicates that all or part of the response message is intended for a single user and MUST NOT be cached by a shared cache.

The max-age directive on a response implies that the response is cacheable (i.e., "public") unless some other, more restrictive cache directive is also present.

Since Incapsula is a CDN, which considers as a shared cache, the value of max-age=NUMBER and Cache-Control=private contradict each other and must not configured in this way.

The statement about max-age implying that a resource is public is false. The HTTP specification explicitly allows private resources to have a max-age:

private

Indicates that all or part of the response message is intended for a single user and MUST NOT be cached by a shared cache. This allows an origin server to state that the specified parts of the response are intended for only one user and are not a valid response for requests by other users. A private (non-shared) cache MAY cache the response.

The correct behaviour would be for Incapsula to give the private directive precendence over max-age (rather than the other way around). Instead it takes the rather dangerous step of simply removing the private directive!

In any case, the point is that Incapsula will violate a Cache-Control: private directive unless you also set max-age=0.

Bolt is a great little CMS, but I am not fond of the recommended installation methods because they don’t make it easy to version control your site without having to include the whole Bolt repository. Better, then, to install it as a Composer library. These instructions are based on these ones, modified to work with the latest version of Bolt.

Create your project directory with the following structure:

/project
  /config
  /theme
  /web
    /bolt-public
    /files

At the root of your project create a composer.json file that looks like this:

{
    "repositories": [
        {
            "type": "vcs",
            "url": "https://github.com/bolt/krumo"
        }
    ],
    "require": {
        "bolt/bolt": "dev-master"
    },
    "minimum-stability": "dev",
    "prefer-stable": true,
    "scripts": {
        "post-install-cmd": [
            "Bolt\\Composer\\ScriptHandler::installAssets"
        ],
        "post-update-cmd": [
            "Bolt\\Composer\\ScriptHandler::installAssets"
        ]
    }
}

Note that Bolt requires the bolt/krumo respository which is not published on Packagist, so you have to specify it as a GitHub repo.

Now install the dependencies with composer install.

Put your bolt configuration files in the config directory. You can start with the templates in vendor/bolt/bolt/app/config. Add a configuration entry to tell Bolt where your theme files are (you should put your template files directly inside this directory):

theme_path: /theme/

Copy the default Bolt .htaccess file (if you’re using Apache — otherwise create the equivalent configuration file for your server) from vendor/bolt/bolt/.htaccess to your project’s web, and set this directory as the root of your site in your server configuration. You will need to modify the paths referenced in the file to match the directory structure created above (the classes are placed in /web/bolt-public/classes/.

Create an index.php file in the web directory that bootstraps Bolt:

<?php
require_once '../vendor/bolt/bolt/app/bootstrap.php';

/* If you want to modify the Silex application before running it, you can do that here, e.g., */
$app->before(function (Request $request) {
    // ...
});

$app->run();

Now you should be able to browse to the root of your site and let Bolt handle the rest!

Note to WordPress plugin developers: use the pre_comment_approved filter with caution if there is a chance that Akismet will be active on sites using your plugin.

There is a long standing bug in WordPress, which is that a self-referential call to remove_filter breaks the filter — any functions that had been scheduled to run after the one being removed will not be called.

Unfortunately, this is exactly what Akismet does in the akismet_result_spam function — it tries to remove its own hook. The result is that whenever Akismet identifies a comment as spam, any functions hooked to the pre_comment_approved hook with priority greater than the default (10), will silently fail to execute.

As an aside, there are other, simpler ways to ensure that a function runs only once — static or class variables, for example.

Bug report and Trac ticket.

While working on the latest release of Conditional CAPTCHA for WordPress, I realised that Akismet stores a whole bunch of metadata for every comment, which can seriously bloat your WordPress database.

For each comment, Akismet records whether or not the comment was flagged as spam. Then, it stores a comment history which logs every change that is made to the status of the comment (spam, pending, approved etc), along with details of user who made it. This metadata is never deleted! At the very least, it means one extra row of metadata per comment (more for spam comments that are later approved).

I don’t find this history particularly useful — certainly not so much that it needs to be kept forever. Here’s what I use to prevent Akismet from storing this metadata:

function prevent_akismet_history( $check, $object_id, $meta_key ) {
    $to_filter = array( 'akismet_result', 'akismet_history', 'akismet_user', 'akismet_user_result' );
    if( in_array( $meta_key, $to_filter ) )
        return false;
    return $check;
}

add_filter( 'add_comment_metadata', 'prevent_akismet_history', 10, 3 );

I’ve also added an option to Conditional CAPTCHA to do this automatically.

To delete existing metadata, the following SQL query would work:

DELETE FROM `$wpdb->commentmeta` WHERE `meta_key` 
    IN ( 'akismet_result', 'akismet_history', 'akismet_user', 'akismet_user_result' );

There are two ways that plugin and theme authors can check whether comments are allowed on a post in WordPress:

Old-school way (wrong unless you have a very good reason):

if( $post->comment_status == 'open' ) {
	// do something here
}

Correct way:

if( comments_open() ) {
	// do something here
}

The second way is better because the comments_open() function is filterable — other plugins and themes can alter its output depending on context.

What makes WordPress so powerful is the underlying API. Plugin and theme authors can modify almost anything, and this is why there are so many thousands of plugins and themes out there. But we plugin and theme authors need to make sure we return the favour, so that our themes/plugins can play nicely with the rest. So, if your plugin or theme is using the first method above, please change it to use the second.

One other note for developers: WordPress has for some time now supported arbitrary post types (not just the posts and pages that we’re all used to), some of which don’t necessarily support comments at all. So it’s a good idea to check whether a post type even supports comments before running any comment-specific code:

if( post_type_supports( get_post_type(), 'comments' ) ) {
	// put comment related code here, including...
	if( comments_open() ) {
		// ...a comment form, maybe
	}
}
else {
	// sit back and relax
}

The code beneath the default WordPress theme uses both of the functions mentioned above.