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