Uri::getScheme()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * Platine HTTP
5
 *
6
 * Platine HTTP Message is the implementation of PSR 7
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine HTTP
11
 * Copyright (c) 2019 Dion Chaika
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file Uri.php
34
 *
35
 *  The Uri class used to manage HTTP Uri
36
 *
37
 *  @package    Platine\Http
38
 *  @author Platine Developers Team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   https://www.platine-php.com
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Http;
49
50
use InvalidArgumentException;
51
52
/**
53
 * @class Uri
54
 * @package Platine\Http
55
 */
56
class Uri implements UriInterface
57
{
58
    /**
59
     * The Uri scheme
60
     * @var string
61
     */
62
    protected string $scheme = '';
63
64
    /**
65
     * The Uri user information
66
     * @var string
67
     */
68
    protected string $userInfo = '';
69
70
    /**
71
     * The Uri host
72
     * @var string
73
     */
74
    protected string $host = '';
75
76
    /**
77
     * The Uri port
78
     * @var int|null
79
     */
80
    protected ?int $port = null;
81
82
    /**
83
     * The Uri path
84
     * @var string
85
     */
86
    protected string $path = '';
87
88
    /**
89
     * The Uri query
90
     * @var string
91
     */
92
    protected ?string $query = '';
93
94
    /**
95
     * The Uri fragment
96
     * @var string
97
     */
98
    protected string $fragment = '';
99
100
    /**
101
     * Create new Uri instance
102
     * @param string $uri
103
     */
104
    public function __construct(string $uri = '')
105
    {
106
        if ($uri !== '') {
107
            $parts = parse_url($uri);
108
109
            if ($parts === false) {
110
                throw new InvalidArgumentException('URL is malformed.');
111
            }
112
            $scheme = !empty($parts['scheme']) ? $parts['scheme'] : '';
113
            $user = !empty($parts['user']) ? $parts['user'] : '';
114
            $password = !empty($parts['pass']) ? $parts['pass'] : null;
115
            $host = !empty($parts['host']) ? $parts['host'] : '';
116
            $port = !empty($parts['port']) ? $parts['port'] : null;
117
            $path = !empty($parts['path']) ? $parts['path'] : '';
118
            $query = !empty($parts['query']) ? $parts['query'] : '';
119
            $fragment = !empty($parts['fragment']) ? $parts['fragment'] : '';
120
121
            $userInfo = $user;
122
            if ($userInfo !== '' && $password !== null && $password !== '') {
123
                $userInfo .= ':' . $password;
124
            }
125
126
            $this->scheme = $this->filterScheme($scheme);
127
            $this->userInfo = $userInfo;
128
            $this->host = $this->filterHost($host);
129
            $this->port = $this->filterPort($port);
130
            $this->path = $this->filterPath($path);
131
            $this->query = $this->filterQuery($query);
132
            $this->fragment = $this->filterFragment($fragment);
133
        }
134
    }
135
136
    /**
137
     * Create new Uri using Super Global
138
     * @return Uri the new Uri instance
139
     */
140
    public static function createFromGlobals(): self
141
    {
142
        $isSecure = !empty($_SERVER['HTTPS'])
143
                        && strnatcasecmp($_SERVER['HTTPS'], 'off') !== 0;
144
        $scheme = $isSecure ? 'https' : 'http';
145
        $host = !empty($_SERVER['SERVER_NAME'])
146
                ? $_SERVER['SERVER_NAME']
147
                : (!empty($_SERVER['SERVER_ADDR'])
148
                    ? $_SERVER['SERVER_ADDR']
149
                : '127.0.0.1');
150
        $port = !empty($_SERVER['SERVER_PORT'])
151
                ? (int) $_SERVER['SERVER_PORT']
152
                : ($isSecure ? 443 : 80);
153
        $path = '/';
154
        if (!empty($_SERVER['REQUEST_URI'])) {
155
            $parts = explode('?', $_SERVER['REQUEST_URI'], 2);
156
            $path = $parts[0];
157
        }
158
        $query = !empty($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
159
160
        return (new static())
161
                        ->withScheme($scheme)
162
                        ->withHost($host)
163
                        ->withPort($port)
164
                        ->withPath($path)
165
                        ->withQuery($query);
166
    }
167
168
    /**
169
     * {@inheritdoc}
170
     */
171
    public function getScheme(): string
172
    {
173
        return $this->scheme;
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179
    public function getAuthority(): string
180
    {
181
        $authority = $this->host;
182
        if ($authority !== '') {
183
            if ($this->userInfo !== '') {
184
                $authority = $this->userInfo . '@' . $authority;
185
            }
186
187
            if ($this->port !== null) {
188
                $authority .= ':' . $this->port;
189
            }
190
        }
191
        return $authority;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197
    public function getUserInfo(): string
198
    {
199
        return $this->userInfo;
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public function getHost(): string
206
    {
207
        return $this->host;
208
    }
209
210
    /**
211
     * {@inheritdoc}
212
     */
213
    public function getPort(): ?int
214
    {
215
        return $this->port;
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function getPath(): string
222
    {
223
        return $this->path;
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     *
229
     * @return string
230
     */
231
    public function getQuery(): string
232
    {
233
        return $this->query ?? '';
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239
    public function getFragment(): string
240
    {
241
        return $this->fragment;
242
    }
243
244
    /**
245
     * {@inheritdoc}
246
     */
247
    public function withScheme(string $scheme): self
248
    {
249
        $that = clone $this;
250
        $that->scheme = $this->filterScheme($scheme);
251
        $that->port = !$this->isStandardPort($that->scheme, $that->port) ? $that->port : null;
252
253
        return $that;
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function withUserInfo(string $user, ?string $password = null): self
260
    {
261
        $userInfo = $user;
262
        if ($userInfo !== '' && $password !== null && $password !== '') {
263
            $userInfo .= ':' . $password;
264
        }
265
        $that = clone $this;
266
        $that->userInfo = $userInfo;
267
268
        return $that;
269
    }
270
271
    /**
272
     * {@inheritdoc}
273
     */
274
    public function withHost(string $host): self
275
    {
276
        $that = clone $this;
277
        $that->host = $this->filterHost($host);
278
279
        return $that;
280
    }
281
282
    /**
283
     * {@inheritdoc}
284
     */
285
    public function withPort(?int $port): self
286
    {
287
        $that = clone $this;
288
        $that->port = $this->filterPort($port);
289
290
        return $that;
291
    }
292
293
    /**
294
     * {@inheritdoc}
295
     */
296
    public function withPath(string $path): self
297
    {
298
        $that = clone $this;
299
        $that->path = $this->filterPath($path);
300
301
        return $that;
302
    }
303
304
    /**
305
     * {@inheritdoc}
306
     */
307
    public function withQuery(string $query): self
308
    {
309
        $that = clone $this;
310
        $that->query = $this->filterQuery($query);
311
312
        return $that;
313
    }
314
315
    /**
316
     * {@inheritdoc}
317
     */
318
    public function withFragment(string $fragment): self
319
    {
320
        $that = clone $this;
321
        $that->fragment = $this->filterFragment($fragment);
322
323
        return $that;
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329
    public function __toString(): string
330
    {
331
        $uri = '';
332
        if ($this->scheme !== '') {
333
            $uri .= $this->scheme . ':';
334
        }
335
336
        $authority = $this->getAuthority();
337
        if ($authority !== '') {
338
            $uri .= '//' . $authority;
339
        }
340
341
        if ($authority !== '' && strncmp($this->path, '/', 1) !== 0) {
342
            $uri .= '/' . $this->path;
343
        } elseif ($authority === '' && strncmp($this->path, '//', 2) === 0) {
344
            $uri .= '/' . ltrim($this->path, '/');
345
        } else {
346
            $uri .= $this->path;
347
        }
348
349
        if ($this->query !== '') {
350
            $uri .= '?' . $this->query;
351
        }
352
353
        if ($this->fragment !== '') {
354
            $uri .= '#' . $this->fragment;
355
        }
356
357
        return $uri;
358
    }
359
360
    /**
361
     * Filter Uri scheme
362
     * @param  string $scheme
363
     * @return string
364
     */
365
    protected function filterScheme(string $scheme): string
366
    {
367
        if ($scheme !== '') {
368
            if (!preg_match('/^[a-zA-Z][a-zA-Z0-9+\-.]*$/', $scheme)) {
369
                throw new InvalidArgumentException(
370
                    'Scheme must be compliant with the "RFC 3986" standart'
371
                );
372
            }
373
            return strtolower($scheme);
374
        }
375
376
        return $scheme;
377
    }
378
379
    /**
380
     * Filter Uri host
381
     * @param  string $host
382
     * @return string
383
     */
384
    protected function filterHost(string $host): string
385
    {
386
        if ($host !== '') {
387
            //Matching an IPvFuture or an IPv6address.
388
            if (preg_match('/^\[.+\]$/', $host)) {
389
                $host = trim($host, '[]');
390
391
                // Matching an IPvFuture.
392
                if (preg_match('/^(v|V)/', $host)) {
393
                    if (
394
                        !preg_match(
395
                            '/^(v|V)[a-fA-F0-9]\.([a-zA-Z0-9\-._~]|[!$&\'()*+,;=]|\:)$/',
396
                            $host
397
                        )
398
                    ) {
399
                        throw new InvalidArgumentException(
400
                            'IP address must be compliant with the '
401
                                        . '"IPvFuture" of the "RFC 3986" standart.'
402
                        );
403
                    }
404
                    // Matching an IPv6address.
405
                    // TODO
406
                } elseif (
407
                        filter_var(
408
                            $host,
409
                            FILTER_VALIDATE_IP,
410
                            FILTER_FLAG_IPV6
411
                        ) === false
412
                ) {
413
                    throw new InvalidArgumentException(
414
                        'IP address must be compliant with the "IPv6address"'
415
                                    . ' of the "RFC 3986" standart.'
416
                    );
417
                }
418
                $host = '[' . $host . ']';
419
420
                // Matching an IPv4address.
421
            } elseif (
422
                preg_match(
423
                    '/^([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\./',
424
                    $host
425
                )
426
            ) {
427
                if (
428
                    filter_var(
429
                        $host,
430
                        FILTER_VALIDATE_IP,
431
                        FILTER_FLAG_IPV4
432
                    ) === false
433
                ) {
434
                    throw new InvalidArgumentException(
435
                        'IP address must be compliant with the '
436
                                    . '"IPv4address" of the "RFC 3986" standart.'
437
                    );
438
                }
439
440
                // Matching a domain name.
441
            } else {
442
                if (
443
                    !preg_match(
444
                        '/^([a-zA-Z0-9\-._~]|%[a-fA-F0-9]{2}|[!$&\'()*+,;=])*$/',
445
                        $host
446
                    )
447
                ) {
448
                    throw new InvalidArgumentException(
449
                        'Host be compliant with the "RFC 3986" standart.'
450
                    );
451
                }
452
            }
453
454
            return strtolower($host);
455
        }
456
        return $host;
457
    }
458
459
    /**
460
     * Filter Uri port
461
     *
462
     * @param int|null $port
463
     *
464
     * @return int|null
465
     */
466
    protected function filterPort(?int $port): ?int
467
    {
468
        if ($port !== null) {
469
            if ($port < 1 || $port > 65535) {
470
                throw new InvalidArgumentException(
471
                    'TCP or UDP port must be between 1 and 65535'
472
                );
473
            }
474
            return !$this->isStandardPort($this->scheme, $port) ? $port : null;
475
        }
476
        return $port;
477
    }
478
479
    /**
480
     * Filter Uri path
481
     * @param  string $path
482
     * @return string
483
     */
484
    protected function filterPath(string $path): string
485
    {
486
        if ($this->scheme === '' && strncmp($path, ':', 1) === 0) {
487
            throw new InvalidArgumentException(
488
                'Path of a URI without a scheme cannot begin with a colon'
489
            );
490
        }
491
492
        $authority = $this->getAuthority();
493
        if ($authority === '' && strncmp($path, '//', 2) === 0) {
494
            throw new InvalidArgumentException(
495
                'Path of a URI without an authority cannot begin with two slashes'
496
            );
497
        }
498
499
        if ($authority !== '' && $path !== '' && strncmp($path, '/', 1) !== 0) {
500
            throw new InvalidArgumentException(
501
                'Path of a URI with an authority must be empty or begin with a slash'
502
            );
503
        }
504
505
        if ($path !== '' && $path !== '/') {
506
            if (
507
                !preg_match(
508
                    '/^([a-zA-Z0-9\-._~]|%[a-fA-F0-9]{2}|[!$&\'()*+,;=]|\:|\@|\/|\%)*$/',
509
                    $path
510
                )
511
            ) {
512
                throw new InvalidArgumentException(
513
                    'Path must be compliant with the "RFC 3986" standart'
514
                );
515
            }
516
517
            return (string) preg_replace_callback(
518
                '/(?:[^a-zA-Z0-9\-._~!$&\'()*+,;=:@\/%]++|%(?![a-fA-F0-9]{2}))/',
519
                function ($matches) {
520
                    return rawurlencode($matches[0]);
521
                },
522
                $path
523
            );
524
        }
525
526
        return $path;
527
    }
528
529
    /**
530
     * Filter Uri query
531
     * @param  string $query
532
     * @return string
533
     */
534
    protected function filterQuery(string $query): string
535
    {
536
        if ($query !== '') {
537
            if (
538
                !preg_match(
539
                    '/^([a-zA-Z0-9\-._~]|%[a-fA-F0-9]{2}|[!$&\'()*+,;=]|\:|\@|\/|\?|\%)*$/',
540
                    $query
541
                )
542
            ) {
543
                throw new InvalidArgumentException(
544
                    'Query must be compliant with the "RFC 3986" standart'
545
                );
546
            }
547
548
            return (string) preg_replace_callback(
549
                '/(?:[^a-zA-Z0-9\-._~!$&\'()*+,;=:@\/?%]++|%(?![a-fA-F0-9]{2}))/',
550
                function ($matches) {
551
                    return rawurlencode($matches[0]);
552
                },
553
                $query
554
            );
555
        }
556
557
        return $query;
558
    }
559
560
    /**
561
     * Filter Uri fragment
562
     * @param  string $fragment
563
     * @return string
564
     */
565
    protected function filterFragment(string $fragment): string
566
    {
567
        if ($fragment !== '') {
568
            return (string) preg_replace_callback(
569
                '/(?:[^a-zA-Z0-9\-._~!$&\'()*+,;=:@\/?%]++|%(?![a-fA-F0-9]{2}))/',
570
                function ($matches) {
571
                    return rawurlencode($matches[0]);
572
                },
573
                $fragment
574
            );
575
        }
576
577
        return $fragment;
578
    }
579
580
    /**
581
     * Check whether the port is standard for the given scheme
582
     * @param  string  $scheme the scheme
583
     * @param  int|null $port the port
584
     * @return boolean
585
     */
586
    protected function isStandardPort(string $scheme, ?int $port): bool
587
    {
588
        return $port === ($scheme === 'https' ? 443 : 80);
589
    }
590
}
591