Passed
Push — master ( 8a04f9...43fdea )
by Terry
01:45
created

src/Psr7/Uri.php (3 issues)

1
<?php 
2
/*
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace Shieldon\Psr7;
14
15
use Psr\Http\Message\UriInterface;
16
use Shieldon\Psr7\Utils\UriHelper;
0 ignored issues
show
The type Shieldon\Psr7\Utils\UriHelper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use InvalidArgumentException;
18
19
use function filter_var;
20
use function gettype;
21
use function is_integer;
22
use function is_null;
23
use function is_string;
24
use function ltrim;
25
use function parse_url;
26
use function rawurlencode;
27
use function sprintf;
28
29
/*
30
 * Value object representing a URI.
31
 */
32
class Uri implements UriInterface
33
{
34
    /**
35
     *    foo://example.com:8042/over/there?name=ferret#nose
36
     *    \_/   \______________/\_________/ \_________/ \__/
37
     *     |           |            |            |        |
38
     *  scheme     authority       path        query   fragment
39
     */
40
41
    /**
42
     * The scheme component of the URI.
43
     * For example, https://terryl.in/
44
     * In this case, "https" is the scheme.
45
     * 
46
     * @var string
47
     */
48
    protected $scheme;
49
50
    /**
51
     * The user component of the URI.
52
     * For example, https://jack:[email protected]
53
     * In this case, "jack" is the user.
54
     *
55
     * @var string
56
     */
57
    protected $user;
58
59
    /**
60
     * The password component of the URI.
61
     * For example, http://jack:[email protected]
62
     * In this case, "1234" is the password.
63
     *
64
     * @var string
65
     */
66
    protected $pass;
67
68
    /**
69
     * The host component of the URI.
70
     * For example, https://terryl.in:443/zh/
71
     * In this case, "terryl.in" is the host.
72
     *
73
     * @var string
74
     */
75
    protected $host;
76
77
    /**
78
     * The port component of the URI.
79
     * For example, https://terryl.in:443
80
     * In this case, "443" is the port.
81
     * 
82
     * @var int|null
83
     */
84
    protected $port;
85
86
    /**
87
     * The path component of the URI.
88
     * For example, https://terryl.in/zh/?paged=2
89
     * In this case, "/zh/" is the path.
90
     *
91
     * @var string
92
     */
93
    protected $path;
94
95
    /**
96
     * The query component of the URI.
97
     * For example, https://terryl.in/zh/?paged=2
98
     * In this case, "paged=2" is the query.
99
     *
100
     * @var string
101
     */
102
    protected $query;
103
104
    /**
105
     * The fragment component of the URI.
106
     * For example, https://terryl.in/#main-container
107
     * In this case, "main-container" is the fragment.
108
     *
109
     * @var string
110
     */
111
    protected $fragment;
112
113
    /**
114
     * Uri constructor.
115
     * 
116
     * @param string $uri The URI.
117
     */
118
    public function __construct($uri = '')
119
    {
120
        $this->assertString($uri, 'uri');
121
        $this->init(parse_url($uri));
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function getScheme(): string
128
    {
129
        return $this->scheme;
130
    }
131
132
    /**
133
     * {@inheritdoc}
134
     */
135
    public function getAuthority(): string
136
    {
137
        $authority = '';
138
139
        if ($this->getUserInfo()) {
140
            $authority .= $this->getUserInfo() . '@';
141
        }
142
143
        $authority .= $this->getHost();
144
145
        if (! is_null($this->getPort())) {
146
            $authority .= ':' . $this->getPort();
147
        }
148
149
        return $authority;
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function getUserInfo(): string
156
    {
157
        $userInfo = $this->user;
158
159
        if ($this->pass !== '') {
160
            $userInfo .= ':' . $this->pass;
161
        }
162
163
        return $userInfo;
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function getHost(): string
170
    {
171
        return $this->host;
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177
    public function getPort()
178
    {
179
        return $this->port;
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function getPath(): string
186
    {
187
        return $this->path;
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193
    public function getQuery(): string
194
    {
195
        return $this->query;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function getFragment(): string
202
    {
203
        return $this->fragment;
204
    }
205
206
    /**
207
     * {@inheritdoc}
208
     */
209
    public function withScheme($scheme)
210
    {
211
        $this->assertScheme($scheme);
212
213
        $scheme = $this->filter('scheme', ['scheme' => $scheme]);
214
215
        $clone = clone $this;
216
        $clone->scheme = $scheme;
217
        return $clone;
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     */
223
    public function withUserInfo($user, $pass = null)
224
    {
225
        $this->assertString($user, 'user');
226
        $user = $this->filter('user', ['user' => $user]);
227
228
        if ($pass) {
229
            $this->assertString($pass, 'pass');
230
            $pass = $this->filter('pass', ['pass' => $pass]);
231
        }
232
233
        $clone = clone $this;
234
        $clone->user = $user;
235
        $clone->pass = $pass;
236
237
        return $clone;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243
    public function withHost($host)
244
    {
245
        $this->assertHost($host);
246
247
        $host = $this->filter('host', ['host' => $host]);
248
249
        $clone = clone $this;
250
        $clone->host = $host;
251
252
        return $clone;
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258
    public function withPort($port)
259
    {
260
        $this->assertPort($port);
261
262
        $port = $this->filter('port', ['port' => $port]);
263
264
        $clone = clone $this;
265
        $clone->port = $port;
0 ignored issues
show
Documentation Bug introduced by
It seems like $port can also be of type string. However, the property $port is declared as type integer|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
266
267
        return $clone;
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273
    public function withPath($path)
274
    {
275
        $this->assertString($path, 'path');
276
277
        $path = $this->filter('path', ['path' => $path]);
278
279
        $clone = clone $this;
280
        $clone->path = '/' . rawurlencode(ltrim($path, '/'));
281
282
        return $clone;
283
    }
284
285
    /**
286
     * {@inheritdoc}
287
     */
288
    public function withQuery($query)
289
    {
290
        $this->assertString($query, 'query');
291
292
        $query = $this->filter('query', ['query' => $query]);
293
294
        // & => %26
295
        // ? => %3F
296
297
        $clone = clone $this;
298
        $clone->query = $query;
299
300
        return $clone;
301
    }
302
303
    /**
304
     * {@inheritdoc}
305
     */
306
    public function withFragment($fragment)
307
    {
308
        $this->assertString($fragment, 'fragment');
309
310
        $fragment = $this->filter('fragment', ['fragment' => $fragment]);
311
312
        $clone = clone $this;
313
        $clone->fragment = $fragment;
314
315
        return $clone;
316
    }
317
318
    /**
319
     * {@inheritdoc}
320
     */
321
    public function __toString(): string
322
    {
323
        $uri = '';
324
325
        // If a scheme is present, it MUST be suffixed by ":".
326
        if ($this->getScheme() !== '') {
327
            $uri .= $this->getScheme() . ':';
328
        }
329
330
        // If an authority is present, it MUST be prefixed by "//".
331
        if ($this->getAuthority() !== '') {
332
            $uri .= '//' . $this->getAuthority();
333
        }
334
335
        // If the path is rootless and an authority is present, the path MUST
336
        // be prefixed by "/".
337
        $uri .= '/' . ltrim($this->getPath(), '/');
338
339
        // If a query is present, it MUST be prefixed by "?".
340
        if ($this->getQuery() !== '') {
341
            $uri .= '?' . $this->getQuery();
342
        }
343
344
        // If a fragment is present, it MUST be prefixed by "#".
345
        if ($this->getFragment() !== '') {
346
            $uri .= '#' . $this->getFragment();
347
        }
348
349
        return $uri;
350
    }
351
352
    /*
353
    |--------------------------------------------------------------------------
354
    | Non PSR-7 Methods.
355
    |--------------------------------------------------------------------------
356
    */
357
358
    /**
359
     * Initialize.
360
     *
361
     * @param array $data Parsed URL data.
362
     *
363
     * @return void
364
     */
365
    protected function init(array $data = []): void
366
    {
367
        $components = [
368
            'scheme',
369
            'user',
370
            'pass',
371
            'host',
372
            'port',
373
            'path',
374
            'query',
375
            'fragment'
376
        ];
377
378
        foreach ($components as $v) {
379
            $this->{$v} = $this->filter($v, $data);
380
        }
381
    }
382
383
    /**
384
     * Filter URI components.
385
     * 
386
     * Users can provide both encoded and decoded characters.
387
     * Implementations ensure the correct encoding as outlined.
388
     * @see https://tools.ietf.org/html/rfc3986#section-2.2
389
     *
390
     * @param string $key  The part of URI.
391
     * @param array  $data Data parsed from a given URL.
392
     *
393
     * @return string|int|null
394
     */
395
    protected function filter(string $key, $data)
396
    {
397
        $notExists = [
398
            'scheme' => '',
399
            'user' => '',
400
            'pass' => '',
401
            'host' => '',
402
            'port' => null,
403
            'path' => '',
404
            'query' => '',
405
            'fragment' => '',
406
        ];
407
408
        if (!isset($data[$key])) {
409
            return $notExists[$key];
410
        }
411
412
        $value = $data[$key];
413
         
414
        // gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
415
        // $genDelims = ':/\?#\[\]@';
416
 
417
        // sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
418
        //             / "*" / "+" / "," / ";" / "="
419
        $subDelims = '!\$&\'\(\)\*\+,;=';
420
421
        // $unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
422
        $unReserved = 'a-zA-Z0-9\-\._~';
423
424
        // Encoded characters, such as "?" encoded to "%3F".
425
        $encodePattern = '%(?![A-Fa-f0-9]{2})';
426
427
        $regex = '';
428
429
        switch ($key) {
430
            case 'host':
431
            case 'scheme':
432
                return strtolower($value);
433
                break;
0 ignored issues
show
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
434
435
            case 'query':
436
            case 'fragment':
437
                $specPattern = '%:@\/\?';
438
                $regex = '/(?:[^' . $unReserved . $subDelims . $specPattern . ']+|' . $encodePattern . ')/';
439
                break;
440
441
            case 'path':
442
                $specPattern = '%:@\/';
443
                $regex = '/(?:[^' . $unReserved . $subDelims . $specPattern . ']+|' . $encodePattern . ')/';
444
                break;
445
446
            case 'user':
447
            case 'pass':
448
                $regex = '/(?:[^%' . $unReserved . $subDelims . ']+|' . $encodePattern . ')/';
449
                break;
450
451
            case 'port':
452
                if ($this->scheme === 'http' && (int) $value !== 80) {
453
                    return (int) $value;
454
                }
455
                if ($this->scheme === 'https' && (int) $value !== 443) {
456
                    return (int) $value;
457
                } 
458
                if ($this->scheme === '') {
459
                    return (int) $value;
460
                }
461
                return null;
462
463
            default:
464
                break;
465
        }
466
467
        if ($regex) {
468
            return preg_replace_callback(
469
                $regex,
470
                function ($match) {
471
                    return rawurlencode($match[0]);
472
                },
473
                $value
474
            );
475
        }
476
477
        return $value;
478
    }
479
480
    /**
481
     * Throw exception for the invalid scheme.
482
     *
483
     * @param string $scheme The scheme string of a URI.
484
     *
485
     * @return void
486
     * 
487
     * @throws InvalidArgumentException
488
     */
489
    protected function assertScheme($scheme): void
490
    {
491
        $this->assertString($scheme, 'scheme');
492
493
        $validSchemes = [
494
            0 => '',
495
            1 => 'http',
496
            2 => 'https',
497
        ];
498
499
        if (!in_array($scheme, $validSchemes)) {
500
            throw new InvalidArgumentException(
501
                sprintf(
502
                    'The string "%s" is not a valid scheme.',
503
                    $scheme
504
                )
505
            );
506
        }
507
    }
508
509
    /**
510
     * Throw exception for the invalid value.
511
     *
512
     * @param string $value The value to check.
513
     * @param string $name  The name of the value.
514
     *
515
     * @return void
516
     * 
517
     * @throws InvalidArgumentException
518
     */
519
    protected function assertString($value, string $name = 'it'): void
520
    {
521
        if (!is_string($value)) {
522
            throw new InvalidArgumentException(
523
                sprintf(
524
                    ucfirst($name) . ' must be a string, but %s provided.',
525
                    gettype($value)
526
                )
527
            );
528
        }
529
    }
530
531
    /**
532
     * Throw exception for the invalid host string.
533
     *
534
     * @param string $host The host string to of a URI.
535
     * 
536
     * @return void
537
     * 
538
     * @throws InvalidArgumentException
539
     */
540
    protected function assertHost($host): void
541
    {
542
        $this->assertString($host);
543
544
        if (empty($host)) {
545
            // Note: An empty host value is equivalent to removing the host.
546
            // So that if the host is empty, ignore the following check.
547
            return;
548
        }
549
550
        if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
551
            throw new InvalidArgumentException(
552
                sprintf(
553
                    '"%s" is not a valid host',
554
                    $host
555
                )
556
            );
557
        }
558
    }
559
560
    /**
561
     * Throw exception for the invalid port.
562
     *
563
     * @param null|int $port The port number to of a URI.
564
     * 
565
     * @return void
566
     *
567
     * @throws InvalidArgumentException
568
     */
569
    protected function assertPort($port): void
570
    {
571
        if (
572
            !is_null($port) && 
573
            !is_integer($port)
574
        ) {
575
            throw new InvalidArgumentException(
576
                sprintf(
577
                    'Port must be an integer or a null value, but %s provided.',
578
                    gettype($port)
579
                )
580
            );
581
        }
582
583
        if (!($port > 0 && $port < 65535)) {
584
            throw new InvalidArgumentException(
585
                sprintf(
586
                    'Port number should be in a range of 0-65535, but %s provided.',
587
                    $port
588
                )
589
            );
590
        }
591
    }
592
}
593