Why X Theme and Pro sometimes cause PHP warnings on WPEngine

The Problem

Earlier this fall I was working on an e-commerce site that’s hosted on WPEngine that used Themeco’s Pro as its parent theme most pages hadn’t seen any customization yet, most of the code was stock, and this appeared on the Products page:

PHP Warning:  Invalid argument supplied for foreach() in wp-content/mu-plugins/wpengine-common/plugin.php

Hmm. Who is passing what to a foreach?

It turns out that both X Theme and it’s progeny Pro do this turn off WordPress’s automatic responsive images:

// Disable WordPress 4.4 Responsive Images
// =============================================================================

if ( ! function_exists( 'x_disable_wp_image_srcset' ) ) :
  function x_disable_wp_image_srcset( $source ) {
    return false;
  }
  add_filter( 'wp_calculate_image_srcset', 'x_disable_wp_image_srcset' );
endif;

They’re hooking into the wp_calculate_image_srcset filter and returning false, which stops WordPress from adding a srcset attribute to an image, (more on why in a moment). The problem here is that the value being passed to the filter, $sources, is an array, but x_disable_wp_image_srcset() turns that array into a boolean false, so then anything else hooking into wp_calculate_image_srcset, for example, WPEngine’s CDN system, tries to iterate over the $sources array, but it’s not an array, hence the error. By filtering $sources to a boolean false Themeco is breaking the expected, and documented, behaviour of the wp_calculate_image_srcset filter.

Solutions

Luckily, there are two easy fixes for this problem. The first is to make sure you’re using the latest version of WPEngine’s must-use plugin. They’ve caught on to the problem and check to make sure $sources is actually an array:

if ( is_array( $sources ) ) {
    foreach ( $sources as $source ) {
        // do the stuff
    }
}

If you can’t do that, at least Themeco made the function pluggable, so you can add your own version of x_disable_wp_image_srcset() to your theme’s functions.php or a plugin that returns an empty array and plays nicely with other plugins:

// This is a pluggable function. If we don't define it here then Pro does in 
// pro/framework/functions/global/admin/thumbnails/setup.php
function x_disable_wp_image_srcset( $sources, $size_array, $image_src, $image_meta, $attachment_id ) {
    return [];
}

Or, you could use responsive images.

Confusion, (aka, why this happens in the first place)

When I first discovered this problem I E-mailed Themeco to tell them about the problem, however they pointed me at the documentation for the wp_calculate_image_srcset() function, which states the function can return false, and sent me on my way:

Hey John,

Thanks for writing in. In looking over WordPress' official documentation for that function/hook, I believe that boolean false should be the correct value to return:

https://developer.wordpress.org/reference/functions/wp_calculate_image_srcset/

In the "Return" section you'll see that it is supposed to return a string or a boolean false if there is an "error or only one source exists." It seems as though WPEngine should need to revise their function to account for these instances anyway even if not for X or Pro (and at least work to catch any fatal errors like this).

Hopefully that helps, cheers.


Best Regards,
Your Themeco Team

This confusion occurs because the convention in WordPress is that if a filter and a function have the same name, the filter filters the output of the function. However, in this case the convention is broken – when I raised the issue in the Advanced WordPress Facebook group a very experienced WP developer made the exact same mistake and assumed that allowable return types for the function should be fine to return from the filter. After we cleared up the confusion I filed a bug about the confusing names, go star it so it gets some attention!

A Fluke of PHP

So why does returning false in the wp_calculate_image_srcset filter stop WordPress from using responsive images? Here’s the relevant code from media.php:

$sources = apply_filters( 'wp_calculate_image_srcset', $sources, $size_array, $image_src, $image_meta, $attachment_id );

// Only return a 'srcset' value if there is more than one source.
if ( ! $src_matched || count( $sources ) < 2 ) {
    return false;
}

To follow along, if $sources is turned into a boolean false at the apply_filters() call, then a count( false ) happens in the condition on the next non-comment line. Instead of an error like I expected, running count() on any non-array value returns 1, so count( false ) == 1, also count( 0 ) == 1

Conclusion

First, be a good citizen of the WordPress ecosystem and don’t change the type of a filtered value to something other than the documented types for that value. Second, if you have any non-core themes or plugins installed you can’t trust that a value coming into a filter will be the type that the docs say, so check! When WPEngine realized there was a problem an is_array() fixed the problem. Third, read support E-mails thoroughly and make sure you understand them, it hurts when I go to the effort to document a problem and come up with a solution, and I’m told it’s not a problem. Finally, go +1 my ticket so maybe others won’t get bitten.

Really Purging a YouTube Iframe in Internet Explorer

I’ve been working on a website where we display YouTube videos in a modal overlay. This seems simple: absolutely position a div and centre YouTube’s iframe embed within it. When the user tires of the video remove the iframe and hide or remove the div. It works great, then I fired up Internet Explorer for testing.

In IE this is what appears in the Javascript console,  after the iframe is removed:

Errors in the IE Javascript Error Console.
These errors appear after html5player.js shouldn’t exist anymore.

With the iframe gone html5player.js and its code shouldn’t  exist anymore and therefore should not throw errors. It turns out that when an iframe is removed from a web page IE doesn’t completely purge the contents of the iframe from memory right away, if ever.

How do we fix it?

The errors occurred when using jQuery to set the innerHTML of the containing div to an empty string, so I started trying to explicitly remove the iframe’s DOM node using pure Javascript. Element.removeChild() the node instead of destroying it, so that didn’t kill the errors. The docs for IE’s removeNode() don’t specify if the node is held in memory, but the errors continued with it as well.

Some Googling turned up, (I forget where), that setting the iframe’s src attribute to an empty string might help. This makes sense because it’s the equivalent of navigating the iframe to a new page, which should unload any Javascript. Trying to set src=” worked, most of the time. When it didn’t work IE went crazy and became unusable.

In the end I took the approach of trying to wipe out the contents of the iframe as thoroughly as possible, first by changing the iframe’s src to an empty string, then by accessing the iframe’s DOM with Javascript and setting its document element to null. This did the trick. Here’s the function:

// note: modal is the div that contains the YouTube iframe
// jquery is used for some selecting, but the guts of the removal are pure Javascript

function purgeVideo() {
    // purge video must be called while the video is still displayed on the page, 
    // so doing a jQuery fadeOut(purgeVideo) may throw errors, (I think because 
    // fadeOut sets display:none before calling its callback).

    if( modal ) {
        // get the iframe, declare variables
        var ifr = $('iframe', modal), rifr;
        
        // if the iframe is found get the underlying DOM node from jQuery
        if ( ifr.length > 0 ) {
            rifr = ifr.get(0);
        }

        // make extra sure the DOM node exists before starting to work on it.
        if( rifr ) {
            // set src to '' to start removing YouTube and get around cross-domain access restrictions
            rifr.src = '';

            // now set the document element of the iframe to null, (I think this is IE-specific)
            if ( rifr.documentWindow ) {
                rifr.documentWindow.document = null;
                rifr.documentWindow = null;
            }

            // delete the iframe, (also removes it from DOM).
            delete rifr;
        }

        // this won't throw an error, even if the iframe is gone, (we're back to jQuery here).
        ifr.remove();

        // remove anything else, (titles, etc), from the modal overlay
        modal.html('');
    }

}

Adding to a WordPress page’s URL without changing the URL of its Child Pages.

I was recently asked to add an extra path component to the URL of a WordPress page for SEO reasons. It took some diving into the internals of WordPress. Here’s the setup:

  • WordPress is installed in the site root, at example.com
  • We are using a static homepage
  • The company blog is at example.com/blog/
  • I was asked to make the blog URL be example.com/blog/some-extra-keywords/ without changing the URL of the current and future blog posts, which are currently example.com/blog/post-name/

There are 3 things required to make this work:

  1. A filter to tell WordPress to process requests for /blog/some-extra-keywords/ as if they are for /blog/
  2. A filter to change permalinks from /blog/ to /blog/some-extra-keywords/ (Optional, but recommended).
  3. A redirect from /blog/ to /blog/some-extra-keywords/ (Also optional, but also recommended).

Treat /blog/some-extra-keywords/ requests like /blog/

We don’t want to change the URL of the blog page in WordPress’s Edit Page screen because that will change the URL of past and future all blog posts. We need to tell WordPress that a request for /blog/some-extra-keywords/ is a request for /blog/. This is done by adding a filter to WordPress’s request filter hook:

/**
 * Make the URL /blog/some-extra-keywords/ respond as /blog/ 
 * would.
 * @param  array $request The request array that WP generates.
 * @return array          The modified request array.
 */
function jb_filter_blog_url_request( $request ) {

    if ( isset( $request['name'] ) && 'some-extra-keywords' == $request['name'] ) {
                // it's a page we're requesting. If you're doing this to something other
                // than a page, try setting $request['name'], and don't unset it 2 lines
                // later
        $request['pagename'] = 'blog';
        unset( $request['name'] );
    }

    return $request;
}
add_filter( 'request', 'jb_filter_blog_url_request' );

Since WordPress identifies pieces of content by the last part of the URL, (the “name”), that’s all we have to change. In this case, because the blog is on a page, we had to set $request['pagename'].

Note: This is optional, but recommended. If you don’t do this you must do the next item, (redirect /blog/ to /blog/some-extra-keywords/), but you get the most SEO benefit if you do both.

With #1 it’s time to change links to the blog page to point to the new URL. The old URL still works, and you’ll be redirecting it in Step 3, but modifying the links cuts out a round-trip to the server, making your site faster, and it means that anytime a spider crawls the site it will find the correct URL.

Again, we create a filter function and hook it up. Because I’m doing this all to a page, I’m hooking on to the page_link filter, but the function also works with the post_link filter. In fact, I originally used the post_link filter, which doesn’t work for pages, and spent a while banging my head against a wall trying to figure out why it didn’t work.

By hooking into page_link or post_link you’ll modify the URL everywhere it is generated with get_permalink(), which is almost everywhere. Menus, lists of pages, and even the XML Sitemap that Yoast’s WordPress SEO plugin makes will be have the new URL. If you have typed the URL somewhere this filter will not change it, but that’s why you’ll set up a redirect in Step 3.

The code:

/**
 * Modifies the blog URL when it's requested using get_permalink(). 
 * 
 * Note that this filter is set up to work on both posts & pages, and the 
 * post_link and page_link filters don't pass quite the same parameters:
 * - $post is an post object for post_link and a post ID for page_link, so we
 *   get the post object if $post is numeric
 * - $dontuse is completely different between the two filters, but not needed,
 *   so we ignore it.
 * 
 * @param  string $url       The URL to be filtered.
 * @param  mixed  $post      The post ID or post object that corresponds to $url
 * @param  mixed  $leavename Differs depending on filter. Ignore.
 * @return string            The possibly-modified URL.
 */
function jb_modify_blog_url( $url, $post, $leavename ) {
    $true = false;
 
        // don't do it in the admin, I'm afraid the modified URL will get
        // added to the URL slug field on the Edit Page screen, and get
        // permanently added, with another copy of it being added every time
        // the page is saved.
    if( ! is_admin() ) {

        if ( is_numeric( $post ) ) {
            $post = get_post( $post );
        }


        if( 'blog' == $post->post_name ) {
            $url .= 'some-extra-keywords/';
        }
    }
    return $url;
}
add_filter( 'page_link', 'jb_modify_blog_url', 10, 3 );

The magic is in the third if condition: If the page name is ‘blog’ add the extra keywords to the URL.

Redirect from /blog/ to /blog/some-extra-keywords/

Note: This is optional, as well, but if you don’t do it the blog homepage will be available at both /blog and /blog/some-extra-keywords/ which could lead to a duplicate content penalty from Google.

We’re going to do a 301 Redirect to tell search engines that the blog is now at /blog/some-extra-keywords/. I’m going to do this in a .htaccess file because that way the web server doesn’t have to start PHP or WordPress.

If you’ve already got a rewrite section in your .htaccess file, add the following line to it:

RewriteRule ^blog/?$ /blog/some-extra-keywords/ [R=301,L]

This will redirect both /blog/ and /blog, (the first ? makes the slash right before it optional), to /blog/some-extra-keywords/. If you don’t have a rewrite section, add one:

<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /

RewriteRule ^blog/?$ /blog/whole-house-fan-energy-saving-tips/ [R=301,L]

</IfModule>

And that’s it.

Conditionally Loading Javascript & CSS with Fancybox for WordPress

The Problem

I’ve been working on improving the performance of a client’s WordPress-based website recently and it’s become very clear to me just how much CSS & Javascript plugins load, even when it’s not needed.

On this particular website 47% of the Javascript and 57% of the CSS loaded on the site’s homepage is not required on the homepage, but is required elsewhere, so it should be loaded conditionally. Sometimes this is easy, but sometimes it’s not.

Fancybox for WordPress is not an easy plugin to make load client-side resources, after all, it’s made to show a lightbox on any image, and especially on any gallery. When the wp_enqueue_scripts action happens, where it is recommended that we set the scripts and styles that will be used on a page, we don’t yet know what will be on the page. Fortunately, we can call the wp_enqueue_script() function after that, during the body of the page, and those scripts will be printed in the wp_footer() function. The same is true for wp_enqueue_style(). So, here’s what we do:

  1. Stop Fancybox for WordPress from including its scripts & styles by default.
  2. If a gallery or image that we want to use Fancybox on is displayed, we tell WordPress to display the Fancybox scripts & styles in the footer.
  3. Enjoy faster pageloads.

Sounds simple, but there are a few interesting bits. Now for the code.

The Code

I use three functions to check if the Fancybox scripts & styles are needed. They can be in a theme’s functions.php, (where I have them at the moment), or they could even be rolled into their own plugin.

First tell Fancybox not to include its code by default. Instead of manually dequeuing every style & script, remove the Fancybox functions that do the enqueuing from the wp_enqueue_scripts and wp_head action hooks:

// don't enqueue Fancybox scripts by default.
function jb_dequeue_fancybox_css_js() {
    remove_action( 'wp_enqueue_scripts', 'mfbfw_styles' );
    remove_action( 'wp_enqueue_scripts', 'mfbfw_scripts' );
    remove_action( 'wp_head', 'mfbfw_init' );
}
add_action( 'wp', 'jb_dequeue_fancybox_css_js' );

Next create a function that re-enables Fancybox, and remembers that it did so. Here there are two static variables, which will hold their value between function calls, (kind of like hidden globals), which store state. The $fancybox_is_used variable is returned so this function can be called with no arguments to find out if Fancybox has been used on page. Note the priority of 25 when hooking mfbfw_init() to the wp_footer action. This is needed because mfbfw_init() prints some Javascript to the page that relies on jQuery, and WordPress prints the enqueued scripts in the wp_footer action with a priority of 20, so mfbfw_init() needs to execute with a priority higher than 20.

function jb_fancybox_is_used( $used = false ) {

    // this is returned so we can call this function with no arguments to learn
    // if Fancybox has been used on a particular page.
    static $fancybox_is_used = false;

    // remember if Fancybox has been re-enabled already, so we don't enqueue the 
    // scripts multiple times
    static $fancybox_is_setup = false;

    if( $used === true ) {
        $fancybox_is_used = true;
    }

    if( $fancybox_is_used && ! $fancybox_is_setup ) {
        if( function_exists( 'mfbfw_styles' ) ) {
            mfbfw_styles();  // enqueue fancybox styles
            mfbfw_scripts(); // enqueue fancybox scripts
            // the 25 is important. WordPress prints footer scripts in the
            // wp_footer action with a priority of 20, and mfbfw_init() has to
            // be called after the footer scripts are already on the page.
            add_action( 'wp_footer', 'mfbfw_init', 25 ); 
        }
        $fancybox_is_setup = true;
    }

    return $fancybox_is_used;
}

Finally, make a function that looks for places where Fancybox is used on the page. On the site I’m working on the CSS class fancybox is used on images and in galleries that I want to be Fancyboxed, so I look for the string “fancybox” in the_content filter, and when it’s found I call jb_fancybox_is_used( true ) to re-enable Fancybox on the page. I added this to the_content with priority 11 because shortcodes, including gallery shortcodes, are executed at priority 10, and I want to be able to look through the output of short codes for the fancybox CSS class:

function jb_hunt_for_fancybox( $content ) {

    if( false !== stripos( $content, 'fancybox') || false !== stripos( $content, 'thickbox' ) ) {
        jb_fancybox_is_used( true );
    }
    return $content;
}
add_filter( 'the_content', 'jb_hunt_for_fancybox', 11 );

If you include a something you want to fancybox in a template you can call jb_fancybox_is_used( true ) manually from the template file to include the CSS & Javascript.

Other Ways

This isn’t the only way to conditionally include Fancybox’s Javascript & CSS. Instead of using jb_hunt_for_fancybox() to filter the_content there’s probably an action or filter in the gallery shortcode that jb_fancybox_is_used() could be hooked onto. It may even be possible to use the $wp_query object in an action hook just before wp_enqueue_scripts to determine if there is content on the page that needs to be Fancyboxed, let that decide whether or not to run jb_dequeue_fancybox_css_js(), and forget about the other two functions.

Let’s Do Better

Plugin authors should be working hard to only add what is needed to each page load. Who is doing a great job? How can we hack our themes to bend other plugins to our will? Comment or tweet @johnbeales to let me know.

Announcing DeadTrees

Today I’m releasing DeadTrees, a WordPress plugin to share the books you read. Get it from wordpress.org or search for DeadTrees in the Plugins > Add section of your WordPress admin.

Features

DeadTrees lets you post the books you read, with or without writing about them, (really, does the internet need to know what you thought of the last mystery you read?). It generates Amazon affiliate links to those books so you, (or I), can make a little money if your readers buy the books, and it auto-fetches the books’ cover art from Amazon so things look cool.

Why?

I have been posting about books that I read for a while now, but ground to a halt when I got lazy & didn’t want to write a whole post about each book, and realized often it doesn’t matter what I think about a book. However, I did want to keep posting at least the te title & author of each book I read, (and so my sister can check to see what I’ve read before giving me a book).

Why write a plugin when there are other plugins to share the books I read? Because the other plugins didn’t do it how I wanted them to. I couldn’t find another plugin that uses WordPress’s Custom Post Types to store books I’ve read, and books are such a perfect use of CPTs that they’re even used as the example in the WordPress documentation!

Support & All That

I’ve put DeadTrees up at GitHub, if you have issues try to submit them there. My contact page is also always available to reach me.

See It Live

DeadTrees is up & running here. Take a look at the books I’ve read.