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

UpdateChecker::isMuPlugin()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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