Uri::withUserInfo()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

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