Completed
Pull Request — master (#1867)
by Bloody
02:15
created

Uri::removeDotSegments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
        $result = self::getFilteredQueryString($uri, [$key]);
305
306
        return $uri->withQuery(implode('&', $result));
307
    }
308
309
    /**
310
     * Creates a new URI with a specific query string value.
311
     *
312
     * Any existing query string values that exactly match the provided key are
313
     * removed and replaced with the given key value pair.
314
     *
315
     * A value of null will set the query string key without a value, e.g. "key"
316
     * instead of "key=value".
317
     *
318
     * @param UriInterface $uri   URI to use as a base.
319
     * @param string       $key   Key to set.
320
     * @param string|null  $value Value to set
321
     *
322
     * @return UriInterface
323
     */
324
    public static function withQueryValue(UriInterface $uri, $key, $value)
325
    {
326
        $result = self::getFilteredQueryString($uri, [$key]);
327
328
        $result[] = self::generateQueryString($key, $value);
329
330
        return $uri->withQuery(implode('&', $result));
331
    }
332
333
    /**
334
     * Creates a new URI with multiple specific query string values.
335
     *
336
     * It has the same behavior as withQueryValue() but for an associative array of key => value.
337
     *
338
     * @param UriInterface $uri           URI to use as a base.
339
     * @param array        $keyValueArray Associative array of key and values
340
     *
341
     * @return UriInterface
342
     */
343
    public static function withQueryValues(UriInterface $uri, array $keyValueArray)
344
    {
345
        $result = self::getFilteredQueryString($uri, array_keys($keyValueArray));
346
347
        foreach ($keyValueArray as $key => $value) {
348
            $result[] = self::generateQueryString($key, $value);
349
        }
350
351
        return $uri->withQuery(implode('&', $result));
352
    }
353
354
    /**
355
     * Creates a URI from a hash of `parse_url` components.
356
     *
357
     * @param array $parts
358
     *
359
     * @return UriInterface
360
     * @link http://php.net/manual/en/function.parse-url.php
361
     *
362
     * @throws \InvalidArgumentException If the components do not form a valid URI.
363
     */
364
    public static function fromParts(array $parts)
365
    {
366
        $uri = new self();
367
        $uri->applyParts($parts);
368
        $uri->validateState();
369
370
        return $uri;
371
    }
372
373
    public function getScheme()
374
    {
375
        return $this->scheme;
376
    }
377
378
    public function getAuthority()
379
    {
380
        $authority = $this->host;
381
        if ($this->userInfo !== '') {
382
            $authority = $this->userInfo . '@' . $authority;
383
        }
384
385
        if ($this->port !== null) {
386
            $authority .= ':' . $this->port;
387
        }
388
389
        return $authority;
390
    }
391
392
    public function getUserInfo()
393
    {
394
        return $this->userInfo;
395
    }
396
397
    public function getHost()
398
    {
399
        return $this->host;
400
    }
401
402
    public function getPort()
403
    {
404
        return $this->port;
405
    }
406
407
    public function getPath()
408
    {
409
        return $this->path;
410
    }
411
412
    public function getQuery()
413
    {
414
        return $this->query;
415
    }
416
417
    public function getFragment()
418
    {
419
        return $this->fragment;
420
    }
421
422
    public function withScheme($scheme)
423
    {
424
        $scheme = $this->filterScheme($scheme);
425
426
        if ($this->scheme === $scheme) {
427
            return $this;
428
        }
429
430
        $new = clone $this;
431
        $new->scheme = $scheme;
432
        $new->removeDefaultPort();
433
        $new->validateState();
434
435
        return $new;
436
    }
437
438
    public function withUserInfo($user, $password = null)
439
    {
440
        $info = $user;
441
        if ($password != '') {
442
            $info .= ':' . $password;
443
        }
444
445
        if ($this->userInfo === $info) {
446
            return $this;
447
        }
448
449
        $new = clone $this;
450
        $new->userInfo = $info;
451
        $new->validateState();
452
453
        return $new;
454
    }
455
456
    public function withHost($host)
457
    {
458
        $host = $this->filterHost($host);
459
460
        if ($this->host === $host) {
461
            return $this;
462
        }
463
464
        $new = clone $this;
465
        $new->host = $host;
466
        $new->validateState();
467
468
        return $new;
469
    }
470
471
    public function withPort($port)
472
    {
473
        $port = $this->filterPort($port);
474
475
        if ($this->port === $port) {
476
            return $this;
477
        }
478
479
        $new = clone $this;
480
        $new->port = $port;
481
        $new->removeDefaultPort();
482
        $new->validateState();
483
484
        return $new;
485
    }
486
487
    public function withPath($path)
488
    {
489
        $path = $this->filterPath($path);
490
491
        if ($this->path === $path) {
492
            return $this;
493
        }
494
495
        $new = clone $this;
496
        $new->path = $path;
497
        $new->validateState();
498
499
        return $new;
500
    }
501
502
    public function withQuery($query)
503
    {
504
        $query = $this->filterQueryAndFragment($query);
505
506
        if ($this->query === $query) {
507
            return $this;
508
        }
509
510
        $new = clone $this;
511
        $new->query = $query;
512
513
        return $new;
514
    }
515
516
    public function withFragment($fragment)
517
    {
518
        $fragment = $this->filterQueryAndFragment($fragment);
519
520
        if ($this->fragment === $fragment) {
521
            return $this;
522
        }
523
524
        $new = clone $this;
525
        $new->fragment = $fragment;
526
527
        return $new;
528
    }
529
530
    /**
531
     * Apply parse_url parts to a URI.
532
     *
533
     * @param array $parts Array of parse_url parts to apply.
534
     */
535
    private function applyParts(array $parts)
536
    {
537
        $this->scheme = isset($parts['scheme'])
538
            ? $this->filterScheme($parts['scheme'])
539
            : '';
540
        $this->userInfo = isset($parts['user']) ? $parts['user'] : '';
541
        $this->host = isset($parts['host'])
542
            ? $this->filterHost($parts['host'])
543
            : '';
544
        $this->port = isset($parts['port'])
545
            ? $this->filterPort($parts['port'])
546
            : null;
547
        $this->path = isset($parts['path'])
548
            ? $this->filterPath($parts['path'])
549
            : '';
550
        $this->query = isset($parts['query'])
551
            ? $this->filterQueryAndFragment($parts['query'])
552
            : '';
553
        $this->fragment = isset($parts['fragment'])
554
            ? $this->filterQueryAndFragment($parts['fragment'])
555
            : '';
556
        if (isset($parts['pass'])) {
557
            $this->userInfo .= ':' . $parts['pass'];
558
        }
559
560
        $this->removeDefaultPort();
561
    }
562
563
    /**
564
     * @param string $scheme
565
     *
566
     * @return string
567
     *
568
     * @throws \InvalidArgumentException If the scheme is invalid.
569
     */
570
    private function filterScheme($scheme)
571
    {
572
        if (!is_string($scheme)) {
573
            throw new \InvalidArgumentException('Scheme must be a string');
574
        }
575
576
        return strtolower($scheme);
577
    }
578
579
    /**
580
     * @param string $host
581
     *
582
     * @return string
583
     *
584
     * @throws \InvalidArgumentException If the host is invalid.
585
     */
586
    private function filterHost($host)
587
    {
588
        if (!is_string($host)) {
589
            throw new \InvalidArgumentException('Host must be a string');
590
        }
591
592
        return strtolower($host);
593
    }
594
595
    /**
596
     * @param int|null $port
597
     *
598
     * @return int|null
599
     *
600
     * @throws \InvalidArgumentException If the port is invalid.
601
     */
602
    private function filterPort($port)
603
    {
604
        if ($port === null) {
605
            return null;
606
        }
607
608
        $port = (int) $port;
609
        if (1 > $port || 0xffff < $port) {
610
            throw new \InvalidArgumentException(
611
                sprintf('Invalid port: %d. Must be between 1 and 65535', $port)
612
            );
613
        }
614
615
        return $port;
616
    }
617
618
    /**
619
     * @param UriInterface $uri
620
     * @param array        $keys
621
     * 
622
     * @return array
623
     */
624
    private static function getFilteredQueryString(UriInterface $uri, array $keys)
625
    {
626
        $current = $uri->getQuery();
627
628
        if ($current === '') {
629
            return [];
630
        }
631
632
        $decodedKeys = array_map('rawurldecode', $keys);
633
634
        return array_filter(explode('&', $current), function ($part) use ($decodedKeys) {
635
            return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true);
636
        });
637
    }
638
639
    /**
640
     * @param string      $key
641
     * @param string|null $value
642
     * 
643
     * @return string
644
     */
645
    private static function generateQueryString($key, $value)
646
    {
647
        // Query string separators ("=", "&") within the key or value need to be encoded
648
        // (while preventing double-encoding) before setting the query string. All other
649
        // chars that need percent-encoding will be encoded by withQuery().
650
        $queryString = strtr($key, self::$replaceQuery);
651
652
        if ($value !== null) {
653
            $queryString .= '=' . strtr($value, self::$replaceQuery);
654
        }
655
656
        return $queryString;
657
    }
658
659
    private function removeDefaultPort()
660
    {
661
        if ($this->port !== null && self::isDefaultPort($this)) {
662
            $this->port = null;
663
        }
664
    }
665
666
    /**
667
     * Filters the path of a URI
668
     *
669
     * @param string $path
670
     *
671
     * @return string
672
     *
673
     * @throws \InvalidArgumentException If the path is invalid.
674
     */
675 View Code Duplication
    private function filterPath($path)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
676
    {
677
        if (!is_string($path)) {
678
            throw new \InvalidArgumentException('Path must be a string');
679
        }
680
681
        return preg_replace_callback(
682
            '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/',
683
            [$this, 'rawurlencodeMatchZero'],
684
            $path
685
        );
686
    }
687
688
    /**
689
     * Filters the query string or fragment of a URI.
690
     *
691
     * @param string $str
692
     *
693
     * @return string
694
     *
695
     * @throws \InvalidArgumentException If the query or fragment is invalid.
696
     */
697 View Code Duplication
    private function filterQueryAndFragment($str)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
698
    {
699
        if (!is_string($str)) {
700
            throw new \InvalidArgumentException('Query and fragment must be a string');
701
        }
702
703
        return preg_replace_callback(
704
            '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/',
705
            [$this, 'rawurlencodeMatchZero'],
706
            $str
707
        );
708
    }
709
710
    private function rawurlencodeMatchZero(array $match)
711
    {
712
        return rawurlencode($match[0]);
713
    }
714
715
    private function validateState()
716
    {
717
        if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) {
718
            $this->host = self::HTTP_DEFAULT_HOST;
719
        }
720
721
        if ($this->getAuthority() === '') {
722
            if (0 === strpos($this->path, '//')) {
723
                throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"');
724
            }
725
            if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) {
726
                throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon');
727
            }
728
        } elseif (isset($this->path[0]) && $this->path[0] !== '/') {
729
            @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
730
                'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' .
731
                'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.',
732
                E_USER_DEPRECATED
733
            );
734
            $this->path = '/'. $this->path;
735
            //throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty');
736
        }
737
    }
738
}
739