<?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;

