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

sirn-se / phrity-net-uri / 9826570433

07 Jul 2024 10:25AM UTC coverage: 96.903% (-3.1%) from 100.0%
9826570433

push

github

sirn-se
URI encode/decode

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

7 existing lines in 1 file now uncovered.

219 of 226 relevant lines covered (96.9%)

59.46 hits per line

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

96.88
/src/Uri.php
1
<?php
2

3
/**
4
 * File for Net\Uri class.
5
 * @package Phrity > Net > Uri
6
 * @see https://www.rfc-editor.org/rfc/rfc3986
7
 * @see https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface
8
 */
9

10
namespace Phrity\Net;
11

12
use InvalidArgumentException;
13
use JsonSerializable;
14
use Psr\Http\Message\UriInterface;
15
use Stringable;
16
use TypeError;
17

18
/**
19
 * Net\Uri class.
20
 */
21
class Uri implements JsonSerializable, Stringable, UriInterface
22
{
23
    public const REQUIRE_PORT = 1; // Always include port, explicit or default
24
    public const ABSOLUTE_PATH = 2; // Enforce absolute path
25
    public const NORMALIZE_PATH = 4; // Normalize path
26
    public const IDNA = 8; // @deprecated, replaced by IDN_ENCODE
27
    public const IDN_ENCODE = 16; // IDN-encode host
28
    public const IDN_DECODE = 32; // IDN-decode host
29
    public const URI_DECODE = 64; // Decoded URI
30
    public const URI_ENCODE = 128; // Minimal URI encoded
31
    public const URI_ENCODE_3986 = 256; // URI encoded RFC 3986
32

33
    private const RE_MAIN = '!^(?P<schemec>(?P<scheme>[^:/?#]+):)?(?P<authorityc>//(?P<authority>[^/?#]*))?'
34
                          . '(?P<path>[^?#]*)(?P<queryc>\?(?P<query>[^#]*))?(?P<fragmentc>#(?P<fragment>.*))?$!';
35
    private const RE_AUTH = '!^(?P<userinfoc>(?P<user>[^:/?#]+)(?P<passc>:(?P<pass>[^:/?#]+))?@)?'
36
                          . '(?P<host>[^:/?#]*|\[[^/?#]*\])(?P<portc>:(?P<port>[0-9]*))?$!';
37

38
    private static array $port_defaults = [
39
        'acap' => 674,
40
        'afp' => 548,
41
        'dict' => 2628,
42
        'dns' => 53,
43
        'ftp' => 21,
44
        'git' => 9418,
45
        'gopher' => 70,
46
        'http' => 80,
47
        'https' => 443,
48
        'imap' => 143,
49
        'ipp' => 631,
50
        'ipps' => 631,
51
        'irc' => 194,
52
        'ircs' => 6697,
53
        'ldap' => 389,
54
        'ldaps' => 636,
55
        'mms' => 1755,
56
        'msrp' => 2855,
57
        'mtqp' => 1038,
58
        'nfs' => 111,
59
        'nntp' => 119,
60
        'nntps' => 563,
61
        'pop' => 110,
62
        'prospero' => 1525,
63
        'redis' => 6379,
64
        'rsync' => 873,
65
        'rtsp' => 554,
66
        'rtsps' => 322,
67
        'rtspu' => 5005,
68
        'sftp' => 22,
69
        'smb' => 445,
70
        'snmp' => 161,
71
        'ssh' => 22,
72
        'svn' => 3690,
73
        'telnet' => 23,
74
        'ventrilo' => 3784,
75
        'vnc' => 5900,
76
        'wais' => 210,
77
        'ws' => 80,
78
        'wss' => 443,
79
    ];
80

81
    private string $scheme = '';
82
    private bool $authority = false;
83
    private string $host = '';
84
    private int|null $port = null;
85
    private string $user = '';
86
    private string|null $pass = null;
87
    private string $path = '';
88
    private string $query = '';
89
    private string $fragment = '';
90

91
    /**
92
     * Create new URI instance using a string
93
     * @param string $uri_string URI as string
94
     * @throws \InvalidArgumentException If the given URI cannot be parsed
95
     */
96
    public function __construct(string $uri_string = '')
97
    {
98
        $this->parse($uri_string);
220✔
99
    }
100

101

102
    // ---------- PSR-7 getters ---------------------------------------------------------------------------------------
103

104
    /**
105
     * Retrieve the scheme component of the URI.
106
     * @param int $flags Optional modifier flags
107
     * @return string The URI scheme
108
     */
109
    public function getScheme(int $flags = 0): string
110
    {
111
        return $this->scheme;
149✔
112
    }
113

114
    /**
115
     * Retrieve the authority component of the URI.
116
     * @param int $flags Optional modifier flags
117
     * @return string The URI authority, in "[user-info@]host[:port]" format
118
     */
119
    public function getAuthority(int $flags = 0): string
120
    {
121
        $host = $this->formatComponent($this->getHost($flags));
125✔
122
        if ($host === '') {
125✔
123
            return '';
3✔
124
        }
125
        $userinfo = $this->formatComponent($this->getUserInfo($flags), '', '@');
122✔
126
        $port = $this->formatComponent($this->getPort($flags), ':');
122✔
127
        return "{$userinfo}{$host}{$port}";
122✔
128
    }
129

130
    /**
131
     * Retrieve the user information component of the URI.
132
     * @param int $flags Optional modifier flags
133
     * @return string The URI user information, in "username[:password]" format
134
     */
135
    public function getUserInfo(int $flags = 0): string
136
    {
137
        $user = $this->formatComponent($this->uriOutput($this->user, $flags));
123✔
138
        $pass = $this->formatComponent($this->uriOutput($this->pass ?? '', $flags), ':');
123✔
139
        return $user === '' ? '' : "{$user}{$pass}";
123✔
140
    }
141

142
    /**
143
     * Retrieve the host component of the URI.
144
     * @param int $flags Optional modifier flags
145
     * @return string The URI host
146
     */
147
    public function getHost(int $flags = 0): string
148
    {
149
        if ($flags & self::IDNA) {
132✔
150
            trigger_error("Flag IDNA is deprecated; use IDN_ENCODE instead", E_USER_DEPRECATED);
1✔
151
            return $this->idnEncode($this->host);
1✔
152
        }
153
        if ($flags & self::IDN_ENCODE) {
131✔
154
            return $this->idnEncode($this->host);
1✔
155
        }
156
        if ($flags & self::IDN_DECODE) {
131✔
157
            return $this->idnDecode($this->host);
1✔
158
        }
159
        return $this->host;
131✔
160
    }
161

162
    /**
163
     * Retrieve the port component of the URI.
164
     * @param int $flags Optional modifier flags
165
     * @return null|int The URI port
166
     */
167
    public function getPort(int $flags = 0): int|null
168
    {
169
        $default = self::$port_defaults[$this->scheme] ?? null;
126✔
170
        if ($flags & self::REQUIRE_PORT) {
126✔
171
            return $this->port !== null ? $this->port : $default;
2✔
172
        }
173
        return $this->port === $default ? null : $this->port;
125✔
174
    }
175

176
    /**
177
     * Retrieve the path component of the URI.
178
     * @param int $flags Optional modifier flags
179
     * @return string The URI path
180
     */
181
    public function getPath(int $flags = 0): string
182
    {
183
        $path = $this->path;
159✔
184
        if ($flags & self::NORMALIZE_PATH) {
159✔
185
            $path = $this->normalizePath($path);
1✔
186
        }
187
        if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
159✔
188
            $path = "/{$path}";
3✔
189
        }
190
        return $this->uriOutput($path, $flags, '\/:@');
159✔
191
    }
192

193
    /**
194
     * Retrieve the query string of the URI.
195
     * @param int $flags Optional modifier flags
196
     * @return string The URI query string
197
     */
198
    public function getQuery(int $flags = 0): string
199
    {
200
        return $this->uriOutput($this->query, $flags, '\/:@?');
157✔
201
    }
202

203
    /**
204
     * Retrieve the fragment component of the URI.
205
     * @param int $flags Optional modifier flags
206
     * @return string The URI fragment
207
     */
208
    public function getFragment(int $flags = 0): string
209
    {
210
        return $this->uriOutput($this->fragment, $flags, '\/:@?'); ;
154✔
211
    }
212

213

214
    // ---------- PSR-7 setters ---------------------------------------------------------------------------------------
215

216
    /**
217
     * Return an instance with the specified scheme.
218
     * @param string $scheme The scheme to use with the new instance
219
     * @param int $flags Optional modifier flags
220
     * @return static A new instance with the specified scheme
221
     * @throws \InvalidArgumentException for invalid schemes
222
     * @throws \InvalidArgumentException for unsupported schemes
223
     */
224
    public function withScheme(string $scheme, int $flags = 0): UriInterface
225
    {
226
        $clone = $this->clone($flags);
11✔
227
        $clone->setScheme($scheme, $flags);
11✔
228
        return $clone;
8✔
229
    }
230

231
    /**
232
     * Return an instance with the specified user information.
233
     * @param string $user The user name to use for authority
234
     * @param null|string $password The password associated with $user
235
     * @param int $flags Optional modifier flags
236
     * @return static A new instance with the specified user information
237
     */
238
    public function withUserInfo(string $user, string|null $password = null, int $flags = 0): UriInterface
239
    {
240
        $clone = $this->clone($flags);
24✔
241
        $clone->setUserInfo($user, $password);
24✔
242
        return $clone;
24✔
243
    }
244

245
    /**
246
     * Return an instance with the specified host.
247
     * @param string $host The hostname to use with the new instance
248
     * @param int $flags Optional modifier flags
249
     * @return static A new instance with the specified host
250
     * @throws \InvalidArgumentException for invalid hostnames
251
     */
252
    public function withHost(string $host, int $flags = 0): UriInterface
253
    {
254
        $clone = $this->clone($flags);
11✔
255
        $clone->setHost($host, $flags);
11✔
256
        return $clone;
11✔
257
    }
258

259
    /**
260
     * Return an instance with the specified port.
261
     * @param null|int $port The port to use with the new instance
262
     * @param int $flags Optional modifier flags
263
     * @return static A new instance with the specified port
264
     * @throws \InvalidArgumentException for invalid ports
265
     */
266
    public function withPort(int|null $port, int $flags = 0): UriInterface
267
    {
268
        $clone = $this->clone($flags);
8✔
269
        $clone->setPort($port, $flags);
8✔
270
        return $clone;
6✔
271
    }
272

273
    /**
274
     * Return an instance with the specified path.
275
     * @param string $path The path to use with the new instance
276
     * @param int $flags Optional modifier flags
277
     * @return static A new instance with the specified path
278
     * @throws \InvalidArgumentException for invalid paths
279
     */
280
    public function withPath(string $path, int $flags = 0): UriInterface
281
    {
282
        $clone = $this->clone($flags);
17✔
283
        $clone->setPath($path, $flags);
17✔
284
        return $clone;
17✔
285
    }
286

287
    /**
288
     * Return an instance with the specified query string.
289
     * @param string $query The query string to use with the new instance
290
     * @param int $flags Optional modifier flags
291
     * @return static A new instance with the specified query string
292
     * @throws \InvalidArgumentException for invalid query strings
293
     */
294
    public function withQuery(string $query, int $flags = 0): UriInterface
295
    {
296
        $clone = $this->clone($flags);
12✔
297
        $clone->setQuery($query, $flags);
12✔
298
        return $clone;
12✔
299
    }
300

301
    /**
302
     * Return an instance with the specified URI fragment.
303
     * @param string $fragment The fragment to use with the new instance
304
     * @param int $flags Optional modifier flags
305
     * @return static A new instance with the specified fragment
306
     */
307
    public function withFragment(string $fragment, int $flags = 0): UriInterface
308
    {
309
        $clone = $this->clone($flags);
11✔
310
        $clone->setFragment($fragment, $flags);
11✔
311
        return $clone;
11✔
312
    }
313

314

315
    // ---------- PSR-7 string & Stringable ---------------------------------------------------------------------------
316

317
    /**
318
     * Return the string representation as a URI reference.
319
     * @return string
320
     */
321
    public function __toString(): string
322
    {
323
        return $this->toString();
139✔
324
    }
325

326

327
    // ---------- JsonSerializable ------------------------------------------------------------------------------------
328

329
    /**
330
     * Return JSON encode value as URI reference.
331
     * @return string
332
     */
333
    public function jsonSerialize(): string
334
    {
335
        return $this->toString();
1✔
336
    }
337

338

339
    // ---------- Extensions ------------------------------------------------------------------------------------------
340

341
    /**
342
     * Return the string representation as a URI reference.
343
     * @param int $flags Optional modifier flags
344
     * @param tring $format Optional format specification
345
     * @return string
346
     */
347
    public function toString(int $flags = 0, string $format = '{scheme}{authority}{path}{query}{fragment}'): string
348
    {
349
        $path_flags = ($this->authority && $this->path ? self::ABSOLUTE_PATH : 0) | $flags;
144✔
350
        return str_replace([
144✔
351
            '{scheme}',
144✔
352
            '{authority}',
144✔
353
            '{path}',
144✔
354
            '{query}',
144✔
355
            '{fragment}',
144✔
356
        ], [
144✔
357
            $this->formatComponent($this->getScheme($flags), '', ':'),
144✔
358
            $this->authority ? "//{$this->formatComponent($this->getAuthority($flags))}" : '',
144✔
359
            $this->formatComponent($this->getPath($path_flags)),
144✔
360
            $this->formatComponent($this->getQuery(), '?'),
144✔
361
            $this->formatComponent($this->getFragment(), '#'),
144✔
362
        ], $format);
144✔
363
    }
364

365
    /**
366
     * Get compontsns as array; as parse_url() method
367
     * @param int $flags Optional modifier flags
368
     * @return array
369
     */
370
    public function getComponents(int $flags = 0): array
371
    {
372
        return array_filter([
1✔
373
            'scheme' => $this->getScheme($flags),
1✔
374
            'host' => $this->getHost($flags),
1✔
375
            'port' => $this->getPort($flags | self::REQUIRE_PORT),
1✔
376
            'user' => $this->user,
1✔
377
            'pass' => $this->pass,
1✔
378
            'path' => $this->getPath($flags),
1✔
379
            'query' => $this->getQuery($flags),
1✔
380
            'fragment' => $this->getFragment($flags),
1✔
381
        ]);
1✔
382
    }
383

384
    /**
385
     * Return an instance with the specified compontents set.
386
     * @return static A new instance with the specified components
387
     */
388
    public function withComponents(array $components, int $flags = 0): UriInterface
389
    {
390
        $clone = $this->clone($flags);
2✔
391
        foreach ($components as $component => $value) {
2✔
392
            switch ($component) {
393
                case 'port':
2✔
394
                    $clone->setPort($value, $flags);
1✔
395
                    break;
1✔
396
                case 'scheme':
2✔
397
                    $clone->setScheme($value, $flags);
1✔
398
                    break;
1✔
399
                case 'host':
2✔
400
                    $clone->setHost($value, $flags);
1✔
401
                    break;
1✔
402
                case 'path':
2✔
403
                    $clone->setPath($value, $flags);
1✔
404
                    break;
1✔
405
                case 'query':
2✔
406
                    $clone->setQuery($value, $flags);
1✔
407
                    break;
1✔
408
                case 'fragment':
2✔
409
                    $clone->setFragment($value, $flags);
1✔
410
                    break;
1✔
411
                case 'userInfo':
2✔
412
                    $clone->setUserInfo(...$value);
1✔
413
                    break;
1✔
414
                default:
415
                    throw new InvalidArgumentException("Invalid URI component: '{$component}'");
1✔
416
            }
417
        }
418
        return $clone;
1✔
419
    }
420

421
    /**
422
     * Return all query items (if any) as associative array.
423
     * @param int $flags Optional modifier flags
424
     * @return array Query items
425
     */
426
    public function getQueryItems(int $flags = 0): array
427
    {
428
        parse_str($this->getQuery(), $result);
2✔
429
        return $result;
2✔
430
    }
431

432
    /**
433
     * Return query item value for named query item, or null if not present.
434
     * @param string $name Name of query item to retrieve
435
     * @param int $flags Optional modifier flags
436
     * @return array|string|null Query item value
437
     */
438
    public function getQueryItem(string $name, int $flags = 0): array|string|null
439
    {
440
        parse_str($this->getQuery(), $result);
2✔
441
        return $result[$name] ?? null;
2✔
442
    }
443

444
    /**
445
     * Add query items as associative array that will be merged qith current items.
446
     * @param array $items Array of query items to add
447
     * @param int $flags Optional modifier flags
448
     * @return static A new instance with the added query items
449
     */
450
    public function withQueryItems(array $items, int $flags = 0): UriInterface
451
    {
452
        $clone = $this->clone($flags);
2✔
453
        $clone->setQuery(http_build_query(
2✔
454
            $this->queryMerge($this->getQueryItems($flags), $items),
2✔
455
            '',
2✔
456
            null,
2✔
457
            PHP_QUERY_RFC3986
2✔
458
        ), $flags);
2✔
459
        return $clone;
2✔
460
    }
461

462
    /**
463
     * Add query item value for named query item
464
     * @param string $name Name of query item to add
465
     * @param array|string|null $value Value of query item to add
466
     * @param int $flags Optional modifier flags
467
     * @return static A new instance with the added query items
468
     */
469
    public function withQueryItem(string $name, array|string|null $value, int $flags = 0): UriInterface
470
    {
471
        return $this->withQueryItems([$name => $value], $flags);
2✔
472
    }
473

474

475
    // ---------- Protected helper methods ----------------------------------------------------------------------------
476

477
    protected function setPort(int|null $port, int $flags = 0): void
478
    {
479
        if ($port !== null && ($port < 0 || $port > 65535)) {
138✔
480
            throw new InvalidArgumentException("Invalid port '{$port}'");
2✔
481
        }
482
        $this->port = $port;
136✔
483
    }
484

485
    protected function setScheme(string $scheme, int $flags = 0): void
486
    {
487
        $pattern = '/^[a-z][a-z0-9-+.]*$/i';
162✔
488
        if ($scheme !== '' && preg_match($pattern, $scheme) == 0) {
162✔
489
            throw new InvalidArgumentException("Invalid scheme '{$scheme}': Should match {$pattern}");
4✔
490
        }
491
        $this->scheme = mb_strtolower($scheme);
158✔
492
    }
493

494
    protected function setHost(string $host, int $flags = 0): void
495
    {
496
        $this->authority = $this->authority || $host !== '';
139✔
497
        if ($flags & self::IDNA) {
139✔
498
            trigger_error("Flag IDNA is deprecated; use IDN_ENCODE instead", E_USER_DEPRECATED);
1✔
499
            $host = $this->idnEncode($host);
1✔
500
        }
501
        if ($flags & self::IDN_ENCODE) {
139✔
502
            $host = $this->idnEncode($host);
1✔
503
        }
504
        if ($flags & self::IDN_DECODE) {
139✔
505
            $host = $this->idnDecode($host);
1✔
506
        }
507
        $this->host = mb_strtolower($host);
139✔
508
    }
509

510
    protected function setPath(string $path, int $flags = 0): void
511
    {
512
        if ($flags & self::NORMALIZE_PATH) {
167✔
513
            $path = $this->normalizePath($path);
1✔
514
        }
515
        if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
167✔
516
            $path = "/{$path}";
1✔
517
        }
518
        $this->path = $this->uriDecode($path);
167✔
519
    }
520

521
    protected function setQuery(string $query, int $flags = 0): void
522
    {
523
        $this->query = $this->uriDecode($query);
164✔
524
    }
525

526
    protected function setFragment(string $fragment, int $flags = 0): void
527
    {
528
        $this->fragment = $this->uriDecode($fragment);
163✔
529
    }
530

531
    protected function setUser(string $user, int $flags = 0): void
532
    {
533
        $this->user = $this->uriDecode($user);
134✔
534
    }
535

536
    protected function setPassword(string|null $pass, int $flags = 0): void
537
    {
538
        $this->pass = $pass === null ? null : $this->uriDecode($pass);
134✔
539
    }
540

541
    protected function setUserInfo(string $user = '', string|null $pass = null, int $flags = 0): void
542
    {
543
        $this->setUser($user);
25✔
544
        $this->setPassword($pass);
25✔
545
    }
546

547

548
    // ---------- Private helper methods ------------------------------------------------------------------------------
549

550
    private function parse(string $uri_string = ''): void
551
    {
552
        if ($uri_string === '') {
220✔
553
            return;
68✔
554
        }
555
        preg_match(self::RE_MAIN, $uri_string, $main);
153✔
556
        $this->authority = !empty($main['authorityc']);
153✔
557
        $this->setScheme(isset($main['schemec']) ? $main['scheme'] : '');
153✔
558
        $this->setPath(isset($main['path']) ? $main['path'] : '');
152✔
559
        $this->setQuery(isset($main['queryc']) ? $main['query'] : '');
152✔
560
        $this->setFragment(isset($main['fragmentc']) ? $main['fragment'] : '');
152✔
561
        if ($this->authority) {
152✔
562
            preg_match(self::RE_AUTH, $main['authority'], $auth);
135✔
563
            if (empty($auth) && $main['authority'] !== '') {
135✔
564
                throw new InvalidArgumentException("Invalid 'authority'.");
3✔
565
            }
566
            if ($auth['host'] === '' && $auth['user'] !== '') {
132✔
567
                throw new InvalidArgumentException("Invalid 'authority'.");
1✔
568
            }
569
            $this->setUser(isset($auth['user']) ? $auth['user'] : '');
131✔
570
            $this->setPassword(isset($auth['passc']) ? $auth['pass'] : null);
131✔
571
            $this->setHost(isset($auth['host']) ? $auth['host'] : '');
131✔
572
            $this->setPort(isset($auth['portc']) ? (int)$auth['port'] : null);
131✔
573
        }
574
    }
575

576
    private function clone(int $flags = 0): self
577
    {
578
        $clone = clone $this;
84✔
579
        if ($flags & self::REQUIRE_PORT) {
84✔
580
            $clone->setPort($this->getPort(self::REQUIRE_PORT), $flags);
1✔
581
        }
582
        return $clone;
84✔
583
    }
584

585
    private function uriOutput(string $source, int $flags = 0, string $keep = ''): string
586
    {
587
        if ($flags & self::URI_DECODE) {
179✔
588
            return $source;
8✔
589
        }
590

591
        $unreserved = 'a-zA-Z0-9_\-\.~';
179✔
592
        $subdelim = '!\$&\'\(\)\*\+,;=';
179✔
593
        $char = '\pL';
179✔
594
        $pct = '%(?![A-Fa-f0-9]{2}))';
179✔
595

596
        $re = "/(?:[^%{$unreserved}{$subdelim}{$keep}]+|{$pct}/u";
179✔
597

598
        if ($flags & self::URI_ENCODE) {
179✔
599
            $re = "/(?:[^%{$unreserved}{$subdelim}{$keep}{$char}]+|{$pct}/u";
11✔
600
        }
601
        return preg_replace_callback($re, function ($matches) {
179✔
602
            return rawurlencode($matches[0]);
33✔
603
        }, $source);
179✔
604
    }
605

606
    private function uriEncode(string $source, int $flags = 0, string $keep = ''): string
607
    {
UNCOV
608
        $exclude = "[^%\/:=&!\$'()*+,;@{$keep}]+";
×
UNCOV
609
        $exp = "/(%{$exclude})|({$exclude})/";
×
UNCOV
610
        return preg_replace_callback($exp, function ($matches) {
×
UNCOV
611
            if ($e = preg_match('/^(%[0-9a-fA-F]{2})/', $matches[0], $m)) {
×
UNCOV
612
                return substr($matches[0], 0, 3) . rawurlencode(substr($matches[0], 3));
×
613
            } else {
UNCOV
614
                return rawurlencode($matches[0]);
×
615
            }
UNCOV
616
        }, $source);
×
617
    }
618

619
    private function uriDecode(string $source): string
620
    {
621
        $re = "/(%[A-Fa-f0-9]{2})/u";
187✔
622
        return preg_replace_callback($re, function ($matches) {
187✔
623
            return rawurldecode($matches[0]);
12✔
624
        }, $source);
187✔
625
    }
626

627
    private function formatComponent(string|int|null $value, string $before = '', string $after = ''): string
628
    {
629
        $string = strval($value);
145✔
630
        return $string === '' ? '' : "{$before}{$string}{$after}";
145✔
631
    }
632

633
    private function normalizePath(string $path): string
634
    {
635
        $result = [];
1✔
636
        preg_match_all('!([^/]*/|[^/]*$)!', $path, $items);
1✔
637
        foreach ($items[0] as $item) {
1✔
638
            switch ($item) {
639
                case '':
1✔
640
                case './':
1✔
641
                case '.':
1✔
642
                    break; // just skip
1✔
643
                case '/':
1✔
644
                    if (empty($result)) {
1✔
645
                        array_push($result, $item); // add
1✔
646
                    }
647
                    break;
1✔
648
                case '..':
1✔
649
                case '../':
1✔
650
                    if (empty($result) || end($result) == '../') {
1✔
651
                        array_push($result, $item); // add
1✔
652
                    } else {
653
                        array_pop($result); // remove previous
1✔
654
                    }
655
                    break;
1✔
656
                default:
657
                    array_push($result, $item); // add
1✔
658
            }
659
        }
660
        return implode('', $result);
1✔
661
    }
662

663
    private function idnEncode(string $value): string
664
    {
665
        if ($value === '' || !is_callable('idn_to_ascii')) {
2✔
666
            return $value; // Can't convert, but don't cause exception
1✔
667
        }
668
        return idn_to_ascii($value, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
2✔
669
    }
670

671
    private function idnDecode(string $value): string
672
    {
673
        if ($value === '' || !is_callable('idn_to_utf8')) {
1✔
674
            return $value; // Can't convert, but don't cause exception
1✔
675
        }
676
        return idn_to_utf8($value, IDNA_NONTRANSITIONAL_TO_UNICODE, INTL_IDNA_VARIANT_UTS46);
1✔
677
    }
678

679
    private function queryMerge(array $a, array $b): array
680
    {
681
        foreach ($b as $key => $value) {
2✔
682
            if (is_int($key)) {
2✔
683
                $a[] = $value;
1✔
684
            } elseif (is_array($value)) {
2✔
685
                $a[$key] = $this->queryMerge($a[$key] ?? [], $b[$key] ?? []);
1✔
686
            } elseif (is_scalar($value)) {
2✔
687
                $a[$key] = $this->uriDecode($b[$key]);
2✔
688
            } else {
689
                unset($a[$key]);
1✔
690
            }
691
        }
692
        return $a;
2✔
693
    }
694
}
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