Completed
Push — issue#666 ( 5c565a...94715f )
by Guilherme
03:27
created

HttpUri::getComponents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 0
dl 0
loc 12
ccs 1
cts 1
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file is part of the login-cidadao project or it's bundles.
4
 *
5
 * (c) Guilherme Donato <guilhermednt on github>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace LoginCidadao\RemoteClaimsBundle\Model;
12
13
use Psr\Http\Message\UriInterface;
14
15
class HttpUri implements UriInterface
16
{
17
    /**
18
     * Pattern extracted from Symfony\Component\Validator\Constraints\UrlValidator
19
     */
20
    const PATTERN = '~^
21
            (?<scheme>https|http)://                                 # protocol
22
            (?<userInfo>([\pL\pN-]+:)?([\pL\pN-]+)@)?          # basic auth
23
            (?<host>
24
                ([\pL\pN\pS-\.])+(\.?([\pL\pN]|xn\-\-[\pL\pN-]+)+\.?) # a domain name
25
                    |                                                 # or
26
                \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}                    # an IP address
27
                    |                                                 # or
28
                \[
29
                    (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::))))
30
                \]  # an IPv6 address
31
            )
32
            (?<port>:[0-9]+)?                              # a port (optional)
33
            (?<path>(?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%%[0-9A-Fa-f]{2})* )*)      # a path
34
            (?<query>\? (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )?   # a query (optional)
35
            (?<fragment>\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )?   # a fragment (optional)
36
        $~ixu';
37
38
    /** @var string */
39
    private $scheme = '';
40
41
    /** @var string */
42
    private $userInfo = '';
43
44
    /** @var string */
45
    private $host = '';
46
47
    /** @var null|int */
48
    private $port = null;
49
50
    /** @var string */
51
    private $path = '';
52
53
    /** @var string */
54
    private $query = '';
55
56
    /** @var string */
57
    private $fragment = '';
58
59
    /**
60
     * Retrieve the scheme component of the URI.
61
     *
62
     * If no scheme is present, this method MUST return an empty string.
63
     *
64
     * The value returned MUST be normalized to lowercase, per RFC 3986
65
     * Section 3.1.
66
     *
67
     * The trailing ":" character is not part of the scheme and MUST NOT be
68
     * added.
69
     *
70
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
71
     * @return string The URI scheme.
72
     */
73 10
    public function getScheme()
74
    {
75 10
        return $this->scheme;
76
    }
77
78
    /**
79
     * Retrieve the authority component of the URI.
80
     *
81
     * If no authority information is present, this method MUST return an empty
82
     * string.
83
     *
84
     * The authority syntax of the URI is:
85
     *
86
     * <pre>
87
     * [user-info@]host[:port]
88
     * </pre>
89
     *
90
     * If the port component is not set or is the standard port for the current
91
     * scheme, it SHOULD NOT be included.
92
     *
93
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
94
     * @return string The URI authority, in "[user-info@]host[:port]" format.
95
     */
96 9
    public function getAuthority()
97
    {
98 9
        $userInfoHost = implode('@', array_filter([$this->getUserInfo(), $this->getHost()]));
99
100 9
        return implode(':', array_filter([$userInfoHost, $this->getPort()]));
101
    }
102
103
    /**
104
     * Retrieve the user information component of the URI.
105
     *
106
     * If no user information is present, this method MUST return an empty
107
     * string.
108
     *
109
     * If a user is present in the URI, this will return that value;
110
     * additionally, if the password is also present, it will be appended to the
111
     * user value, with a colon (":") separating the values.
112
     *
113
     * The trailing "@" character is not part of the user information and MUST
114
     * NOT be added.
115
     *
116
     * @return string The URI user information, in "username[:password]" format.
117
     */
118 9
    public function getUserInfo()
119
    {
120 9
        return $this->userInfo;
121
    }
122
123
    /**
124
     * Retrieve the host component of the URI.
125
     *
126
     * If no host is present, this method MUST return an empty string.
127
     *
128
     * The value returned MUST be normalized to lowercase, per RFC 3986
129
     * Section 3.2.2.
130
     *
131
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
132
     * @return string The URI host.
133
     */
134 10
    public function getHost()
135
    {
136 10
        return $this->host;
137
    }
138
139
    /**
140
     * Retrieve the port component of the URI.
141
     *
142
     * If a port is present, and it is non-standard for the current scheme,
143
     * this method MUST return it as an integer. If the port is the standard port
144
     * used with the current scheme, this method SHOULD return null.
145
     *
146
     * If no port is present, and no scheme is present, this method MUST return
147
     * a null value.
148
     *
149
     * If no port is present, but a scheme is present, this method MAY return
150
     * the standard port for that scheme, but SHOULD return null.
151
     *
152
     * @return null|int The URI port.
153
     */
154 9
    public function getPort()
155
    {
156 9
        return $this->port;
157
    }
158
159
    /**
160
     * Retrieve the path component of the URI.
161
     *
162
     * The path can either be empty or absolute (starting with a slash) or
163
     * rootless (not starting with a slash). Implementations MUST support all
164
     * three syntaxes.
165
     *
166
     * Normally, the empty path "" and absolute path "/" are considered equal as
167
     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
168
     * do this normalization because in contexts with a trimmed base path, e.g.
169
     * the front controller, this difference becomes significant. It's the task
170
     * of the user to handle both "" and "/".
171
     *
172
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
173
     * any characters. To determine what characters to encode, please refer to
174
     * RFC 3986, Sections 2 and 3.3.
175
     *
176
     * As an example, if the value should include a slash ("/") not intended as
177
     * delimiter between path segments, that value MUST be passed in encoded
178
     * form (e.g., "%2F") to the instance.
179
     *
180
     * @see https://tools.ietf.org/html/rfc3986#section-2
181
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
182
     * @return string The URI path.
183
     */
184 9
    public function getPath()
185
    {
186 9
        return $this->path;
187
    }
188
189
    /**
190
     * Retrieve the query string of the URI.
191
     *
192
     * If no query string is present, this method MUST return an empty string.
193
     *
194
     * The leading "?" character is not part of the query and MUST NOT be
195
     * added.
196
     *
197
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
198
     * any characters. To determine what characters to encode, please refer to
199
     * RFC 3986, Sections 2 and 3.4.
200
     *
201
     * As an example, if a value in a key/value pair of the query string should
202
     * include an ampersand ("&") not intended as a delimiter between values,
203
     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
204
     *
205
     * @see https://tools.ietf.org/html/rfc3986#section-2
206
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
207
     * @return string The URI query string.
208
     */
209 9
    public function getQuery()
210
    {
211 9
        return $this->query;
212
    }
213
214
    /**
215
     * Retrieve the fragment component of the URI.
216
     *
217
     * If no fragment is present, this method MUST return an empty string.
218
     *
219
     * The leading "#" character is not part of the fragment and MUST NOT be
220
     * added.
221
     *
222
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
223
     * any characters. To determine what characters to encode, please refer to
224
     * RFC 3986, Sections 2 and 3.5.
225
     *
226
     * @see https://tools.ietf.org/html/rfc3986#section-2
227
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
228
     * @return string The URI fragment.
229
     */
230 9
    public function getFragment()
231
    {
232 9
        return $this->fragment;
233
    }
234
235
    /**
236
     * Return an instance with the specified scheme.
237
     *
238
     * This method MUST retain the state of the current instance, and return
239
     * an instance that contains the specified scheme.
240
     *
241
     * Implementations MUST support the schemes "http" and "https" case
242
     * insensitively, and MAY accommodate other schemes if required.
243
     *
244
     * An empty scheme is equivalent to removing the scheme.
245
     *
246
     * @param string $scheme The scheme to use with the new instance.
247
     * @return static A new instance with the specified scheme.
248
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
249
     */
250
    public function withScheme($scheme)
251
    {
252
        return $this->with('scheme', $scheme);
253
    }
254
255
    /**
256
     * Return an instance with the specified user information.
257
     *
258
     * This method MUST retain the state of the current instance, and return
259
     * an instance that contains the specified user information.
260
     *
261
     * Password is optional, but the user information MUST include the
262
     * user; an empty string for the user is equivalent to removing user
263
     * information.
264
     *
265
     * @param string $user The user name to use for authority.
266
     * @param null|string $password The password associated with $user.
267
     * @return static A new instance with the specified user information.
268
     */
269
    public function withUserInfo($user, $password = null)
270
    {
271
        $userInfo = implode(':', [$user, $password]);
272
273
        return $this->with('userInfo', $userInfo);
274
    }
275
276
    /**
277
     * Return an instance with the specified host.
278
     *
279
     * This method MUST retain the state of the current instance, and return
280
     * an instance that contains the specified host.
281
     *
282
     * An empty host value is equivalent to removing the host.
283
     *
284
     * @param string $host The hostname to use with the new instance.
285
     * @return static A new instance with the specified host.
286
     * @throws \InvalidArgumentException for invalid hostnames.
287
     */
288
    public function withHost($host)
289
    {
290
        return $this->with('host', $host);
291
    }
292
293
    /**
294
     * Return an instance with the specified port.
295
     *
296
     * This method MUST retain the state of the current instance, and return
297
     * an instance that contains the specified port.
298
     *
299
     * Implementations MUST raise an exception for ports outside the
300
     * established TCP and UDP port ranges.
301
     *
302
     * A null value provided for the port is equivalent to removing the port
303
     * information.
304
     *
305
     * @param null|int $port The port to use with the new instance; a null value
306
     *     removes the port information.
307
     * @return static A new instance with the specified port.
308
     * @throws \InvalidArgumentException for invalid ports.
309
     */
310
    public function withPort($port)
311
    {
312
        return $this->with('port', $port);
313
    }
314
315
    /**
316
     * Return an instance with the specified path.
317
     *
318
     * This method MUST retain the state of the current instance, and return
319
     * an instance that contains the specified path.
320
     *
321
     * The path can either be empty or absolute (starting with a slash) or
322
     * rootless (not starting with a slash). Implementations MUST support all
323
     * three syntaxes.
324
     *
325
     * If the path is intended to be domain-relative rather than path relative then
326
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
327
     * are assumed to be relative to some base path known to the application or
328
     * consumer.
329
     *
330
     * Users can provide both encoded and decoded path characters.
331
     * Implementations ensure the correct encoding as outlined in getPath().
332
     *
333
     * @param string $path The path to use with the new instance.
334
     * @return static A new instance with the specified path.
335
     * @throws \InvalidArgumentException for invalid paths.
336
     */
337
    public function withPath($path)
338
    {
339
        return $this->with('path', $path);
340
    }
341
342
    /**
343
     * Return an instance with the specified query string.
344
     *
345
     * This method MUST retain the state of the current instance, and return
346
     * an instance that contains the specified query string.
347
     *
348
     * Users can provide both encoded and decoded query characters.
349
     * Implementations ensure the correct encoding as outlined in getQuery().
350
     *
351
     * An empty query string value is equivalent to removing the query string.
352
     *
353
     * @param string $query The query string to use with the new instance.
354
     * @return static A new instance with the specified query string.
355
     * @throws \InvalidArgumentException for invalid query strings.
356
     */
357
    public function withQuery($query)
358
    {
359
        return $this->with('query', $query);
360
    }
361
362
    /**
363
     * Return an instance with the specified URI fragment.
364
     *
365
     * This method MUST retain the state of the current instance, and return
366
     * an instance that contains the specified URI fragment.
367
     *
368
     * Users can provide both encoded and decoded fragment characters.
369
     * Implementations ensure the correct encoding as outlined in getFragment().
370
     *
371
     * An empty fragment value is equivalent to removing the fragment.
372
     *
373
     * @param string $fragment The fragment to use with the new instance.
374
     * @return static A new instance with the specified fragment.
375
     */
376
    public function withFragment($fragment)
377
    {
378
        return $this->with('fragment', $fragment);
379
    }
380
381
    /**
382
     * Return the string representation as a URI reference.
383
     *
384
     * Depending on which components of the URI are present, the resulting
385
     * string is either a full URI or relative reference according to RFC 3986,
386
     * Section 4.1. The method concatenates the various components of the URI,
387
     * using the appropriate delimiters:
388
     *
389
     * - If a scheme is present, it MUST be suffixed by ":".
390
     * - If an authority is present, it MUST be prefixed by "//".
391
     * - The path can be concatenated without delimiters. But there are two
392
     *   cases where the path has to be adjusted to make the URI reference
393
     *   valid as PHP does not allow to throw an exception in __toString():
394
     *     - If the path is rootless and an authority is present, the path MUST
395
     *       be prefixed by "/".
396
     *     - If the path is starting with more than one "/" and no authority is
397
     *       present, the starting slashes MUST be reduced to one.
398
     * - If a query is present, it MUST be prefixed by "?".
399
     * - If a fragment is present, it MUST be prefixed by "#".
400
     *
401
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
402 9
     * @return string
403
     */
404 9
    public function __toString()
405 9
    {
406 9
        $scheme = $this->getScheme();
407 9
        $authority = $this->getAuthority();
408 9
        $path = $this->getPath();
409
        $fragment = $this->getFragment() ? '#'.$this->getFragment() : '';
410 9
        $query = $this->getQuery() ? '?'.$this->getQuery() : '';
411
412
        return "{$scheme}://{$authority}{$path}{$query}{$fragment}";
413
    }
414
415
    /**
416
     * @param string $scheme
417 10
     * @return HttpUri
418
     */
419 10
    public function setScheme($scheme)
420
    {
421 10
        $this->scheme = $scheme;
422
423
        return $this;
424
    }
425
426
    /**
427
     * @param string $userInfo
428 10
     * @return HttpUri
429
     */
430 10
    public function setUserInfo($userInfo)
431
    {
432 10
        $this->userInfo = $userInfo;
433
434
        return $this;
435
    }
436
437
    /**
438
     * @param string $host
439 10
     * @return HttpUri
440
     */
441 10
    public function setHost($host)
442
    {
443 10
        $this->host = $host;
444
445
        return $this;
446
    }
447
448
    /**
449
     * @param int|null $port
450 10
     * @return HttpUri
451
     */
452 10
    public function setPort($port)
453
    {
454 10
        $this->port = $port;
455
456
        return $this;
457
    }
458
459
    /**
460
     * @param string $path
461 10
     * @return HttpUri
462
     */
463 10
    public function setPath($path)
464
    {
465 10
        $this->path = $path;
466
467
        return $this;
468
    }
469
470
    /**
471
     * @param string $query
472 10
     * @return HttpUri
473
     */
474 10
    public function setQuery($query)
475
    {
476 10
        $this->query = $query;
477
478
        return $this;
479
    }
480
481
    /**
482
     * @param string $fragment
483 10
     * @return HttpUri
484
     */
485 10
    public function setFragment($fragment)
486
    {
487 10
        $this->fragment = $fragment;
488
489
        return $this;
490 12
    }
491
492 12
    private static function regexDecomposeUri($uri)
493
    {
494 12
        if (!preg_match(self::PATTERN, $uri, $m)) {
495 4
            throw new \InvalidArgumentException("Invalid HTTP URI");
496
        }
497
498 9
        foreach ($m as $key => $value) {
499
            if (is_int($key)) {
500 9
                unset($m[$key]);
501 9
            }
502 9
        }
503
504
        return $m;
505 9
    }
506 9
507 9
    private static function sanitizeComponents($components)
508 9
    {
509 9
        $components['userInfo'] = preg_replace('/[@]$/', '', $components['userInfo']);
510 1
        $components['query'] = preg_replace('/^[?]/', '', $components['query']);
511 1
        $components['fragment'] = preg_replace('/^[#]/', '', $components['fragment']);
512 9
        $components['port'] = str_replace(':', '', $components['port']);
513 9
        if (!is_numeric($components['port'])) {
514
            $components['port'] = null;
515 9
        }
516
517
        return $components;
518
    }
519 9
520
    public static function parseUri($uri)
521 9
    {
522 9
        $components = self::getDefaultComponents();
523 9
        $allowedComponents = ['scheme', 'userInfo', 'host', 'port', 'path', 'query', 'fragment'];
524 9
525 9
        foreach (self::regexDecomposeUri($uri) as $component => $value) {
526 9
            if (array_search($component, $allowedComponents) !== false) {
527 8
                $components[$component] = $value;
528
            }
529 9
        }
530 9
531 9
        return self::sanitizeComponents($components);
532 9
    }
533 9
534 9
    public static function createFromString($uri)
535 9
    {
536
        $parts = self::parseUri($uri);
537 9
538
        return self::createFromComponents($parts);
539 9
    }
540
541 9
    public static function createFromComponents($parts)
542
    {
543
        // Set default values
544 10
        $parts = array_merge(self::getDefaultComponents(), $parts);
545
546 10
        $uri = (new HttpUri())
547
            ->setScheme($parts['scheme'])
548 7
            ->setUserInfo($parts['userInfo'])
549
            ->setHost($parts['host'])
550
            ->setPort($parts['port'])
551 10
            ->setPath($parts['path'])
552
            ->setQuery($parts['query'])
553
            ->setFragment($parts['fragment']);
554 10
555
        return $uri;
556 10
    }
557 10
558 10
    private static function getDefaultComponents()
559 10
    {
560 10
        return [
561 10
            'scheme' => '',
562 10
            'userInfo' => '',
563 10
            'host' => '',
564
            'port' => null,
565 10
            'path' => '',
566
            'query' => '',
567
            'fragment' => '',
568 12
        ];
569
    }
570
571 12
    private function getComponents()
572
    {
573
        return [
574
            'scheme' => $this->getScheme(),
575
            'userInfo' => $this->getUserInfo(),
576
            'host' => $this->getHost(),
577
            'port' => $this->getPort(),
578
            'path' => $this->getPath(),
579
            'query' => $this->getQuery(),
580
            'fragment' => $this->getFragment(),
581
        ];
582
    }
583
584
    private function with($component, $value)
585
    {
586
        $components = $this->getComponents();
587
        $components[$component] = $value;
588
589
        return self::createFromComponents($components);
590
    }
591
}
592