Ui.php 9.6 KB

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