Passed
Push — main ( 44ea53...137754 )
by TARIQ
15:15 queued 02:39
created

Ui::isMyPluginFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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