From 9931919237c76d35143d717f0a5c078df6632ee2 Mon Sep 17 00:00:00 2001 From: Yahnis Elsts Date: Wed, 13 Jan 2016 18:03:06 +0200 Subject: [PATCH 1/4] Extract scheduling logic to a separate class. WIP. --- debug-bar-panel.php | 15 +- plugin-update-checker.php | 329 ++++++++++++++++++++++---------------- 2 files changed, 195 insertions(+), 149 deletions(-) diff --git a/debug-bar-panel.php b/debug-bar-panel.php index 135efae..3f141f8 100644 --- a/debug-bar-panel.php +++ b/debug-bar-panel.php @@ -48,20 +48,21 @@ class PluginUpdateCheckerPanel extends Debug_Bar_Panel { } $this->row('Metadata URL', htmlentities($this->updateChecker->metadataUrl) . ' ' . $requestInfoButton . $this->responseBox); - if ( $this->updateChecker->checkPeriod > 0 ) { - $this->row('Automatic checks', 'Every ' . $this->updateChecker->checkPeriod . ' hours'); + $scheduler = $this->updateChecker->scheduler; + if ( $scheduler->checkPeriod > 0 ) { + $this->row('Automatic checks', 'Every ' . $scheduler->checkPeriod . ' hours'); } else { $this->row('Automatic checks', 'Disabled'); } - if ( isset($this->updateChecker->throttleRedundantChecks) ) { - if ( $this->updateChecker->throttleRedundantChecks && ($this->updateChecker->checkPeriod > 0) ) { + if ( isset($scheduler->throttleRedundantChecks) ) { + if ( $scheduler->throttleRedundantChecks && ($scheduler->checkPeriod > 0) ) { $this->row( 'Throttling', sprintf( 'Enabled. If an update is already available, check for updates every %1$d hours instead of every %2$d hours.', - $this->updateChecker->throttledCheckPeriod, - $this->updateChecker->checkPeriod + $scheduler->throttledCheckPeriod, + $scheduler->checkPeriod ) ); } else { @@ -86,7 +87,7 @@ class PluginUpdateCheckerPanel extends Debug_Bar_Panel { $this->row('Last check', 'Never'); } - $nextCheck = wp_next_scheduled($this->updateChecker->getCronHookName()); + $nextCheck = wp_next_scheduled($this->updateChecker->scheduler->getCronHookName()); $this->row('Next automatic check', $this->formatTimeWithDelta($nextCheck)); if ( isset($state, $state->checkedVersion) ) { diff --git a/plugin-update-checker.php b/plugin-update-checker.php index 58225b9..42a358f 100644 --- a/plugin-update-checker.php +++ b/plugin-update-checker.php @@ -22,17 +22,13 @@ class PluginUpdateChecker_2_3 { public $pluginAbsolutePath = ''; //Full path of the main plugin file. public $pluginFile = ''; //Plugin filename relative to the plugins directory. Many WP APIs use this to identify plugins. public $slug = ''; //Plugin slug. - public $checkPeriod = 12; //How often to check for updates (in hours). public $optionName = ''; //Where to store the update info. public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory. public $debugMode = false; //Set to TRUE to enable error reporting. Errors are raised using trigger_error() //and should be logged to the standard PHP error log. + public $scheduler; - public $throttleRedundantChecks = false; //Check less often if we already know that an update is available. - public $throttledCheckPeriod = 72; - - private $cronHook = null; private $debugBarPlugin = null; private $cachedInstalledVersion = null; @@ -55,7 +51,6 @@ class PluginUpdateChecker_2_3 { $this->pluginAbsolutePath = $pluginFile; $this->pluginFile = plugin_basename($this->pluginAbsolutePath); $this->muPluginFile = $muPluginFile; - $this->checkPeriod = $checkPeriod; $this->slug = $slug; $this->optionName = $optionName; $this->debugMode = (bool)(constant('WP_DEBUG')); @@ -75,9 +70,24 @@ class PluginUpdateChecker_2_3 { if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) { $this->muPluginFile = $this->pluginFile; } - + + $this->scheduler = $this->createScheduler($checkPeriod); + $this->installHooks(); } + + /** + * Create an instance of the scheduler. + * + * This is implemented as a method to make it possible for plugins to subclass the update checker + * and substitute their own scheduler. + * + * @param int $checkPeriod + * @return PucScheduler + */ + protected function createScheduler($checkPeriod) { + return new PucScheduler($this, $checkPeriod); + } /** * Install the hooks required to run periodic update checks and inject update info @@ -101,50 +111,6 @@ class PluginUpdateChecker_2_3 { add_filter('upgrader_post_install', array($this, 'clearCachedVersion')); add_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion')); - //Set up the periodic update checks - $this->cronHook = 'check_plugin_updates-' . $this->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->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); - } - if ( did_action('plugins_loaded') ) { $this->initDebugBarPanel(); } else { @@ -165,41 +131,6 @@ class PluginUpdateChecker_2_3 { add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2); } - /** - * 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; - } - /** * Explicitly allow HTTP requests to the metadata URL. * @@ -413,62 +344,6 @@ class PluginUpdateChecker_2_3 { return $this->getUpdate(); } - /** - * 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->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->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->slug, - $shouldCheck, - (!empty($state) && isset($state->lastCheck)) ? $state->lastCheck : 0, - $this->checkPeriod - ); - - if ( $shouldCheck ){ - $this->checkForUpdates(); - } - } - /** * Load the update checker state from the DB. * @@ -1318,6 +1193,176 @@ class PluginUpdate_2_3 { endif; +if ( !class_exists('PucScheduler', false) ): + +/** + * The scheduler decides when and how often to check for updates. + * It calls @see PluginUpdateChecker_2_3::checkForUpdates() to perform the actual checks. + * + * @version 3.0 + */ +class PucScheduler { + 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_2_3 + */ + protected $updateChecker; + + private $cronHook = null; + + /** + * Scheduler constructor. + * + * @param PluginUpdateChecker_2_3 $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('PucFactory', false) ): /** From 487df1d1d641cfe5f8775d9e5f371c7d2dc17a70 Mon Sep 17 00:00:00 2001 From: Yahnis Elsts Date: Wed, 13 Jan 2016 18:45:56 +0200 Subject: [PATCH 2/4] Minor: Bump copyright year to 2016 --- plugin-update-checker.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin-update-checker.php b/plugin-update-checker.php index 42a358f..bd830f4 100644 --- a/plugin-update-checker.php +++ b/plugin-update-checker.php @@ -3,7 +3,7 @@ * Plugin Update Checker Library 2.3.0 * http://w-shadow.com/ * - * Copyright 2015 Janis Elsts + * Copyright 2016 Janis Elsts * Released under the MIT license. See license.txt for details. */ @@ -13,7 +13,7 @@ if ( !class_exists('PluginUpdateChecker_2_3', false) ): * A custom plugin update checker. * * @author Janis Elsts - * @copyright 2015 + * @copyright 2016 * @version 2.3 * @access public */ @@ -943,7 +943,7 @@ if ( !class_exists('PluginInfo_2_3', false) ): * A container class for holding and transforming various plugin metadata. * * @author Janis Elsts - * @copyright 2015 + * @copyright 2016 * @version 2.3 * @access public */ @@ -1084,7 +1084,7 @@ if ( !class_exists('PluginUpdate_2_3', false) ): * A simple container class for holding information about an available update. * * @author Janis Elsts - * @copyright 2015 + * @copyright 2016 * @version 2.3 * @access public */ From 3006c8b7366576aac285fdc497d6ece260d74781 Mon Sep 17 00:00:00 2001 From: Yahnis Elsts Date: Thu, 14 Jan 2016 17:26:44 +0200 Subject: [PATCH 3/4] Bump version to 3.0 because there are backwards-incompatible changes. --- github-checker.php | 10 +++---- plugin-update-checker.php | 56 +++++++++++++++++++-------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/github-checker.php b/github-checker.php index e9c54a0..c83c9b5 100644 --- a/github-checker.php +++ b/github-checker.php @@ -1,8 +1,8 @@ filename = $this->pluginFile; $info->slug = $this->slug; $info->sections = array(); @@ -360,7 +360,7 @@ class PucGitHubChecker_2_3 extends PluginUpdateChecker_2_3 { * Copy plugin metadata from a file header to a PluginInfo object. * * @param array $fileHeader - * @param PluginInfo_2_3 $pluginInfo + * @param PluginInfo_3_0 $pluginInfo */ protected function setInfoFromHeader($fileHeader, $pluginInfo) { $headerToPropertyMap = array( @@ -394,7 +394,7 @@ class PucGitHubChecker_2_3 extends PluginUpdateChecker_2_3 { * Copy plugin metadata from the remote readme.txt file. * * @param string $ref GitHub tag or branch where to look for the readme. - * @param PluginInfo_2_3 $pluginInfo + * @param PluginInfo_3_0 $pluginInfo */ protected function setInfoFromRemoteReadme($ref, $pluginInfo) { $readmeTxt = $this->getRemoteFile('readme.txt', $ref); diff --git a/plugin-update-checker.php b/plugin-update-checker.php index 5ed769a..1ec78a2 100644 --- a/plugin-update-checker.php +++ b/plugin-update-checker.php @@ -1,23 +1,23 @@ validateApiResponse($result); $pluginInfo = null; if ( !is_wp_error($status) ){ - $pluginInfo = PluginInfo_2_3::fromJson($result['body']); + $pluginInfo = PluginInfo_3_0::fromJson($result['body']); if ( $pluginInfo !== null ) { $pluginInfo->filename = $this->pluginFile; $pluginInfo->slug = $this->slug; @@ -257,7 +257,7 @@ class PluginUpdateChecker_2_3 { if ( $pluginInfo == null ){ return null; } - return PluginUpdate_2_3::fromPluginInfo($pluginInfo); + return PluginUpdate_3_0::fromPluginInfo($pluginInfo); } /** @@ -359,7 +359,7 @@ class PluginUpdateChecker_2_3 { } if ( !empty($state) && isset($state->update) && is_object($state->update) ){ - $state->update = PluginUpdate_2_3::fromObject($state->update); + $state->update = PluginUpdate_3_0::fromObject($state->update); } return $state; } @@ -940,17 +940,17 @@ class PluginUpdateChecker_2_3 { endif; -if ( !class_exists('PluginInfo_2_3', false) ): +if ( !class_exists('PluginInfo_3_0', false) ): /** * A container class for holding and transforming various plugin metadata. * * @author Janis Elsts * @copyright 2016 - * @version 2.3 + * @version 3.0 * @access public */ -class PluginInfo_2_3 { +class PluginInfo_3_0 { //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; @@ -1081,17 +1081,17 @@ class PluginInfo_2_3 { endif; -if ( !class_exists('PluginUpdate_2_3', false) ): +if ( !class_exists('PluginUpdate_3_0', false) ): /** * A simple container class for holding information about an available update. * * @author Janis Elsts * @copyright 2016 - * @version 2.3 + * @version 3.0 * @access public */ -class PluginUpdate_2_3 { +class PluginUpdate_3_0 { public $id = 0; public $slug; public $version; @@ -1112,7 +1112,7 @@ class PluginUpdate_2_3 { //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); + $pluginInfo = PluginInfo_3_0::fromJson($json); if ( $pluginInfo != null ) { return self::fromPluginInfo($pluginInfo); } else { @@ -1196,21 +1196,21 @@ class PluginUpdate_2_3 { endif; -if ( !class_exists('PucScheduler', false) ): +if ( !class_exists('PucScheduler_3_0', false) ): /** * The scheduler decides when and how often to check for updates. - * It calls @see PluginUpdateChecker_2_3::checkForUpdates() to perform the actual checks. + * It calls @see PluginUpdateChecker_3_0::checkForUpdates() to perform the actual checks. * * @version 3.0 */ -class PucScheduler { +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_2_3 + * @var PluginUpdateChecker_3_0 */ protected $updateChecker; @@ -1219,7 +1219,7 @@ class PucScheduler { /** * Scheduler constructor. * - * @param PluginUpdateChecker_2_3 $updateChecker + * @param PluginUpdateChecker_3_0 $updateChecker * @param int $checkPeriod How often to check for updates (in hours). */ public function __construct($updateChecker, $checkPeriod) { @@ -1458,23 +1458,23 @@ endif; require_once(dirname(__FILE__) . '/github-checker.php'); //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'); -PucFactory::addVersion('PucGitHubChecker', 'PucGitHubChecker_2_3', '2.3'); +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'); /** * 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 { } + class PluginUpdateChecker extends PluginUpdateChecker_3_0 { } } if ( !class_exists('PluginUpdate', false) ) { - class PluginUpdate extends PluginUpdate_2_3 {} + class PluginUpdate extends PluginUpdate_3_0 {} } if ( !class_exists('PluginInfo', false) ) { - class PluginInfo extends PluginInfo_2_3 {} + class PluginInfo extends PluginInfo_3_0 {} } From 58a8a13efbed2d257ee47223eb067504a1cc87d9 Mon Sep 17 00:00:00 2001 From: Yahnis Elsts Date: Thu, 14 Jan 2016 19:32:58 +0200 Subject: [PATCH 4/4] Extract the "which plugin is being upgraded?" heuristics to a new class. --- plugin-update-checker.php | 259 +++++++++++++++++++++----------------- 1 file changed, 145 insertions(+), 114 deletions(-) diff --git a/plugin-update-checker.php b/plugin-update-checker.php index 1ec78a2..00ec54c 100644 --- a/plugin-update-checker.php +++ b/plugin-update-checker.php @@ -29,11 +29,11 @@ class PluginUpdateChecker_3_0 { //and should be logged to the standard PHP error log. public $scheduler; + protected $upgraderStatus; + private $debugBarPlugin = null; private $cachedInstalledVersion = null; - private $upgradedPluginFile = null; //The plugin that is currently being upgraded by WordPress. - private $metadataHost = ''; //The host component of $metadataUrl. /** @@ -72,6 +72,7 @@ class PluginUpdateChecker_3_0 { } $this->scheduler = $this->createScheduler($checkPeriod); + $this->upgraderStatus = new PucUpgraderStatus_3_0(); $this->installHooks(); } @@ -578,121 +579,11 @@ class PluginUpdateChecker_3_0 { /** * Is there and update being installed RIGHT NOW, for this 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 WP_Upgrader|null $upgrader The upgrader that's performing the current update. * @return bool */ public function isPluginBeingUpgraded($upgrader = null) { - if ( isset($upgrader) ) { - $pluginFile = $this->getPluginBeingUpgradedBy($upgrader); - if ( !empty($pluginFile) ) { - $this->upgradedPluginFile = $pluginFile; - } - } - return ( !empty($this->upgradedPluginFile) && ($this->upgradedPluginFile === $this->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; + return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader); } /** @@ -1200,7 +1091,7 @@ if ( !class_exists('PucScheduler_3_0', false) ): /** * The scheduler decides when and how often to check for updates. - * It calls @see PluginUpdateChecker_3_0::checkForUpdates() to perform the actual checks. + * It calls @see PluginUpdateChecker::checkForUpdates() to perform the actual checks. * * @version 3.0 */ @@ -1365,6 +1256,146 @@ class PucScheduler_3_0 { endif; +/** + * 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; + } +} + if ( !class_exists('PucFactory', false) ):