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

GitLabApi::setAuthentication()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 3
rs 10
1
<?php
2
namespace YahnisElsts\PluginUpdateChecker\v5p0\Vcs;
3
4
if ( !class_exists(GitLabApi::class, false) ):
5
6
	class GitLabApi extends Api {
7
		/**
8
		 * @var string GitLab username.
9
		 */
10
		protected $userName;
11
12
		/**
13
		 * @var string GitLab server host.
14
		 */
15
		protected $repositoryHost;
16
17
		/**
18
		 * @var string Protocol used by this GitLab server: "http" or "https".
19
		 */
20
		protected $repositoryProtocol = 'https';
21
22
		/**
23
		 * @var string GitLab repository name.
24
		 */
25
		protected $repositoryName;
26
27
		/**
28
		 * @var string GitLab authentication token. Optional.
29
		 */
30
		protected $accessToken;
31
32
		/**
33
		 * @var bool Whether to download release assets instead of the auto-generated source code archives.
34
		 */
35
		protected $releaseAssetsEnabled = false;
36
37
		/**
38
		 * @var bool Whether to download release asset package rather than release asset source.
39
		 */
40
		protected $releasePackageEnabled = false;
41
42
		public function __construct($repositoryUrl, $accessToken = null, $subgroup = null) {
43
			//Parse the repository host to support custom hosts.
44
			$port = wp_parse_url($repositoryUrl, PHP_URL_PORT);
45
			if ( !empty($port) ) {
46
				$port = ':' . $port;
47
			}
48
			$this->repositoryHost = wp_parse_url($repositoryUrl, PHP_URL_HOST) . $port;
49
50
			if ( $this->repositoryHost !== 'gitlab.com' ) {
51
				$this->repositoryProtocol = wp_parse_url($repositoryUrl, PHP_URL_SCHEME);
52
			}
53
54
			//Find the repository information
55
			$path = wp_parse_url($repositoryUrl, PHP_URL_PATH);
56
			if ( preg_match('@^/?(?P<username>[^/]+?)/(?P<repository>[^/#?&]+?)/?$@', $path, $matches) ) {
57
				$this->userName = $matches['username'];
58
				$this->repositoryName = $matches['repository'];
59
			} elseif ( ($this->repositoryHost === 'gitlab.com') ) {
60
				//This is probably a repository in a subgroup, e.g. "/organization/category/repo".
61
				$parts = explode('/', trim($path, '/'));
62
				if ( count($parts) < 3 ) {
63
					throw new \InvalidArgumentException('Invalid GitLab.com repository URL: "' . $repositoryUrl . '"');
64
				}
65
				$lastPart = array_pop($parts);
66
				$this->userName = implode('/', $parts);
67
				$this->repositoryName = $lastPart;
68
			} else {
69
				//There could be subgroups in the URL:  gitlab.domain.com/group/subgroup/subgroup2/repository
70
				if ( $subgroup !== null ) {
71
					$path = str_replace(trailingslashit($subgroup), '', $path);
72
				}
73
74
				//This is not a traditional url, it could be gitlab is in a deeper subdirectory.
75
				//Get the path segments.
76
				$segments = explode('/', untrailingslashit(ltrim($path, '/')));
77
78
				//We need at least /user-name/repository-name/
79
				if ( count($segments) < 2 ) {
80
					throw new \InvalidArgumentException('Invalid GitLab repository URL: "' . $repositoryUrl . '"');
81
				}
82
83
				//Get the username and repository name.
84
				$usernameRepo = array_splice($segments, -2, 2);
85
				$this->userName = $usernameRepo[0];
86
				$this->repositoryName = $usernameRepo[1];
87
88
				//Append the remaining segments to the host if there are segments left.
89
				if ( count($segments) > 0 ) {
90
					$this->repositoryHost = trailingslashit($this->repositoryHost) . implode('/', $segments);
91
				}
92
93
				//Add subgroups to username.
94
				if ( $subgroup !== null ) {
95
					$this->userName = $usernameRepo[0] . '/' . untrailingslashit($subgroup);
96
				}
97
			}
98
99
			parent::__construct($repositoryUrl, $accessToken);
100
		}
101
102
		/**
103
		 * Get the latest release from GitLab.
104
		 *
105
		 * @return Reference|null
106
		 */
107
		public function getLatestRelease() {
108
			$releases = $this->api('/:id/releases');
109
			if ( is_wp_error($releases) || empty($releases) || !is_array($releases) ) {
110
				return null;
111
			}
112
113
			foreach ($releases as $release) {
114
				if ( true !== $release->upcoming_release ) {
115
					break 1; //Break the loop on the first release we find that isn't an upcoming release
116
				}
117
			}
118
			if ( is_wp_error($release) || !is_object($release) || !isset($release->tag_name) ) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $release seems to be defined by a foreach iteration on line 113. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
119
				return null;
120
			}
121
122
			$reference = new Reference(array(
123
				'name'        => $release->tag_name,
124
				'version'     => ltrim($release->tag_name, 'v'), //Remove the "v" prefix from "v1.2.3".
125
				'downloadUrl' => '',
126
				'updated'     => $release->released_at,
127
				'apiResponse' => $release,
128
			));
129
			$download_url = false;
130
131
			if ( $this->releasePackageEnabled && isset($release->assets, $release->assets->links) ) {
132
				/**
133
				 * Use the first asset LINK that is a zip format file generated by a Gitlab Release Pipeline
134
				 *
135
				 * @link https://gist.github.com/timwiel/9dfd3526c768efad4973254085e065ce
136
				 */
137
				foreach ($release->assets->links as $link) {
138
					//TODO: Check the "format" property instead of the URL suffix.
139
					if ( 'zip' === substr($link->url, -3) ) {
140
						$download_url = $link->url;
141
						break 1;
142
					}
143
				}
144
				if ( empty( $download_url ) ) {
145
					return null;
146
				}
147
				if ( ! empty( $this->accessToken ) ) {
148
					$download_url = add_query_arg('private_token', $this->accessToken, $download_url);
149
				}
150
				$reference->downloadUrl = $download_url;
151
				return $reference;
152
153
			} elseif ( isset($release->assets) ) {
154
				/**
155
				 * Use the first asset SOURCE file that is a zip format from a Gitlab Release which should be a zip file
156
				 */
157
				foreach ($release->assets->sources as $source) {
158
					if ( 'zip' === $source->format ) {
159
						$download_url = $source->url;
160
						break 1;
161
					}
162
				}
163
				if ( empty( $download_url ) ) {
164
					return null;
165
				}
166
				if ( ! empty( $this->accessToken ) ) {
167
					$download_url = add_query_arg('private_token', $this->accessToken, $download_url);
168
				}
169
				$reference->downloadUrl = $download_url;
170
				return $reference;
171
172
			}
173
174
			//If we get this far without a return then obviosuly noi release download urls were found
175
			return null;
176
			}
177
178
		/**
179
		 * Get the tag that looks like the highest version number.
180
		 *
181
		 * @return Reference|null
182
		 */
183
		public function getLatestTag() {
184
			$tags = $this->api('/:id/repository/tags');
185
			if ( is_wp_error($tags) || empty($tags) || !is_array($tags) ) {
186
				return null;
187
			}
188
189
			$versionTags = $this->sortTagsByVersion($tags);
190
			if ( empty($versionTags) ) {
191
				return null;
192
			}
193
194
			$tag = $versionTags[0];
195
			return new Reference(array(
196
				'name'        => $tag->name,
197
				'version'     => ltrim($tag->name, 'v'),
198
				'downloadUrl' => $this->buildArchiveDownloadUrl($tag->name),
199
				'apiResponse' => $tag,
200
			));
201
		}
202
203
		/**
204
		 * Get a branch by name.
205
		 *
206
		 * @param string $branchName
207
		 * @return null|Reference
208
		 */
209
		public function getBranch($branchName) {
210
			$branch = $this->api('/:id/repository/branches/' . $branchName);
211
			if ( is_wp_error($branch) || empty($branch) ) {
212
				return null;
213
			}
214
215
			$reference = new Reference(array(
216
				'name'        => $branch->name,
217
				'downloadUrl' => $this->buildArchiveDownloadUrl($branch->name),
218
				'apiResponse' => $branch,
219
			));
220
221
			if ( isset($branch->commit, $branch->commit->committed_date) ) {
222
				$reference->updated = $branch->commit->committed_date;
223
			}
224
225
			return $reference;
226
		}
227
228
		/**
229
		 * Get the timestamp of the latest commit that changed the specified branch or tag.
230
		 *
231
		 * @param string $ref Reference name (e.g. branch or tag).
232
		 * @return string|null
233
		 */
234
		public function getLatestCommitTime($ref) {
235
			$commits = $this->api('/:id/repository/commits/', array('ref_name' => $ref));
236
			if ( is_wp_error($commits) || !is_array($commits) || !isset($commits[0]) ) {
237
				return null;
238
			}
239
240
			return $commits[0]->committed_date;
241
		}
242
243
		/**
244
		 * Perform a GitLab API request.
245
		 *
246
		 * @param string $url
247
		 * @param array $queryParams
248
		 * @return mixed|\WP_Error
249
		 */
250
		protected function api($url, $queryParams = array()) {
251
			$baseUrl = $url;
252
			$url = $this->buildApiUrl($url, $queryParams);
253
254
			$options = array('timeout' => 10);
255
			if ( !empty($this->httpFilterName) ) {
256
				$options = apply_filters($this->httpFilterName, $options);
257
			}
258
259
			$response = wp_remote_get($url, $options);
260
			if ( is_wp_error($response) ) {
261
				do_action('puc_api_error', $response, null, $url, $this->slug);
262
				return $response;
263
			}
264
265
			$code = wp_remote_retrieve_response_code($response);
266
			$body = wp_remote_retrieve_body($response);
267
			if ( $code === 200 ) {
268
				return json_decode($body);
269
			}
270
271
			$error = new \WP_Error(
272
				'puc-gitlab-http-error',
273
				sprintf('GitLab API error. URL: "%s",  HTTP status code: %d.', $baseUrl, $code)
274
			);
275
			do_action('puc_api_error', $error, $response, $url, $this->slug);
276
277
			return $error;
278
		}
279
280
		/**
281
		 * Build a fully qualified URL for an API request.
282
		 *
283
		 * @param string $url
284
		 * @param array $queryParams
285
		 * @return string
286
		 */
287
		protected function buildApiUrl($url, $queryParams) {
288
			$variables = array(
289
				'user' => $this->userName,
290
				'repo' => $this->repositoryName,
291
				'id'   => $this->userName . '/' . $this->repositoryName,
292
			);
293
294
			foreach ($variables as $name => $value) {
295
				$url = str_replace("/:{$name}", '/' . urlencode($value), $url);
296
			}
297
298
			$url = substr($url, 1);
299
			$url = sprintf('%1$s://%2$s/api/v4/projects/%3$s', $this->repositoryProtocol, $this->repositoryHost, $url);
300
301
			if ( !empty($this->accessToken) ) {
302
				$queryParams['private_token'] = $this->accessToken;
303
			}
304
305
			if ( !empty($queryParams) ) {
306
				$url = add_query_arg($queryParams, $url);
307
			}
308
309
			return $url;
310
		}
311
312
		/**
313
		 * Get the contents of a file from a specific branch or tag.
314
		 *
315
		 * @param string $path File name.
316
		 * @param string $ref
317
		 * @return null|string Either the contents of the file, or null if the file doesn't exist or there's an error.
318
		 */
319
		public function getRemoteFile($path, $ref = 'master') {
320
			$response = $this->api('/:id/repository/files/' . $path, array('ref' => $ref));
321
			if ( is_wp_error($response) || !isset($response->content) || $response->encoding !== 'base64' ) {
322
				return null;
323
			}
324
325
			return base64_decode($response->content);
326
		}
327
328
		/**
329
		 * Generate a URL to download a ZIP archive of the specified branch/tag/etc.
330
		 *
331
		 * @param string $ref
332
		 * @return string
333
		 */
334
		public function buildArchiveDownloadUrl($ref = 'master') {
335
			$url = sprintf(
336
				'%1$s://%2$s/api/v4/projects/%3$s/repository/archive.zip',
337
				$this->repositoryProtocol,
338
				$this->repositoryHost,
339
				urlencode($this->userName . '/' . $this->repositoryName)
340
			);
341
			$url = add_query_arg('sha', urlencode($ref), $url);
342
343
			if ( !empty($this->accessToken) ) {
344
				$url = add_query_arg('private_token', $this->accessToken, $url);
345
			}
346
347
			return $url;
348
		}
349
350
		/**
351
		 * Get a specific tag.
352
		 *
353
		 * @param string $tagName
354
		 * @return void
355
		 */
356
		public function getTag($tagName) {
357
			throw new \LogicException('The ' . __METHOD__ . ' method is not implemented and should not be used.');
358
		}
359
360
		protected function getUpdateDetectionStrategies($configBranch) {
361
			$strategies = array();
362
363
			if ( ($configBranch === 'main') || ($configBranch === 'master') ) {
364
				$strategies[self::STRATEGY_LATEST_RELEASE] = array($this, 'getLatestRelease');
365
				$strategies[self::STRATEGY_LATEST_TAG] = array($this, 'getLatestTag');
366
			}
367
368
			$strategies[self::STRATEGY_BRANCH] = function() use ($configBranch) {
369
				return $this->getBranch($configBranch);
370
			};
371
372
			return $strategies;
373
		}
374
375
		public function setAuthentication($credentials) {
376
			parent::setAuthentication($credentials);
377
			$this->accessToken = is_string($credentials) ? $credentials : null;
378
		}
379
380
		public function enableReleaseAssets() {
381
			$this->releaseAssetsEnabled  = true;
382
			$this->releasePackageEnabled = false;
383
		}
384
385
		public function enableReleasePackages() {
386
			$this->releaseAssetsEnabled  = false;
387
			$this->releasePackageEnabled = true;
388
		}
389
390
	}
391
392
endif;
393