xref: /plugin/combo/ComboStrap/Web/Url.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1<?php
2
3
4namespace ComboStrap\Web;
5
6use ComboStrap\ArrayCaseInsensitive;
7use ComboStrap\DataType;
8use ComboStrap\ExceptionBadArgument;
9use ComboStrap\ExceptionBadSyntax;
10use ComboStrap\ExceptionCompile;
11use ComboStrap\ExceptionNotEquals;
12use ComboStrap\ExceptionNotExists;
13use ComboStrap\ExceptionNotFound;
14use ComboStrap\ExceptionRuntimeInternal;
15use ComboStrap\FetcherRawLocalPath;
16use ComboStrap\FetcherSystem;
17use ComboStrap\LocalFileSystem;
18use ComboStrap\LogUtility;
19use ComboStrap\Path;
20use ComboStrap\PathAbs;
21use ComboStrap\Site;
22use ComboStrap\Web\UrlRewrite;
23use ComboStrap\WikiPath;
24
25/**
26 * Class Url
27 * @package ComboStrap
28 * There is no URL class in php
29 * Only function
30 * https://www.php.net/manual/en/ref.url.php
31 */
32class Url extends PathAbs
33{
34
35
36    public const PATH_SEP = "/";
37    /**
38     * In HTML (not in css)
39     *
40     * Because ampersands are used to denote HTML entities,
41     * if you want to use them as literal characters, you must escape them as entities,
42     * e.g.  &amp;.
43     *
44     * In HTML, Browser will do the translation for you if you give an URL
45     * not encoded but testing library may not and refuse them
46     *
47     * This URL encoding is mandatory for the {@link ml} function
48     * when there is a width and use them not otherwise
49     *
50     * Thus, if you want to link to:
51     * http://images.google.com/images?num=30&q=larry+bird
52     * you need to encode (ie pass this parameter to the {@link ml} function:
53     * http://images.google.com/images?num=30&amp;q=larry+bird
54     *
55     * https://daringfireball.net/projects/markdown/syntax#autoescape
56     *
57     */
58    public const AMPERSAND_URL_ENCODED_FOR_HTML = '&amp;';
59    /**
60     * Used in dokuwiki syntax & in CSS attribute
61     * (Css attribute value are then HTML encoded as value of the attribute)
62     */
63    public const AMPERSAND_CHARACTER = "&";
64
65    const CANONICAL = "url";
66    /**
67     * The schemes that are relative (normallu only URL ? ie http, https)
68     * This class is much more an URI
69     */
70    const RELATIVE_URL_SCHEMES = ["http", "https"];
71
72
73    private ArrayCaseInsensitive $query;
74    private ?string $path = null;
75    private ?string $scheme = null;
76    private ?string $host = null;
77    private ?string $fragment = null;
78    /**
79     * @var string - original url string
80     */
81    private $url;
82    private ?int $port = null;
83    /**
84     * @var bool - does the URL rewrite occurs
85     */
86    private bool $withRewrite = true;
87
88
89    /**
90     * UrlUtility constructor.
91     * @throws ExceptionBadSyntax
92     * @throws ExceptionBadArgument
93     */
94    public function __construct(string $url = null)
95    {
96
97        $this->url = $url;
98        $this->query = new ArrayCaseInsensitive();
99        if ($this->url !== null) {
100            /**
101             *
102             * @var false
103             *
104             * Note: Url validation is hard with regexp
105             * for instance:
106             *  - http://example.lan/utility/a-combostrap-component-to-render-web-code-in-a-web-page-javascript-html-...-u8fe6ahw
107             *  - does not pass return preg_match('|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i', $url);
108             * of preg_match('/^https?:\/\//',$url) ? from redirect plugin
109             *
110             * We try to create the object, the object use the {@link parse_url()}
111             * method to validate or send an exception if it can be parsed
112             */
113            $urlComponents = parse_url($url);
114            if ($urlComponents === false) {
115                throw new ExceptionBadSyntax("The url ($url) is not valid");
116            }
117            parse_str($urlComponents['query'], $queryKeys);
118            $this->query = new ArrayCaseInsensitive($queryKeys);
119            $this->scheme = $urlComponents["scheme"];
120            $this->host = $urlComponents["host"];
121            $port = $urlComponents["port"];
122            try {
123                if ($port !== null) {
124                    $this->port = DataType::toInteger($port);
125                }
126            } catch (ExceptionBadArgument $e) {
127                throw new ExceptionBadArgument("The port ($port) in ($url) is not an integer. Error: {$e->getMessage()}");
128            }
129            $pathUrlComponent = $urlComponents["path"];
130            if ($pathUrlComponent !== null) {
131                $this->setPath($pathUrlComponent);
132            }
133            $this->fragment = $urlComponents["fragment"];
134        }
135    }
136
137
138    const RESERVED_WORDS = [':', '!', '#', '$', '&', '\'', '(', ')', '*', '+', ',', '/', ';', '=', '?', '@', '[', ']'];
139
140    /**
141     * A text to an encoded url
142     * @param $string -  a string
143     * @param string $separator - the path separator in the string
144     */
145    public static function encodeToUrlPath($string, string $separator = WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT): string
146    {
147        $parts = explode($separator, $string);
148        $encodedParts = array_map(function ($e) {
149            return urlencode($e);
150        }, $parts);
151        return implode("/", $encodedParts);
152    }
153
154    public static function createEmpty(): Url
155    {
156        return new Url();
157    }
158
159    /**
160     *
161     */
162    public static function createFromGetOrPostGlobalVariable(): Url
163    {
164        /**
165         * $_REQUEST is a merge between get and post property
166         * Shared check between post and get HTTP method
167         * {@link \TestRequest} is using it
168         */
169        $url = Url::createEmpty();
170        foreach ($_REQUEST as $key => $value) {
171            if (is_array($value)) {
172                foreach ($value as $subkey => $subval) {
173                    if (is_array($subval)) {
174                        if ($key !== "config") {
175                            // dokuwiki things
176                            LogUtility::warning("The key ($key) is an array of an array and was not taken into account in the request url.");
177                        }
178                        continue;
179                    }
180                    if ($key == "do") {
181                        // for whatever reason, dokuwiki puts the value in the key
182                        $url->addQueryParameter($key, $subkey);
183                        continue;
184                    }
185                    $url->addQueryParameter($key, $subval);
186
187                }
188            } else {
189                /**
190                 * Bad URL format test
191                 * In the `src` attribute of `script`, the url should not be encoded
192                 * with {@link Url::AMPERSAND_URL_ENCODED_FOR_HTML}
193                 * otherwise we get `amp;` as prefix
194                 * in Chrome
195                 */
196                if (strpos($key, "amp;") === 0) {
197                    /**
198                     * We don't advertise this error, it should not happen
199                     * and there is nothing to do to get back on its feet
200                     */
201                    $message = "The url in src has a bad encoding (the attribute have a amp; prefix. Infinite cache will not work.";
202                    throw new ExceptionRuntimeInternal($message);
203                }
204                $url->addQueryParameter($key, $value);
205            }
206        }
207        return $url;
208    }
209
210    /**
211     * Utility class to transform windows separator to url path separator
212     * @param string $pathString
213     * @return array|string|string[]
214     */
215    public static function toUrlSeparator(string $pathString)
216    {
217        return str_replace('\\', '/', $pathString);
218    }
219
220
221    function getQueryProperties(): array
222    {
223        return $this->query->getOriginalArray();
224    }
225
226    /**
227     * @throws ExceptionNotFound
228     */
229    function getQueryPropertyValue($key)
230    {
231        $value = $this->query[$key];
232        if ($value === null) {
233            throw new ExceptionNotFound("The key ($key) was not found");
234        }
235        return $value;
236    }
237
238    /**
239     * Extract the value of a property
240     * @param $propertyName
241     * @return string - the value of the property
242     * @throws ExceptionNotFound
243     */
244    public function getPropertyValue($propertyName): string
245    {
246        if (!isset($this->query[$propertyName])) {
247            throw new ExceptionNotFound("The property ($propertyName) was not found", self::CANONICAL);
248        }
249        return $this->query[$propertyName];
250    }
251
252
253    /**
254     * @throws ExceptionBadSyntax|ExceptionBadArgument
255     */
256    public static function createFromString(string $url): Url
257    {
258        return new Url($url);
259    }
260
261    /**
262     * @throws ExceptionNotFound
263     */
264    public function getScheme(): string
265    {
266        if ($this->scheme === null) {
267            throw new ExceptionNotFound("The scheme was not found");
268        }
269        return $this->scheme;
270    }
271
272    /**
273     * @param string $path
274     * @return $this
275     * in a https scheme: Not the path has a leading `/` that makes the path absolute
276     * in a email scheme: the path is the email (without /) then
277     */
278    public function setPath(string $path): Url
279    {
280
281        /**
282         * Normalization hack
283         */
284        if (strpos($path, "/./") === 0) {
285            $path = substr($path, 2);
286        }
287        $this->path = $path;
288        return $this;
289    }
290
291    /**
292     * @return bool - true if http, https scheme
293     */
294    public function isHttpUrl(): bool
295    {
296        try {
297            return in_array($this->getScheme(), ["http", "https"]);
298        } catch (ExceptionNotFound $e) {
299            return false;
300        }
301    }
302
303    /**
304     * Multiple parameter can be set to form an array
305     *
306     * Example: s=word1&s=word2
307     *
308     * https://stackoverflow.com/questions/24059773/correct-way-to-pass-multiple-values-for-same-parameter-name-in-get-request
309     */
310    public function addQueryParameter(string $key, ?string $value = null): Url
311    {
312        /**
313         * Php Array syntax
314         */
315        if (substr($key, -2) === "[]") {
316            $key = substr($key, 0, -2);
317            $actualValue = $this->query[$key];
318            if ($actualValue === null || is_array($actualValue)) {
319                $this->query[$key] = [$value];
320            } else {
321                $actualValue[] = $value;
322                $this->query[$key] = $actualValue;
323            }
324            return $this;
325        }
326        if (isset($this->query[$key])) {
327            $actualValue = $this->query[$key];
328            if (is_array($actualValue)) {
329                $this->query[$key][] = $value;
330            } else {
331                $this->query[$key] = [$actualValue, $value];
332            }
333        } else {
334            $this->query[$key] = $value;
335        }
336        return $this;
337    }
338
339
340    public function hasProperty(string $key): bool
341    {
342        if (isset($this->query[$key])) {
343            return true;
344        }
345        return false;
346    }
347
348    /**
349     * @return Url - add the scheme and the host based on the request if not present
350     */
351    public function toAbsoluteUrl(): Url
352    {
353        try {
354            $this->getScheme();
355        } catch (ExceptionNotFound $e) {
356            /**
357             * See {@link getBaseURL()}
358             */
359            $https = $_SERVER['HTTPS'];
360            if (empty($https)) {
361                $this->setScheme("http");
362            } else {
363                $this->setScheme("https");
364            }
365        }
366        try {
367            $this->getHost();
368        } catch (ExceptionNotFound $e) {
369            $remoteHost = Site::getServerHost();
370            $this->setHost($remoteHost);
371
372        }
373        return $this;
374    }
375
376    /**
377     * @return string - utility function that call {@link Url::toAbsoluteUrl()} absolute and {@link Url::toString()}
378     */
379    public function toAbsoluteUrlString(): string
380    {
381        $this->toAbsoluteUrl();
382        return $this->toString();
383    }
384
385    /**
386     * @throws ExceptionNotFound
387     */
388    public function getHost(): string
389    {
390        if ($this->host === null) {
391            throw new ExceptionNotFound("No host");
392        }
393        return $this->host;
394    }
395
396    /**
397     * @throws ExceptionNotFound
398     */
399    public function getPath(): string
400    {
401        if ($this->path === null) {
402            throw new ExceptionNotFound("The path was not found");
403        }
404        return $this->path;
405    }
406
407    /**
408     * @throws ExceptionNotFound
409     */
410    public function getFragment(): string
411    {
412        if ($this->fragment === null) {
413            throw new ExceptionNotFound("The fragment was not set");
414        }
415        return $this->fragment;
416    }
417
418
419    public function __toString()
420    {
421        return $this->toString();
422    }
423
424    public function getQueryPropertyValueOrDefault(string $key, string $defaultIfNull)
425    {
426        try {
427            return $this->getQueryPropertyValue($key);
428        } catch (ExceptionNotFound $e) {
429            return $defaultIfNull;
430        }
431    }
432
433    /**
434     * Actual vs expected
435     *
436     * We use this vocabulary (actual/expected) and not (internal/external or left/right) because this function
437     * is mostly used in a test framework.
438     *
439     * @throws ExceptionNotEquals
440     */
441    public function equals(Url $expectedUrl)
442    {
443        /**
444         * Scheme
445         */
446        try {
447            $actualScheme = $this->getScheme();
448        } catch (ExceptionNotFound $e) {
449            $actualScheme = "";
450        }
451        try {
452            $expectedScheme = $expectedUrl->getScheme();
453        } catch (ExceptionNotFound $e) {
454            $expectedScheme = "";
455        }
456        if ($actualScheme !== $expectedScheme) {
457            throw new ExceptionNotEquals("The scheme are not equals ($actualScheme vs $expectedScheme)");
458        }
459        /**
460         * Host
461         */
462        try {
463            $actualHost = $this->getHost();
464        } catch (ExceptionNotFound $e) {
465            $actualHost = "";
466        }
467        try {
468            $expectedHost = $expectedUrl->getHost();
469        } catch (ExceptionNotFound $e) {
470            $expectedHost = "";
471        }
472        if ($actualHost !== $expectedHost) {
473            throw new ExceptionNotEquals("The host are not equals ($actualHost vs $expectedHost)");
474        }
475        /**
476         * Query
477         */
478        $actualQuery = $this->getQueryProperties();
479        $expectedQuery = $expectedUrl->getQueryProperties();
480        foreach ($actualQuery as $key => $value) {
481            $expectedValue = $expectedQuery[$key];
482            if ($expectedValue === null) {
483                throw new ExceptionNotEquals("The expected url does not have the $key property");
484            }
485            if ($expectedValue !== $value) {
486                throw new ExceptionNotEquals("The $key property does not have the same value ($value vs $expectedValue)");
487            }
488            unset($expectedQuery[$key]);
489        }
490        foreach ($expectedQuery as $key => $value) {
491            throw new ExceptionNotEquals("The expected URL has an extra property ($key=$value)");
492        }
493
494        /**
495         * Fragment
496         */
497        try {
498            $actualFragment = $this->getFragment();
499        } catch (ExceptionNotFound $e) {
500            $actualFragment = "";
501        }
502        try {
503            $expectedFragment = $expectedUrl->getFragment();
504        } catch (ExceptionNotFound $e) {
505            $expectedFragment = "";
506        }
507        if ($actualFragment !== $expectedFragment) {
508            throw new ExceptionNotEquals("The fragment are not equals ($actualFragment vs $expectedFragment)");
509        }
510
511    }
512
513    public function setScheme(string $scheme): Url
514    {
515        $this->scheme = $scheme;
516        return $this;
517    }
518
519    public function setHost($host): Url
520    {
521        $this->host = $host;
522        return $this;
523    }
524
525    public function setFragment(string $fragment): Url
526    {
527        $this->fragment = $fragment;
528        return $this;
529    }
530
531    /**
532     * @throws ExceptionNotFound
533     */
534    public function getQueryString($ampersand = Url::AMPERSAND_CHARACTER): string
535    {
536        if (sizeof($this->query) === 0) {
537            throw new ExceptionNotFound("No Query string");
538        }
539        /**
540         * To be able to diff them
541         */
542        $originalArray = $this->query->getOriginalArray();
543        ksort($originalArray);
544
545        /**
546         * We don't use {@link http_build_query} because:
547         *   * it does not the follow the array format (ie s[]=searchword1+seachword2)
548         *   * it output 'key=' instead of `key` when the value is null
549         */
550        $queryString = null;
551        foreach ($originalArray as $key => $value) {
552            if ($queryString !== null) {
553                /**
554                 * HTML encoding (ie {@link self::AMPERSAND_URL_ENCODED_FOR_HTML}
555                 * happens only when outputing to HTML
556                 * The url may also be used elsewhere where &amp; is unknown or not wanted such as css ...
557                 *
558                 * In test, we may ask the url HTML encoded
559                 */
560                $queryString .= $ampersand;
561            }
562            if ($value === null) {
563                $queryString .= urlencode($key);
564            } else {
565                if (is_array($value)) {
566                    for ($i = 0; $i < sizeof($value); $i++) {
567                        $val = $value[$i];
568                        if ($i > 0) {
569                            $queryString .= self::AMPERSAND_CHARACTER;
570                        }
571                        $queryString .= urlencode($key) . "[]=" . urlencode($val);
572                    }
573                } else {
574                    $queryString .= urlencode($key) . "=" . urlencode($value);
575                }
576            }
577        }
578        return $queryString;
579
580
581    }
582
583    /**
584     * @throws ExceptionNotFound
585     */
586    public function getQueryPropertyValueAndRemoveIfPresent(string $key)
587    {
588        $value = $this->getQueryPropertyValue($key);
589        unset($this->query[$key]);
590        return $value;
591    }
592
593
594    /**
595     * @throws ExceptionNotFound
596     */
597    function getLastName(): string
598    {
599        $names = $this->getNames();
600        $namesCount = count($names);
601        if ($namesCount === 0) {
602            throw new ExceptionNotFound("No last name");
603        }
604        return $names[$namesCount - 1];
605
606    }
607
608    /**
609     * @return string
610     * @throws ExceptionNotFound
611     */
612    public function getExtension(): string
613    {
614        if ($this->hasProperty(FetcherRawLocalPath::$MEDIA_QUERY_PARAMETER)) {
615
616            try {
617                return FetcherSystem::createPathFetcherFromUrl($this)->getMime()->getExtension();
618            } catch (ExceptionCompile $e) {
619                LogUtility::internalError("Build error from a Media Fetch URL. We were unable to get the mime. Error: {$e->getMessage()}");
620            }
621
622        }
623        return parent::getExtension();
624    }
625
626
627    function getNames()
628    {
629
630        try {
631            $names = explode(self::PATH_SEP, $this->getPath());
632            return array_slice($names, 1);
633        } catch (ExceptionNotFound $e) {
634            return [];
635        }
636
637    }
638
639    /**
640     * @throws ExceptionNotFound
641     */
642    function getParent(): Url
643    {
644        $names = $this->getNames();
645        $count = count($names);
646        if ($count === 0) {
647            throw new ExceptionNotFound("No Parent");
648        }
649        $parentPath = implode(self::PATH_SEP, array_splice($names, 0, $count - 1));
650        return $this->setPath($parentPath);
651    }
652
653    function toAbsoluteId(): string
654    {
655        try {
656            return $this->getPath();
657        } catch (ExceptionNotFound $e) {
658            return "";
659        }
660    }
661
662    function toAbsolutePath(): Url
663    {
664        return $this->toAbsoluteUrl();
665    }
666
667    function resolve(string $name): Url
668    {
669        try {
670            $path = $this->getPath();
671            if ($this->path[strlen($path) - 1] === URL::PATH_SEP) {
672                $this->path .= $name;
673            } else {
674                $this->path .= URL::PATH_SEP . $name;
675            }
676            return $this;
677        } catch (ExceptionNotFound $e) {
678            $this->setPath($name);
679            return $this;
680        }
681
682    }
683
684    /**
685     * @param string $ampersand
686     * @return string
687     */
688    public function toString(string $ampersand = Url::AMPERSAND_CHARACTER): string
689    {
690
691        try {
692            $scheme = $this->getScheme();
693        } catch (ExceptionNotFound $e) {
694            $scheme = null;
695        }
696
697
698        switch ($scheme) {
699            case LocalFileSystem::SCHEME:
700                /**
701                 * file://host/path
702                 */
703                $base = "$scheme://";
704                try {
705                    $base = "$base{$this->getHost()}";
706                } catch (ExceptionNotFound $e) {
707                    // no host
708                }
709                try {
710                    $path = $this->getAbsolutePath();
711                    // linux, network share (file://host/path)
712                    $base = "$base{$path}";
713                } catch (ExceptionNotFound $e) {
714                    // no path
715                }
716                return $base;
717            case "mailto":
718            case "whatsapp":
719            case "skype":
720                /**
721                 * Skype. Example: skype:echo123?call
722                 * https://docs.microsoft.com/en-us/skype-sdk/skypeuris/skypeuris
723                 * Mailto: Example: mailto:java-net@java.sun.com?subject=yolo
724                 * https://datacadamia.com/marketing/email/mailto
725                 */
726                $base = "$scheme:";
727                try {
728                    $base = "$base{$this->getPath()}";
729                } catch (ExceptionNotFound $e) {
730                    // no path
731                }
732                try {
733                    $base = "$base?{$this->getQueryString()}";
734                } catch (ExceptionNotFound $e) {
735                    // no query string
736                }
737                try {
738                    $base = "$base#{$this->getFragment()}";
739                } catch (ExceptionNotFound $e) {
740                    // no fragment
741                }
742                return $base;
743            case "http":
744            case "https":
745            case "ftp":
746            default:
747                /**
748                 * Url Rewrite
749                 * Absolute vs Relative, __media, ...
750                 */
751                if ($this->withRewrite) {
752                    UrlRewrite::rewrite($this);
753                }
754                /**
755                 * Rewrite may have set a default scheme
756                 * We read it again
757                 */
758                try {
759                    $scheme = $this->getScheme();
760                } catch (ExceptionNotFound $e) {
761                    $scheme = null;
762                }
763                try {
764                    $host = $this->getHost();
765                } catch (ExceptionNotFound $e) {
766                    $host = null;
767                }
768                /**
769                 * Absolute/Relative Uri
770                 */
771                $base = "";
772                if ($host !== null) {
773                    if ($scheme !== null) {
774                        $base = "{$scheme}://";
775                    }
776                    $base = "$base{$host}";
777                    try {
778                        $base = "$base:{$this->getPort()}";
779                    } catch (ExceptionNotFound $e) {
780                        // no port
781                    }
782                } else {
783                    if (!in_array($scheme, self::RELATIVE_URL_SCHEMES) && $scheme !== null) {
784                        $base = "{$scheme}:";
785                    }
786                }
787
788                try {
789                    $base = "$base{$this->getAbsolutePath()}";
790                } catch (ExceptionNotFound $e) {
791                    // ok
792                }
793
794                try {
795                    $base = "$base?{$this->getQueryString($ampersand)}";
796                } catch (ExceptionNotFound $e) {
797                    // ok
798                }
799
800                try {
801                    $base = "$base#{$this->getFragment()}";
802                } catch (ExceptionNotFound $e) {
803                    // ok
804                }
805                return $base;
806        }
807
808
809    }
810
811    /**
812     * Query parameter can have several values
813     * This function makes sure that there is only one value for one key
814     * if the value are different, the value will be added
815     * @param string $key
816     * @param string $value
817     * @return Url
818     */
819    public function addQueryParameterIfNotActualSameValue(string $key, string $value): Url
820    {
821        try {
822            $actualValue = $this->getQueryPropertyValue($key);
823            if ($actualValue !== $value) {
824                $this->addQueryParameter($key, $value);
825            }
826        } catch (ExceptionNotFound $e) {
827            $this->addQueryParameter($key, $value);
828        }
829
830        return $this;
831
832    }
833
834    function getUrl(): Url
835    {
836        return $this;
837    }
838
839    public function toHtmlString(): string
840    {
841        return $this->toString(Url::AMPERSAND_URL_ENCODED_FOR_HTML);
842    }
843
844    /**
845     * @throws ExceptionNotFound
846     */
847    private function getPort(): int
848    {
849        if ($this->port === null) {
850            throw new ExceptionNotFound("No port specified");
851        }
852        return $this->port;
853    }
854
855    public function addQueryParameterIfNotPresent(string $key, string $value)
856    {
857        if (!$this->hasProperty($key)) {
858            $this->addQueryParameterIfNotActualSameValue($key, $value);
859        }
860    }
861
862    /**
863     * Set/replace a query parameter with the new value
864     * @param string $key
865     * @param string $value
866     * @return Url
867     */
868    public function setQueryParameter(string $key, string $value): Url
869    {
870        $this->deleteQueryParameter($key);
871        $this->addQueryParameter($key, $value);
872        return $this;
873    }
874
875    public function deleteQueryParameter(string $key)
876    {
877        unset($this->query[$key]);
878    }
879
880    /**
881     * @return string - An url in the DOM use the ampersand character
882     * If you want to check the value of a DOM attribute, you need to check it with this value
883     */
884    public function toDomString(): string
885    {
886        // ampersand for dom string
887        return $this->toString();
888    }
889
890    public function toCssString(): string
891    {
892        // ampersand for css
893        return $this->toString();
894    }
895
896    /**
897     * @return bool - if the url points to the same website than the host
898     */
899    public function isExternal(): bool
900    {
901        try {
902            $localHost = Url::createEmpty()->toAbsoluteUrl()->getHost();
903            return $localHost !== $this->getHost();
904        } catch (ExceptionNotFound $e) {
905            // no host meaning that the url is relative and then local
906            return false;
907        }
908    }
909
910    /**
911     * In a url, in a case, the path should be absolute
912     * This function makes it absolute if not.
913     * In case of messaging scheme (mailto, whatsapp, ...), this is not the case
914     * @throws ExceptionNotFound
915     */
916    private function getAbsolutePath(): string
917    {
918        $pathString = $this->getPath();
919        if ($pathString[0] !== "/") {
920            return "/{$pathString}";
921        }
922        return $pathString;
923    }
924
925
926    /**
927     * @throws ExceptionBadSyntax
928     * @throws ExceptionBadArgument
929     */
930    public static function createFromUri(string $uri): Path
931    {
932        return new Url($uri);
933    }
934
935    public function deleteQueryProperties(): Url
936    {
937        $this->query = new ArrayCaseInsensitive();;
938        return $this;
939    }
940
941    public function withoutRewrite(): Url
942    {
943        $this->withRewrite = false;
944        return $this;
945    }
946
947}
948