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

Api::looksLikeVersion()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 11
rs 10
1
<?php
2
namespace YahnisElsts\PluginUpdateChecker\v5p0\Vcs;
3
4
use Parsedown;
5
use PucReadmeParser;
6
7
if ( !class_exists(Api::class, false) ):
8
9
	abstract class Api {
10
		const STRATEGY_LATEST_RELEASE = 'latest_release';
11
		const STRATEGY_LATEST_TAG = 'latest_tag';
12
		const STRATEGY_STABLE_TAG = 'stable_tag';
13
		const STRATEGY_BRANCH = 'branch';
14
15
		protected $tagNameProperty = 'name';
16
		protected $slug = '';
17
18
		/**
19
		 * @var string
20
		 */
21
		protected $repositoryUrl = '';
22
23
		/**
24
		 * @var mixed Authentication details for private repositories. Format depends on service.
25
		 */
26
		protected $credentials = null;
27
28
		/**
29
		 * @var string The filter tag that's used to filter options passed to wp_remote_get.
30
		 * For example, "puc_request_info_options-slug" or "puc_request_update_options_theme-slug".
31
		 */
32
		protected $httpFilterName = '';
33
34
		/**
35
		 * @var string The filter applied to the list of update detection strategies that
36
		 * are used to find the latest version.
37
		 */
38
		protected $strategyFilterName = '';
39
40
		/**
41
		 * @var string|null
42
		 */
43
		protected $localDirectory = null;
44
45
		/**
46
		 * Api constructor.
47
		 *
48
		 * @param string $repositoryUrl
49
		 * @param array|string|null $credentials
50
		 */
51
		public function __construct($repositoryUrl, $credentials = null) {
52
			$this->repositoryUrl = $repositoryUrl;
53
			$this->setAuthentication($credentials);
54
		}
55
56
		/**
57
		 * @return string
58
		 */
59
		public function getRepositoryUrl() {
60
			return $this->repositoryUrl;
61
		}
62
63
		/**
64
		 * Figure out which reference (i.e. tag or branch) contains the latest version.
65
		 *
66
		 * @param string $configBranch Start looking in this branch.
67
		 * @return null|Reference
68
		 */
69
		public function chooseReference($configBranch) {
70
			$strategies = $this->getUpdateDetectionStrategies($configBranch);
71
72
			if ( !empty($this->strategyFilterName) ) {
73
				$strategies = apply_filters(
74
					$this->strategyFilterName,
75
					$strategies,
76
					$this->slug
77
				);
78
			}
79
80
			foreach ($strategies as $strategy) {
81
				$reference = call_user_func($strategy);
82
				if ( !empty($reference) ) {
83
					return $reference;
84
				}
85
			}
86
			return null;
87
		}
88
89
		/**
90
		 * Get an ordered list of strategies that can be used to find the latest version.
91
		 *
92
		 * The update checker will try each strategy in order until one of them
93
		 * returns a valid reference.
94
		 *
95
		 * @param string $configBranch
96
		 * @return array<callable> Array of callables that return Vcs_Reference objects.
97
		 */
98
		abstract protected function getUpdateDetectionStrategies($configBranch);
99
100
		/**
101
		 * Get the readme.txt file from the remote repository and parse it
102
		 * according to the plugin readme standard.
103
		 *
104
		 * @param string $ref Tag or branch name.
105
		 * @return array Parsed readme.
106
		 */
107
		public function getRemoteReadme($ref = 'master') {
108
			$fileContents = $this->getRemoteFile($this->getLocalReadmeName(), $ref);
109
			if ( empty($fileContents) ) {
110
				return array();
111
			}
112
113
			$parser = new PucReadmeParser();
114
			return $parser->parse_readme_contents($fileContents);
115
		}
116
117
		/**
118
		 * Get the case-sensitive name of the local readme.txt file.
119
		 *
120
		 * In most cases it should just be called "readme.txt", but some plugins call it "README.txt",
121
		 * "README.TXT", or even "Readme.txt". Most VCS are case-sensitive so we need to know the correct
122
		 * capitalization.
123
		 *
124
		 * Defaults to "readme.txt" (all lowercase).
125
		 *
126
		 * @return string
127
		 */
128
		public function getLocalReadmeName() {
129
			static $fileName = null;
130
			if ( $fileName !== null ) {
131
				return $fileName;
132
			}
133
134
			$fileName = 'readme.txt';
135
			if ( isset($this->localDirectory) ) {
136
				$files = scandir($this->localDirectory);
137
				if ( !empty($files) ) {
138
					foreach ($files as $possibleFileName) {
139
						if ( strcasecmp($possibleFileName, 'readme.txt') === 0 ) {
140
							$fileName = $possibleFileName;
141
							break;
142
						}
143
					}
144
				}
145
			}
146
			return $fileName;
147
		}
148
149
		/**
150
		 * Get a branch.
151
		 *
152
		 * @param string $branchName
153
		 * @return Reference|null
154
		 */
155
		abstract public function getBranch($branchName);
156
157
		/**
158
		 * Get a specific tag.
159
		 *
160
		 * @param string $tagName
161
		 * @return Reference|null
162
		 */
163
		abstract public function getTag($tagName);
164
165
		/**
166
		 * Get the tag that looks like the highest version number.
167
		 * (Implementations should skip pre-release versions if possible.)
168
		 *
169
		 * @return Reference|null
170
		 */
171
		abstract public function getLatestTag();
172
173
		/**
174
		 * Check if a tag name string looks like a version number.
175
		 *
176
		 * @param string $name
177
		 * @return bool
178
		 */
179
		protected function looksLikeVersion($name) {
180
			//Tag names may be prefixed with "v", e.g. "v1.2.3".
181
			$name = ltrim($name, 'v');
182
183
			//The version string must start with a number.
184
			if ( !is_numeric(substr($name, 0, 1)) ) {
185
				return false;
186
			}
187
188
			//The goal is to accept any SemVer-compatible or "PHP-standardized" version number.
189
			return (preg_match('@^(\d{1,5}?)(\.\d{1,10}?){0,4}?($|[abrdp+_\-]|\s)@i', $name) === 1);
190
		}
191
192
		/**
193
		 * Check if a tag appears to be named like a version number.
194
		 *
195
		 * @param \stdClass $tag
196
		 * @return bool
197
		 */
198
		protected function isVersionTag($tag) {
199
			$property = $this->tagNameProperty;
200
			return isset($tag->$property) && $this->looksLikeVersion($tag->$property);
201
		}
202
203
		/**
204
		 * Sort a list of tags as if they were version numbers.
205
		 * Tags that don't look like version number will be removed.
206
		 *
207
		 * @param \stdClass[] $tags Array of tag objects.
208
		 * @return \stdClass[] Filtered array of tags sorted in descending order.
209
		 */
210
		protected function sortTagsByVersion($tags) {
211
			//Keep only those tags that look like version numbers.
212
			$versionTags = array_filter($tags, array($this, 'isVersionTag'));
213
			//Sort them in descending order.
214
			usort($versionTags, array($this, 'compareTagNames'));
215
216
			return $versionTags;
217
		}
218
219
		/**
220
		 * Compare two tags as if they were version number.
221
		 *
222
		 * @param \stdClass $tag1 Tag object.
223
		 * @param \stdClass $tag2 Another tag object.
224
		 * @return int
225
		 */
226
		protected function compareTagNames($tag1, $tag2) {
227
			$property = $this->tagNameProperty;
228
			if ( !isset($tag1->$property) ) {
229
				return 1;
230
			}
231
			if ( !isset($tag2->$property) ) {
232
				return -1;
233
			}
234
			return -version_compare(ltrim($tag1->$property, 'v'), ltrim($tag2->$property, 'v'));
235
		}
236
237
		/**
238
		 * Get the contents of a file from a specific branch or tag.
239
		 *
240
		 * @param string $path File name.
241
		 * @param string $ref
242
		 * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
243
		 */
244
		abstract public function getRemoteFile($path, $ref = 'master');
245
246
		/**
247
		 * Get the timestamp of the latest commit that changed the specified branch or tag.
248
		 *
249
		 * @param string $ref Reference name (e.g. branch or tag).
250
		 * @return string|null
251
		 */
252
		abstract public function getLatestCommitTime($ref);
253
254
		/**
255
		 * Get the contents of the changelog file from the repository.
256
		 *
257
		 * @param string $ref
258
		 * @param string $localDirectory Full path to the local plugin or theme directory.
259
		 * @return null|string The HTML contents of the changelog.
260
		 */
261
		public function getRemoteChangelog($ref, $localDirectory) {
262
			$filename = $this->findChangelogName($localDirectory);
263
			if ( empty($filename) ) {
264
				return null;
265
			}
266
267
			$changelog = $this->getRemoteFile($filename, $ref);
268
			if ( $changelog === null ) {
269
				return null;
270
			}
271
272
			return Parsedown::instance()->text($changelog);
273
		}
274
275
		/**
276
		 * Guess the name of the changelog file.
277
		 *
278
		 * @param string $directory
279
		 * @return string|null
280
		 */
281
		protected function findChangelogName($directory = null) {
282
			if ( !isset($directory) ) {
283
				$directory = $this->localDirectory;
284
			}
285
			if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
286
				return null;
287
			}
288
289
			$possibleNames = array('CHANGES.md', 'CHANGELOG.md', 'changes.md', 'changelog.md');
290
			$files = scandir($directory);
291
			$foundNames = array_intersect($possibleNames, $files);
292
293
			if ( !empty($foundNames) ) {
294
				return reset($foundNames);
295
			}
296
			return null;
297
		}
298
299
		/**
300
		 * Set authentication credentials.
301
		 *
302
		 * @param $credentials
303
		 */
304
		public function setAuthentication($credentials) {
305
			$this->credentials = $credentials;
306
		}
307
308
		public function isAuthenticationEnabled() {
309
			return !empty($this->credentials);
310
		}
311
312
		/**
313
		 * @param string $url
314
		 * @return string
315
		 */
316
		public function signDownloadUrl($url) {
317
			return $url;
318
		}
319
320
		/**
321
		 * @param string $filterName
322
		 */
323
		public function setHttpFilterName($filterName) {
324
			$this->httpFilterName = $filterName;
325
		}
326
327
		/**
328
		 * @param string $filterName
329
		 */
330
		public function setStrategyFilterName($filterName) {
331
			$this->strategyFilterName = $filterName;
332
		}
333
334
		/**
335
		 * @param string $directory
336
		 */
337
		public function setLocalDirectory($directory) {
338
			if ( empty($directory) || !is_dir($directory) || ($directory === '.') ) {
339
				$this->localDirectory = null;
340
			} else {
341
				$this->localDirectory = $directory;
342
			}
343
		}
344
345
		/**
346
		 * @param string $slug
347
		 */
348
		public function setSlug($slug) {
349
			$this->slug = $slug;
350
		}
351
	}
352
353
endif;
354