Uri::getUserInfo()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 4
c 2
b 0
f 0
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 10
cc 2
nc 2
nop 0
crap 2
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 InvalidArgumentException;
17
18
use function filter_var;
19
use function gettype;
20
use function is_integer;
21
use function is_null;
22
use function is_string;
23
use function ltrim;
24
use function parse_url;
25
use function rawurlencode;
26
use function sprintf;
27
28
/*
29
 * Value object representing a URI.
30
 */
31
class Uri implements UriInterface
32
{
33
    /**
34
     *    foo://example.com:8042/over/there?name=ferret#nose
35
     *    \_/   \______________/\_________/ \_________/ \__/
36
     *     |           |            |            |        |
37
     *  scheme     authority       path        query   fragment
38
     */
39
40
    /**
41
     * The scheme component of the URI.
42
     * For example, https://terryl.in/
43
     * In this case, "https" is the scheme.
44
     * 
45
     * @var string
46
     */
47
    protected $scheme;
48
49
    /**
50
     * The user component of the URI.
51
     * For example, https://jack:[email protected]
52
     * In this case, "jack" is the user.
53
     *
54
     * @var string
55
     */
56
    protected $user;
57
58
    /**
59
     * The password component of the URI.
60
     * For example, http://jack:[email protected]
61
     * In this case, "1234" is the password.
62
     *
63
     * @var string
64
     */
65
    protected $pass;
66
67
    /**
68
     * The host component of the URI.
69
     * For example, https://terryl.in:443/zh/
70
     * In this case, "terryl.in" is the host.
71
     *
72
     * @var string
73
     */
74
    protected $host;
75
76
    /**
77
     * The port component of the URI.
78
     * For example, https://terryl.in:443
79
     * In this case, "443" is the port.
80
     * 
81
     * @var int|null
82
     */
83
    protected $port;
84
85
    /**
86
     * The path component of the URI.
87
     * For example, https://terryl.in/zh/?paged=2
88
     * In this case, "/zh/" is the path.
89
     *
90
     * @var string
91
     */
92
    protected $path;
93
94
    /**
95
     * The query component of the URI.
96
     * For example, https://terryl.in/zh/?paged=2
97
     * In this case, "paged=2" is the query.
98
     *
99
     * @var string
100
     */
101
    protected $query;
102
103
    /**
104
     * The fragment component of the URI.
105
     * For example, https://terryl.in/#main-container
106
     * In this case, "main-container" is the fragment.
107
     *
108
     * @var string
109
     */
110
    protected $fragment;
111
112
    /**
113
     * Uri constructor.
114
     * 
115
     * @param string $uri The URI.
116
     */
117 35
    public function __construct($uri = '')
118
    {
119 35
        $this->assertString($uri, 'uri');
120 35
        $this->init((array) parse_url($uri));
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126 5
    public function getScheme(): string
127
    {
128 5
        return $this->scheme;
129
    }
130
131
    /**
132
     * {@inheritdoc}
133
     */
134 1
    public function getAuthority(): string
135
    {
136 1
        $authority = '';
137
138 1
        if ($this->getUserInfo()) {
139 1
            $authority .= $this->getUserInfo() . '@';
140
        }
141
142 1
        $authority .= $this->getHost();
143
144 1
        if (!is_null($this->getPort())) {
145 1
            $authority .= ':' . $this->getPort();
146
        }
147
148 1
        return $authority;
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154 5
    public function getUserInfo(): string
155
    {
156 5
        $userInfo = $this->user;
157
158 5
        if ($this->pass !== '') {
159 4
            $userInfo .= ':' . $this->pass;
160
        }
161
162 5
        return $userInfo;
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168 7
    public function getHost(): string
169
    {
170 7
        return $this->host;
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176 6
    public function getPort(): ?int
177
    {
178 6
        return $this->port;
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184 6
    public function getPath(): string
185
    {
186 6
        return $this->path;
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 6
    public function getQuery(): string
193
    {
194 6
        return $this->query;
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 5
    public function getFragment(): string
201
    {
202 5
        return $this->fragment;
203
    }
204
205
    /**
206
     * {@inheritdoc}
207
     */
208 1
    public function withScheme($scheme): UriInterface
209
    {
210 1
        $this->assertScheme($scheme);
211
212 1
        $scheme = $this->filter('scheme', ['scheme' => $scheme]);
213
214 1
        $clone = clone $this;
215 1
        $clone->scheme = $scheme;
216 1
        return $clone;
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222 1
    public function withUserInfo($user, $pass = null): UriInterface
223
    {
224 1
        $this->assertString($user, 'user');
225 1
        $user = $this->filter('user', ['user' => $user]);
226
227 1
        if ($pass) {
228 1
            $this->assertString($pass, 'pass');
229 1
            $pass = $this->filter('pass', ['pass' => $pass]);
230
        }
231
232 1
        $clone = clone $this;
233 1
        $clone->user = $user;
234 1
        $clone->pass = $pass;
235
236 1
        return $clone;
237
    }
238
239
    /**
240
     * {@inheritdoc}
241
     */
242 1
    public function withHost($host): UriInterface
243
    {
244 1
        $this->assertHost($host);
245
246 1
        $host = $this->filter('host', ['host' => $host]);
247
248 1
        $clone = clone $this;
249 1
        $clone->host = $host;
250
251 1
        return $clone;
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257 1
    public function withPort($port): UriInterface
258
    {
259 1
        $this->assertPort($port);
260
261 1
        $port = $this->filter('port', ['port' => $port]);
262
263 1
        $clone = clone $this;
264 1
        $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...
265
266 1
        return $clone;
267
    }
268
269
    /**
270
     * {@inheritdoc}
271
     */
272 1
    public function withPath($path): UriInterface
273
    {
274 1
        $this->assertString($path, 'path');
275
276 1
        $path = $this->filter('path', ['path' => $path]);
277
278 1
        $clone = clone $this;
279 1
        $clone->path = '/' . rawurlencode(ltrim($path, '/'));
280
281 1
        return $clone;
282
    }
283
284
    /**
285
     * {@inheritdoc}
286
     */
287 1
    public function withQuery($query): UriInterface
288
    {
289 1
        $this->assertString($query, 'query');
290
291 1
        $query = $this->filter('query', ['query' => $query]);
292
293
        // & => %26
294
        // ? => %3F
295
296 1
        $clone = clone $this;
297 1
        $clone->query = $query;
298
299 1
        return $clone;
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305 1
    public function withFragment($fragment): UriInterface
306
    {
307 1
        $this->assertString($fragment, 'fragment');
308
309 1
        $fragment = $this->filter('fragment', ['fragment' => $fragment]);
310
311 1
        $clone = clone $this;
312 1
        $clone->fragment = $fragment;
313
314 1
        return $clone;
315
    }
316
317
    /**
318
     * {@inheritdoc}
319
     */
320 1
    public function __toString(): string
321
    {
322 1
        $uri = '';
323
324
        // If a scheme is present, it MUST be suffixed by ":".
325 1
        if ($this->getScheme() !== '') {
326 1
            $uri .= $this->getScheme() . ':';
327
        }
328
329
        // If an authority is present, it MUST be prefixed by "//".
330 1
        if ($this->getAuthority() !== '') {
331 1
            $uri .= '//' . $this->getAuthority();
332
        }
333
334
        // If the path is rootless and an authority is present, the path MUST
335
        // be prefixed by "/".
336 1
        $uri .= '/' . ltrim($this->getPath(), '/');
337
338
        // If a query is present, it MUST be prefixed by "?".
339 1
        if ($this->getQuery() !== '') {
340 1
            $uri .= '?' . $this->getQuery();
341
        }
342
343
        // If a fragment is present, it MUST be prefixed by "#".
344 1
        if ($this->getFragment() !== '') {
345 1
            $uri .= '#' . $this->getFragment();
346
        }
347
348 1
        return $uri;
349
    }
350
351
    /*
352
    |--------------------------------------------------------------------------
353
    | Non PSR-7 Methods.
354
    |--------------------------------------------------------------------------
355
    */
356
357
    /**
358
     * Initialize.
359
     *
360
     * @param array $data Parsed URL data.
361
     *
362
     * @return void
363
     */
364 35
    protected function init(array $data = []): void
365
    {
366 35
        $components = [
367 35
            'scheme',
368 35
            'user',
369 35
            'pass',
370 35
            'host',
371 35
            'port',
372 35
            'path',
373 35
            'query',
374 35
            'fragment'
375 35
        ];
376
377 35
        foreach ($components as $v) {
378 35
            $this->{$v} = $this->filter($v, $data);
379
        }
380
    }
381
382
    /**
383
     * Filter URI components.
384
     * 
385
     * Users can provide both encoded and decoded characters.
386
     * Implementations ensure the correct encoding as outlined.
387
     * @see https://tools.ietf.org/html/rfc3986#section-2.2
388
     *
389
     * @param string $key  The part of URI.
390
     * @param array  $data Data parsed from a given URL.
391
     *
392
     * @return string|int|null
393
     */
394 35
    protected function filter(string $key, $data)
395
    {
396 35
        $notExists = [
397 35
            'scheme' => '',
398 35
            'user' => '',
399 35
            'pass' => '',
400 35
            'host' => '',
401 35
            'port' => null,
402 35
            'path' => '',
403 35
            'query' => '',
404 35
            'fragment' => '',
405 35
        ];
406
407 35
        if (!isset($data[$key])) {
408 34
            return $notExists[$key];
409
        }
410
411 35
        $value = $data[$key];
412
         
413
        // gen-delims  = ":" / "/" / "?" / "#" / "[" / "]" / "@"
414
        // $genDelims = ':/\?#\[\]@';
415
 
416
        // sub-delims  = "!" / "$" / "&" / "'" / "(" / ")"
417
        //             / "*" / "+" / "," / ";" / "="
418 35
        $subDelims = '!\$&\'\(\)\*\+,;=';
419
420
        // $unreserved  = ALPHA / DIGIT / "-" / "." / "_" / "~"
421 35
        $unReserved = 'a-zA-Z0-9\-\._~';
422
423
        // Encoded characters, such as "?" encoded to "%3F".
424 35
        $encodePattern = '%(?![A-Fa-f0-9]{2})';
425
426 35
        $regex = '';
427
428
        switch ($key) {
429 35
            case 'host':
430 35
            case 'scheme':
431 21
                return strtolower($value);
432
                break;
0 ignored issues
show
Unused Code introduced by
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...
433
434 32
            case 'query':
435 32
            case 'fragment':
436 8
                $specPattern = '%:@\/\?';
437 8
                $regex = '/(?:[^' . $unReserved . $subDelims . $specPattern . ']+|' . $encodePattern . ')/';
438 8
                break;
439
440 32
            case 'path':
441 31
                $specPattern = '%:@\/';
442 31
                $regex = '/(?:[^' . $unReserved . $subDelims . $specPattern . ']+|' . $encodePattern . ')/';
443 31
                break;
444
445 11
            case 'user':
446 11
            case 'pass':
447 7
                $regex = '/(?:[^%' . $unReserved . $subDelims . ']+|' . $encodePattern . ')/';
448 7
                break;
449
450 10
            case 'port':
451 10
                if ($this->scheme === 'http' && (int) $value !== 80) {
452 4
                    return (int) $value;
453
                }
454 7
                if ($this->scheme === 'https' && (int) $value !== 443) {
455 1
                    return (int) $value;
456
                } 
457 6
                if ($this->scheme === '') {
458 1
                    return (int) $value;
459
                }
460 6
                return null;
461
462
            // endswitch
463
        }
464
465 31
        if ($regex) {
466 31
            return preg_replace_callback(
467 31
                $regex,
468 31
                function ($match) {
469 1
                    return rawurlencode($match[0]);
470 31
                },
471 31
                $value
472 31
            );
473
        }
474
475
        // @codeCoverageIgnoreStart
476
477
        return $value;
478
479
        // @codeCoverageIgnoreEnd
480
    }
481
482
    /**
483
     * Throw exception for the invalid scheme.
484
     *
485
     * @param string $scheme The scheme string of a URI.
486
     *
487
     * @return void
488
     * 
489
     * @throws InvalidArgumentException
490
     */
491 2
    protected function assertScheme($scheme): void
492
    {
493 2
        $this->assertString($scheme, 'scheme');
494
495 2
        $validSchemes = [
496 2
            0 => '',
497 2
            1 => 'http',
498 2
            2 => 'https',
499 2
        ];
500
501 2
        if (!in_array($scheme, $validSchemes)) {
502 1
            throw new InvalidArgumentException(
503 1
                sprintf(
504 1
                    'The string "%s" is not a valid scheme.',
505 1
                    $scheme
506 1
                )
507 1
            );
508
        }
509
    }
510
511
    /**
512
     * Throw exception for the invalid value.
513
     *
514
     * @param string $value The value to check.
515
     * @param string $name  The name of the value.
516
     *
517
     * @return void
518
     * 
519
     * @throws InvalidArgumentException
520
     */
521 35
    protected function assertString($value, string $name = 'it'): void
522
    {
523 35
        if (!is_string($value)) {
0 ignored issues
show
introduced by
The condition is_string($value) is always true.
Loading history...
524 1
            throw new InvalidArgumentException(
525 1
                sprintf(
526 1
                    ucfirst($name) . ' must be a string, but %s provided.',
527 1
                    gettype($value)
528 1
                )
529 1
            );
530
        }
531
    }
532
533
    /**
534
     * Throw exception for the invalid host string.
535
     *
536
     * @param string $host The host string to of a URI.
537
     * 
538
     * @return void
539
     * 
540
     * @throws InvalidArgumentException
541
     */
542 3
    protected function assertHost($host): void
543
    {
544 3
        $this->assertString($host);
545
546 3
        if (empty($host)) {
547
            // Note: An empty host value is equivalent to removing the host.
548
            // So that if the host is empty, ignore the following check.
549 1
            return;
550
        }
551
552 2
        if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) {
553 1
            throw new InvalidArgumentException(
554 1
                sprintf(
555 1
                    '"%s" is not a valid host',
556 1
                    $host
557 1
                )
558 1
            );
559
        }
560
    }
561
562
    /**
563
     * Throw exception for the invalid port.
564
     *
565
     * @param null|int $port The port number to of a URI.
566
     * 
567
     * @return void
568
     *
569
     * @throws InvalidArgumentException
570
     */
571 3
    protected function assertPort($port): void
572
    {
573
        if (
574 3
            !is_null($port) && 
575 3
            !is_integer($port)
0 ignored issues
show
introduced by
The condition is_integer($port) is always true.
Loading history...
576
        ) {
577 1
            throw new InvalidArgumentException(
578 1
                sprintf(
579 1
                    'Port must be an integer or a null value, but %s provided.',
580 1
                    gettype($port)
581 1
                )
582 1
            );
583
        }
584
585 2
        if (!($port > 0 && $port < 65535)) {
586 1
            throw new InvalidArgumentException(
587 1
                sprintf(
588 1
                    'Port number should be in a range of 0-65535, but %s provided.',
589 1
                    $port
590 1
                )
591 1
            );
592
        }
593
    }
594
}
595