* @access public\r
*/\r
class PluginUpdateChecker_3_1 {\r
- public $metadataUrl = ''; //The URL of the plugin's metadata file.\r
- public $pluginAbsolutePath = ''; //Full path of the main plugin file.\r
- public $pluginFile = ''; //Plugin filename relative to the plugins directory. Many WP APIs use this to identify plugins.\r
- public $slug = ''; //Plugin slug.\r
- public $optionName = ''; //Where to store the update info.\r
- public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.\r
-\r
- public $debugMode = false; //Set to TRUE to enable error reporting. Errors are raised using trigger_error()\r
+ public $metadataUrl = ''; //The URL of the plugin's metadata file.\r
+ public $pluginAbsolutePath = ''; //Full path of the main plugin file.\r
+ public $pluginFile = ''; //Plugin filename relative to the plugins directory. Many WP APIs use this to identify plugins.\r
+ public $slug = ''; //Plugin slug.\r
+ public $optionName = ''; //Where to store the update info.\r
+ public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.\r
+\r
+ public $debugMode = false; //Set to TRUE to enable error reporting. Errors are raised using trigger_error()\r
//and should be logged to the standard PHP error log.\r
- public $scheduler;\r
-\r
- protected $upgraderStatus;\r
-\r
- private $debugBarPlugin = null;\r
- private $cachedInstalledVersion = null;\r
-\r
- private $metadataHost = ''; //The host component of $metadataUrl.\r
-\r
- /**\r
- * Class constructor.\r
- *\r
- * @param string $metadataUrl The URL of the plugin's metadata file.\r
- * @param string $pluginFile Fully qualified path to the main plugin file.\r
- * @param string $slug The plugin's 'slug'. If not specified, the filename part of $pluginFile sans '.php' will be used as the slug.\r
- * @param integer $checkPeriod How often to check for updates (in hours). Defaults to checking every 12 hours. Set to 0 to disable automatic update checks.\r
- * @param string $optionName Where to store book-keeping info about update checks. Defaults to 'external_updates-$slug'.\r
- * @param string $muPluginFile Optional. The plugin filename relative to the mu-plugins directory.\r
- */\r
- public function __construct($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = ''){\r
- $this->metadataUrl = $metadataUrl;\r
- $this->pluginAbsolutePath = $pluginFile;\r
- $this->pluginFile = plugin_basename($this->pluginAbsolutePath);\r
- $this->muPluginFile = $muPluginFile;\r
- $this->slug = $slug;\r
- $this->optionName = $optionName;\r
- $this->debugMode = (bool)(constant('WP_DEBUG'));\r
-\r
- //If no slug is specified, use the name of the main plugin file as the slug.\r
- //For example, 'my-cool-plugin/cool-plugin.php' becomes 'cool-plugin'.\r
- if ( empty($this->slug) ){\r
- $this->slug = basename($this->pluginFile, '.php');\r
- }\r
-\r
- if ( empty($this->optionName) ){\r
- $this->optionName = 'external_updates-' . $this->slug;\r
- }\r
-\r
- //Backwards compatibility: If the plugin is a mu-plugin but no $muPluginFile is specified, assume\r
- //it's the same as $pluginFile given that it's not in a subdirectory (WP only looks in the base dir).\r
- if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) {\r
- $this->muPluginFile = $this->pluginFile;\r
- }\r
-\r
- $this->scheduler = $this->createScheduler($checkPeriod);\r
- $this->upgraderStatus = new PucUpgraderStatus_3_1();\r
-\r
- $this->installHooks();\r
- }\r
-\r
- /**\r
- * Create an instance of the scheduler.\r
- *\r
- * This is implemented as a method to make it possible for plugins to subclass the update checker\r
- * and substitute their own scheduler.\r
- *\r
- * @param int $checkPeriod\r
- * @return PucScheduler_3_1\r
- */\r
- protected function createScheduler($checkPeriod) {\r
- return new PucScheduler_3_1($this, $checkPeriod);\r
- }\r
-\r
- /**\r
- * Install the hooks required to run periodic update checks and inject update info\r
- * into WP data structures.\r
- *\r
- * @return void\r
- */\r
- protected function installHooks(){\r
- //Override requests for plugin information\r
- add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);\r
-\r
- //Insert our update info into the update array maintained by WP.\r
- add_filter('site_transient_update_plugins', array($this,'injectUpdate')); //WP 3.0+\r
- add_filter('transient_update_plugins', array($this,'injectUpdate')); //WP 2.8+\r
- add_filter('site_transient_update_plugins', array($this, 'injectTranslationUpdates'));\r
-\r
- add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);\r
- add_action('admin_init', array($this, 'handleManualCheck'));\r
- add_action('all_admin_notices', array($this, 'displayManualCheckResult'));\r
-\r
- //Clear the version number cache when something - anything - is upgraded or WP clears the update cache.\r
- add_filter('upgrader_post_install', array($this, 'clearCachedVersion'));\r
- add_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));\r
- //Clear translation updates when WP clears the update cache.\r
- //This needs to be done directly because the library doesn't actually remove obsolete plugin updates,\r
- //it just hides them (see getUpdate()). We can't do that with translations - too much disk I/O.\r
- add_action('delete_site_transient_update_plugins', array($this, 'clearCachedTranslationUpdates'));\r
-\r
- if ( did_action('plugins_loaded') ) {\r
- $this->initDebugBarPanel();\r
- } else {\r
- add_action('plugins_loaded', array($this, 'initDebugBarPanel'));\r
- }\r
-\r
- //Rename the update directory to be the same as the existing directory.\r
- add_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10, 3);\r
-\r
- //Enable language support (i18n).\r
- load_plugin_textdomain('plugin-update-checker', false, plugin_basename(dirname(__FILE__)) . '/languages');\r
-\r
- //Allow HTTP requests to the metadata URL even if it's on a local host.\r
- $this->metadataHost = @parse_url($this->metadataUrl, PHP_URL_HOST);\r
- add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);\r
- }\r
-\r
- /**\r
- * Explicitly allow HTTP requests to the metadata URL.\r
- *\r
- * WordPress has a security feature where the HTTP API will reject all requests that are sent to\r
- * another site hosted on the same server as the current site (IP match), a local host, or a local\r
- * IP, unless the host exactly matches the current site.\r
- *\r
- * This feature is opt-in (at least in WP 4.4). Apparently some people enable it.\r
- *\r
- * That can be a problem when you're developing your plugin and you decide to host the update information\r
- * on the same server as your test site. Update requests will mysteriously fail.\r
- *\r
- * We fix that by adding an exception for the metadata host.\r
- *\r
- * @param bool $allow\r
- * @param string $host\r
- * @return bool\r
- */\r
- public function allowMetadataHost($allow, $host) {\r
- if ( strtolower($host) === strtolower($this->metadataHost) ) {\r
- return true;\r
- }\r
- return $allow;\r
- }\r
-\r
- /**\r
- * Retrieve plugin info from the configured API endpoint.\r
- *\r
- * @uses wp_remote_get()\r
- *\r
- * @param array $queryArgs Additional query arguments to append to the request. Optional.\r
- * @return PluginInfo_3_1\r
- */\r
- public function requestInfo($queryArgs = array()){\r
- //Query args to append to the URL. Plugins can add their own by using a filter callback (see addQueryArgFilter()).\r
- $installedVersion = $this->getInstalledVersion();\r
- $queryArgs['installed_version'] = ($installedVersion !== null) ? $installedVersion : '';\r
- $queryArgs = apply_filters('puc_request_info_query_args-'.$this->slug, $queryArgs);\r
-\r
- //Various options for the wp_remote_get() call. Plugins can filter these, too.\r
- $options = array(\r
- 'timeout' => 10, //seconds\r
- 'headers' => array(\r
- 'Accept' => 'application/json'\r
- ),\r
- );\r
- $options = apply_filters('puc_request_info_options-'.$this->slug, $options);\r
-\r
- //The plugin info should be at 'http://your-api.com/url/here/$slug/info.json'\r
- $url = $this->metadataUrl;\r
- if ( !empty($queryArgs) ){\r
- $url = add_query_arg($queryArgs, $url);\r
- }\r
-\r
- $result = wp_remote_get(\r
- $url,\r
- $options\r
- );\r
-\r
- //Try to parse the response\r
- $status = $this->validateApiResponse($result);\r
- $pluginInfo = null;\r
- if ( !is_wp_error($status) ){\r
- $pluginInfo = PluginInfo_3_1::fromJson($result['body']);\r
- if ( $pluginInfo !== null ) {\r
- $pluginInfo->filename = $this->pluginFile;\r
- $pluginInfo->slug = $this->slug;\r
- }\r
- } else {\r
- $this->triggerError(\r
- sprintf('The URL %s does not point to a valid plugin metadata file. ', $url)\r
- . $status->get_error_message(),\r
- E_USER_WARNING\r
- );\r
- }\r
-\r
- $pluginInfo = apply_filters('puc_request_info_result-'.$this->slug, $pluginInfo, $result);\r
- return $pluginInfo;\r
- }\r
-\r
- /**\r
- * Check if $result is a successful update API response.\r
- *\r
- * @param array|WP_Error $result\r
- * @return true|WP_Error\r
- */\r
- private function validateApiResponse($result) {\r
-\r
- if ( is_wp_error($result) ) { /** @var WP_Error $result */\r
- return new WP_Error($result->get_error_code(), 'WP HTTP Error: ' . $result->get_error_message());\r
- }\r
-\r
- if ( !isset($result['response']['code']) ) {\r
- return new WP_Error('puc_no_response_code', 'wp_remote_get() returned an unexpected result.');\r
- }\r
-\r
- if ( $result['response']['code'] !== 200 ) {\r
- return new WP_Error(\r
- 'puc_unexpected_response_code',\r
- 'HTTP response code is ' . $result['response']['code'] . ' (expected: 200)'\r
- );\r
- }\r
-\r
- if ( empty($result['body']) ) {\r
- return new WP_Error('puc_empty_response', 'The metadata file appears to be empty.');\r
- }\r
-\r
- return true;\r
- }\r
-\r
- /**\r
- * Retrieve the latest update (if any) from the configured API endpoint.\r
- *\r
- * @uses PluginUpdateChecker::requestInfo()\r
- *\r
- * @return PluginUpdate_3_1 An instance of PluginUpdate, or NULL when no updates are available.\r
- */\r
- public function requestUpdate(){\r
- //For the sake of simplicity, this function just calls requestInfo()\r
- //and transforms the result accordingly.\r
- $pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));\r
- if ( $pluginInfo == null ){\r
- return null;\r
- }\r
- $update = PluginUpdate_3_1::fromPluginInfo($pluginInfo);\r
-\r
- //Keep only those translation updates that apply to this site.\r
- $update->translations = $this->filterApplicableTranslations($update->translations);\r
-\r
- return $update;\r
- }\r
-\r
- /**\r
- * Filter a list of translation updates and return a new list that contains only updates\r
- * that apply to the current site.\r
- *\r
- * @param array $translations\r
- * @return array\r
- */\r
- private function filterApplicableTranslations($translations) {\r
- $languages = array_flip(array_values(get_available_languages()));\r
- $installedTranslations = wp_get_installed_translations('plugins');\r
- if ( isset($installedTranslations[$this->slug]) ) {\r
- $installedTranslations = $installedTranslations[$this->slug];\r
- } else {\r
- $installedTranslations = array();\r
- }\r
-\r
- $applicableTranslations = array();\r
- foreach($translations as $translation) {\r
- //Does it match one of the available core languages?\r
- $isApplicable = array_key_exists($translation->language, $languages);\r
- //Is it more recent than an already-installed translation?\r
- if ( isset($installedTranslations[$translation->language]) ) {\r
- $updateTimestamp = strtotime($translation->updated);\r
- $installedTimestamp = strtotime($installedTranslations[$translation->language]['PO-Revision-Date']);\r
- $isApplicable = $updateTimestamp > $installedTimestamp;\r
- }\r
-\r
- if ( $isApplicable ) {\r
- $applicableTranslations[] = $translation;\r
- }\r
- }\r
-\r
- return $applicableTranslations;\r
- }\r
-\r
- /**\r
- * Get the currently installed version of the plugin.\r
- *\r
- * @return string Version number.\r
- */\r
- public function getInstalledVersion(){\r
- if ( isset($this->cachedInstalledVersion) ) {\r
- return $this->cachedInstalledVersion;\r
- }\r
-\r
- $pluginHeader = $this->getPluginHeader();\r
- if ( isset($pluginHeader['Version']) ) {\r
- $this->cachedInstalledVersion = $pluginHeader['Version'];\r
- return $pluginHeader['Version'];\r
- } else {\r
- //This can happen if the filename points to something that is not a plugin.\r
- $this->triggerError(\r
- sprintf(\r
- "Can't to read the Version header for '%s'. The filename is incorrect or is not a plugin.",\r
- $this->pluginFile\r
- ),\r
- E_USER_WARNING\r
- );\r
- return null;\r
- }\r
- }\r
-\r
- /**\r
- * Get plugin's metadata from its file header.\r
- *\r
- * @return array\r
- */\r
- protected function getPluginHeader() {\r
- if ( !is_file($this->pluginAbsolutePath) ) {\r
- //This can happen if the plugin filename is wrong.\r
- $this->triggerError(\r
- sprintf(\r
- "Can't to read the plugin header for '%s'. The file does not exist.",\r
- $this->pluginFile\r
- ),\r
- E_USER_WARNING\r
- );\r
- return array();\r
- }\r
-\r
- if ( !function_exists('get_plugin_data') ){\r
- /** @noinspection PhpIncludeInspection */\r
- require_once( ABSPATH . '/wp-admin/includes/plugin.php' );\r
- }\r
- return get_plugin_data($this->pluginAbsolutePath, false, false);\r
- }\r
-\r
- /**\r
- * Check for plugin updates.\r
- * The results are stored in the DB option specified in $optionName.\r
- *\r
- * @return PluginUpdate_3_1|null\r
- */\r
- public function checkForUpdates(){\r
- $installedVersion = $this->getInstalledVersion();\r
- //Fail silently if we can't find the plugin or read its header.\r
- if ( $installedVersion === null ) {\r
- $this->triggerError(\r
- sprintf('Skipping update check for %s - installed version unknown.', $this->pluginFile),\r
- E_USER_WARNING\r
- );\r
- return null;\r
- }\r
-\r
- $state = $this->getUpdateState();\r
- if ( empty($state) ){\r
- $state = new stdClass;\r
- $state->lastCheck = 0;\r
- $state->checkedVersion = '';\r
- $state->update = null;\r
- }\r
-\r
- $state->lastCheck = time();\r
- $state->checkedVersion = $installedVersion;\r
- $this->setUpdateState($state); //Save before checking in case something goes wrong\r
-\r
- $state->update = $this->requestUpdate();\r
- $this->setUpdateState($state);\r
-\r
- return $this->getUpdate();\r
- }\r
-\r
- /**\r
- * Load the update checker state from the DB.\r
- *\r
- * @return stdClass|null\r
- */\r
- public function getUpdateState() {\r
- $state = get_site_option($this->optionName, null);\r
- if ( empty($state) || !is_object($state)) {\r
- $state = null;\r
- }\r
-\r
- if ( isset($state, $state->update) && is_object($state->update) ) {\r
- $state->update = PluginUpdate_3_1::fromObject($state->update);\r
- }\r
- return $state;\r
- }\r
-\r
-\r
- /**\r
- * Persist the update checker state to the DB.\r
- *\r
- * @param StdClass $state\r
- * @return void\r
- */\r
- private function setUpdateState($state) {\r
- if ( isset($state->update) && is_object($state->update) && method_exists($state->update, 'toStdClass') ) {\r
- $update = $state->update; /** @var PluginUpdate_3_1 $update */\r
- $state->update = $update->toStdClass();\r
- }\r
- update_site_option($this->optionName, $state);\r
- }\r
-\r
- /**\r
- * Reset update checker state - i.e. last check time, cached update data and so on.\r
- *\r
- * Call this when your plugin is being uninstalled, or if you want to\r
- * clear the update cache.\r
- */\r
- public function resetUpdateState() {\r
- delete_site_option($this->optionName);\r
- }\r
-\r
- /**\r
- * Intercept plugins_api() calls that request information about our plugin and\r
- * use the configured API endpoint to satisfy them.\r
- *\r
- * @see plugins_api()\r
- *\r
- * @param mixed $result\r
- * @param string $action\r
- * @param array|object $args\r
- * @return mixed\r
- */\r
- public function injectInfo($result, $action = null, $args = null){\r
- $relevant = ($action == 'plugin_information') && isset($args->slug) && (\r
- ($args->slug == $this->slug) || ($args->slug == dirname($this->pluginFile))\r
- );\r
- if ( !$relevant ) {\r
- return $result;\r
- }\r
-\r
- $pluginInfo = $this->requestInfo();\r
- $pluginInfo = apply_filters('puc_pre_inject_info-' . $this->slug, $pluginInfo);\r
- if ( $pluginInfo ) {\r
- return $pluginInfo->toWpFormat();\r
- }\r
-\r
- return $result;\r
- }\r
-\r
- /**\r
- * Insert the latest update (if any) into the update list maintained by WP.\r
- *\r
- * @param StdClass $updates Update list.\r
- * @return StdClass Modified update list.\r
- */\r
- public function injectUpdate($updates){\r
- //Is there an update to insert?\r
- $update = $this->getUpdate();\r
-\r
- //No update notifications for mu-plugins unless explicitly enabled. The MU plugin file\r
- //is usually different from the main plugin file so the update wouldn't show up properly anyway.\r
- if ( $this->isUnknownMuPlugin() ) {\r
- $update = null;\r
- }\r
-\r
- if ( !empty($update) ) {\r
- //Let plugins filter the update info before it's passed on to WordPress.\r
- $update = apply_filters('puc_pre_inject_update-' . $this->slug, $update);\r
- $updates = $this->addUpdateToList($updates, $update);\r
- } else {\r
- //Clean up any stale update info.\r
- $updates = $this->removeUpdateFromList($updates);\r
- }\r
-\r
- return $updates;\r
- }\r
-\r
- /**\r
- * @param StdClass|null $updates\r
- * @param PluginUpdate_3_1 $updateToAdd\r
- * @return StdClass\r
- */\r
- private function addUpdateToList($updates, $updateToAdd) {\r
- if ( !is_object($updates) ) {\r
- $updates = new stdClass();\r
- $updates->response = array();\r
- }\r
-\r
- $wpUpdate = $updateToAdd->toWpFormat();\r
- $pluginFile = $this->pluginFile;\r
-\r
- if ( $this->isMuPlugin() ) {\r
- //WP does not support automatic update installation for mu-plugins, but we can still display a notice.\r
- $wpUpdate->package = null;\r
- $pluginFile = $this->muPluginFile;\r
- }\r
- $updates->response[$pluginFile] = $wpUpdate;\r
- return $updates;\r
- }\r
-\r
- /**\r
- * @param stdClass|null $updates\r
- * @return stdClass|null\r
- */\r
- private function removeUpdateFromList($updates) {\r
- if ( isset($updates, $updates->response) ) {\r
- unset($updates->response[$this->pluginFile]);\r
- if ( !empty($this->muPluginFile) ) {\r
- unset($updates->response[$this->muPluginFile]);\r
- }\r
- }\r
- return $updates;\r
- }\r
-\r
- /**\r
- * Insert translation updates into the list maintained by WordPress.\r
- *\r
- * @param stdClass $updates\r
- * @return stdClass\r
- */\r
- public function injectTranslationUpdates($updates) {\r
- $translationUpdates = $this->getTranslationUpdates();\r
- if ( empty($translationUpdates) ) {\r
- return $updates;\r
- }\r
-\r
- //Being defensive.\r
- if ( !is_object($updates) ) {\r
- $updates = new stdClass();\r
- }\r
- if ( !isset($updates->translations) ) {\r
- $updates->translations = array();\r
- }\r
-\r
- //In case there's a name collision with a plugin hosted on wordpress.org,\r
- //remove any preexisting updates that match our plugin.\r
- $translationType = 'plugin';\r
- $filteredTranslations = array();\r
- foreach($updates->translations as $translation) {\r
- if ( ($translation['type'] === $translationType) && ($translation['slug'] === $this->slug) ) {\r
- continue;\r
- }\r
- $filteredTranslations[] = $translation;\r
- }\r
- $updates->translations = $filteredTranslations;\r
-\r
- //Add our updates to the list.\r
- foreach($translationUpdates as $update) {\r
- $convertedUpdate = array_merge(\r
- array(\r
- 'type' => $translationType,\r
- 'slug' => $this->slug,\r
- 'autoupdate' => 0,\r
- //AFAICT, WordPress doesn't actually use the "version" field for anything.\r
- //But lets make sure it's there, just in case.\r
- 'version' => isset($update->version) ? $update->version : ('1.' . strtotime($update->updated)),\r
- ),\r
- (array)$update\r
- );\r
-\r
- $updates->translations[] = $convertedUpdate;\r
- }\r
-\r
- return $updates;\r
- }\r
-\r
- /**\r
- * Rename the update directory to match the existing plugin directory.\r
- *\r
- * When WordPress installs a plugin or theme update, it assumes that the ZIP file will contain\r
- * exactly one directory, and that the directory name will be the same as the directory where\r
- * the plugin/theme is currently installed.\r
- *\r
- * GitHub and other repositories provide ZIP downloads, but they often use directory names like\r
- * "project-branch" or "project-tag-hash". We need to change the name to the actual plugin folder.\r
- *\r
- * This is a hook callback. Don't call it from a plugin.\r
- *\r
- * @param string $source The directory to copy to /wp-content/plugins. Usually a subdirectory of $remoteSource.\r
- * @param string $remoteSource WordPress has extracted the update to this directory.\r
- * @param WP_Upgrader $upgrader\r
- * @return string|WP_Error\r
- */\r
- public function fixDirectoryName($source, $remoteSource, $upgrader) {\r
- global $wp_filesystem; /** @var WP_Filesystem_Base $wp_filesystem */\r
-\r
- //Basic sanity checks.\r
- if ( !isset($source, $remoteSource, $upgrader, $upgrader->skin, $wp_filesystem) ) {\r
- return $source;\r
- }\r
-\r
- //If WordPress is upgrading anything other than our plugin, leave the directory name unchanged.\r
- if ( !$this->isPluginBeingUpgraded($upgrader) ) {\r
- return $source;\r
- }\r
-\r
- //Rename the source to match the existing plugin directory.\r
- $pluginDirectoryName = dirname($this->pluginFile);\r
- if ( $pluginDirectoryName === '.' ) {\r
- return $source;\r
- }\r
- $correctedSource = trailingslashit($remoteSource) . $pluginDirectoryName . '/';\r
- if ( $source !== $correctedSource ) {\r
- //The update archive should contain a single directory that contains the rest of plugin files. Otherwise,\r
- //WordPress will try to copy the entire working directory ($source == $remoteSource). We can't rename\r
- //$remoteSource because that would break WordPress code that cleans up temporary files after update.\r
- if ( $this->isBadDirectoryStructure($remoteSource) ) {\r
- return new WP_Error(\r
- 'puc-incorrect-directory-structure',\r
- sprintf(\r
- 'The directory structure of the update is incorrect. All plugin files should be inside ' .\r
- 'a directory named <span class="code">%s</span>, not at the root of the ZIP file.',\r
- htmlentities($this->slug)\r
- )\r
- );\r
- }\r
-\r
- /** @var WP_Upgrader_Skin $upgrader->skin */\r
- $upgrader->skin->feedback(sprintf(\r
- 'Renaming %s to %s…',\r
- '<span class="code">' . basename($source) . '</span>',\r
- '<span class="code">' . $pluginDirectoryName . '</span>'\r
- ));\r
-\r
- if ( $wp_filesystem->move($source, $correctedSource, true) ) {\r
- $upgrader->skin->feedback('Plugin directory successfully renamed.');\r
- return $correctedSource;\r
- } else {\r
- return new WP_Error(\r
- 'puc-rename-failed',\r
- 'Unable to rename the update to match the existing plugin directory.'\r
- );\r
- }\r
- }\r
-\r
- return $source;\r
- }\r
-\r
- /**\r
- * Check for incorrect update directory structure. An update must contain a single directory,\r
- * all other files should be inside that directory.\r
- *\r
- * @param string $remoteSource Directory path.\r
- * @return bool\r
- */\r
- private function isBadDirectoryStructure($remoteSource) {\r
- global $wp_filesystem; /** @var WP_Filesystem_Base $wp_filesystem */\r
-\r
- $sourceFiles = $wp_filesystem->dirlist($remoteSource);\r
- if ( is_array($sourceFiles) ) {\r
- $sourceFiles = array_keys($sourceFiles);\r
- $firstFilePath = trailingslashit($remoteSource) . $sourceFiles[0];\r
- return (count($sourceFiles) > 1) || (!$wp_filesystem->is_dir($firstFilePath));\r
- }\r
-\r
- //Assume it's fine.\r
- return false;\r
- }\r
-\r
- /**\r
- * Is there and update being installed RIGHT NOW, for this specific plugin?\r
- *\r
- * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.\r
- * @return bool\r
- */\r
- public function isPluginBeingUpgraded($upgrader = null) {\r
- return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader);\r
- }\r
-\r
- /**\r
- * Get the details of the currently available update, if any.\r
- *\r
- * If no updates are available, or if the last known update version is below or equal\r
- * to the currently installed version, this method will return NULL.\r
- *\r
- * Uses cached update data. To retrieve update information straight from\r
- * the metadata URL, call requestUpdate() instead.\r
- *\r
- * @return PluginUpdate_3_1|null\r
- */\r
- public function getUpdate() {\r
- $state = $this->getUpdateState(); /** @var StdClass $state */\r
-\r
- //Is there an update available?\r
- if ( isset($state, $state->update) ) {\r
- $update = $state->update;\r
- //Check if the update is actually newer than the currently installed version.\r
- $installedVersion = $this->getInstalledVersion();\r
- if ( ($installedVersion !== null) && version_compare($update->version, $installedVersion, '>') ){\r
- $update->filename = $this->pluginFile;\r
- return $update;\r
- }\r
- }\r
- return null;\r
- }\r
-\r
- /**\r
- * Get a list of available translation updates.\r
- *\r
- * This method will return an empty array if there are no updates.\r
- * Uses cached update data.\r
- *\r
- * @return array\r
- */\r
- public function getTranslationUpdates() {\r
- $state = $this->getUpdateState();\r
- if ( isset($state, $state->update, $state->update->translations) ) {\r
- return $state->update->translations;\r
- }\r
- return array();\r
- }\r
-\r
- /**\r
- * Remove all cached translation updates.\r
- *\r
- * @see wp_clean_update_cache\r
- */\r
- public function clearCachedTranslationUpdates() {\r
- $state = $this->getUpdateState();\r
- if ( isset($state, $state->update, $state->update->translations) ) {\r
- $state->update->translations = array();\r
- $this->setUpdateState($state);\r
- }\r
- }\r
-\r
- /**\r
- * Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,\r
- * the new link will appear after the "Visit plugin site" link.\r
- *\r
- * You can change the link text by using the "puc_manual_check_link-$slug" filter.\r
- * Returning an empty string from the filter will disable the link.\r
- *\r
- * @param array $pluginMeta Array of meta links.\r
- * @param string $pluginFile\r
- * @return array\r
- */\r
- public function addCheckForUpdatesLink($pluginMeta, $pluginFile) {\r
- $isRelevant = ($pluginFile == $this->pluginFile)\r
- || (!empty($this->muPluginFile) && $pluginFile == $this->muPluginFile);\r
-\r
- if ( $isRelevant && current_user_can('update_plugins') ) {\r
- $linkUrl = wp_nonce_url(\r
- add_query_arg(\r
- array(\r
- 'puc_check_for_updates' => 1,\r
- 'puc_slug' => $this->slug,\r
- ),\r
- self_admin_url('plugins.php')\r
- ),\r
- 'puc_check_for_updates'\r
- );\r
-\r
- $linkText = apply_filters('puc_manual_check_link-' . $this->slug, __('Check for updates', 'plugin-update-checker'));\r
- if ( !empty($linkText) ) {\r
- $pluginMeta[] = sprintf('<a href="%s">%s</a>', esc_attr($linkUrl), $linkText);\r
- }\r
- }\r
- return $pluginMeta;\r
- }\r
-\r
- /**\r
- * Check for updates when the user clicks the "Check for updates" link.\r
- * @see self::addCheckForUpdatesLink()\r
- *\r
- * @return void\r
- */\r
- public function handleManualCheck() {\r
- $shouldCheck =\r
- isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])\r
- && $_GET['puc_slug'] == $this->slug\r
- && current_user_can('update_plugins')\r
- && check_admin_referer('puc_check_for_updates');\r
-\r
- if ( $shouldCheck ) {\r
- $update = $this->checkForUpdates();\r
- $status = ($update === null) ? 'no_update' : 'update_available';\r
- wp_redirect(add_query_arg(\r
- array(\r
- 'puc_update_check_result' => $status,\r
- 'puc_slug' => $this->slug,\r
- ),\r
- self_admin_url('plugins.php')\r
- ));\r
- }\r
- }\r
-\r
- /**\r
- * Display the results of a manual update check.\r
- * @see self::handleManualCheck()\r
- *\r
- * You can change the result message by using the "puc_manual_check_message-$slug" filter.\r
- */\r
- public function displayManualCheckResult() {\r
- if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->slug) ) {\r
- $status = strval($_GET['puc_update_check_result']);\r
- if ( $status == 'no_update' ) {\r
- $message = __('This plugin is up to date.', 'plugin-update-checker');\r
- } else if ( $status == 'update_available' ) {\r
- $message = __('A new version of this plugin is available.', 'plugin-update-checker');\r
- } else {\r
- $message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), htmlentities($status));\r
- }\r
- printf(\r
- '<div class="updated notice is-dismissible"><p>%s</p></div>',\r
- apply_filters('puc_manual_check_message-' . $this->slug, $message, $status)\r
- );\r
- }\r
- }\r
-\r
- /**\r
- * Check if the plugin file is inside the mu-plugins directory.\r
- *\r
- * @return bool\r
- */\r
- protected function isMuPlugin() {\r
- static $cachedResult = null;\r
-\r
- if ( $cachedResult === null ) {\r
- //Convert both paths to the canonical form before comparison.\r
- $muPluginDir = realpath(WPMU_PLUGIN_DIR);\r
- $pluginPath = realpath($this->pluginAbsolutePath);\r
-\r
- $cachedResult = (strpos($pluginPath, $muPluginDir) === 0);\r
- }\r
-\r
- return $cachedResult;\r
- }\r
-\r
- /**\r
- * MU plugins are partially supported, but only when we know which file in mu-plugins\r
- * corresponds to this plugin.\r
- *\r
- * @return bool\r
- */\r
- protected function isUnknownMuPlugin() {\r
- return empty($this->muPluginFile) && $this->isMuPlugin();\r
- }\r
-\r
- /**\r
- * Clear the cached plugin version. This method can be set up as a filter (hook) and will\r
- * return the filter argument unmodified.\r
- *\r
- * @param mixed $filterArgument\r
- * @return mixed\r
- */\r
- public function clearCachedVersion($filterArgument = null) {\r
- $this->cachedInstalledVersion = null;\r
- return $filterArgument;\r
- }\r
-\r
- /**\r
- * Register a callback for filtering query arguments.\r
- *\r
- * The callback function should take one argument - an associative array of query arguments.\r
- * It should return a modified array of query arguments.\r
- *\r
- * @uses add_filter() This method is a convenience wrapper for add_filter().\r
- *\r
- * @param callable $callback\r
- * @return void\r
- */\r
- public function addQueryArgFilter($callback){\r
- add_filter('puc_request_info_query_args-'.$this->slug, $callback);\r
- }\r
-\r
- /**\r
- * Register a callback for filtering arguments passed to wp_remote_get().\r
- *\r
- * The callback function should take one argument - an associative array of arguments -\r
- * and return a modified array or arguments. See the WP documentation on wp_remote_get()\r
- * for details on what arguments are available and how they work.\r
- *\r
- * @uses add_filter() This method is a convenience wrapper for add_filter().\r
- *\r
- * @param callable $callback\r
- * @return void\r
- */\r
- public function addHttpRequestArgFilter($callback){\r
- add_filter('puc_request_info_options-'.$this->slug, $callback);\r
- }\r
-\r
- /**\r
- * Register a callback for filtering the plugin info retrieved from the external API.\r
- *\r
- * The callback function should take two arguments. If the plugin info was retrieved\r
- * successfully, the first argument passed will be an instance of PluginInfo. Otherwise,\r
- * it will be NULL. The second argument will be the corresponding return value of\r
- * wp_remote_get (see WP docs for details).\r
- *\r
- * The callback function should return a new or modified instance of PluginInfo or NULL.\r
- *\r
- * @uses add_filter() This method is a convenience wrapper for add_filter().\r
- *\r
- * @param callable $callback\r
- * @return void\r
- */\r
- public function addResultFilter($callback){\r
- add_filter('puc_request_info_result-'.$this->slug, $callback, 10, 2);\r
- }\r
-\r
- /**\r
- * Register a callback for one of the update checker filters.\r
- *\r
- * Identical to add_filter(), except it automatically adds the "puc_" prefix\r
- * and the "-$plugin_slug" suffix to the filter name. For example, "request_info_result"\r
- * becomes "puc_request_info_result-your_plugin_slug".\r
- *\r
- * @param string $tag\r
- * @param callable $callback\r
- * @param int $priority\r
- * @param int $acceptedArgs\r
- */\r
- public function addFilter($tag, $callback, $priority = 10, $acceptedArgs = 1) {\r
- add_filter('puc_' . $tag . '-' . $this->slug, $callback, $priority, $acceptedArgs);\r
- }\r
-\r
- /**\r
- * Initialize the update checker Debug Bar plugin/add-on thingy.\r
- */\r
- public function initDebugBarPanel() {\r
- $debugBarPlugin = dirname(__FILE__) . '/debug-bar-plugin.php';\r
- if ( class_exists('Debug_Bar', false) && file_exists($debugBarPlugin) ) {\r
- /** @noinspection PhpIncludeInspection */\r
- require_once $debugBarPlugin;\r
- $this->debugBarPlugin = new PucDebugBarPlugin_3_1($this);\r
- }\r
- }\r
-\r
- /**\r
- * Trigger a PHP error, but only when $debugMode is enabled.\r
- *\r
- * @param string $message\r
- * @param int $errorType\r
- */\r
- protected function triggerError($message, $errorType) {\r
- if ( $this->debugMode ) {\r
- trigger_error($message, $errorType);\r
- }\r
- }\r
+ public $scheduler;\r
+\r
+ protected $upgraderStatus;\r
+\r
+ private $debugBarPlugin = null;\r
+ private $cachedInstalledVersion = null;\r
+\r
+ private $metadataHost = ''; //The host component of $metadataUrl.\r
+\r
+ /**\r
+ * Class constructor.\r
+ *\r
+ * @param string $metadataUrl The URL of the plugin's metadata file.\r
+ * @param string $pluginFile Fully qualified path to the main plugin file.\r
+ * @param string $slug The plugin's 'slug'. If not specified, the filename part of $pluginFile sans '.php' will be used as the slug.\r
+ * @param integer $checkPeriod How often to check for updates (in hours). Defaults to checking every 12 hours. Set to 0 to disable automatic update checks.\r
+ * @param string $optionName Where to store book-keeping info about update checks. Defaults to 'external_updates-$slug'.\r
+ * @param string $muPluginFile Optional. The plugin filename relative to the mu-plugins directory.\r
+ */\r
+ public function __construct($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = ''){\r
+ $this->metadataUrl = $metadataUrl;\r
+ $this->pluginAbsolutePath = $pluginFile;\r
+ $this->pluginFile = plugin_basename($this->pluginAbsolutePath);\r
+ $this->muPluginFile = $muPluginFile;\r
+ $this->slug = $slug;\r
+ $this->optionName = $optionName;\r
+ $this->debugMode = (bool)(constant('WP_DEBUG'));\r
+\r
+ //If no slug is specified, use the name of the main plugin file as the slug.\r
+ //For example, 'my-cool-plugin/cool-plugin.php' becomes 'cool-plugin'.\r
+ if ( empty($this->slug) ){\r
+ $this->slug = basename($this->pluginFile, '.php');\r
+ }\r
+\r
+ if ( empty($this->optionName) ){\r
+ $this->optionName = 'external_updates-' . $this->slug;\r
+ }\r
+\r
+ //Backwards compatibility: If the plugin is a mu-plugin but no $muPluginFile is specified, assume\r
+ //it's the same as $pluginFile given that it's not in a subdirectory (WP only looks in the base dir).\r
+ if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) {\r
+ $this->muPluginFile = $this->pluginFile;\r
+ }\r
+\r
+ $this->scheduler = $this->createScheduler($checkPeriod);\r
+ $this->upgraderStatus = new PucUpgraderStatus_3_1();\r
+\r
+ $this->installHooks();\r
+ }\r
+\r
+ /**\r
+ * Create an instance of the scheduler.\r
+ *\r
+ * This is implemented as a method to make it possible for plugins to subclass the update checker\r
+ * and substitute their own scheduler.\r
+ *\r
+ * @param int $checkPeriod\r
+ * @return PucScheduler_3_1\r
+ */\r
+ protected function createScheduler($checkPeriod) {\r
+ return new PucScheduler_3_1($this, $checkPeriod);\r
+ }\r
+\r
+ /**\r
+ * Install the hooks required to run periodic update checks and inject update info\r
+ * into WP data structures.\r
+ *\r
+ * @return void\r
+ */\r
+ protected function installHooks(){\r
+ //Override requests for plugin information\r
+ add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);\r
+\r
+ //Insert our update info into the update array maintained by WP.\r
+ add_filter('site_transient_update_plugins', array($this,'injectUpdate')); //WP 3.0+\r
+ add_filter('transient_update_plugins', array($this,'injectUpdate')); //WP 2.8+\r
+ add_filter('site_transient_update_plugins', array($this, 'injectTranslationUpdates'));\r
+\r
+ add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);\r
+ add_action('admin_init', array($this, 'handleManualCheck'));\r
+ add_action('all_admin_notices', array($this, 'displayManualCheckResult'));\r
+\r
+ //Clear the version number cache when something - anything - is upgraded or WP clears the update cache.\r
+ add_filter('upgrader_post_install', array($this, 'clearCachedVersion'));\r
+ add_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));\r
+ //Clear translation updates when WP clears the update cache.\r
+ //This needs to be done directly because the library doesn't actually remove obsolete plugin updates,\r
+ //it just hides them (see getUpdate()). We can't do that with translations - too much disk I/O.\r
+ add_action('delete_site_transient_update_plugins', array($this, 'clearCachedTranslationUpdates'));\r
+\r
+ if ( did_action('plugins_loaded') ) {\r
+ $this->initDebugBarPanel();\r
+ } else {\r
+ add_action('plugins_loaded', array($this, 'initDebugBarPanel'));\r
+ }\r
+\r
+ //Rename the update directory to be the same as the existing directory.\r
+ add_filter('upgrader_source_selection', array($this, 'fixDirectoryName'), 10, 3);\r
+\r
+ //Enable language support (i18n).\r
+ load_plugin_textdomain('plugin-update-checker', false, plugin_basename(dirname(__FILE__)) . '/languages');\r
+\r
+ //Allow HTTP requests to the metadata URL even if it's on a local host.\r
+ $this->metadataHost = @parse_url($this->metadataUrl, PHP_URL_HOST);\r
+ add_filter('http_request_host_is_external', array($this, 'allowMetadataHost'), 10, 2);\r
+ }\r
+\r
+ /**\r
+ * Explicitly allow HTTP requests to the metadata URL.\r
+ *\r
+ * WordPress has a security feature where the HTTP API will reject all requests that are sent to\r
+ * another site hosted on the same server as the current site (IP match), a local host, or a local\r
+ * IP, unless the host exactly matches the current site.\r
+ *\r
+ * This feature is opt-in (at least in WP 4.4). Apparently some people enable it.\r
+ *\r
+ * That can be a problem when you're developing your plugin and you decide to host the update information\r
+ * on the same server as your test site. Update requests will mysteriously fail.\r
+ *\r
+ * We fix that by adding an exception for the metadata host.\r
+ *\r
+ * @param bool $allow\r
+ * @param string $host\r
+ * @return bool\r
+ */\r
+ public function allowMetadataHost($allow, $host) {\r
+ if ( strtolower($host) === strtolower($this->metadataHost) ) {\r
+ return true;\r
+ }\r
+ return $allow;\r
+ }\r
+\r
+ /**\r
+ * Retrieve plugin info from the configured API endpoint.\r
+ *\r
+ * @uses wp_remote_get()\r
+ *\r
+ * @param array $queryArgs Additional query arguments to append to the request. Optional.\r
+ * @return PluginInfo_3_1\r
+ */\r
+ public function requestInfo($queryArgs = array()){\r
+ //Query args to append to the URL. Plugins can add their own by using a filter callback (see addQueryArgFilter()).\r
+ $installedVersion = $this->getInstalledVersion();\r
+ $queryArgs['installed_version'] = ($installedVersion !== null) ? $installedVersion : '';\r
+ $queryArgs = apply_filters('puc_request_info_query_args-'.$this->slug, $queryArgs);\r
+\r
+ //Various options for the wp_remote_get() call. Plugins can filter these, too.\r
+ $options = array(\r
+ 'timeout' => 10, //seconds\r
+ 'headers' => array(\r
+ 'Accept' => 'application/json'\r
+ ),\r
+ );\r
+ $options = apply_filters('puc_request_info_options-'.$this->slug, $options);\r
+\r
+ //The plugin info should be at 'http://your-api.com/url/here/$slug/info.json'\r
+ $url = $this->metadataUrl;\r
+ if ( !empty($queryArgs) ){\r
+ $url = add_query_arg($queryArgs, $url);\r
+ }\r
+\r
+ $result = wp_remote_get(\r
+ $url,\r
+ $options\r
+ );\r
+\r
+ //Try to parse the response\r
+ $status = $this->validateApiResponse($result);\r
+ $pluginInfo = null;\r
+ if ( !is_wp_error($status) ){\r
+ $pluginInfo = PluginInfo_3_1::fromJson($result['body']);\r
+ if ( $pluginInfo !== null ) {\r
+ $pluginInfo->filename = $this->pluginFile;\r
+ $pluginInfo->slug = $this->slug;\r
+ }\r
+ } else {\r
+ $this->triggerError(\r
+ sprintf('The URL %s does not point to a valid plugin metadata file. ', $url)\r
+ . $status->get_error_message(),\r
+ E_USER_WARNING\r
+ );\r
+ }\r
+\r
+ $pluginInfo = apply_filters('puc_request_info_result-'.$this->slug, $pluginInfo, $result);\r
+ return $pluginInfo;\r
+ }\r
+\r
+ /**\r
+ * Check if $result is a successful update API response.\r
+ *\r
+ * @param array|WP_Error $result\r
+ * @return true|WP_Error\r
+ */\r
+ private function validateApiResponse($result) {\r
+\r
+ if ( is_wp_error($result) ) { /** @var WP_Error $result */\r
+ return new WP_Error($result->get_error_code(), 'WP HTTP Error: ' . $result->get_error_message());\r
+ }\r
+\r
+ if ( !isset($result['response']['code']) ) {\r
+ return new WP_Error('puc_no_response_code', 'wp_remote_get() returned an unexpected result.');\r
+ }\r
+\r
+ if ( $result['response']['code'] !== 200 ) {\r
+ return new WP_Error(\r
+ 'puc_unexpected_response_code',\r
+ 'HTTP response code is ' . $result['response']['code'] . ' (expected: 200)'\r
+ );\r
+ }\r
+\r
+ if ( empty($result['body']) ) {\r
+ return new WP_Error('puc_empty_response', 'The metadata file appears to be empty.');\r
+ }\r
+\r
+ return true;\r
+ }\r
+\r
+ /**\r
+ * Retrieve the latest update (if any) from the configured API endpoint.\r
+ *\r
+ * @uses PluginUpdateChecker::requestInfo()\r
+ *\r
+ * @return PluginUpdate_3_1 An instance of PluginUpdate, or NULL when no updates are available.\r
+ */\r
+ public function requestUpdate(){\r
+ //For the sake of simplicity, this function just calls requestInfo()\r
+ //and transforms the result accordingly.\r
+ $pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));\r
+ if ( $pluginInfo == null ){\r
+ return null;\r
+ }\r
+ $update = PluginUpdate_3_1::fromPluginInfo($pluginInfo);\r
+\r
+ //Keep only those translation updates that apply to this site.\r
+ $update->translations = $this->filterApplicableTranslations($update->translations);\r
+\r
+ return $update;\r
+ }\r
+\r
+ /**\r
+ * Filter a list of translation updates and return a new list that contains only updates\r
+ * that apply to the current site.\r
+ *\r
+ * @param array $translations\r
+ * @return array\r
+ */\r
+ private function filterApplicableTranslations($translations) {\r
+ $languages = array_flip(array_values(get_available_languages()));\r
+ $installedTranslations = wp_get_installed_translations('plugins');\r
+ if ( isset($installedTranslations[$this->slug]) ) {\r
+ $installedTranslations = $installedTranslations[$this->slug];\r
+ } else {\r
+ $installedTranslations = array();\r
+ }\r
+\r
+ $applicableTranslations = array();\r
+ foreach($translations as $translation) {\r
+ //Does it match one of the available core languages?\r
+ $isApplicable = array_key_exists($translation->language, $languages);\r
+ //Is it more recent than an already-installed translation?\r
+ if ( isset($installedTranslations[$translation->language]) ) {\r
+ $updateTimestamp = strtotime($translation->updated);\r
+ $installedTimestamp = strtotime($installedTranslations[$translation->language]['PO-Revision-Date']);\r
+ $isApplicable = $updateTimestamp > $installedTimestamp;\r
+ }\r
+\r
+ if ( $isApplicable ) {\r
+ $applicableTranslations[] = $translation;\r
+ }\r
+ }\r
+\r
+ return $applicableTranslations;\r
+ }\r
+\r
+ /**\r
+ * Get the currently installed version of the plugin.\r
+ *\r
+ * @return string Version number.\r
+ */\r
+ public function getInstalledVersion(){\r
+ if ( isset($this->cachedInstalledVersion) ) {\r
+ return $this->cachedInstalledVersion;\r
+ }\r
+\r
+ $pluginHeader = $this->getPluginHeader();\r
+ if ( isset($pluginHeader['Version']) ) {\r
+ $this->cachedInstalledVersion = $pluginHeader['Version'];\r
+ return $pluginHeader['Version'];\r
+ } else {\r
+ //This can happen if the filename points to something that is not a plugin.\r
+ $this->triggerError(\r
+ sprintf(\r
+ "Can't to read the Version header for '%s'. The filename is incorrect or is not a plugin.",\r
+ $this->pluginFile\r
+ ),\r
+ E_USER_WARNING\r
+ );\r
+ return null;\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Get plugin's metadata from its file header.\r
+ *\r
+ * @return array\r
+ */\r
+ protected function getPluginHeader() {\r
+ if ( !is_file($this->pluginAbsolutePath) ) {\r
+ //This can happen if the plugin filename is wrong.\r
+ $this->triggerError(\r
+ sprintf(\r
+ "Can't to read the plugin header for '%s'. The file does not exist.",\r
+ $this->pluginFile\r
+ ),\r
+ E_USER_WARNING\r
+ );\r
+ return array();\r
+ }\r
+\r
+ if ( !function_exists('get_plugin_data') ){\r
+ /** @noinspection PhpIncludeInspection */\r
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );\r
+ }\r
+ return get_plugin_data($this->pluginAbsolutePath, false, false);\r
+ }\r
+\r
+ /**\r
+ * Check for plugin updates.\r
+ * The results are stored in the DB option specified in $optionName.\r
+ *\r
+ * @return PluginUpdate_3_1|null\r
+ */\r
+ public function checkForUpdates(){\r
+ $installedVersion = $this->getInstalledVersion();\r
+ //Fail silently if we can't find the plugin or read its header.\r
+ if ( $installedVersion === null ) {\r
+ $this->triggerError(\r
+ sprintf('Skipping update check for %s - installed version unknown.', $this->pluginFile),\r
+ E_USER_WARNING\r
+ );\r
+ return null;\r
+ }\r
+\r
+ $state = $this->getUpdateState();\r
+ if ( empty($state) ){\r
+ $state = new stdClass;\r
+ $state->lastCheck = 0;\r
+ $state->checkedVersion = '';\r
+ $state->update = null;\r
+ }\r
+\r
+ $state->lastCheck = time();\r
+ $state->checkedVersion = $installedVersion;\r
+ $this->setUpdateState($state); //Save before checking in case something goes wrong\r
+\r
+ $state->update = $this->requestUpdate();\r
+ $this->setUpdateState($state);\r
+\r
+ return $this->getUpdate();\r
+ }\r
+\r
+ /**\r
+ * Load the update checker state from the DB.\r
+ *\r
+ * @return stdClass|null\r
+ */\r
+ public function getUpdateState() {\r
+ $state = get_site_option($this->optionName, null);\r
+ if ( empty($state) || !is_object($state)) {\r
+ $state = null;\r
+ }\r
+\r
+ if ( isset($state, $state->update) && is_object($state->update) ) {\r
+ $state->update = PluginUpdate_3_1::fromObject($state->update);\r
+ }\r
+ return $state;\r
+ }\r
+\r
+\r
+ /**\r
+ * Persist the update checker state to the DB.\r
+ *\r
+ * @param StdClass $state\r
+ * @return void\r
+ */\r
+ private function setUpdateState($state) {\r
+ if ( isset($state->update) && is_object($state->update) && method_exists($state->update, 'toStdClass') ) {\r
+ $update = $state->update; /** @var PluginUpdate_3_1 $update */\r
+ $state->update = $update->toStdClass();\r
+ }\r
+ update_site_option($this->optionName, $state);\r
+ }\r
+\r
+ /**\r
+ * Reset update checker state - i.e. last check time, cached update data and so on.\r
+ *\r
+ * Call this when your plugin is being uninstalled, or if you want to\r
+ * clear the update cache.\r
+ */\r
+ public function resetUpdateState() {\r
+ delete_site_option($this->optionName);\r
+ }\r
+\r
+ /**\r
+ * Intercept plugins_api() calls that request information about our plugin and\r
+ * use the configured API endpoint to satisfy them.\r
+ *\r
+ * @see plugins_api()\r
+ *\r
+ * @param mixed $result\r
+ * @param string $action\r
+ * @param array|object $args\r
+ * @return mixed\r
+ */\r
+ public function injectInfo($result, $action = null, $args = null){\r
+ $relevant = ($action == 'plugin_information') && isset($args->slug) && (\r
+ ($args->slug == $this->slug) || ($args->slug == dirname($this->pluginFile))\r
+ );\r
+ if ( !$relevant ) {\r
+ return $result;\r
+ }\r
+\r
+ $pluginInfo = $this->requestInfo();\r
+ $pluginInfo = apply_filters('puc_pre_inject_info-' . $this->slug, $pluginInfo);\r
+ if ( $pluginInfo ) {\r
+ return $pluginInfo->toWpFormat();\r
+ }\r
+\r
+ return $result;\r
+ }\r
+\r
+ /**\r
+ * Insert the latest update (if any) into the update list maintained by WP.\r
+ *\r
+ * @param StdClass $updates Update list.\r
+ * @return StdClass Modified update list.\r
+ */\r
+ public function injectUpdate($updates){\r
+ //Is there an update to insert?\r
+ $update = $this->getUpdate();\r
+\r
+ //No update notifications for mu-plugins unless explicitly enabled. The MU plugin file\r
+ //is usually different from the main plugin file so the update wouldn't show up properly anyway.\r
+ if ( $this->isUnknownMuPlugin() ) {\r
+ $update = null;\r
+ }\r
+\r
+ if ( !empty($update) ) {\r
+ //Let plugins filter the update info before it's passed on to WordPress.\r
+ $update = apply_filters('puc_pre_inject_update-' . $this->slug, $update);\r
+ $updates = $this->addUpdateToList($updates, $update);\r
+ } else {\r
+ //Clean up any stale update info.\r
+ $updates = $this->removeUpdateFromList($updates);\r
+ }\r
+\r
+ return $updates;\r
+ }\r
+\r
+ /**\r
+ * @param StdClass|null $updates\r
+ * @param PluginUpdate_3_1 $updateToAdd\r
+ * @return StdClass\r
+ */\r
+ private function addUpdateToList($updates, $updateToAdd) {\r
+ if ( !is_object($updates) ) {\r
+ $updates = new stdClass();\r
+ $updates->response = array();\r
+ }\r
+\r
+ $wpUpdate = $updateToAdd->toWpFormat();\r
+ $pluginFile = $this->pluginFile;\r
+\r
+ if ( $this->isMuPlugin() ) {\r
+ //WP does not support automatic update installation for mu-plugins, but we can still display a notice.\r
+ $wpUpdate->package = null;\r
+ $pluginFile = $this->muPluginFile;\r
+ }\r
+ $updates->response[$pluginFile] = $wpUpdate;\r
+ return $updates;\r
+ }\r
+\r
+ /**\r
+ * @param stdClass|null $updates\r
+ * @return stdClass|null\r
+ */\r
+ private function removeUpdateFromList($updates) {\r
+ if ( isset($updates, $updates->response) ) {\r
+ unset($updates->response[$this->pluginFile]);\r
+ if ( !empty($this->muPluginFile) ) {\r
+ unset($updates->response[$this->muPluginFile]);\r
+ }\r
+ }\r
+ return $updates;\r
+ }\r
+\r
+ /**\r
+ * Insert translation updates into the list maintained by WordPress.\r
+ *\r
+ * @param stdClass $updates\r
+ * @return stdClass\r
+ */\r
+ public function injectTranslationUpdates($updates) {\r
+ $translationUpdates = $this->getTranslationUpdates();\r
+ if ( empty($translationUpdates) ) {\r
+ return $updates;\r
+ }\r
+\r
+ //Being defensive.\r
+ if ( !is_object($updates) ) {\r
+ $updates = new stdClass();\r
+ }\r
+ if ( !isset($updates->translations) ) {\r
+ $updates->translations = array();\r
+ }\r
+\r
+ //In case there's a name collision with a plugin hosted on wordpress.org,\r
+ //remove any preexisting updates that match our plugin.\r
+ $translationType = 'plugin';\r
+ $filteredTranslations = array();\r
+ foreach($updates->translations as $translation) {\r
+ if ( ($translation['type'] === $translationType) && ($translation['slug'] === $this->slug) ) {\r
+ continue;\r
+ }\r
+ $filteredTranslations[] = $translation;\r
+ }\r
+ $updates->translations = $filteredTranslations;\r
+\r
+ //Add our updates to the list.\r
+ foreach($translationUpdates as $update) {\r
+ $convertedUpdate = array_merge(\r
+ array(\r
+ 'type' => $translationType,\r
+ 'slug' => $this->slug,\r
+ 'autoupdate' => 0,\r
+ //AFAICT, WordPress doesn't actually use the "version" field for anything.\r
+ //But lets make sure it's there, just in case.\r
+ 'version' => isset($update->version) ? $update->version : ('1.' . strtotime($update->updated)),\r
+ ),\r
+ (array)$update\r
+ );\r
+\r
+ $updates->translations[] = $convertedUpdate;\r
+ }\r
+\r
+ return $updates;\r
+ }\r
+\r
+ /**\r
+ * Rename the update directory to match the existing plugin directory.\r
+ *\r
+ * When WordPress installs a plugin or theme update, it assumes that the ZIP file will contain\r
+ * exactly one directory, and that the directory name will be the same as the directory where\r
+ * the plugin/theme is currently installed.\r
+ *\r
+ * GitHub and other repositories provide ZIP downloads, but they often use directory names like\r
+ * "project-branch" or "project-tag-hash". We need to change the name to the actual plugin folder.\r
+ *\r
+ * This is a hook callback. Don't call it from a plugin.\r
+ *\r
+ * @param string $source The directory to copy to /wp-content/plugins. Usually a subdirectory of $remoteSource.\r
+ * @param string $remoteSource WordPress has extracted the update to this directory.\r
+ * @param WP_Upgrader $upgrader\r
+ * @return string|WP_Error\r
+ */\r
+ public function fixDirectoryName($source, $remoteSource, $upgrader) {\r
+ global $wp_filesystem; /** @var WP_Filesystem_Base $wp_filesystem */\r
+\r
+ //Basic sanity checks.\r
+ if ( !isset($source, $remoteSource, $upgrader, $upgrader->skin, $wp_filesystem) ) {\r
+ return $source;\r
+ }\r
+\r
+ //If WordPress is upgrading anything other than our plugin, leave the directory name unchanged.\r
+ if ( !$this->isPluginBeingUpgraded($upgrader) ) {\r
+ return $source;\r
+ }\r
+\r
+ //Rename the source to match the existing plugin directory.\r
+ $pluginDirectoryName = dirname($this->pluginFile);\r
+ if ( $pluginDirectoryName === '.' ) {\r
+ return $source;\r
+ }\r
+ $correctedSource = trailingslashit($remoteSource) . $pluginDirectoryName . '/';\r
+ if ( $source !== $correctedSource ) {\r
+ //The update archive should contain a single directory that contains the rest of plugin files. Otherwise,\r
+ //WordPress will try to copy the entire working directory ($source == $remoteSource). We can't rename\r
+ //$remoteSource because that would break WordPress code that cleans up temporary files after update.\r
+ if ( $this->isBadDirectoryStructure($remoteSource) ) {\r
+ return new WP_Error(\r
+ 'puc-incorrect-directory-structure',\r
+ sprintf(\r
+ 'The directory structure of the update is incorrect. All plugin files should be inside ' .\r
+ 'a directory named <span class="code">%s</span>, not at the root of the ZIP file.',\r
+ htmlentities($this->slug)\r
+ )\r
+ );\r
+ }\r
+\r
+ /** @var WP_Upgrader_Skin $upgrader->skin */\r
+ $upgrader->skin->feedback(sprintf(\r
+ 'Renaming %s to %s…',\r
+ '<span class="code">' . basename($source) . '</span>',\r
+ '<span class="code">' . $pluginDirectoryName . '</span>'\r
+ ));\r
+\r
+ if ( $wp_filesystem->move($source, $correctedSource, true) ) {\r
+ $upgrader->skin->feedback('Plugin directory successfully renamed.');\r
+ return $correctedSource;\r
+ } else {\r
+ return new WP_Error(\r
+ 'puc-rename-failed',\r
+ 'Unable to rename the update to match the existing plugin directory.'\r
+ );\r
+ }\r
+ }\r
+\r
+ return $source;\r
+ }\r
+\r
+ /**\r
+ * Check for incorrect update directory structure. An update must contain a single directory,\r
+ * all other files should be inside that directory.\r
+ *\r
+ * @param string $remoteSource Directory path.\r
+ * @return bool\r
+ */\r
+ private function isBadDirectoryStructure($remoteSource) {\r
+ global $wp_filesystem; /** @var WP_Filesystem_Base $wp_filesystem */\r
+\r
+ $sourceFiles = $wp_filesystem->dirlist($remoteSource);\r
+ if ( is_array($sourceFiles) ) {\r
+ $sourceFiles = array_keys($sourceFiles);\r
+ $firstFilePath = trailingslashit($remoteSource) . $sourceFiles[0];\r
+ return (count($sourceFiles) > 1) || (!$wp_filesystem->is_dir($firstFilePath));\r
+ }\r
+\r
+ //Assume it's fine.\r
+ return false;\r
+ }\r
+\r
+ /**\r
+ * Is there and update being installed RIGHT NOW, for this specific plugin?\r
+ *\r
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.\r
+ * @return bool\r
+ */\r
+ public function isPluginBeingUpgraded($upgrader = null) {\r
+ return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader);\r
+ }\r
+\r
+ /**\r
+ * Get the details of the currently available update, if any.\r
+ *\r
+ * If no updates are available, or if the last known update version is below or equal\r
+ * to the currently installed version, this method will return NULL.\r
+ *\r
+ * Uses cached update data. To retrieve update information straight from\r
+ * the metadata URL, call requestUpdate() instead.\r
+ *\r
+ * @return PluginUpdate_3_1|null\r
+ */\r
+ public function getUpdate() {\r
+ $state = $this->getUpdateState(); /** @var StdClass $state */\r
+\r
+ //Is there an update available?\r
+ if ( isset($state, $state->update) ) {\r
+ $update = $state->update;\r
+ //Check if the update is actually newer than the currently installed version.\r
+ $installedVersion = $this->getInstalledVersion();\r
+ if ( ($installedVersion !== null) && version_compare($update->version, $installedVersion, '>') ){\r
+ $update->filename = $this->pluginFile;\r
+ return $update;\r
+ }\r
+ }\r
+ return null;\r
+ }\r
+\r
+ /**\r
+ * Get a list of available translation updates.\r
+ *\r
+ * This method will return an empty array if there are no updates.\r
+ * Uses cached update data.\r
+ *\r
+ * @return array\r
+ */\r
+ public function getTranslationUpdates() {\r
+ $state = $this->getUpdateState();\r
+ if ( isset($state, $state->update, $state->update->translations) ) {\r
+ return $state->update->translations;\r
+ }\r
+ return array();\r
+ }\r
+\r
+ /**\r
+ * Remove all cached translation updates.\r
+ *\r
+ * @see wp_clean_update_cache\r
+ */\r
+ public function clearCachedTranslationUpdates() {\r
+ $state = $this->getUpdateState();\r
+ if ( isset($state, $state->update, $state->update->translations) ) {\r
+ $state->update->translations = array();\r
+ $this->setUpdateState($state);\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,\r
+ * the new link will appear after the "Visit plugin site" link.\r
+ *\r
+ * You can change the link text by using the "puc_manual_check_link-$slug" filter.\r
+ * Returning an empty string from the filter will disable the link.\r
+ *\r
+ * @param array $pluginMeta Array of meta links.\r
+ * @param string $pluginFile\r
+ * @return array\r
+ */\r
+ public function addCheckForUpdatesLink($pluginMeta, $pluginFile) {\r
+ $isRelevant = ($pluginFile == $this->pluginFile)\r
+ || (!empty($this->muPluginFile) && $pluginFile == $this->muPluginFile);\r
+\r
+ if ( $isRelevant && current_user_can('update_plugins') ) {\r
+ $linkUrl = wp_nonce_url(\r
+ add_query_arg(\r
+ array(\r
+ 'puc_check_for_updates' => 1,\r
+ 'puc_slug' => $this->slug,\r
+ ),\r
+ self_admin_url('plugins.php')\r
+ ),\r
+ 'puc_check_for_updates'\r
+ );\r
+\r
+ $linkText = apply_filters('puc_manual_check_link-' . $this->slug, __('Check for updates', 'plugin-update-checker'));\r
+ if ( !empty($linkText) ) {\r
+ $pluginMeta[] = sprintf('<a href="%s">%s</a>', esc_attr($linkUrl), $linkText);\r
+ }\r
+ }\r
+ return $pluginMeta;\r
+ }\r
+\r
+ /**\r
+ * Check for updates when the user clicks the "Check for updates" link.\r
+ * @see self::addCheckForUpdatesLink()\r
+ *\r
+ * @return void\r
+ */\r
+ public function handleManualCheck() {\r
+ $shouldCheck =\r
+ isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])\r
+ && $_GET['puc_slug'] == $this->slug\r
+ && current_user_can('update_plugins')\r
+ && check_admin_referer('puc_check_for_updates');\r
+\r
+ if ( $shouldCheck ) {\r
+ $update = $this->checkForUpdates();\r
+ $status = ($update === null) ? 'no_update' : 'update_available';\r
+ wp_redirect(add_query_arg(\r
+ array(\r
+ 'puc_update_check_result' => $status,\r
+ 'puc_slug' => $this->slug,\r
+ ),\r
+ self_admin_url('plugins.php')\r
+ ));\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Display the results of a manual update check.\r
+ * @see self::handleManualCheck()\r
+ *\r
+ * You can change the result message by using the "puc_manual_check_message-$slug" filter.\r
+ */\r
+ public function displayManualCheckResult() {\r
+ if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->slug) ) {\r
+ $status = strval($_GET['puc_update_check_result']);\r
+ if ( $status == 'no_update' ) {\r
+ $message = __('This plugin is up to date.', 'plugin-update-checker');\r
+ } else if ( $status == 'update_available' ) {\r
+ $message = __('A new version of this plugin is available.', 'plugin-update-checker');\r
+ } else {\r
+ $message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), htmlentities($status));\r
+ }\r
+ printf(\r
+ '<div class="updated notice is-dismissible"><p>%s</p></div>',\r
+ apply_filters('puc_manual_check_message-' . $this->slug, $message, $status)\r
+ );\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Check if the plugin file is inside the mu-plugins directory.\r
+ *\r
+ * @return bool\r
+ */\r
+ protected function isMuPlugin() {\r
+ static $cachedResult = null;\r
+\r
+ if ( $cachedResult === null ) {\r
+ //Convert both paths to the canonical form before comparison.\r
+ $muPluginDir = realpath(WPMU_PLUGIN_DIR);\r
+ $pluginPath = realpath($this->pluginAbsolutePath);\r
+\r
+ $cachedResult = (strpos($pluginPath, $muPluginDir) === 0);\r
+ }\r
+\r
+ return $cachedResult;\r
+ }\r
+\r
+ /**\r
+ * MU plugins are partially supported, but only when we know which file in mu-plugins\r
+ * corresponds to this plugin.\r
+ *\r
+ * @return bool\r
+ */\r
+ protected function isUnknownMuPlugin() {\r
+ return empty($this->muPluginFile) && $this->isMuPlugin();\r
+ }\r
+\r
+ /**\r
+ * Clear the cached plugin version. This method can be set up as a filter (hook) and will\r
+ * return the filter argument unmodified.\r
+ *\r
+ * @param mixed $filterArgument\r
+ * @return mixed\r
+ */\r
+ public function clearCachedVersion($filterArgument = null) {\r
+ $this->cachedInstalledVersion = null;\r
+ return $filterArgument;\r
+ }\r
+\r
+ /**\r
+ * Register a callback for filtering query arguments.\r
+ *\r
+ * The callback function should take one argument - an associative array of query arguments.\r
+ * It should return a modified array of query arguments.\r
+ *\r
+ * @uses add_filter() This method is a convenience wrapper for add_filter().\r
+ *\r
+ * @param callable $callback\r
+ * @return void\r
+ */\r
+ public function addQueryArgFilter($callback){\r
+ add_filter('puc_request_info_query_args-'.$this->slug, $callback);\r
+ }\r
+\r
+ /**\r
+ * Register a callback for filtering arguments passed to wp_remote_get().\r
+ *\r
+ * The callback function should take one argument - an associative array of arguments -\r
+ * and return a modified array or arguments. See the WP documentation on wp_remote_get()\r
+ * for details on what arguments are available and how they work.\r
+ *\r
+ * @uses add_filter() This method is a convenience wrapper for add_filter().\r
+ *\r
+ * @param callable $callback\r
+ * @return void\r
+ */\r
+ public function addHttpRequestArgFilter($callback){\r
+ add_filter('puc_request_info_options-'.$this->slug, $callback);\r
+ }\r
+\r
+ /**\r
+ * Register a callback for filtering the plugin info retrieved from the external API.\r
+ *\r
+ * The callback function should take two arguments. If the plugin info was retrieved\r
+ * successfully, the first argument passed will be an instance of PluginInfo. Otherwise,\r
+ * it will be NULL. The second argument will be the corresponding return value of\r
+ * wp_remote_get (see WP docs for details).\r
+ *\r
+ * The callback function should return a new or modified instance of PluginInfo or NULL.\r
+ *\r
+ * @uses add_filter() This method is a convenience wrapper for add_filter().\r
+ *\r
+ * @param callable $callback\r
+ * @return void\r
+ */\r
+ public function addResultFilter($callback){\r
+ add_filter('puc_request_info_result-'.$this->slug, $callback, 10, 2);\r
+ }\r
+\r
+ /**\r
+ * Register a callback for one of the update checker filters.\r
+ *\r
+ * Identical to add_filter(), except it automatically adds the "puc_" prefix\r
+ * and the "-$plugin_slug" suffix to the filter name. For example, "request_info_result"\r
+ * becomes "puc_request_info_result-your_plugin_slug".\r
+ *\r
+ * @param string $tag\r
+ * @param callable $callback\r
+ * @param int $priority\r
+ * @param int $acceptedArgs\r
+ */\r
+ public function addFilter($tag, $callback, $priority = 10, $acceptedArgs = 1) {\r
+ add_filter('puc_' . $tag . '-' . $this->slug, $callback, $priority, $acceptedArgs);\r
+ }\r
+\r
+ /**\r
+ * Initialize the update checker Debug Bar plugin/add-on thingy.\r
+ */\r
+ public function initDebugBarPanel() {\r
+ $debugBarPlugin = dirname(__FILE__) . '/debug-bar-plugin.php';\r
+ if ( class_exists('Debug_Bar', false) && file_exists($debugBarPlugin) ) {\r
+ /** @noinspection PhpIncludeInspection */\r
+ require_once $debugBarPlugin;\r
+ $this->debugBarPlugin = new PucDebugBarPlugin_3_1($this);\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Trigger a PHP error, but only when $debugMode is enabled.\r
+ *\r
+ * @param string $message\r
+ * @param int $errorType\r
+ */\r
+ protected function triggerError($message, $errorType) {\r
+ if ( $this->debugMode ) {\r
+ trigger_error($message, $errorType);\r
+ }\r
+ }\r
}\r
\r
endif;\r
* @access public\r
*/\r
class PluginInfo_3_1 {\r
- //Most fields map directly to the contents of the plugin's info.json file.\r
- //See the relevant docs for a description of their meaning.\r
- public $name;\r
- public $slug;\r
- public $version;\r
- public $homepage;\r
- public $sections = array();\r
- public $banners;\r
- public $translations = array();\r
- public $download_url;\r
-\r
- public $author;\r
- public $author_homepage;\r
-\r
- public $requires;\r
- public $tested;\r
- public $upgrade_notice;\r
-\r
- public $rating;\r
- public $num_ratings;\r
- public $downloaded;\r
- public $active_installs;\r
- public $last_updated;\r
-\r
- public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything.\r
-\r
- public $filename; //Plugin filename relative to the plugins directory.\r
-\r
- /**\r
- * Create a new instance of PluginInfo from JSON-encoded plugin info\r
- * returned by an external update API.\r
- *\r
- * @param string $json Valid JSON string representing plugin info.\r
- * @return PluginInfo_3_1|null New instance of PluginInfo, or NULL on error.\r
- */\r
- public static function fromJson($json){\r
- /** @var StdClass $apiResponse */\r
- $apiResponse = json_decode($json);\r
- if ( empty($apiResponse) || !is_object($apiResponse) ){\r
- trigger_error(\r
- "Failed to parse plugin metadata. Try validating your .json file with http://jsonlint.com/\n".print_r($apiResponse,1),\r
- E_USER_NOTICE\r
- );\r
- return null;\r
- }\r
-\r
- $valid = self::validateMetadata($apiResponse);\r
- if ( is_wp_error($valid) ){\r
- trigger_error($valid->get_error_message(), E_USER_NOTICE);\r
- return null;\r
- }\r
-\r
- $info = new self();\r
- foreach(get_object_vars($apiResponse) as $key => $value){\r
- $info->$key = $value;\r
- }\r
-\r
- //json_decode decodes assoc. arrays as objects. We want it as an array.\r
- $info->sections = (array)$info->sections;\r
-\r
- return $info;\r
- }\r
-\r
- /**\r
- * Very, very basic validation.\r
- *\r
- * @param StdClass $apiResponse\r
- * @return bool|WP_Error\r
- */\r
- protected static function validateMetadata($apiResponse) {\r
- if (\r
- !isset($apiResponse->name, $apiResponse->version)\r
- || empty($apiResponse->name)\r
- || empty($apiResponse->version)\r
- ) {\r
- return new WP_Error(\r
- 'puc-invalid-metadata',\r
- "The plugin metadata file does not contain the required 'name' and/or 'version' keys."\r
- );\r
- }\r
- return true;\r
- }\r
-\r
-\r
- /**\r
- * Transform plugin info into the format used by the native WordPress.org API\r
- *\r
- * @return object\r
- */\r
- public function toWpFormat(){\r
- $info = new stdClass;\r
-\r
- //The custom update API is built so that many fields have the same name and format\r
- //as those returned by the native WordPress.org API. These can be assigned directly.\r
- $sameFormat = array(\r
- 'name', 'slug', 'version', 'requires', 'tested', 'rating', 'upgrade_notice',\r
- 'num_ratings', 'downloaded', 'active_installs', 'homepage', 'last_updated',\r
- );\r
- foreach($sameFormat as $field){\r
- if ( isset($this->$field) ) {\r
- $info->$field = $this->$field;\r
- } else {\r
- $info->$field = null;\r
- }\r
- }\r
-\r
- //Other fields need to be renamed and/or transformed.\r
- $info->download_link = $this->download_url;\r
- $info->author = $this->getFormattedAuthor();\r
- $info->sections = array_merge(array('description' => ''), $this->sections);\r
-\r
- if ( !empty($this->banners) ) {\r
- //WP expects an array with two keys: "high" and "low". Both are optional.\r
- //Docs: https://wordpress.org/plugins/about/faq/#banners\r
- $info->banners = is_object($this->banners) ? get_object_vars($this->banners) : $this->banners;\r
- $info->banners = array_intersect_key($info->banners, array('high' => true, 'low' => true));\r
- }\r
-\r
- return $info;\r
- }\r
-\r
- protected function getFormattedAuthor() {\r
- if ( !empty($this->author_homepage) ){\r
- return sprintf('<a href="%s">%s</a>', $this->author_homepage, $this->author);\r
- }\r
- return $this->author;\r
- }\r
+ //Most fields map directly to the contents of the plugin's info.json file.\r
+ //See the relevant docs for a description of their meaning.\r
+ public $name;\r
+ public $slug;\r
+ public $version;\r
+ public $homepage;\r
+ public $sections = array();\r
+ public $banners;\r
+ public $translations = array();\r
+ public $download_url;\r
+\r
+ public $author;\r
+ public $author_homepage;\r
+\r
+ public $requires;\r
+ public $tested;\r
+ public $upgrade_notice;\r
+\r
+ public $rating;\r
+ public $num_ratings;\r
+ public $downloaded;\r
+ public $active_installs;\r
+ public $last_updated;\r
+\r
+ public $id = 0; //The native WP.org API returns numeric plugin IDs, but they're not used for anything.\r
+\r
+ public $filename; //Plugin filename relative to the plugins directory.\r
+\r
+ /**\r
+ * Create a new instance of PluginInfo from JSON-encoded plugin info\r
+ * returned by an external update API.\r
+ *\r
+ * @param string $json Valid JSON string representing plugin info.\r
+ * @return PluginInfo_3_1|null New instance of PluginInfo, or NULL on error.\r
+ */\r
+ public static function fromJson($json){\r
+ /** @var StdClass $apiResponse */\r
+ $apiResponse = json_decode($json);\r
+ if ( empty($apiResponse) || !is_object($apiResponse) ){\r
+ trigger_error(\r
+ "Failed to parse plugin metadata. Try validating your .json file with http://jsonlint.com/\n".print_r($apiResponse,1),\r
+ E_USER_NOTICE\r
+ );\r
+ return null;\r
+ }\r
+\r
+ $valid = self::validateMetadata($apiResponse);\r
+ if ( is_wp_error($valid) ){\r
+ trigger_error($valid->get_error_message(), E_USER_NOTICE);\r
+ return null;\r
+ }\r
+\r
+ $info = new self();\r
+ foreach(get_object_vars($apiResponse) as $key => $value){\r
+ $info->$key = $value;\r
+ }\r
+\r
+ //json_decode decodes assoc. arrays as objects. We want it as an array.\r
+ $info->sections = (array)$info->sections;\r
+\r
+ return $info;\r
+ }\r
+\r
+ /**\r
+ * Very, very basic validation.\r
+ *\r
+ * @param StdClass $apiResponse\r
+ * @return bool|WP_Error\r
+ */\r
+ protected static function validateMetadata($apiResponse) {\r
+ if (\r
+ !isset($apiResponse->name, $apiResponse->version)\r
+ || empty($apiResponse->name)\r
+ || empty($apiResponse->version)\r
+ ) {\r
+ return new WP_Error(\r
+ 'puc-invalid-metadata',\r
+ "The plugin metadata file does not contain the required 'name' and/or 'version' keys."\r
+ );\r
+ }\r
+ return true;\r
+ }\r
+\r
+\r
+ /**\r
+ * Transform plugin info into the format used by the native WordPress.org API\r
+ *\r
+ * @return object\r
+ */\r
+ public function toWpFormat(){\r
+ $info = new stdClass;\r
+\r
+ //The custom update API is built so that many fields have the same name and format\r
+ //as those returned by the native WordPress.org API. These can be assigned directly.\r
+ $sameFormat = array(\r
+ 'name', 'slug', 'version', 'requires', 'tested', 'rating', 'upgrade_notice',\r
+ 'num_ratings', 'downloaded', 'active_installs', 'homepage', 'last_updated',\r
+ );\r
+ foreach($sameFormat as $field){\r
+ if ( isset($this->$field) ) {\r
+ $info->$field = $this->$field;\r
+ } else {\r
+ $info->$field = null;\r
+ }\r
+ }\r
+\r
+ //Other fields need to be renamed and/or transformed.\r
+ $info->download_link = $this->download_url;\r
+ $info->author = $this->getFormattedAuthor();\r
+ $info->sections = array_merge(array('description' => ''), $this->sections);\r
+\r
+ if ( !empty($this->banners) ) {\r
+ //WP expects an array with two keys: "high" and "low". Both are optional.\r
+ //Docs: https://wordpress.org/plugins/about/faq/#banners\r
+ $info->banners = is_object($this->banners) ? get_object_vars($this->banners) : $this->banners;\r
+ $info->banners = array_intersect_key($info->banners, array('high' => true, 'low' => true));\r
+ }\r
+\r
+ return $info;\r
+ }\r
+\r
+ protected function getFormattedAuthor() {\r
+ if ( !empty($this->author_homepage) ){\r
+ return sprintf('<a href="%s">%s</a>', $this->author_homepage, $this->author);\r
+ }\r
+ return $this->author;\r
+ }\r
}\r
\r
endif;\r
* @access public\r
*/\r
class PluginUpdate_3_1 {\r
- public $id = 0;\r
- public $slug;\r
- public $version;\r
- public $homepage;\r
- public $download_url;\r
- public $upgrade_notice;\r
- public $tested;\r
- public $translations = array();\r
- public $filename; //Plugin filename relative to the plugins directory.\r
-\r
- private static $fields = array(\r
- 'id', 'slug', 'version', 'homepage', 'tested',\r
- 'download_url', 'upgrade_notice', 'filename',\r
- 'translations'\r
- );\r
-\r
- /**\r
- * Create a new instance of PluginUpdate from its JSON-encoded representation.\r
- *\r
- * @param string $json\r
- * @return PluginUpdate_3_1|null\r
- */\r
- public static function fromJson($json){\r
- //Since update-related information is simply a subset of the full plugin info,\r
- //we can parse the update JSON as if it was a plugin info string, then copy over\r
- //the parts that we care about.\r
- $pluginInfo = PluginInfo_3_1::fromJson($json);\r
- if ( $pluginInfo != null ) {\r
- return self::fromPluginInfo($pluginInfo);\r
- } else {\r
- return null;\r
- }\r
- }\r
-\r
- /**\r
- * Create a new instance of PluginUpdate based on an instance of PluginInfo.\r
- * Basically, this just copies a subset of fields from one object to another.\r
- *\r
- * @param PluginInfo_3_1 $info\r
- * @return PluginUpdate_3_1\r
- */\r
- public static function fromPluginInfo($info){\r
- return self::fromObject($info);\r
- }\r
-\r
- /**\r
- * Create a new instance of PluginUpdate by copying the necessary fields from\r
- * another object.\r
- *\r
- * @param StdClass|PluginInfo_3_1|PluginUpdate_3_1 $object The source object.\r
- * @return PluginUpdate_3_1 The new copy.\r
- */\r
- public static function fromObject($object) {\r
- $update = new self();\r
- $fields = self::$fields;\r
- if ( !empty($object->slug) ) {\r
- $fields = apply_filters('puc_retain_fields-' . $object->slug, $fields);\r
- }\r
- foreach($fields as $field){\r
- if (property_exists($object, $field)) {\r
- $update->$field = $object->$field;\r
- }\r
- }\r
- return $update;\r
- }\r
-\r
- /**\r
- * Create an instance of StdClass that can later be converted back to\r
- * a PluginUpdate. Useful for serialization and caching, as it avoids\r
- * the "incomplete object" problem if the cached value is loaded before\r
- * this class.\r
- *\r
- * @return StdClass\r
- */\r
- public function toStdClass() {\r
- $object = new stdClass();\r
- $fields = self::$fields;\r
- if ( !empty($this->slug) ) {\r
- $fields = apply_filters('puc_retain_fields-' . $this->slug, $fields);\r
- }\r
- foreach($fields as $field){\r
- if (property_exists($this, $field)) {\r
- $object->$field = $this->$field;\r
- }\r
- }\r
- return $object;\r
- }\r
-\r
-\r
- /**\r
- * Transform the update into the format used by WordPress native plugin API.\r
- *\r
- * @return object\r
- */\r
- public function toWpFormat(){\r
- $update = new stdClass;\r
-\r
- $update->id = $this->id;\r
- $update->slug = $this->slug;\r
- $update->new_version = $this->version;\r
- $update->url = $this->homepage;\r
- $update->package = $this->download_url;\r
- $update->tested = $this->tested;\r
- $update->plugin = $this->filename;\r
-\r
- if ( !empty($this->upgrade_notice) ){\r
- $update->upgrade_notice = $this->upgrade_notice;\r
- }\r
-\r
- return $update;\r
- }\r
+ public $id = 0;\r
+ public $slug;\r
+ public $version;\r
+ public $homepage;\r
+ public $download_url;\r
+ public $upgrade_notice;\r
+ public $tested;\r
+ public $translations = array();\r
+ public $filename; //Plugin filename relative to the plugins directory.\r
+\r
+ private static $fields = array(\r
+ 'id', 'slug', 'version', 'homepage', 'tested',\r
+ 'download_url', 'upgrade_notice', 'filename',\r
+ 'translations'\r
+ );\r
+\r
+ /**\r
+ * Create a new instance of PluginUpdate from its JSON-encoded representation.\r
+ *\r
+ * @param string $json\r
+ * @return PluginUpdate_3_1|null\r
+ */\r
+ public static function fromJson($json){\r
+ //Since update-related information is simply a subset of the full plugin info,\r
+ //we can parse the update JSON as if it was a plugin info string, then copy over\r
+ //the parts that we care about.\r
+ $pluginInfo = PluginInfo_3_1::fromJson($json);\r
+ if ( $pluginInfo != null ) {\r
+ return self::fromPluginInfo($pluginInfo);\r
+ } else {\r
+ return null;\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Create a new instance of PluginUpdate based on an instance of PluginInfo.\r
+ * Basically, this just copies a subset of fields from one object to another.\r
+ *\r
+ * @param PluginInfo_3_1 $info\r
+ * @return PluginUpdate_3_1\r
+ */\r
+ public static function fromPluginInfo($info){\r
+ return self::fromObject($info);\r
+ }\r
+\r
+ /**\r
+ * Create a new instance of PluginUpdate by copying the necessary fields from\r
+ * another object.\r
+ *\r
+ * @param StdClass|PluginInfo_3_1|PluginUpdate_3_1 $object The source object.\r
+ * @return PluginUpdate_3_1 The new copy.\r
+ */\r
+ public static function fromObject($object) {\r
+ $update = new self();\r
+ $fields = self::$fields;\r
+ if ( !empty($object->slug) ) {\r
+ $fields = apply_filters('puc_retain_fields-' . $object->slug, $fields);\r
+ }\r
+ foreach($fields as $field){\r
+ if (property_exists($object, $field)) {\r
+ $update->$field = $object->$field;\r
+ }\r
+ }\r
+ return $update;\r
+ }\r
+\r
+ /**\r
+ * Create an instance of StdClass that can later be converted back to\r
+ * a PluginUpdate. Useful for serialization and caching, as it avoids\r
+ * the "incomplete object" problem if the cached value is loaded before\r
+ * this class.\r
+ *\r
+ * @return StdClass\r
+ */\r
+ public function toStdClass() {\r
+ $object = new stdClass();\r
+ $fields = self::$fields;\r
+ if ( !empty($this->slug) ) {\r
+ $fields = apply_filters('puc_retain_fields-' . $this->slug, $fields);\r
+ }\r
+ foreach($fields as $field){\r
+ if (property_exists($this, $field)) {\r
+ $object->$field = $this->$field;\r
+ }\r
+ }\r
+ return $object;\r
+ }\r
+\r
+\r
+ /**\r
+ * Transform the update into the format used by WordPress native plugin API.\r
+ *\r
+ * @return object\r
+ */\r
+ public function toWpFormat(){\r
+ $update = new stdClass;\r
+\r
+ $update->id = $this->id;\r
+ $update->slug = $this->slug;\r
+ $update->new_version = $this->version;\r
+ $update->url = $this->homepage;\r
+ $update->package = $this->download_url;\r
+ $update->tested = $this->tested;\r
+ $update->plugin = $this->filename;\r
+\r
+ if ( !empty($this->upgrade_notice) ){\r
+ $update->upgrade_notice = $this->upgrade_notice;\r
+ }\r
+\r
+ return $update;\r
+ }\r
}\r
\r
endif;\r
* @version 3.0\r
*/\r
class PucScheduler_3_1 {\r
- public $checkPeriod = 12; //How often to check for updates (in hours).\r
- public $throttleRedundantChecks = false; //Check less often if we already know that an update is available.\r
- public $throttledCheckPeriod = 72;\r
-\r
- /**\r
- * @var PluginUpdateChecker_3_1\r
- */\r
- protected $updateChecker;\r
-\r
- private $cronHook = null;\r
-\r
- /**\r
- * Scheduler constructor.\r
- *\r
- * @param PluginUpdateChecker_3_1 $updateChecker\r
- * @param int $checkPeriod How often to check for updates (in hours).\r
- */\r
- public function __construct($updateChecker, $checkPeriod) {\r
- $this->updateChecker = $updateChecker;\r
- $this->checkPeriod = $checkPeriod;\r
-\r
- //Set up the periodic update checks\r
- $this->cronHook = 'check_plugin_updates-' . $this->updateChecker->slug;\r
- if ( $this->checkPeriod > 0 ){\r
-\r
- //Trigger the check via Cron.\r
- //Try to use one of the default schedules if possible as it's less likely to conflict\r
- //with other plugins and their custom schedules.\r
- $defaultSchedules = array(\r
- 1 => 'hourly',\r
- 12 => 'twicedaily',\r
- 24 => 'daily',\r
- );\r
- if ( array_key_exists($this->checkPeriod, $defaultSchedules) ) {\r
- $scheduleName = $defaultSchedules[$this->checkPeriod];\r
- } else {\r
- //Use a custom cron schedule.\r
- $scheduleName = 'every' . $this->checkPeriod . 'hours';\r
- add_filter('cron_schedules', array($this, '_addCustomSchedule'));\r
- }\r
-\r
- if ( !wp_next_scheduled($this->cronHook) && !defined('WP_INSTALLING') ) {\r
- wp_schedule_event(time(), $scheduleName, $this->cronHook);\r
- }\r
- add_action($this->cronHook, array($this, 'maybeCheckForUpdates'));\r
-\r
- register_deactivation_hook($this->updateChecker->pluginFile, array($this, '_removeUpdaterCron'));\r
-\r
- //In case Cron is disabled or unreliable, we also manually trigger\r
- //the periodic checks while the user is browsing the Dashboard.\r
- add_action( 'admin_init', array($this, 'maybeCheckForUpdates') );\r
-\r
- //Like WordPress itself, we check more often on certain pages.\r
- /** @see wp_update_plugins */\r
- add_action('load-update-core.php', array($this, 'maybeCheckForUpdates'));\r
- add_action('load-plugins.php', array($this, 'maybeCheckForUpdates'));\r
- add_action('load-update.php', array($this, 'maybeCheckForUpdates'));\r
- //This hook fires after a bulk update is complete.\r
- add_action('upgrader_process_complete', array($this, 'maybeCheckForUpdates'), 11, 0);\r
-\r
- } else {\r
- //Periodic checks are disabled.\r
- wp_clear_scheduled_hook($this->cronHook);\r
- }\r
- }\r
-\r
- /**\r
- * Check for updates if the configured check interval has already elapsed.\r
- * Will use a shorter check interval on certain admin pages like "Dashboard -> Updates" or when doing cron.\r
- *\r
- * You can override the default behaviour by using the "puc_check_now-$slug" filter.\r
- * The filter callback will be passed three parameters:\r
- * - Current decision. TRUE = check updates now, FALSE = don't check now.\r
- * - Last check time as a Unix timestamp.\r
- * - Configured check period in hours.\r
- * Return TRUE to check for updates immediately, or FALSE to cancel.\r
- *\r
- * This method is declared public because it's a hook callback. Calling it directly is not recommended.\r
- */\r
- public function maybeCheckForUpdates(){\r
- if ( empty($this->checkPeriod) ){\r
- return;\r
- }\r
-\r
- $state = $this->updateChecker->getUpdateState();\r
- $shouldCheck =\r
- empty($state) ||\r
- !isset($state->lastCheck) ||\r
- ( (time() - $state->lastCheck) >= $this->getEffectiveCheckPeriod() );\r
-\r
- //Let plugin authors substitute their own algorithm.\r
- $shouldCheck = apply_filters(\r
- 'puc_check_now-' . $this->updateChecker->slug,\r
- $shouldCheck,\r
- (!empty($state) && isset($state->lastCheck)) ? $state->lastCheck : 0,\r
- $this->checkPeriod\r
- );\r
-\r
- if ( $shouldCheck ) {\r
- $this->updateChecker->checkForUpdates();\r
- }\r
- }\r
-\r
- /**\r
- * Calculate the actual check period based on the current status and environment.\r
- *\r
- * @return int Check period in seconds.\r
- */\r
- protected function getEffectiveCheckPeriod() {\r
- $currentFilter = current_filter();\r
- if ( in_array($currentFilter, array('load-update-core.php', 'upgrader_process_complete')) ) {\r
- //Check more often when the user visits "Dashboard -> Updates" or does a bulk update.\r
- $period = 60;\r
- } else if ( in_array($currentFilter, array('load-plugins.php', 'load-update.php')) ) {\r
- //Also check more often on the "Plugins" page and /wp-admin/update.php.\r
- $period = 3600;\r
- } else if ( $this->throttleRedundantChecks && ($this->updateChecker->getUpdate() !== null) ) {\r
- //Check less frequently if it's already known that an update is available.\r
- $period = $this->throttledCheckPeriod * 3600;\r
- } else if ( defined('DOING_CRON') && constant('DOING_CRON') ) {\r
- //WordPress cron schedules are not exact, so lets do an update check even\r
- //if slightly less than $checkPeriod hours have elapsed since the last check.\r
- $cronFuzziness = 20 * 60;\r
- $period = $this->checkPeriod * 3600 - $cronFuzziness;\r
- } else {\r
- $period = $this->checkPeriod * 3600;\r
- }\r
-\r
- return $period;\r
- }\r
-\r
- /**\r
- * Add our custom schedule to the array of Cron schedules used by WP.\r
- *\r
- * @param array $schedules\r
- * @return array\r
- */\r
- public function _addCustomSchedule($schedules){\r
- if ( $this->checkPeriod && ($this->checkPeriod > 0) ){\r
- $scheduleName = 'every' . $this->checkPeriod . 'hours';\r
- $schedules[$scheduleName] = array(\r
- 'interval' => $this->checkPeriod * 3600,\r
- 'display' => sprintf('Every %d hours', $this->checkPeriod),\r
- );\r
- }\r
- return $schedules;\r
- }\r
-\r
- /**\r
- * Remove the scheduled cron event that the library uses to check for updates.\r
- *\r
- * @return void\r
- */\r
- public function _removeUpdaterCron(){\r
- wp_clear_scheduled_hook($this->cronHook);\r
- }\r
-\r
- /**\r
- * Get the name of the update checker's WP-cron hook. Mostly useful for debugging.\r
- *\r
- * @return string\r
- */\r
- public function getCronHookName() {\r
- return $this->cronHook;\r
- }\r
+ public $checkPeriod = 12; //How often to check for updates (in hours).\r
+ public $throttleRedundantChecks = false; //Check less often if we already know that an update is available.\r
+ public $throttledCheckPeriod = 72;\r
+\r
+ /**\r
+ * @var PluginUpdateChecker_3_1\r
+ */\r
+ protected $updateChecker;\r
+\r
+ private $cronHook = null;\r
+\r
+ /**\r
+ * Scheduler constructor.\r
+ *\r
+ * @param PluginUpdateChecker_3_1 $updateChecker\r
+ * @param int $checkPeriod How often to check for updates (in hours).\r
+ */\r
+ public function __construct($updateChecker, $checkPeriod) {\r
+ $this->updateChecker = $updateChecker;\r
+ $this->checkPeriod = $checkPeriod;\r
+\r
+ //Set up the periodic update checks\r
+ $this->cronHook = 'check_plugin_updates-' . $this->updateChecker->slug;\r
+ if ( $this->checkPeriod > 0 ){\r
+\r
+ //Trigger the check via Cron.\r
+ //Try to use one of the default schedules if possible as it's less likely to conflict\r
+ //with other plugins and their custom schedules.\r
+ $defaultSchedules = array(\r
+ 1 => 'hourly',\r
+ 12 => 'twicedaily',\r
+ 24 => 'daily',\r
+ );\r
+ if ( array_key_exists($this->checkPeriod, $defaultSchedules) ) {\r
+ $scheduleName = $defaultSchedules[$this->checkPeriod];\r
+ } else {\r
+ //Use a custom cron schedule.\r
+ $scheduleName = 'every' . $this->checkPeriod . 'hours';\r
+ add_filter('cron_schedules', array($this, '_addCustomSchedule'));\r
+ }\r
+\r
+ if ( !wp_next_scheduled($this->cronHook) && !defined('WP_INSTALLING') ) {\r
+ wp_schedule_event(time(), $scheduleName, $this->cronHook);\r
+ }\r
+ add_action($this->cronHook, array($this, 'maybeCheckForUpdates'));\r
+\r
+ register_deactivation_hook($this->updateChecker->pluginFile, array($this, '_removeUpdaterCron'));\r
+\r
+ //In case Cron is disabled or unreliable, we also manually trigger\r
+ //the periodic checks while the user is browsing the Dashboard.\r
+ add_action( 'admin_init', array($this, 'maybeCheckForUpdates') );\r
+\r
+ //Like WordPress itself, we check more often on certain pages.\r
+ /** @see wp_update_plugins */\r
+ add_action('load-update-core.php', array($this, 'maybeCheckForUpdates'));\r
+ add_action('load-plugins.php', array($this, 'maybeCheckForUpdates'));\r
+ add_action('load-update.php', array($this, 'maybeCheckForUpdates'));\r
+ //This hook fires after a bulk update is complete.\r
+ add_action('upgrader_process_complete', array($this, 'maybeCheckForUpdates'), 11, 0);\r
+\r
+ } else {\r
+ //Periodic checks are disabled.\r
+ wp_clear_scheduled_hook($this->cronHook);\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Check for updates if the configured check interval has already elapsed.\r
+ * Will use a shorter check interval on certain admin pages like "Dashboard -> Updates" or when doing cron.\r
+ *\r
+ * You can override the default behaviour by using the "puc_check_now-$slug" filter.\r
+ * The filter callback will be passed three parameters:\r
+ * - Current decision. TRUE = check updates now, FALSE = don't check now.\r
+ * - Last check time as a Unix timestamp.\r
+ * - Configured check period in hours.\r
+ * Return TRUE to check for updates immediately, or FALSE to cancel.\r
+ *\r
+ * This method is declared public because it's a hook callback. Calling it directly is not recommended.\r
+ */\r
+ public function maybeCheckForUpdates(){\r
+ if ( empty($this->checkPeriod) ){\r
+ return;\r
+ }\r
+\r
+ $state = $this->updateChecker->getUpdateState();\r
+ $shouldCheck =\r
+ empty($state) ||\r
+ !isset($state->lastCheck) ||\r
+ ( (time() - $state->lastCheck) >= $this->getEffectiveCheckPeriod() );\r
+\r
+ //Let plugin authors substitute their own algorithm.\r
+ $shouldCheck = apply_filters(\r
+ 'puc_check_now-' . $this->updateChecker->slug,\r
+ $shouldCheck,\r
+ (!empty($state) && isset($state->lastCheck)) ? $state->lastCheck : 0,\r
+ $this->checkPeriod\r
+ );\r
+\r
+ if ( $shouldCheck ) {\r
+ $this->updateChecker->checkForUpdates();\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Calculate the actual check period based on the current status and environment.\r
+ *\r
+ * @return int Check period in seconds.\r
+ */\r
+ protected function getEffectiveCheckPeriod() {\r
+ $currentFilter = current_filter();\r
+ if ( in_array($currentFilter, array('load-update-core.php', 'upgrader_process_complete')) ) {\r
+ //Check more often when the user visits "Dashboard -> Updates" or does a bulk update.\r
+ $period = 60;\r
+ } else if ( in_array($currentFilter, array('load-plugins.php', 'load-update.php')) ) {\r
+ //Also check more often on the "Plugins" page and /wp-admin/update.php.\r
+ $period = 3600;\r
+ } else if ( $this->throttleRedundantChecks && ($this->updateChecker->getUpdate() !== null) ) {\r
+ //Check less frequently if it's already known that an update is available.\r
+ $period = $this->throttledCheckPeriod * 3600;\r
+ } else if ( defined('DOING_CRON') && constant('DOING_CRON') ) {\r
+ //WordPress cron schedules are not exact, so lets do an update check even\r
+ //if slightly less than $checkPeriod hours have elapsed since the last check.\r
+ $cronFuzziness = 20 * 60;\r
+ $period = $this->checkPeriod * 3600 - $cronFuzziness;\r
+ } else {\r
+ $period = $this->checkPeriod * 3600;\r
+ }\r
+\r
+ return $period;\r
+ }\r
+\r
+ /**\r
+ * Add our custom schedule to the array of Cron schedules used by WP.\r
+ *\r
+ * @param array $schedules\r
+ * @return array\r
+ */\r
+ public function _addCustomSchedule($schedules){\r
+ if ( $this->checkPeriod && ($this->checkPeriod > 0) ){\r
+ $scheduleName = 'every' . $this->checkPeriod . 'hours';\r
+ $schedules[$scheduleName] = array(\r
+ 'interval' => $this->checkPeriod * 3600,\r
+ 'display' => sprintf('Every %d hours', $this->checkPeriod),\r
+ );\r
+ }\r
+ return $schedules;\r
+ }\r
+\r
+ /**\r
+ * Remove the scheduled cron event that the library uses to check for updates.\r
+ *\r
+ * @return void\r
+ */\r
+ public function _removeUpdaterCron(){\r
+ wp_clear_scheduled_hook($this->cronHook);\r
+ }\r
+\r
+ /**\r
+ * Get the name of the update checker's WP-cron hook. Mostly useful for debugging.\r
+ *\r
+ * @return string\r
+ */\r
+ public function getCronHookName() {\r
+ return $this->cronHook;\r
+ }\r
}\r
\r
endif;\r
* This class uses a few workarounds and heuristics to get the file name.\r
*/\r
class PucUpgraderStatus_3_1 {\r
- private $upgradedPluginFile = null; //The plugin that is currently being upgraded by WordPress.\r
-\r
- public function __construct() {\r
- //Keep track of which plugin WordPress is currently upgrading.\r
- add_filter('upgrader_pre_install', array($this, 'setUpgradedPlugin'), 10, 2);\r
- add_filter('upgrader_package_options', array($this, 'setUpgradedPluginFromOptions'), 10, 1);\r
- add_filter('upgrader_post_install', array($this, 'clearUpgradedPlugin'), 10, 1);\r
- add_action('upgrader_process_complete', array($this, 'clearUpgradedPlugin'), 10, 1);\r
- }\r
-\r
- /**\r
- * Is there and update being installed RIGHT NOW, for a specific plugin?\r
- *\r
- * Caution: This method is unreliable. WordPress doesn't make it easy to figure out what it is upgrading,\r
- * and upgrader implementations are liable to change without notice.\r
- *\r
- * @param string $pluginFile The plugin to check.\r
- * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.\r
- * @return bool True if the plugin identified by $pluginFile is being upgraded.\r
- */\r
- public function isPluginBeingUpgraded($pluginFile, $upgrader = null) {\r
- if ( isset($upgrader) ) {\r
- $upgradedPluginFile = $this->getPluginBeingUpgradedBy($upgrader);\r
- if ( !empty($upgradedPluginFile) ) {\r
- $this->upgradedPluginFile = $upgradedPluginFile;\r
- }\r
- }\r
- return ( !empty($this->upgradedPluginFile) && ($this->upgradedPluginFile === $pluginFile) );\r
- }\r
-\r
- /**\r
- * Get the file name of the plugin that's currently being upgraded.\r
- *\r
- * @param Plugin_Upgrader|WP_Upgrader $upgrader\r
- * @return string|null\r
- */\r
- private function getPluginBeingUpgradedBy($upgrader) {\r
- if ( !isset($upgrader, $upgrader->skin) ) {\r
- return null;\r
- }\r
-\r
- //Figure out which plugin is being upgraded.\r
- $pluginFile = null;\r
- $skin = $upgrader->skin;\r
- if ( $skin instanceof Plugin_Upgrader_Skin ) {\r
- if ( isset($skin->plugin) && is_string($skin->plugin) && ($skin->plugin !== '') ) {\r
- $pluginFile = $skin->plugin;\r
- }\r
- } elseif ( isset($skin->plugin_info) && is_array($skin->plugin_info) ) {\r
- //This case is tricky because Bulk_Plugin_Upgrader_Skin (etc) doesn't actually store the plugin\r
- //filename anywhere. Instead, it has the plugin headers in $plugin_info. So the best we can\r
- //do is compare those headers to the headers of installed plugins.\r
- $pluginFile = $this->identifyPluginByHeaders($skin->plugin_info);\r
- }\r
-\r
- return $pluginFile;\r
- }\r
-\r
- /**\r
- * Identify an installed plugin based on its headers.\r
- *\r
- * @param array $searchHeaders The plugin file header to look for.\r
- * @return string|null Plugin basename ("foo/bar.php"), or NULL if we can't identify the plugin.\r
- */\r
- private function identifyPluginByHeaders($searchHeaders) {\r
- if ( !function_exists('get_plugins') ){\r
- /** @noinspection PhpIncludeInspection */\r
- require_once( ABSPATH . '/wp-admin/includes/plugin.php' );\r
- }\r
-\r
- $installedPlugins = get_plugins();\r
- $matches = array();\r
- foreach($installedPlugins as $pluginBasename => $headers) {\r
- $diff1 = array_diff_assoc($headers, $searchHeaders);\r
- $diff2 = array_diff_assoc($searchHeaders, $headers);\r
- if ( empty($diff1) && empty($diff2) ) {\r
- $matches[] = $pluginBasename;\r
- }\r
- }\r
-\r
- //It's possible (though very unlikely) that there could be two plugins with identical\r
- //headers. In that case, we can't unambiguously identify the plugin that's being upgraded.\r
- if ( count($matches) !== 1 ) {\r
- return null;\r
- }\r
-\r
- return reset($matches);\r
- }\r
-\r
- /**\r
- * @access private\r
- *\r
- * @param mixed $input\r
- * @param array $hookExtra\r
- * @return mixed Returns $input unaltered.\r
- */\r
- public function setUpgradedPlugin($input, $hookExtra) {\r
- if (!empty($hookExtra['plugin']) && is_string($hookExtra['plugin'])) {\r
- $this->upgradedPluginFile = $hookExtra['plugin'];\r
- } else {\r
- $this->upgradedPluginFile = null;\r
- }\r
- return $input;\r
- }\r
-\r
- /**\r
- * @access private\r
- *\r
- * @param array $options\r
- * @return array\r
- */\r
- public function setUpgradedPluginFromOptions($options) {\r
- if (isset($options['hook_extra']['plugin']) && is_string($options['hook_extra']['plugin'])) {\r
- $this->upgradedPluginFile = $options['hook_extra']['plugin'];\r
- } else {\r
- $this->upgradedPluginFile = null;\r
- }\r
- return $options;\r
- }\r
-\r
- /**\r
- * @access private\r
- *\r
- * @param mixed $input\r
- * @return mixed Returns $input unaltered.\r
- */\r
- public function clearUpgradedPlugin($input = null) {\r
- $this->upgradedPluginFile = null;\r
- return $input;\r
- }\r
+ private $upgradedPluginFile = null; //The plugin that is currently being upgraded by WordPress.\r
+\r
+ public function __construct() {\r
+ //Keep track of which plugin WordPress is currently upgrading.\r
+ add_filter('upgrader_pre_install', array($this, 'setUpgradedPlugin'), 10, 2);\r
+ add_filter('upgrader_package_options', array($this, 'setUpgradedPluginFromOptions'), 10, 1);\r
+ add_filter('upgrader_post_install', array($this, 'clearUpgradedPlugin'), 10, 1);\r
+ add_action('upgrader_process_complete', array($this, 'clearUpgradedPlugin'), 10, 1);\r
+ }\r
+\r
+ /**\r
+ * Is there and update being installed RIGHT NOW, for a specific plugin?\r
+ *\r
+ * Caution: This method is unreliable. WordPress doesn't make it easy to figure out what it is upgrading,\r
+ * and upgrader implementations are liable to change without notice.\r
+ *\r
+ * @param string $pluginFile The plugin to check.\r
+ * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.\r
+ * @return bool True if the plugin identified by $pluginFile is being upgraded.\r
+ */\r
+ public function isPluginBeingUpgraded($pluginFile, $upgrader = null) {\r
+ if ( isset($upgrader) ) {\r
+ $upgradedPluginFile = $this->getPluginBeingUpgradedBy($upgrader);\r
+ if ( !empty($upgradedPluginFile) ) {\r
+ $this->upgradedPluginFile = $upgradedPluginFile;\r
+ }\r
+ }\r
+ return ( !empty($this->upgradedPluginFile) && ($this->upgradedPluginFile === $pluginFile) );\r
+ }\r
+\r
+ /**\r
+ * Get the file name of the plugin that's currently being upgraded.\r
+ *\r
+ * @param Plugin_Upgrader|WP_Upgrader $upgrader\r
+ * @return string|null\r
+ */\r
+ private function getPluginBeingUpgradedBy($upgrader) {\r
+ if ( !isset($upgrader, $upgrader->skin) ) {\r
+ return null;\r
+ }\r
+\r
+ //Figure out which plugin is being upgraded.\r
+ $pluginFile = null;\r
+ $skin = $upgrader->skin;\r
+ if ( $skin instanceof Plugin_Upgrader_Skin ) {\r
+ if ( isset($skin->plugin) && is_string($skin->plugin) && ($skin->plugin !== '') ) {\r
+ $pluginFile = $skin->plugin;\r
+ }\r
+ } elseif ( isset($skin->plugin_info) && is_array($skin->plugin_info) ) {\r
+ //This case is tricky because Bulk_Plugin_Upgrader_Skin (etc) doesn't actually store the plugin\r
+ //filename anywhere. Instead, it has the plugin headers in $plugin_info. So the best we can\r
+ //do is compare those headers to the headers of installed plugins.\r
+ $pluginFile = $this->identifyPluginByHeaders($skin->plugin_info);\r
+ }\r
+\r
+ return $pluginFile;\r
+ }\r
+\r
+ /**\r
+ * Identify an installed plugin based on its headers.\r
+ *\r
+ * @param array $searchHeaders The plugin file header to look for.\r
+ * @return string|null Plugin basename ("foo/bar.php"), or NULL if we can't identify the plugin.\r
+ */\r
+ private function identifyPluginByHeaders($searchHeaders) {\r
+ if ( !function_exists('get_plugins') ){\r
+ /** @noinspection PhpIncludeInspection */\r
+ require_once( ABSPATH . '/wp-admin/includes/plugin.php' );\r
+ }\r
+\r
+ $installedPlugins = get_plugins();\r
+ $matches = array();\r
+ foreach($installedPlugins as $pluginBasename => $headers) {\r
+ $diff1 = array_diff_assoc($headers, $searchHeaders);\r
+ $diff2 = array_diff_assoc($searchHeaders, $headers);\r
+ if ( empty($diff1) && empty($diff2) ) {\r
+ $matches[] = $pluginBasename;\r
+ }\r
+ }\r
+\r
+ //It's possible (though very unlikely) that there could be two plugins with identical\r
+ //headers. In that case, we can't unambiguously identify the plugin that's being upgraded.\r
+ if ( count($matches) !== 1 ) {\r
+ return null;\r
+ }\r
+\r
+ return reset($matches);\r
+ }\r
+\r
+ /**\r
+ * @access private\r
+ *\r
+ * @param mixed $input\r
+ * @param array $hookExtra\r
+ * @return mixed Returns $input unaltered.\r
+ */\r
+ public function setUpgradedPlugin($input, $hookExtra) {\r
+ if (!empty($hookExtra['plugin']) && is_string($hookExtra['plugin'])) {\r
+ $this->upgradedPluginFile = $hookExtra['plugin'];\r
+ } else {\r
+ $this->upgradedPluginFile = null;\r
+ }\r
+ return $input;\r
+ }\r
+\r
+ /**\r
+ * @access private\r
+ *\r
+ * @param array $options\r
+ * @return array\r
+ */\r
+ public function setUpgradedPluginFromOptions($options) {\r
+ if (isset($options['hook_extra']['plugin']) && is_string($options['hook_extra']['plugin'])) {\r
+ $this->upgradedPluginFile = $options['hook_extra']['plugin'];\r
+ } else {\r
+ $this->upgradedPluginFile = null;\r
+ }\r
+ return $options;\r
+ }\r
+\r
+ /**\r
+ * @access private\r
+ *\r
+ * @param mixed $input\r
+ * @return mixed Returns $input unaltered.\r
+ */\r
+ public function clearUpgradedPlugin($input = null) {\r
+ $this->upgradedPluginFile = null;\r
+ return $input;\r
+ }\r
}\r
\r
endif;\r
* to get the class name and then create it with <code>new $class(...)</code>.\r
*/\r
class PucFactory {\r
- protected static $classVersions = array();\r
- protected static $sorted = false;\r
-\r
- /**\r
- * Create a new instance of PluginUpdateChecker.\r
- *\r
- * @see PluginUpdateChecker::__construct()\r
- *\r
- * @param $metadataUrl\r
- * @param $pluginFile\r
- * @param string $slug\r
- * @param int $checkPeriod\r
- * @param string $optionName\r
- * @param string $muPluginFile\r
- * @return PluginUpdateChecker_3_1\r
- */\r
- public static function buildUpdateChecker($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {\r
- $class = self::getLatestClassVersion('PluginUpdateChecker');\r
- return new $class($metadataUrl, $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile);\r
- }\r
-\r
- /**\r
- * Get the specific class name for the latest available version of a class.\r
- *\r
- * @param string $class\r
- * @return string|null\r
- */\r
- public static function getLatestClassVersion($class) {\r
- if ( !self::$sorted ) {\r
- self::sortVersions();\r
- }\r
-\r
- if ( isset(self::$classVersions[$class]) ) {\r
- return reset(self::$classVersions[$class]);\r
- } else {\r
- return null;\r
- }\r
- }\r
-\r
- /**\r
- * Sort available class versions in descending order (i.e. newest first).\r
- */\r
- protected static function sortVersions() {\r
- foreach ( self::$classVersions as $class => $versions ) {\r
- uksort($versions, array(__CLASS__, 'compareVersions'));\r
- self::$classVersions[$class] = $versions;\r
- }\r
- self::$sorted = true;\r
- }\r
-\r
- protected static function compareVersions($a, $b) {\r
- return -version_compare($a, $b);\r
- }\r
-\r
- /**\r
- * Register a version of a class.\r
- *\r
- * @access private This method is only for internal use by the library.\r
- *\r
- * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.\r
- * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.\r
- * @param string $version Version number, e.g. '1.2'.\r
- */\r
- public static function addVersion($generalClass, $versionedClass, $version) {\r
- if ( !isset(self::$classVersions[$generalClass]) ) {\r
- self::$classVersions[$generalClass] = array();\r
- }\r
- self::$classVersions[$generalClass][$version] = $versionedClass;\r
- self::$sorted = false;\r
- }\r
+ protected static $classVersions = array();\r
+ protected static $sorted = false;\r
+\r
+ /**\r
+ * Create a new instance of PluginUpdateChecker.\r
+ *\r
+ * @see PluginUpdateChecker::__construct()\r
+ *\r
+ * @param $metadataUrl\r
+ * @param $pluginFile\r
+ * @param string $slug\r
+ * @param int $checkPeriod\r
+ * @param string $optionName\r
+ * @param string $muPluginFile\r
+ * @return PluginUpdateChecker_3_1\r
+ */\r
+ public static function buildUpdateChecker($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {\r
+ $class = self::getLatestClassVersion('PluginUpdateChecker');\r
+ return new $class($metadataUrl, $pluginFile, $slug, $checkPeriod, $optionName, $muPluginFile);\r
+ }\r
+\r
+ /**\r
+ * Get the specific class name for the latest available version of a class.\r
+ *\r
+ * @param string $class\r
+ * @return string|null\r
+ */\r
+ public static function getLatestClassVersion($class) {\r
+ if ( !self::$sorted ) {\r
+ self::sortVersions();\r
+ }\r
+\r
+ if ( isset(self::$classVersions[$class]) ) {\r
+ return reset(self::$classVersions[$class]);\r
+ } else {\r
+ return null;\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Sort available class versions in descending order (i.e. newest first).\r
+ */\r
+ protected static function sortVersions() {\r
+ foreach ( self::$classVersions as $class => $versions ) {\r
+ uksort($versions, array(__CLASS__, 'compareVersions'));\r
+ self::$classVersions[$class] = $versions;\r
+ }\r
+ self::$sorted = true;\r
+ }\r
+\r
+ protected static function compareVersions($a, $b) {\r
+ return -version_compare($a, $b);\r
+ }\r
+\r
+ /**\r
+ * Register a version of a class.\r
+ *\r
+ * @access private This method is only for internal use by the library.\r
+ *\r
+ * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.\r
+ * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.\r
+ * @param string $version Version number, e.g. '1.2'.\r
+ */\r
+ public static function addVersion($generalClass, $versionedClass, $version) {\r
+ if ( !isset(self::$classVersions[$generalClass]) ) {\r
+ self::$classVersions[$generalClass] = array();\r
+ }\r
+ self::$classVersions[$generalClass][$version] = $versionedClass;\r
+ self::$sorted = false;\r
+ }\r
}\r
\r
endif;\r