Getting WordPress cron work in multisite environment

There are several bits and pieces around that tell you how to get the WordPress cron work properly in simple situations. I just spent a morning getting it all working in a multisite environment so here come my notes on how to get it to work properly.

“Cron” is the scheduler of background tasks. You want to disable the built-in cron simulator when your site has the capability to run the normal UNIX cron jobs. The built-in simulator is quite unreliable and causes problems sooner or later. It is a good thing to have when the normal UNIX cron is not there but if you can install cron jobs you are better off with a standard reliable regular execution of your background tasks.

First of all, the usually advised first step is quite correct whatever the environment. You go to the installation directory, edit your wp-config.php and add the line before that last require_once statement (or anywhere in the middle of the file really) to disable the built-in cron simulator:

/* Disable wp-cron, we run linux cron */
define('DISABLE_WP_CRON', true);

Next, for a site that is not a network, i.e. not a multisite, it is sufficient to add something like this as a cron job:

*/2 * * * * www-data /usr/bin/php /var/www/yoursite/wp-cron.php > /dev/null )

This executes the cron of WordPress every two minutes. If you cannot run the command-line PHP for some reason but have wget (or any other thing, like lynx, curl, whatever) you can add a line to cron to call a URL instead, although that’s going to consume a tiny bit more resources:

*/2 * * * * www-data /usr/bin/wget -q -O - http://yoursite/wp-cron.php > /dev/null

Many recommend adding the parameter “doing_wp_cron” at the end of the URL but that is a mistake. Having that parameter assumes that you already acquired a lock for the cron job execution and may cause problems if you have long running jobs. Without that parameter, the wp-cron will do what it is supposed to do: acquire a lock so there is no contention, and then run its jobs.

For the multisite, however, it does not work so easily. You must execute the wp-cron for each and every of the “sites” in your network separately. This is kinda nuts but that’s how things are. There is no way to do that from a command line (as far as I can tell), so you have to call the URLs at each site.

I have the following script that reads the names of the sites and goes through… -->

continue reading →

ownCloud: “Archives of type inode/x-empty are not supported”

I came across a strange problem while trying to install Mozilla Sync app on my ownCloud server. The application installation on ownCloud fails with a cryptic message:

Archives of type inode/x-empty are not supported

First things first, I tried to look it up and I found a few mentions of this problem on forums but no useful answers. The problem did not seem to have a direct solution, so I thought I would have a quick look myself.

The log file is actually much more informative:

…”copy(http://apps.owncloud.com/CONTENT/content-files/161793-mozilla_sync-1.4.zip): failed to open stream: no suitable wrapper could be found at …/private/installer.php#72″,…

After some head-scratching I realized that this means the script is trying to fopen a URL with the app to download. Ah, but fopen is, of course, disabled for security reasons, so ownCloud will not be able to use it. That is what that “no suitable wrapper” means: php fails to load a file from a remote host because there is no way to do that. Simple.

So the fix for that is a manual install. There you go.… -->

continue reading →

Automatic language tagging with Polylang in WordPress

I use the Polylang plugin for the multi-language capabilities of the sites written on WordPress. One feature that I implemented and find extremely useful is the automatic tagging of the posts with a language tag (like “en”, “ru”, etc.) when the post is saved. The code that goes into the functions.php file of the theme is as follows:

function tigra_add_polylang_language_tag( $post_id ) {
        global $polylang;
        if (isset($polylang) ) {
                if ( !$polylang->model->get_post_language($post_id) ) {
                        $polylang->model->set_post_language($post_id, pll_default_language());
                }
                $post_lang = $polylang->model->get_post_language($post_id);
                $languages = $polylang->model->get_languages_list(array('fields' => 'slug'));
                $post_tags = get_the_tags($post_id);
                $post_tags = wp_list_pluck($post_tags, 'name');
                $post_tags = array_diff($post_tags, $languages);
                if ( empty($post_tags) ) {
                        $post_tags = array();
                }
                array_push($post_tags, $post_lang->slug);
                wp_set_post_tags($post_id, $post_tags);
        }
}
add_action( 'save_post', 'tigra_add_polylang_language_tag' );

The code verifies that the default language is set (as I heard some reports that the mobile applications sometimes manage to bypass the Polylang capabilities) and sets it if necessary. Then, it extracts all tags from the post, wipes all supported (selected in Polylang configuration) language tags and adds a single tag corresponding to the language of the post.

This procedure makes sure that the language tag is always present and that the old tag is wiped when you change the language of the post. Now you can retrieve the posts, sort or do other manipulations based on the tag.

Big kudos to Chouby for the plugin and the help with code.… -->

continue reading →

CakePHP: plugins models

I spent two days trying to trace why the plugin I am writing did not work. Eventually I realized that the files defining the models were not loaded at all. After several hours trying to figure this out I came across this explanation provided by zuha-3:

My bet is that you don’t have a plugin defined somewhere. I had the same problem in a couple of places after the upgrade because I had forgotten to name which plugin the model could be found in.


belongsTo = array(
className = [PLUGIN].[MODEL]

instead of...

belongsTo = array(
className = [MODEL]

And this goes for all relationships, and you might have just missed one.

And so finally it worked. This should be like written in really large letters on the page that talks about plugins in CakePHP documentation. Two days for this simple stupid thing…… -->

continue reading →

Web security 0.1

I had thus silly idea to quickly throw together a website for myself to list up my motorbikes. Well, not “mine” per se, I do own only one, but the ones I have ridden over the years and have an opinion about. Since I like to do things “my way”, I searched for a rapid development framework instead of heading over to a motorcycle website. Silly me. But that’s besides the point.

I looked at the CakePHP framework and it appears a solid piece of engineering I could be happy with. The only problem is that I have been trying to get the user registration and login set up for the better part of two months now (I have a day job too).

There are plugins for doing user management and complete login and registration libraries but all that I looked at share one thing in common (and with CakePHP itself, too): they are written with a frightening disregard for security. Every single one of them. So much for Open Source and crowd-sourcing. I would not use any of them to manage my website. Period.

So, I embarked on a quest to write my own plugin for CakePHP that would demonstrate what you can do with the user sessions security if you really put your mind to it. I have a first draft running now, very simple actions only but it works. So I am confident that once I get my head around the concept of plugins in CakePHP I will be able to provide a plugin with far better security for user sessions.

Some points that come to mind when thinking what has to be done vs. what is usually done:

  1. The user identifier is usually predictable, allowing often for a user listing. It is either an auto-increment in the database, or a UUID. Neither is unpredictable. A random 64-bit value will be far better, even when the random generator is not all that great.
  2. The user name is the primary identification. This is, in my personal opinion, passe. The user ID is the e-mail address.
  3. The confirmation links (registration, password change) are silly MD5 hashes of the current time. I think, again, a 64-128 bit random will be so much better.
  4. Some send out a new password to the user in an e-mail. Instead of a random token. That is simply not to be done.
  5. The “remember me” cookie is usually implemented by sending a user name and a password hash to the user browser in a cookie. That is plain silly too. This, again, has to be a random token stored in your database and given to the user.

That’s what I come up with just off… -->

continue reading →

CakePHP: bind hasMany as hasOne

This is totally brilliant. I came across this marvel somewhere and adapted to my application. See, if you have a hasMany relation, you end up with (1) an extra query and (2) with a lot of data. I have a case where I just need the last (time of creation) row. So I basically want to bind that model in a hasOne relation where the one row is determined by an expression selecting a single row.… -->

continue reading →