metadataUrl = $metadataUrl; $this->enableAutomaticChecking = $enableAutomaticChecking; $this->theme = $theme; $this->optionName = 'external_theme_updates-'.$this->theme; $this->installHooks(); } /** * Install the hooks required to run periodic update checks and inject update info * into WP data structures. * * @return void */ public function installHooks(){ //Check for updates when WordPress does. We can detect when that happens by tracking //updates to the "update_themes" transient, which only happen in wp_update_themes(). if ( $this->enableAutomaticChecking ){ add_filter('pre_set_site_transient_update_themes', array($this, 'onTransientUpdate')); } //Insert our update info into the update list maintained by WP. add_filter('site_transient_update_themes', array($this,'injectUpdate')); //Delete our update info when WP deletes its own. //This usually happens when a theme is installed, removed or upgraded. add_action('delete_site_transient_update_themes', array($this, 'deleteStoredData')); } /** * Retrieve update info from the configured metadata URL. * * Returns either an instance of ThemeUpdate, or NULL if there is * no newer version available or if there's an error. * * @uses wp_remote_get() * * @param array $queryArgs Additional query arguments to append to the request. Optional. * @return ThemeUpdate */ public function requestUpdate($queryArgs = array()){ //Query args to append to the URL. Themes can add their own by using a filter callback (see addQueryArgFilter()). $queryArgs['installed_version'] = $this->getInstalledVersion(); $queryArgs = apply_filters(self::$filterPrefix.'query_args-'.$this->theme, $queryArgs); //Various options for the wp_remote_get() call. Themes can filter these, too. $options = array( 'timeout' => 10, //seconds ); $options = apply_filters(self::$filterPrefix.'options-'.$this->theme, $options); $url = $this->metadataUrl; if ( !empty($queryArgs) ){ $url = add_query_arg($queryArgs, $url); } //Send the request. $result = wp_remote_get($url, $options); //Try to parse the response $themeUpdate = null; $code = wp_remote_retrieve_response_code($result); $body = wp_remote_retrieve_body($result); if ( ($code == 200) && !empty($body) ){ $themeUpdate = ThemeUpdate::fromJson($body); //The update should be newer than the currently installed version. if ( ($themeUpdate != null) && version_compare($themeUpdate->version, $this->getInstalledVersion(), '<=') ){ $themeUpdate = null; } } $themeUpdate = apply_filters(self::$filterPrefix.'result-'.$this->theme, $themeUpdate, $result); return $themeUpdate; } /** * Get the currently installed version of our theme. * * @return string Version number. */ public function getInstalledVersion(){ if ( function_exists('wp_get_theme') ) { $theme = wp_get_theme($this->theme); return $theme->get('Version'); } /** @noinspection PhpDeprecationInspection get_themes() used for compatibility with WP 3.3 and below. */ foreach(get_themes() as $theme){ if ( $theme['Stylesheet'] === $this->theme ){ return $theme['Version']; } } return ''; } /** * Check for theme updates. * * @return void */ public function checkForUpdates(){ $state = get_option($this->optionName); if ( empty($state) ){ $state = new StdClass; $state->lastCheck = 0; $state->checkedVersion = ''; $state->update = null; } $state->lastCheck = time(); $state->checkedVersion = $this->getInstalledVersion(); update_option($this->optionName, $state); //Save before checking in case something goes wrong $state->update = $this->requestUpdate(); update_option($this->optionName, $state); } /** * Run the automatic update check, but no more than once per page load. * This is a callback for WP hooks. Do not call it directly. * * @param mixed $value * @return mixed */ public function onTransientUpdate($value){ if ( !$this->automaticCheckDone ){ $this->checkForUpdates(); $this->automaticCheckDone = true; } return $value; } /** * Insert the latest update (if any) into the update list maintained by WP. * * @param StdClass $updates Update list. * @return array Modified update list. */ public function injectUpdate($updates){ $state = get_option($this->optionName); //Is there an update to insert? if ( !empty($state) && isset($state->update) && !empty($state->update) ){ $updates->response[$this->theme] = $state->update->toWpFormat(); } return $updates; } /** * Delete any stored book-keeping data. * * @return void */ public function deleteStoredData(){ delete_option($this->optionName); } /** * Register a callback for filtering query arguments. * * The callback function should take one argument - an associative array of query arguments. * It should return a modified array of query arguments. * * @param callable $callback * @return void */ public function addQueryArgFilter($callback){ add_filter(self::$filterPrefix.'query_args-'.$this->theme, $callback); } /** * Register a callback for filtering arguments passed to wp_remote_get(). * * The callback function should take one argument - an associative array of arguments - * and return a modified array or arguments. See the WP documentation on wp_remote_get() * for details on what arguments are available and how they work. * * @param callable $callback * @return void */ public function addHttpRequestArgFilter($callback){ add_filter(self::$filterPrefix.'options-'.$this->theme, $callback); } /** * Register a callback for filtering the theme info retrieved from the external API. * * The callback function should take two arguments. If a theme update was retrieved * successfully, the first argument passed will be an instance of ThemeUpdate. Otherwise, * it will be NULL. The second argument will be the corresponding return value of * wp_remote_get (see WP docs for details). * * The callback function should return a new or modified instance of ThemeUpdate or NULL. * * @param callable $callback * @return void */ public function addResultFilter($callback){ add_filter(self::$filterPrefix.'result-'.$this->theme, $callback, 10, 2); } } endif; if ( !class_exists('ThemeUpdate') ): /** * A simple container class for holding information about an available update. * * @author Janis Elsts * @copyright 2012 * @version 1.0 * @access public */ class ThemeUpdate { public $version; //Version number. public $details_url; //The URL where the user can learn more about this version. public $download_url; //The download URL for this version of the theme. Optional. /** * Create a new instance of ThemeUpdate from its JSON-encoded representation. * * @param string $json Valid JSON string representing a theme information object. * @return ThemeUpdate New instance of ThemeUpdate, or NULL on error. */ public static function fromJson($json){ $apiResponse = json_decode($json); if ( empty($apiResponse) || !is_object($apiResponse) ){ return null; } //Very, very basic validation. $valid = isset($apiResponse->version) && !empty($apiResponse->version) && isset($apiResponse->details_url) && !empty($apiResponse->details_url); if ( !$valid ){ return null; } $update = new self(); foreach(get_object_vars($apiResponse) as $key => $value){ $update->$key = $value; } return $update; } /** * Transform the update into the format expected by the WordPress core. * * @return array */ public function toWpFormat(){ $update = array( 'new_version' => $this->version, 'url' => $this->details_url, ); if ( !empty($this->download_url) ){ $update['package'] = $this->download_url; } return $update; } } endif;