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