_notice', 'filename');
/**
* Create a new instance of PluginUpdate from its JSON-encoded representation.
*
* @param string $json
* @return PluginUpdate_3_0|null
*/
public static function fromJson($json){
//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_3_0::fromJson($json);
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_3_0 $info
* @return PluginUpdate_3_0
*/
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_3_0|PluginUpdate_3_0 $object The source object.
* @return PluginUpdate_3_0 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('PucScheduler_3_0', false) ):
/**
* The scheduler decides when and how often to check for updates.
* It calls @see PluginUpdateChecker::checkForUpdates() to perform the actual checks.
*
* @version 3.0
*/
class PucScheduler_3_0 {
public $checkPeriod = 12; //How often to check for updates (in hours).
public $throttleRedundantChecks = false; //Check less often if we already know that an update is available.
public $throttledCheckPeriod = 72;
/**
* @var PluginUpdateChecker_3_0
*/
protected $updateChecker;
private $cronHook = null;
/**
* Scheduler constructor.
*
* @param PluginUpdateChecker_3_0 $updateChecker
* @param int $checkPeriod How often to check for updates (in hours).
*/
public function __construct($updateChecker, $checkPeriod) {
$this->updateChecker = $updateChecker;
$this->checkPeriod = $checkPeriod;
//Set up the periodic update checks
$this->cronHook = 'check_plugin_updates-' . $this->updateChecker->slug;
if ( $this->checkPeriod > 0 ){
//Trigger the check via Cron.
//Try to use one of the default schedules if possible as it's less likely to conflict
//with other plugins and their custom schedules.
$defaultSchedules = array(
1 => 'hourly',
12 => 'twicedaily',
24 => 'daily',
);
if ( array_key_exists($this->checkPeriod, $defaultSchedules) ) {
$scheduleName = $defaultSchedules[$this->checkPeriod];
} else {
//Use a custom cron schedule.
$scheduleName = 'every' . $this->checkPeriod . 'hours';
add_filter('cron_schedules', array($this, '_addCustomSchedule'));
}
if ( !wp_next_scheduled($this->cronHook) && !defined('WP_INSTALLING') ) {
wp_schedule_event(time(), $scheduleName, $this->cronHook);
}
add_action($this->cronHook, array($this, 'maybeCheckForUpdates'));
register_deactivation_hook($this->updateChecker->pluginFile, array($this, '_removeUpdaterCron'));
//In case Cron is disabled or unreliable, we also manually trigger
//the periodic checks while the user is browsing the Dashboard.
add_action( 'admin_init', array($this, 'maybeCheckForUpdates') );
//Like WordPress itself, we check more often on certain pages.
/** @see wp_update_plugins */
add_action('load-update-core.php', array($this, 'maybeCheckForUpdates'));
add_action('load-plugins.php', array($this, 'maybeCheckForUpdates'));
add_action('load-update.php', array($this, 'maybeCheckForUpdates'));
//This hook fires after a bulk update is complete.
add_action('upgrader_process_complete', array($this, 'maybeCheckForUpdates'), 11, 0);
} else {
//Periodic checks are disabled.
wp_clear_scheduled_hook($this->cronHook);
}
}
/**
* Check for updates if the configured check interval has already elapsed.
* Will use a shorter check interval on certain admin pages like "Dashboard -> Updates" or when doing cron.
*
* You can override the default behaviour by using the "puc_check_now-$slug" filter.
* The filter callback will be passed three parameters:
* - Current decision. TRUE = check updates now, FALSE = don't check now.
* - Last check time as a Unix timestamp.
* - Configured check period in hours.
* Return TRUE to check for updates immediately, or FALSE to cancel.
*
* This method is declared public because it's a hook callback. Calling it directly is not recommended.
*/
public function maybeCheckForUpdates(){
if ( empty($this->checkPeriod) ){
return;
}
$currentFilter = current_filter();
if ( in_array($currentFilter, array('load-update-core.php', 'upgrader_process_complete')) ) {
//Check more often when the user visits "Dashboard -> Updates" or does a bulk update.
$timeout = 60;
} else if ( in_array($currentFilter, array('load-plugins.php', 'load-update.php')) ) {
//Also check more often on the "Plugins" page and /wp-admin/update.php.
$timeout = 3600;
} else if ( $this->throttleRedundantChecks && ($this->updateChecker->getUpdate() !== null) ) {
//Check less frequently if it's already known that an update is available.
$timeout = $this->throttledCheckPeriod * 3600;
} else if ( defined('DOING_CRON') && constant('DOING_CRON') ) {
//WordPress cron schedules are not exact, so lets do an update check even
//if slightly less than $checkPeriod hours have elapsed since the last check.
$cronFuzziness = 20 * 60;
$timeout = $this->checkPeriod * 3600 - $cronFuzziness;
} else {
$timeout = $this->checkPeriod * 3600;
}
$state = $this->updateChecker->getUpdateState();
$shouldCheck =
empty($state) ||
!isset($state->lastCheck) ||
( (time() - $state->lastCheck) >= $timeout );
//Let plugin authors substitute their own algorithm.
$shouldCheck = apply_filters(
'puc_check_now-' . $this->updateChecker->slug,
$shouldCheck,
(!empty($state) && isset($state->lastCheck)) ? $state->lastCheck : 0,
$this->checkPeriod
);
if ( $shouldCheck ) {
$this->updateChecker->checkForUpdates();
}
}
/**
* Add our custom schedule to the array of Cron schedules used by WP.
*
* @param array $schedules
* @return array
*/
public function _addCustomSchedule($schedules){
if ( $this->checkPeriod && ($this->checkPeriod > 0) ){
$scheduleName = 'every' . $this->checkPeriod . 'hours';
$schedules[$scheduleName] = array(
'interval' => $this->checkPeriod * 3600,
'display' => sprintf('Every %d hours', $this->checkPeriod),
);
}
return $schedules;
}
/**
* Remove the scheduled cron event that the library uses to check for updates.
*
* @return void
*/
public function _removeUpdaterCron(){
wp_clear_scheduled_hook($this->cronHook);
}
/**
* Get the name of the update checker's WP-cron hook. Mostly useful for debugging.
*
* @return string
*/
public function getCronHookName() {
return $this->cronHook;
}
}
endif;
if ( !class_exists('PucUpgraderStatus_3_0', false) ):
/**
* A utility class that helps figure out which plugin WordPress is upgrading.
*
* It may seem strange to have an separate class just for that, but the task is surprisingly complicated.
* Core classes like Plugin_Upgrader don't expose the plugin file name during an in-progress update (AFAICT).
* This class uses a few workarounds and heuristics to get the file name.
*/
class PucUpgraderStatus_3_0 {
private $upgradedPluginFile = null; //The plugin that is currently being upgraded by WordPress.
public function __construct() {
//Keep track of which plugin WordPress is currently upgrading.
add_filter('upgrader_pre_install', array($this, 'setUpgradedPlugin'), 10, 2);
add_filter('upgrader_package_options', array($this, 'setUpgradedPluginFromOptions'), 10, 1);
add_filter('upgrader_post_install', array($this, 'clearUpgradedPlugin'), 10, 1);
add_action('upgrader_process_complete', array($this, 'clearUpgradedPlugin'), 10, 1);
}
/**
* Is there and update being installed RIGHT NOW, for a specific plugin?
*
* Caution: This method is unreliable. WordPress doesn't make it easy to figure out what it is upgrading,
* and upgrader implementations are liable to change without notice.
*
* @param string $pluginFile The plugin to check.
* @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
* @return bool True if the plugin identified by $pluginFile is being upgraded.
*/
public function isPluginBeingUpgraded($pluginFile, $upgrader = null) {
if ( isset($upgrader) ) {
$upgradedPluginFile = $this->getPluginBeingUpgradedBy($upgrader);
if ( !empty($upgradedPluginFile) ) {
$this->upgradedPluginFile = $upgradedPluginFile;
}
}
return ( !empty($this->upgradedPluginFile) && ($this->upgradedPluginFile === $pluginFile) );
}
/**
* Get the file name of the plugin that's currently being upgraded.
*
* @param Plugin_Upgrader|WP_Upgrader $upgrader
* @return string|null
*/
private function getPluginBeingUpgradedBy($upgrader) {
if ( !isset($upgrader, $upgrader->skin) ) {
return null;
}
//Figure out which plugin is being upgraded.
$pluginFile = null;
$skin = $upgrader->skin;
if ( $skin instanceof Plugin_Upgrader_Skin ) {
if ( isset($skin->plugin) && is_string($skin->plugin) && ($skin->plugin !== '') ) {
$pluginFile = $skin->plugin;
}
} elseif ( isset($skin->plugin_info) && is_array($skin->plugin_info) ) {
//This case is tricky because Bulk_Plugin_Upgrader_Skin (etc) doesn't actually store the plugin
//filename anywhere. Instead, it has the plugin headers in $plugin_info. So the best we can
//do is compare those headers to the headers of installed plugins.
$pluginFile = $this->identifyPluginByHeaders($skin->plugin_info);
}
return $pluginFile;
}
/**
* Identify an installed plugin based on its headers.
*
* @param array $searchHeaders The plugin file header to look for.
* @return string|null Plugin basename ("foo/bar.php"), or NULL if we can't identify the plugin.
*/
private function identifyPluginByHeaders($searchHeaders) {
if ( !function_exists('get_plugins') ){
/** @noinspection PhpIncludeInspection */
require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
}
$installedPlugins = get_plugins();
$matches = array();
foreach($installedPlugins as $pluginBasename => $headers) {
$diff1 = array_diff_assoc($headers, $searchHeaders);
$diff2 = array_diff_assoc($searchHeaders, $headers);
if ( empty($diff1) && empty($diff2) ) {
$matches[] = $pluginBasename;
}
}
//It's possible (though very unlikely) that there could be two plugins with identical
//headers. In that case, we can't unambiguously identify the plugin that's being upgraded.
if ( count($matches) !== 1 ) {
return null;
}
return reset($matches);
}
/**
* @access private
*
* @param mixed $input
* @param array $hookExtra
* @return mixed Returns $input unaltered.
*/
public function setUpgradedPlugin($input, $hookExtra) {
if (!empty($hookExtra['plugin']) && is_string($hookExtra['plugin'])) {
$this->upgradedPluginFile = $hookExtra['plugin'];
} else {
$this->upgradedPluginFile = null;
}
return $input;
}
/**
* @access private
*
* @param array $options
* @return array
*/
public function setUpgradedPluginFromOptions($options) {
if (isset($options['hook_extra']['plugin']) && is_string($options['hook_extra']['plugin'])) {
$this->upgradedPluginFile = $options['hook_extra']['plugin'];
} else {
$this->upgradedPluginFile = null;
}
return $options;
}
/**
* @access private
*
* @param mixed $input
* @return mixed Returns $input unaltered.
*/
public function clearUpgradedPlugin($input = null) {
$this->upgradedPluginFile = null;
return $input;
}
}
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_3_0
*/
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;
require_once(dirname(__FILE__) . '/github-checker.php');
//Register classes defined in this file with the factory.
PucFactory::addVersion('PluginUpdateChecker', 'PluginUpdateChecker_3_0', '3.0');
PucFactory::addVersion('PluginUpdate', 'PluginUpdate_3_0', '3.0');
PucFactory::addVersion('PluginInfo', 'PluginInfo_3_0', '3.0');
PucFactory::addVersion('PucGitHubChecker', 'PucGitHubChecker_3_0', '3.0');