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

PucFactory::getVcsService()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 14
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 25
rs 9.7998
1
<?php
2
3
namespace YahnisElsts\PluginUpdateChecker\v5p0;
4
5
use YahnisElsts\PluginUpdateChecker\v5p0\Plugin;
6
use YahnisElsts\PluginUpdateChecker\v5p0\Theme;
7
use YahnisElsts\PluginUpdateChecker\v5p0\Vcs;
8
9
if ( !class_exists(PucFactory::class, false) ):
10
11
	/**
12
	 * A factory that builds update checker instances.
13
	 *
14
	 * When multiple versions of the same class have been loaded (e.g. PluginUpdateChecker 4.0
15
	 * and 4.1), this factory will always use the latest available minor version. Register class
16
	 * versions by calling {@link PucFactory::addVersion()}.
17
	 *
18
	 * At the moment it can only build instances of the UpdateChecker class. Other classes are
19
	 * intended mainly for internal use and refer directly to specific implementations.
20
	 */
21
	class PucFactory {
22
		protected static $classVersions = array();
23
		protected static $sorted = false;
24
25
		protected static $myMajorVersion = '';
26
		protected static $latestCompatibleVersion = '';
27
28
		/**
29
		 * A wrapper method for buildUpdateChecker() that reads the metadata URL from the plugin or theme header.
30
		 *
31
		 * @param string $fullPath Full path to the main plugin file or the theme's style.css.
32
		 * @param array $args Optional arguments. Keys should match the argument names of the buildUpdateChecker() method.
33
		 * @return Plugin\UpdateChecker|Theme\UpdateChecker|Vcs\BaseChecker
34
		 */
35
		public static function buildFromHeader($fullPath, $args = array()) {
36
			$fullPath = self::normalizePath($fullPath);
37
38
			//Set up defaults.
39
			$defaults = array(
40
				'metadataUrl'  => '',
41
				'slug'         => '',
42
				'checkPeriod'  => 12,
43
				'optionName'   => '',
44
				'muPluginFile' => '',
45
			);
46
			$args = array_merge($defaults, array_intersect_key($args, $defaults));
47
			extract($args, EXTR_SKIP);
48
49
			//Check for the service URI
50
			if ( empty($metadataUrl) ) {
51
				$metadataUrl = self::getServiceURI($fullPath);
52
			}
53
54
			return self::buildUpdateChecker($metadataUrl, $fullPath, $slug, $checkPeriod, $optionName, $muPluginFile);
55
		}
56
57
		/**
58
		 * Create a new instance of the update checker.
59
		 *
60
		 * This method automatically detects if you're using it for a plugin or a theme and chooses
61
		 * the appropriate implementation for your update source (JSON file, GitHub, BitBucket, etc).
62
		 *
63
		 * @see UpdateChecker::__construct
64
		 *
65
		 * @param string $metadataUrl The URL of the metadata file, a GitHub repository, or another supported update source.
66
		 * @param string $fullPath Full path to the main plugin file or to the theme directory.
67
		 * @param string $slug Custom slug. Defaults to the name of the main plugin file or the theme directory.
68
		 * @param int $checkPeriod How often to check for updates (in hours).
69
		 * @param string $optionName Where to store bookkeeping info about update checks.
70
		 * @param string $muPluginFile The plugin filename relative to the mu-plugins directory.
71
		 * @return Plugin\UpdateChecker|Theme\UpdateChecker|Vcs\BaseChecker
72
		 */
73
		public static function buildUpdateChecker($metadataUrl, $fullPath, $slug = '', $checkPeriod = 12, $optionName = '', $muPluginFile = '') {
74
			$fullPath = self::normalizePath($fullPath);
75
			$id = null;
76
77
			//Plugin or theme?
78
			$themeDirectory = self::getThemeDirectoryName($fullPath);
79
			if ( self::isPluginFile($fullPath) ) {
80
				$type = 'Plugin';
81
				$id = $fullPath;
82
			} else if ( $themeDirectory !== null ) {
83
				$type = 'Theme';
84
				$id = $themeDirectory;
85
			} else {
86
				throw new \RuntimeException(sprintf(
87
					'The update checker cannot determine if "%s" is a plugin or a theme. ' .
88
					'This is a bug. Please contact the PUC developer.',
89
					htmlentities($fullPath)
90
				));
91
			}
92
93
			//Which hosting service does the URL point to?
94
			$service = self::getVcsService($metadataUrl);
95
96
			$apiClass = null;
97
			if ( empty($service) ) {
98
				//The default is to get update information from a remote JSON file.
99
				$checkerClass = $type . '\\UpdateChecker';
100
			} else {
101
				//You can also use a VCS repository like GitHub.
102
				$checkerClass = 'Vcs\\' . $type . 'UpdateChecker';
103
				$apiClass = $service . 'Api';
104
			}
105
106
			$checkerClass = self::getCompatibleClassVersion($checkerClass);
107
			if ( $checkerClass === null ) {
108
				//phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
109
				trigger_error(
110
					esc_html(sprintf(
111
						'PUC %s does not support updates for %ss %s',
112
						self::$latestCompatibleVersion,
113
						strtolower($type),
114
						$service ? ('hosted on ' . $service) : 'using JSON metadata'
115
					)),
116
					E_USER_ERROR
117
				);
118
			}
119
120
			if ( !isset($apiClass) ) {
121
				//Plain old update checker.
122
				return new $checkerClass($metadataUrl, $id, $slug, $checkPeriod, $optionName, $muPluginFile);
123
			} else {
124
				//VCS checker + an API client.
125
				$apiClass = self::getCompatibleClassVersion($apiClass);
126
				if ( $apiClass === null ) {
127
					//phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
128
					trigger_error(esc_html(sprintf(
129
						'PUC %s does not support %s',
130
						self::$latestCompatibleVersion,
131
						$service
132
					)), E_USER_ERROR);
133
				}
134
135
				return new $checkerClass(
136
					new $apiClass($metadataUrl),
137
					$id,
138
					$slug,
139
					$checkPeriod,
140
					$optionName,
141
					$muPluginFile
142
				);
143
			}
144
		}
145
146
		/**
147
		 *
148
		 * Normalize a filesystem path. Introduced in WP 3.9.
149
		 * Copying here allows use of the class on earlier versions.
150
		 * This version adapted from WP 4.8.2 (unchanged since 4.5.0)
151
		 *
152
		 * @param string $path Path to normalize.
153
		 * @return string Normalized path.
154
		 */
155
		public static function normalizePath($path) {
156
			if ( function_exists('wp_normalize_path') ) {
157
				return wp_normalize_path($path);
158
			}
159
			$path = str_replace('\\', '/', $path);
160
			$path = preg_replace('|(?<=.)/+|', '/', $path);
161
			if ( substr($path, 1, 1) === ':' ) {
162
				$path = ucfirst($path);
163
			}
164
			return $path;
165
		}
166
167
		/**
168
		 * Check if the path points to a plugin file.
169
		 *
170
		 * @param string $absolutePath Normalized path.
171
		 * @return bool
172
		 */
173
		protected static function isPluginFile($absolutePath) {
174
			//Is the file inside the "plugins" or "mu-plugins" directory?
175
			$pluginDir = self::normalizePath(WP_PLUGIN_DIR);
176
			$muPluginDir = self::normalizePath(WPMU_PLUGIN_DIR);
177
			if ( (strpos($absolutePath, $pluginDir) === 0) || (strpos($absolutePath, $muPluginDir) === 0) ) {
178
				return true;
179
			}
180
181
			//Is it a file at all? Caution: is_file() can fail if the parent dir. doesn't have the +x permission set.
182
			if ( !is_file($absolutePath) ) {
183
				return false;
184
			}
185
186
			//Does it have a valid plugin header?
187
			//This is a last-ditch check for plugins symlinked from outside the WP root.
188
			if ( function_exists('get_file_data') ) {
189
				$headers = get_file_data($absolutePath, array('Name' => 'Plugin Name'), 'plugin');
190
				return !empty($headers['Name']);
191
			}
192
193
			return false;
194
		}
195
196
		/**
197
		 * Get the name of the theme's directory from a full path to a file inside that directory.
198
		 * E.g. "/abc/public_html/wp-content/themes/foo/whatever.php" => "foo".
199
		 *
200
		 * Note that subdirectories are currently not supported. For example,
201
		 * "/xyz/wp-content/themes/my-theme/includes/whatever.php" => NULL.
202
		 *
203
		 * @param string $absolutePath Normalized path.
204
		 * @return string|null Directory name, or NULL if the path doesn't point to a theme.
205
		 */
206
		protected static function getThemeDirectoryName($absolutePath) {
207
			if ( is_file($absolutePath) ) {
208
				$absolutePath = dirname($absolutePath);
209
			}
210
211
			if ( file_exists($absolutePath . '/style.css') ) {
212
				return basename($absolutePath);
213
			}
214
			return null;
215
		}
216
217
		/**
218
		 * Get the service URI from the file header.
219
		 *
220
		 * @param string $fullPath
221
		 * @return string
222
		 */
223
		private static function getServiceURI($fullPath) {
224
			//Look for the URI
225
			if ( is_readable($fullPath) ) {
226
				$seek = array(
227
					'github' => 'GitHub URI',
228
					'gitlab' => 'GitLab URI',
229
					'bucket' => 'BitBucket URI',
230
				);
231
				$seek = apply_filters('puc_get_source_uri', $seek);
232
				$data = get_file_data($fullPath, $seek);
233
				foreach ($data as $key => $uri) {
234
					if ( $uri ) {
235
						return $uri;
236
					}
237
				}
238
			}
239
240
			//URI was not found so throw an error.
241
			throw new \RuntimeException(
242
				sprintf('Unable to locate URI in header of "%s"', htmlentities($fullPath))
243
			);
244
		}
245
246
		/**
247
		 * Get the name of the hosting service that the URL points to.
248
		 *
249
		 * @param string $metadataUrl
250
		 * @return string|null
251
		 */
252
		private static function getVcsService($metadataUrl) {
253
			$service = null;
254
255
			//Which hosting service does the URL point to?
256
			$host = (string)(wp_parse_url($metadataUrl, PHP_URL_HOST));
257
			$path = (string)(wp_parse_url($metadataUrl, PHP_URL_PATH));
258
259
			//Check if the path looks like "/user-name/repository".
260
			//For GitLab.com it can also be "/user/group1/group2/.../repository".
261
			$repoRegex = '@^/?([^/]+?)/([^/#?&]+?)/?$@';
262
			if ( $host === 'gitlab.com' ) {
263
				$repoRegex = '@^/?(?:[^/#?&]++/){1,20}(?:[^/#?&]++)/?$@';
264
			}
265
			if ( preg_match($repoRegex, $path) ) {
266
				$knownServices = array(
267
					'github.com' => 'GitHub',
268
					'bitbucket.org' => 'BitBucket',
269
					'gitlab.com' => 'GitLab',
270
				);
271
				if ( isset($knownServices[$host]) ) {
272
					$service = $knownServices[$host];
273
				}
274
			}
275
276
			return apply_filters('puc_get_vcs_service', $service, $host, $path, $metadataUrl);
277
		}
278
279
		/**
280
		 * Get the latest version of the specified class that has the same major version number
281
		 * as this factory class.
282
		 *
283
		 * @param string $class Partial class name.
284
		 * @return string|null Full class name.
285
		 */
286
		protected static function getCompatibleClassVersion($class) {
287
			if ( isset(self::$classVersions[$class][self::$latestCompatibleVersion]) ) {
288
				return self::$classVersions[$class][self::$latestCompatibleVersion];
289
			}
290
			return null;
291
		}
292
293
		/**
294
		 * Get the specific class name for the latest available version of a class.
295
		 *
296
		 * @param string $class
297
		 * @return null|string
298
		 */
299
		public static function getLatestClassVersion($class) {
300
			if ( !self::$sorted ) {
301
				self::sortVersions();
302
			}
303
304
			if ( isset(self::$classVersions[$class]) ) {
305
				return reset(self::$classVersions[$class]);
306
			} else {
307
				return null;
308
			}
309
		}
310
311
		/**
312
		 * Sort available class versions in descending order (i.e. newest first).
313
		 */
314
		protected static function sortVersions() {
315
			foreach ( self::$classVersions as $class => $versions ) {
316
				uksort($versions, array(__CLASS__, 'compareVersions'));
317
				self::$classVersions[$class] = $versions;
318
			}
319
			self::$sorted = true;
320
		}
321
322
		protected static function compareVersions($a, $b) {
323
			return -version_compare($a, $b);
324
		}
325
326
		/**
327
		 * Register a version of a class.
328
		 *
329
		 * @access private This method is only for internal use by the library.
330
		 *
331
		 * @param string $generalClass Class name without version numbers, e.g. 'PluginUpdateChecker'.
332
		 * @param string $versionedClass Actual class name, e.g. 'PluginUpdateChecker_1_2'.
333
		 * @param string $version Version number, e.g. '1.2'.
334
		 */
335
		public static function addVersion($generalClass, $versionedClass, $version) {
336
			if ( empty(self::$myMajorVersion) ) {
337
				$lastNamespaceSegment = substr(__NAMESPACE__, strrpos(__NAMESPACE__, '\\') + 1);
338
				self::$myMajorVersion = substr(ltrim($lastNamespaceSegment, 'v'), 0, 1);
339
			}
340
341
			//Store the greatest version number that matches our major version.
342
			$components = explode('.', $version);
343
			if ( $components[0] === self::$myMajorVersion ) {
344
345
				if (
346
					empty(self::$latestCompatibleVersion)
347
					|| version_compare($version, self::$latestCompatibleVersion, '>')
348
				) {
349
					self::$latestCompatibleVersion = $version;
350
				}
351
352
			}
353
354
			if ( !isset(self::$classVersions[$generalClass]) ) {
355
				self::$classVersions[$generalClass] = array();
356
			}
357
			self::$classVersions[$generalClass][$version] = $versionedClass;
358
			self::$sorted = false;
359
		}
360
	}
361
362
endif;
363