|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
declare (strict_types = 1); |
|
4
|
|
|
|
|
5
|
|
|
namespace Nyholm\Psr7; |
|
6
|
|
|
|
|
7
|
|
|
use Psr\Http\Message\UriInterface; |
|
8
|
|
|
|
|
9
|
|
|
/** |
|
10
|
|
|
* PSR-7 URI implementation. |
|
11
|
|
|
* |
|
12
|
|
|
* @author Michael Dowling |
|
13
|
|
|
* @author Tobias Schultze |
|
14
|
|
|
* @author Matthew Weier O'Phinney |
|
15
|
|
|
* @author Tobias Nyholm <[email protected]> |
|
16
|
|
|
*/ |
|
17
|
|
|
class Uri implements UriInterface |
|
18
|
|
|
{ |
|
19
|
|
|
private static $schemes = [ |
|
20
|
|
|
'http' => 80, |
|
21
|
|
|
'https' => 443, |
|
22
|
|
|
]; |
|
23
|
|
|
|
|
24
|
|
|
private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; |
|
25
|
|
|
private static $charSubDelims = '!\$&\'\(\)\*\+,;='; |
|
26
|
|
|
private static $replaceQuery = ['=' => '%3D', '&' => '%26']; |
|
27
|
|
|
|
|
28
|
|
|
/** |
|
29
|
|
|
* @var string Uri scheme. |
|
30
|
|
|
*/ |
|
31
|
|
|
private $scheme = ''; |
|
32
|
|
|
|
|
33
|
|
|
/** |
|
34
|
|
|
* @var string Uri user info. |
|
35
|
|
|
*/ |
|
36
|
|
|
private $userInfo = ''; |
|
37
|
|
|
|
|
38
|
|
|
/** |
|
39
|
|
|
* @var string Uri host. |
|
40
|
|
|
*/ |
|
41
|
|
|
private $host = ''; |
|
42
|
|
|
|
|
43
|
|
|
/** |
|
44
|
|
|
* @var int|null Uri port. |
|
45
|
|
|
*/ |
|
46
|
|
|
private $port; |
|
47
|
|
|
|
|
48
|
|
|
/** |
|
49
|
|
|
* @var string Uri path. |
|
50
|
|
|
*/ |
|
51
|
|
|
private $path = ''; |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* @var string Uri query string. |
|
55
|
|
|
*/ |
|
56
|
|
|
private $query = ''; |
|
57
|
|
|
|
|
58
|
|
|
/** |
|
59
|
|
|
* @var string Uri fragment. |
|
60
|
|
|
*/ |
|
61
|
|
|
private $fragment = ''; |
|
62
|
|
|
|
|
63
|
|
|
/** |
|
64
|
|
|
* @param string $uri |
|
65
|
|
|
*/ |
|
66
|
|
|
public function __construct($uri = '') |
|
67
|
|
|
{ |
|
68
|
|
|
if ($uri != '') { |
|
69
|
|
|
$parts = parse_url($uri); |
|
70
|
|
|
if ($parts === false) { |
|
71
|
|
|
throw new \InvalidArgumentException("Unable to parse URI: $uri"); |
|
72
|
|
|
} |
|
73
|
|
|
|
|
74
|
|
|
$this->applyParts($parts); |
|
75
|
|
|
} |
|
76
|
|
|
} |
|
77
|
|
|
|
|
78
|
|
|
/** |
|
79
|
|
|
* {@inheritdoc} |
|
80
|
|
|
*/ |
|
81
|
|
|
public function __toString(): string |
|
82
|
|
|
{ |
|
83
|
|
|
return self::createUriString( |
|
84
|
|
|
$this->scheme, |
|
85
|
|
|
$this->getAuthority(), |
|
86
|
|
|
$this->path, |
|
87
|
|
|
$this->query, |
|
88
|
|
|
$this->fragment |
|
89
|
|
|
); |
|
90
|
|
|
} |
|
91
|
|
|
|
|
92
|
|
|
/** |
|
93
|
|
|
* {@inheritdoc} |
|
94
|
|
|
*/ |
|
95
|
|
|
public function getScheme(): string |
|
96
|
|
|
{ |
|
97
|
|
|
return $this->scheme; |
|
98
|
|
|
} |
|
99
|
|
|
|
|
100
|
|
|
/** |
|
101
|
|
|
* {@inheritdoc} |
|
102
|
|
|
*/ |
|
103
|
|
|
public function getAuthority(): string |
|
104
|
|
|
{ |
|
105
|
|
|
if ($this->host == '') { |
|
106
|
|
|
return ''; |
|
107
|
|
|
} |
|
108
|
|
|
|
|
109
|
|
|
$authority = $this->host; |
|
110
|
|
|
if ($this->userInfo != '') { |
|
111
|
|
|
$authority = $this->userInfo.'@'.$authority; |
|
112
|
|
|
} |
|
113
|
|
|
|
|
114
|
|
|
if ($this->port !== null) { |
|
115
|
|
|
$authority .= ':'.$this->port; |
|
116
|
|
|
} |
|
117
|
|
|
|
|
118
|
|
|
return $authority; |
|
119
|
|
|
} |
|
120
|
|
|
|
|
121
|
|
|
/** |
|
122
|
|
|
* {@inheritdoc} |
|
123
|
|
|
*/ |
|
124
|
|
|
public function getUserInfo(): string |
|
125
|
|
|
{ |
|
126
|
|
|
return $this->userInfo; |
|
127
|
|
|
} |
|
128
|
|
|
|
|
129
|
|
|
/** |
|
130
|
|
|
* {@inheritdoc} |
|
131
|
|
|
*/ |
|
132
|
|
|
public function getHost(): string |
|
133
|
|
|
{ |
|
134
|
|
|
return $this->host; |
|
135
|
|
|
} |
|
136
|
|
|
|
|
137
|
|
|
/** |
|
138
|
|
|
* {@inheritdoc} |
|
139
|
|
|
*/ |
|
140
|
|
|
public function getPort() |
|
141
|
|
|
{ |
|
142
|
|
|
return $this->port; |
|
143
|
|
|
} |
|
144
|
|
|
|
|
145
|
|
|
/** |
|
146
|
|
|
* {@inheritdoc} |
|
147
|
|
|
*/ |
|
148
|
|
|
public function getPath(): string |
|
149
|
|
|
{ |
|
150
|
|
|
return $this->path; |
|
151
|
|
|
} |
|
152
|
|
|
|
|
153
|
|
|
/** |
|
154
|
|
|
* {@inheritdoc} |
|
155
|
|
|
*/ |
|
156
|
|
|
public function getQuery(): string |
|
157
|
|
|
{ |
|
158
|
|
|
return $this->query; |
|
159
|
|
|
} |
|
160
|
|
|
|
|
161
|
|
|
/** |
|
162
|
|
|
* {@inheritdoc} |
|
163
|
|
|
*/ |
|
164
|
|
|
public function getFragment(): string |
|
165
|
|
|
{ |
|
166
|
|
|
return $this->fragment; |
|
167
|
|
|
} |
|
168
|
|
|
|
|
169
|
|
|
/** |
|
170
|
|
|
* {@inheritdoc} |
|
171
|
|
|
*/ |
|
172
|
|
|
public function withScheme($scheme): self |
|
173
|
|
|
{ |
|
174
|
|
|
$scheme = $this->filterScheme($scheme); |
|
175
|
|
|
|
|
176
|
|
|
if ($this->scheme === $scheme) { |
|
177
|
|
|
return $this; |
|
178
|
|
|
} |
|
179
|
|
|
|
|
180
|
|
|
$new = clone $this; |
|
181
|
|
|
$new->scheme = $scheme; |
|
182
|
|
|
$new->port = $new->filterPort($new->port); |
|
183
|
|
|
|
|
184
|
|
|
return $new; |
|
185
|
|
|
} |
|
186
|
|
|
|
|
187
|
|
|
/** |
|
188
|
|
|
* {@inheritdoc} |
|
189
|
|
|
*/ |
|
190
|
|
|
public function withUserInfo($user, $password = null): self |
|
191
|
|
|
{ |
|
192
|
|
|
$info = $user; |
|
193
|
|
|
if ($password != '') { |
|
194
|
|
|
$info .= ':'.$password; |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
|
|
if ($this->userInfo === $info) { |
|
198
|
|
|
return $this; |
|
199
|
|
|
} |
|
200
|
|
|
|
|
201
|
|
|
$new = clone $this; |
|
202
|
|
|
$new->userInfo = $info; |
|
203
|
|
|
|
|
204
|
|
|
return $new; |
|
205
|
|
|
} |
|
206
|
|
|
|
|
207
|
|
|
/** |
|
208
|
|
|
* {@inheritdoc} |
|
209
|
|
|
*/ |
|
210
|
|
|
public function withHost($host): self |
|
211
|
|
|
{ |
|
212
|
|
|
$host = $this->filterHost($host); |
|
213
|
|
|
|
|
214
|
|
|
if ($this->host === $host) { |
|
215
|
|
|
return $this; |
|
216
|
|
|
} |
|
217
|
|
|
|
|
218
|
|
|
$new = clone $this; |
|
219
|
|
|
$new->host = $host; |
|
220
|
|
|
|
|
221
|
|
|
return $new; |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
/** |
|
225
|
|
|
* {@inheritdoc} |
|
226
|
|
|
*/ |
|
227
|
|
|
public function withPort($port): self |
|
228
|
|
|
{ |
|
229
|
|
|
$port = $this->filterPort($port); |
|
230
|
|
|
|
|
231
|
|
|
if ($this->port === $port) { |
|
232
|
|
|
return $this; |
|
233
|
|
|
} |
|
234
|
|
|
|
|
235
|
|
|
$new = clone $this; |
|
236
|
|
|
$new->port = $port; |
|
237
|
|
|
|
|
238
|
|
|
return $new; |
|
239
|
|
|
} |
|
240
|
|
|
|
|
241
|
|
|
/** |
|
242
|
|
|
* {@inheritdoc} |
|
243
|
|
|
*/ |
|
244
|
|
|
public function withPath($path): self |
|
245
|
|
|
{ |
|
246
|
|
|
$path = $this->filterPath($path); |
|
247
|
|
|
|
|
248
|
|
|
if ($this->path === $path) { |
|
249
|
|
|
return $this; |
|
250
|
|
|
} |
|
251
|
|
|
|
|
252
|
|
|
$new = clone $this; |
|
253
|
|
|
$new->path = $path; |
|
254
|
|
|
|
|
255
|
|
|
return $new; |
|
256
|
|
|
} |
|
257
|
|
|
|
|
258
|
|
|
/** |
|
259
|
|
|
* {@inheritdoc} |
|
260
|
|
|
*/ |
|
261
|
|
|
public function withQuery($query): self |
|
262
|
|
|
{ |
|
263
|
|
|
$query = $this->filterQueryAndFragment($query); |
|
264
|
|
|
|
|
265
|
|
|
if ($this->query === $query) { |
|
266
|
|
|
return $this; |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
$new = clone $this; |
|
270
|
|
|
$new->query = $query; |
|
271
|
|
|
|
|
272
|
|
|
return $new; |
|
273
|
|
|
} |
|
274
|
|
|
|
|
275
|
|
|
/** |
|
276
|
|
|
* {@inheritdoc} |
|
277
|
|
|
*/ |
|
278
|
|
|
public function withFragment($fragment): self |
|
279
|
|
|
{ |
|
280
|
|
|
$fragment = $this->filterQueryAndFragment($fragment); |
|
281
|
|
|
|
|
282
|
|
|
if ($this->fragment === $fragment) { |
|
283
|
|
|
return $this; |
|
284
|
|
|
} |
|
285
|
|
|
|
|
286
|
|
|
$new = clone $this; |
|
287
|
|
|
$new->fragment = $fragment; |
|
288
|
|
|
|
|
289
|
|
|
return $new; |
|
290
|
|
|
} |
|
291
|
|
|
|
|
292
|
|
|
/** |
|
293
|
|
|
* Apply parse_url parts to a URI. |
|
294
|
|
|
* |
|
295
|
|
|
* @param array $parts Array of parse_url parts to apply. |
|
296
|
|
|
*/ |
|
297
|
|
|
private function applyParts(array $parts) |
|
298
|
|
|
{ |
|
299
|
|
|
$this->scheme = isset($parts['scheme']) |
|
300
|
|
|
? $this->filterScheme($parts['scheme']) |
|
301
|
|
|
: ''; |
|
302
|
|
|
$this->userInfo = isset($parts['user']) ? $parts['user'] : ''; |
|
303
|
|
|
$this->host = isset($parts['host']) |
|
304
|
|
|
? $this->filterHost($parts['host']) |
|
305
|
|
|
: ''; |
|
306
|
|
|
$this->port = isset($parts['port']) |
|
307
|
|
|
? $this->filterPort($parts['port']) |
|
308
|
|
|
: null; |
|
309
|
|
|
$this->path = isset($parts['path']) |
|
310
|
|
|
? $this->filterPath($parts['path']) |
|
311
|
|
|
: ''; |
|
312
|
|
|
$this->query = isset($parts['query']) |
|
313
|
|
|
? $this->filterQueryAndFragment($parts['query']) |
|
314
|
|
|
: ''; |
|
315
|
|
|
$this->fragment = isset($parts['fragment']) |
|
316
|
|
|
? $this->filterQueryAndFragment($parts['fragment']) |
|
317
|
|
|
: ''; |
|
318
|
|
|
if (isset($parts['pass'])) { |
|
319
|
|
|
$this->userInfo .= ':'.$parts['pass']; |
|
320
|
|
|
} |
|
321
|
|
|
} |
|
322
|
|
|
|
|
323
|
|
|
/** |
|
324
|
|
|
* Create a URI string from its various parts. |
|
325
|
|
|
* |
|
326
|
|
|
* @param string $scheme |
|
327
|
|
|
* @param string $authority |
|
328
|
|
|
* @param string $path |
|
329
|
|
|
* @param string $query |
|
330
|
|
|
* @param string $fragment |
|
331
|
|
|
* |
|
332
|
|
|
* @return string |
|
333
|
|
|
*/ |
|
334
|
|
|
private static function createUriString($scheme, $authority, $path, $query, $fragment): string |
|
335
|
|
|
{ |
|
336
|
|
|
$uri = ''; |
|
337
|
|
|
|
|
338
|
|
|
if ($scheme != '') { |
|
339
|
|
|
$uri .= $scheme.':'; |
|
340
|
|
|
} |
|
341
|
|
|
|
|
342
|
|
|
if ($authority != '') { |
|
343
|
|
|
$uri .= '//'.$authority; |
|
344
|
|
|
} |
|
345
|
|
|
|
|
346
|
|
|
if ($path != '') { |
|
347
|
|
|
if ($path[0] !== '/') { |
|
348
|
|
|
if ($authority != '') { |
|
349
|
|
|
// If the path is rootless and an authority is present, the path MUST be prefixed by "/" |
|
350
|
|
|
$path = '/'.$path; |
|
351
|
|
|
} |
|
352
|
|
|
} elseif (isset($path[1]) && $path[1] === '/') { |
|
353
|
|
|
if ($authority == '') { |
|
354
|
|
|
// If the path is starting with more than one "/" and no authority is present, the |
|
355
|
|
|
// starting slashes MUST be reduced to one. |
|
356
|
|
|
$path = '/'.ltrim($path, '/'); |
|
357
|
|
|
} |
|
358
|
|
|
} |
|
359
|
|
|
|
|
360
|
|
|
$uri .= $path; |
|
361
|
|
|
} |
|
362
|
|
|
|
|
363
|
|
|
if ($query != '') { |
|
364
|
|
|
$uri .= '?'.$query; |
|
365
|
|
|
} |
|
366
|
|
|
|
|
367
|
|
|
if ($fragment != '') { |
|
368
|
|
|
$uri .= '#'.$fragment; |
|
369
|
|
|
} |
|
370
|
|
|
|
|
371
|
|
|
return $uri; |
|
372
|
|
|
} |
|
373
|
|
|
|
|
374
|
|
|
/** |
|
375
|
|
|
* Is a given port non-standard for the current scheme? |
|
376
|
|
|
* |
|
377
|
|
|
* @param string $scheme |
|
378
|
|
|
* @param int $port |
|
379
|
|
|
* |
|
380
|
|
|
* @return bool |
|
381
|
|
|
*/ |
|
382
|
|
|
private static function isNonStandardPort($scheme, $port): bool |
|
383
|
|
|
{ |
|
384
|
|
|
return !isset(self::$schemes[$scheme]) || $port !== self::$schemes[$scheme]; |
|
385
|
|
|
} |
|
386
|
|
|
|
|
387
|
|
|
/** |
|
388
|
|
|
* @param string $scheme |
|
389
|
|
|
* |
|
390
|
|
|
* @return string |
|
391
|
|
|
* |
|
392
|
|
|
* @throws \InvalidArgumentException If the scheme is invalid. |
|
393
|
|
|
*/ |
|
394
|
|
|
private function filterScheme($scheme): string |
|
395
|
|
|
{ |
|
396
|
|
|
if (!is_string($scheme)) { |
|
397
|
|
|
throw new \InvalidArgumentException('Scheme must be a string'); |
|
398
|
|
|
} |
|
399
|
|
|
|
|
400
|
|
|
return strtolower($scheme); |
|
401
|
|
|
} |
|
402
|
|
|
|
|
403
|
|
|
/** |
|
404
|
|
|
* @param string $host |
|
405
|
|
|
* |
|
406
|
|
|
* @return string |
|
407
|
|
|
* |
|
408
|
|
|
* @throws \InvalidArgumentException If the host is invalid. |
|
409
|
|
|
*/ |
|
410
|
|
|
private function filterHost($host): string |
|
411
|
|
|
{ |
|
412
|
|
|
if (!is_string($host)) { |
|
413
|
|
|
throw new \InvalidArgumentException('Host must be a string'); |
|
414
|
|
|
} |
|
415
|
|
|
|
|
416
|
|
|
return strtolower($host); |
|
417
|
|
|
} |
|
418
|
|
|
|
|
419
|
|
|
/** |
|
420
|
|
|
* @param int|null $port |
|
421
|
|
|
* |
|
422
|
|
|
* @return int|null |
|
423
|
|
|
* |
|
424
|
|
|
* @throws \InvalidArgumentException If the port is invalid. |
|
425
|
|
|
*/ |
|
426
|
|
|
private function filterPort($port) |
|
427
|
|
|
{ |
|
428
|
|
|
if ($port === null) { |
|
429
|
|
|
return; |
|
430
|
|
|
} |
|
431
|
|
|
|
|
432
|
|
|
$port = (int) $port; |
|
433
|
|
|
if (1 > $port || 0xffff < $port) { |
|
434
|
|
|
throw new \InvalidArgumentException( |
|
435
|
|
|
sprintf('Invalid port: %d. Must be between 1 and 65535', $port) |
|
436
|
|
|
); |
|
437
|
|
|
} |
|
438
|
|
|
|
|
439
|
|
|
return self::isNonStandardPort($this->scheme, $port) ? $port : null; |
|
440
|
|
|
} |
|
441
|
|
|
|
|
442
|
|
|
/** |
|
443
|
|
|
* Filters the path of a URI. |
|
444
|
|
|
* |
|
445
|
|
|
* @param string $path |
|
446
|
|
|
* |
|
447
|
|
|
* @return string |
|
448
|
|
|
* |
|
449
|
|
|
* @throws \InvalidArgumentException If the path is invalid. |
|
450
|
|
|
*/ |
|
451
|
|
View Code Duplication |
private function filterPath($path): string |
|
|
|
|
|
|
452
|
|
|
{ |
|
453
|
|
|
if (!is_string($path)) { |
|
454
|
|
|
throw new \InvalidArgumentException('Path must be a string'); |
|
455
|
|
|
} |
|
456
|
|
|
|
|
457
|
|
|
return preg_replace_callback( |
|
458
|
|
|
'/(?:[^'.self::$charUnreserved.self::$charSubDelims.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/', |
|
459
|
|
|
[$this, 'rawurlencodeMatchZero'], |
|
460
|
|
|
$path |
|
461
|
|
|
); |
|
462
|
|
|
} |
|
463
|
|
|
|
|
464
|
|
|
/** |
|
465
|
|
|
* Filters the query string or fragment of a URI. |
|
466
|
|
|
* |
|
467
|
|
|
* @param string $str |
|
468
|
|
|
* |
|
469
|
|
|
* @return string |
|
470
|
|
|
* |
|
471
|
|
|
* @throws \InvalidArgumentException If the query or fragment is invalid. |
|
472
|
|
|
*/ |
|
473
|
|
View Code Duplication |
private function filterQueryAndFragment($str): string |
|
|
|
|
|
|
474
|
|
|
{ |
|
475
|
|
|
if (!is_string($str)) { |
|
476
|
|
|
throw new \InvalidArgumentException('Query and fragment must be a string'); |
|
477
|
|
|
} |
|
478
|
|
|
|
|
479
|
|
|
return preg_replace_callback( |
|
480
|
|
|
'/(?:[^'.self::$charUnreserved.self::$charSubDelims.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', |
|
481
|
|
|
[$this, 'rawurlencodeMatchZero'], |
|
482
|
|
|
$str |
|
483
|
|
|
); |
|
484
|
|
|
} |
|
485
|
|
|
|
|
486
|
|
|
private function rawurlencodeMatchZero(array $match): string |
|
487
|
|
|
{ |
|
488
|
|
|
return rawurlencode($match[0]); |
|
489
|
|
|
} |
|
490
|
|
|
} |
|
491
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.