Ui.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. <?php
  2. namespace YahnisElsts\PluginUpdateChecker\v5p1\Plugin;
  3. if ( !class_exists('Ui', false) ):
  4. /**
  5. * Additional UI elements for plugins.
  6. */
  7. class Ui {
  8. private $updateChecker;
  9. private $manualCheckErrorTransient = '';
  10. /**
  11. * @param UpdateChecker $updateChecker
  12. */
  13. public function __construct($updateChecker) {
  14. $this->updateChecker = $updateChecker;
  15. $this->manualCheckErrorTransient = $this->updateChecker->getUniqueName('manual_check_errors');
  16. add_action('admin_init', array($this, 'onAdminInit'));
  17. }
  18. public function onAdminInit() {
  19. if ( $this->updateChecker->userCanInstallUpdates() ) {
  20. $this->handleManualCheck();
  21. add_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10, 3);
  22. add_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10, 2);
  23. add_action('all_admin_notices', array($this, 'displayManualCheckResult'));
  24. }
  25. }
  26. /**
  27. * Add a "View Details" link to the plugin row in the "Plugins" page. By default,
  28. * the new link will appear before the "Visit plugin site" link (if present).
  29. *
  30. * You can change the link text by using the "puc_view_details_link-$slug" filter.
  31. * Returning an empty string from the filter will disable the link.
  32. *
  33. * You can change the position of the link using the
  34. * "puc_view_details_link_position-$slug" filter.
  35. * Returning 'before' or 'after' will place the link immediately before/after
  36. * the "Visit plugin site" link.
  37. * Returning 'append' places the link after any existing links at the time of the hook.
  38. * Returning 'replace' replaces the "Visit plugin site" link.
  39. * Returning anything else disables the link when there is a "Visit plugin site" link.
  40. *
  41. * If there is no "Visit plugin site" link 'append' is always used!
  42. *
  43. * @param array $pluginMeta Array of meta links.
  44. * @param string $pluginFile
  45. * @param array $pluginData Array of plugin header data.
  46. * @return array
  47. */
  48. public function addViewDetailsLink($pluginMeta, $pluginFile, $pluginData = array()) {
  49. if ( $this->isMyPluginFile($pluginFile) && !isset($pluginData['slug']) ) {
  50. $linkText = apply_filters($this->updateChecker->getUniqueName('view_details_link'), __('View details'));
  51. if ( !empty($linkText) ) {
  52. $viewDetailsLinkPosition = 'append';
  53. //Find the "Visit plugin site" link (if present).
  54. $visitPluginSiteLinkIndex = count($pluginMeta) - 1;
  55. if ( $pluginData['PluginURI'] ) {
  56. $escapedPluginUri = esc_url($pluginData['PluginURI']);
  57. foreach ($pluginMeta as $linkIndex => $existingLink) {
  58. if ( strpos($existingLink, $escapedPluginUri) !== false ) {
  59. $visitPluginSiteLinkIndex = $linkIndex;
  60. $viewDetailsLinkPosition = apply_filters(
  61. $this->updateChecker->getUniqueName('view_details_link_position'),
  62. 'before'
  63. );
  64. break;
  65. }
  66. }
  67. }
  68. $viewDetailsLink = sprintf('<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
  69. esc_url(network_admin_url('plugin-install.php?tab=plugin-information&plugin=' . urlencode($this->updateChecker->slug) .
  70. '&TB_iframe=true&width=600&height=550')),
  71. esc_attr(sprintf(__('More information about %s'), $pluginData['Name'])),
  72. esc_attr($pluginData['Name']),
  73. $linkText
  74. );
  75. switch ($viewDetailsLinkPosition) {
  76. case 'before':
  77. array_splice($pluginMeta, $visitPluginSiteLinkIndex, 0, $viewDetailsLink);
  78. break;
  79. case 'after':
  80. array_splice($pluginMeta, $visitPluginSiteLinkIndex + 1, 0, $viewDetailsLink);
  81. break;
  82. case 'replace':
  83. $pluginMeta[$visitPluginSiteLinkIndex] = $viewDetailsLink;
  84. break;
  85. case 'append':
  86. default:
  87. $pluginMeta[] = $viewDetailsLink;
  88. break;
  89. }
  90. }
  91. }
  92. return $pluginMeta;
  93. }
  94. /**
  95. * Add a "Check for updates" link to the plugin row in the "Plugins" page. By default,
  96. * the new link will appear after the "Visit plugin site" link if present, otherwise
  97. * after the "View plugin details" link.
  98. *
  99. * You can change the link text by using the "puc_manual_check_link-$slug" filter.
  100. * Returning an empty string from the filter will disable the link.
  101. *
  102. * @param array $pluginMeta Array of meta links.
  103. * @param string $pluginFile
  104. * @return array
  105. */
  106. public function addCheckForUpdatesLink($pluginMeta, $pluginFile) {
  107. if ( $this->isMyPluginFile($pluginFile) ) {
  108. $linkUrl = wp_nonce_url(
  109. add_query_arg(
  110. array(
  111. 'puc_check_for_updates' => 1,
  112. 'puc_slug' => $this->updateChecker->slug,
  113. ),
  114. self_admin_url('plugins.php')
  115. ),
  116. 'puc_check_for_updates'
  117. );
  118. $linkText = apply_filters(
  119. $this->updateChecker->getUniqueName('manual_check_link'),
  120. __('Check for updates', 'plugin-update-checker')
  121. );
  122. if ( !empty($linkText) ) {
  123. /** @noinspection HtmlUnknownTarget */
  124. $pluginMeta[] = sprintf('<a href="%s">%s</a>', esc_attr($linkUrl), $linkText);
  125. }
  126. }
  127. return $pluginMeta;
  128. }
  129. protected function isMyPluginFile($pluginFile) {
  130. return ($pluginFile == $this->updateChecker->pluginFile)
  131. || (!empty($this->updateChecker->muPluginFile) && ($pluginFile == $this->updateChecker->muPluginFile));
  132. }
  133. /**
  134. * Check for updates when the user clicks the "Check for updates" link.
  135. *
  136. * @see self::addCheckForUpdatesLink()
  137. *
  138. * @return void
  139. */
  140. public function handleManualCheck() {
  141. $shouldCheck =
  142. isset($_GET['puc_check_for_updates'], $_GET['puc_slug'])
  143. && $_GET['puc_slug'] == $this->updateChecker->slug
  144. && check_admin_referer('puc_check_for_updates');
  145. if ( $shouldCheck ) {
  146. $update = $this->updateChecker->checkForUpdates();
  147. $status = ($update === null) ? 'no_update' : 'update_available';
  148. $lastRequestApiErrors = $this->updateChecker->getLastRequestApiErrors();
  149. if ( ($update === null) && !empty($lastRequestApiErrors) ) {
  150. //Some errors are not critical. For example, if PUC tries to retrieve the readme.txt
  151. //file from GitHub and gets a 404, that's an API error, but it doesn't prevent updates
  152. //from working. Maybe the plugin simply doesn't have a readme.
  153. //Let's only show important errors.
  154. $foundCriticalErrors = false;
  155. $questionableErrorCodes = array(
  156. 'puc-github-http-error',
  157. 'puc-gitlab-http-error',
  158. 'puc-bitbucket-http-error',
  159. );
  160. foreach ($lastRequestApiErrors as $item) {
  161. $wpError = $item['error'];
  162. /** @var \WP_Error $wpError */
  163. if ( !in_array($wpError->get_error_code(), $questionableErrorCodes) ) {
  164. $foundCriticalErrors = true;
  165. break;
  166. }
  167. }
  168. if ( $foundCriticalErrors ) {
  169. $status = 'error';
  170. set_site_transient($this->manualCheckErrorTransient, $lastRequestApiErrors, 60);
  171. }
  172. }
  173. wp_redirect(add_query_arg(
  174. array(
  175. 'puc_update_check_result' => $status,
  176. 'puc_slug' => $this->updateChecker->slug,
  177. ),
  178. self_admin_url('plugins.php')
  179. ));
  180. exit;
  181. }
  182. }
  183. /**
  184. * Display the results of a manual update check.
  185. *
  186. * @see self::handleManualCheck()
  187. *
  188. * You can change the result message by using the "puc_manual_check_message-$slug" filter.
  189. */
  190. public function displayManualCheckResult() {
  191. //phpcs:disable WordPress.Security.NonceVerification.Recommended -- Just displaying a message.
  192. if ( isset($_GET['puc_update_check_result'], $_GET['puc_slug']) && ($_GET['puc_slug'] == $this->updateChecker->slug) ) {
  193. $status = sanitize_key($_GET['puc_update_check_result']);
  194. $title = $this->updateChecker->getInstalledPackage()->getPluginTitle();
  195. $noticeClass = 'updated notice-success';
  196. $details = '';
  197. if ( $status == 'no_update' ) {
  198. $message = sprintf(_x('The %s plugin is up to date.', 'the plugin title', 'plugin-update-checker'), $title);
  199. } else if ( $status == 'update_available' ) {
  200. $message = sprintf(_x('A new version of the %s plugin is available.', 'the plugin title', 'plugin-update-checker'), $title);
  201. } else if ( $status === 'error' ) {
  202. $message = sprintf(_x('Could not determine if updates are available for %s.', 'the plugin title', 'plugin-update-checker'), $title);
  203. $noticeClass = 'error notice-error';
  204. $details = $this->formatManualCheckErrors(get_site_transient($this->manualCheckErrorTransient));
  205. delete_site_transient($this->manualCheckErrorTransient);
  206. } else {
  207. $message = sprintf(__('Unknown update checker status "%s"', 'plugin-update-checker'), $status);
  208. $noticeClass = 'error notice-error';
  209. }
  210. $message = esc_html($message);
  211. //Plugins can replace the message with their own, including adding HTML.
  212. $message = apply_filters(
  213. $this->updateChecker->getUniqueName('manual_check_message'),
  214. $message,
  215. $status
  216. );
  217. printf(
  218. '<div class="notice %s is-dismissible"><p>%s</p>%s</div>',
  219. esc_attr($noticeClass),
  220. //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Was escaped above, and plugins can add HTML.
  221. $message,
  222. //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Contains HTML. Content should already be escaped.
  223. $details
  224. );
  225. }
  226. //phpcs:enable
  227. }
  228. /**
  229. * Format the list of errors that were thrown during an update check.
  230. *
  231. * @param array $errors
  232. * @return string
  233. */
  234. protected function formatManualCheckErrors($errors) {
  235. if ( empty($errors) ) {
  236. return '';
  237. }
  238. $output = '';
  239. $showAsList = count($errors) > 1;
  240. if ( $showAsList ) {
  241. $output .= '<ol>';
  242. $formatString = '<li>%1$s <code>%2$s</code></li>';
  243. } else {
  244. $formatString = '<p>%1$s <code>%2$s</code></p>';
  245. }
  246. foreach ($errors as $item) {
  247. $wpError = $item['error'];
  248. /** @var \WP_Error $wpError */
  249. $output .= sprintf(
  250. $formatString,
  251. esc_html($wpError->get_error_message()),
  252. esc_html($wpError->get_error_code())
  253. );
  254. }
  255. if ( $showAsList ) {
  256. $output .= '</ol>';
  257. }
  258. return $output;
  259. }
  260. public function removeHooks() {
  261. remove_action('admin_init', array($this, 'onAdminInit'));
  262. remove_filter('plugin_row_meta', array($this, 'addViewDetailsLink'), 10);
  263. remove_filter('plugin_row_meta', array($this, 'addCheckForUpdatesLink'), 10);
  264. remove_action('all_admin_notices', array($this, 'displayManualCheckResult'));
  265. }
  266. }
  267. endif;