Multi-site database upgrade claims success, but db_version not updated

Thanks to helpful prodding from TheDeadMedic, I see my problem is caused by my own application environment, which I failed to provide in the question because I didn’t think it was relevant. Sorry about that! I usually do briefly describe the environment when posting a question, but it has never been relevant before, so I skipped it this time 🙁

I’m using WordPress as a sort of subsystem to a Zend Framework MVC application that requires login. Access to nearly all WordPress pages requires that the user is logged-in to the ZF application; if there is not a logged-in ZF session when a request is made, the request is redirected to the login page, and the login page is displayed with a message saying login is required – but that page is displayed with a 200 status code (oddly enough, I’ve recently considered a change to give a 401 or 403 status when displaying the login page with that message, but I haven’t done that yet).

The network upgrade.php script uses cURL to request the upgrade.php script for each sub-site in the network. Those cURL requests are actually getting a response (after redirect) that is just the login page for the ZF application; but since it has a status code of 200, the network upgrade believes that each site upgrade succeeded, and everyone is happy, even though nothing was changed in the wp db.

I was confused/mistaken when I said that the root site was upgraded. I must have updated it manually and forgotten I had done so when I used phpMyAdmin to see what happened to the database. Starting over from a backup, I can see that when running the network upgrade, none of the wp_*options tables got their db_version updated (nor did the wp_blog_versions table get updated).

At the moment I could update each sub-site manually since there are only 10 of them, but that number should grow substantially. I guess I’ll ask a new question to see if there’s a way to get the cURL requests to use the calling code’s PHP session (the ZF application stores login state in the session).

The underlying problem and solution


As noted above, the cURL requests (which occur within WP_Http::request(), called by network/upgrade.php to upgrade each individual sub-site) were getting redirected to the ZF application's login page. This was because they were not accessing the same PHP session as the network upgrade script, because they were not being passed the PHPSESSID cookie. cURL and WP_Http::request() provide a means of passing cookies with a request, and there is a filter called ‘http_request_args’ that can be used to add cookies to a request. I could use that to pass the PHPSESSID cookie, which improved the behavior, but it still wasn’t quite right. It turned out there were two additional problems:

  1. My code to integrate WordPress into the ZF application prevents access to the root site unless the WordPress user is a super-admin. And since WordPress uses cookies to maintain login state, those cookies also need to be passed in the requests that run the upgrade scripts. Apparently the individual upgrade scripts themselves don’t check WordPress login status(!). So I used the ‘http_request_args’ filter to merge in the $_COOKIE superglobal.

  2. When a PHP session is active, the file holding the session data is locked. So in order for the request running an upgrade script to use the calling script’s session, the calling script needs to call session_write_close() to unlock the session file. Not an issue for WordPress itself since it doesn’t use the session, but essential for my integration with the ZF application. There was no hook I could easily find to add that call, so I edited it into the core class-http.php file immediately before its call to Requests::request, protected by tests that my ZF application integration is active, a PHP session is active, and the request is being made to the same server as specified in the current request:

    if (ZF_PLUGGABLE_RUN && session_status() == PHP_SESSION_ACTIVE && $defaults['cookies'] == $_COOKIE) {
        // PHP keeps session file locked, need to close it to allow the request to use it
        session_write_close();
    }
    

I do try to avoid editing core files, but there are a handful of places where I haven’t been able to find a suitable hook. I maintain the core files under git and separately track each file I change in each release, so that picking up a new release involves a fairly small well-defined merge process.

Final solution without editing core file


Although there wasn’t a hook immediately before the call to Requests::request(), it turned out that the ‘http_request_args’ filter was close enough. So now I’ve removed the core file edits and just define the ‘http_request_args’ filter as follows (within a class that defines other filters for integrating WordPress with my ZF application). I only add this filter in the case where I want my ZF application integration to be active, so I don’t need to test the ZF_PLUGGABLE_RUN constant:

public function http_request_args($args, $url) {
    $retval = $args;
    if (@parse_url($url, PHP_URL_HOST) == $_SERVER['SERVER_NAME']) {
        // The request is going to our own server, so we need to provide it our current cookies,
        // including both PHPSESSID for the session holding the ZF user, and the WordPress cookies
        // holding the WordPress user.
        if (! array_key_exists('cookies', $args)) {
            // The default array settings in WP_Http::request() include a 'cookies' element,
            // so that element should always be present in $args.
            throw new Exception("Unexpected: \$args does not contain an element name 'cookies'");
        }
        if (! is_array($args['cookies'])) {
            // The default value of $args['cookies'] is an empty array, not null
            throw new Exception("Unexpected: \$args['cookies'] is not an array");
        }

        // In known example of self-directed requests for database update, $args['cookies']
        // will be empty. But for generality, instead of assigning it a value, just merge in
        // the $_COOKIE superglobal, letting any user-specified cookies in $args['cookies'] override.
        $retval['cookies'] = array_merge($_COOKIE, $args['cookies']);

        // PHP keeps the session file locked, so we need to close it to allow the request to use it
        if (session_status() == PHP_SESSION_ACTIVE) {
            session_write_close();
        }
    }
    return $retval;
 }