An updated theme (based on Atticus Finch) for ClassicPress
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

546 lines
17 KiB

3 years ago
  1. <?php
  2. if ( !class_exists('Puc_v4p1_Plugin_UpdateChecker', false) ):
  3. /**
  4. * A custom plugin update checker.
  5. *
  6. * @author Janis Elsts
  7. * @copyright 2016
  8. * @access public
  9. */
  10. class Puc_v4p1_Plugin_UpdateChecker extends Puc_v4p1_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. private $cachedInstalledVersion = null;
  17. /**
  18. * Class constructor.
  19. *
  20. * @param string $metadataUrl The URL of the plugin's metadata file.
  21. * @param string $pluginFile Fully qualified path to the main plugin file.
  22. * @param string $slug The plugin's 'slug'. If not specified, the filename part of $pluginFile sans '.php' will be used as the slug.
  23. * @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.
  24. * @param string $optionName Where to store book-keeping info about update checks. Defaults to 'external_updates-$slug'.
  25. * @param string $muPluginFile Optional. The plugin filename relative to the mu-plugins directory.
  26. */
  27. public function __construct($metadataUrl, $pluginFile, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = ''){
  28. $this->pluginAbsolutePath = $pluginFile;
  29. $this->pluginFile = plugin_basename($this->pluginAbsolutePath);
  30. $this->muPluginFile = $muPluginFile;
  31. //If no slug is specified, use the name of the main plugin file as the slug.
  32. //For example, 'my-cool-plugin/cool-plugin.php' becomes 'cool-plugin'.
  33. if ( empty($slug) ){
  34. $slug = basename($this->pluginFile, '.php');
  35. }
  36. //Plugin slugs must be unique.
  37. $slugCheckFilter = 'puc_is_slug_in_use-' . $this->slug;
  38. $slugUsedBy = apply_filters($slugCheckFilter, false);
  39. if ( $slugUsedBy ) {
  40. $this->triggerError(sprintf(
  41. 'Plugin slug "%s" is already in use by %s. Slugs must be unique.',
  42. htmlentities($this->slug),
  43. htmlentities($slugUsedBy)
  44. ), E_USER_ERROR);
  45. }
  46. add_filter($slugCheckFilter, array($this, 'getAbsolutePath'));
  47. //Backwards compatibility: If the plugin is a mu-plugin but no $muPluginFile is specified, assume
  48. //it's the same as $pluginFile given that it's not in a subdirectory (WP only looks in the base dir).
  49. if ( (strpbrk($this->pluginFile, '/\\') === false) && $this->isUnknownMuPlugin() ) {
  50. $this->muPluginFile = $this->pluginFile;
  51. }
  52. parent::__construct($metadataUrl, dirname($this->pluginFile), $slug, $checkPeriod, $optionName);
  53. }
  54. /**
  55. * Create an instance of the scheduler.
  56. *
  57. * @param int $checkPeriod
  58. * @return Puc_v4p1_Scheduler
  59. */
  60. protected function createScheduler($checkPeriod) {
  61. $scheduler = new Puc_v4p1_Scheduler($this, $checkPeriod, array('load-plugins.php'));
  62. register_deactivation_hook($this->pluginFile, array($scheduler, 'removeUpdaterCron'));
  63. return $scheduler;
  64. }
  65. /**
  66. * Install the hooks required to run periodic update checks and inject update info
  67. * into WP data structures.
  68. *
  69. * @return void
  70. */
  71. protected function installHooks(){
  72. //Override requests for plugin information
  73. add_filter('plugins_api', array($this, 'injectInfo'), 20, 3);
  74. add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);
  75. add_action('admin_init', array($this, 'handleManualCheck'));
  76. add_action('all_admin_notices', array($this, 'displayManualCheckResult'));
  77. //Clear the version number cache when something - anything - is upgraded or WP clears the update cache.
  78. add_filter('upgrader_post_install', array($this, 'clearCachedVersion'));
  79. add_action('delete_site_transient_update_plugins', array($this, 'clearCachedVersion'));
  80. parent::installHooks();
  81. }
  82. /**
  83. * Retrieve plugin info from the configured API endpoint.
  84. *
  85. * @uses wp_remote_get()
  86. *
  87. * @param array $queryArgs Additional query arguments to append to the request. Optional.
  88. * @return Puc_v4p1_Plugin_Info
  89. */
  90. public function requestInfo($queryArgs = array()) {
  91. list($pluginInfo, $result) = $this->requestMetadata('Puc_v4p1_Plugin_Info', 'request_info', $queryArgs);
  92. if ( $pluginInfo !== null ) {
  93. /** @var Puc_v4p1_Plugin_Info $pluginInfo */
  94. $pluginInfo->filename = $this->pluginFile;
  95. $pluginInfo->slug = $this->slug;
  96. }
  97. $pluginInfo = apply_filters($this->getUniqueName('request_info_result'), $pluginInfo, $result);
  98. return $pluginInfo;
  99. }
  100. /**
  101. * Retrieve the latest update (if any) from the configured API endpoint.
  102. *
  103. * @uses PluginUpdateChecker::requestInfo()
  104. *
  105. * @return Puc_v4p1_Update|null An instance of Plugin_Update, or NULL when no updates are available.
  106. */
  107. public function requestUpdate() {
  108. //For the sake of simplicity, this function just calls requestInfo()
  109. //and transforms the result accordingly.
  110. $pluginInfo = $this->requestInfo(array('checking_for_updates' => '1'));
  111. if ( $pluginInfo == null ){
  112. return null;
  113. }
  114. $update = Puc_v4p1_Plugin_Update::fromPluginInfo($pluginInfo);
  115. $update = $this->filterUpdateResult($update);
  116. return $update;
  117. }
  118. /**
  119. * Get the currently installed version of the plugin.
  120. *
  121. * @return string Version number.
  122. */
  123. public function getInstalledVersion(){
  124. if ( isset($this->cachedInstalledVersion) ) {
  125. return $this->cachedInstalledVersion;
  126. }
  127. $pluginHeader = $this->getPluginHeader();
  128. if ( isset($pluginHeader['Version']) ) {
  129. $this->cachedInstalledVersion = $pluginHeader['Version'];
  130. return $pluginHeader['Version'];
  131. } else {
  132. //This can happen if the filename points to something that is not a plugin.
  133. $this->triggerError(
  134. sprintf(
  135. "Can't to read the Version header for '%s'. The filename is incorrect or is not a plugin.",
  136. $this->pluginFile
  137. ),
  138. E_USER_WARNING
  139. );
  140. return null;
  141. }
  142. }
  143. /**
  144. * Get plugin's metadata from its file header.
  145. *
  146. * @return array
  147. */
  148. protected function getPluginHeader() {
  149. if ( !is_file($this->pluginAbsolutePath) ) {
  150. //This can happen if the plugin filename is wrong.
  151. $this->triggerError(
  152. sprintf(
  153. "Can't to read the plugin header for '%s'. The file does not exist.",
  154. $this->pluginFile
  155. ),
  156. E_USER_WARNING
  157. );
  158. return array();
  159. }
  160. if ( !function_exists('get_plugin_data') ){
  161. /** @noinspection PhpIncludeInspection */
  162. require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
  163. }
  164. return get_plugin_data($this->pluginAbsolutePath, false, false);
  165. }
  166. /**
  167. * @return array
  168. */
  169. protected function getHeaderNames() {
  170. return array(
  171. 'Name' => 'Plugin Name',
  172. 'PluginURI' => 'Plugin URI',
  173. 'Version' => 'Version',
  174. 'Description' => 'Description',
  175. 'Author' => 'Author',
  176. 'AuthorURI' => 'Author URI',
  177. 'TextDomain' => 'Text Domain',
  178. 'DomainPath' => 'Domain Path',
  179. 'Network' => 'Network',
  180. //The newest WordPress version that this plugin requires or has been tested with.
  181. //We support several different formats for compatibility with other libraries.
  182. 'Tested WP' => 'Tested WP',
  183. 'Requires WP' => 'Requires WP',
  184. 'Tested up to' => 'Tested up to',
  185. 'Requires at least' => 'Requires at least',
  186. );
  187. }
  188. /**
  189. * Intercept plugins_api() calls that request information about our plugin and
  190. * use the configured API endpoint to satisfy them.
  191. *
  192. * @see plugins_api()
  193. *
  194. * @param mixed $result
  195. * @param string $action
  196. * @param array|object $args
  197. * @return mixed
  198. */
  199. public function injectInfo($result, $action = null, $args = null){
  200. $relevant = ($action == 'plugin_information') && isset($args->slug) && (
  201. ($args->slug == $this->slug) || ($args->slug == dirname($this->pluginFile))
  202. );
  203. if ( !$relevant ) {
  204. return $result;
  205. }
  206. $pluginInfo = $this->requestInfo();
  207. $pluginInfo = apply_filters($this->getUniqueName('pre_inject_info'), $pluginInfo);
  208. if ( $pluginInfo ) {
  209. return $pluginInfo->toWpFormat();
  210. }
  211. return $result;
  212. }
  213. protected function shouldShowUpdates() {
  214. //No update notifications for mu-plugins unless explicitly enabled. The MU plugin file
  215. //is usually different from the main plugin file so the update wouldn't show up properly anyway.
  216. return !$this->isUnknownMuPlugin();
  217. }
  218. /**
  219. * @param stdClass|null $updates
  220. * @param stdClass $updateToAdd
  221. * @return stdClass
  222. */
  223. protected function addUpdateToList($updates, $updateToAdd) {
  224. if ( $this->isMuPlugin() ) {
  225. //WP does not support automatic update installation for mu-plugins, but we can
  226. //still display a notice.
  227. $updateToAdd->package = null;
  228. }
  229. return parent::addUpdateToList($updates, $updateToAdd);
  230. }
  231. /**
  232. * @param stdClass|null $updates
  233. * @return stdClass|null
  234. */
  235. protected function removeUpdateFromList($updates) {
  236. $updates = parent::removeUpdateFromList($updates);
  237. if ( !empty($this->muPluginFile) && isset($updates, $updates->response) ) {
  238. unset($updates->response[$this->muPluginFile]);
  239. }
  240. return $updates;
  241. }
  242. /**
  243. * For plugins, the update array is indexed by the plugin filename relative to the "plugins"
  244. * directory. Example: "plugin-name/plugin.php".
  245. *
  246. * @return string
  247. */
  248. protected function getUpdateListKey() {
  249. if ( $this->isMuPlugin() ) {
  250. return $this->muPluginFile;
  251. }
  252. return $this->pluginFile;
  253. }
  254. /**
  255. * Alias for isBeingUpgraded().
  256. *
  257. * @deprecated
  258. * @param WP_Upgrader|null $upgrader The upgrader that's performing the current update.
  259. * @return bool
  260. */
  261. public function isPluginBeingUpgraded($upgrader = null) {
  262. return $this->isBeingUpgraded($upgrader);
  263. }
  264. /**
  265. * Is there an update being installed for this plugin, right now?
  266. *
  267. * @param WP_Upgrader|null $upgrader
  268. * @return bool
  269. */
  270. public function isBeingUpgraded($upgrader = null) {
  271. return $this->upgraderStatus->isPluginBeingUpgraded($this->pluginFile, $upgrader);
  272. }
  273. /**
  274. * Get the details of the currently available update, if any.
  275. *
  276. * If no updates are available, or if the last known update version is below or equal
  277. * to the currently installed version, this method will return NULL.
  278. *
  279. * Uses cached update data. To retrieve update information straight from
  280. * the metadata URL, call requestUpdate() instead.
  281. *
  282. * @return Puc_v4p1_Plugin_Update|null
  283. */
  284. public function getUpdate() {
  285. $update = parent::getUpdate();
  286. if ( isset($update) ) {
  287. /** @var Puc_v4p1_Plugin_Update $update */
  288. $update->filename = $this->pluginFile;
  289. }
  290. return $update;
  291. }
  292. /**
  293. * Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,
  294. * the new link will appear after the "Visit plugin site" link.
  295. *
  296. * You can change the link text by using the "puc_manual_check_link-$slug" filter.
  297. * Returning an empty string from the filter will disable the link.
  298. *
  299. * @param array $pluginMeta Array of meta links.
  300. * @param string $pluginFile
  301. * @return array
  302. */
  303. public function addCheckForUpdatesLink($pluginMeta, $pluginFile) {
  304. $isRelevant = ($pluginFile == $this->pluginFile)
  305. || (!empty($this->muPluginFile) && $pluginFile == $this->muPluginFile);
  306. if ( $isRelevant && $this->userCanInstallUpdates() ) {
  307. $linkUrl = wp_nonce_url(
  308. add_query_arg(
  309. array(
  310. 'puc_check_for_updates' => 1,
  311. 'puc_slug' => $this->slug,
  312. ),
  313. self_admin_url('plugins.php')
  314. ),
  315. 'puc_check_for_updates'
  316. );
  317. $linkText = apply_filters(
  318. $this->getUniqueName('manual_check_link'),
  319. __('Check for updates', 'plugin-update-checker')
  320. );
  321. if ( !empty($linkText) ) {
  322. /** @noinspection HtmlUnknownTarget */
  323. $pluginMeta[] = sprintf('<a href="%s">%s</a>', esc_attr($linkUrl), $linkText);
  324. }
  325. }
  326. return $pluginMeta;
  327. }
  328. /**
  329. * Check for updates when the user clicks the "Check for updates" link.
  330. * @see self::addCheckForUpdatesLink()
  331. *
  332. * @return void
  333. */
  334. public function handleManualCheck() {
  335. $shouldCheck =
  336. isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])
  337. && $_GET['puc_slug'] == $this->slug
  338. && $this->userCanInstallUpdates()
  339. && check_admin_referer('puc_check_for_updates');
  340. if ( $shouldCheck ) {
  341. $update = $this->checkForUpdates();
  342. $status = ($update === null) ? 'no_update' : 'update_available';
  343. wp_redirect(add_query_arg(
  344. array(
  345. 'puc_update_check_result' => $status,
  346. 'puc_slug' => $this->slug,
  347. ),
  348. self_admin_url('plugins.php')
  349. ));
  350. }
  351. }
  352. /**
  353. * Display the results of a manual update check.
  354. * @see self::handleManualCheck()
  355. *
  356. * You can change the result message by using the "puc_manual_check_message-$slug" filter.
  357. */
  358. public function displayManualCheckResult() {
  359. if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->slug) ) {
  360. $status = strval($_GET['puc_update_check_result']);
  361. $title = $this->getPluginTitle();
  362. if ( $status == 'no_update' ) {
  363. $message = sprintf(_x('The %s plugin is up to date.', 'the plugin title', 'plugin-update-checker'), $title);
  364. } else if ( $status == 'update_available' ) {
  365. $message = sprintf(_x('A new version of the %s plugin is available.', 'the plugin title', 'plugin-update-checker'), $title);
  366. } else {
  367. $message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), htmlentities($status));
  368. }
  369. printf(
  370. '<div class="updated notice is-dismissible"><p>%s</p></div>',
  371. apply_filters($this->getUniqueName('manual_check_message'), $message, $status)
  372. );
  373. }
  374. }
  375. /**
  376. * Get the translated plugin title.
  377. *
  378. * @return string
  379. */
  380. protected function getPluginTitle() {
  381. $title = '';
  382. $header = $this->getPluginHeader();
  383. if ( $header && !empty($header['Name']) && isset($header['TextDomain']) ) {
  384. $title = translate($header['Name'], $header['TextDomain']);
  385. }
  386. return $title;
  387. }
  388. /**
  389. * Check if the current user has the required permissions to install updates.
  390. *
  391. * @return bool
  392. */
  393. public function userCanInstallUpdates() {
  394. return current_user_can('update_plugins');
  395. }
  396. /**
  397. * Check if the plugin file is inside the mu-plugins directory.
  398. *
  399. * @return bool
  400. */
  401. protected function isMuPlugin() {
  402. static $cachedResult = null;
  403. if ( $cachedResult === null ) {
  404. //Convert both paths to the canonical form before comparison.
  405. $muPluginDir = realpath(WPMU_PLUGIN_DIR);
  406. $pluginPath = realpath($this->pluginAbsolutePath);
  407. $cachedResult = (strpos($pluginPath, $muPluginDir) === 0);
  408. }
  409. return $cachedResult;
  410. }
  411. /**
  412. * MU plugins are partially supported, but only when we know which file in mu-plugins
  413. * corresponds to this plugin.
  414. *
  415. * @return bool
  416. */
  417. protected function isUnknownMuPlugin() {
  418. return empty($this->muPluginFile) && $this->isMuPlugin();
  419. }
  420. /**
  421. * Clear the cached plugin version. This method can be set up as a filter (hook) and will
  422. * return the filter argument unmodified.
  423. *
  424. * @param mixed $filterArgument
  425. * @return mixed
  426. */
  427. public function clearCachedVersion($filterArgument = null) {
  428. $this->cachedInstalledVersion = null;
  429. return $filterArgument;
  430. }
  431. /**
  432. * Get absolute path to the main plugin file.
  433. *
  434. * @return string
  435. */
  436. public function getAbsolutePath() {
  437. return $this->pluginAbsolutePath;
  438. }
  439. /**
  440. * Register a callback for filtering query arguments.
  441. *
  442. * The callback function should take one argument - an associative array of query arguments.
  443. * It should return a modified array of query arguments.
  444. *
  445. * @uses add_filter() This method is a convenience wrapper for add_filter().
  446. *
  447. * @param callable $callback
  448. * @return void
  449. */
  450. public function addQueryArgFilter($callback){
  451. $this->addFilter('request_info_query_args', $callback);
  452. }
  453. /**
  454. * Register a callback for filtering arguments passed to wp_remote_get().
  455. *
  456. * The callback function should take one argument - an associative array of arguments -
  457. * and return a modified array or arguments. See the WP documentation on wp_remote_get()
  458. * for details on what arguments are available and how they work.
  459. *
  460. * @uses add_filter() This method is a convenience wrapper for add_filter().
  461. *
  462. * @param callable $callback
  463. * @return void
  464. */
  465. public function addHttpRequestArgFilter($callback) {
  466. $this->addFilter('request_info_options', $callback);
  467. }
  468. /**
  469. * Register a callback for filtering the plugin info retrieved from the external API.
  470. *
  471. * The callback function should take two arguments. If the plugin info was retrieved
  472. * successfully, the first argument passed will be an instance of PluginInfo. Otherwise,
  473. * it will be NULL. The second argument will be the corresponding return value of
  474. * wp_remote_get (see WP docs for details).
  475. *
  476. * The callback function should return a new or modified instance of PluginInfo or NULL.
  477. *
  478. * @uses add_filter() This method is a convenience wrapper for add_filter().
  479. *
  480. * @param callable $callback
  481. * @return void
  482. */
  483. public function addResultFilter($callback) {
  484. $this->addFilter('request_info_result', $callback, 10, 2);
  485. }
  486. protected function createDebugBarExtension() {
  487. return new Puc_v4p1_DebugBar_PluginExtension($this);
  488. }
  489. }
  490. endif;