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

codeigniter4 / CodeIgniter4 / 23715552580

29 Mar 2026 06:05PM UTC coverage: 86.546% (-0.009%) from 86.555%
23715552580

Pull #10000

github

web-flow
Merge 8b5113776 into 51deee54e
Pull Request #10000: refactor: remove deprecations in HTTP

14 of 15 new or added lines in 5 files covered. (93.33%)

6 existing lines in 1 file now uncovered.

22675 of 26200 relevant lines covered (86.55%)

219.83 hits per line

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

96.44
/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\InvalidArgumentException;
17
use CodeIgniter\HTTP\Exceptions\HTTPException;
18
use Config\App;
19
use SensitiveParameter;
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
     * List of URI segments.
41
     *
42
     * Starts at 1 instead of 0
43
     *
44
     * @var array<int, string>
45
     */
46
    protected $segments = [];
47

48
    /**
49
     * The URI Scheme.
50
     *
51
     * @var string
52
     */
53
    protected $scheme = 'http';
54

55
    /**
56
     * URI User Info
57
     *
58
     * @var string|null
59
     */
60
    protected $user;
61

62
    /**
63
     * URI User Password
64
     *
65
     * @var string|null
66
     */
67
    protected $password;
68

69
    /**
70
     * URI Host
71
     *
72
     * @var string|null
73
     */
74
    protected $host;
75

76
    /**
77
     * URI Port
78
     *
79
     * @var int|null
80
     */
81
    protected $port;
82

83
    /**
84
     * URI path.
85
     *
86
     * @var string|null
87
     */
88
    protected $path;
89

90
    /**
91
     * The name of any fragment.
92
     *
93
     * @var string
94
     */
95
    protected $fragment = '';
96

97
    /**
98
     * The query string.
99
     *
100
     * @var array<string, string>
101
     */
102
    protected $query = [];
103

104
    /**
105
     * Default schemes/ports.
106
     *
107
     * @var array{
108
     *  http: int,
109
     *  https: int,
110
     *  ftp: int,
111
     *  sftp: int,
112
     * }
113
     */
114
    protected $defaultPorts = [
115
        'http'  => 80,
116
        'https' => 443,
117
        'ftp'   => 21,
118
        'sftp'  => 22,
119
    ];
120

121
    /**
122
     * Whether passwords should be shown in userInfo/authority calls.
123
     * Default to false because URIs often show up in logs
124
     *
125
     * @var bool
126
     */
127
    protected $showPassword = false;
128

129
    /**
130
     * If true, will continue instead of throwing exceptions.
131
     *
132
     * @var bool
133
     */
134
    protected $silent = false;
135

136
    /**
137
     * If true, will use raw query string.
138
     *
139
     * @var bool
140
     */
141
    protected $rawQueryString = false;
142

143
    /**
144
     * Builds a representation of the string from the component parts.
145
     *
146
     * @param string|null $scheme URI scheme. E.g., http, ftp
147
     *
148
     * @return string URI string with only passed parts. Maybe incomplete as a URI.
149
     */
150
    public static function createURIString(
151
        ?string $scheme = null,
152
        ?string $authority = null,
153
        ?string $path = null,
154
        ?string $query = null,
155
        ?string $fragment = null,
156
    ): string {
157
        $uri = '';
1,865✔
158

159
        if ((string) $scheme !== '') {
1,865✔
160
            $uri .= $scheme . '://';
1,863✔
161
        }
162

163
        if ((string) $authority !== '') {
1,865✔
164
            $uri .= $authority;
1,814✔
165
        }
166

167
        if ((string) $path !== '') {
1,865✔
168
            $uri .= str_ends_with($uri, '/')
1,858✔
169
                ? ltrim($path, '/')
51✔
170
                : '/' . ltrim($path, '/');
1,807✔
171
        }
172

173
        if ((string) $query !== '') {
1,865✔
174
            $uri .= '?' . $query;
79✔
175
        }
176

177
        if ((string) $fragment !== '') {
1,865✔
178
            $uri .= '#' . $fragment;
8✔
179
        }
180

181
        return $uri;
1,865✔
182
    }
183

184
    /**
185
     * Used when resolving and merging paths to correctly interpret and
186
     * remove single and double dot segments from the path per
187
     * RFC 3986 Section 5.2.4
188
     *
189
     * @see http://tools.ietf.org/html/rfc3986#section-5.2.4
190
     *
191
     * @internal
192
     */
193
    public static function removeDotSegments(string $path): string
194
    {
195
        if ($path === '' || $path === '/') {
1,970✔
196
            return $path;
1,760✔
197
        }
198

199
        $output = [];
1,905✔
200

201
        $input = explode('/', $path);
1,905✔
202

203
        if ($input[0] === '') {
1,905✔
204
            unset($input[0]);
1,870✔
205
            $input = array_values($input);
1,870✔
206
        }
207

208
        // This is not a perfect representation of the
209
        // RFC, but matches most cases and is pretty
210
        // much what Guzzle uses. Should be good enough
211
        // for almost every real use case.
212
        foreach ($input as $segment) {
1,905✔
213
            if ($segment === '..') {
1,905✔
214
                array_pop($output);
16✔
215
            } elseif ($segment !== '.' && $segment !== '') {
1,903✔
216
                $output[] = $segment;
1,900✔
217
            }
218
        }
219

220
        $output = implode('/', $output);
1,905✔
221
        $output = trim($output, '/ ');
1,905✔
222

223
        // Add leading slash if necessary
224
        if (str_starts_with($path, '/')) {
1,905✔
225
            $output = '/' . $output;
1,870✔
226
        }
227

228
        // Add trailing slash if necessary
229
        if ($output !== '/' && str_ends_with($path, '/')) {
1,905✔
230
            $output .= '/';
1,167✔
231
        }
232

233
        return $output;
1,905✔
234
    }
235

236
    /**
237
     * Constructor.
238
     *
239
     * @param string|null $uri The URI to parse.
240
     *
241
     * @throws HTTPException
242
     *
243
     * @TODO null for param $uri should be removed.
244
     *      See https://www.php-fig.org/psr/psr-17/#26-urifactoryinterface
245
     */
246
    public function __construct(?string $uri = null)
247
    {
248
        $this->setUri($uri);
2,040✔
249
    }
250

251
    /**
252
     * If $silent == true, then will not throw exceptions and will
253
     * attempt to continue gracefully.
254
     *
255
     * @deprecated 4.4.0 Method not in PSR-7
256
     *
257
     * @return URI
258
     */
259
    public function setSilent(bool $silent = true)
260
    {
NEW
261
        @trigger_error(sprintf('The %s method is deprecated and will be removed in CodeIgniter 5.0.', __METHOD__), E_USER_DEPRECATED);
×
262

UNCOV
263
        $this->silent = $silent;
×
264

UNCOV
265
        return $this;
×
266
    }
267

268
    /**
269
     * If $raw == true, then will use parseStr() method
270
     * instead of native parse_str() function.
271
     *
272
     * Note: Method not in PSR-7
273
     *
274
     * @return URI
275
     */
276
    public function useRawQueryString(bool $raw = true)
277
    {
278
        $this->rawQueryString = $raw;
187✔
279

280
        return $this;
187✔
281
    }
282

283
    /**
284
     * Sets and overwrites any current URI information.
285
     *
286
     * @throws HTTPException
287
     */
288
    private function setUri(?string $uri = null): self
289
    {
290
        if ($uri === null) {
2,040✔
291
            return $this;
318✔
292
        }
293

294
        $parts = parse_url($uri);
1,911✔
295

296
        if (is_array($parts)) {
1,911✔
297
            $this->applyParts($parts);
1,908✔
298

299
            return $this;
1,908✔
300
        }
301

302
        if ($this->silent) {
3✔
UNCOV
303
            return $this;
×
304
        }
305

306
        throw HTTPException::forUnableToParseURI($uri);
3✔
307
    }
308

309
    /**
310
     * Retrieve the scheme component of the URI.
311
     *
312
     * If no scheme is present, this method MUST return an empty string.
313
     *
314
     * The value returned MUST be normalized to lowercase, per RFC 3986
315
     * Section 3.1.
316
     *
317
     * The trailing ":" character is not part of the scheme and MUST NOT be
318
     * added.
319
     *
320
     * @see https://tools.ietf.org/html/rfc3986#section-3.1
321
     */
322
    public function getScheme(): string
323
    {
324
        return $this->scheme;
1,915✔
325
    }
326

327
    /**
328
     * Retrieve the authority component of the URI.
329
     *
330
     * If no authority information is present, this method MUST return an empty
331
     * string.
332
     *
333
     * The authority syntax of the URI is:
334
     *
335
     * <pre>
336
     * [user-info@]host[:port]
337
     * </pre>
338
     *
339
     * If the port component is not set or is the standard port for the current
340
     * scheme, it SHOULD NOT be included.
341
     *
342
     * @see https://tools.ietf.org/html/rfc3986#section-3.2
343
     *
344
     * @return string The URI authority, in "[user-info@]host[:port]" format.
345
     */
346
    public function getAuthority(bool $ignorePort = false): string
347
    {
348
        if ((string) $this->host === '') {
1,871✔
349
            return '';
101✔
350
        }
351

352
        $authority = $this->host;
1,821✔
353

354
        if ((string) $this->getUserInfo() !== '') {
1,821✔
355
            $authority = $this->getUserInfo() . '@' . $authority;
7✔
356
        }
357

358
        // Don't add port if it's a standard port for this scheme
359
        if ((int) $this->port !== 0 && ! $ignorePort && $this->port !== ($this->defaultPorts[$this->scheme] ?? null)) {
1,821✔
360
            $authority .= ':' . $this->port;
200✔
361
        }
362

363
        $this->showPassword = false;
1,821✔
364

365
        return $authority;
1,821✔
366
    }
367

368
    /**
369
     * Retrieve the user information component of the URI.
370
     *
371
     * If no user information is present, this method MUST return an empty
372
     * string.
373
     *
374
     * If a user is present in the URI, this will return that value;
375
     * additionally, if the password is also present, it will be appended to the
376
     * user value, with a colon (":") separating the values.
377
     *
378
     * NOTE that be default, the password, if available, will NOT be shown
379
     * as a security measure as discussed in RFC 3986, Section 7.5. If you know
380
     * the password is not a security issue, you can force it to be shown
381
     * with $this->showPassword();
382
     *
383
     * The trailing "@" character is not part of the user information and MUST
384
     * NOT be added.
385
     *
386
     * @return string|null The URI user information, in "username[:password]" format.
387
     */
388
    public function getUserInfo()
389
    {
390
        $userInfo = $this->user;
1,821✔
391

392
        if ($this->showPassword === true && (string) $this->password !== '') {
1,821✔
393
            $userInfo .= ':' . $this->password;
1✔
394
        }
395

396
        return $userInfo;
1,821✔
397
    }
398

399
    /**
400
     * Temporarily sets the URI to show a password in userInfo. Will
401
     * reset itself after the first call to authority().
402
     *
403
     * Note: Method not in PSR-7
404
     *
405
     * @return URI
406
     */
407
    public function showPassword(bool $val = true)
408
    {
409
        $this->showPassword = $val;
1✔
410

411
        return $this;
1✔
412
    }
413

414
    /**
415
     * Retrieve the host component of the URI.
416
     *
417
     * If no host is present, this method MUST return an empty string.
418
     *
419
     * The value returned MUST be normalized to lowercase, per RFC 3986
420
     * Section 3.2.2.
421
     *
422
     * @see    http://tools.ietf.org/html/rfc3986#section-3.2.2
423
     *
424
     * @return string The URI host.
425
     */
426
    public function getHost(): string
427
    {
428
        return $this->host ?? '';
1,932✔
429
    }
430

431
    /**
432
     * Retrieve the port component of the URI.
433
     *
434
     * If a port is present, and it is non-standard for the current scheme,
435
     * this method MUST return it as an integer. If the port is the standard port
436
     * used with the current scheme, this method SHOULD return null.
437
     *
438
     * If no port is present, and no scheme is present, this method MUST return
439
     * a null value.
440
     *
441
     * If no port is present, but a scheme is present, this method MAY return
442
     * the standard port for that scheme, but SHOULD return null.
443
     *
444
     * @return int|null The URI port.
445
     */
446
    public function getPort()
447
    {
448
        return $this->port;
56✔
449
    }
450

451
    /**
452
     * Retrieve the path component of the URI.
453
     *
454
     * The path can either be empty or absolute (starting with a slash) or
455
     * rootless (not starting with a slash). Implementations MUST support all
456
     * three syntaxes.
457
     *
458
     * Normally, the empty path "" and absolute path "/" are considered equal as
459
     * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically
460
     * do this normalization because in contexts with a trimmed base path, e.g.
461
     * the front controller, this difference becomes significant. It's the task
462
     * of the user to handle both "" and "/".
463
     *
464
     * The value returned MUST be percent-encoded, but MUST NOT double-encode
465
     * any characters. To determine what characters to encode, please refer to
466
     * RFC 3986, Sections 2 and 3.3.
467
     *
468
     * As an example, if the value should include a slash ("/") not intended as
469
     * delimiter between path segments, that value MUST be passed in encoded
470
     * form (e.g., "%2F") to the instance.
471
     *
472
     * @see    https://tools.ietf.org/html/rfc3986#section-2
473
     * @see    https://tools.ietf.org/html/rfc3986#section-3.3
474
     *
475
     * @return string The URI path.
476
     */
477
    public function getPath(): string
478
    {
479
        return $this->path ?? '';
1,879✔
480
    }
481

482
    /**
483
     * Retrieve the query string
484
     *
485
     * @param array{except?: list<string>|string, only?: list<string>|string} $options
486
     */
487
    public function getQuery(array $options = []): string
488
    {
489
        $vars = $this->query;
1,860✔
490

491
        if (array_key_exists('except', $options)) {
1,860✔
492
            if (! is_array($options['except'])) {
2✔
493
                $options['except'] = [$options['except']];
1✔
494
            }
495

496
            foreach ($options['except'] as $var) {
2✔
497
                unset($vars[$var]);
2✔
498
            }
499
        } elseif (array_key_exists('only', $options)) {
1,859✔
500
            $temp = [];
3✔
501

502
            if (! is_array($options['only'])) {
3✔
503
                $options['only'] = [$options['only']];
1✔
504
            }
505

506
            foreach ($options['only'] as $var) {
3✔
507
                if (array_key_exists($var, $vars)) {
3✔
508
                    $temp[$var] = $vars[$var];
3✔
509
                }
510
            }
511

512
            $vars = $temp;
3✔
513
        }
514

515
        return $vars === [] ? '' : http_build_query($vars);
1,860✔
516
    }
517

518
    /**
519
     * Retrieve a URI fragment
520
     */
521
    public function getFragment(): string
522
    {
523
        return $this->fragment ?? '';
1,856✔
524
    }
525

526
    /**
527
     * Returns the segments of the path as an array.
528
     *
529
     * @return array<int, string>
530
     */
531
    public function getSegments(): array
532
    {
533
        return $this->segments;
35✔
534
    }
535

536
    /**
537
     * Returns the value of a specific segment of the URI path.
538
     * Allows to get only existing segments or the next one.
539
     *
540
     * @param int    $number  Segment number starting at 1
541
     * @param string $default Default value
542
     *
543
     * @return string The value of the segment. If you specify the last +1
544
     *                segment, the $default value. If you specify the last +2
545
     *                or more throws HTTPException.
546
     */
547
    public function getSegment(int $number, string $default = ''): string
548
    {
549
        if ($number < 1) {
14✔
550
            throw HTTPException::forURISegmentOutOfRange($number);
1✔
551
        }
552

553
        if ($number > count($this->segments) + 1 && ! $this->silent) {
13✔
554
            throw HTTPException::forURISegmentOutOfRange($number);
4✔
555
        }
556

557
        // The segment should treat the array as 1-based for the user
558
        // but we still have to deal with a zero-based array.
559
        $number--;
9✔
560

561
        return $this->segments[$number] ?? $default;
9✔
562
    }
563

564
    /**
565
     * Set the value of a specific segment of the URI path.
566
     * Allows to set only existing segments or add new one.
567
     *
568
     * Note: Method not in PSR-7
569
     *
570
     * @param int        $number Segment number starting at 1
571
     * @param int|string $value
572
     *
573
     * @return $this
574
     */
575
    public function setSegment(int $number, $value)
576
    {
577
        if ($number < 1) {
19✔
578
            throw HTTPException::forURISegmentOutOfRange($number);
1✔
579
        }
580

581
        if ($number > count($this->segments) + 1) {
18✔
582
            if ($this->silent) {
3✔
UNCOV
583
                return $this;
×
584
            }
585

586
            throw HTTPException::forURISegmentOutOfRange($number);
3✔
587
        }
588

589
        // The segment should treat the array as 1-based for the user
590
        // but we still have to deal with a zero-based array.
591
        $number--;
16✔
592

593
        $this->segments[$number] = $value;
16✔
594

595
        return $this->refreshPath();
16✔
596
    }
597

598
    /**
599
     * Returns the total number of segments.
600
     *
601
     * Note: Method not in PSR-7
602
     */
603
    public function getTotalSegments(): int
604
    {
605
        return count($this->segments);
33✔
606
    }
607

608
    /**
609
     * Formats the URI as a string.
610
     *
611
     * Warning: For backwards-compatibility this method
612
     * assumes URIs with the same host as baseURL should
613
     * be relative to the project's configuration.
614
     * This aspect of __toString() is deprecated and should be avoided.
615
     */
616
    public function __toString(): string
617
    {
618
        $path   = $this->getPath();
1,748✔
619
        $scheme = $this->getScheme();
1,748✔
620

621
        // If the hosts matches then assume this should be relative to baseURL
622
        [$scheme, $path] = $this->changeSchemeAndPath($scheme, $path);
1,748✔
623

624
        return static::createURIString(
1,748✔
625
            $scheme,
1,748✔
626
            $this->getAuthority(),
1,748✔
627
            $path, // Absolute URIs should use a "/" for an empty path
1,748✔
628
            $this->getQuery(),
1,748✔
629
            $this->getFragment(),
1,748✔
630
        );
1,748✔
631
    }
632

633
    /**
634
     * Change the path (and scheme) assuming URIs with the same host as baseURL
635
     * should be relative to the project's configuration.
636
     *
637
     * @return array{string, string}
638
     *
639
     * @deprecated 4.2.0 This method will be deleted.
640
     */
641
    private function changeSchemeAndPath(string $scheme, string $path): array
642
    {
643
        // Check if this is an internal URI
644
        $config  = config(App::class);
1,748✔
645
        $baseUri = new self($config->baseURL);
1,748✔
646

647
        if (
648
            str_starts_with($this->getScheme(), 'http')
1,748✔
649
            && $this->getHost() === $baseUri->getHost()
1,748✔
650
        ) {
651
            // Check for additional segments
652
            $basePath = trim($baseUri->getPath(), '/') . '/';
1,701✔
653
            $trimPath = ltrim($path, '/');
1,701✔
654

655
            if ($basePath !== '/' && ! str_starts_with($trimPath, $basePath)) {
1,701✔
656
                $path = $basePath . $trimPath;
×
657
            }
658

659
            // Check for forced HTTPS
660
            if ($config->forceGlobalSecureRequests) {
1,701✔
661
                $scheme = 'https';
5✔
662
            }
663
        }
664

665
        return [$scheme, $path];
1,748✔
666
    }
667

668
    /**
669
     * Parses the given string and saves the appropriate authority pieces.
670
     *
671
     * Note: Method not in PSR-7
672
     *
673
     * @return $this
674
     */
675
    public function setAuthority(string $str)
676
    {
677
        $parts = parse_url($str);
102✔
678

679
        if (! isset($parts['path'])) {
102✔
680
            $parts['path'] = $this->getPath();
1✔
681
        }
682

683
        if (! isset($parts['host']) && $parts['path'] !== '') {
102✔
684
            $parts['host'] = $parts['path'];
53✔
685
            unset($parts['path']);
53✔
686
        }
687

688
        $this->applyParts($parts);
102✔
689

690
        return $this;
102✔
691
    }
692

693
    /**
694
     * Return an instance with the specified scheme.
695
     *
696
     * This method MUST retain the state of the current instance, and return
697
     * an instance that contains the specified scheme.
698
     *
699
     * Implementations MUST support the schemes "http" and "https" case
700
     * insensitively, and MAY accommodate other schemes if required.
701
     *
702
     * An empty scheme is equivalent to removing the scheme.
703
     *
704
     * @param string $scheme The scheme to use with the new instance.
705
     *
706
     * @return static A new instance with the specified scheme.
707
     *
708
     * @throws InvalidArgumentException for invalid or unsupported schemes.
709
     */
710
    public function withScheme(string $scheme)
711
    {
712
        $uri = clone $this;
1,909✔
713

714
        $scheme = strtolower($scheme);
1,909✔
715

716
        $uri->scheme = preg_replace('#:(//)?$#', '', $scheme);
1,909✔
717

718
        return $uri;
1,909✔
719
    }
720

721
    /**
722
     * Sets the userInfo/Authority portion of the URI.
723
     *
724
     * @param string $user The user's username
725
     * @param string $pass The user's password
726
     *
727
     * @return $this
728
     *
729
     * @TODO PSR-7: Should be `withUserInfo($user, $password = null)`.
730
     */
731
    public function setUserInfo(string $user, #[SensitiveParameter] string $pass)
732
    {
733
        $this->user     = trim($user);
2✔
734
        $this->password = trim($pass);
2✔
735

736
        return $this;
2✔
737
    }
738

739
    /**
740
     * Sets the host name to use.
741
     *
742
     * @return $this
743
     *
744
     * @TODO PSR-7: Should be `withHost($host)`.
745
     */
746
    public function setHost(string $str)
747
    {
748
        $this->host = trim($str);
142✔
749

750
        return $this;
142✔
751
    }
752

753
    /**
754
     * Sets the port portion of the URI.
755
     *
756
     * @return $this
757
     *
758
     * @TODO PSR-7: Should be `withPort($port)`.
759
     */
760
    public function setPort(?int $port = null)
761
    {
762
        if ($port === null) {
4✔
763
            return $this;
×
764
        }
765

766
        if ($port > 0 && $port <= 65535) {
4✔
767
            $this->port = $port;
1✔
768

769
            return $this;
1✔
770
        }
771

772
        if ($this->silent) {
3✔
UNCOV
773
            return $this;
×
774
        }
775

776
        throw HTTPException::forInvalidPort($port);
3✔
777
    }
778

779
    /**
780
     * Sets the path portion of the URI.
781
     *
782
     * @return $this
783
     *
784
     * @TODO PSR-7: Should be `withPath($port)`.
785
     */
786
    public function setPath(string $path)
787
    {
788
        $this->path = $this->filterPath($path);
121✔
789

790
        $tempPath = trim($this->path, '/');
121✔
791

792
        $this->segments = ($tempPath === '') ? [] : explode('/', $tempPath);
121✔
793

794
        return $this;
121✔
795
    }
796

797
    /**
798
     * Sets the path portion of the URI based on segments.
799
     *
800
     * @return $this
801
     */
802
    protected function refreshPath(): self
803
    {
804
        $this->path = $this->filterPath(implode('/', $this->segments));
11✔
805

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

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

810
        return $this;
11✔
811
    }
812

813
    /**
814
     * Sets the query portion of the URI, while attempting
815
     * to clean the various parts of the query keys and values.
816
     *
817
     * @return $this
818
     *
819
     * @TODO PSR-7: Should be `withQuery($query)`.
820
     */
821
    public function setQuery(string $query)
822
    {
823
        if (str_contains($query, '#')) {
283✔
824
            if ($this->silent) {
1✔
UNCOV
825
                return $this;
×
826
            }
827

828
            throw HTTPException::forMalformedQueryString();
1✔
829
        }
830

831
        // Can't have leading ?
832
        if ($query !== '' && str_starts_with($query, '?')) {
282✔
833
            $query = substr($query, 1);
2✔
834
        }
835

836
        if ($this->rawQueryString) {
282✔
837
            $this->query = $this->parseStr($query);
2✔
838
        } else {
839
            parse_str($query, $this->query);
280✔
840
        }
841

842
        return $this;
282✔
843
    }
844

845
    /**
846
     * A convenience method to pass an array of items in as the Query
847
     * portion of the URI.
848
     *
849
     * @return URI
850
     *
851
     * @TODO: PSR-7: Should be `withQueryParams(array $query)`
852
     */
853
    public function setQueryArray(array $query)
854
    {
855
        $query = http_build_query($query);
16✔
856

857
        return $this->setQuery($query);
16✔
858
    }
859

860
    /**
861
     * Adds a single new element to the query vars.
862
     *
863
     * Note: Method not in PSR-7
864
     *
865
     * @param int|string|null $value
866
     *
867
     * @return $this
868
     */
869
    public function addQuery(string $key, $value = null)
870
    {
871
        $this->query[$key] = $value;
40✔
872

873
        return $this;
40✔
874
    }
875

876
    /**
877
     * Removes one or more query vars from the URI.
878
     *
879
     * Note: Method not in PSR-7
880
     *
881
     * @param string ...$params
882
     *
883
     * @return $this
884
     */
885
    public function stripQuery(...$params)
886
    {
887
        foreach ($params as $param) {
1✔
888
            unset($this->query[$param]);
1✔
889
        }
890

891
        return $this;
1✔
892
    }
893

894
    /**
895
     * Filters the query variables so that only the keys passed in
896
     * are kept. The rest are removed from the object.
897
     *
898
     * Note: Method not in PSR-7
899
     *
900
     * @param string ...$params
901
     *
902
     * @return $this
903
     */
904
    public function keepQuery(...$params)
905
    {
906
        $temp = [];
1✔
907

908
        foreach ($this->query as $key => $value) {
1✔
909
            if (! in_array($key, $params, true)) {
1✔
910
                continue;
1✔
911
            }
912

913
            $temp[$key] = $value;
1✔
914
        }
915

916
        $this->query = $temp;
1✔
917

918
        return $this;
1✔
919
    }
920

921
    /**
922
     * Sets the fragment portion of the URI.
923
     *
924
     * @see https://tools.ietf.org/html/rfc3986#section-3.5
925
     *
926
     * @return $this
927
     *
928
     * @TODO PSR-7: Should be `withFragment($fragment)`.
929
     */
930
    public function setFragment(string $string)
931
    {
932
        $this->fragment = trim($string, '# ');
206✔
933

934
        return $this;
206✔
935
    }
936

937
    /**
938
     * Encodes any dangerous characters, and removes dot segments.
939
     * While dot segments have valid uses according to the spec,
940
     * this URI class does not allow them.
941
     */
942
    protected function filterPath(?string $path = null): string
943
    {
944
        $orig = $path;
1,917✔
945

946
        // Decode/normalize percent-encoded chars so
947
        // we can always have matching for Routes, etc.
948
        $path = urldecode($path);
1,917✔
949

950
        // Remove dot segments
951
        $path = self::removeDotSegments($path);
1,917✔
952

953
        // Fix up some leading slash edge cases...
954
        if (str_starts_with($orig, './')) {
1,917✔
955
            $path = '/' . $path;
1✔
956
        }
957
        if (str_starts_with($orig, '../')) {
1,917✔
958
            $path = '/' . $path;
1✔
959
        }
960

961
        // Encode characters
962
        $path = preg_replace_callback(
1,917✔
963
            '/(?:[^' . static::CHAR_UNRESERVED . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/',
1,917✔
964
            static fn (array $matches): string => rawurlencode($matches[0]),
1,917✔
965
            $path,
1,917✔
966
        );
1,917✔
967

968
        return $path;
1,917✔
969
    }
970

971
    /**
972
     * Saves our parts from a parse_url call.
973
     *
974
     * @param array{
975
     *  host?: string,
976
     *  user?: string,
977
     *  path?: string,
978
     *  query?: string,
979
     *  fragment?: string,
980
     *  scheme?: string,
981
     *  port?: int,
982
     *  pass?: string,
983
     * } $parts
984
     *
985
     * @return void
986
     */
987
    protected function applyParts(array $parts)
988
    {
989
        if (isset($parts['host']) && $parts['host'] !== '') {
1,909✔
990
            $this->host = $parts['host'];
1,861✔
991
        }
992

993
        if (isset($parts['user']) && $parts['user'] !== '') {
1,909✔
994
            $this->user = $parts['user'];
5✔
995
        }
996

997
        if (isset($parts['path']) && $parts['path'] !== '') {
1,909✔
998
            $this->path = $this->filterPath($parts['path']);
1,907✔
999
        }
1000

1001
        if (isset($parts['query']) && $parts['query'] !== '') {
1,909✔
1002
            $this->setQuery($parts['query']);
10✔
1003
        }
1004

1005
        if (isset($parts['fragment']) && $parts['fragment'] !== '') {
1,909✔
1006
            $this->fragment = $parts['fragment'];
4✔
1007
        }
1008

1009
        $this->scheme = $this->withScheme($parts['scheme'] ?? 'http')->getScheme();
1,909✔
1010

1011
        if (isset($parts['port'])) {
1,909✔
1012
            // Valid port numbers are enforced by earlier parse_url or setPort()
1013
            $this->port = $parts['port'];
203✔
1014
        }
1015

1016
        if (isset($parts['pass'])) {
1,909✔
1017
            $this->password = $parts['pass'];
2✔
1018
        }
1019

1020
        if (isset($parts['path']) && $parts['path'] !== '') {
1,909✔
1021
            $tempPath = trim($parts['path'], '/');
1,907✔
1022

1023
            $this->segments = $tempPath === '' ? [] : explode('/', $tempPath);
1,907✔
1024
        }
1025
    }
1026

1027
    /**
1028
     * Combines one URI string with this one based on the rules set out in
1029
     * RFC 3986 Section 2
1030
     *
1031
     * @see http://tools.ietf.org/html/rfc3986#section-5.2
1032
     *
1033
     * @return URI
1034
     */
1035
    public function resolveRelativeURI(string $uri)
1036
    {
1037
        /*
1038
         * NOTE: We don't use removeDotSegments in this
1039
         * algorithm since it's already done by this line!
1040
         */
1041
        $relative = new self($uri);
101✔
1042

1043
        if ($relative->getScheme() === $this->getScheme()) {
101✔
1044
            $relative = $relative->withScheme('');
93✔
1045
        }
1046

1047
        $transformed = clone $relative;
101✔
1048

1049
        // 5.2.2 Transform References in a non-strict method (no scheme)
1050
        if ($relative->getAuthority() !== '') {
101✔
1051
            $transformed
2✔
1052
                ->setAuthority($relative->getAuthority())
2✔
1053
                ->setPath($relative->getPath())
2✔
1054
                ->setQuery($relative->getQuery());
2✔
1055
        } else {
1056
            if ($relative->getPath() === '') {
99✔
1057
                $transformed->setPath($this->getPath());
4✔
1058

1059
                if ($relative->getQuery() !== '') {
4✔
1060
                    $transformed->setQuery($relative->getQuery());
2✔
1061
                } else {
1062
                    $transformed->setQuery($this->getQuery());
2✔
1063
                }
1064
            } else {
1065
                if (str_starts_with($relative->getPath(), '/')) {
95✔
1066
                    $transformed->setPath($relative->getPath());
52✔
1067
                } else {
1068
                    $transformed->setPath($this->mergePaths($this, $relative));
43✔
1069
                }
1070

1071
                $transformed->setQuery($relative->getQuery());
95✔
1072
            }
1073

1074
            $transformed->setAuthority($this->getAuthority());
99✔
1075
        }
1076

1077
        $transformed = $transformed->withScheme($this->getScheme());
101✔
1078
        $transformed->setFragment($relative->getFragment());
101✔
1079

1080
        return $transformed;
101✔
1081
    }
1082

1083
    /**
1084
     * Given 2 paths, will merge them according to rules set out in RFC 2986,
1085
     * Section 5.2
1086
     *
1087
     * @see http://tools.ietf.org/html/rfc3986#section-5.2.3
1088
     */
1089
    protected function mergePaths(self $base, self $reference): string
1090
    {
1091
        if ($base->getAuthority() !== '' && $base->getPath() === '') {
43✔
1092
            return '/' . ltrim($reference->getPath(), '/ ');
1✔
1093
        }
1094

1095
        $path = explode('/', $base->getPath());
42✔
1096

1097
        if ($path[0] === '') {
42✔
1098
            unset($path[0]);
42✔
1099
        }
1100

1101
        array_pop($path);
42✔
1102
        $path[] = $reference->getPath();
42✔
1103

1104
        return implode('/', $path);
42✔
1105
    }
1106

1107
    /**
1108
     * This is equivalent to the native PHP parse_str() function.
1109
     * This version allows the dot to be used as a key of the query string.
1110
     *
1111
     * @return array<string, string>
1112
     */
1113
    protected function parseStr(string $query): array
1114
    {
1115
        $return = [];
2✔
1116
        $query  = explode('&', $query);
2✔
1117

1118
        $params = array_map(static fn (string $chunk): ?string => preg_replace_callback(
2✔
1119
            '/^(?<key>[^&=]+?)(?:\[[^&=]*\])?=(?<value>[^&=]+)/',
2✔
1120
            static fn (array $match): string => str_replace($match['key'], bin2hex($match['key']), $match[0]),
2✔
1121
            urldecode($chunk),
2✔
1122
        ), $query);
2✔
1123

1124
        $params = implode('&', $params);
2✔
1125
        parse_str($params, $result);
2✔
1126

1127
        foreach ($result as $key => $value) {
2✔
1128
            // Array key might be int
1129
            $return[hex2bin((string) $key)] = $value;
2✔
1130
        }
1131

1132
        return $return;
2✔
1133
    }
1134
}
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