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