• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 14575173072

21 Apr 2025 02:26PM UTC coverage: 84.402% (+0.007%) from 84.395%
14575173072

Pull #9528

github

web-flow
Merge 681f2d691 into 3d3ba0512
Pull Request #9528: feat: add Time::addCalendarMonths() function

11 of 11 new or added lines in 1 file covered. (100.0%)

136 existing lines in 21 files now uncovered.

20827 of 24676 relevant lines covered (84.4%)

191.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.71
/system/HTTP/URI.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\HTTP;
15

16
use CodeIgniter\Exceptions\BadMethodCallException;
17
use CodeIgniter\Exceptions\InvalidArgumentException;
18
use CodeIgniter\HTTP\Exceptions\HTTPException;
19
use Config\App;
20
use Stringable;
21

22
/**
23
 * Abstraction for a uniform resource identifier (URI).
24
 *
25
 * @see \CodeIgniter\HTTP\URITest
26
 */
27
class URI implements Stringable
28
{
29
    /**
30
     * Sub-delimiters used in query strings and fragments.
31
     */
32
    public const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
33

34
    /**
35
     * Unreserved characters used in paths, query strings, and fragments.
36
     */
37
    public const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
38

39
    /**
40
     * Current URI string
41
     *
42
     * @var string
43
     *
44
     * @deprecated 4.4.0 Not used.
45
     */
46
    protected $uriString;
47

48
    /**
49
     * The Current baseURL.
50
     *
51
     * @deprecated 4.4.0 Use SiteURI instead.
52
     */
53
    private ?string $baseURL = null;
54

55
    /**
56
     * List of URI segments.
57
     *
58
     * Starts at 1 instead of 0
59
     *
60
     * @var array<int, string>
61
     */
62
    protected $segments = [];
63

64
    /**
65
     * The URI Scheme.
66
     *
67
     * @var string
68
     */
69
    protected $scheme = 'http';
70

71
    /**
72
     * URI User Info
73
     *
74
     * @var string|null
75
     */
76
    protected $user;
77

78
    /**
79
     * URI User Password
80
     *
81
     * @var string|null
82
     */
83
    protected $password;
84

85
    /**
86
     * URI Host
87
     *
88
     * @var string|null
89
     */
90
    protected $host;
91

92
    /**
93
     * URI Port
94
     *
95
     * @var int|null
96
     */
97
    protected $port;
98

99
    /**
100
     * URI path.
101
     *
102
     * @var string|null
103
     */
104
    protected $path;
105

106
    /**
107
     * The name of any fragment.
108
     *
109
     * @var string
110
     */
111
    protected $fragment = '';
112

113
    /**
114
     * The query string.
115
     *
116
     * @var array<string, string>
117
     */
118
    protected $query = [];
119

120
    /**
121
     * Default schemes/ports.
122
     *
123
     * @var array{
124
     *  http: int,
125
     *  https: int,
126
     *  ftp: int,
127
     *  sftp: int,
128
     * }
129
     */
130
    protected $defaultPorts = [
131
        'http'  => 80,
132
        'https' => 443,
133
        'ftp'   => 21,
134
        'sftp'  => 22,
135
    ];
136

137
    /**
138
     * Whether passwords should be shown in userInfo/authority calls.
139
     * Default to false because URIs often show up in logs
140
     *
141
     * @var bool
142
     */
143
    protected $showPassword = false;
144

145
    /**
146
     * If true, will continue instead of throwing exceptions.
147
     *
148
     * @var bool
149
     */
150
    protected $silent = false;
151

152
    /**
153
     * If true, will use raw query string.
154
     *
155
     * @var bool
156
     */
157
    protected $rawQueryString = false;
158

159
    /**
160
     * Builds a representation of the string from the component parts.
161
     *
162
     * @param string|null $scheme URI scheme. E.g., http, ftp
163
     *
164
     * @return string URI string with only passed parts. Maybe incomplete as a URI.
165
     */
166
    public static function createURIString(
167
        ?string $scheme = null,
168
        ?string $authority = null,
169
        ?string $path = null,
170
        ?string $query = null,
171
        ?string $fragment = null,
172
    ): string {
173
        $uri = '';
1,603✔
174

175
        if ((string) $scheme !== '') {
1,603✔
176
            $uri .= $scheme . '://';
1,601✔
177
        }
178

179
        if ((string) $authority !== '') {
1,603✔
180
            $uri .= $authority;
1,570✔
181
        }
182

183
        if ((string) $path !== '') {
1,603✔
184
            $uri .= ! str_ends_with($uri, '/')
1,596✔
185
                ? '/' . ltrim($path, '/')
1,563✔
186
                : ltrim($path, '/');
33✔
187
        }
188

189
        if ((string) $query !== '') {
1,603✔
190
            $uri .= '?' . $query;
62✔
191
        }
192

193
        if ((string) $fragment !== '') {
1,603✔
194
            $uri .= '#' . $fragment;
8✔
195
        }
196

197
        return $uri;
1,603✔
198
    }
199

200
    /**
201
     * Used when resolving and merging paths to correctly interpret and
202
     * remove single and double dot segments from the path per
203
     * RFC 3986 Section 5.2.4
204
     *
205
     * @see http://tools.ietf.org/html/rfc3986#section-5.2.4
206
     *
207
     * @internal
208
     */
209
    public static function removeDotSegments(string $path): string
210
    {
211
        if ($path === '' || $path === '/') {
1,700✔
212
            return $path;
1,517✔
213
        }
214

215
        $output = [];
1,637✔
216

217
        $input = explode('/', $path);
1,637✔
218

219
        if ($input[0] === '') {
1,637✔
220
            unset($input[0]);
1,612✔
221
            $input = array_values($input);
1,612✔
222
        }
223

224
        // This is not a perfect representation of the
225
        // RFC, but matches most cases and is pretty
226
        // much what Guzzle uses. Should be good enough
227
        // for almost every real use case.
228
        foreach ($input as $segment) {
1,637✔
229
            if ($segment === '..') {
1,637✔
230
                array_pop($output);
16✔
231
            } elseif ($segment !== '.' && $segment !== '') {
1,635✔
232
                $output[] = $segment;
1,632✔
233
            }
234
        }
235

236
        $output = implode('/', $output);
1,637✔
237
        $output = trim($output, '/ ');
1,637✔
238

239
        // Add leading slash if necessary
240
        if (str_starts_with($path, '/')) {
1,637✔
241
            $output = '/' . $output;
1,612✔
242
        }
243

244
        // Add trailing slash if necessary
245
        if ($output !== '/' && str_ends_with($path, '/')) {
1,637✔
246
            $output .= '/';
997✔
247
        }
248

249
        return $output;
1,637✔
250
    }
251

252
    /**
253
     * Constructor.
254
     *
255
     * @param string|null $uri The URI to parse.
256
     *
257
     * @throws HTTPException
258
     *
259
     * @TODO null for param $uri should be removed.
260
     *      See https://www.php-fig.org/psr/psr-17/#26-urifactoryinterface
261
     */
262
    public function __construct(?string $uri = null)
263
    {
264
        $this->setURI($uri);
1,771✔
265
    }
266

267
    /**
268
     * If $silent == true, then will not throw exceptions and will
269
     * attempt to continue gracefully.
270
     *
271
     * @deprecated 4.4.0 Method not in PSR-7
272
     *
273
     * @return URI
274
     */
275
    public function setSilent(bool $silent = true)
276
    {
277
        $this->silent = $silent;
14✔
278

279
        return $this;
14✔
280
    }
281

282
    /**
283
     * If $raw == true, then will use parseStr() method
284
     * instead of native parse_str() function.
285
     *
286
     * Note: Method not in PSR-7
287
     *
288
     * @return URI
289
     */
290
    public function useRawQueryString(bool $raw = true)
291
    {
292
        $this->rawQueryString = $raw;
156✔
293

294
        return $this;
156✔
295
    }
296

297
    /**
298
     * Sets and overwrites any current URI information.
299
     *
300
     * @return URI
301
     *
302
     * @throws HTTPException
303
     *
304
     * @deprecated 4.4.0 This method will be private.
305
     */
306
    public function setURI(?string $uri = null)
307
    {
308
        if ($uri === null) {
1,771✔
309
            return $this;
305✔
310
        }
311

312
        $parts = parse_url($uri);
1,654✔
313

314
        if (is_array($parts)) {
1,654✔
315
            $this->applyParts($parts);
1,650✔
316

317
            return $this;
1,650✔
318
        }
319

320
        if ($this->silent) {
4✔
321
            return $this;
1✔
322
        }
323

324
        throw HTTPException::forUnableToParseURI($uri);
3✔
325
    }
326

327
    /**
328
     * Retrieve the scheme component of the URI.
329
     *
330
     * If no scheme is present, this method MUST return an empty string.
331
     *
332
     * The value returned MUST be normalized to lowercase, per RFC 3986
333
     * Section 3.1.
334
     *
335
     * The trailing ":" character is not part of the scheme and MUST NOT be
336
     * added.
337
     *
338
     * @see    https://tools.ietf.org/html/rfc3986#section-3.1
339
     *
340
     * @return string The URI scheme.
341
     */
342
    public function getScheme(): string
343
    {
344
        return $this->scheme;
1,600✔
345
    }
346

347
    /**
348
     * Retrieve the authority component of the URI.
349
     *
350
     * If no authority information is present, this method MUST return an empty
351
     * string.
352
     *
353
     * The authority syntax of the URI is:
354
     *
355
     * <pre>
356
     * [user-info@]host[:port]
357
     * </pre>
358
     *
359
     * If the port component is not set or is the standard port for the current
360
     * scheme, it SHOULD NOT be included.
361
     *
362
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
363
     *
364
     * @return string The URI authority, in "[user-info@]host[:port]" format.
365
     */
366
    public function getAuthority(bool $ignorePort = false): string
367
    {
368
        if ((string) $this->host === '') {
1,605✔
369
            return '';
81✔
370
        }
371

372
        $authority = $this->host;
1,573✔
373

374
        if ((string) $this->getUserInfo() !== '') {
1,573✔
375
            $authority = $this->getUserInfo() . '@' . $authority;
7✔
376
        }
377

378
        // Don't add port if it's a standard port for this scheme
379
        if ((int) $this->port !== 0 && ! $ignorePort && $this->port !== $this->defaultPorts[$this->scheme]) {
1,573✔
380
            $authority .= ':' . $this->port;
83✔
381
        }
382

383
        $this->showPassword = false;
1,573✔
384

385
        return $authority;
1,573✔
386
    }
387

388
    /**
389
     * Retrieve the user information component of the URI.
390
     *
391
     * If no user information is present, this method MUST return an empty
392
     * string.
393
     *
394
     * If a user is present in the URI, this will return that value;
395
     * additionally, if the password is also present, it will be appended to the
396
     * user value, with a colon (":") separating the values.
397
     *
398
     * NOTE that be default, the password, if available, will NOT be shown
399
     * as a security measure as discussed in RFC 3986, Section 7.5. If you know
400
     * the password is not a security issue, you can force it to be shown
401
     * with $this->showPassword();
402
     *
403
     * The trailing "@" character is not part of the user information and MUST
404
     * NOT be added.
405
     *
406
     * @return string|null The URI user information, in "username[:password]" format.
407
     */
408
    public function getUserInfo()
409
    {
410
        $userInfo = $this->user;
1,573✔
411

412
        if ($this->showPassword === true && (string) $this->password !== '') {
1,573✔
413
            $userInfo .= ':' . $this->password;
1✔
414
        }
415

416
        return $userInfo;
1,573✔
417
    }
418

419
    /**
420
     * Temporarily sets the URI to show a password in userInfo. Will
421
     * reset itself after the first call to authority().
422
     *
423
     * Note: Method not in PSR-7
424
     *
425
     * @return URI
426
     */
427
    public function showPassword(bool $val = true)
428
    {
429
        $this->showPassword = $val;
1✔
430

431
        return $this;
1✔
432
    }
433

434
    /**
435
     * Retrieve the host component of the URI.
436
     *
437
     * If no host is present, this method MUST return an empty string.
438
     *
439
     * The value returned MUST be normalized to lowercase, per RFC 3986
440
     * Section 3.2.2.
441
     *
442
     * @see    http://tools.ietf.org/html/rfc3986#section-3.2.2
443
     *
444
     * @return string The URI host.
445
     */
446
    public function getHost(): string
447
    {
448
        return $this->host ?? '';
1,661✔
449
    }
450

451
    /**
452
     * Retrieve the port component of the URI.
453
     *
454
     * If a port is present, and it is non-standard for the current scheme,
455
     * this method MUST return it as an integer. If the port is the standard port
456
     * used with the current scheme, this method SHOULD return null.
457
     *
458
     * If no port is present, and no scheme is present, this method MUST return
459
     * a null value.
460
     *
461
     * If no port is present, but a scheme is present, this method MAY return
462
     * the standard port for that scheme, but SHOULD return null.
463
     *
464
     * @return int|null The URI port.
465
     */
466
    public function getPort()
467
    {
468
        return $this->port;
54✔
469
    }
470

471
    /**
472
     * Retrieve the path component of the URI.
473
     *
474
     * The path can either be empty or absolute (starting with a slash) or
475
     * rootless (not starting with a slash). Implementations MUST support all
476
     * three syntaxes.
477
     *
478
     * Normally, the empty path "" and absolute path "/" are considered equal as
479
     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
480
     * do this normalization because in contexts with a trimmed base path, e.g.
481
     * the front controller, this difference becomes significant. It's the task
482
     * of the user to handle both "" and "/".
483
     *
484
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
485
     * any characters. To determine what characters to encode, please refer to
486
     * RFC 3986, Sections 2 and 3.3.
487
     *
488
     * As an example, if the value should include a slash ("/") not intended as
489
     * delimiter between path segments, that value MUST be passed in encoded
490
     * form (e.g., "%2F") to the instance.
491
     *
492
     * @see    https://tools.ietf.org/html/rfc3986#section-2
493
     * @see    https://tools.ietf.org/html/rfc3986#section-3.3
494
     *
495
     * @return string The URI path.
496
     */
497
    public function getPath(): string
498
    {
499
        return $this->path ?? '';
1,617✔
500
    }
501

502
    /**
503
     * Retrieve the query string
504
     *
505
     * @param array{except?: list<string>|string, only?: list<string>|string} $options
506
     */
507
    public function getQuery(array $options = []): string
508
    {
509
        $vars = $this->query;
1,600✔
510

511
        if (array_key_exists('except', $options)) {
1,600✔
512
            if (! is_array($options['except'])) {
2✔
513
                $options['except'] = [$options['except']];
1✔
514
            }
515

516
            foreach ($options['except'] as $var) {
2✔
517
                unset($vars[$var]);
2✔
518
            }
519
        } elseif (array_key_exists('only', $options)) {
1,599✔
520
            $temp = [];
3✔
521

522
            if (! is_array($options['only'])) {
3✔
523
                $options['only'] = [$options['only']];
1✔
524
            }
525

526
            foreach ($options['only'] as $var) {
3✔
527
                if (array_key_exists($var, $vars)) {
3✔
528
                    $temp[$var] = $vars[$var];
3✔
529
                }
530
            }
531

532
            $vars = $temp;
3✔
533
        }
534

535
        return $vars === [] ? '' : http_build_query($vars);
1,600✔
536
    }
537

538
    /**
539
     * Retrieve a URI fragment
540
     */
541
    public function getFragment(): string
542
    {
543
        return $this->fragment ?? '';
1,595✔
544
    }
545

546
    /**
547
     * Returns the segments of the path as an array.
548
     *
549
     * @return array<int, string>
550
     */
551
    public function getSegments(): array
552
    {
553
        return $this->segments;
39✔
554
    }
555

556
    /**
557
     * Returns the value of a specific segment of the URI path.
558
     * Allows to get only existing segments or the next one.
559
     *
560
     * @param int    $number  Segment number starting at 1
561
     * @param string $default Default value
562
     *
563
     * @return string The value of the segment. If you specify the last +1
564
     *                segment, the $default value. If you specify the last +2
565
     *                or more throws HTTPException.
566
     */
567
    public function getSegment(int $number, string $default = ''): string
568
    {
569
        if ($number < 1) {
18✔
570
            throw HTTPException::forURISegmentOutOfRange($number);
1✔
571
        }
572

573
        if ($number > count($this->segments) + 1 && ! $this->silent) {
17✔
574
            throw HTTPException::forURISegmentOutOfRange($number);
4✔
575
        }
576

577
        // The segment should treat the array as 1-based for the user
578
        // but we still have to deal with a zero-based array.
579
        $number--;
13✔
580

581
        return $this->segments[$number] ?? $default;
13✔
582
    }
583

584
    /**
585
     * Set the value of a specific segment of the URI path.
586
     * Allows to set only existing segments or add new one.
587
     *
588
     * Note: Method not in PSR-7
589
     *
590
     * @param int        $number Segment number starting at 1
591
     * @param int|string $value
592
     *
593
     * @return $this
594
     */
595
    public function setSegment(int $number, $value)
596
    {
597
        if ($number < 1) {
21✔
598
            throw HTTPException::forURISegmentOutOfRange($number);
1✔
599
        }
600

601
        if ($number > count($this->segments) + 1) {
20✔
602
            if ($this->silent) {
5✔
603
                return $this;
2✔
604
            }
605

606
            throw HTTPException::forURISegmentOutOfRange($number);
3✔
607
        }
608

609
        // The segment should treat the array as 1-based for the user
610
        // but we still have to deal with a zero-based array.
611
        $number--;
16✔
612

613
        $this->segments[$number] = $value;
16✔
614

615
        return $this->refreshPath();
16✔
616
    }
617

618
    /**
619
     * Returns the total number of segments.
620
     *
621
     * Note: Method not in PSR-7
622
     */
623
    public function getTotalSegments(): int
624
    {
625
        return count($this->segments);
35✔
626
    }
627

628
    /**
629
     * Formats the URI as a string.
630
     *
631
     * Warning: For backwards-compatability this method
632
     * assumes URIs with the same host as baseURL should
633
     * be relative to the project's configuration.
634
     * This aspect of __toString() is deprecated and should be avoided.
635
     */
636
    public function __toString(): string
637
    {
638
        $path   = $this->getPath();
1,507✔
639
        $scheme = $this->getScheme();
1,507✔
640

641
        // If the hosts matches then assume this should be relative to baseURL
642
        [$scheme, $path] = $this->changeSchemeAndPath($scheme, $path);
1,507✔
643

644
        return static::createURIString(
1,507✔
645
            $scheme,
1,507✔
646
            $this->getAuthority(),
1,507✔
647
            $path, // Absolute URIs should use a "/" for an empty path
1,507✔
648
            $this->getQuery(),
1,507✔
649
            $this->getFragment(),
1,507✔
650
        );
1,507✔
651
    }
652

653
    /**
654
     * Change the path (and scheme) assuming URIs with the same host as baseURL
655
     * should be relative to the project's configuration.
656
     *
657
     * @return array{string, string}
658
     *
659
     * @deprecated This method will be deleted.
660
     */
661
    private function changeSchemeAndPath(string $scheme, string $path): array
662
    {
663
        // Check if this is an internal URI
664
        $config  = config(App::class);
1,507✔
665
        $baseUri = new self($config->baseURL);
1,507✔
666

667
        if (
668
            str_starts_with($this->getScheme(), 'http')
1,507✔
669
            && $this->getHost() === $baseUri->getHost()
1,507✔
670
        ) {
671
            // Check for additional segments
672
            $basePath = trim($baseUri->getPath(), '/') . '/';
1,470✔
673
            $trimPath = ltrim($path, '/');
1,470✔
674

675
            if ($basePath !== '/' && ! str_starts_with($trimPath, $basePath)) {
1,470✔
UNCOV
676
                $path = $basePath . $trimPath;
×
677
            }
678

679
            // Check for forced HTTPS
680
            if ($config->forceGlobalSecureRequests) {
1,470✔
681
                $scheme = 'https';
5✔
682
            }
683
        }
684

685
        return [$scheme, $path];
1,507✔
686
    }
687

688
    /**
689
     * Parses the given string and saves the appropriate authority pieces.
690
     *
691
     * Note: Method not in PSR-7
692
     *
693
     * @return $this
694
     */
695
    public function setAuthority(string $str)
696
    {
697
        $parts = parse_url($str);
82✔
698

699
        if (! isset($parts['path'])) {
82✔
700
            $parts['path'] = $this->getPath();
1✔
701
        }
702

703
        if (! isset($parts['host']) && $parts['path'] !== '') {
82✔
704
            $parts['host'] = $parts['path'];
51✔
705
            unset($parts['path']);
51✔
706
        }
707

708
        $this->applyParts($parts);
82✔
709

710
        return $this;
82✔
711
    }
712

713
    /**
714
     * Sets the scheme for this URI.
715
     *
716
     * Because of the large number of valid schemes we cannot limit this
717
     * to only http or https.
718
     *
719
     * @see https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
720
     *
721
     * @return $this
722
     *
723
     * @deprecated 4.4.0 Use `withScheme()` instead.
724
     */
725
    public function setScheme(string $str)
726
    {
727
        $str          = strtolower($str);
1,651✔
728
        $this->scheme = preg_replace('#:(//)?$#', '', $str);
1,651✔
729

730
        return $this;
1,651✔
731
    }
732

733
    /**
734
     * Return an instance with the specified scheme.
735
     *
736
     * This method MUST retain the state of the current instance, and return
737
     * an instance that contains the specified scheme.
738
     *
739
     * Implementations MUST support the schemes "http" and "https" case
740
     * insensitively, and MAY accommodate other schemes if required.
741
     *
742
     * An empty scheme is equivalent to removing the scheme.
743
     *
744
     * @param string $scheme The scheme to use with the new instance.
745
     *
746
     * @return static A new instance with the specified scheme.
747
     *
748
     * @throws InvalidArgumentException for invalid or unsupported schemes.
749
     */
750
    public function withScheme(string $scheme)
751
    {
752
        $uri = clone $this;
7✔
753

754
        $scheme = strtolower($scheme);
7✔
755

756
        $uri->scheme = preg_replace('#:(//)?$#', '', $scheme);
7✔
757

758
        return $uri;
7✔
759
    }
760

761
    /**
762
     * Sets the userInfo/Authority portion of the URI.
763
     *
764
     * @param string $user The user's username
765
     * @param string $pass The user's password
766
     *
767
     * @return $this
768
     *
769
     * @TODO PSR-7: Should be `withUserInfo($user, $password = null)`.
770
     */
771
    public function setUserInfo(string $user, string $pass)
772
    {
773
        $this->user     = trim($user);
2✔
774
        $this->password = trim($pass);
2✔
775

776
        return $this;
2✔
777
    }
778

779
    /**
780
     * Sets the host name to use.
781
     *
782
     * @return $this
783
     *
784
     * @TODO PSR-7: Should be `withHost($host)`.
785
     */
786
    public function setHost(string $str)
787
    {
788
        $this->host = trim($str);
106✔
789

790
        return $this;
106✔
791
    }
792

793
    /**
794
     * Sets the port portion of the URI.
795
     *
796
     * @return $this
797
     *
798
     * @TODO PSR-7: Should be `withPort($port)`.
799
     */
800
    public function setPort(?int $port = null)
801
    {
802
        if ($port === null) {
5✔
UNCOV
803
            return $this;
×
804
        }
805

806
        if ($port > 0 && $port <= 65535) {
5✔
807
            $this->port = $port;
1✔
808

809
            return $this;
1✔
810
        }
811

812
        if ($this->silent) {
4✔
813
            return $this;
1✔
814
        }
815

816
        throw HTTPException::forInvalidPort($port);
3✔
817
    }
818

819
    /**
820
     * Sets the path portion of the URI.
821
     *
822
     * @return $this
823
     *
824
     * @TODO PSR-7: Should be `withPath($port)`.
825
     */
826
    public function setPath(string $path)
827
    {
828
        $this->path = $this->filterPath($path);
101✔
829

830
        $tempPath = trim($this->path, '/');
101✔
831

832
        $this->segments = ($tempPath === '') ? [] : explode('/', $tempPath);
101✔
833

834
        return $this;
101✔
835
    }
836

837
    /**
838
     * Sets the current baseURL.
839
     *
840
     * @interal
841
     *
842
     * @deprecated Use SiteURI instead.
843
     */
844
    public function setBaseURL(string $baseURL): void
845
    {
UNCOV
846
        $this->baseURL = $baseURL;
×
847
    }
848

849
    /**
850
     * Returns the current baseURL.
851
     *
852
     * @interal
853
     *
854
     * @deprecated Use SiteURI instead.
855
     */
856
    public function getBaseURL(): string
857
    {
UNCOV
858
        if ($this->baseURL === null) {
×
UNCOV
859
            throw new BadMethodCallException('The $baseURL is not set.');
×
860
        }
861

UNCOV
862
        return $this->baseURL;
×
863
    }
864

865
    /**
866
     * Sets the path portion of the URI based on segments.
867
     *
868
     * @return $this
869
     *
870
     * @deprecated This method will be private.
871
     */
872
    public function refreshPath()
873
    {
874
        $this->path = $this->filterPath(implode('/', $this->segments));
11✔
875

876
        $tempPath = trim($this->path, '/');
11✔
877

878
        $this->segments = $tempPath === '' ? [] : explode('/', $tempPath);
11✔
879

880
        return $this;
11✔
881
    }
882

883
    /**
884
     * Sets the query portion of the URI, while attempting
885
     * to clean the various parts of the query keys and values.
886
     *
887
     * @return $this
888
     *
889
     * @TODO PSR-7: Should be `withQuery($query)`.
890
     */
891
    public function setQuery(string $query)
892
    {
893
        if (str_contains($query, '#')) {
234✔
894
            if ($this->silent) {
2✔
895
                return $this;
1✔
896
            }
897

898
            throw HTTPException::forMalformedQueryString();
1✔
899
        }
900

901
        // Can't have leading ?
902
        if ($query !== '' && str_starts_with($query, '?')) {
232✔
903
            $query = substr($query, 1);
2✔
904
        }
905

906
        if ($this->rawQueryString) {
232✔
907
            $this->query = $this->parseStr($query);
2✔
908
        } else {
909
            parse_str($query, $this->query);
230✔
910
        }
911

912
        return $this;
232✔
913
    }
914

915
    /**
916
     * A convenience method to pass an array of items in as the Query
917
     * portion of the URI.
918
     *
919
     * @return URI
920
     *
921
     * @TODO: PSR-7: Should be `withQueryParams(array $query)`
922
     */
923
    public function setQueryArray(array $query)
924
    {
925
        $query = http_build_query($query);
16✔
926

927
        return $this->setQuery($query);
16✔
928
    }
929

930
    /**
931
     * Adds a single new element to the query vars.
932
     *
933
     * Note: Method not in PSR-7
934
     *
935
     * @param int|string|null $value
936
     *
937
     * @return $this
938
     */
939
    public function addQuery(string $key, $value = null)
940
    {
941
        $this->query[$key] = $value;
40✔
942

943
        return $this;
40✔
944
    }
945

946
    /**
947
     * Removes one or more query vars from the URI.
948
     *
949
     * Note: Method not in PSR-7
950
     *
951
     * @param string ...$params
952
     *
953
     * @return $this
954
     */
955
    public function stripQuery(...$params)
956
    {
957
        foreach ($params as $param) {
1✔
958
            unset($this->query[$param]);
1✔
959
        }
960

961
        return $this;
1✔
962
    }
963

964
    /**
965
     * Filters the query variables so that only the keys passed in
966
     * are kept. The rest are removed from the object.
967
     *
968
     * Note: Method not in PSR-7
969
     *
970
     * @param string ...$params
971
     *
972
     * @return $this
973
     */
974
    public function keepQuery(...$params)
975
    {
976
        $temp = [];
1✔
977

978
        foreach ($this->query as $key => $value) {
1✔
979
            if (! in_array($key, $params, true)) {
1✔
980
                continue;
1✔
981
            }
982

983
            $temp[$key] = $value;
1✔
984
        }
985

986
        $this->query = $temp;
1✔
987

988
        return $this;
1✔
989
    }
990

991
    /**
992
     * Sets the fragment portion of the URI.
993
     *
994
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
995
     *
996
     * @return $this
997
     *
998
     * @TODO PSR-7: Should be `withFragment($fragment)`.
999
     */
1000
    public function setFragment(string $string)
1001
    {
1002
        $this->fragment = trim($string, '# ');
176✔
1003

1004
        return $this;
176✔
1005
    }
1006

1007
    /**
1008
     * Encodes any dangerous characters, and removes dot segments.
1009
     * While dot segments have valid uses according to the spec,
1010
     * this URI class does not allow them.
1011
     */
1012
    protected function filterPath(?string $path = null): string
1013
    {
1014
        $orig = $path;
1,659✔
1015

1016
        // Decode/normalize percent-encoded chars so
1017
        // we can always have matching for Routes, etc.
1018
        $path = urldecode($path);
1,659✔
1019

1020
        // Remove dot segments
1021
        $path = self::removeDotSegments($path);
1,659✔
1022

1023
        // Fix up some leading slash edge cases...
1024
        if (str_starts_with($orig, './')) {
1,659✔
1025
            $path = '/' . $path;
1✔
1026
        }
1027
        if (str_starts_with($orig, '../')) {
1,659✔
1028
            $path = '/' . $path;
1✔
1029
        }
1030

1031
        // Encode characters
1032
        $path = preg_replace_callback(
1,659✔
1033
            '/(?:[^' . static::CHAR_UNRESERVED . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
1,659✔
1034
            static fn (array $matches): string => rawurlencode($matches[0]),
1,659✔
1035
            $path,
1,659✔
1036
        );
1,659✔
1037

1038
        return $path;
1,659✔
1039
    }
1040

1041
    /**
1042
     * Saves our parts from a parse_url call.
1043
     *
1044
     * @param array{
1045
     *  host?: string,
1046
     *  user?: string,
1047
     *  path?: string,
1048
     *  query?: string,
1049
     *  fragment?: string,
1050
     *  scheme?: string,
1051
     *  port?: int,
1052
     *  pass?: string,
1053
     * } $parts
1054
     *
1055
     * @return void
1056
     */
1057
    protected function applyParts(array $parts)
1058
    {
1059
        if (isset($parts['host']) && $parts['host'] !== '') {
1,651✔
1060
            $this->host = $parts['host'];
1,621✔
1061
        }
1062

1063
        if (isset($parts['user']) && $parts['user'] !== '') {
1,651✔
1064
            $this->user = $parts['user'];
5✔
1065
        }
1066

1067
        if (isset($parts['path']) && $parts['path'] !== '') {
1,651✔
1068
            $this->path = $this->filterPath($parts['path']);
1,649✔
1069
        }
1070

1071
        if (isset($parts['query']) && $parts['query'] !== '') {
1,651✔
1072
            $this->setQuery($parts['query']);
10✔
1073
        }
1074

1075
        if (isset($parts['fragment']) && $parts['fragment'] !== '') {
1,651✔
1076
            $this->fragment = $parts['fragment'];
4✔
1077
        }
1078

1079
        if (isset($parts['scheme'])) {
1,651✔
1080
            $this->setScheme(rtrim($parts['scheme'], ':/'));
1,620✔
1081
        } else {
1082
            $this->setScheme('http');
84✔
1083
        }
1084

1085
        if (isset($parts['port'])) {
1,651✔
1086
            // Valid port numbers are enforced by earlier parse_url or setPort()
1087
            $this->port = $parts['port'];
92✔
1088
        }
1089

1090
        if (isset($parts['pass'])) {
1,651✔
1091
            $this->password = $parts['pass'];
2✔
1092
        }
1093

1094
        if (isset($parts['path']) && $parts['path'] !== '') {
1,651✔
1095
            $tempPath = trim($parts['path'], '/');
1,649✔
1096

1097
            $this->segments = $tempPath === '' ? [] : explode('/', $tempPath);
1,649✔
1098
        }
1099
    }
1100

1101
    /**
1102
     * Combines one URI string with this one based on the rules set out in
1103
     * RFC 3986 Section 2
1104
     *
1105
     * @see http://tools.ietf.org/html/rfc3986#section-5.2
1106
     *
1107
     * @return URI
1108
     */
1109
    public function resolveRelativeURI(string $uri)
1110
    {
1111
        /*
1112
         * NOTE: We don't use removeDotSegments in this
1113
         * algorithm since it's already done by this line!
1114
         */
1115
        $relative = new self();
81✔
1116
        $relative->setURI($uri);
81✔
1117

1118
        if ($relative->getScheme() === $this->getScheme()) {
81✔
1119
            $relative->setScheme('');
73✔
1120
        }
1121

1122
        $transformed = clone $relative;
81✔
1123

1124
        // 5.2.2 Transform References in a non-strict method (no scheme)
1125
        if ($relative->getAuthority() !== '') {
81✔
1126
            $transformed
2✔
1127
                ->setAuthority($relative->getAuthority())
2✔
1128
                ->setPath($relative->getPath())
2✔
1129
                ->setQuery($relative->getQuery());
2✔
1130
        } else {
1131
            if ($relative->getPath() === '') {
79✔
1132
                $transformed->setPath($this->getPath());
4✔
1133

1134
                if ($relative->getQuery() !== '') {
4✔
1135
                    $transformed->setQuery($relative->getQuery());
2✔
1136
                } else {
1137
                    $transformed->setQuery($this->getQuery());
2✔
1138
                }
1139
            } else {
1140
                if (str_starts_with($relative->getPath(), '/')) {
75✔
1141
                    $transformed->setPath($relative->getPath());
34✔
1142
                } else {
1143
                    $transformed->setPath($this->mergePaths($this, $relative));
41✔
1144
                }
1145

1146
                $transformed->setQuery($relative->getQuery());
75✔
1147
            }
1148

1149
            $transformed->setAuthority($this->getAuthority());
79✔
1150
        }
1151

1152
        $transformed->setScheme($this->getScheme());
81✔
1153

1154
        $transformed->setFragment($relative->getFragment());
81✔
1155

1156
        return $transformed;
81✔
1157
    }
1158

1159
    /**
1160
     * Given 2 paths, will merge them according to rules set out in RFC 2986,
1161
     * Section 5.2
1162
     *
1163
     * @see http://tools.ietf.org/html/rfc3986#section-5.2.3
1164
     */
1165
    protected function mergePaths(self $base, self $reference): string
1166
    {
1167
        if ($base->getAuthority() !== '' && $base->getPath() === '') {
41✔
1168
            return '/' . ltrim($reference->getPath(), '/ ');
1✔
1169
        }
1170

1171
        $path = explode('/', $base->getPath());
40✔
1172

1173
        if ($path[0] === '') {
40✔
1174
            unset($path[0]);
40✔
1175
        }
1176

1177
        array_pop($path);
40✔
1178
        $path[] = $reference->getPath();
40✔
1179

1180
        return implode('/', $path);
40✔
1181
    }
1182

1183
    /**
1184
     * This is equivalent to the native PHP parse_str() function.
1185
     * This version allows the dot to be used as a key of the query string.
1186
     *
1187
     * @return array<string, string>
1188
     */
1189
    protected function parseStr(string $query): array
1190
    {
1191
        $return = [];
2✔
1192
        $query  = explode('&', $query);
2✔
1193

1194
        $params = array_map(static fn (string $chunk): ?string => preg_replace_callback(
2✔
1195
            '/^(?<key>[^&=]+?)(?:\[[^&=]*\])?=(?<value>[^&=]+)/',
2✔
1196
            static fn (array $match): string => str_replace($match['key'], bin2hex($match['key']), $match[0]),
2✔
1197
            urldecode($chunk),
2✔
1198
        ), $query);
2✔
1199

1200
        $params = implode('&', $params);
2✔
1201
        parse_str($params, $result);
2✔
1202

1203
        foreach ($result as $key => $value) {
2✔
1204
            // Array key might be int
1205
            $return[hex2bin((string) $key)] = $value;
2✔
1206
        }
1207

1208
        return $return;
2✔
1209
    }
1210
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc