PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.2
WooCommerce v10.9.2
11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / vendor / opis / uri / src / Uri.php
woocommerce / vendor / opis / uri / src Last commit date
Punycode.php 1 year ago PunycodeException.php 1 year ago Uri.php 1 year ago UriTemplate.php 1 year ago
Uri.php
965 lines
1 <?php
2 /* ============================================================================
3 * Copyright 2021 Zindex Software
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 * ============================================================================ */
17
18 namespace Opis\Uri;
19
20 use Opis\String\UnicodeString;
21
22 class Uri
23 {
24 protected const URI_REGEX = '`^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$`';
25
26 protected const SCHEME_REGEX = '`^[a-z][a-z0-9-+.]*$`i';
27
28 protected const USER_OR_PASS_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-._~!$&\'()*+,:;=]+)*$`i';
29
30 protected const USERINFO_REGEX = '`^(?<user>[^:]+)(?::(?<pass>.*))?$`';
31
32 protected const HOST_LABEL_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-]+)*$`i';
33
34 protected const AUTHORITY_REGEX = '`^(?:(?<userinfo>[^@]+)\@)?(?<host>(\[[a-f0-9:]+\]|[^:]+))(?::(?<port>\d+))?$`i';
35
36 protected const PATH_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-._~!$&\'()*+,;=:@/]+)*$`i';
37
38 protected const QUERY_OR_FRAGMENT_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-._~!$&\'"()\[\]*+,;=:@?/%]+)*$`i';
39
40 protected array $components;
41
42 protected ?string $str = null;
43
44 /**
45 * @param array $components An array of normalized components
46 */
47 public function __construct(array $components)
48 {
49 $this->components = $components + [
50 'scheme' => null,
51 'user' => null,
52 'pass' => null,
53 'host' => null,
54 'port' => null,
55 'path' => null,
56 'query' => null,
57 'fragment' => null,
58 ];
59 }
60
61 /**
62 * @return string|null
63 */
64 public function scheme(): ?string
65 {
66 return $this->components['scheme'];
67 }
68
69 /**
70 * @return string|null
71 */
72 public function user(): ?string
73 {
74 return $this->components['user'];
75 }
76
77 /**
78 * @return string|null
79 */
80 public function pass(): ?string
81 {
82 return $this->components['pass'];
83 }
84
85 /**
86 * @return string|null
87 */
88 public function userInfo(): ?string
89 {
90 if ($this->components['user'] === null) {
91 return null;
92 }
93
94 if ($this->components['pass'] === null) {
95 return $this->components['user'];
96 }
97
98 return $this->components['user'] . ':' . $this->components['pass'];
99 }
100
101 /**
102 * @return string|null
103 */
104 public function host(): ?string
105 {
106 return $this->components['host'];
107 }
108
109 /**
110 * @return int|null
111 */
112 public function port(): ?int
113 {
114 return $this->components['port'];
115 }
116
117 /**
118 * @return string|null
119 */
120 public function authority(): ?string
121 {
122 if ($this->components['host'] === null) {
123 return null;
124 }
125
126 $authority = $this->userInfo();
127 if ($authority !== null) {
128 $authority .= '@';
129 }
130
131 $authority .= $this->components['host'];
132
133 if ($this->components['port'] !== null) {
134 $authority .= ':' . $this->components['port'];
135 }
136
137 return $authority;
138 }
139
140 /**
141 * @return string|null
142 */
143 public function path(): ?string
144 {
145 return $this->components['path'];
146 }
147
148 /**
149 * @return string|null
150 */
151 public function query(): ?string
152 {
153 return $this->components['query'];
154 }
155
156 /**
157 * @return string|null
158 */
159 public function fragment(): ?string
160 {
161 return $this->components['fragment'];
162 }
163
164 /**
165 * @return array|null[]
166 */
167 public function components(): array
168 {
169 return $this->components;
170 }
171
172 /**
173 * @return bool
174 */
175 public function isAbsolute(): bool
176 {
177 return $this->components['scheme'] !== null;
178 }
179
180 /**
181 * Use this URI as base to resolve the reference
182 * @param static|string|array $ref
183 * @param bool $normalize
184 * @return $this|null
185 */
186 public function resolveRef($ref, bool $normalize = false): ?self
187 {
188 $ref = self::resolveComponents($ref);
189 if ($ref === null) {
190 return $this;
191 }
192
193 return new static(self::mergeComponents($ref, $this->components, $normalize));
194 }
195
196 /**
197 * Resolve this URI reference using a base URI
198 * @param static|string|array $base
199 * @param bool $normalize
200 * @return static
201 */
202 public function resolve($base, bool $normalize = false): self
203 {
204 if ($this->isAbsolute()) {
205 return $this;
206 }
207
208 $base = self::resolveComponents($base);
209
210 if ($base === null) {
211 return $this;
212 }
213
214 return new static(self::mergeComponents($this->components, $base, $normalize));
215 }
216
217 /**
218 * @return string
219 */
220 public function __toString(): string
221 {
222 if ($this->str !== null) {
223 return $this->str;
224 }
225
226 $str = '';
227
228 if ($this->components['scheme'] !== null) {
229 $str .= $this->components['scheme'] . ':';
230 }
231
232 if ($this->components['host'] !== null) {
233 $str .= '//' . $this->authority();
234 }
235
236 $str .= $this->components['path'];
237
238 if ($this->components['query'] !== null) {
239 $str .= '?' . $this->components['query'];
240 }
241
242 if ($this->components['fragment'] !== null) {
243 $str .= '#' . $this->components['fragment'];
244 }
245
246 return $this->str = $str;
247 }
248
249 /**
250 * @param string $uri
251 * @param bool $normalize
252 * @return static|null
253 */
254 public static function create(string $uri, bool $normalize = false): ?self
255 {
256 $comp = self::parseComponents($uri);
257 if (!$comp) {
258 return null;
259 }
260
261 if ($normalize) {
262 $comp = self::normalizeComponents($comp);
263 }
264
265 return new static($comp);
266 }
267
268 /**
269 * Checks if the scheme contains valid chars
270 * @param string $scheme
271 * @return bool
272 */
273 public static function isValidScheme(string $scheme): bool
274 {
275 return (bool)preg_match(self::SCHEME_REGEX, $scheme);
276 }
277
278 /**
279 * Checks if user contains valid chars
280 * @param string $user
281 * @return bool
282 */
283 public static function isValidUser(string $user): bool
284 {
285 return (bool)preg_match(self::USER_OR_PASS_REGEX, $user);
286 }
287
288 /**
289 * Checks if pass contains valid chars
290 * @param string $pass
291 * @return bool
292 */
293 public static function isValidPass(string $pass): bool
294 {
295 return (bool)preg_match(self::USER_OR_PASS_REGEX, $pass);
296 }
297
298 /**
299 * @param string $userInfo
300 * @return bool
301 */
302 public static function isValidUserInfo(string $userInfo): bool
303 {
304 /** @var array|string $userInfo */
305
306 if (!preg_match(self::USERINFO_REGEX, $userInfo, $userInfo)) {
307 return false;
308 }
309
310 if (!self::isValidUser($userInfo['user'])) {
311 return false;
312 }
313
314 if (isset($userInfo['pass'])) {
315 return self::isValidPass($userInfo['pass']);
316 }
317
318 return true;
319 }
320
321 /**
322 * Checks if host is valid
323 * @param string $host
324 * @return bool
325 */
326 public static function isValidHost(string $host): bool
327 {
328 // min and max length
329 if ($host === '' || isset($host[253])) {
330 return false;
331 }
332
333 // check ipv6
334 if ($host[0] === '[') {
335 if ($host[-1] !== ']') {
336 return false;
337 }
338
339 return filter_var(
340 substr($host, 1, -1),
341 \FILTER_VALIDATE_IP,
342 \FILTER_FLAG_IPV6
343 ) !== false;
344 }
345
346 // check ipv4
347 if (preg_match('`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\$`', $host)) {
348 return \filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) !== false;
349 }
350
351 foreach (explode('.', $host) as $host) {
352 // empty or too long label
353 if ($host === '' || isset($host[63])) {
354 return false;
355 }
356 if ($host[0] === '-' || $host[-1] === '-') {
357 return false;
358 }
359 if (!preg_match(self::HOST_LABEL_REGEX, $host)) {
360 return false;
361 }
362 }
363
364 return true;
365 }
366
367 /**
368 * Checks if the port is valid
369 * @param int $port
370 * @return bool
371 */
372 public static function isValidPort(int $port): bool
373 {
374 return $port >= 0 && $port <= 65535;
375 }
376
377 /**
378 * Checks if authority contains valid chars
379 * @param string $authority
380 * @return bool
381 */
382 public static function isValidAuthority(string $authority): bool
383 {
384 if ($authority === '') {
385 return true;
386 }
387
388 /** @var array|string $authority */
389
390 if (!preg_match(self::AUTHORITY_REGEX, $authority, $authority)) {
391 return false;
392 }
393
394 if (isset($authority['port']) && !self::isValidPort((int)$authority['port'])) {
395 return false;
396 }
397
398 if (isset($authority['userinfo']) && !self::isValidUserInfo($authority['userinfo'])) {
399 return false;
400 }
401
402 return self::isValidHost($authority['host']);
403 }
404
405 /**
406 * Checks if the path contains valid chars
407 * @param string $path
408 * @return bool
409 */
410 public static function isValidPath(string $path): bool
411 {
412 return $path === '' || (bool)preg_match(self::PATH_REGEX, $path);
413 }
414
415 /**
416 * Checks if the query string contains valid chars
417 * @param string $query
418 * @return bool
419 */
420 public static function isValidQuery(string $query): bool
421 {
422 return $query === '' || (bool)preg_match(self::QUERY_OR_FRAGMENT_REGEX, $query);
423 }
424
425 /**
426 * Checks if the fragment contains valid chars
427 * @param string $fragment
428 * @return bool
429 */
430 public static function isValidFragment(string $fragment): bool
431 {
432 return $fragment === '' || (bool)preg_match(self::QUERY_OR_FRAGMENT_REGEX, $fragment);
433 }
434
435 /**
436 * @param string $uri
437 * @param bool $expand_authority
438 * @param bool $validate
439 * @return array|null
440 */
441 public static function parseComponents(string $uri, bool $expand_authority = true, bool $validate = true): ?array
442 {
443 if (!preg_match(self::URI_REGEX, $uri, $uri)) {
444 return null;
445 }
446
447 $comp = [];
448
449 // scheme
450 if (isset($uri[2]) && $uri[2] !== '') {
451 if ($validate && !self::isValidScheme($uri[2])) {
452 return null;
453 }
454 $comp['scheme'] = $uri[2];
455 }
456
457 // authority
458 if (isset($uri[4]) && isset($uri[3][0])) {
459 if ($uri[4] === '') {
460 if ($expand_authority) {
461 $comp['host'] = '';
462 } else {
463 $comp['authority'] = '';
464 }
465 } elseif ($expand_authority) {
466 $au = self::parseAuthorityComponents($uri[4], $validate);
467 if ($au === null) {
468 return null;
469 }
470 $comp += $au;
471 unset($au);
472 } else {
473 if ($validate && !self::isValidAuthority($uri[4])) {
474 return null;
475 }
476 $comp['authority'] = $uri[4];
477 }
478 }
479
480 // path
481 if (isset($uri[5])) {
482 if ($validate && !self::isValidPath($uri[5])) {
483 return null;
484 }
485 $comp['path'] = $uri[5];
486 // not a relative uri, remove dot segments
487 if (isset($comp['scheme']) || isset($comp['authority']) || isset($comp['host'])) {
488 $comp['path'] = self::removeDotSegmentsFromPath($comp['path']);
489 }
490 }
491
492 // query
493 if (isset($uri[7]) && isset($uri[6][0])) {
494 if ($validate && !self::isValidQuery($uri[7])) {
495 return null;
496 }
497 $comp['query'] = $uri[7];
498 }
499
500 // fragment
501 if (isset($uri[9]) && isset($uri[8][0])) {
502 if ($validate && !self::isValidFragment($uri[9])) {
503 return null;
504 }
505 $comp['fragment'] = $uri[9];
506 }
507
508 return $comp;
509 }
510
511 /**
512 * @param self|string|array $uri
513 * @return array|null
514 */
515 public static function resolveComponents($uri): ?array
516 {
517 if ($uri instanceof self) {
518 return $uri->components;
519 }
520
521 if (is_string($uri)) {
522 return self::parseComponents($uri);
523 }
524
525 if (is_array($uri)) {
526 if (isset($uri['host'])) {
527 unset($uri['authority']);
528 } elseif (isset($uri['authority'])) {
529 $au = self::parseAuthorityComponents($uri['authority']);
530 unset($uri['authority']);
531 if ($au !== null) {
532 unset($uri['user'], $uri['pass'], $uri['host'], $uri['port']);
533 $uri += $au;
534 }
535 }
536 return $uri;
537 }
538
539 return null;
540 }
541
542 /**
543 * @param string $authority
544 * @param bool $validate
545 * @return array|null
546 */
547 public static function parseAuthorityComponents(string $authority, bool $validate = true): ?array
548 {
549 /** @var array|string $authority */
550
551 if (!preg_match(self::AUTHORITY_REGEX, $authority, $authority)) {
552 return null;
553 }
554
555 $comp = [];
556
557 // userinfo
558 if (isset($authority['userinfo']) && $authority['userinfo'] !== '') {
559 if (!preg_match(self::USERINFO_REGEX, $authority['userinfo'], $ui)) {
560 return null;
561 }
562
563 // user
564 if ($validate && !self::isValidUser($ui['user'])) {
565 return null;
566 }
567 $comp['user'] = $ui['user'];
568
569 // pass
570 if (isset($ui['pass']) && $ui['pass'] !== '') {
571 if ($validate && !self::isValidPass($ui['pass'])) {
572 return null;
573 }
574 $comp['pass'] = $ui['pass'];
575 }
576
577 unset($ui);
578 }
579
580 // host
581 if ($validate && !self::isValidHost($authority['host'])) {
582 return null;
583 }
584 $comp['host'] = $authority['host'];
585
586
587 // port
588 if (isset($authority['port'])) {
589 $authority['port'] = (int)$authority['port'];
590 if (!self::isValidPort($authority['port'])) {
591 return null;
592 }
593 $comp['port'] = $authority['port'];
594 }
595
596 return $comp;
597 }
598
599 /**
600 * @param array $ref
601 * @param array $base
602 * @param bool $normalize
603 * @return array
604 */
605 public static function mergeComponents(array $ref, array $base, bool $normalize = false): array
606 {
607 if (isset($ref['scheme'])) {
608 $dest = $ref;
609 } else {
610 $dest = [];
611
612 $dest['scheme'] = $base['scheme'] ?? null;
613
614 if (isset($ref['authority']) || isset($ref['host'])) {
615 $dest += $ref;
616 } else {
617 if (isset($base['authority'])) {
618 $dest['authority'] = $base['authority'];
619 } else {
620 $dest['user'] = $base['user'] ?? null;
621 $dest['pass'] = $base['pass'] ?? null;
622 $dest['host'] = $base['host'] ?? null;
623 $dest['port'] = $base['port'] ?? null;
624 }
625
626 if (!isset($ref['path'])) {
627 $ref['path'] = '';
628 }
629 if (!isset($base['path'])) {
630 $base['path'] = '';
631 }
632
633 if ($ref['path'] === '') {
634 $dest['path'] = $base['path'];
635 $dest['query'] = $ref['query'] ?? $base['query'] ?? null;
636 } else {
637 if ($ref['path'][0] === '/') {
638 $dest['path'] = $ref['path'];
639 } else {
640 if ((isset($base['authority']) || isset($base['host'])) && $base['path'] === '') {
641 $dest['path'] = '/' . $ref['path'];
642 } else {
643 $dest['path'] = $base['path'];
644
645 if ($dest['path'] !== '') {
646 $pos = strrpos($dest['path'], '/');
647 if ($pos === false) {
648 $dest['path'] = '';
649 } else {
650 $dest['path'] = substr($dest['path'], 0, $pos);
651 }
652
653 unset($pos);
654 }
655 $dest['path'] .= '/' . $ref['path'];
656 }
657 }
658
659 $dest['query'] = $ref['query'] ?? null;
660 }
661 }
662 }
663
664 $dest['fragment'] = $ref['fragment'] ?? null;
665
666 if ($normalize) {
667 return self::normalizeComponents($dest);
668 }
669
670 if (isset($dest['path'])) {
671 $dest['path'] = self::removeDotSegmentsFromPath($dest['path']);
672 }
673
674 return $dest;
675 }
676
677 public static function normalizeComponents(array $components): array
678 {
679 if (isset($components['scheme'])) {
680 $components['scheme'] = strtolower($components['scheme']);
681 // Remove default port
682 if (isset($components['port']) && self::getSchemePort($components['scheme']) === $components['port']) {
683 $components['port'] = null;
684 }
685 }
686
687 if (isset($components['host'])) {
688 $components['host'] = strtolower($components['host']);
689 }
690
691 if (isset($components['path'])) {
692 $components['path'] = self::removeDotSegmentsFromPath($components['path']);
693 }
694
695 if (isset($components['query'])) {
696 $components['query'] = self::normalizeQueryString($components['query']);
697 }
698
699 return $components;
700 }
701
702 /**
703 * Removes dot segments from path
704 * @param string $path
705 * @return string
706 */
707 public static function removeDotSegmentsFromPath(string $path): string
708 {
709 // Fast check common simple paths
710 if ($path === '' || $path === '/') {
711 return $path;
712 }
713
714 $output = '';
715 $last_slash = 0;
716
717 $len = strlen($path);
718 $i = 0;
719
720 while ($i < $len) {
721 if ($path[$i] === '.') {
722 $j = $i + 1;
723 // search for .
724 if ($j >= $len) {
725 break;
726 }
727
728 // search for ./
729 if ($path[$j] === '/') {
730 $i = $j + 1;
731 continue;
732 }
733
734 // search for ../
735 if ($path[$j] === '.') {
736 $k = $j + 1;
737 if ($k >= $len) {
738 break;
739 }
740 if ($path[$k] === '/') {
741 $i = $k + 1;
742 continue;
743 }
744 }
745 } elseif ($path[$i] === '/') {
746 $j = $i + 1;
747 if ($j >= $len) {
748 $output .= '/';
749 break;
750 }
751
752 // search for /.
753 if ($path[$j] === '.') {
754 $k = $j + 1;
755 if ($k >= $len) {
756 $output .= '/';
757 break;
758 }
759 // search for /./
760 if ($path[$k] === '/') {
761 $i = $k;
762 continue;
763 }
764 // search for /..
765 if ($path[$k] === '.') {
766 $n = $k + 1;
767 if ($n >= $len) {
768 // keep the slash
769 $output = substr($output, 0, $last_slash + 1);
770 break;
771 }
772 // search for /../
773 if ($path[$n] === '/') {
774 $output = substr($output, 0, $last_slash);
775 $last_slash = (int)strrpos($output, '/');
776 $i = $n;
777 continue;
778 }
779 }
780 }
781 }
782
783 $pos = strpos($path, '/', $i + 1);
784
785 if ($pos === false) {
786 $output .= substr($path, $i);
787 break;
788 }
789
790 $last_slash = strlen($output);
791 $output .= substr($path, $i, $pos - $i);
792
793 $i = $pos;
794 }
795
796 return $output;
797 }
798
799 /**
800 * @param string|null $query
801 * @return array
802 */
803 public static function parseQueryString(?string $query): array
804 {
805 if ($query === null) {
806 return [];
807 }
808
809 $list = [];
810
811 foreach (explode('&', $query) as $name) {
812 $value = null;
813 if (($pos = strpos($name, '=')) !== false) {
814 $value = self::decodeComponent(substr($name, $pos + 1));
815 $name = self::decodeComponent(substr($name, 0, $pos));
816 } else {
817 $name = self::decodeComponent($name);
818 }
819 $list[$name] = $value;
820 }
821
822 return $list;
823 }
824
825 /**
826 * @param array $qs
827 * @param string|null $prefix
828 * @param string $separator
829 * @param bool $sort
830 * @return string
831 */
832 public static function buildQueryString(array $qs, ?string $prefix = null,
833 string $separator = '&', bool $sort = false): string
834 {
835 $isIndexed = static function (array $array): bool {
836 for ($i = 0, $max = count($array); $i < $max; $i++) {
837 if (!array_key_exists($i, $array)) {
838 return false;
839 }
840 }
841 return true;
842 };
843
844 $f = static function (array $arr, ?string $prefix = null) use (&$f, &$isIndexed): iterable {
845 $indexed = $prefix !== null && $isIndexed($arr);
846
847 foreach ($arr as $key => $value) {
848 if ($prefix !== null) {
849 $key = $prefix . ($indexed ? "[]" : "[{$key}]");
850 }
851 if (is_array($value)) {
852 yield from $f($value, $key);
853 } else {
854 yield $key => $value;
855 }
856 }
857 };
858
859 $data = [];
860
861 foreach ($f($qs, $prefix) as $key => $value) {
862 $item = is_string($key) ? self::encodeComponent($key) : $key;
863 if ($value !== null) {
864 $item .= '=';
865 $item .= is_string($value) ? self::encodeComponent($value) : $value;
866 }
867 if ($item === '' || $item === '=') {
868 continue;
869 }
870 $data[] = $item;
871 }
872
873 if (!$data) {
874 return '';
875 }
876
877 if ($sort) {
878 sort($data);
879 }
880
881 return implode($separator, $data);
882 }
883
884 /**
885 * @param string $query
886 * @return string
887 */
888 public static function normalizeQueryString(string $query): string
889 {
890 return static::buildQueryString(self::parseQueryString($query), null, '&', true);
891 }
892
893 public static function decodeComponent(string $component): string
894 {
895 return rawurldecode($component);
896 }
897
898 public static function encodeComponent(string $component, ?array $skip = null): string
899 {
900 if (!$skip) {
901 return rawurlencode($component);
902 }
903
904 $str = '';
905
906 foreach (UnicodeString::walkString($component) as [$cp, $chars]) {
907 if ($cp < 0x80) {
908 if ($cp === 0x2D || $cp === 0x2E ||
909 $cp === 0x5F || $cp === 0x7E ||
910 ($cp >= 0x41 && $cp <= 0x5A) ||
911 ($cp >= 0x61 && $cp <= 0x7A) ||
912 ($cp >= 0x30 && $cp <= 0x39) ||
913 in_array($cp, $skip, true)
914 ) {
915 $str .= chr($cp);
916 } else {
917 $str .= '%' . strtoupper(dechex($cp));
918 }
919 } else {
920 $i = 0;
921 while (isset($chars[$i])) {
922 $str .= '%' . strtoupper(dechex($chars[$i++]));
923 }
924 }
925 }
926
927 return $str;
928 }
929
930 public static function setSchemePort(string $scheme, ?int $port): void
931 {
932 $scheme = strtolower($scheme);
933
934 if ($port === null) {
935 unset(self::$KNOWN_PORTS[$scheme]);
936 } else {
937 self::$KNOWN_PORTS[$scheme] = $port;
938 }
939 }
940
941 public static function getSchemePort(string $scheme): ?int
942 {
943 return self::$KNOWN_PORTS[strtolower($scheme)] ?? null;
944 }
945
946 protected static array $KNOWN_PORTS = [
947 'ftp' => 21,
948 'ssh' => 22,
949 'telnet' => 23,
950 'smtp' => 25,
951 'tftp' => 69,
952 'http' => 80,
953 'pop' => 110,
954 'sftp' => 115,
955 'imap' => 143,
956 'irc' => 194,
957 'ldap' => 389,
958 'https' => 443,
959 'ldaps' => 636,
960 'telnets' => 992,
961 'imaps' => 993,
962 'ircs' => 994,
963 'pops' => 995,
964 ];
965 }