Completed
Push — master ( 2ef68a...bd1ace )
by Derek
02:20
created

Uri::sanitizeUriPartsArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 6
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 3
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( //TODO: Probably don't need this anymore; refactor tests and remove
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...
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 59
    public function __construct($uri)
76
    {
77 59
        if (!is_string($uri)) {
78 6
            throw new \InvalidArgumentException("New Uri objects must be constructed with a string URI");
79
        }
80
81 53
        $this->explodeUri($uri);
82 53
    }
83
84
    /**
85
     * Retrieve the scheme component of the URI.
86
     *
87
     * If no scheme is present, this method MUST return an empty string.
88
     *
89
     * The value returned MUST be normalized to lowercase, per RFC 3986
90
     * Section 3.1.
91
     *
92
     * The trailing ":" character is not part of the scheme and MUST NOT be
93
     * added.
94
     *
95
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
96
     * @return string The URI scheme.
97
     */
98 21
    public function getScheme()
99
    {
100 21
        return (string) $this->scheme;
101
    }
102
103
    /**
104
     * Retrieve the authority component of the URI.
105
     *
106
     * If no authority information is present, this method MUST return an empty
107
     * string.
108
     *
109
     * The authority syntax of the URI is:
110
     *
111
     * <pre>
112
     * [user-info@]host[:port]
113
     * </pre>
114
     *
115
     * If the port component is not set or is the standard port for the current
116
     * scheme, it SHOULD NOT be included.
117
     *
118
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
119
     * @return string The URI authority, in "[user-info@]host[:port]" format.
120
     */
121 25
    public function getAuthority()
122
    {
123 25
        return $this->user_info->toUriString() . $this->host->toUriString() . $this->port->toUriString($this->scheme);
124
    }
125
126
    /**
127
     * Retrieve the user information component of the URI.
128
     *
129
     * If no user information is present, this method MUST return an empty
130
     * string.
131
     *
132
     * If a user is present in the URI, this will return that value;
133
     * additionally, if the password is also present, it will be appended to the
134
     * user value, with a colon (":") separating the values.
135
     *
136
     * The trailing "@" character is not part of the user information and MUST
137
     * NOT be added.
138
     *
139
     * @return string The URI user information, in "username[:password]" format.
140
     */
141 23
    public function getUserInfo()
142
    {
143 23
        return (string) $this->user_info;
144
    }
145
146
    /**
147
     * Retrieve the host component of the URI.
148
     *
149
     * If no host is present, this method MUST return an empty string.
150
     *
151
     * The value returned MUST be normalized to lowercase, per RFC 3986
152
     * Section 3.2.2.
153
     *
154
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
155
     * @return string The URI host.
156
     */
157 23
    public function getHost()
158
    {
159 23
        return (string) $this->host;
160
    }
161
162
    /**
163
     * Retrieve the port component of the URI.
164
     *
165
     * If a port is present, and it is non-standard for the current scheme,
166
     * this method MUST return it as an integer. If the port is the standard port
167
     * used with the current scheme, this method SHOULD return null.
168
     *
169
     * If no port is present, and no scheme is present, this method MUST return
170
     * a null value.
171
     *
172
     * If no port is present, but a scheme is present, this method MAY return
173
     * the standard port for that scheme, but SHOULD return null.
174
     *
175
     * @return null|int The URI port.
176
     */
177 25
    public function getPort()
178
    {
179 25
        $normalized_port = $this->port->normalizePortAgainstScheme($this->scheme);
180
181 25
        return $normalized_port;
182
    }
183
184
    /**
185
     * Retrieve the path component of the URI.
186
     *
187
     * The path can either be empty or absolute (starting with a slash) or
188
     * rootless (not starting with a slash). Implementations MUST support all
189
     * three syntaxes.
190
     *
191
     * Normally, the empty path "" and absolute path "/" are considered equal as
192
     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
193
     * do this normalization because in contexts with a trimmed base path, e.g.
194
     * the front controller, this difference becomes significant. It's the task
195
     * of the user to handle both "" and "/".
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.3.
200
     *
201
     * As an example, if the value should include a slash ("/") not intended as
202
     * delimiter between path segments, that value MUST be passed in encoded
203
     * form (e.g., "%2F") to the instance.
204
     *
205
     * @see https://tools.ietf.org/html/rfc3986#section-2
206
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
207
     * @return string The URI path.
208
     */
209 30
    public function getPath()
210
    {
211 30
        return (string) $this->path;
212
    }
213
214
    /**
215
     * Retrieve the query string of the URI.
216
     *
217
     * If no query string is present, this method MUST return an empty string.
218
     *
219
     * The leading "?" character is not part of the query 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.4.
225
     *
226
     * As an example, if a value in a key/value pair of the query string should
227
     * include an ampersand ("&") not intended as a delimiter between values,
228
     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
229
     *
230
     * @see https://tools.ietf.org/html/rfc3986#section-2
231
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
232
     * @return string The URI query string.
233
     */
234 22
    public function getQuery()
235
    {
236 22
        return (string) $this->query;
237
    }
238
239
    /**
240
     * Retrieve the fragment component of the URI.
241
     *
242
     * If no fragment is present, this method MUST return an empty string.
243
     *
244
     * The leading "#" character is not part of the fragment and MUST NOT be
245
     * added.
246
     *
247
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
248
     * any characters. To determine what characters to encode, please refer to
249
     * RFC 3986, Sections 2 and 3.5.
250
     *
251
     * @see https://tools.ietf.org/html/rfc3986#section-2
252
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
253
     * @return string The URI fragment.
254
     */
255 22
    public function getFragment()
256
    {
257 22
        return (string) $this->fragment;
258
    }
259
260
    /**
261
     * Return an instance with the specified scheme.
262
     *
263
     * This method MUST retain the state of the current instance, and return
264
     * an instance that contains the specified scheme.
265
     *
266
     * Implementations MUST support the schemes "http" and "https" case
267
     * insensitively, and MAY accommodate other schemes if required.
268
     *
269
     * An empty scheme is equivalent to removing the scheme.
270
     *
271
     * @param string $scheme The scheme to use with the new instance.
272
     * @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...
273
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
274
     */
275
    public function withScheme($scheme)
276
    {
277
        // 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...
278
    }
279
280
    /**
281
     * Return an instance with the specified authority.
282
     *
283
     * This method MUST retain the state of the current instance, and return
284
     * an instance that contains the specified authority.
285
     *
286
     * Replacing the authority is equivalent to replacing or removing all authority components depending upon the
287
     * composition of the authority.
288
     *
289
     * An empty authority is equivalent to removing the authority and all authority components.
290
     *
291
     * @param string $authority The scheme to use with the new instance.
292
     * @return static A new instance with the specified authority.
293
     * @throws \InvalidArgumentException for invalid authorities.
294
     */
295 21
    public function withAuthority($authority)
296
    {
297 21
        $with_authority = new Authority($authority);
298
299 15
        $new_authority_uri = clone $this;
300
301 15
        $new_authority_uri->authority = $with_authority;
302
303 15
        $new_authority_uri->user_info = $with_authority->getUserInfo();
304 15
        $new_authority_uri->host      = $with_authority->getHost();
305 15
        $new_authority_uri->port      = $with_authority->getPort();
306
307 15
        return $new_authority_uri;
308
    }
309
310
    /**
311
     * Return an instance with the specified user information.
312
     *
313
     * This method MUST retain the state of the current instance, and return
314
     * an instance that contains the specified user information.
315
     *
316
     * Password is optional, but the user information MUST include the
317
     * user; an empty string for the user is equivalent to removing user
318
     * information.
319
     *
320
     * @param string $user The user name to use for authority.
321
     * @param null|string $password The password associated with $user.
322
     * @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...
323
     */
324
    public function withUserInfo($user, $password = null)
325
    {
326
        // 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...
327
    }
328
329
    /**
330
     * Return an instance with the specified host.
331
     *
332
     * This method MUST retain the state of the current instance, and return
333
     * an instance that contains the specified host.
334
     *
335
     * An empty host value is equivalent to removing the host.
336
     *
337
     * @param string $host The hostname to use with the new instance.
338
     * @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...
339
     * @throws \InvalidArgumentException for invalid hostnames.
340
     */
341
    public function withHost($host)
342
    {
343
        // 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...
344
    }
345
346
    /**
347
     * Return an instance with the specified port.
348
     *
349
     * This method MUST retain the state of the current instance, and return
350
     * an instance that contains the specified port.
351
     *
352
     * Implementations MUST raise an exception for ports outside the
353
     * established TCP and UDP port ranges.
354
     *
355
     * A null value provided for the port is equivalent to removing the port
356
     * information.
357
     *
358
     * @param null|int $port The port to use with the new instance; a null value
359
     *     removes the port information.
360
     * @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...
361
     * @throws \InvalidArgumentException for invalid ports.
362
     */
363
    public function withPort($port)
364
    {
365
        // 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...
366
    }
367
368
    /**
369
     * Return an instance with the specified path.
370
     *
371
     * This method MUST retain the state of the current instance, and return
372
     * an instance that contains the specified path.
373
     *
374
     * The path can either be empty or absolute (starting with a slash) or
375
     * rootless (not starting with a slash). Implementations MUST support all
376
     * three syntaxes.
377
     *
378
     * If the path is intended to be domain-relative rather than path relative then
379
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
380
     * are assumed to be relative to some base path known to the application or
381
     * consumer.
382
     *
383
     * Users can provide both encoded and decoded path characters.
384
     * Implementations ensure the correct encoding as outlined in getPath().
385
     *
386
     * @throws \InvalidArgumentException for invalid paths and invalid path/authority combinations.
387
     *
388
     * @param string $path The path to use with the new instance.
389
     * @return static A new instance with the specified path.
390
     */
391 18
    public function withPath($path)
392
    {
393 18
        if (!Path::validatePathAgainstAuthority($path, $this->authority)) {
394 1
            throw new \InvalidArgumentException("Cannot create a URI with an authority given a rootless path");
395
        }
396
397 11
        $with_path = new Path($path);
398
399 11
        $new_path_uri = clone $this;
400
401 11
        $new_path_uri->path = $with_path;
402
403 11
        return $new_path_uri;
404
    }
405
406
    /**
407
     * Return an instance with the specified query string.
408
     *
409
     * This method MUST retain the state of the current instance, and return
410
     * an instance that contains the specified query string.
411
     *
412
     * Users can provide both encoded and decoded query characters.
413
     * Implementations ensure the correct encoding as outlined in getQuery().
414
     *
415
     * An empty query string value is equivalent to removing the query string.
416
     *
417
     * @param string $query The query string to use with the new instance.
418
     * @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...
419
     * @throws \InvalidArgumentException for invalid query strings.
420
     */
421
    public function withQuery($query)
422
    {
423
        // 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...
424
    }
425
426
    /**
427
     * Return an instance with the specified URI fragment.
428
     *
429
     * This method MUST retain the state of the current instance, and return
430
     * an instance that contains the specified URI fragment.
431
     *
432
     * Users can provide both encoded and decoded fragment characters.
433
     * Implementations ensure the correct encoding as outlined in getFragment().
434
     *
435
     * An empty fragment value is equivalent to removing the fragment.
436
     *
437
     * @param string $fragment The fragment to use with the new instance.
438
     * @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...
439
     */
440
    public function withFragment($fragment)
441
    {
442
        // 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...
443
    }
444
445
    /**
446
     * Return the string representation as a URI reference.
447
     *
448
     * Depending on which components of the URI are present, the resulting
449
     * string is either a full URI or relative reference according to RFC 3986,
450
     * Section 4.1. The method concatenates the various components of the URI,
451
     * using the appropriate delimiters:
452
     *
453
     * - If a scheme is present, it MUST be suffixed by ":".
454
     * - If an authority is present, it MUST be prefixed by "//".
455
     * - The path can be concatenated without delimiters. But there are two
456
     *   cases where the path has to be adjusted to make the URI reference
457
     *   valid as PHP does not allow to throw an exception in __toString():
458
     *     - If the path is rootless and an authority is present, the path MUST
459
     *       be prefixed by "/".
460
     *     - If the path is starting with more than one "/" and no authority is
461
     *       present, the starting slashes MUST be reduced to one.
462
     * - If a query is present, it MUST be prefixed by "?".
463
     * - If a fragment is present, it MUST be prefixed by "#".
464
     *
465
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
466
     * @return string
467
     */
468 10
    public function __toString()
469
    {
470 10
        $uri_string = "";
471
472 10
        $uri_string .= $this->scheme->toUriString();
473
474 10
        $uri_string .= $this->authority->toUriString($this->scheme);
475
476 10
        $uri_string .= $this->path->toUriString($this->authority);
477
478 10
        $uri_string .= $this->query->toUriString();
479
480 10
        $uri_string .= $this->fragment->toUriString();
481
482 10
        return $uri_string;
483
    }
484
485
    /**
486
     * Splits a string URI into its component parts, returning true if the URI string matches a valid URI's syntax
487
     * and false if the URI string does not
488
     *
489
     * @param string $uri   The URI string to be decomposed
490
     * @return bool         Returns true if the URI string matches a valid URI's syntax
491
     *                      Returns false otherwise
492
     */
493 53
    private function explodeUri($uri)
494
    {
495 53
        $reg_start     = '/^';
496 53
        $scheme_part   = '(?:(?\'scheme\'[A-Za-z0-9][^\/\?#:]+):)?';
497 53
        $hier_part     = '(?\'hier_part\'[^\?#]+)?';
498 53
        $query_part    = '(?:\?(?\'query\'[^#]*))?';
499 53
        $fragment_part = '(?:#(?\'fragment\'.*))?';
500 53
        $reg_end       = '/';
501
502 53
        $uri_syntax = $reg_start . $scheme_part . $hier_part . $query_part . $fragment_part . $reg_end;
503
504 53
        $uri_valid = preg_match($uri_syntax, $uri, $parts);
505
506 53
        $this->uri_parts = array_merge($this->uri_parts, $parts); //overwriting default values with matches
507
508 53
        $this->explodeHierParts($this->uri_parts["hier_part"]);
509
510 53
        $this->parseExplosions();
511
512 53
        return (bool) $uri_valid;
513
    }
514
515
    /**
516
     * Splits URI hierarchy data into authority and path data.
517
     *
518
     * @param string $hier_part     The hierarchy part of a URI to be decomposed
519
     * @return void
520
     */
521 53
    private function explodeHierParts($hier_part)
522
    {
523
        $hier_parts = array(
524 53
            "authority" => "",
525 53
            "path"      => "",
526 53
        );
527
528 53
        $reg_start      = '/^';
529 53
        $authority_part = '(?:(?:\/\/)(?\'authority\'.[^\/]+))?';
530 53
        $path_part      = '(?\'path\'.+)?';
531 53
        $reg_end        = '/';
532
533 53
        $hier_part_syntax = $reg_start . $authority_part . $path_part . $reg_end;
534
535 53
        preg_match($hier_part_syntax, $hier_part, $matches);
536
537 53
        $hier_parts = array_merge($hier_parts, $matches);
538
539 53
        $this->uri_parts = array_merge($this->uri_parts, $hier_parts);
540 53
    }
541
542
    /**
543
     * Parses the results of exploding URI parts.  Creates component objects and fills in uri_parts array with
544
     * sub-component data.
545
     *
546
     * @return void
547
     */
548 53
    private function parseExplosions()
549
    {
550 53
        $this->scheme    = new Scheme($this->uri_parts["scheme"]);
551 53
        $this->query     = new Query($this->uri_parts["query"]);
552 53
        $this->fragment  = new Fragment($this->uri_parts["fragment"]);
553 53
        $this->authority = new Authority($this->uri_parts["authority"]);
554 53
        $this->user_info = $this->authority->getUserInfo();
555 53
        $this->host      = $this->authority->getHost();
556 53
        $this->port      = $this->authority->getPort();
557 53
        $this->path      = new Path($this->uri_parts["path"]);
558
559 53
        $this->uri_parts["user_info"] = (string) $this->user_info;
560 53
        $this->uri_parts["host"]      = (string) $this->host;
561 53
        $this->uri_parts["port"]      = $this->port->getPort();
562 53
    }
563
}
564