Url::build()   F
last analyzed

Complexity

Conditions 40
Paths > 20000

Size

Total Lines 121
Code Lines 71

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 1640

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 71
c 1
b 0
f 1
dl 0
loc 121
rs 0
ccs 0
cts 0
cp 0
cc 40
nc 14929920
nop 4
crap 1640

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * @inspiration
5
 * https://github.com/drupal/drupal/blob/8.8.x/core/lib/Drupal/Component/Utility/UrlHelper.php
6
 * https://github.com/JBZoo/Utils/blob/master/src/Url.php
7
 */
8
9
namespace Nip\Utility;
10
11
/**
12
 * Class Url
13
 * @package Nip\Utility
14
 */
15
class Url
16
{
17
18
    public const ARG_SEPARATOR = '&';
19
20
    /**
21
     * URL constants as defined in the PHP Manual under "Constants usable with
22
     * http_build_url()".
23
     *
24
     * @see http://us2.php.net/manual/en/http.constants.php#http.constants.url
25
     */
26
    const URL_REPLACE = 1;
27
    const URL_JOIN_PATH = 2;
28
    const URL_JOIN_QUERY = 4;
29
    const URL_STRIP_USER = 8;
30
    const URL_STRIP_PASS = 16;
31
    const URL_STRIP_AUTH = 32;
32
    const URL_STRIP_PORT = 64;
33
    const URL_STRIP_PATH = 128;
34
    const URL_STRIP_QUERY = 256;
35
    const URL_STRIP_FRAGMENT = 512;
36
    const URL_STRIP_ALL = 1024;
37
38
    public const PORT_HTTP = 80;
39
    public const PORT_HTTPS = 443;
40
41
    /**
42
     * The list of allowed protocols.
43
     *
44
     * @var array
45
     */
46
    protected static $allowedProtocols = ['http', 'https'];
47
48
    public static function addArg(array $newParams, ?string $uri = null): string
49
    {
50
        $uri = $uri ?? ($_SERVER['REQUEST_URI'] ?? '');
51
52
        // Parse the URI into it's components
53
        $parsedUri = parse_url($uri);
54
55
        if (isset($parsedUri['query'])) {
56
            parse_str($parsedUri['query'], $queryParams);
57
            $queryParams = array_merge($queryParams, $newParams);
58
        } elseif (isset($parsedUri['path']) && strstr($parsedUri['path'], '=') !== false) {
59
            $parsedUri['query'] = $parsedUri['path'];
60
            unset($parsedUri['path']);
61
            parse_str($parsedUri['query'], $queryParams);
62
            $queryParams = array_merge($queryParams, $newParams);
63
        } else {
64
            $queryParams = $newParams;
65
        }
66
67
        // Strip out any query params that are set to false.
68
        // Properly handle valueless parameters.
69
        foreach ($queryParams as $param => $value) {
70
            if ($value === false) {
71
                unset($queryParams[$param]);
72
            } elseif ($value === null) {
73
                $queryParams[$param] = '';
74
            }
75
        }
76
77
        // Re-construct the query string
78
        $parsedUri['query'] = http_build_query($queryParams);
79
80
        // Re-construct the entire URL
81
        $newUri = static::build($parsedUri);
82
83
84
        // Make the URI consistent with our input
85
        if ($newUri[0] === '/' && strstr($uri, '/') === false) {
86
            $newUri = substr($newUri, 1);
87
        }
88
89
        if ($newUri[0] === '?' && strstr($uri, '?') === false) {
90
            $newUri = substr($newUri, 1);
91
        }
92
93
        return rtrim($newUri, '?');
94
    }
95
96
    /**
97
     * Removes an item or list from the query string.
98
     *
99
     * @param   string|array  $keys  Query key or keys to remove.
100
     * @param   string|null   $uri   When null uses the $_SERVER value
101
     *
102
     * @return string
103
     */
104
    public static function delArg($keys, ?string $uri = null): string
105
    {
106
        if (is_array($keys)) {
107
            $params = array_combine($keys, array_fill(0, count($keys), false)) ?: [];
108 3
109
            return self::addArg($params, (string)$uri);
110 3
        }
111 3
112 3
        return self::addArg([$keys => false], (string)$uri);
113
    }
114
115
    /**
116
     * Parses an array into a valid, rawurlencoded query string.
117
     *
118
     *
119
     * rawurlencode() is RFC3986 compliant, and as a consequence RFC3987
120
     * compliant. The latter defines the required format of "URLs" in HTML5.
121
     * urlencode() is almost the same as rawurlencode(), except that it encodes
122
     * spaces as "+" instead of "%20". This makes its result non compliant to
123
     * RFC3986 and as a consequence non compliant to RFC3987 and as a consequence
124
     * not valid as a "URL" in HTML5.
125
     *
126
     * @param array $query
127
     *   The query parameter array to be processed,
128
     *   e.g. \Drupal::request()->query->all().
129
     * @param string $parent
130
     *   Internal use only. Used to build the $query array key for nested items.
131
     *
132
     * @return string
133
     *   A rawurlencoded string which can be used as or appended to the URL query
134
     *   string.
135
     *
136
     * @ingroup php_wrappers
137
     * @todo Remove this function once PHP 5.4 is required as we can use just
138
     *   http_build_query() directly.
139
     *
140
     */
141
    public static function buildQuery(array $query, $parent = '')
142
    {
143
        $params = [];
144
145
        foreach ($query as $key => $value) {
146
            $key = ($parent ? $parent . '[' . rawurlencode($key) . ']' : rawurlencode($key));
147
148
            // Recurse into children.
149
            if (is_array($value)) {
150
                $params[] = static::buildQuery($value, $key);
151
            } // If a query parameter value is NULL, only append its key.
152
            elseif (!isset($value)) {
153
                $params[] = $key;
154
            } else {
155
                // For better readability of paths in query strings, we decode slashes.
156
                $params[] = $key . '=' . str_replace('%2F', '/', rawurlencode($value));
157
            }
158
        }
159
160
        return implode('&', $params);
161
    }
162
163
    /**
164
     * Reverse of the PHP built-in function parse_url
165
     *
166
     * @see http://php.net/parse_url
167
     * @param $url
168
     * @return string
169
     */
170
    public static function build($url, $parts = [], $flags = self::URL_REPLACE, &$new_url = array())
171
    {
172
        is_array($url) || $url = parse_url($url);
173
        is_array($parts) || $parts = parse_url($parts);
174
175
        isset($url['query']) && is_string($url['query']) || $url['query'] = null;
176
        isset($parts['query']) && is_string($parts['query']) || $parts['query'] = null;
177
178
        $keys = ['user', 'pass', 'port', 'path', 'query', 'fragment'];
179
        // URL_STRIP_ALL and URL_STRIP_AUTH cover several other flags.
180
        if ($flags & self::URL_STRIP_ALL) {
181
            $flags |= self::URL_STRIP_USER | self::URL_STRIP_PASS
182
                | self::URL_STRIP_PORT | self::URL_STRIP_PATH
183
                | self::URL_STRIP_QUERY | self::URL_STRIP_FRAGMENT;
184
        } elseif ($flags & self::URL_STRIP_AUTH) {
185
            $flags |= self::URL_STRIP_USER | self::URL_STRIP_PASS;
186
        }
187
188
        // Schema and host are alwasy replaced
189
        foreach (['scheme', 'host'] as $part) {
190
            if (isset($parts[$part])) {
191
                $url[$part] = $parts[$part];
192
            }
193
        }
194
195
        if ($flags & self::URL_REPLACE) {
196
            foreach ($keys as $key) {
197
                if (isset($parts[$key])) {
198
                    $url[$key] = $parts[$key];
199
                }
200
            }
201
        } else {
202
            if (isset($parts['path']) && ($flags & self::URL_JOIN_PATH)) {
203
                if (isset($url['path']) && substr($parts['path'], 0, 1) !== '/') {
204
                    $url['path'] = rtrim(
205
                            str_replace(basename($url['path']), '', $url['path']),
206
                            '/'
207
                        ) . '/' . ltrim($parts['path'], '/');
208
                } else {
209
                    $url['path'] = $parts['path'];
210
                }
211
            }
212
213
            if (isset($parts['query']) && ($flags & self::URL_JOIN_QUERY)) {
214
                if (isset($url['query'])) {
215
                    parse_str($url['query'], $url_query);
216
                    parse_str($parts['query'], $parts_query);
217
218
                    $url['query'] = http_build_query(
219
                        array_replace_recursive(
220
                            $url_query,
221
                            $parts_query
222
                        )
223
                    );
224
                } else {
225
                    $url['query'] = $parts['query'];
226
                }
227
            }
228
        }
229
230
        if (isset($url['path']) && substr($url['path'], 0, 1) !== '/') {
231
            $url['path'] = '/' . $url['path'];
232
        }
233
234
        foreach ($keys as $key) {
235
            $strip = 'URL_STRIP_' . strtoupper($key);
236
            if ($flags & constant(Url::class . '::' . $strip)) {
237
                unset($url[$key]);
238
            }
239
        }
240
241
        if (isset($url['port'])) {
242
            $url['port'] = intval($url['port']);
243
            if ($url['port'] === self::PORT_HTTPS) {
244
                $url['scheme'] = 'https';
245
            } elseif ($url['port'] === self::PORT_HTTP) {
246
                $url['scheme'] = 'http';
247
            }
248
        }
249
250
        $parsed_string = '';
251
252
        if (isset($url['scheme'])) {
253
            $parsed_string .= $url['scheme'] . '://';
254
        }
255
256
        if (isset($url['user']) && !empty($url['user'])) {
257
            $parsed_string .= $url['user'];
258
259
            if (isset($url['pass'])) {
260
                $parsed_string .= ':' . $url['pass'];
261
            }
262
263
            $parsed_string .= '@';
264
        }
265
266
        if (isset($url['host'])) {
267
            $parsed_string .= $url['host'];
268
        }
269
270
        if (isset($url['port']) && $url['port'] !== self::PORT_HTTP && $url['scheme'] === 'http') {
271
            $parsed_string .= ':' . $url['port'];
272
        }
273
274
        if (!empty($url['path'])) {
275
            $parsed_string .= $url['path'];
276
        } else {
277
            $parsed_string .= '/';
278
        }
279
280
        if (isset($url['query']) && !empty($url['query'])) {
281
            $parsed_string .= '?' . $url['query'];
282
        }
283
284
        if (isset($url['fragment'])) {
285
            $parsed_string .= '#' . trim($url['fragment'], '#');
286
        }
287
288
        $new_url = $url;
289
290
        return $parsed_string;
291
    }
292
293
    /**
294
     * Create URL from array params
295
     *
296
     * @param   array  $parts
297
     *
298
     * @return string
299
     */
300
    public static function create(array $parts = []): string
301
    {
302
        $parts = array_merge(
303
            [
304
                'scheme' => 'https',
305
                'query'  => [],
306
            ],
307
            $parts
308
        );
309
310
        if (is_array($parts['query'])) {
311
            $parts['query'] = self::buildQuery($parts['query']);
312
        }
313
314
        /** @noinspection ArgumentEqualsDefaultValueInspection */
315
        return self::build('', $parts, self::URL_REPLACE);
316
    }
317
318
    /**
319
     * Parses a URL string into its path, query, and fragment components.
320
     *
321
     * This function splits both internal paths like @code node?b=c#d @endcode and
322
     * external URLs like @code https://example.com/a?b=c#d @endcode into their
323
     * component parts. See
324
     * @link    http://tools.ietf.org/html/rfc3986#section-3 RFC 3986 @endlink for an
325
     * explanation of what the component parts are.
326
     *
327
     * Note that, unlike the RFC, when passed an external URL, this function
328
     * groups the scheme, authority, and path together into the path component.
329
     *
330
     * @param   string  $url
331
     *   The internal path or external URL string to parse.
332
     *
333
     * @return array
334
     *   An associative array containing:
335
     *   - path: The path component of $url. If $url is an external URL, this
336
     *     includes the scheme, authority, and path.
337
     *   - query: An array of query parameters from $url, if they exist.
338
     *   - fragment: The fragment component from $url, if it exists.
339
     *
340
     * @see     \Drupal\Core\Utility\LinkGenerator
341
     * @see     http://tools.ietf.org/html/rfc3986
342
     *
343
     * @ingroup php_wrappers
344
     */
345
    public static function parse($url)
346
    {
347
        $options = [
348
            'path'     => null,
349
            'query'    => [],
350
            'fragment' => '',
351
        ];
352
353
        // External URLs: not using parse_url() here, so we do not have to rebuild
354
        // the scheme, host, and path without having any use for it.
355
        // The URL is considered external if it contains the '://' delimiter. Since
356
        // a URL can also be passed as a query argument, we check if this delimiter
357
        // appears in front of the '?' query argument delimiter.
358
        $scheme_delimiter_position = strpos($url, '://');
359
        $query_delimiter_position  = strpos($url, '?');
360
        if ($scheme_delimiter_position !== false && ($query_delimiter_position === false || $scheme_delimiter_position < $query_delimiter_position)) {
361
            // Split off the fragment, if any.
362
            if (strpos($url, '#') !== false) {
363
                list($url, $options['fragment']) = explode('#', $url, 2);
364
            }
365
366
            // Split off everything before the query string into 'path'.
367
            $parts = explode('?', $url, 2);
368
369
            // Don't support URLs without a path, like 'http://'.
370
            list(, $path) = explode('://', $parts[0], 2);
371
            if ($path != '') {
372
                $options['path'] = $parts[0];
373
            }
374
            // If there is a query string, transform it into keyed query parameters.
375
            if (isset($parts[1])) {
376
                parse_str($parts[1], $options['query']);
377
            }
378
        } // Internal URLs.
379
        else {
380
            // parse_url() does not support relative URLs, so make it absolute. For
381
            // instance, the relative URL "foo/bar:1" isn't properly parsed.
382
            $parts = parse_url('http://example.com/' . $url);
383
            // Strip the leading slash that was just added.
384
            $options['path'] = substr($parts['path'], 1);
385
            if (isset($parts['query'])) {
386
                parse_str($parts['query'], $options['query']);
387
            }
388
            if (isset($parts['fragment'])) {
389
                $options['fragment'] = $parts['fragment'];
390
            }
391
        }
392
393
        return $options;
394
    }
395
396
    /**
397
     * Verifies the syntax of the given URL.
398
     *
399
     * This function should only be used on actual URLs. It should not be used for
400
     * Drupal menu paths, which can contain arbitrary characters.
401
     * Valid values per RFC 3986.
402
     *
403
     * @param   string  $url
404
     *   The URL to verify.
405
     * @param   bool    $absolute
406
     *   Whether the URL is absolute (beginning with a scheme such as "http:").
407
     *
408
     * @return bool
409
     *   TRUE if the URL is in a valid format, FALSE otherwise.
410
     */
411
    public static function isValid($url, $absolute = true)
412
    {
413
        if ($absolute) {
414
            return (bool)preg_match(
415
                "
416
        /^                                                      # Start at the beginning of the text
417
        (?:ftp|https?|feed):\/\/                                # Look for ftp, http, https or feed schemes
418
        (?:                                                     # Userinfo (optional) which is typically
419
          (?:(?:[\w\.\-\+!$&'\(\)*\+,;=]|%[0-9a-f]{2})+:)*      # a username or a username and password
420
          (?:[\w\.\-\+%!$&'\(\)*\+,;=]|%[0-9a-f]{2})+@          # combination
421
        )?
422
        (?:
423
          (?:[a-z0-9\-\.]|%[0-9a-f]{2})+                        # A domain name or a IPv4 address
424
          |(?:\[(?:[0-9a-f]{0,4}:)*(?:[0-9a-f]{0,4})\])         # or a well formed IPv6 address
425
        )
426
        (?::[0-9]+)?                                            # Server port number (optional)
427
        (?:[\/|\?]
428
          (?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})   # The path and query (optional)
429
        *)?
430
      $/xi",
431
                $url
432
            );
433
        } else {
434
            return (bool)preg_match("/^(?:[\w#!:\.\?\+=&@$'~*,;\/\(\)\[\]\-]|%[0-9a-f]{2})+$/i", $url);
435
        }
436
    }
437
438
    /**
439
     * Is absolute url
440
     *
441
     * @param   string  $path
442
     *
443
     * @return bool
444
     */
445
    public static function isAbsolute(string $path): bool
446
    {
447
        return strpos($path, '//') === 0 || preg_match('#^[a-z-]{3,}:\/\/#i', $path);
448
    }
449
450
    /**
451
     * Turns all of the links in a string into HTML links.
452
     *
453
     * Part of the LinkifyURL Project <https://github.com/jmrware/LinkifyURL>
454
     *
455
     * @param   string  $text  The string to parse
456
     *
457
     * @return string
458
     */
459
    public static function linkify($text)
460
    {
461
        // IE does not handle &apos; entity!
462
        $text = (string)preg_replace('/&apos;/', '&#39;', $text);
463
464
        $sectionHtmlPattern = '%            # Rev:20100913_0900 github.com/jmrware/LinkifyURL
465
                                            # Section text into HTML <A> tags  and everything else.
466
             (                              # $1: Everything not HTML <A> tag.
467
               [^<]+(?:(?!<a\b)<[^<]*)*     # non A tag stuff starting with non-"<".
468
               | (?:(?!<a\b)<[^<]*)+        # non A tag stuff starting with "<".
469
             )                              # End $1.
470
             | (                            # $2: HTML <A...>...</A> tag.
471
                 <a\b[^>]*>                 # <A...> opening tag.
472
                 [^<]*(?:(?!</a\b)<[^<]*)*  # A tag contents.
473
                 </a\s*>                    # </A> closing tag.
474
             )                              # End $2:
475
             %ix';
476
477
        return (string)preg_replace_callback(
478
            $sectionHtmlPattern,
479
            /**
480
             * @param   array  $matches
481
             *
482
             * @return string
483
             */
484
            static function (array $matches): string {
485
                return self::linkifyCallback($matches);
486
            },
487
            $text
488
        );
489
    }
490
491
    /**
492
     * Callback for the preg_replace in the linkify() method.
493
     * Part of the LinkifyURL Project <https://github.com/jmrware/LinkifyURL>
494
     *
495
     * @param   array  $matches  Matches from the preg_ function
496
     *
497
     * @return string
498
     */
499
    protected static function linkifyCallback(array $matches): string
500
    {
501
        return $matches[2] ?? self::linkifyRegex($matches[1]);
502
    }
503
504
505
    /**
506
     * Callback for the preg_replace in the linkify() method.
507
     * Part of the LinkifyURL Project <https://github.com/jmrware/LinkifyURL>
508
     *
509
     * @param string $text Matches from the preg_ function
510
     * @return string
511
     */
512
    protected static function linkifyRegex(string $text): string
513
    {
514
        $urlPattern = '/                                            # Rev:20100913_0900 github.com\/jmrware\/LinkifyURL
515
                                                                    # Match http & ftp URL that is not already linkified
516
                                                                    # Alternative 1: URL delimited by (parentheses).
517
            (\()                                                    # $1 "(" start delimiter.
518
            ((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]+) # $2: URL.
519
            (\))                                                    # $3: ")" end delimiter.
520
            |                                                       # Alternative 2: URL delimited by [square brackets].
521
            (\[)                                                    # $4: "[" start delimiter.
522
            ((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]+) # $5: URL.
523
            (\])                                                    # $6: "]" end delimiter.
524
            |                                                       # Alternative 3: URL delimited by {curly braces}.
525
            (\{)                                                    # $7: "{" start delimiter.
526
            ((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]+) # $8: URL.
527
            (\})                                                    # $9: "}" end delimiter.
528
            |                                                       # Alternative 4: URL delimited by <angle brackets>.
529
            (<|&(?:lt|\#60|\#x3c);)                                 # $10: "<" start delimiter (or HTML entity).
530
            ((?:ht|f)tps?:\/\/[a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]+) # $11: URL.
531
            (>|&(?:gt|\#62|\#x3e);)                                 # $12: ">" end delimiter (or HTML entity).
532
            |                                                       # Alt. 5: URL not delimited by (), [], {} or <>.
533
            (                                                       # $13: Prefix proving URL not already linked.
534
            (?: ^                                                   # Can be a beginning of line or string, or
535
             | [^=\s\'"\]]                                          # a non-"=", non-quote, non-"]", followed by
536
            ) \s*[\'"]?                                             # optional whitespace and optional quote;
537
              | [^=\s]\s+                                           # or... a non-equals sign followed by whitespace.
538
            )                                                       # End $13. Non-prelinkified-proof prefix.
539
            (\b                                                     # $14: Other non-delimited URL.
540
            (?:ht|f)tps?:\/\/                                       # Required literal http, https, ftp or ftps prefix.
541
            [a-z0-9\-._~!$\'()*+,;=:\/?#[\]@%]+                     # All URI chars except "&" (normal*).
542
            (?:                                                     # Either on a "&" or at the end of URI.
543
            (?!                                                     # Allow a "&" char only if not start of an...
544
            &(?:gt|\#0*62|\#x0*3e);                                 # HTML ">" entity, or
545
            | &(?:amp|apos|quot|\#0*3[49]|\#x0*2[27]);              # a [&\'"] entity if
546
            [.!&\',:?;]?                                            # followed by optional punctuation then
547
            (?:[^a-z0-9\-._~!$&\'()*+,;=:\/?#[\]@%]|$)              # a non-URI char or EOS.
548
           ) &                                                      # If neg-assertion true, match "&" (special).
549
            [a-z0-9\-._~!$\'()*+,;=:\/?#[\]@%]*                     # More non-& URI chars (normal*).
550
           )*                                                       # Unroll-the-loop (special normal*)*.
551
            [a-z0-9\-_~$()*+=\/#[\]@%]                              # Last char can\'t be [.!&\',;:?]
552
           )                                                        # End $14. Other non-delimited URL.
553
            /imx';
554
555
        $urlReplace = '$1$4$7$10$13<a href="$2$5$8$11$14">$2$5$8$11$14</a>$3$6$9$12';
556
557
        return (string)preg_replace($urlPattern, $urlReplace, $text);
558
    }
559
}
560