Completed
Push — master ( 546ba5...28ae59 )
by Derek
02:29
created

Uri::getFragment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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