UpdateChecker.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <?php
  2. if ( !class_exists('Puc_v4p11_Plugin_UpdateChecker', false) ):
  3. /**
  4. * A custom plugin update checker.
  5. *
  6. * @author Janis Elsts
  7. * @copyright 2018
  8. * @access public
  9. */
  10. class Puc_v4p11_Plugin_UpdateChecker extends Puc_v4p11_UpdateChecker {
  11. protected $updateTransient = 'update_plugins';
  12. protected $translationType = 'plugin';
  13. public $pluginAbsolutePath = ''; //Full path of the main plugin file.
  14. public $pluginFile = ''; //Plugin filename relative to the plugins directory. Many WP APIs use this to identify plugins.
  15. public $muPluginFile = ''; //For MU plugins, the plugin filename relative to the mu-plugins directory.
  16. /**
  17. * @var Puc_v4p11_Plugin_Package
  18. */
  19. protected $package;
  20. private $extraUi = null;
  21. /**
  22. * Class constructor.
  23. *
  24. * @param string $metadataUrl The URL of the plugin's metadata file.
  25. * @param string $pluginFile Fully qualified path to the main plugin file.
  26. * @param string $slug The plugin's 'slug'. If not specified, the filename part of $pluginFile sans '.php' will be used as the slug.
  27. * @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.
  28. * @param string $optionName Where to store book-keeping info about update checks. Defaults to 'external_updates-$slug'.
  29. * @param string $muPluginFile Optional. The plugin filename relative to the mu-plugins directory.
  30. */
  31. public function __construct($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = ''){
  32. $this->pluginAbsolutePath = $pluginFile;
  33. $this->pluginFile = plugin_basename($this->pluginAbsolutePath);
  34. $this->muPluginFile = $muPluginFile;
  35. //If no slug is specified, use the name of the main plugin file as the slug.
  36. //For example, 'my-cool-plugin/cool-plugin.php' becomes 'cool-plugin'.
  37. if ( empty($slug) ){
  38. $slug = basename($this->pluginFile, '.php');
  39. }
  40. //Plugin slugs must be unique.
  41. $slugCheckFilter = 'puc_is_slug_in_use-' . $slug;
  42. $slugUsedBy = apply_filters($slugCheckFilter, false);
  43. if ( $slugUsedBy ) {
  44. $this->triggerError(sprintf(
  45. 'Plugin slug "%s" is already in use by %s. Slugs must be unique.',
  46. htmlentities($slug),
  47. htmlentities($slugUsedBy)
  48. ), E_USER_ERROR);
  49. }
  50. add_filter($slugCheckFilter, array($this, 'getAbsolutePath'));
  51. parent::__construct($metadataUrl, dirname($this->pluginFile), $slug, $checkPeriod, $optionName);
  52. //Backwards compatibility: If the plugin is a mu-plugin but no $muPluginFile is specified, assume
  53. //it's the same as $pluginFile given that it's not in a subdirectory (WP only looks in the base dir).
  54. if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) {
  55. $this->muPluginFile = $this->pluginFile;
  56. }
  57. //To prevent a crash during plugin uninstallation, remove updater hooks when the user removes the plugin.
  58. //Details: https://github.com/YahnisElsts/plugin-update-checker/issues/138#issuecomment-335590964
  59. add_action('uninstall_' . $this->pluginFile, array($this, 'removeHooks'));
  60. $this->extraUi = new Puc_v4p11_Plugin_Ui($this);
  61. }
  62. /**
  63. * Create an instance of the scheduler.
  64. *
  65. * @param int $checkPeriod
  66. * @return Puc_v4p11_Scheduler
  67. */
  68. protected function createScheduler($checkPeriod) {
  69. $scheduler = new Puc_v4p11_Scheduler($this, $checkPeriod, array('load-plugins.php'));
  70. register_deactivation_hook($this->pluginFile, array($scheduler, 'removeUpdaterCron'));
  71. return $scheduler;
  72. }
  73. /**
  74. * Install the hooks required to run periodic update checks and inject update info
  75. * into WP data structures.
  76. *
  77. * @return void
  78. */
  79. protected function installHooks(){
  80. //Override requests for plugin information
  81. add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);
  82. parent::installHooks();
  83. }
  84. /**
  85. * Remove update checker hooks.
  86. *
  87. * The intent is to prevent a fatal error that can happen if the plugin has an uninstall
  88. * hook. During uninstallation, WP includes the main plugin file (which creates a PUC instance),
  89. * the uninstall hook runs, WP deletes the plugin files and then updates some transients.
  90. * If PUC hooks are still around at this time, they could throw an error while trying to
  91. * autoload classes from files that no longer exist.
  92. *
  93. * The "site_transient_{$transient}" filter is the main problem here, but let's also remove
  94. * most other PUC hooks to be safe.
  95. *
  96. * @internal
  97. */
  98. public function removeHooks() {
  99. parent::removeHooks();
  100. $this->extraUi->removeHooks();
  101. $this->package->removeHooks();
  102. remove_filter('plugins_api', array($this, 'injectInfo'), 20);
  103. }
  104. /**
  105. * Retrieve plugin info from the configured API endpoint.
  106. *
  107. * @uses wp_remote_get()
  108. *
  109. * @param array $queryArgs Additional query arguments to append to the request. Optional.
  110. * @return Puc_v4p11_Plugin_Info
  111. */
  112. public function requestInfo($queryArgs = array()) {
  113. list($pluginInfo, $result) = $this->requestMetadata('Puc_v4p11_Plugin_Info', 'request_info', $queryArgs);
  114. if ( $pluginInfo !== null ) {
  115. /** @var Puc_v4p11_Plugin_Info $pluginInfo */
  116. $pluginInfo->filename = $this->pluginFile;
  117. $pluginInfo->slug = $this->slug;
  118. }
  119. $pluginInfo = apply_filters($this->getUniqueName('request_info_result'), $pluginInfo, $result);
  120. return $pluginInfo;
  121. }
  122. /**
  123. * Retrieve the latest update (if any) from the configured API endpoint.
  124. *
  125. * @uses PluginUpdateChecker::requestInfo()
  126. *
  127. * @return Puc_v4p11_Update|null An instance of Plugin_Update, or NULL when no updates are available.
  128. */
  129. public function requestUpdate() {
  130. //For the sake of simplicity, this function just calls requestInfo()
  131. //and transforms the result accordingly.
  132. $pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));
  133. if ( $pluginInfo === null ){
  134. return null;
  135. }
  136. $update = Puc_v4p11_Plugin_Update::fromPluginInfo($pluginInfo);
  137. $update = $this->filterUpdateResult($update);
  138. return $update;
  139. }
  140. /**
  141. * Intercept plugins_api() calls that request information about our plugin and
  142. * use the configured API endpoint to satisfy them.
  143. *
  144. * @see plugins_api()
  145. *
  146. * @param mixed $result
  147. * @param string $action
  148. * @param array|object $args
  149. * @return mixed
  150. */
  151. public function injectInfo($result, $action = null, $args = null){
  152. $relevant = ($action == 'plugin_information') && isset($args->slug) && (
  153. ($args->slug == $this->slug) || ($args->slug == dirname($this->pluginFile))
  154. );
  155. if ( !$relevant ) {
  156. return $result;
  157. }
  158. $pluginInfo = $this->requestInfo();
  159. $this->fixSupportedWordpressVersion($pluginInfo);
  160. $pluginInfo = apply_filters($this->getUniqueName('pre_inject_info'), $pluginInfo);
  161. if ( $pluginInfo ) {
  162. return $pluginInfo->toWpFormat();
  163. }
  164. return $result;
  165. }
  166. protected function shouldShowUpdates() {
  167. //No update notifications for mu-plugins unless explicitly enabled. The MU plugin file
  168. //is usually different from the main plugin file so the update wouldn't show up properly anyway.
  169. return !$this->isUnknownMuPlugin();
  170. }
  171. /**
  172. * @param stdClass|null $updates
  173. * @param stdClass $updateToAdd
  174. * @return stdClass
  175. */
  176. protected function addUpdateToList($updates, $updateToAdd) {
  177. if ( $this->package->isMuPlugin() ) {
  178. //WP does not support automatic update installation for mu-plugins, but we can
  179. //still display a notice.
  180. $updateToAdd->package = null;
  181. }
  182. return parent::addUpdateToList($updates, $updateToAdd);
  183. }
  184. /**
  185. * @param stdClass|null $updates
  186. * @return stdClass|null
  187. */
  188. protected function removeUpdateFromList($updates) {
  189. $updates = parent::removeUpdateFromList($updates);
  190. if ( !empty($this->muPluginFile) && isset($updates, $updates->response) ) {
  191. unset($updates->response[$this->muPluginFile]);
  192. }
  193. return $updates;
  194. }
  195. /**
  196. * For plugins, the update array is indexed by the plugin filename relative to the "plugins"
  197. * directory. Example: "plugin-name/plugin.php".
  198. *
  199. * @return string
  200. */
  201. protected function getUpdateListKey() {
  202. if ( $this->package->isMuPlugin() ) {
  203. return $this->muPluginFile;
  204. }
  205. return $this->pluginFile;
  206. }
  207. protected function getNoUpdateItemFields() {
  208. return array_merge(
  209. parent::getNoUpdateItemFields(),
  210. array(
  211. 'id' => $this->pluginFile,
  212. 'slug' => $this->slug,
  213. 'plugin' => $this->pluginFile,
  214. 'icons' => array(),
  215. 'banners' => array(),
  216. 'banners_rtl' => array(),
  217. 'tested' => '',
  218. 'compatibility' => new stdClass(),
  219. )
  220. );
  221. }
  222. /**
  223. * Alias for isBeingUpgraded().
  224. *
  225. * @deprecated
  226. * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
  227. * @return bool
  228. */
  229. public function isPluginBeingUpgraded($upgrader = null) {
  230. return $this->isBeingUpgraded($upgrader);
  231. }
  232. /**
  233. * Is there an update being installed for this plugin, right now?
  234. *
  235. * @param WP_Upgrader|null $upgrader
  236. * @return bool
  237. */
  238. public function isBeingUpgraded($upgrader = null) {
  239. return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader);
  240. }
  241. /**
  242. * Get the details of the currently available update, if any.
  243. *
  244. * If no updates are available, or if the last known update version is below or equal
  245. * to the currently installed version, this method will return NULL.
  246. *
  247. * Uses cached update data. To retrieve update information straight from
  248. * the metadata URL, call requestUpdate() instead.
  249. *
  250. * @return Puc_v4p11_Plugin_Update|null
  251. */
  252. public function getUpdate() {
  253. $update = parent::getUpdate();
  254. if ( isset($update) ) {
  255. /** @var Puc_v4p11_Plugin_Update $update */
  256. $update->filename = $this->pluginFile;
  257. }
  258. return $update;
  259. }
  260. /**
  261. * Get the translated plugin title.
  262. *
  263. * @deprecated
  264. * @return string
  265. */
  266. public function getPluginTitle() {
  267. return $this->package->getPluginTitle();
  268. }
  269. /**
  270. * Check if the current user has the required permissions to install updates.
  271. *
  272. * @return bool
  273. */
  274. public function userCanInstallUpdates() {
  275. return current_user_can('update_plugins');
  276. }
  277. /**
  278. * Check if the plugin file is inside the mu-plugins directory.
  279. *
  280. * @deprecated
  281. * @return bool
  282. */
  283. protected function isMuPlugin() {
  284. return $this->package->isMuPlugin();
  285. }
  286. /**
  287. * MU plugins are partially supported, but only when we know which file in mu-plugins
  288. * corresponds to this plugin.
  289. *
  290. * @return bool
  291. */
  292. protected function isUnknownMuPlugin() {
  293. return empty($this->muPluginFile) && $this->package->isMuPlugin();
  294. }
  295. /**
  296. * Get absolute path to the main plugin file.
  297. *
  298. * @return string
  299. */
  300. public function getAbsolutePath() {
  301. return $this->pluginAbsolutePath;
  302. }
  303. /**
  304. * Register a callback for filtering query arguments.
  305. *
  306. * The callback function should take one argument - an associative array of query arguments.
  307. * It should return a modified array of query arguments.
  308. *
  309. * @uses add_filter() This method is a convenience wrapper for add_filter().
  310. *
  311. * @param callable $callback
  312. * @return void
  313. */
  314. public function addQueryArgFilter($callback){
  315. $this->addFilter('request_info_query_args', $callback);
  316. }
  317. /**
  318. * Register a callback for filtering arguments passed to wp_remote_get().
  319. *
  320. * The callback function should take one argument - an associative array of arguments -
  321. * and return a modified array or arguments. See the WP documentation on wp_remote_get()
  322. * for details on what arguments are available and how they work.
  323. *
  324. * @uses add_filter() This method is a convenience wrapper for add_filter().
  325. *
  326. * @param callable $callback
  327. * @return void
  328. */
  329. public function addHttpRequestArgFilter($callback) {
  330. $this->addFilter('request_info_options', $callback);
  331. }
  332. /**
  333. * Register a callback for filtering the plugin info retrieved from the external API.
  334. *
  335. * The callback function should take two arguments. If the plugin info was retrieved
  336. * successfully, the first argument passed will be an instance of PluginInfo. Otherwise,
  337. * it will be NULL. The second argument will be the corresponding return value of
  338. * wp_remote_get (see WP docs for details).
  339. *
  340. * The callback function should return a new or modified instance of PluginInfo or NULL.
  341. *
  342. * @uses add_filter() This method is a convenience wrapper for add_filter().
  343. *
  344. * @param callable $callback
  345. * @return void
  346. */
  347. public function addResultFilter($callback) {
  348. $this->addFilter('request_info_result', $callback, 10, 2);
  349. }
  350. protected function createDebugBarExtension() {
  351. return new Puc_v4p11_DebugBar_PluginExtension($this);
  352. }
  353. /**
  354. * Create a package instance that represents this plugin or theme.
  355. *
  356. * @return Puc_v4p11_InstalledPackage
  357. */
  358. protected function createInstalledPackage() {
  359. return new Puc_v4p11_Plugin_Package($this->pluginAbsolutePath, $this);
  360. }
  361. /**
  362. * @return Puc_v4p11_Plugin_Package
  363. */
  364. public function getInstalledPackage() {
  365. return $this->package;
  366. }
  367. }
  368. endif;