Passed
Push — master ( 157485...b7615a )
by Nikolaos
09:23
created

Uri::checkValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 10
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 3
crap 6
1
<?php
2
3
/**
4
 * This file is part of the Phalcon Framework.
5
 *
6
 * (c) Phalcon Team <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE.txt
9
 * file that was distributed with this source code.
10
 *
11
 * Implementation of this file has been influenced by Zend Diactoros
12
 * @link    https://github.com/zendframework/zend-diactoros
13
 * @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md
14
 */
15
16
declare(strict_types=1);
17
18
namespace Phalcon\Http\Message;
19
20
use Phalcon\Helper\Arr;
21
use Phalcon\Helper\Str;
22
use Phalcon\Http\Message\Exception\InvalidArgumentException;
23
use Psr\Http\Message\UriInterface;
24
25
use function array_keys;
26
use function explode;
27
use function implode;
28
use function ltrim;
29
use function parse_url;
30
use function preg_replace;
31
use function rawurlencode;
32
use function strpos;
33
use function strtolower;
34
35
/**
36
 * PSR-7 Uri
37
 *
38
 * @property string   $fragment
39
 * @property string   $host
40
 * @property string   $pass
41
 * @property string   $path
42
 * @property int|null $port
43
 * @property string   $query
44
 * @property string   $scheme
45
 * @property string   $user
46
 */
47
final class Uri extends AbstractCommon implements UriInterface
48
{
49
    /**
50
     * Returns the fragment of the URL
51
     *
52
     * @return string
53
     */
54
    protected $fragment = "";
55
56
    /**
57
     * Retrieve the host component of the URI.
58
     *
59
     * If no host is present, this method MUST return an empty string.
60
     *
61
     * The value returned MUST be normalized to lowercase, per RFC 3986
62
     * Section 3.2.2.
63
     *
64
     * @see http://tools.ietf.org/html/rfc3986#section-3.2.2
65
     *
66
     * @return string
67
     */
68
    protected $host = "";
69
70
    /**
71
     * @var string
72
     */
73
    protected $pass = "";
74
75
    /**
76
     * Returns the path of the URL
77
     *
78
     * @return string
79
     */
80
    protected $path = "";
81
82
    /**
83
     * Retrieve the port component of the URI.
84
     *
85
     * If a port is present, and it is non-standard for the current scheme,
86
     * this method MUST return it as an integer. If the port is the standard
87
     * port used with the current scheme, this method SHOULD return null.
88
     *
89
     * If no port is present, and no scheme is present, this method MUST return
90
     * a null value.
91
     *
92
     * If no port is present, but a scheme is present, this method MAY return
93
     * the standard port for that scheme, but SHOULD return null.
94
     *
95
     * @return int|null
96
     */
97
    protected $port = null;
98
99
    /**
100
     * Returns the query of the URL
101
     *
102
     * @return string
103
     */
104
    protected $query = "";
105
106
    /**
107
     * Retrieve the scheme component of the URI.
108
     *
109
     * If no scheme is present, this method MUST return an empty string.
110
     *
111
     * The value returned MUST be normalized to lowercase, per RFC 3986
112
     * Section 3.1.
113
     *
114
     * The trailing ":" character is not part of the scheme and MUST NOT be
115
     * added.
116
     *
117
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
118
     *
119
     * @return string
120
     */
121
    protected $scheme = "https";
122
123
    /**
124
     * @var string
125
     */
126
    protected $user = "";
127
128
    /**
129
     * Uri constructor.
130
     *
131
     * @param string $uri
132
     */
133
    public function __construct(string $uri = '')
134
    {
135
        if ('' !== $uri) {
136
            $urlParts = parse_url($uri);
137
138
            if (false === $urlParts) {
139
                $urlParts = [];
140
            }
141
142
            $this->fragment = $this->filterFragment(
143
                Arr::get($urlParts, 'fragment', '')
0 ignored issues
show
Bug introduced by
It seems like Phalcon\Helper\Arr::get(...lParts, 'fragment', '') can also be of type null; however, parameter $fragment of Phalcon\Http\Message\Uri::filterFragment() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

143
                /** @scrutinizer ignore-type */ Arr::get($urlParts, 'fragment', '')
Loading history...
144
            );
145
            $this->host     = strtolower(
146
                Arr::get($urlParts, 'host', '')
147
            );
148
            $this->pass     = rawurlencode(
149
                Arr::get($urlParts, 'pass', '')
150
            );
151
            $this->path     = $this->filterPath(
152
                Arr::get($urlParts, 'path', '')
0 ignored issues
show
Bug introduced by
It seems like Phalcon\Helper\Arr::get($urlParts, 'path', '') can also be of type null; however, parameter $path of Phalcon\Http\Message\Uri::filterPath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

152
                /** @scrutinizer ignore-type */ Arr::get($urlParts, 'path', '')
Loading history...
153
            );
154
            $this->port     = $this->filterPort(
155
                Arr::get($urlParts, 'port', null)
156
            );
157
            $this->query    = $this->filterQuery(
158
                Arr::get($urlParts, 'query', '')
0 ignored issues
show
Bug introduced by
It seems like Phalcon\Helper\Arr::get($urlParts, 'query', '') can also be of type null; however, parameter $query of Phalcon\Http\Message\Uri::filterQuery() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
                /** @scrutinizer ignore-type */ Arr::get($urlParts, 'query', '')
Loading history...
159
            );
160
            $this->scheme   = $this->filterScheme(
161
                Arr::get($urlParts, 'scheme', '')
0 ignored issues
show
Bug introduced by
It seems like Phalcon\Helper\Arr::get($urlParts, 'scheme', '') can also be of type null; however, parameter $scheme of Phalcon\Http\Message\Uri::filterScheme() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

161
                /** @scrutinizer ignore-type */ Arr::get($urlParts, 'scheme', '')
Loading history...
162
            );
163
            $this->user     = rawurlencode(
164
                Arr::get($urlParts, 'user', '')
165
            );
166
        }
167
    }
168
169
    /**
170
     * Return the string representation as a URI reference.
171
     *
172
     * Depending on which components of the URI are present, the resulting
173
     * string is either a full URI or relative reference according to RFC 3986,
174
     * Section 4.1. The method concatenates the various components of the URI,
175
     * using the appropriate delimiters
176
     *
177
     * @return string
178
     */
179
    public function __toString(): string
180
    {
181
        $authority = $this->getAuthority();
182
        $path      = $this->path;
183
184
        /**
185
         * The path can be concatenated without delimiters. But there are two
186
         * cases where the path has to be adjusted to make the URI reference
187
         * valid as PHP does not allow to throw an exception in __toString():
188
         *   - If the path is rootless and an authority is present, the path
189
         *     MUST be prefixed by "/".
190
         *   - If the path is starting with more than one "/" and no authority
191
         *     is present, the starting slashes MUST be reduced to one.
192
         */
193
        if (
194
            "" !== $path &&
195
            true !== Str::startsWith($path, "/") &&
196
            "" !== $authority
197
        ) {
198
            $path = "/" . $path;
199
        }
200
201
        return $this->checkValue($this->scheme, "", ":")
202
            . $this->checkValue($authority, "//")
203
            . $path
204
            . $this->checkValue($this->query, "?")
205
            . $this->checkValue($this->fragment, "#");
206
    }
207
208
    /**
209
     * Retrieve the authority component of the URI.
210
     *
211
     * @return string
212
     */
213
    public function getAuthority(): string
214
    {
215
        /**
216
         * If no authority information is present, this method MUST return an
217
         * empty string.
218
         */
219
        if ("" === $this->host) {
220
            return "";
221
        }
222
223
        $authority = $this->host;
224
        $userInfo  = $this->getUserInfo();
225
226
        /**
227
         * The authority syntax of the URI is:
228
         *
229
         * [user-info@]host[:port]
230
         */
231
        if ("" !== $userInfo) {
232
            $authority = $userInfo . "@" . $authority;
233
        }
234
235
        /**
236
         * If the port component is not set or is the standard port for the
237
         * current scheme, it SHOULD NOT be included.
238
         */
239
        if (null !== $this->port) {
240
            $authority .= ":" . $this->port;
241
        }
242
243
        return $authority;
244
    }
245
246
    /**
247
     * Retrieve the user information component of the URI.
248
     *
249
     * If no user information is present, this method MUST return an empty
250
     * string.
251
     *
252
     * If a user is present in the URI, this will return that value;
253
     * additionally, if the password is also present, it will be appended to the
254
     * user value, with a colon (":") separating the values.
255
     *
256
     * The trailing "@" character is not part of the user information and MUST
257
     * NOT be added.
258
     *
259
     * @return string The URI user information, in "username[:password]" format.
260
     */
261
    public function getUserInfo(): string
262
    {
263
        if (true !== empty($this->pass)) {
264
            return $this->user . ":" . $this->pass;
265
        }
266
267
        return $this->user;
268
    }
269
270
    /**
271
     * Return an instance with the specified URI fragment.
272
     *
273
     * This method MUST retain the state of the current instance, and return
274
     * an instance that contains the specified URI fragment.
275
     *
276
     * Users can provide both encoded and decoded fragment characters.
277
     * Implementations ensure the correct encoding as outlined in getFragment().
278
     *
279
     * An empty fragment value is equivalent to removing the fragment.
280
     *
281
     * @param string $fragment
282
     *
283
     * @return Uri
284
     */
285
    public function withFragment($fragment): Uri
286
    {
287
        $this->checkStringParameter($fragment);
288
289
        $fragment = $this->filterFragment($fragment);
290
291
        return $this->cloneInstance($fragment, "fragment");
292
    }
293
294
    /**
295
     * Return an instance with the specified path.
296
     *
297
     * This method MUST retain the state of the current instance, and return
298
     * an instance that contains the specified path.
299
     *
300
     * The path can either be empty or absolute (starting with a slash) or
301
     * rootless (not starting with a slash). Implementations MUST support all
302
     * three syntaxes.
303
     *
304
     * If an HTTP path is intended to be host-relative rather than path-relative
305
     * then it must begin with a slash ("/"). HTTP paths not starting with a
306
     * slash are assumed to be relative to some base path known to the
307
     * application or consumer.
308
     *
309
     * Users can provide both encoded and decoded path characters.
310
     * Implementations ensure the correct encoding as outlined in getPath().
311
     *
312
     * @param string $path
313
     *
314
     * @return Uri
315
     * @throws InvalidArgumentException for invalid paths.
316
     */
317
    public function withPath($path): Uri
318
    {
319
        $this->checkStringParameter($path);
320
321
        if (
322
            false !== strpos($path, "?") ||
323
            false !== strpos($path, "#")
324
        ) {
325
            throw new InvalidArgumentException(
326
                "Path cannot contain a query string or fragment"
327
            );
328
        }
329
330
        $path = $this->filterPath($path);
331
332
        return $this->cloneInstance($path, "path");
333
    }
334
335
    /**
336
     * Return an instance with the specified port.
337
     *
338
     * This method MUST retain the state of the current instance, and return
339
     * an instance that contains the specified port.
340
     *
341
     * Implementations MUST raise an exception for ports outside the
342
     * established TCP and UDP port ranges.
343
     *
344
     * A null value provided for the port is equivalent to removing the port
345
     * information.
346
     *
347
     * @param int|null $port
348
     *
349
     * @return Uri
350
     * @throws InvalidArgumentException for invalid ports.
351
     */
352
    public function withPort($port): Uri
353
    {
354
        if (null !== $port) {
355
            $port = $this->filterPort($port);
356
357
            if (null !== $port && ($port < 1 || $port > 65535)) {
358
                throw new InvalidArgumentException(
359
                    "Method expects valid port (1-65535)"
360
                );
361
            }
362
        }
363
364
        return $this->cloneInstance($port, "port");
365
    }
366
367
    /**
368
     * Return an instance with the specified query string.
369
     *
370
     * This method MUST retain the state of the current instance, and return
371
     * an instance that contains the specified query string.
372
     *
373
     * Users can provide both encoded and decoded query characters.
374
     * Implementations ensure the correct encoding as outlined in getQuery().
375
     *
376
     * An empty query string value is equivalent to removing the query string.
377
     *
378
     * @param string $query
379
     *
380
     * @return Uri
381
     * @throws InvalidArgumentException for invalid query strings.
382
     */
383
    public function withQuery($query): Uri
384
    {
385
        $this->checkStringParameter($query);
386
387
        if (false !== strpos($query, "#")) {
388
            throw new InvalidArgumentException(
389
                "Query cannot contain a query fragment"
390
            );
391
        }
392
393
        $query = $this->filterQuery($query);
394
395
        return $this->cloneInstance($query, "query");
396
    }
397
398
    /**
399
     * Return an instance with the specified scheme.
400
     *
401
     * This method MUST retain the state of the current instance, and return
402
     * an instance that contains the specified scheme.
403
     *
404
     * Implementations MUST support the schemes "http" and "https" case
405
     * insensitively, and MAY accommodate other schemes if required.
406
     *
407
     * An empty scheme is equivalent to removing the scheme.
408
     *
409
     * @param string $scheme
410
     *
411
     * @return Uri
412
     * @throws InvalidArgumentException for invalid schemes.
413
     * @throws InvalidArgumentException for unsupported schemes.
414
     */
415
    public function withScheme($scheme): Uri
416
    {
417
        $this->checkStringParameter($scheme);
418
419
        $scheme = $this->filterScheme($scheme);
420
421
        return $this->processWith($scheme, "scheme");
422
    }
423
424
    /**
425
     * Return an instance with the specified user information.
426
     *
427
     * @param string      $user
428
     * @param string|null $password
429
     *
430
     * @return Uri
431
     */
432
    public function withUserInfo($user, $password = null): Uri
433
    {
434
        $this->checkStringParameter($user);
435
436
        if (null !== $password) {
437
            $this->checkStringParameter($user);
438
        }
439
440
        $user = rawurlencode($user);
441
442
        if (null !== $password) {
443
            $password = rawurlencode($password);
444
        }
445
446
        /**
447
         * Immutable - need to send a new object back
448
         */
449
        $newInstance       = $this->cloneInstance($user, "user");
450
        $newInstance->pass = $password;
451
452
        return $newInstance;
453
    }
454
455
    /**
456
     * Return an instance with the specified host.
457
     *
458
     * This method MUST retain the state of the current instance, and return
459
     * an instance that contains the specified host.
460
     *
461
     * An empty host value is equivalent to removing the host.
462
     *
463
     * @param string $host
464
     *
465
     * @return Uri
466
     * @throws InvalidArgumentException for invalid hostnames.
467
     *
468
     */
469
    public function withHost($host): Uri
470
    {
471
        return $this->processWith($host, "host");
472
    }
473
474
    /**
475
     * @return string
476
     */
477
    public function getFragment(): string
478
    {
479
        return $this->fragment;
480
    }
481
482
    /**
483
     * @return string
484
     */
485
    public function getHost()
486
    {
487
        return $this->host;
488
    }
489
490
    /**
491
     * @return string
492
     */
493
    public function getPath()
494
    {
495
        return $this->path;
496
    }
497
498
    /**
499
     * @return int|null
500
     */
501
    public function getPort()
502
    {
503
        return $this->port;
504
    }
505
506
    public function getQuery()
507
    {
508
        return $this->query;
509
    }
510
511
    public function getScheme()
512
    {
513
        return $this->scheme;
514
    }
515
516
    /**
517
     * If the value passed is empty it returns it prefixed and suffixed with
518
     * the passed parameters
519
     *
520
     * @param string $value
521
     * @param string $prefix
522
     * @param string $suffix
523
     *
524
     * @return string
525
     */
526
    private function checkValue(
527
        string $value,
528
        string $prefix = "",
529
        string $suffix = ""
530
    ): string {
531
        if ("" !== $value) {
532
            $value = $prefix . $value . $suffix;
533
        }
534
535
        return $value;
536
    }
537
538
    /**
539
     * If no fragment is present, this method MUST return an empty string.
540
     *
541
     * The leading "#" character is not part of the fragment and MUST NOT be
542
     * added.
543
     *
544
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
545
     * any characters. To determine what characters to encode, please refer to
546
     * RFC 3986, Sections 2 and 3.5.
547
     *
548
     * @see https://tools.ietf.org/html/rfc3986#section-2
549
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
550
     *
551
     * @param string $fragment
552
     *
553
     * @return string
554
     */
555
    private function filterFragment(string $fragment): string
556
    {
557
        return rawurlencode($fragment);
558
    }
559
560
    /**
561
     *
562
     * The path can either be empty or absolute (starting with a slash) or
563
     * rootless (not starting with a slash). Implementations MUST support all
564
     * three syntaxes.
565
     *
566
     * Normally, the empty path "" and absolute path "/" are considered equal as
567
     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
568
     * do this normalization because in contexts with a trimmed base path, e.g.
569
     * the front controller, this difference becomes significant. It's the task
570
     * of the user to handle both "" and "/".
571
     *
572
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
573
     * any characters. To determine what characters to encode, please refer to
574
     * RFC 3986, Sections 2 and 3.3.
575
     *
576
     * As an example, if the value should include a slash ("/") not intended as
577
     * delimiter between path segments, that value MUST be passed in encoded
578
     * form (e.g., "%2F") to the instance.
579
     *
580
     * @see https://tools.ietf.org/html/rfc3986#section-2
581
     * @see https://tools.ietf.org/html/rfc3986#section-3.3
582
     *
583
     * @param string $path
584
     *
585
     * @return string The URI path.
586
     */
587
    private function filterPath(string $path): string
588
    {
589
        if ("" === $path || true !== Str::startsWith($path, "/")) {
590
            return $path;
591
        }
592
593
        $parts = explode("/", $path);
594
        foreach ($parts as $key => $element) {
595
            $parts[$key] = rawurlencode($element);
596
        }
597
598
        $path = implode("/", $parts);
599
600
        return "/" . ltrim($path, "/");
601
    }
602
603
    /**
604
     * Checks the port. If it is a standard one (80,443) then it returns null
605
     *
606
     * @param int|null $port
607
     *
608
     * @return int|null
609
     */
610
    private function filterPort($port): ?int
611
    {
612
        $ports = [
613
            80  => 1,
614
            443 => 1
615
        ];
616
617
        if (null !== $port) {
618
            $port = (int) $port;
619
            if (isset($ports[$port])) {
620
                $port = null;
621
            }
622
        }
623
624
        return $port;
625
    }
626
627
    /**
628
     * If no query string is present, this method MUST return an empty string.
629
     *
630
     * The leading "?" character is not part of the query and MUST NOT be
631
     * added.
632
     *
633
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
634
     * any characters. To determine what characters to encode, please refer to
635
     * RFC 3986, Sections 2 and 3.4.
636
     *
637
     * As an example, if a value in a key/value pair of the query string should
638
     * include an ampersand ("&") not intended as a delimiter between values,
639
     * that value MUST be passed in encoded form (e.g., "%26") to the instance.
640
     *
641
     * @see https://tools.ietf.org/html/rfc3986#section-2
642
     * @see https://tools.ietf.org/html/rfc3986#section-3.4
643
     *
644
     * @param string $query
645
     *
646
     * @return string The URI query string.
647
     */
648
    private function filterQuery(string $query): string
649
    {
650
        if ("" === $query) {
651
            return "";
652
        }
653
654
        $query = ltrim($query, "?");
655
        $parts = explode("&", $query);
656
657
        foreach ($parts as $index => $part) {
658
            $split = $this->splitQueryValue($part);
659
            if (null === $split[1]) {
660
                $parts[$index] = rawurlencode($split[0]);
661
                continue;
662
            }
663
664
            $parts[$index] = rawurlencode($split[0]) . "=" . rawurlencode($split[1]);
665
        }
666
667
        return implode("&", $parts);
668
    }
669
670
    /**
671
     * Filters the passed scheme - only allowed schemes
672
     *
673
     * @param string $scheme
674
     *
675
     * @return string
676
     */
677
    private function filterScheme(string $scheme): string
678
    {
679
        $filtered = preg_replace("#:(//)?$#", "", mb_strtolower($scheme));
680
        $schemes  = [
681
            "http"  => 1,
682
            "https" => 1
683
        ];
684
685
        if ("" === $filtered) {
686
            return "";
687
        }
688
689
        if (!isset($schemes[$filtered])) {
690
            throw new InvalidArgumentException(
691
                "Unsupported scheme [" . $filtered . "]. " .
692
                "Scheme must be one of [" .
693
                implode(", ", array_keys($schemes)) . "]"
694
            );
695
        }
696
697
        return $scheme;
698
    }
699
700
    /**
701
     * @param string $element
702
     *
703
     * @return array
704
     */
705
    private function splitQueryValue(string $element): array
706
    {
707
        $data = explode("=", $element, 2);
708
        if (!isset($data[1])) {
709
            $data[] = null;
710
        }
711
712
        return $data;
713
    }
714
}
715