and transforming various plugin metadata. * * @author Janis Elsts * @copyright 2015 * @version 2.3 * @access public */ class PluginInfo_2_3 { //Most fields map directly to the contents of the plugin's info.json file. //See the relevant docs for a description of their meaning. public $name; public $slug; public $version; public $homepage; public $sections; public $banners; public $download_url; public $author; public $author_homepage; public $requires; public $tested; public $upgrade_notice; public $rating; public $num_ratings; public $downloaded; public $active_installs; public $last_updated; public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything. public $filename; //Plugin filename relative to the plugins directory. /** * Create a new instance of PluginInfo from JSON-encoded plugin info * returned by an external update API. * * @param string $json Valid JSON string representing plugin info. * @param bool $triggerErrors * @return PluginInfo|null New instance of PluginInfo, or NULL on error. */ public static function fromJson($json, $triggerErrors = false){ /** @var StdClass $apiResponse */ $apiResponse = json_decode($json); if ( empty($apiResponse) || !is_object($apiResponse) ){ if ( $triggerErrors ) { trigger_error( "Failed to parse plugin metadata. Try validating your .json file with http://jsonlint.com/", E_USER_NOTICE ); } return null; } //Very, very basic validation. $valid = isset($apiResponse->name) && !empty($apiResponse->name) && isset($apiResponse->version) && !empty($apiResponse->version); if ( !$valid ){ if ( $triggerErrors ) { trigger_error( "The plugin metadata file does not contain the required 'name' and/or 'version' keys.", E_USER_NOTICE ); } return null; } $info = new self(); foreach(get_object_vars($apiResponse) as $key => $value){ $info->$key = $value; } return $info; } /** * Transform plugin info into the format used by the native WordPress.org API * * @return object */ public function toWpFormat(){ $info = new StdClass; //The custom update API is built so that many fields have the same name and format //as those returned by the native WordPress.org API. These can be assigned directly. $sameFormat = array( 'name', 'slug', 'version', 'requires', 'tested', 'rating', 'upgrade_notice', 'num_ratings', 'downloaded', 'active_installs', 'homepage', 'last_updated', ); foreach($sameFormat as $field){ if ( isset($this->$field) ) { $info->$field = $this->$field; } else { $info->$field = null; } } //Other fields need to be renamed and/or transformed. $info->download_link = $this->download_url; if ( !empty($this->author_homepage) ){ $info->author = sprintf('%s', $this->author_homepage, $this->author); } else { $info->author = $this->author; } if ( is_object($this->sections) ){ $info->sections = get_object_vars($this->sections); } elseif ( is_array($this->sections) ) { $info->sections = $this->sections; } else { $info->sections = array('description' => ''); } if ( !empty($this->banners) ) { //WP expects an array with two keys: "high" and "low". Both are optional. //Docs: https://wordpress.org/plugins/about/faq/#banners $info->banners = is_object($this->banners) ? get_object_vars($this->banners) : $this->banners; $info->banners = array_intersect_key($info->banners, array('high' => true, 'low' => true)); } return $info; } } endif; if ( !class_exists('PluginUpdate_2_3', false) ): /** * A simple container class for holding information about an available update. * * @author Janis Elsts * @copyright 2015 * @version 2.3 * @access public */ class PluginUpdate_2_3 { public $id = 0; public $slug; public $version; public $homepage; public $download_url; public $upgrade_notice; public $filename; //Plugin filename relative to the plugins directory. private static $fields = array('id', 'slug', 'version', 'homepage', 'download_url', 'upgrade_notice', 'filename'); /** * Create a new instance of PluginUpdate from its JSON-encoded representation. * * @param string $json * @param bool $triggerErrors * @return PluginUpdate|null */ public static function fromJson($json, $triggerErrors = false){ //Since update-related information is simply a subset of the full plugin info, //we can parse the update JSON as if it was a plugin info string, then copy over //the parts that we care about. $pluginInfo = PluginInfo_2_3::fromJson($json, $triggerErrors); if ( $pluginInfo != null ) { return self::fromPluginInfo($pluginInfo); } else { return null; } } /** * Create a new instance of PluginUpdate based on an instance of PluginInfo. * Basically, this just copies a subset of fields from one object to another. * * @param PluginInfo $info * @return PluginUpdate */ public static function fromPluginInfo($info){ return self::fromObject($info); } /** * Create a new instance of PluginUpdate by copying the necessary fields from * another object. * * @param StdClass|PluginInfo|PluginUpdate $object The source object. * @return PluginUpdate The new copy. */ public static function fromObject($object) { $update = new self(); $fields = self::$fields; if (!empty($object->slug)) $fields = apply_filters('puc_retain_fields-'.$object->slug, $fields); foreach($fields as $field){ if (property_exists($object, $field)) { $update->$field = $object->$field; } } return $update; } /** * Create an instance of StdClass that can later be converted back to * a PluginUpdate. Useful for serialization and caching, as it avoids * the "incomplete object" problem if the cached value is loaded before * this class. * * @return StdClass */ public function toStdClass() { $object = new StdClass(); $fields = self::$fields; if (!empty($this->slug)) $fields = apply_filters('puc_retain_fields-'.$this->slug, $fields); foreach($fields as $field){ if (property_exists($this, $field)) { $object->$field = $this->$field; } } return $object; } /** * Transform the update into the format used by WordPress native plugin API. * * @return object */ public function toWpFormat(){ $update = new StdClass; $update->id = $this->id; $update->slug = $this->slug; $update->new_version = $this->version; $update->url = $this->homepage; $update->package = $this->download_url; $update->plugin = $this->filename; if ( !empty($this->upgrade_notice) ){ $update->upgrade_notice = $this->upgrade_notice; } return $update; } } endif; if ( !class_exists('PucFactory', false) ): /** * A factory that builds instances of other classes from this library. * * When multiple versions of the same class have been loaded (e.g. PluginUpdateChecker 1.2 * and 1.3), this factory will always use the latest available version. Register class * versions by calling {@link PucFactory::addVersion()}. * * At the moment it can only build instances of the PluginUpdateChecker class. Other classes * are intended mainly for internal use and refer directly to specific implementations. If you * want to instantiate one of them anyway, you can use {@link PucFactory::getLatestClassVersion()} * to get the class name and then create it with new $class(...). */ class PucFactory { protected static $classVersions = array(); protected static $sorted = false; /** * Create a new instance of PluginUpdateChecker. * * @see PluginUpdateChecker::__construct() * * @param $metadataUrl * @param $pluginFile * @param string $slug * @param int $checkPeriod * @param string $optionName * @param string $muPluginFile * @return PluginUpdateChecker */ public static function buildUpdateChecker($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') { $class = self::getLatestClassVersion('PluginUpdateChecker'); return new $class($metadataUrl, $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile); } /** * Get the specific class name for the latest available version of a class. * * @param string $class * @return string|null */ public static function getLatestClassVersion($class) { if ( !self::$sorted ) { self::sortVersions(); } if ( isset(self::$classVersions[$class]) ) { return reset(self::$classVersions[$class]); } else { return null; } } /** * Sort available class versions in descending order (i.e. newest first). */ protected static function sortVersions() { foreach ( self::$classVersions as $class => $versions ) { uksort($versions, array(__CLASS__, 'compareVersions')); self::$classVersions[$class] = $versions; } self::$sorted = true; } protected static function compareVersions($a, $b) { return -version_compare($a, $b); } /** * Register a version of a class. * * @access private This method is only for internal use by the library. * * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'. * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'. * @param string $version Version number, e.g. '1.2'. */ public static function addVersion($generalClass, $versionedClass, $version) { if ( !isset(self::$classVersions[$generalClass]) ) { self::$classVersions[$generalClass] = array(); } self::$classVersions[$generalClass][$version] = $versionedClass; self::$sorted = false; } } endif; //Register classes defined in this file with the factory. PucFactory::addVersion('PluginUpdateChecker', 'PluginUpdateChecker_2_3', '2.3'); PucFactory::addVersion('PluginUpdate', 'PluginUpdate_2_3', '2.3'); PucFactory::addVersion('PluginInfo', 'PluginInfo_2_3', '2.3'); /** * Create non-versioned variants of the update checker classes. This allows for backwards * compatibility with versions that did not use a factory, and it simplifies doc-comments. */ if ( !class_exists('PluginUpdateChecker', false) ) { class PluginUpdateChecker extends PluginUpdateChecker_2_3 { } } if ( !class_exists('PluginUpdate', false) ) { class PluginUpdate extends PluginUpdate_2_3 {} } if ( !class_exists('PluginInfo', false) ) { class PluginInfo extends PluginInfo_2_3 {} }