Completed
Push — master ( e5addf...f73fec )
by Derek
02:17
created

Uri::parseExplosions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 15
ccs 13
cts 13
cp 1
rs 9.4285
cc 1
eloc 12
nc 1
nop 0
crap 1
1
<?php
2
namespace Subreality\Dilmun\Anshar\Http;
3
4
use Psr\Http\Message\UriInterface;
5
use Subreality\Dilmun\Anshar\Http\UriParts\Authority;
6
use Subreality\Dilmun\Anshar\Http\UriParts\Fragment;
7
use Subreality\Dilmun\Anshar\Http\UriParts\Host;
8
use Subreality\Dilmun\Anshar\Http\UriParts\Path;
9
use Subreality\Dilmun\Anshar\Http\UriParts\Port;
10
use Subreality\Dilmun\Anshar\Http\UriParts\Query;
11
use Subreality\Dilmun\Anshar\Http\UriParts\Scheme;
12
use Subreality\Dilmun\Anshar\Http\UriParts\UserInfo;
13
use Subreality\Dilmun\Anshar\Utils\ArrayHelper;
14
15
/**
16
 * Class Uri
17
 * @package Subreality\Dilmun\Anshar\Http
18
 */
19
class Uri implements UriInterface
20
{
21
    protected $uri_parts = array(
22
        "scheme"    => "",
23
        "hier_part" => "",
24
        "authority" => "",
25
        "user_info" => "",
26
        "host"      => "",
27
        "port"      => null,
28
        "path"      => "",
29
        "query"     => "",
30
        "fragment"  => "",
31
    );
32
33
    /** @var  Scheme */
34
    protected $scheme;
35
    /** @var  Authority */
36
    protected $authority;
37
    /** @var  UserInfo */
38
    protected $user_info;
39
    /** @var  Host */
40
    protected $host;
41
    /** @var  Port */
42
    protected $port;
43
    /** @var  Path */
44
    protected $path;
45
    /** @var  Query */
46
    protected $query;
47
    /** @var  Fragment */
48
    protected $fragment;
49
50
    protected $sub_delims = array(
51
        "!",
52
        "$",
53
        "&",
54
        "'",
55
        "(",
56
        ")",
57
        "*",
58
        "+",
59
        ",",
60
        ";",
61
        "=",
62
    );
63
64
    protected $pchar_unencoded = array(
65
        ":",
66
        "@",
67
    );
68
69
    /**
70
     * Uri constructor.  Accepts a string representing a URI and parses the string into the URI's component parts.
71
     *
72
     * @throws \InvalidArgumentException    Throws an \InvalidArgumentException when its parameter is not a string
73
     * @param string $uri
74
     */
75 78
    public function __construct($uri)
76
    {
77 78
        if (!is_string($uri)) {
78 6
            throw new \InvalidArgumentException("New Uri objects must be constructed with a string URI");
79
        }
80
81 72
        $this->explodeUri($uri);
82 72
    }
83
84
    /**
85
     * Retrieve the parsed components of the URI string.
86
     *
87
     * If the class was provided an invalid or empty URI string, URI components will be empty strings, except port,
88
     * which will be null
89
     *
90
     * @return mixed[]
91
     */
92 55
    public function getParsedUri()
93
    {
94 55
        return $this->uri_parts;
95 10
    }
96
97
    /**
98
     * Retrieve the scheme component of the URI.
99
     *
100
     * If no scheme is present, this method MUST return an empty string.
101
     *
102
     * The value returned MUST be normalized to lowercase, per RFC 3986
103
     * Section 3.1.
104
     *
105
     * The trailing ":" character is not part of the scheme and MUST NOT be
106
     * added.
107
     *
108
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
109
     * @return string The URI scheme.
110
     */
111 3
    public function getScheme()
112
    {
113 3
        return (string) $this->scheme;
114
    }
115
116
    /**
117
     * Retrieve the authority component of the URI.
118
     *
119
     * If no authority information is present, this method MUST return an empty
120
     * string.
121
     *
122
     * The authority syntax of the URI is:
123
     *
124
     * <pre>
125
     * [user-info@]host[:port]
126
     * </pre>
127
     *
128
     * If the port component is not set or is the standard port for the current
129
     * scheme, it SHOULD NOT be included.
130
     *
131
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
132
     * @return string The URI authority, in "[user-info@]host[:port]" format.
133
     */
134 4
    public function getAuthority()
135
    {
136 4
        return $this->user_info->toUriString() . $this->host->toUriString() . $this->port->toUriString($this->scheme);
137
    }
138
139
    /**
140
     * Retrieve the user information component of the URI.
141
     *
142
     * If no user information is present, this method MUST return an empty
143
     * string.
144
     *
145
     * If a user is present in the URI, this will return that value;
146
     * additionally, if the password is also present, it will be appended to the
147
     * user value, with a colon (":") separating the values.
148
     *
149
     * The trailing "@" character is not part of the user information and MUST
150
     * NOT be added.
151
     *
152
     * @return string The URI user information, in "username[:password]" format.
153
     */
154 2
    public function getUserInfo()
155
    {
156 2
        return (string) $this->user_info;
157
    }
158
159
    /**
160
     * Retrieve the host component of the URI.
161
     *
162
     * If no host is present, this method MUST return an empty string.
163
     *
164
     * The value returned MUST be normalized to lowercase, per RFC 3986
165
     * Section 3.2.2.
166
     *
167
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
168
     * @return string The URI host.
169
     */
170 3
    public function getHost()
171
    {
172 3
        return (string) $this->host;
173
    }
174
175
    /**
176
     * Retrieve the port component of the URI.
177
     *
178
     * If a port is present, and it is non-standard for the current scheme,
179
     * this method MUST return it as an integer. If the port is the standard port
180
     * used with the current scheme, this method SHOULD return null.
181
     *
182
     * If no port is present, and no scheme is present, this method MUST return
183
     * a null value.
184
     *
185
     * If no port is present, but a scheme is present, this method MAY return
186
     * the standard port for that scheme, but SHOULD return null.
187
     *
188
     * @return null|int The URI port.
189
     */
190 4
    public function getPort()
191
    {
192 4
        $normalized_port = $this->port->normalizePortAgainstScheme($this->scheme);
193
194 4
        return $normalized_port;
195
    }
196
197
    /**
198
     * Retrieve the path component of the URI.
199
     *
200
     * The path can either be empty or absolute (starting with a slash) or
201
     * rootless (not starting with a slash). Implementations MUST support all
202
     * three syntaxes.
203
     *
204
     * Normally, the empty path "" and absolute path "/" are considered equal as
205
     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
206
     * do this normalization because in contexts with a trimmed base path, e.g.
207
     * the front controller, this difference becomes significant. It's the task
208
     * of the user to handle both "" and "/".
209
     *
210
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
211
     * any characters. To determine what characters to encode, please refer to
212
     * RFC 3986, Sections 2 and 3.3.
213
     *
214
     * As an example, if the value should include a slash ("/") not intended as
215
     * delimiter between path segments, that value MUST be passed in encoded
216
     * form (e.g., "%2F") to the instance.
217
     *
218
     * @see https://tools.ietf.org/html/rfc3986#section-2
219
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
220
     * @return string The URI path.
221
     */
222 8
    public function getPath()
223
    {
224 8
        return (string) $this->path;
225
    }
226
227
    /**
228
     * Retrieve the query string of the URI.
229
     *
230
     * If no query string is present, this method MUST return an empty string.
231
     *
232
     * The leading "?" character is not part of the query and MUST NOT be
233
     * added.
234
     *
235
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
236
     * any characters. To determine what characters to encode, please refer to
237
     * RFC 3986, Sections 2 and 3.4.
238
     *
239
     * As an example, if a value in a key/value pair of the query string should
240
     * include an ampersand ("&") not intended as a delimiter between values,
241
     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
242
     *
243
     * @see https://tools.ietf.org/html/rfc3986#section-2
244
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
245
     * @return string The URI query string.
246
     */
247 4
    public function getQuery()
248
    {
249 4
        return (string) $this->query;
250
    }
251
252
    /**
253
     * Retrieve the fragment component of the URI.
254
     *
255
     * If no fragment is present, this method MUST return an empty string.
256
     *
257
     * The leading "#" character is not part of the fragment and MUST NOT be
258
     * added.
259
     *
260
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
261
     * any characters. To determine what characters to encode, please refer to
262
     * RFC 3986, Sections 2 and 3.5.
263
     *
264
     * @see https://tools.ietf.org/html/rfc3986#section-2
265
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
266
     * @return string The URI fragment.
267
     */
268 4
    public function getFragment()
269
    {
270 4
        return (string) $this->fragment;
271
    }
272
273
    /**
274
     * Return an instance with the specified scheme.
275
     *
276
     * This method MUST retain the state of the current instance, and return
277
     * an instance that contains the specified scheme.
278
     *
279
     * Implementations MUST support the schemes "http" and "https" case
280
     * insensitively, and MAY accommodate other schemes if required.
281
     *
282
     * An empty scheme is equivalent to removing the scheme.
283
     *
284
     * @param string $scheme The scheme to use with the new instance.
285
     * @return static A new instance with the specified scheme.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
286
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
287
     */
288
    public function withScheme($scheme)
289
    {
290
        // TODO: Implement withScheme() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
291
    }
292
293
    /**
294
     * Return an instance with the specified authority.
295
     *
296
     * This method MUST retain the state of the current instance, and return
297
     * an instance that contains the specified authority.
298
     *
299
     * Replacing the authority is equivalent to replacing or removing all authority components depending upon the
300
     * composition of the authority.
301
     *
302
     * An empty authority is equivalent to removing the authority and all authority components.
303
     *
304
     * @param string $authority The scheme to use with the new instance.
305
     * @return static A new instance with the specified authority.
306
     * @throws \InvalidArgumentException for invalid authorities.
307
     */
308 21
    public function withAuthority($authority)
309
    {
310 21
        $with_authority = new Authority($authority);
311
312 15
        $new_authority_uri = clone $this;
313
314 15
        $new_authority_uri->authority = $with_authority;
315
316 15
        $new_authority_uri->uri_parts["authority"] = $authority;
317 15
        $new_authority_uri->uri_parts["user_info"] = (string) $with_authority->getUserInfo();
318 15
        $new_authority_uri->uri_parts["host"]      = (string) $with_authority->getHost();
319 15
        $new_authority_uri->uri_parts["port"]      = $with_authority->getPort()->getPort();
320
321 15
        return $new_authority_uri;
322
    }
323
324
    /**
325
     * Return an instance with the specified user information.
326
     *
327
     * This method MUST retain the state of the current instance, and return
328
     * an instance that contains the specified user information.
329
     *
330
     * Password is optional, but the user information MUST include the
331
     * user; an empty string for the user is equivalent to removing user
332
     * information.
333
     *
334
     * @param string $user The user name to use for authority.
335
     * @param null|string $password The password associated with $user.
336
     * @return static A new instance with the specified user information.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
337
     */
338
    public function withUserInfo($user, $password = null)
339
    {
340
        // TODO: Implement withUserInfo() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
341
    }
342
343
    /**
344
     * Return an instance with the specified host.
345
     *
346
     * This method MUST retain the state of the current instance, and return
347
     * an instance that contains the specified host.
348
     *
349
     * An empty host value is equivalent to removing the host.
350
     *
351
     * @param string $host The hostname to use with the new instance.
352
     * @return static A new instance with the specified host.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
353
     * @throws \InvalidArgumentException for invalid hostnames.
354
     */
355
    public function withHost($host)
356
    {
357
        // TODO: Implement withHost() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
358
    }
359
360
    /**
361
     * Return an instance with the specified port.
362
     *
363
     * This method MUST retain the state of the current instance, and return
364
     * an instance that contains the specified port.
365
     *
366
     * Implementations MUST raise an exception for ports outside the
367
     * established TCP and UDP port ranges.
368
     *
369
     * A null value provided for the port is equivalent to removing the port
370
     * information.
371
     *
372
     * @param null|int $port The port to use with the new instance; a null value
373
     *     removes the port information.
374
     * @return static A new instance with the specified port.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
375
     * @throws \InvalidArgumentException for invalid ports.
376
     */
377
    public function withPort($port)
378
    {
379
        // TODO: Implement withPort() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
380
    }
381
382
    /**
383
     * Return an instance with the specified path.
384
     *
385
     * This method MUST retain the state of the current instance, and return
386
     * an instance that contains the specified path.
387
     *
388
     * The path can either be empty or absolute (starting with a slash) or
389
     * rootless (not starting with a slash). Implementations MUST support all
390
     * three syntaxes.
391
     *
392
     * If the path is intended to be domain-relative rather than path relative then
393
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
394
     * are assumed to be relative to some base path known to the application or
395
     * consumer.
396
     *
397
     * Users can provide both encoded and decoded path characters.
398
     * Implementations ensure the correct encoding as outlined in getPath().
399
     *
400
     * @throws \InvalidArgumentException for invalid paths and invalid path/authority combinations.
401
     *
402
     * @param string $path The path to use with the new instance.
403
     * @return static A new instance with the specified path.
404
     */
405 18
    public function withPath($path)
406
    {
407 18
        if (!Path::validatePathAgainstAuthority($path, $this->authority)) {
408 1
            throw new \InvalidArgumentException("Cannot create a URI with an authority given a rootless path");
409
        }
410
411 11
        $with_path = new Path($path);
412
413 11
        $new_path_uri = clone $this;
414
415 11
        $new_path_uri->path = $with_path;
416
417 11
        $new_path_uri->uri_parts["path"] = $path;
418
419 11
        return $new_path_uri;
420
    }
421
422
    /**
423
     * Return an instance with the specified query string.
424
     *
425
     * This method MUST retain the state of the current instance, and return
426
     * an instance that contains the specified query string.
427
     *
428
     * Users can provide both encoded and decoded query characters.
429
     * Implementations ensure the correct encoding as outlined in getQuery().
430
     *
431
     * An empty query string value is equivalent to removing the query string.
432
     *
433
     * @param string $query The query string to use with the new instance.
434
     * @return static A new instance with the specified query string.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
435
     * @throws \InvalidArgumentException for invalid query strings.
436
     */
437
    public function withQuery($query)
438
    {
439
        // TODO: Implement withQuery() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
440
    }
441
442
    /**
443
     * Return an instance with the specified URI fragment.
444
     *
445
     * This method MUST retain the state of the current instance, and return
446
     * an instance that contains the specified URI fragment.
447
     *
448
     * Users can provide both encoded and decoded fragment characters.
449
     * Implementations ensure the correct encoding as outlined in getFragment().
450
     *
451
     * An empty fragment value is equivalent to removing the fragment.
452
     *
453
     * @param string $fragment The fragment to use with the new instance.
454
     * @return static A new instance with the specified fragment.
0 ignored issues
show
Documentation introduced by
Should the return type not be Uri|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
455
     */
456
    public function withFragment($fragment)
457
    {
458
        // TODO: Implement withFragment() method.
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
459
    }
460
461
    /**
462
     * Return the string representation as a URI reference.
463
     *
464
     * Depending on which components of the URI are present, the resulting
465
     * string is either a full URI or relative reference according to RFC 3986,
466
     * Section 4.1. The method concatenates the various components of the URI,
467
     * using the appropriate delimiters:
468
     *
469
     * - If a scheme is present, it MUST be suffixed by ":".
470
     * - If an authority is present, it MUST be prefixed by "//".
471
     * - The path can be concatenated without delimiters. But there are two
472
     *   cases where the path has to be adjusted to make the URI reference
473
     *   valid as PHP does not allow to throw an exception in __toString():
474
     *     - If the path is rootless and an authority is present, the path MUST
475
     *       be prefixed by "/".
476
     *     - If the path is starting with more than one "/" and no authority is
477
     *       present, the starting slashes MUST be reduced to one.
478
     * - If a query is present, it MUST be prefixed by "?".
479
     * - If a fragment is present, it MUST be prefixed by "#".
480
     *
481
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
482
     * @return string
483
     */
484 8
    public function __toString()
485
    {
486 8
        return $this->toString();
487
    }
488
489
    /**
490
     * Converts a given array of URI parts to a string according to the specification of the __toString magic method
491
     *
492
     * @see Uri::__toString
493
     *
494
     * @return string           The string combined from the array of URI parts
495
     */
496 8
    private function toString()
497
    {
498 8
        $uri_string = "";
499
500 8
        $uri_string .= $this->scheme->toUriString();
501
502 8
        $uri_string .= $this->authority->toUriString($this->scheme);
503
504 8
        $uri_string .= $this->path->toUriString($this->authority);
505
506 8
        $uri_string .= $this->query->toUriString();
507
508 8
        $uri_string .= $this->fragment->toUriString();
509
510 8
        return $uri_string;
511
    }
512
513
    /**
514
     * Splits a string URI into its component parts, returning true if the URI string matches a valid URI's syntax
515
     * and false if the URI string does not
516
     *
517
     * @param string $uri   The URI string to be decomposed
518
     * @return bool         Returns true if the URI string matches a valid URI's syntax
519
     *                      Returns false otherwise
520
     */
521 72
    private function explodeUri($uri)
522
    {
523 72
        $reg_start     = '/^';
524 72
        $scheme_part   = '(?:(?\'scheme\'[A-Za-z0-9][^\/\?#:]+):)?';
525 72
        $hier_part     = '(?\'hier_part\'[^\?#]+)?';
526 72
        $query_part    = '(?:\?(?\'query\'[^#]*))?';
527 72
        $fragment_part = '(?:#(?\'fragment\'.*))?';
528 72
        $reg_end       = '/';
529
530 72
        $uri_syntax = $reg_start . $scheme_part . $hier_part . $query_part . $fragment_part . $reg_end;
531
532 72
        $uri_valid = preg_match($uri_syntax, $uri, $parts);
533
534 72
        $this->uri_parts = array_merge($this->uri_parts, $parts); //overwriting default values with matches
535
536 72
        $this->explodeHierParts($this->uri_parts["hier_part"]);
537
538 72
        $this->parseExplosions();
539
540 72
        $this->sanitizeUriPartsArray();
541
542 72
        return (bool) $uri_valid;
543
    }
544
545
    /**
546
     * Splits URI hierarchy data into authority and path data.
547
     *
548
     * @param string $hier_part     The hierarchy part of a URI to be decomposed
549
     * @return void
550
     */
551 72
    private function explodeHierParts($hier_part)
552
    {
553
        $hier_parts = array(
554 72
            "authority" => "",
555 72
            "path"      => "",
556 72
        );
557
558 72
        $reg_start      = '/^';
559 72
        $authority_part = '(?:(?:\/\/)(?\'authority\'.[^\/]+))?';
560 72
        $path_part      = '(?\'path\'.+)?';
561 72
        $reg_end        = '/';
562
563 72
        $hier_part_syntax = $reg_start . $authority_part . $path_part . $reg_end;
564
565 72
        preg_match($hier_part_syntax, $hier_part, $matches);
566
567 72
        $hier_parts = array_merge($hier_parts, $matches);
568
569 72
        $this->uri_parts = array_merge($this->uri_parts, $hier_parts);
570 72
    }
571
572
    /**
573
     * Parses the results of exploding URI parts.  Creates component objects and fills in uri_parts array with
574
     * sub-component data.
575
     *
576
     * @return void
577
     */
578 72
    private function parseExplosions()
579
    {
580 72
        $this->scheme    = new Scheme($this->uri_parts["scheme"]);
581 72
        $this->query     = new Query($this->uri_parts["query"]);
582 72
        $this->fragment  = new Fragment($this->uri_parts["fragment"]);
583 72
        $this->authority = new Authority($this->uri_parts["authority"]);
584 72
        $this->user_info = $this->authority->getUserInfo();
585 72
        $this->host      = $this->authority->getHost();
586 72
        $this->port      = $this->authority->getPort();
587 72
        $this->path      = new Path($this->uri_parts["path"]);
588
589 72
        $this->uri_parts["user_info"] = (string) $this->user_info;
590 72
        $this->uri_parts["host"]      = (string) $this->host;
591 72
        $this->uri_parts["port"]      = $this->port->getPort();
592 72
    }
593
594
    /**
595
     * Sanitizes the URI component array by removing redundant key/value pairs
596
     *
597
     * @return void
598
     */
599 72
    private function sanitizeUriPartsArray()
600
    {
601 72
        $uri_part_array = new ArrayHelper($this->uri_parts);
602
603 72
        $this->uri_parts = $uri_part_array->removeNumericKeys();
604 72
    }
605
}
606