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

JBZoo / Utils / 7526328988

14 Oct 2023 07:08PM UTC coverage: 93.158%. Remained the same
7526328988

push

github

web-flow
PHP CS Fixer - `ordered_types` (#46)

4 of 4 new or added lines in 2 files covered. (100.0%)

9 existing lines in 4 files now uncovered.

1661 of 1783 relevant lines covered (93.16%)

22.54 hits per line

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

98.8
/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'] ?? '');
8✔
59

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

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

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

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

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

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

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

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

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

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

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

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

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

137
        return $url === '' ? null : $url;
8✔
138
    }
139

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

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

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

157
        $serverData = data($_SERVER);
24✔
158

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

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

173
        if (!isStrEmpty($url)) {
24✔
174
            if ($isHttps) {
20✔
175
                return 'https://' . $url;
4✔
176
            }
177

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

182
        return null;
4✔
183
    }
184

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

193
        $user = $_SERVER['PHP_AUTH_USER'] ?? '';
4✔
194

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

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

203
            $result .= '@';
4✔
204
        }
205

206
        return $result;
4✔
207
    }
208

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

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

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

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

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

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

266
        if ($parts->has('host')) {
48✔
267
            $url['host'] = $parts->get('host');
40✔
268
        }
269

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

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

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

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

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

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

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

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

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

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

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

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

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

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

352
        $newUrl = $url->getArrayCopy();
48✔
353

354
        return $parsedString;
48✔
355
    }
356

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

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

372
        // Default is not SSL
373
        return false;
24✔
374
    }
375

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

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

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

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

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

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

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

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

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

436
        $relative = \ltrim($relative, '/');
4✔
437

438
        return $relative;
4✔
439
    }
440

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

450
        return "{$root}/{$rel}";
4✔
451
    }
452

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

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

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

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

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

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

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

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