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

JBZoo / Utils / 9676422156

22 Jun 2024 09:31AM UTC coverage: 92.722% (-0.04%) from 92.766%
9676422156

push

github

web-flow
Fix escaping of backslashes in regex and test cases (#53)

6 of 7 new or added lines in 1 file covered. (85.71%)

2 existing lines in 2 files now uncovered.

1669 of 1800 relevant lines covered (92.72%)

41.52 hits per line

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

98.81
/src/Url.php
1
<?php
2

3
/**
4
 * JBZoo Toolbox - Utils.
5
 *
6
 * This file is part of the JBZoo Toolbox project.
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @license    MIT
11
 * @copyright  Copyright (C) JBZoo.com, All rights reserved.
12
 * @see        https://github.com/JBZoo/Utils
13
 */
14

15
declare(strict_types=1);
16

17
namespace JBZoo\Utils;
18

19
use function JBZoo\Data\data;
20

21
/**
22
 * @SuppressWarnings(PHPMD.TooManyPublicMethods)
23
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
24
 */
25
final class Url
26
{
27
    /**
28
     * URL constants as defined in the PHP Manual under "Constants usable with http_build_url()".
29
     * @see http://us2.php.net/manual/en/http.constants.php#http.constants.url
30
     */
31
    public const URL_REPLACE        = 1;
32
    public const URL_JOIN_PATH      = 2;
33
    public const URL_JOIN_QUERY     = 4;
34
    public const URL_STRIP_USER     = 8;
35
    public const URL_STRIP_PASS     = 16;
36
    public const URL_STRIP_AUTH     = 32;
37
    public const URL_STRIP_PORT     = 64;
38
    public const URL_STRIP_PATH     = 128;
39
    public const URL_STRIP_QUERY    = 256;
40
    public const URL_STRIP_FRAGMENT = 512;
41
    public const URL_STRIP_ALL      = 1024;
42

43
    public const ARG_SEPARATOR = '&';
44

45
    public const PORT_HTTP  = 80;
46
    public const PORT_HTTPS = 443;
47

48
    /**
49
     * Add or remove query arguments to the URL.
50
     * @param array       $newParams Either new key or an associative array
51
     * @param null|string $uri       URI or URL to append the query/queries to
52
     * @SuppressWarnings(PHPMD.NPathComplexity)
53
     * @SuppressWarnings(PHPMD.Superglobals)
54
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
55
     */
56
    public static function addArg(array $newParams, ?string $uri = null): string
57
    {
58
        $uri ??= ($_SERVER['REQUEST_URI'] ?? '');
12✔
59

60
        // Parse the URI into it's components
61
        $parsedUri = data((array)\parse_url($uri));
12✔
62

63
        $parsedQuery = $parsedUri->getString('query');
12✔
64
        $parsedPath  = $parsedUri->getString('path');
12✔
65

66
        if (!isStrEmpty($parsedQuery)) {
12✔
67
            \parse_str($parsedQuery, $queryParams);
12✔
68
            $queryParams = \array_merge($queryParams, $newParams);
12✔
69
        } elseif (!isStrEmpty($parsedPath) && \str_contains($parsedPath, '=')) {
6✔
70
            $parsedUri['query'] = $parsedUri['path'];
6✔
71
            $parsedUri->remove('path');
6✔
72
            \parse_str((string)$parsedUri['query'], $queryParams);
6✔
73
            $queryParams = \array_merge($queryParams, $newParams);
6✔
74
        } else {
75
            $queryParams = $newParams;
6✔
76
        }
77

78
        // Strip out any query params that are set to false.
79
        // Properly handle valueless parameters.
80
        foreach ($queryParams as $param => $value) {
12✔
81
            if ($value === false) {
12✔
82
                unset($queryParams[$param]);
12✔
83
            } elseif ($value === null) {
12✔
84
                $queryParams[$param] = '';
6✔
85
            }
86
        }
87

88
        // Re-construct the query string
89
        $parsedUri['query'] = self::build($queryParams);
12✔
90

91
        // Strip = from valueless parameters.
92
        $parsedUri['query'] = (string)\preg_replace('/=(?=&|$)/', '', (string)$parsedUri['query']);
12✔
93

94
        // Re-construct the entire URL
95
        $newUri = self::buildAll((array)$parsedUri);
12✔
96

97
        // Make the URI consistent with our input
98
        foreach ([':', '/', '?'] as $char) {
12✔
99
            if ($newUri[0] === $char && !\str_contains($uri, $char)) {
12✔
100
                $newUri = \substr($newUri, 1);
6✔
101
            }
102
        }
103

104
        return \rtrim($newUri, '?');
12✔
105
    }
106

107
    /**
108
     * Returns the current URL.
109
     */
110
    public static function current(bool $addAuth = false): ?string
111
    {
112
        $root   = self::root($addAuth);
12✔
113
        $path   = self::path();
12✔
114
        $result = \trim("{$root}{$path}");
12✔
115

116
        return $result === '' ? null : $result;
12✔
117
    }
118

119
    /**
120
     * Returns the current path.
121
     * @SuppressWarnings(PHPMD.Superglobals)
122
     */
123
    public static function path(): ?string
124
    {
125
        $url = '';
12✔
126

127
        // Get the rest of the URL
128
        if (!\array_key_exists('REQUEST_URI', $_SERVER)) {
12✔
129
            // Microsoft IIS doesn't set REQUEST_URI by default
130
            $queryString = $_SERVER['QUERY_STRING'] ?? null;
6✔
131
            if ($queryString !== null) {
6✔
UNCOV
132
                $url .= '?' . $queryString;
2✔
133
            }
134
        } else {
135
            $url .= $_SERVER['REQUEST_URI'];
6✔
136
        }
137

138
        return $url === '' ? null : $url;
12✔
139
    }
140

141
    /**
142
     * Returns current root URL.
143
     * @SuppressWarnings(PHPMD.Superglobals)
144
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
145
     */
146
    public static function root(bool $addAuth = false): ?string
147
    {
148
        $url = '';
36✔
149

150
        // Check to see if it's over https
151
        $isHttps = self::isHttps();
36✔
152

153
        // Was a username or password passed?
154
        if ($addAuth) {
36✔
155
            $url .= self::getAuth() ?? '';
6✔
156
        }
157

158
        $serverData = data($_SERVER);
36✔
159

160
        // We want the user to stay on the same host they are currently on,
161
        // but beware of security issues
162
        // see http://shiflett.org/blog/2006/mar/server-name-versus-http-host
163
        $host = (string)$serverData->get('HTTP_HOST');
36✔
164
        $port = (int)$serverData->get('SERVER_PORT');
36✔
165
        $url .= \str_replace(':' . $port, '', $host);
36✔
166

167
        // Is it on a non-standard port?
168
        if ($isHttps && $port !== self::PORT_HTTPS) {
36✔
169
            $url .= $port > 0 ? ":{$port}" : '';
6✔
170
        } elseif (!$isHttps && $port !== self::PORT_HTTP) {
36✔
171
            $url .= $port > 0 ? ":{$port}" : '';
18✔
172
        }
173

174
        if (!isStrEmpty($url)) {
36✔
175
            if ($isHttps) {
30✔
176
                return 'https://' . $url;
6✔
177
            }
178

179
            /** @noinspection HttpUrlsUsage */
180
            return 'http://' . $url;
30✔
181
        }
182

183
        return null;
6✔
184
    }
185

186
    /**
187
     * Get current auth info.
188
     * @SuppressWarnings(PHPMD.Superglobals)
189
     */
190
    public static function getAuth(): ?string
191
    {
192
        $result = null;
6✔
193

194
        $user = $_SERVER['PHP_AUTH_USER'] ?? '';
6✔
195

196
        if ($user !== '') {
6✔
197
            $result   = $user;
6✔
198
            $password = $_SERVER['PHP_AUTH_PW'] ?? '';
6✔
199

200
            if ($password !== '') {
6✔
201
                $result .= ':' . $password;
6✔
202
            }
203

204
            $result .= '@';
6✔
205
        }
206

207
        return $result;
6✔
208
    }
209

210
    /**
211
     * Builds HTTP query from array.
212
     */
213
    public static function build(array $queryParams): string
214
    {
215
        return \http_build_query($queryParams, '', self::ARG_SEPARATOR);
72✔
216
    }
217

218
    /**
219
     * Build a URL. The parts of the second URL will be merged into the first according to the flags' argument.
220
     *
221
     * @param array|string $sourceUrl (part(s) of) a URL in form of a string
222
     *                                or associative array like parse_url() returns
223
     * @param array|string $destParts Same as the first argument
224
     * @param int          $flags     A bitmask of binary or HTTP_URL constants; HTTP_URL_REPLACE is the default
225
     * @param array        $newUrl    If set, it will be filled with the parts of the composed url like parse_url()
226
     *                                would return
227
     *
228
     * @SuppressWarnings(PHPMD.CyclomaticComplexity)
229
     * @SuppressWarnings(PHPMD.NPathComplexity)
230
     * @SuppressWarnings(PHPMD.Superglobals)
231
     * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
232
     *
233
     * @see    https://github.com/jakeasmith/http_build_url/
234
     * @author Jake Smith <theman@jakeasmith.com>
235
     */
236
    public static function buildAll(
237
        array|string $sourceUrl,
238
        array|string $destParts = [],
239
        int $flags = self::URL_REPLACE,
240
        array &$newUrl = [],
241
    ): string {
242
        if (!\is_array($sourceUrl)) {
72✔
243
            $sourceUrl = \parse_url($sourceUrl);
60✔
244
        }
245

246
        if (!\is_array($destParts)) {
72✔
247
            $destParts = \parse_url($destParts);
×
248
        }
249

250
        $url     = data((array)$sourceUrl);
72✔
251
        $parts   = data((array)$destParts);
72✔
252
        $allKeys = ['user', 'pass', 'port', 'path', 'query', 'fragment'];
72✔
253

254
        // HTTP_URL_STRIP_ALL and HTTP_URL_STRIP_AUTH cover several other flags.
255
        if (($flags & self::URL_STRIP_ALL) > 0) {
72✔
256
            $flags |= self::URL_STRIP_USER | self::URL_STRIP_PASS | self::URL_STRIP_PORT | self::URL_STRIP_PATH
6✔
257
                | self::URL_STRIP_QUERY | self::URL_STRIP_FRAGMENT;
6✔
258
        } elseif (($flags & self::URL_STRIP_AUTH) > 0) {
72✔
259
            $flags |= self::URL_STRIP_USER | self::URL_STRIP_PASS;
6✔
260
        }
261

262
        // Schema and host are always replaced
263
        if ($parts->has('scheme')) {
72✔
264
            $url['scheme'] = $parts->get('scheme');
60✔
265
        }
266

267
        if ($parts->has('host')) {
72✔
268
            $url['host'] = $parts->get('host');
60✔
269
        }
270

271
        if (($flags & self::URL_REPLACE) > 0) {
72✔
272
            foreach ($allKeys as $key) {
72✔
273
                if ($parts->has($key)) {
72✔
274
                    $url[$key] = $parts->get($key);
60✔
275
                }
276
            }
277
        } else {
278
            // PATH
279
            if (($flags & self::URL_JOIN_PATH) > 0 && $parts->has('path')) {
6✔
280
                if ($url->has('path') && $parts->get('path')[0] !== '/') {
6✔
281
                    $url['path'] = \rtrim(\str_replace(\basename((string)$url['path']), '', (string)$url['path']), '/')
6✔
282
                        . '/'
6✔
283
                        . \ltrim((string)$parts['path'], '/');
6✔
284
                } else {
285
                    $url['path'] = $parts['path'];
6✔
286
                }
287
            }
288

289
            // QUERY
290
            if (($flags & self::URL_JOIN_QUERY) > 0 && $parts->has('query')) {
6✔
291
                \parse_str($url->get('query', ''), $urlQuery);
6✔
292
                \parse_str($parts->get('query', ''), $partsQuery);
6✔
293

294
                $queryParams  = \array_replace_recursive($urlQuery, $partsQuery);
6✔
295
                $url['query'] = self::build($queryParams);
6✔
296
            }
297
        }
298

299
        $urlPath = $url->getString('path');
72✔
300
        if (!isStrEmpty($urlPath)) {
72✔
301
            $url['path'] = '/' . \ltrim($urlPath, '/');
72✔
302
        }
303

304
        foreach ($allKeys as $key) {
72✔
305
            $strip = 'URL_STRIP_' . \strtoupper($key);
72✔
306
            if (($flags & \constant(__CLASS__ . '::' . $strip)) > 0) {
72✔
307
                $url->remove($key);
6✔
308
            }
309
        }
310

311
        if ($url->get('port', null, 'int') === self::PORT_HTTPS) {
72✔
312
            $url['scheme'] = 'https';
6✔
313
        } elseif ($url->get('port', null, 'int') === self::PORT_HTTP) {
72✔
314
            $url['scheme'] = 'http';
6✔
315
        }
316

317
        if ($url->getInt('port') === 0) {
72✔
318
            if ($url->is('scheme', 'https')) {
72✔
319
                $url['port'] = 443;
60✔
320
            } elseif ($url->is('scheme', 'http')) {
72✔
321
                $url['port'] = 80;
60✔
322
            }
323
        }
324

325
        $parsedString = $url->has('scheme') ? ($url['scheme'] . '://') : '';
72✔
326

327
        if ($url->getString('user') !== '') {
72✔
328
            $parsedString .= $url['user'];
12✔
329
            $parsedString .= $url->getString('pass') === '' ? '' : (':' . $url->getString('pass'));
12✔
330
            $parsedString .= '@';
12✔
331
        }
332

333
        $parsedString .= $url->has('host') ? $url['host'] : '';
72✔
334

335
        if ((int)$url->get('port') !== self::PORT_HTTP && $url->get('scheme') === 'http') {
72✔
336
            $parsedString .= ':' . $url['port'];
6✔
337
        }
338

339
        if ($url->getString('path') !== '') {
72✔
340
            $parsedString .= $url['path'];
72✔
341
        } else {
342
            $parsedString .= '/';
18✔
343
        }
344

345
        if ($url->getString('query') !== '') {
72✔
346
            $parsedString .= '?' . $url->getString('query');
72✔
347
        }
348

349
        if ($url->getString('fragment') !== '') {
72✔
350
            $parsedString .= '#' . \trim($url->getString('fragment'), '#');
18✔
351
        }
352

353
        $newUrl = $url->getArrayCopy();
72✔
354

355
        return $parsedString;
72✔
356
    }
357

358
    /**
359
     * Checks to see if the page is being server over SSL or not.
360
     * @SuppressWarnings(PHPMD.Superglobals)
361
     */
362
    public static function isHttps(bool $trustProxyHeaders = false): bool
363
    {
364
        // Check standard HTTPS header
365
        if (\array_key_exists('HTTPS', $_SERVER)) {
84✔
366
            return !isStrEmpty($_SERVER['HTTPS'] ?? '') && $_SERVER['HTTPS'] !== 'off';
54✔
367
        }
368

369
        if ($trustProxyHeaders && \array_key_exists('X-FORWARDED-PROTO', $_SERVER)) {
36✔
370
            return $_SERVER['X-FORWARDED-PROTO'] === 'https';
×
371
        }
372

373
        // Default is not SSL
374
        return false;
36✔
375
    }
376

377
    /**
378
     * Removes an item or list from the query string.
379
     * @param array|string $keys query key or keys to remove
380
     * @param null|string  $uri  When null uses the $_SERVER value
381
     */
382
    public static function delArg(array|string $keys, ?string $uri = null): string
383
    {
384
        if (\is_array($keys)) {
6✔
385
            $params = \array_combine($keys, \array_fill(0, \count($keys), false));
6✔
386

387
            return self::addArg($params, (string)$uri);
6✔
388
        }
389

390
        return self::addArg([$keys => false], (string)$uri);
6✔
391
    }
392

393
    /**
394
     * Turns all the links in a string into HTML links.
395
     * Part of the LinkifyURL Project <https://github.com/jmrware/LinkifyURL>.
396
     * @param string $text The string to parse
397
     */
398
    public static function parseLink(string $text): string
399
    {
400
        $text = (string)\preg_replace('/&apos;/', '&#39;', $text); // IE does not handle &apos; entity!
6✔
401

402
        $sectionHtmlPattern = '%            # Rev:20100913_0900 github.com/jmrware/LinkifyURL
6✔
403
                                            # Section text into HTML <A> tags  and everything else.
404
             (                              # $1: Everything not HTML <A> tag.
405
               [^<]+(?:(?!<a\b)<[^<]*)*     # non A tag stuff starting with non-"<".
406
               | (?:(?!<a\b)<[^<]*)+        # non A tag stuff starting with "<".
407
             )                              # End $1.
408
             | (                            # $2: HTML <A...>...</A> tag.
409
                 <a\b[^>]*>                 # <A...> opening tag.
410
                 [^<]*(?:(?!</a\b)<[^<]*)*  # A tag contents.
411
                 </a\s*>                    # </A> closing tag.
412
             )                              # End $2:
413
             %ix';
6✔
414

415
        return (string)\preg_replace_callback(
6✔
416
            $sectionHtmlPattern,
6✔
417
            static fn (array $matches): string => self::linkifyCallback($matches),
6✔
418
            $text,
6✔
419
        );
6✔
420
    }
421

422
    /**
423
     * Convert file path to relative URL.
424
     * @SuppressWarnings(PHPMD.Superglobals)
425
     */
426
    public static function pathToRel(string $path): string
427
    {
428
        $root = FS::clean($_SERVER['DOCUMENT_ROOT'] ?? null);
6✔
429
        $path = FS::clean($path);
6✔
430

431
        $normRoot = \str_replace(\DIRECTORY_SEPARATOR, '/', $root);
6✔
432
        $normPath = \str_replace(\DIRECTORY_SEPARATOR, '/', $path);
6✔
433

434
        $regExp   = '/^' . \preg_quote($normRoot, '/') . '/i';
6✔
435
        $relative = (string)\preg_replace($regExp, '', $normPath);
6✔
436

437
        $relative = \ltrim($relative, '/');
6✔
438

439
        return $relative;
6✔
440
    }
441

442
    /**
443
     * Convert file path to absolute URL.
444
     * @SuppressWarnings(PHPMD.Superglobals)
445
     */
446
    public static function pathToUrl(string $path): string
447
    {
448
        $root = self::root();
6✔
449
        $rel  = self::pathToRel($path);
6✔
450

451
        return "{$root}/{$rel}";
6✔
452
    }
453

454
    /**
455
     * Check if URL is not relative.
456
     */
457
    public static function isAbsolute(string $path): bool
458
    {
459
        return \str_starts_with($path, '//') || \preg_match('#^[a-z-]{3,}://#i', $path) > 0;
6✔
460
    }
461

462
    /**
463
     * Create URL from array params.
464
     */
465
    public static function create(array $parts = []): string
466
    {
467
        $parts = \array_merge([
54✔
468
            'scheme' => 'https',
54✔
469
            'query'  => [],
54✔
470
        ], $parts);
54✔
471

472
        if (\is_array($parts['query'])) {
54✔
473
            $parts['query'] = self::build($parts['query']);
54✔
474
        }
475

476
        return self::buildAll('', $parts, self::URL_REPLACE);
54✔
477
    }
478

479
    /**
480
     * Callback for the preg_replace in the linkify() method.
481
     * Part of the LinkifyURL Project <https://github.com/jmrware/LinkifyURL>.
482
     * @param array $matches Matches from the preg_ function
483
     */
484
    private static function linkifyCallback(array $matches): string
485
    {
486
        return $matches[2] ?? self::linkifyRegex($matches[1]);
6✔
487
    }
488

489
    /**
490
     * Callback for the preg_replace in the linkify() method.
491
     * Part of the LinkifyURL Project <https://github.com/jmrware/LinkifyURL>.
492
     * @param string $text Matches from the preg_ function
493
     */
494
    private static function linkifyRegex(string $text): string
495
    {
496
        $urlPattern = '/                                            # Rev:20100913_0900 github.com\/jmrware\/LinkifyURL
6✔
497
                                                                    # Match http & ftp URL that is not already linkified
498
                                                                    # Alternative 1: URL delimited by (parentheses).
499
            (\()                                                    # $1 "(" start delimiter.
500
            ((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]+) # $2: URL.
501
            (\))                                                    # $3: ")" end delimiter.
502
            |                                                       # Alternative 2: URL delimited by [square brackets].
503
            (\[)                                                    # $4: "[" start delimiter.
504
            ((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]+) # $5: URL.
505
            (\])                                                    # $6: "]" end delimiter.
506
            |                                                       # Alternative 3: URL delimited by {curly braces}.
507
            (\{)                                                    # $7: "{" start delimiter.
508
            ((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]+) # $8: URL.
509
            (\})                                                    # $9: "}" end delimiter.
510
            |                                                       # Alternative 4: URL delimited by <angle brackets>.
511
            (<|&(?:lt|\#60|\#x3c);)                                 # $10: "<" start delimiter (or HTML entity).
512
            ((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]+) # $11: URL.
513
            (>|&(?:gt|\#62|\#x3e);)                                 # $12: ">" end delimiter (or HTML entity).
514
            |                                                       # Alt. 5: URL not delimited by (), [], {} or <>.
515
            (                                                       # $13: Prefix proving URL not already linked.
516
            (?: ^                                                   # Can be a beginning of line or string, or
517
             | [^=\s\'"\]]                                          # a non-"=", non-quote, non-"]", followed by
518
            ) \s*[\'"]?                                             # optional whitespace and optional quote;
519
              | [^=\s]\s+                                           # or... a non-equals sign followed by whitespace.
520
            )                                                       # End $13. Non-prelinkified-proof prefix.
521
            (\b                                                     # $14: Other non-delimited URL.
522
            (?:ht|f)tps?:\/\/                                       # Required literal http, https, ftp or ftps prefix.
523
            [a-z0-9\-._~!$\'()*+,;=:\/?#[\]@%]+                     # All URI chars except "&" (normal*).
524
            (?:                                                     # Either on a "&" or at the end of URI.
525
            (?!                                                     # Allow a "&" char only if not start of an...
526
            &(?:gt|\#0*62|\#x0*3e);                                 # HTML ">" entity, or
527
            | &(?:amp|apos|quot|\#0*3[49]|\#x0*2[27]);              # a [&\'"] entity if
528
            [.!&\',:?;]?                                            # followed by optional punctuation then
529
            (?:[^a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]|$)              # a non-URI char or EOS.
530
           ) &                                                      # If neg-assertion true, match "&" (special).
531
            [a-z0-9\-._~!$\'()*+,;=:\/?#[\]@%]*                     # More non-& URI chars (normal*).
532
           )*                                                       # Unroll-the-loop (special normal*)*.
533
            [a-z0-9\-_~$()*+=\/#[\]@%]                              # Last char can\'t be [.!&\',;:?]
534
           )                                                        # End $14. Other non-delimited URL.
535
            /imx';
6✔
536

537
        $urlReplace = '$1$4$7$10$13<a href="$2$5$8$11$14">$2$5$8$11$14</a>$3$6$9$12';
6✔
538

539
        return (string)\preg_replace($urlPattern, $urlReplace, $text);
6✔
540
    }
541
}
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

© 2025 Coveralls, Inc