Automatic updates for WordPress themes which are not in the theme directory

You can install automatic updates for themes from the WordPress.org directory. Here I show you how you can provide these automatic updates for themes, which are not in the directory.

Update from February 9, 2017: This solution does not work for multisite installations.

I did not want to dive into the subject of theme updates. My search for a solution for my shop guided me to the plugin WooCommerce API Manager, which I used for a short while. From the beginning, I did not like the idea, that the users have to create an account on my site for getting automatic updates. After stumbling over a few problems with child themes and update notifications which appear twice, I searched for the reason in the plugin’s code and WordPress core.

After some time I have not fixed all problems but understood how theme updates are working. So I created my own solution because I did not have many requirements.

Update routine requirements

I do not need to check in how many installations a bought theme is active because I do not limit the downloads. The following points should be fulfilled:

  • The solution works with the default download link for digital products from WooCommerce, which is sent to the customer.
  • To activate the automatic updates, the customer inserts the link into a field in the customizer.
  • The updates have to work with an active child theme of the paid theme.
  • Switching to a theme which is neither the paid theme nor its child theme removes the download link.

Before we continue with the solution, here comes a summary of how theme updates work in WordPress.

Procedure of theme updates in WordPress

It is important for us that there is a transient update_themes which stores information about the installed themes and available updates. The transient’s content can look something like that when all themes are up to date:

object(stdClass)#273 (4) { ["last_checked"]=>int(1485868202) ["checked"]=>array(7) { ["extant"]=>string(5) "1.0.1" ["rindby"]=>string(5) "1.1.3" ["schlicht-child/schlicht-child"]=>string(3) "1.0" ["schlicht"]=>string(5) "1.0.4" ["twentyfifteen"]=>string(3) "1.7" ["twentyfourteen"]=>string(3) "1.9" ["twentysixteen"]=>string(3) "1.3" } ["response"]=>array(0) { } ["translations"]=>array(0) { } }
Code language: PHP (php)

If we decrease the version of Extant manually, we get the following output (and an update notification in the backend):

object(stdClass)#273 (4) { ["last_checked"]=>int(1485868693) ["checked"]=>array(7) { ["extant"]=>string(3) "1.0" ["rindby"]=>string(5) "1.1.3" ["schlicht-child/schlicht-child"]=>string(3) "1.0" ["schlicht"]=>string(5) "1.0.4" ["twentyfifteen"]=>string(3) "1.7" ["twentyfourteen"]=>string(3) "1.9" ["twentysixteen"]=>string(3) "1.3" } ["response"]=>array(1) { ["extant"]=>array(4) { ["theme"]=>string(6) "extant" ["new_version"]=>string(5) "1.0.1" ["url"]=>string(36) "https://wordpress.org/themes/extant/" ["package"]=>string(54) "https://downloads.wordpress.org/theme/extant.1.0.1.zip" } } ["translations"]=>array(0) { } }
Code language: PHP (php)

The checked version of extant now is 1.0 – the theme directory’s version is 1.0.1. Interesting is the response part, which stores information about the new version. There is an array with the theme slug as the key. Inside this array, there is another array with the following entries:

  • theme contains the theme slug again. As far as I can say, this is optional.
  • new_version contains the new version number.
  • url is a URL, which is displayed in the overlay after clicking View version 1.0.1 details (optional).
  • package is the URL of the ZIP which contains the latest theme version (optional). If this is not set, the user will get an update notification without the update possibility.

To notify WordPress about a theme update, you only have to insert the right information into the response array. Everything else is handled by the theme update routine of WordPress core. To modify the transient, there is the filter pre_set_site_transient_update_themes, and with that we are ready for the implementation.

Implementation of the update script

Our solution requires the following:

  • A public page with metadata of the latest theme version, so the update script can check if a new version is available. It is necessary to specify at least new_version.
  • Customizer option for the download link used for package.
  • A function which is hooked to pre_set_site_transient_update_themes which checks for updates and modifies the transient, to kick off the update routine of WordPress.
  • A function for the switch_theme hook to remove the value of the customizer option, if neither the paid theme nor its child theme is active.

Creating metadata page

It does not matter, how this metadata page is created. You can just upload a static JSON file to your server and update it after releasing a new version. On my site, I have a custom post type for displaying the WordPress themes and their changelogs. The changelog is visible via appending a changelog/ to the single view of a theme (for example the changelog of Schlicht).

So I can get the current version number from this changelog implementation, and created a second sub-page for each theme: upgrade-json/. This is the output of Schlicht’s metadata page:

{ "new_version": "1.0.4", "url": "https://florianbrinkmann.com/en/wordpress-themes/schlicht/changelog/", "theme_id": 2936 }
Code language: JSON / JSON with Comments (json)

theme_id is the ID of the WooCommerce product, which will later be used to check if the URL is a URL to a Schlicht ZIP as good as possible. The update would work with every URL to a valid theme, but the paid theme would be overwritten by the other theme.

This is the function for outputting the JSON code:

function fbn_theme_upgrade_json() { $theme_id = get_field( 'woocommerce-product' ); if ( have_rows( 'changelog' ) ) { while ( have_rows( 'changelog' ) ) { the_row(); $version = get_sub_field( 'version' ); $changelog_url = get_the_permalink() . 'changelog/'; $version_data = array( 'new_version' => $version, 'url' => $changelog_url, 'theme_id' => $theme_id, ); wp_send_json( $version_data ); } } }
Code language: PHP (php)

wp_send_json() creates the JSON output. All other tasks have to be done in the theme which should get updates.

Customizer option for the update URL

Let us create the customizer option for the update URL first. We insert it into the section schlicht_options, which is already created by the theme:

/** * Customizer settings for theme update * * @param WP_Customize_Manager $wp_customize The Customizer object. */ function schlicht_update_customize_register( $wp_customize ) { $wp_customize->add_setting( 'schlicht_upgrade_url', array( 'type' => 'option', 'default' => '', 'sanitize_callback' => 'schlicht_esc_update_url' ) ); $wp_customize->add_control( 'schlicht_upgrade_url', array( 'priority' => 1, 'type' => 'url', 'section' => 'schlicht_options', 'label' => __( 'Paste your download link for »Schlicht« to enable automatic theme updates.', 'schlicht' ), ) ); } add_action( 'customize_register', 'schlicht_update_customize_register', 12 );
Code language: PHP (php)

Important to note is that type in the add_setting() method is option, so this option is not theme specific, and the updates will also work if a child theme of the paid theme is active – no matter if the URL was inserted with active parent or child theme. We could use esc_url_raw as sanitize_callback, but I would like to check if the URL starts with https://florianbrinkmann.com/en/?download_file=. That is the beginning of a download URL created by WooCommerce.

The sanitize function looks like that:

/** * Escape URL and check if it matches format https://florianbrinkmann.com/en/?download_file= * * @param $url * * @return string */ function schlicht_esc_update_url( $url ) { $url = esc_url_raw( $url ); $pattern = '|^(https://florianbrinkmann.com/en/?download_file=)|'; preg_match( $pattern, $url, $matches ); if ( ! empty ( $matches ) ) { return $url; } else { return ''; } }
Code language: PHP (php)

First, we run esc_url_raw(), and after that, we check if the mentioned pattern matches the URL. If so, we return the URL, otherwise an empty string.

Checking for new updates and modifying the transient

Now the interesting part. Parts of the following code are inspired by the WooCommerce API Manager plugin:

/** * Checking for updates and updating the transient for theme updates * * @param $transient * * @return mixed */ function schlicht_theme_update( $transient ) { if ( empty( $transient->checked ) ) { return $transient; } $request = schlicht_fetch_data_of_latest_version(); if ( is_wp_error( $request ) || wp_remote_retrieve_response_code( $request ) != 200 ) { return $transient; } else { $response = wp_remote_retrieve_body( $request ); } $data = json_decode( $response ); $theme_id = $data->theme_id; unset( $data->theme_id ); if ( version_compare( $transient->checked['schlicht'], $data->new_version, '<' ) ) { $transient->response['schlicht'] = (array) $data; $theme_package = get_option( 'schlicht_upgrade_url' ); if ( ! empty ( $theme_package ) ) { $pattern = '|^(https://florianbrinkmann.com/en/?download_file=' . $theme_id . ')|'; preg_match( $pattern, $theme_package, $matches ); if ( ! empty ( $matches ) ) { $transient->response['schlicht']['package'] = $theme_package; } else { } } } return $transient; } add_filter( 'pre_set_site_transient_update_themes', 'schlicht_theme_update' );
Code language: PHP (php)

After checking if the checked entry is empty, we store the result of a wp_safe_remote_get() call in $request. which is made in schlicht_fetch_data_of_latest_version(). This is the schlicht_fetch_data_of_latest_version():

/** * Fetch data of latest theme version * * @return array|WP_Error */ function schlicht_fetch_data_of_latest_version() { $request = wp_safe_remote_get( 'https://florianbrinkmann.com/en/wordpress-themes/schlicht/upgrade-json/' ); return $request; }
Code language: PHP (php)

The result is the metadata we prepared above. If everything is all right with the result, we fetch the request’s body with the call of wp_remote_retrieve_body( $request ); and save it in $response. We make it an object with json_decode(), save the WooCommerce ID of the theme and remove the ID from the object.

We use version_compare() to check if the currently installed version is lower than the version of the fetched metadata. In this case, we save the metadata (new_version and url) as an array in the transient’s response part for the theme. We get the upgrade URL field’s value through get_option( 'schlicht_upgrade_url' ); and check it against a pattern again. This time, we append the ID of the theme, because the download URL of the WooCommerce product with the ID 123 would start with https://florianbrinkmann.com/en/?download_file=123. I do not check this in the customizer to reduce wp_safe_remote_get() calls.

If the pattern matches, the URL is stored as package, and we return the transient. If it does not match, the user will see an update notification without the possibility to upgrade.

Removing update URL after theme switch

We use the switch_theme action to remove the update URL. We can use wp_get_theme() to get an object of the new theme and check its template property. This is the same as the theme slug of the theme or the parent theme if a child theme is active. For the theme Schlicht, the value to check for is schlicht.

That is the function:

/** * Remove upgrade URL option after switching the theme, * if the new theme is not schlicht or a child theme of schlicht */ function schlicht_remove_upgrade_url() { $theme_object = wp_get_theme(); $template = $theme_object->template; if ( $template == 'schlicht' ) { } else { delete_option( 'schlicht_upgrade_url' ); } } add_action( 'switch_theme', 'schlicht_remove_upgrade_url', 10, 2 );
Code language: PHP (php)

If $theme_object->template has the value schlicht, either the theme Schlicht or one of its child themes is active. If that is not the case, we remove the customizer option’s value with delete_option( 'schlicht_upgrade_url' );.

That is it. We created an automatic update system for themes which are not located in the W.org directory.

Leave a Reply

Your email address will not be published. Required fields are marked *