304 lines
9.0 KiB
PHP
304 lines
9.0 KiB
PHP
<?php
|
|
/**
|
|
* Theme Update Checker Library 1.2
|
|
* http://w-shadow.com/
|
|
*
|
|
* Copyright 2012 Janis Elsts
|
|
* Licensed under the GNU GPL license.
|
|
* http://www.gnu.org/licenses/gpl.html
|
|
*/
|
|
|
|
if ( !class_exists('ThemeUpdateChecker') ):
|
|
|
|
/**
|
|
* A custom theme update checker.
|
|
*
|
|
* @author Janis Elsts
|
|
* @copyright 2012
|
|
* @version 1.2
|
|
* @access public
|
|
*/
|
|
class ThemeUpdateChecker {
|
|
public $theme = ''; //The theme associated with this update checker instance.
|
|
public $metadataUrl = ''; //The URL of the theme's metadata file.
|
|
public $enableAutomaticChecking = true; //Enable/disable automatic update checks.
|
|
|
|
protected $optionName = ''; //Where to store update info.
|
|
protected $automaticCheckDone = false;
|
|
protected static $filterPrefix = 'tuc_request_update_';
|
|
|
|
/**
|
|
* Class constructor.
|
|
*
|
|
* @param string $theme Theme slug, e.g. "twentyten".
|
|
* @param string $metadataUrl The URL of the theme metadata file.
|
|
* @param boolean $enableAutomaticChecking Enable/disable automatic update checking. If set to FALSE, you'll need to explicitly call checkForUpdates() to, err, check for updates.
|
|
*/
|
|
public function __construct($theme, $metadataUrl, $enableAutomaticChecking = true){
|
|
$this->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;
|