Completed
Push — master ( 841dd0...701ded )
by Bohuslav
03:14
created

Uri::getAuthority()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
ccs 4
cts 4
cp 1
rs 9.4285
cc 3
eloc 4
nc 4
nop 0
crap 3
1
<?php
2
namespace Kambo\HttpMessage;
3
4
// \Spl
5
use InvalidArgumentException;
6
7
// \Psr
8
use Psr\Http\Message\UriInterface;
9
10
// \HttpMessage
11
use Kambo\HttpMessage\Enviroment;
12
13
/**
14
 * Value object representing a URI.
15
 *
16
 * This class represent URIs according to RFC 3986 and to
17
 * provide methods for most common operations. Additional functionality for
18
 * working with URIs can be provided on top of the object or externally.
19
 * Its primary use is for HTTP requests, but may also be used in other
20
 * contexts.
21
 *
22
 * Instances of the uri are considered immutable; all methods that
23
 * change state retain the internal state of the current message and return
24
 * an instance that contains the changed state.
25
 *
26
 * @link http://tools.ietf.org/html/rfc3986 (the URI specification)
27
 *
28
 * @package Kambo\HttpMessage
29
 * @author  Bohuslav Simek <[email protected]>
30
 * @license MIT
31
 */
32
class Uri implements UriInterface
33
{
34
    const HTTP  = 'http';
35
    const HTTPS = 'https';
36
37
    /**
38
     * Http port
39
     *
40
     * @var integer
41
     */
42
    const HTTP_PORT = 80;
43
44
    /**
45
     * Https port
46
     *
47
     * @var integer
48
     */
49
    const HTTPS_PORT = 443;
50
51
    /**
52
     * Valid URI schemas
53
     * Schema can be http, https or empty.
54
     *
55
     * @var array in format [ string "schema"=> boolean true (true valid), ... ]
56
     */
57
    private $validSchema = [
58
        '' => true,
59
        self::HTTPS => true,
60
        self::HTTP => true,
61
    ];
62
63
    /**
64
     * URL scheme of the URI.
65
     * Default value is an empty string.
66
     *
67
     * @var string
68
     */
69
    private $scheme = '';
70
71
    /**
72
     * Path component of the URI.
73
     * Default value is an empty string.
74
     *
75
     * @var string
76
     */
77
    private $path = '';
78
79
    /**
80
     * Query string of the URI.
81
     * Default value is an empty string.
82
     *
83
     * @var string
84
     */
85
    private $query = '';
86
87
    /**
88
     * Fragment component of the URI.
89
     * Default value is an empty string.
90
     *
91
     * @var string
92
     */
93
    private $fragment = '';
94
95
    /**
96
     * User part of the URI.
97
     * Default value is an empty string.
98
     *
99
     * @var string
100
     */
101
    private $user = '';
102
103
    /**
104
     * Password part of the URI.
105
     * Default value is an empty string.
106
     *
107
     * @var string
108
     */
109
    private $password = '';
110
111
    /**
112
     * Host component of the URI eg. foo.bar
113
     *
114
     * @var string
115
     */
116
    private $host;
117
118
    /**
119
     * Port component of the URI eg. 443.
120
     *
121
     * @var integer
122
     */
123
    private $port;
124
125
    /**
126
     * Create new Uri.
127
     *
128
     * @param string $scheme   Uri scheme.
129
     * @param string $host     Uri host.
130
     * @param int    $port     Uri port number.
131
     * @param string $path     Uri path.
132
     * @param string $query    Uri query string.
133
     * @param string $fragment Uri fragment.
134
     * @param string $user     Uri user.
135
     * @param string $password Uri password.
136
     */
137 72
    public function __construct(
138
        $scheme,
139
        $host,
140
        $port = null,
141
        $path = '/',
142
        $query = '',
143
        $fragment = '',
144
        $user = '',
145
        $password = ''
146
    ) {
147 72
        $this->scheme   = $this->normalizeScheme($scheme);
148 72
        $this->host     = strtolower($host);
149 72
        $this->port     = $port;
150 72
        $this->path     = $this->normalizePath($path);
151 71
        $this->query    = $this->urlEncode($query);
152 71
        $this->fragment = $fragment;
153 71
        $this->user     = $user;
154 71
        $this->password = $password;
155 71
    }
156
157
    /**
158
     * Retrieve the scheme component of the URI.
159
     *
160
     * If no scheme is present, this method return an empty string.
161
     *
162
     * Underline implementation normalize schema to lowercase, as described in RFC 3986
163
     * Section 3.1.
164
     *
165
     * The trailing ":" character is not part of the scheme and is not added.
166
     *
167
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
168
     *
169
     * @return string The URI scheme.
170
     */
171 15
    public function getScheme()
172
    {
173 15
        return $this->scheme;
174
    }
175
176
    /**
177
     * Retrieve the authority component of the URI.
178
     *
179
     * If no authority information is present, this method returns an empty
180
     * string.
181
     *
182
     * The authority syntax of the URI is:
183
     *
184
     * <pre>
185
     * [user-info@]host[:port]
186
     * </pre>
187
     *
188
     * If the port component is not set or is the standard port for the current
189
     * scheme, it is not included.
190
     *
191
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
192
     *
193
     * @return string The URI authority, in "[user-info@]host[:port]" format.
194
     */
195 11
    public function getAuthority()
196
    {
197 11
        $port     = $this->getPort();
198 11
        $userInfo = $this->getUserInfo();
199
200 11
        return ($userInfo ? $userInfo . '@' : '') . $this->getHost() . ($port ? ':' . $port : '');
201
    }
202
203
    /**
204
     * Retrieve the user information component of the URI.
205
     *
206
     * If no user information is present, this method returns an empty string.
207
     *
208
     * If a user is present in the URI, this method return that value;
209
     * additionally, if the password is also present, it will be appended to the
210
     * user value, with a colon (":") separating the values.
211
     *
212
     * The trailing "@" character is not part of the user information and is not added.
213
     *
214
     * @return string The URI user information, in "username[:password]" format.
215
     */
216 17
    public function getUserInfo()
217
    {
218 17
        $userInfo = '';
219 17
        if (!empty($this->user)) {
220 11
            $userInfo = $this->user;
221 11
            if (!empty($this->password)) {
222 10
                $userInfo .= ':' . $this->password;
223 10
            }
224 11
        }
225
226 17
        return $userInfo;
227
    }
228
229
    /**
230
     * Retrieve the host component of the URI.
231
     *
232
     * If no host is present, this method returns an empty string.
233
     *
234
     * Underline implementation normalize the host name to lowercase, as described in RFC 3986
235
     * Section 3.2.2.
236
     *
237
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
238
     *
239
     * @return string The URI host.
240
     */
241 21
    public function getHost()
242
    {
243 21
        return $this->host;
244
    }
245
246
    /**
247
     * Retrieve the port component of the URI.
248
     *
249
     * If a port is present, and it is non-standard for the current scheme,
250
     * this method return it as an integer. If the port is the standard port
251
     * used with the current scheme, this method return null.
252
     *
253
     * If no port is present, and no scheme is present, this method returns
254
     * a null value.
255
     *
256
     * If no port is present, but a scheme is present, this method MAY return
257
     * the standard port for that scheme, but SHOULD return null.
258
     *
259
     * @return null|int The URI port.
260
     */
261 17
    public function getPort()
262
    {
263 17
        return $this->isStandardPort() ? null : $this->port;
264
    }
265
266
    /**
267
     * Retrieve the path component of the URI.
268
     *
269
     * The path can either be empty or absolute (starting with a slash) or
270
     * rootless (not starting with a slash). Implementation support all
271
     * three syntaxes.
272
     *
273
     * Normally, the empty path "" and absolute path "/" are considered equal as
274
     * defined in RFC 7230 Section 2.7.3. But this method does not automatically
275
     * do this normalization because in contexts with a trimmed base path, e.g.
276
     * the front controller, this difference becomes significant. It's the task
277
     * of the user to handle both "" and "/".
278
     *
279
     * The value returned is percent-encoded, and underline implementation ensure that 
280
     * the value is not double-encode. Encoded characters are defined in 
281
     * RFC 3986, Sections 2 and 3.4.
282
     *
283
     * As an example, if the value should include a slash ("/") not intended as
284
     * delimiter between path segments, that value is passed in encoded
285
     * form (e.g., "%2F") to the instance.
286
     *
287
     * @see https://tools.ietf.org/html/rfc3986#section-2
288
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
289
     *
290
     * @return string The URI path.
291
     */
292 23
    public function getPath()
293
    {
294 23
        return $this->path;
295
    }
296
297
    /**
298
     * Retrieve the query string of the URI.
299
     *
300
     * If no query string is present, this method returns an empty string.
301
     *
302
     * The leading "?" character is not part of the query and it is not added.
303
     *
304
     * The value returned is percent-encoded, and implementation ensure that 
305
     * the value is not double-encode. Encoded characters are defined in 
306
     * RFC 3986, Sections 2 and 3.4.
307
     *
308
     * As an example, if a value in a key/value pair of the query string should
309
     * include an ampersand ("&") not intended as a delimiter between values,
310
     * that value is passed in encoded form (e.g., "%26") to the instance.
311
     *
312
     * @see https://tools.ietf.org/html/rfc3986#section-2
313
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
314
     *
315
     * @return string The URI query string.
316
     */
317 24
    public function getQuery()
318
    {
319 24
        return $this->query;
320
    }
321
322
323
    /**
324
     * Retrieve the fragment component of the URI.
325
     *
326
     * If no fragment is present, this method returns an empty string.
327
     *
328
     * The leading "#" character is not part of the fragment and it is not
329
     * added.
330
     *
331
     * The value returned is percent-encoded, and underline implementation ensure that 
332
     * the value is not double-encode. Encoded characters are defined in 
333
     * RFC 3986, Sections 2 and 3.4.
334
     *
335
     * @see https://tools.ietf.org/html/rfc3986#section-2
336
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
337
     *
338
     * @return string The URI fragment.
339
     */
340 15
    public function getFragment()
341
    {
342 15
        return $this->fragment;
343
    }
344
345
    /**
346
     * Return an instance with the specified scheme.
347
     *
348
     * This method retains the state of the current instance, and returns
349
     * an instance that contains the specified scheme.
350
     *
351
     * Implementations support the schemes "http" and "https" case
352
     * insensitively.
353
     *
354
     * An empty scheme is equivalent to removing the scheme.
355
     *
356
     * @param string $scheme The scheme to use with the new instance.
357
     *
358
     * @return self A new instance with the specified scheme.
359
     *
360
     * @throws \InvalidArgumentException for invalid or unsupported schemes.
361
     */
362 3
    public function withScheme($scheme)
363
    {
364 3
        $clone         = clone $this;
365 3
        $clone->scheme = $this->normalizeScheme($scheme);
366
367 1
        return $clone;
368
    }
369
370
    /**
371
     * Return an instance with the specified user information.
372
     *
373
     * This method retains the state of the current instance, and returns
374
     * an instance that contains the specified user information.
375
     *
376
     * Password is optional, but the user information include the
377
     * user; an empty string for the user is equivalent to removing user
378
     * information.
379
     *
380
     * @param string $user The user name to use for authority.
381
     * @param null|string $password The password associated with $user.
382
     *
383
     * @return self A new instance with the specified user information.
384
     */
385 2
    public function withUserInfo($user, $password = null)
386
    {
387 2
        $clone           = clone $this;
388 2
        $clone->user     = $user;
389 2
        $clone->password = $password;
390
391 2
        return $clone;
392
    }
393
394
    /**
395
     * Return an instance with the specified host.
396
     *
397
     * This method retains the state of the current instance, and returns
398
     * an instance that contains the specified host.
399
     *
400
     * An empty host value is equivalent to removing the host.
401
     *
402
     * @param string $host The hostname to use with the new instance.
403
     *
404
     * @return self A new instance with the specified host.
405
     *
406
     * @throws \InvalidArgumentException for invalid hostnames.
407
     */
408 1
    public function withHost($host)
409
    {
410 1
        $clone       = clone $this;
411 1
        $clone->host = strtolower($host);
412
413 1
        return $clone;
414
    }
415
416
    /**
417
     * Return an instance with the specified port.
418
     *
419
     * This method retains the state of the current instance, and return
420
     * an instance that contains the specified port.
421
     *
422
     * Method raise an exception for ports outside the
423
     * established TCP and UDP port ranges.
424
     *
425
     * A null value provided for the port is equivalent to removing the port
426
     * information.
427
     *
428
     * @param null|int $port The port to use with the new instance; a null value
429
     *     removes the port information.
430
     *
431
     * @return self A new instance with the specified port.
432
     *
433
     * @throws \InvalidArgumentException for invalid ports.
434
     */
435 2
    public function withPort($port)
436
    {
437 2
        $clone       = clone $this;
438 2
        $clone->port = $this->validatePort($port);
439
440 1
        return $clone;
441
    }
442
443
    /**
444
     * Return an instance with the specified path.
445
     *
446
     * This method retains the state of the current instance, and return
447
     * an instance that contains the specified path.
448
     *
449
     * The path can either be empty or absolute (starting with a slash) or
450
     * rootless (not starting with a slash). Implementations support all
451
     * three syntaxes.
452
     *
453
     * If the path is intended to be domain-relative rather than path relative then
454
     * it must begin with a slash ("/"). Paths not starting with a slash ("/")
455
     * are assumed to be relative to some base path known to the application or
456
     * consumer.
457
     *
458
     * Users can provide both encoded and decoded path characters.
459
     * Implementations ensure the correct encoding as outlined in getPath().
460
     *
461
     * @param string $path The path to use with the new instance.
462
     *
463
     * @return self A new instance with the specified path.
464
     *
465
     * @throws \InvalidArgumentException for invalid paths.
466
     */
467 4
    public function withPath($path)
468
    {
469 4
        $this->validatePath($path);
470
471 1
        $clone       = clone $this;
472 1
        $clone->path = $this->urlEncode((string) $path);
473
474 1
        return $clone;
475
    }
476
477
    /**
478
     * Return an instance with the specified query string.
479
     *
480
     * This method retains the state of the current instance, and return
481
     * an instance that contains the specified query string.
482
     *
483
     * Users can provide both encoded and decoded query characters.
484
     * Implementation ensure the correct encoding as outlined in getQuery().
485
     *
486
     * An empty query string value is equivalent to removing the query string.
487
     *
488
     * @param string $query The query string to use with the new instance.
489
     *
490
     * @return self A new instance with the specified query string.
491
     *
492
     * @throws \InvalidArgumentException for invalid query strings.
493
     */
494 5
    public function withQuery($query)
495
    {
496 5
        $this->validateQuery($query);
497
498 2
        $clone        = clone $this;
499 2
        $clone->query = $this->urlEncode((string) $query);
500
501 2
        return $clone;
502
    }
503
504
    /**
505
     * Return an instance with the specified URI fragment.
506
     *
507
     * This method retains the state of the current instance, and return
508
     * an instance that contains the specified URI fragment.
509
     *
510
     * Users can provide both encoded and decoded fragment characters.
511
     * Implementation ensure the correct encoding as outlined in getFragment().
512
     *
513
     * An empty fragment value is equivalent to removing the fragment.
514
     *
515
     * @param string $fragment The fragment to use with the new instance.
516
     *
517
     * @return self A new instance with the specified fragment.
518
     */
519 1
    public function withFragment($fragment)
520
    {
521 1
        $clone           = clone $this;
522 1
        $clone->fragment = $fragment;
523
524 1
        return $clone;
525
    }
526
527
    /**
528
     * Return the string representation as a URI reference.
529
     *
530
     * Depending on which components of the URI are present, the resulting
531
     * string is either a full URI or relative reference according to RFC 3986,
532
     * Section 4.1. The method concatenates the various components of the URI,
533
     * using the appropriate delimiters:
534
     *
535
     * - If a scheme is present, it is suffixed by ":".
536
     * - If an authority is present, it is prefixed by "//".
537
     * - The path can be concatenated without delimiters. But there are two
538
     *   cases where the path has to be adjusted to make the URI reference
539
     *   valid as PHP does not allow to throw an exception in __toString():
540
     *     - If the path is rootless and an authority is present, the path is
541
     *       be prefixed by "/".
542
     *     - If the path is starting with more than one "/" and no authority is
543
     *       present, the starting slashes are be reduced to one.
544
     * - If a query is present, it is prefixed by "?".
545
     * - If a fragment is present, it is prefixed by "#".
546
     *
547
     *         foo://example.com:8042/over/there?name=ferret#nose
548
     *         \_/   \______________/\_________/ \_________/ \__/
549
     *          |           |            |            |        |
550
     *       scheme     authority       path        query   fragment   
551
     *  
552
     * @see http://tools.ietf.org/html/rfc3986#section-4.1
553
     *
554
     * @return string
555
     */
556 11
    public function __toString()
557
    {
558 11
        $scheme    = $this->getScheme();
559 11
        $authority = $this->getAuthority();
560 11
        $path      = $this->getPath();
561 11
        $query     = $this->getQuery();
562 11
        $fragment  = $this->getFragment();
563
564 11
        $path = '/' . ltrim($path, '/');
565
566 11
        return ($scheme ? $scheme . ':' : '') . ($authority ? '//' . $authority : '')
567 11
            . $path . ($query ? '?' . $query : '') . ($fragment ? '#' . $fragment : '');
568
    }
569
570
    // ------------ PRIVATE METHODS
571
572
    /**
573
     * Check if Uri use a standard port.
574
     *
575
     * @return bool
576
     */
577 17
    private function isStandardPort()
578
    {
579 17
        return ($this->scheme === self::HTTP && $this->port === self::HTTP_PORT)
580 17
            || ($this->scheme === self::HTTPS && $this->port === self::HTTPS_PORT);
581
    }
582
583
    /**
584
     * Normalize scheme part of Uri.
585
     *
586
     * @param string $scheme Raw Uri scheme.
587
     *
588
     * @return string Normalized Uri
589
     *
590
     * @throws InvalidArgumentException If the Uri scheme is not a string.
591
     * @throws InvalidArgumentException If Uri scheme is not "", "https", or "http".
592
     */
593 72
    private function normalizeScheme($scheme)
594
    {
595 72
        if (!is_string($scheme) && !method_exists($scheme, '__toString')) {
596 1
            throw new InvalidArgumentException('Uri scheme must be a string');
597
        }
598
599 72
        $scheme = str_replace('://', '', strtolower((string) $scheme));
600 72
        if (!isset($this->validSchema[$scheme])) {
601 1
            throw new InvalidArgumentException('Uri scheme must be one of: "", "https", "http"');
602
        }
603
604 72
        return $scheme;
605
    }
606
607
    /**
608
     * Normalize path part of Uri and ensure it is properly encoded..
609
     *
610
     * @param string $path Raw Uri path.
611
     *
612
     * @return string Normalized Uri path
613
     *
614
     * @throws InvalidArgumentException If the Uri scheme is not a string.
615
     */
616 72
    private function normalizePath($path)
617
    {
618 72
        if (!is_string($path) && !method_exists($path, '__toString')) {
619 1
            throw new InvalidArgumentException('Uri path must be a string');
620
        }
621
622 71
        $path = $this->urlEncode($path);
623
624 71
        if (empty($path)) {
625 1
            return '';
626
        }
627
628 70
        if ($path[0] !== '/') {
629 1
            return $path;
630
        }
631
632
        // Ensure only one leading slash
633 69
        return '/' . ltrim($path, '/');
634
    }
635
636
    /**
637
     * Validate Uri port.
638
     * Value can be null or integer between 1 and 65535.
639
     *
640
     * @param  null|int $port The Uri port number.
641
     *
642
     * @return null|int
643
     *
644
     * @throws InvalidArgumentException If the port is invalid.
645
     */
646 2
    private function validatePort($port)
647
    {
648 2
        if (is_null($port) || (is_integer($port) && ($port >= 1 && $port <= 65535))) {
649 1
            return $port;
650
        }
651
652 1
        throw new InvalidArgumentException('Uri port must be null or an integer between 1 and 65535 (inclusive)');
653
    }
654
655
    /**
656
     * Validate Uri path. 
657
     *
658
     * Path must NOT contain query string or URI fragment. It can be object,
659
     * but then the class must implement __toString method.
660
     *
661
     * @param  string|object $path The Uri path
662
     *
663
     * @return void
664
     *
665
     * @throws InvalidArgumentException If the path is invalid.
666
     */
667 4
    private function validatePath($path)
668
    {
669 4
        if (!is_string($path) && !method_exists($path, '__toString')) {
670 1
            throw new InvalidArgumentException(
671
                'Invalid path provided; must be a string'
672 1
            );
673
        }
674
675 3
        if (strpos($path, '?') !== false) {
676 1
            throw new InvalidArgumentException(
677
                'Invalid path provided; must not contain a query string'
678 1
            );
679
        }
680
681 2
        if (strpos($path, '#') !== false) {
682 1
            throw new InvalidArgumentException(
683
                'Invalid path provided; must not contain a URI fragment'
684 1
            );
685
        }
686 1
    }
687
688
    /**
689
     * Validate query. 
690
     *
691
     * Path must NOT contain URI fragment. It can be object,
692
     * but then the class must implement __toString method.
693
     *
694
     * @param  string|object $query The query path
695
     *
696
     * @return void
697
     *
698
     * @throws InvalidArgumentException If the query is invalid.
699
     */
700 5
    private function validateQuery($query)
701
    {
702 5
        if (!is_string($query) && !method_exists($query, '__toString')) {
703 2
            throw new InvalidArgumentException(
704
                'Query must be a string'
705 2
            );
706
        }
707
708 3
        if (strpos($query, '#') !== false) {
709 1
            throw new InvalidArgumentException(
710
                'Query must not contain a URI fragment'
711 1
            );
712
        }
713 2
    }
714
715
    /**
716
     * Url encode 
717
     *
718
     * This method percent-encodes all reserved
719
     * characters in the provided path string. This method
720
     * will NOT double-encode characters that are already
721
     * percent-encoded.
722
     *
723
     * @param  string $path The raw uri path.
724
     *
725
     * @return string The RFC 3986 percent-encoded uri path.
726
     *
727
     * @link   http://www.faqs.org/rfcs/rfc3986.html
728
     */
729 71
    private function urlEncode($path)
730
    {
731 71
        return preg_replace_callback(
732 71
            '/(?:[^a-zA-Z0-9_\-\.~:@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
733 71
            function ($match) {
734 1
                return rawurlencode($match[0]);
735 71
            },
736
            $path
737 71
        );
738
    }
739
}
740