Completed
Push — master ( d94b34...85a373 )
by smiley
01:50
created

Uri::isAbsolutePathReference()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class Uri
4
 *
5
 * @filesource   Uri.php
6
 * @created      10.08.2018
7
 * @package      chillerlan\HTTP\Psr7
8
 * @author       smiley <[email protected]>
9
 * @copyright    2018 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\HTTP\Psr7;
14
15
use InvalidArgumentException;
16
use Psr\Http\Message\UriInterface;
17
18
class Uri implements UriInterface{
19
20
	protected const DEFAULT_PORTS = [
21
		'http'   => 80,
22
		'https'  => 443,
23
		'ftp'    => 21,
24
		'gopher' => 70,
25
		'nntp'   => 119,
26
		'news'   => 119,
27
		'telnet' => 23,
28
		'tn3270' => 23,
29
		'imap'   => 143,
30
		'pop'    => 110,
31
		'ldap'   => 389,
32
	];
33
34
	/**
35
	 * @var string
36
	 */
37
	protected $scheme = '';
38
39
	/**
40
	 * @var string
41
	 */
42
	protected $user = '';
43
44
	/**
45
	 * @var string
46
	 */
47
	protected $pass = '';
48
49
	/**
50
	 * @var string
51
	 */
52
	protected $host = '';
53
54
	/**
55
	 * @var int
56
	 */
57
	protected $port = null;
58
59
	/**
60
	 * @var string
61
	 */
62
	protected $path = '';
63
64
	/**
65
	 * @var string
66
	 */
67
	protected $query = '';
68
69
	/**
70
	 * @var string
71
	 */
72
	protected $fragment = '';
73
74
	/**
75
	 * Uri constructor.
76
	 *
77
	 * @param string|null $uri
78
	 *
79
	 * @throws \InvalidArgumentException
80
	 */
81
	public function __construct(string $uri = null){
82
83
		if($uri !== ''){
84
			$parts = parse_url($uri);
85
86
			if($parts === false){
87
				throw new InvalidArgumentException('invalid URI: "'.$uri.'"');
88
			}
89
90
			$this->parseUriParts($parts);
91
		}
92
93
	}
94
95
	/**
96
	 * @inheritdoc
97
	 */
98
	public function __toString(){
99
		$this->validateState();
100
101
		$uri       = '';
102
		$authority = $this->getAuthority();
103
104
		if($this->scheme !== ''){
105
			$uri .= $this->scheme.':';
106
		}
107
108
		if($authority !== '' || $this->scheme === 'file'){
109
			$uri .= '//'.$authority;
110
		}
111
112
		$uri .= $this->path;
113
114
		if($this->query !== ''){
115
			$uri .= '?'.$this->query;
116
		}
117
118
		if($this->fragment !== ''){
119
			$uri .= '#'.$this->fragment;
120
		}
121
122
		return $uri;
123
	}
124
125
	/**
126
	 * Scheme
127
	 */
128
129
	/**
130
	 * @param mixed $scheme
131
	 *
132
	 * @return string
133
	 * @throws \InvalidArgumentException
134
	 */
135
	protected function filterScheme($scheme):string{
136
137
		if(!is_string($scheme)){
138
			throw new InvalidArgumentException(sprintf('scheme must be a string'));
139
		}
140
141
		return strtolower($scheme);
142
	}
143
144
	/**
145
	 * @inheritdoc
146
	 */
147
	public function getScheme():string{
148
		return $this->scheme;
149
	}
150
151
	/**
152
	 * @inheritdoc
153
	 */
154
	public function withScheme($scheme):UriInterface{
155
		$scheme = $this->filterScheme($scheme);
156
157
		if($this->scheme === $scheme){
158
			return $this;
159
		}
160
161
		$clone         = clone $this;
162
		$clone->scheme = $scheme;
163
164
		$clone->removeDefaultPort();
165
		$clone->validateState();
166
167
		return $clone;
168
	}
169
170
	/**
171
	 * Authority
172
	 */
173
174
	/**
175
	 * @param mixed $user
176
	 *
177
	 * @return string
178
	 * @throws \InvalidArgumentException
179
	 */
180
	protected function filterUser($user):string{
181
182
		if(!is_string($user)){
183
			throw new InvalidArgumentException(sprintf('user must be a string'));
184
		}
185
186
		return $user;
187
	}
188
189
	/**
190
	 * @param mixed $pass
191
	 *
192
	 * @return string
193
	 * @throws \InvalidArgumentException
194
	 */
195
	protected function filterPass($pass):string{
196
197
		if(!is_string($pass)){
198
			throw new InvalidArgumentException(sprintf('pass must be a string'));
199
		}
200
201
		return $pass;
202
	}
203
204
	/**
205
	 * @inheritdoc
206
	 */
207
	public function getAuthority():string{
208
		$authority = $this->host;
209
		$userInfo  = $this->getUserInfo();
210
211
		if($userInfo !== ''){
212
			$authority = $userInfo.'@'.$authority;
213
		}
214
215
		if($this->port !== null){
216
			$authority .= ':'.$this->port;
217
		}
218
219
		return $authority;
220
	}
221
222
	/**
223
	 * @inheritdoc
224
	 */
225
	public function getUserInfo():string{
226
		return (string)$this->user.($this->pass != '' ? ':'.$this->pass : '');
227
	}
228
229
	/**
230
	 * @inheritdoc
231
	 */
232
	public function withUserInfo($user, $password = null):UriInterface{
233
		$info = $user;
234
235
		if($password !== ''){
236
			$info .= ':'.$password;
237
		}
238
239
		if($this->getUserInfo() === $info){
240
			return $this;
241
		}
242
243
		$clone       = clone $this;
244
		$clone->user = $user;
245
		$clone->pass = $password;
246
247
		$clone->validateState();
248
249
		return $clone;
250
	}
251
252
	/**
253
	 * Host
254
	 */
255
256
	/**
257
	 * @param mixed $host
258
	 *
259
	 * @return string
260
	 * @throws \InvalidArgumentException
261
	 */
262
	protected function filterHost($host):string{
263
264
		if(!is_string($host)){
265
			throw new InvalidArgumentException('host must be a string');
266
		}
267
268
		if(filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){
269
			$host = '['.$host.']';
270
		}
271
272
		return strtolower($host);
273
	}
274
275
	/**
276
	 * @inheritdoc
277
	 */
278
	public function getHost():string{
279
		return $this->host;
280
	}
281
282
	/**
283
	 * @inheritdoc
284
	 */
285
	public function withHost($host):UriInterface{
286
		$host = $this->filterHost($host);
287
288
		if($this->host === $host){
289
			return $this;
290
		}
291
292
		$clone       = clone $this;
293
		$clone->host = $host;
294
295
		$clone->validateState();
296
297
		return $clone;
298
	}
299
300
	/**
301
	 * Port
302
	 */
303
304
	/**
305
	 * @param mixed $port
306
	 *
307
	 * @return int|null
308
	 * @throws \InvalidArgumentException
309
	 */
310
	protected function filterPort($port):?int{
311
312
		if($port === null){
313
			return null;
314
		}
315
316
		$port = (int)$port;
317
318
		if($port >= 1 && $port <= 0xffff){
319
			return $port;
320
		}
321
322
		throw new InvalidArgumentException(sprintf('invalid port: %d', $port));
323
	}
324
325
	/**
326
	 * @inheritdoc
327
	 */
328
	public function getPort():?int{
329
		return $this->port;
330
	}
331
332
	/**
333
	 * @inheritdoc
334
	 */
335
	public function withPort($port):UriInterface{
336
		$port = $this->filterPort($port);
337
338
		if($this->port === $port){
339
			return $this;
340
		}
341
342
		$clone       = clone $this;
343
		$clone->port = $port;
344
345
		$clone->removeDefaultPort();
346
		$clone->validateState();
347
348
		return $clone;
349
	}
350
351
	/**
352
	 * Path
353
	 */
354
355
	/**
356
	 * @param mixed $path
357
	 *
358
	 * @return string
359
	 * @throws \InvalidArgumentException
360
	 */
361
	protected function filterPath($path):string{
362
363
		if(!is_string($path)){
364
			throw new InvalidArgumentException('path must be a string');
365
		}
366
367
		return $this->replaceChars($path);
368
	}
369
370
	/**
371
	 * @inheritdoc
372
	 */
373
	public function getPath():string{
374
		return $this->path;
375
	}
376
377
	/**
378
	 * @inheritdoc
379
	 */
380
	public function withPath($path):UriInterface{
381
		$path = $this->filterPath($path);
382
383
		if($this->path === $path){
384
			return $this;
385
		}
386
387
		$clone       = clone $this;
388
		$clone->path = $path;
389
390
		$clone->validateState();
391
392
		return $clone;
393
	}
394
395
	/**
396
	 * Query
397
	 */
398
399
	/**
400
	 * @param $query
401
	 *
402
	 * @return string
403
	 * @throws \InvalidArgumentException
404
	 */
405
	protected function filterQuery($query):string{
406
407
		if(!is_string($query)){
408
			throw new InvalidArgumentException('query and fragment must be a string');
409
		}
410
411
		return $this->replaceChars($query, true);
412
	}
413
414
	/**
415
	 * @inheritdoc
416
	 */
417
	public function getQuery():string{
418
		return $this->query;
419
	}
420
421
	/**
422
	 * @inheritdoc
423
	 */
424
	public function withQuery($query):UriInterface{
425
		$query = $this->filterQuery($query);
426
427
		if($this->query === $query){
428
			return $this;
429
		}
430
431
		$clone        = clone $this;
432
		$clone->query = $query;
433
434
		$clone->validateState();
435
436
		return $clone;
437
	}
438
439
	/**
440
	 * Fragment
441
	 */
442
443
	/**
444
	 * @param $fragment
445
	 *
446
	 * @return string
447
	 */
448
	protected function filterFragment($fragment):string{
449
		return $this->filterQuery($fragment);
450
	}
451
452
	/**
453
	 * @inheritdoc
454
	 */
455
	public function getFragment():string{
456
		return $this->fragment;
457
	}
458
459
	/**
460
	 * @inheritdoc
461
	 */
462
	public function withFragment($fragment):UriInterface{
463
		$fragment = $this->filterFragment($fragment);
464
465
		if($this->fragment === $fragment){
466
			return $this;
467
		}
468
469
		$clone           = clone $this;
470
		$clone->fragment = $fragment;
471
472
		$clone->validateState();
473
474
		return $clone;
475
	}
476
477
	/**
478
	 * @param array $parts
479
	 *
480
	 * @return void
481
	 */
482
	protected function parseUriParts(array $parts):void{
483
484
		foreach(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment'] as $part){
485
486
			if(!isset($parts[$part])){
487
				continue;
488
			}
489
490
			$this->{$part} = call_user_func_array([$this, 'filter'.ucfirst($part)], [$parts[$part]]);
491
		}
492
493
		$this->removeDefaultPort();
494
	}
495
496
	/**
497
	 * @param string    $str
498
	 * @param bool|null $query
499
	 *
500
	 * @return string
501
	 */
502
	protected function replaceChars(string $str, bool $query = null):string{
503
		return preg_replace_callback(
504
			'/(?:[^'
505
			.'a-z\d_\-\.~'
506
			.'!\$&\'\(\)\*\+,;='
507
			.'%:@\/'.($query ? '\?' : '')
508
			.']++|%(?![a-f\d]{2}))/i',
509
			function(array $match):string{
510
				return rawurlencode($match[0]);
511
			},
512
			$str
513
		);
514
515
	}
516
517
	/**
518
	 * @return void
519
	 */
520
	protected function removeDefaultPort():void{
521
522
		if($this->port !== null && (isset($this::DEFAULT_PORTS[$this->scheme]) && $this->port === $this::DEFAULT_PORTS[$this->scheme])){
523
			$this->port = null;
524
		}
525
526
	}
527
528
	/**
529
	 * @return void
530
	 */
531
	protected function validateState():void{
532
533
		if(empty($this->host) && ($this->scheme === 'http' || $this->scheme === 'https')){
534
			$this->host = 'localhost';
535
		}
536
537
		if($this->getAuthority() !== ''){
538
539
			if(isset($this->path[0]) && $this->path[0] !== '/'){
540
				$this->path = '/'.$this->path; // automagically fix the path, unlike Guzzle
541
			}
542
543
		}
544
		else{
545
546
			if(strpos($this->path, '//') === 0){
547
				$this->path = '/'.ltrim($this->path, '/'); // automagically fix the path, unlike Guzzle
548
			}
549
550
			if(empty($this->scheme) && strpos(explode('/', $this->path, 2)[0], ':') !== false){
551
				throw new InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon');
552
			}
553
554
		}
555
556
	}
557
558
}
559