Subversion Repositories cheapmusic

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
103 - 1
<?php
2
namespace GuzzleHttp\Psr7;
3
 
4
use Psr\Http\Message\UriInterface;
5
 
6
/**
7
 * PSR-7 URI implementation.
8
 *
9
 * @author Michael Dowling
10
 * @author Tobias Schultze
11
 * @author Matthew Weier O'Phinney
12
 */
13
class Uri implements UriInterface
14
{
15
    /**
16
     * Absolute http and https URIs require a host per RFC 7230 Section 2.7
17
     * but in generic URIs the host can be empty. So for http(s) URIs
18
     * we apply this default host when no host is given yet to form a
19
     * valid URI.
20
     */
21
    const HTTP_DEFAULT_HOST = 'localhost';
22
 
23
    private static $defaultPorts = [
24
        'http'  => 80,
25
        'https' => 443,
26
        'ftp' => 21,
27
        'gopher' => 70,
28
        'nntp' => 119,
29
        'news' => 119,
30
        'telnet' => 23,
31
        'tn3270' => 23,
32
        'imap' => 143,
33
        'pop' => 110,
34
        'ldap' => 389,
35
    ];
36
 
37
    private static $charUnreserved = 'a-zA-Z0-9_\-\.~';
38
    private static $charSubDelims = '!\$&\'\(\)\*\+,;=';
39
    private static $replaceQuery = ['=' => '%3D', '&' => '%26'];
40
 
41
    /** @var string Uri scheme. */
42
    private $scheme = '';
43
 
44
    /** @var string Uri user info. */
45
    private $userInfo = '';
46
 
47
    /** @var string Uri host. */
48
    private $host = '';
49
 
50
    /** @var int|null Uri port. */
51
    private $port;
52
 
53
    /** @var string Uri path. */
54
    private $path = '';
55
 
56
    /** @var string Uri query string. */
57
    private $query = '';
58
 
59
    /** @var string Uri fragment. */
60
    private $fragment = '';
61
 
62
    /**
63
     * @param string $uri URI to parse
64
     */
65
    public function __construct($uri = '')
66
    {
67
        // weak type check to also accept null until we can add scalar type hints
68
        if ($uri != '') {
69
            $parts = parse_url($uri);
70
            if ($parts === false) {
71
                throw new \InvalidArgumentException("Unable to parse URI: $uri");
72
            }
73
            $this->applyParts($parts);
74
        }
75
    }
76
 
77
    public function __toString()
78
    {
79
        return self::composeComponents(
80
            $this->scheme,
81
            $this->getAuthority(),
82
            $this->path,
83
            $this->query,
84
            $this->fragment
85
        );
86
    }
87
 
88
    /**
89
     * Composes a URI reference string from its various components.
90
     *
91
     * Usually this method does not need to be called manually but instead is used indirectly via
92
     * `Psr\Http\Message\UriInterface::__toString`.
93
     *
94
     * PSR-7 UriInterface treats an empty component the same as a missing component as
95
     * getQuery(), getFragment() etc. always return a string. This explains the slight
96
     * difference to RFC 3986 Section 5.3.
97
     *
98
     * Another adjustment is that the authority separator is added even when the authority is missing/empty
99
     * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with
100
     * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But
101
     * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to
102
     * that format).
103
     *
104
     * @param string $scheme
105
     * @param string $authority
106
     * @param string $path
107
     * @param string $query
108
     * @param string $fragment
109
     *
110
     * @return string
111
     *
112
     * @link https://tools.ietf.org/html/rfc3986#section-5.3
113
     */
114
    public static function composeComponents($scheme, $authority, $path, $query, $fragment)
115
    {
116
        $uri = '';
117
 
118
        // weak type checks to also accept null until we can add scalar type hints
119
        if ($scheme != '') {
120
            $uri .= $scheme . ':';
121
        }
122
 
123
        if ($authority != ''|| $scheme === 'file') {
124
            $uri .= '//' . $authority;
125
        }
126
 
127
        $uri .= $path;
128
 
129
        if ($query != '') {
130
            $uri .= '?' . $query;
131
        }
132
 
133
        if ($fragment != '') {
134
            $uri .= '#' . $fragment;
135
        }
136
 
137
        return $uri;
138
    }
139
 
140
    /**
141
     * Whether the URI has the default port of the current scheme.
142
     *
143
     * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used
144
     * independently of the implementation.
145
     *
146
     * @param UriInterface $uri
147
     *
148
     * @return bool
149
     */
150
    public static function isDefaultPort(UriInterface $uri)
151
    {
152
        return $uri->getPort() === null
153
            || (isset(self::$defaultPorts[$uri->getScheme()]) && $uri->getPort() === self::$defaultPorts[$uri->getScheme()]);
154
    }
155
 
156
    /**
157
     * Whether the URI is absolute, i.e. it has a scheme.
158
     *
159
     * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true
160
     * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative
161
     * to another URI, the base URI. Relative references can be divided into several forms:
162
     * - network-path references, e.g. '//example.com/path'
163
     * - absolute-path references, e.g. '/path'
164
     * - relative-path references, e.g. 'subpath'
165
     *
166
     * @param UriInterface $uri
167
     *
168
     * @return bool
169
     * @see Uri::isNetworkPathReference
170
     * @see Uri::isAbsolutePathReference
171
     * @see Uri::isRelativePathReference
172
     * @link https://tools.ietf.org/html/rfc3986#section-4
173
     */
174
    public static function isAbsolute(UriInterface $uri)
175
    {
176
        return $uri->getScheme() !== '';
177
    }
178
 
179
    /**
180
     * Whether the URI is a network-path reference.
181
     *
182
     * A relative reference that begins with two slash characters is termed an network-path reference.
183
     *
184
     * @param UriInterface $uri
185
     *
186
     * @return bool
187
     * @link https://tools.ietf.org/html/rfc3986#section-4.2
188
     */
189
    public static function isNetworkPathReference(UriInterface $uri)
190
    {
191
        return $uri->getScheme() === '' && $uri->getAuthority() !== '';
192
    }
193
 
194
    /**
195
     * Whether the URI is a absolute-path reference.
196
     *
197
     * A relative reference that begins with a single slash character is termed an absolute-path reference.
198
     *
199
     * @param UriInterface $uri
200
     *
201
     * @return bool
202
     * @link https://tools.ietf.org/html/rfc3986#section-4.2
203
     */
204
    public static function isAbsolutePathReference(UriInterface $uri)
205
    {
206
        return $uri->getScheme() === ''
207
            && $uri->getAuthority() === ''
208
            && isset($uri->getPath()[0])
209
            && $uri->getPath()[0] === '/';
210
    }
211
 
212
    /**
213
     * Whether the URI is a relative-path reference.
214
     *
215
     * A relative reference that does not begin with a slash character is termed a relative-path reference.
216
     *
217
     * @param UriInterface $uri
218
     *
219
     * @return bool
220
     * @link https://tools.ietf.org/html/rfc3986#section-4.2
221
     */
222
    public static function isRelativePathReference(UriInterface $uri)
223
    {
224
        return $uri->getScheme() === ''
225
            && $uri->getAuthority() === ''
226
            && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/');
227
    }
228
 
229
    /**
230
     * Whether the URI is a same-document reference.
231
     *
232
     * A same-document reference refers to a URI that is, aside from its fragment
233
     * component, identical to the base URI. When no base URI is given, only an empty
234
     * URI reference (apart from its fragment) is considered a same-document reference.
235
     *
236
     * @param UriInterface      $uri  The URI to check
237
     * @param UriInterface|null $base An optional base URI to compare against
238
     *
239
     * @return bool
240
     * @link https://tools.ietf.org/html/rfc3986#section-4.4
241
     */
242
    public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null)
243
    {
244
        if ($base !== null) {
245
            $uri = UriResolver::resolve($base, $uri);
246
 
247
            return ($uri->getScheme() === $base->getScheme())
248
                && ($uri->getAuthority() === $base->getAuthority())
249
                && ($uri->getPath() === $base->getPath())
250
                && ($uri->getQuery() === $base->getQuery());
251
        }
252
 
253
        return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === '';
254
    }
255
 
256
    /**
257
     * Removes dot segments from a path and returns the new path.
258
     *
259
     * @param string $path
260
     *
261
     * @return string
262
     *
263
     * @deprecated since version 1.4. Use UriResolver::removeDotSegments instead.
264
     * @see UriResolver::removeDotSegments
265
     */
266
    public static function removeDotSegments($path)
267
    {
268
        return UriResolver::removeDotSegments($path);
269
    }
270
 
271
    /**
272
     * Converts the relative URI into a new URI that is resolved against the base URI.
273
     *
274
     * @param UriInterface        $base Base URI
275
     * @param string|UriInterface $rel  Relative URI
276
     *
277
     * @return UriInterface
278
     *
279
     * @deprecated since version 1.4. Use UriResolver::resolve instead.
280
     * @see UriResolver::resolve
281
     */
282
    public static function resolve(UriInterface $base, $rel)
283
    {
284
        if (!($rel instanceof UriInterface)) {
285
            $rel = new self($rel);
286
        }
287
 
288
        return UriResolver::resolve($base, $rel);
289
    }
290
 
291
    /**
292
     * Creates a new URI with a specific query string value removed.
293
     *
294
     * Any existing query string values that exactly match the provided key are
295
     * removed.
296
     *
297
     * @param UriInterface $uri URI to use as a base.
298
     * @param string       $key Query string key to remove.
299
     *
300
     * @return UriInterface
301
     */
302
    public static function withoutQueryValue(UriInterface $uri, $key)
303
    {
304
        $current = $uri->getQuery();
305
        if ($current === '') {
306
            return $uri;
307
        }
308
 
309
        $decodedKey = rawurldecode($key);
310
        $result = array_filter(explode('&', $current), function ($part) use ($decodedKey) {
311
            return rawurldecode(explode('=', $part)[0]) !== $decodedKey;
312
        });
313
 
314
        return $uri->withQuery(implode('&', $result));
315
    }
316
 
317
    /**
318
     * Creates a new URI with a specific query string value.
319
     *
320
     * Any existing query string values that exactly match the provided key are
321
     * removed and replaced with the given key value pair.
322
     *
323
     * A value of null will set the query string key without a value, e.g. "key"
324
     * instead of "key=value".
325
     *
326
     * @param UriInterface $uri   URI to use as a base.
327
     * @param string       $key   Key to set.
328
     * @param string|null  $value Value to set
329
     *
330
     * @return UriInterface
331
     */
332
    public static function withQueryValue(UriInterface $uri, $key, $value)
333
    {
334
        $current = $uri->getQuery();
335
 
336
        if ($current === '') {
337
            $result = [];
338
        } else {
339
            $decodedKey = rawurldecode($key);
340
            $result = array_filter(explode('&', $current), function ($part) use ($decodedKey) {
341
                return rawurldecode(explode('=', $part)[0]) !== $decodedKey;
342
            });
343
        }
344
 
345
        // Query string separators ("=", "&") within the key or value need to be encoded
346
        // (while preventing double-encoding) before setting the query string. All other
347
        // chars that need percent-encoding will be encoded by withQuery().
348
        $key = strtr($key, self::$replaceQuery);
349
 
350
        if ($value !== null) {
351
            $result[] = $key . '=' . strtr($value, self::$replaceQuery);
352
        } else {
353
            $result[] = $key;
354
        }
355
 
356
        return $uri->withQuery(implode('&', $result));
357
    }
358
 
359
    /**
360
     * Creates a URI from a hash of `parse_url` components.
361
     *
362
     * @param array $parts
363
     *
364
     * @return UriInterface
365
     * @link http://php.net/manual/en/function.parse-url.php
366
     *
367
     * @throws \InvalidArgumentException If the components do not form a valid URI.
368
     */
369
    public static function fromParts(array $parts)
370
    {
371
        $uri = new self();
372
        $uri->applyParts($parts);
373
        $uri->validateState();
374
 
375
        return $uri;
376
    }
377
 
378
    public function getScheme()
379
    {
380
        return $this->scheme;
381
    }
382
 
383
    public function getAuthority()
384
    {
385
        $authority = $this->host;
386
        if ($this->userInfo !== '') {
387
            $authority = $this->userInfo . '@' . $authority;
388
        }
389
 
390
        if ($this->port !== null) {
391
            $authority .= ':' . $this->port;
392
        }
393
 
394
        return $authority;
395
    }
396
 
397
    public function getUserInfo()
398
    {
399
        return $this->userInfo;
400
    }
401
 
402
    public function getHost()
403
    {
404
        return $this->host;
405
    }
406
 
407
    public function getPort()
408
    {
409
        return $this->port;
410
    }
411
 
412
    public function getPath()
413
    {
414
        return $this->path;
415
    }
416
 
417
    public function getQuery()
418
    {
419
        return $this->query;
420
    }
421
 
422
    public function getFragment()
423
    {
424
        return $this->fragment;
425
    }
426
 
427
    public function withScheme($scheme)
428
    {
429
        $scheme = $this->filterScheme($scheme);
430
 
431
        if ($this->scheme === $scheme) {
432
            return $this;
433
        }
434
 
435
        $new = clone $this;
436
        $new->scheme = $scheme;
437
        $new->removeDefaultPort();
438
        $new->validateState();
439
 
440
        return $new;
441
    }
442
 
443
    public function withUserInfo($user, $password = null)
444
    {
445
        $info = $user;
446
        if ($password != '') {
447
            $info .= ':' . $password;
448
        }
449
 
450
        if ($this->userInfo === $info) {
451
            return $this;
452
        }
453
 
454
        $new = clone $this;
455
        $new->userInfo = $info;
456
        $new->validateState();
457
 
458
        return $new;
459
    }
460
 
461
    public function withHost($host)
462
    {
463
        $host = $this->filterHost($host);
464
 
465
        if ($this->host === $host) {
466
            return $this;
467
        }
468
 
469
        $new = clone $this;
470
        $new->host = $host;
471
        $new->validateState();
472
 
473
        return $new;
474
    }
475
 
476
    public function withPort($port)
477
    {
478
        $port = $this->filterPort($port);
479
 
480
        if ($this->port === $port) {
481
            return $this;
482
        }
483
 
484
        $new = clone $this;
485
        $new->port = $port;
486
        $new->removeDefaultPort();
487
        $new->validateState();
488
 
489
        return $new;
490
    }
491
 
492
    public function withPath($path)
493
    {
494
        $path = $this->filterPath($path);
495
 
496
        if ($this->path === $path) {
497
            return $this;
498
        }
499
 
500
        $new = clone $this;
501
        $new->path = $path;
502
        $new->validateState();
503
 
504
        return $new;
505
    }
506
 
507
    public function withQuery($query)
508
    {
509
        $query = $this->filterQueryAndFragment($query);
510
 
511
        if ($this->query === $query) {
512
            return $this;
513
        }
514
 
515
        $new = clone $this;
516
        $new->query = $query;
517
 
518
        return $new;
519
    }
520
 
521
    public function withFragment($fragment)
522
    {
523
        $fragment = $this->filterQueryAndFragment($fragment);
524
 
525
        if ($this->fragment === $fragment) {
526
            return $this;
527
        }
528
 
529
        $new = clone $this;
530
        $new->fragment = $fragment;
531
 
532
        return $new;
533
    }
534
 
535
    /**
536
     * Apply parse_url parts to a URI.
537
     *
538
     * @param array $parts Array of parse_url parts to apply.
539
     */
540
    private function applyParts(array $parts)
541
    {
542
        $this->scheme = isset($parts['scheme'])
543
            ? $this->filterScheme($parts['scheme'])
544
            : '';
545
        $this->userInfo = isset($parts['user']) ? $parts['user'] : '';
546
        $this->host = isset($parts['host'])
547
            ? $this->filterHost($parts['host'])
548
            : '';
549
        $this->port = isset($parts['port'])
550
            ? $this->filterPort($parts['port'])
551
            : null;
552
        $this->path = isset($parts['path'])
553
            ? $this->filterPath($parts['path'])
554
            : '';
555
        $this->query = isset($parts['query'])
556
            ? $this->filterQueryAndFragment($parts['query'])
557
            : '';
558
        $this->fragment = isset($parts['fragment'])
559
            ? $this->filterQueryAndFragment($parts['fragment'])
560
            : '';
561
        if (isset($parts['pass'])) {
562
            $this->userInfo .= ':' . $parts['pass'];
563
        }
564
 
565
        $this->removeDefaultPort();
566
    }
567
 
568
    /**
569
     * @param string $scheme
570
     *
571
     * @return string
572
     *
573
     * @throws \InvalidArgumentException If the scheme is invalid.
574
     */
575
    private function filterScheme($scheme)
576
    {
577
        if (!is_string($scheme)) {
578
            throw new \InvalidArgumentException('Scheme must be a string');
579
        }
580
 
581
        return strtolower($scheme);
582
    }
583
 
584
    /**
585
     * @param string $host
586
     *
587
     * @return string
588
     *
589
     * @throws \InvalidArgumentException If the host is invalid.
590
     */
591
    private function filterHost($host)
592
    {
593
        if (!is_string($host)) {
594
            throw new \InvalidArgumentException('Host must be a string');
595
        }
596
 
597
        return strtolower($host);
598
    }
599
 
600
    /**
601
     * @param int|null $port
602
     *
603
     * @return int|null
604
     *
605
     * @throws \InvalidArgumentException If the port is invalid.
606
     */
607
    private function filterPort($port)
608
    {
609
        if ($port === null) {
610
            return null;
611
        }
612
 
613
        $port = (int) $port;
614
        if (1 > $port || 0xffff < $port) {
615
            throw new \InvalidArgumentException(
616
                sprintf('Invalid port: %d. Must be between 1 and 65535', $port)
617
            );
618
        }
619
 
620
        return $port;
621
    }
622
 
623
    private function removeDefaultPort()
624
    {
625
        if ($this->port !== null && self::isDefaultPort($this)) {
626
            $this->port = null;
627
        }
628
    }
629
 
630
    /**
631
     * Filters the path of a URI
632
     *
633
     * @param string $path
634
     *
635
     * @return string
636
     *
637
     * @throws \InvalidArgumentException If the path is invalid.
638
     */
639
    private function filterPath($path)
640
    {
641
        if (!is_string($path)) {
642
            throw new \InvalidArgumentException('Path must be a string');
643
        }
644
 
645
        return preg_replace_callback(
646
            '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
647
            [$this, 'rawurlencodeMatchZero'],
648
            $path
649
        );
650
    }
651
 
652
    /**
653
     * Filters the query string or fragment of a URI.
654
     *
655
     * @param string $str
656
     *
657
     * @return string
658
     *
659
     * @throws \InvalidArgumentException If the query or fragment is invalid.
660
     */
661
    private function filterQueryAndFragment($str)
662
    {
663
        if (!is_string($str)) {
664
            throw new \InvalidArgumentException('Query and fragment must be a string');
665
        }
666
 
667
        return preg_replace_callback(
668
            '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
669
            [$this, 'rawurlencodeMatchZero'],
670
            $str
671
        );
672
    }
673
 
674
    private function rawurlencodeMatchZero(array $match)
675
    {
676
        return rawurlencode($match[0]);
677
    }
678
 
679
    private function validateState()
680
    {
681
        if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) {
682
            $this->host = self::HTTP_DEFAULT_HOST;
683
        }
684
 
685
        if ($this->getAuthority() === '') {
686
            if (0 === strpos($this->path, '//')) {
687
                throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"');
688
            }
689
            if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) {
690
                throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon');
691
            }
692
        } elseif (isset($this->path[0]) && $this->path[0] !== '/') {
693
            @trigger_error(
694
                'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' .
695
                'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.',
696
                E_USER_DEPRECATED
697
            );
698
            $this->path = '/'. $this->path;
699
            //throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty');
700
        }
701
    }
702
}