xref: /plugin/combo/ComboStrap/Web/Url.php (revision 70bbd7f1f72440223cc13f3495efdcb2b0a11514)
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    public function setFragment(string $fragment): Url
543    {
544        $this->fragment = $fragment;
545        return $this;
546    }
547
548    /**
549     * @throws ExceptionNotFound
550     */
551    public function getQueryString($ampersand = Url::AMPERSAND_CHARACTER): string
552    {
553        if (sizeof($this->query) === 0) {
554            throw new ExceptionNotFound("No Query string");
555        }
556        /**
557         * To be able to diff them
558         */
559        $originalArray = $this->query->getOriginalArray();
560        ksort($originalArray);
561
562        /**
563         * We don't use {@link http_build_query} because:
564         *   * it does not the follow the array format (ie s[]=searchword1+seachword2)
565         *   * it output 'key=' instead of `key` when the value is null
566         */
567        $queryString = null;
568        foreach ($originalArray as $key => $value) {
569            if ($queryString !== null) {
570                /**
571                 * HTML encoding (ie {@link self::AMPERSAND_URL_ENCODED_FOR_HTML}
572                 * happens only when outputing to HTML
573                 * The url may also be used elsewhere where &amp; is unknown or not wanted such as css ...
574                 *
575                 * In test, we may ask the url HTML encoded
576                 */
577                $queryString .= $ampersand;
578            }
579            if ($value === null) {
580                $queryString .= urlencode($key);
581            } else {
582                if (is_array($value)) {
583                    for ($i = 0; $i < sizeof($value); $i++) {
584                        $val = $value[$i];
585                        if ($i > 0) {
586                            $queryString .= self::AMPERSAND_CHARACTER;
587                        }
588                        $queryString .= urlencode($key) . "[]=" . urlencode($val);
589                    }
590                } else {
591                    $queryString .= urlencode($key) . "=" . urlencode($value);
592                }
593            }
594        }
595        return $queryString;
596
597
598    }
599
600    /**
601     * @throws ExceptionNotFound
602     */
603    public function getQueryPropertyValueAndRemoveIfPresent(string $key)
604    {
605        $value = $this->getQueryPropertyValue($key);
606        unset($this->query[$key]);
607        return $value;
608    }
609
610
611    /**
612     * @throws ExceptionNotFound
613     */
614    function getLastName(): string
615    {
616        $names = $this->getNames();
617        $namesCount = count($names);
618        if ($namesCount === 0) {
619            throw new ExceptionNotFound("No last name");
620        }
621        return $names[$namesCount - 1];
622
623    }
624
625    /**
626     * @return string
627     * @throws ExceptionNotFound
628     */
629    public function getExtension(): string
630    {
631        if ($this->hasProperty(MediaMarkup::$MEDIA_QUERY_PARAMETER)) {
632
633            try {
634                return FetcherSystem::createPathFetcherFromUrl($this)->getMime()->getExtension();
635            } catch (ExceptionCompile $e) {
636                LogUtility::internalError("Build error from a Media Fetch URL. We were unable to get the mime. Error: {$e->getMessage()}");
637            }
638
639        }
640        return parent::getExtension();
641    }
642
643
644    function getNames()
645    {
646
647        try {
648            $names = explode(self::PATH_SEP, $this->getPath());
649            return array_slice($names, 1);
650        } catch (ExceptionNotFound $e) {
651            return [];
652        }
653
654    }
655
656    /**
657     * @throws ExceptionNotFound
658     */
659    function getParent(): Url
660    {
661        $names = $this->getNames();
662        $count = count($names);
663        if ($count === 0) {
664            throw new ExceptionNotFound("No Parent");
665        }
666        $parentPath = implode(self::PATH_SEP, array_splice($names, 0, $count - 1));
667        return $this->setPath($parentPath);
668    }
669
670    function toAbsoluteId(): string
671    {
672        try {
673            return $this->getPath();
674        } catch (ExceptionNotFound $e) {
675            return "";
676        }
677    }
678
679    function toAbsolutePath(): Url
680    {
681        return $this->toAbsoluteUrl();
682    }
683
684    function resolve(string $name): Url
685    {
686        try {
687            $path = $this->getPath();
688            if ($this->path[strlen($path) - 1] === URL::PATH_SEP) {
689                $this->path .= $name;
690            } else {
691                $this->path .= URL::PATH_SEP . $name;
692            }
693            return $this;
694        } catch (ExceptionNotFound $e) {
695            $this->setPath($name);
696            return $this;
697        }
698
699    }
700
701    /**
702     * @param string $ampersand
703     * @return string
704     */
705    public function toString(string $ampersand = Url::AMPERSAND_CHARACTER): string
706    {
707
708        try {
709            $scheme = $this->getScheme();
710        } catch (ExceptionNotFound $e) {
711            $scheme = null;
712        }
713
714
715        switch ($scheme) {
716            case LocalFileSystem::SCHEME:
717                /**
718                 * file://host/path
719                 */
720                $base = "$scheme://";
721                try {
722                    $base = "$base{$this->getHost()}";
723                } catch (ExceptionNotFound $e) {
724                    // no host
725                }
726                try {
727                    $path = $this->getAbsolutePath();
728                    // linux, network share (file://host/path)
729                    $base = "$base{$path}";
730                } catch (ExceptionNotFound $e) {
731                    // no path
732                }
733                return $base;
734            case "mailto":
735            case "whatsapp":
736            case "skype":
737                /**
738                 * Skype. Example: skype:echo123?call
739                 * https://docs.microsoft.com/en-us/skype-sdk/skypeuris/skypeuris
740                 * Mailto: Example: mailto:java-net@java.sun.com?subject=yolo
741                 * https://datacadamia.com/marketing/email/mailto
742                 */
743                $base = "$scheme:";
744                try {
745                    $base = "$base{$this->getPath()}";
746                } catch (ExceptionNotFound $e) {
747                    // no path
748                }
749                try {
750                    $base = "$base?{$this->getQueryString()}";
751                } catch (ExceptionNotFound $e) {
752                    // no query string
753                }
754                try {
755                    $base = "$base#{$this->getFragment()}";
756                } catch (ExceptionNotFound $e) {
757                    // no fragment
758                }
759                return $base;
760            case "http":
761            case "https":
762            case "ftp":
763            default:
764                /**
765                 * Url Rewrite
766                 * Absolute vs Relative, __media, ...
767                 */
768                if ($this->withRewrite) {
769                    UrlRewrite::rewrite($this);
770                }
771                /**
772                 * Rewrite may have set a default scheme
773                 * We read it again
774                 */
775                try {
776                    $scheme = $this->getScheme();
777                } catch (ExceptionNotFound $e) {
778                    $scheme = null;
779                }
780                try {
781                    $host = $this->getHost();
782                } catch (ExceptionNotFound $e) {
783                    $host = null;
784                }
785                /**
786                 * Absolute/Relative Uri
787                 */
788                $base = "";
789                if ($host !== null) {
790                    if ($scheme !== null) {
791                        $base = "{$scheme}://";
792                    }
793                    $base = "$base{$host}";
794                    try {
795                        $base = "$base:{$this->getPort()}";
796                    } catch (ExceptionNotFound $e) {
797                        // no port
798                    }
799                } else {
800                    if (!in_array($scheme, self::RELATIVE_URL_SCHEMES) && $scheme !== null) {
801                        $base = "{$scheme}:";
802                    }
803                }
804
805                try {
806                    $base = "$base{$this->getAbsolutePath()}";
807                } catch (ExceptionNotFound $e) {
808                    // ok
809                }
810
811                try {
812                    $base = "$base?{$this->getQueryString($ampersand)}";
813                } catch (ExceptionNotFound $e) {
814                    // ok
815                }
816
817                try {
818                    $base = "$base#{$this->getFragment()}";
819                } catch (ExceptionNotFound $e) {
820                    // ok
821                }
822                return $base;
823        }
824
825
826    }
827
828    /**
829     * Query parameter can have several values
830     * This function makes sure that there is only one value for one key
831     * if the value are different, the value will be added
832     * @param string $key
833     * @param string $value
834     * @return Url
835     */
836    public function addQueryParameterIfNotActualSameValue(string $key, string $value): Url
837    {
838        try {
839            $actualValue = $this->getQueryPropertyValue($key);
840            if ($actualValue !== $value) {
841                $this->addQueryParameter($key, $value);
842            }
843        } catch (ExceptionNotFound $e) {
844            $this->addQueryParameter($key, $value);
845        }
846
847        return $this;
848
849    }
850
851    function getUrl(): Url
852    {
853        return $this;
854    }
855
856    public function toHtmlString(): string
857    {
858        return $this->toString(Url::AMPERSAND_URL_ENCODED_FOR_HTML);
859    }
860
861    /**
862     * @throws ExceptionNotFound
863     */
864    private function getPort(): int
865    {
866        if ($this->port === null) {
867            throw new ExceptionNotFound("No port specified");
868        }
869        return $this->port;
870    }
871
872    public function addQueryParameterIfNotPresent(string $key, string $value)
873    {
874        if (!$this->hasProperty($key)) {
875            $this->addQueryParameterIfNotActualSameValue($key, $value);
876        }
877    }
878
879    /**
880     * Set/replace a query parameter with the new value
881     * @param string $key
882     * @param string $value
883     * @return Url
884     */
885    public function setQueryParameter(string $key, string $value): Url
886    {
887        $this->deleteQueryParameter($key);
888        $this->addQueryParameter($key, $value);
889        return $this;
890    }
891
892    public function deleteQueryParameter(string $key)
893    {
894        unset($this->query[$key]);
895    }
896
897    /**
898     * @return string - An url in the DOM use the ampersand character
899     * If you want to check the value of a DOM attribute, you need to check it with this value
900     */
901    public function toDomString(): string
902    {
903        // ampersand for dom string
904        return $this->toString();
905    }
906
907    public function toCssString(): string
908    {
909        // ampersand for css
910        return $this->toString();
911    }
912
913    /**
914     * @return bool - if the url points to the same website than the host
915     */
916    public function isExternal(): bool
917    {
918        try {
919            // We set the path, otherwise it's seen as a local url
920            $localHost = Url::createEmpty()->setPath("/")->toAbsoluteUrl()->getHost();
921            return $localHost !== $this->getHost();
922        } catch (ExceptionNotFound $e) {
923            // no host meaning that the url is relative and then local
924            return false;
925        }
926    }
927
928    /**
929     * In a url, in a case, the path should be absolute
930     * This function makes it absolute if not.
931     * In case of messaging scheme (mailto, whatsapp, ...), this is not the case
932     * @throws ExceptionNotFound
933     */
934    private function getAbsolutePath(): string
935    {
936        $pathString = $this->getPath();
937        if ($pathString[0] !== "/") {
938            return "/{$pathString}";
939        }
940        return $pathString;
941    }
942
943
944    /**
945     * @throws ExceptionBadSyntax
946     * @throws ExceptionBadArgument
947     */
948    public static function createFromUri(string $uri): Path
949    {
950        return new Url($uri);
951    }
952
953    public function deleteQueryProperties(): Url
954    {
955        $this->query = new ArrayCaseInsensitive();;
956        return $this;
957    }
958
959    public function withoutRewrite(): Url
960    {
961        $this->withRewrite = false;
962        return $this;
963    }
964
965    /**
966     * Dokuwiki utility to check if the URL is local
967     * (ie has not path, only a fragment such as #id)
968     * @return bool
969     */
970    public function isLocal(): bool
971    {
972        if ($this->path !== null) {
973            return false;
974        }
975        /**
976         * The path paramater of Dokuwiki
977         */
978        if ($this->hasProperty(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE)) {
979            return false;
980        }
981        if ($this->hasProperty(MediaMarkup::$MEDIA_QUERY_PARAMETER)) {
982            return false;
983        }
984        if ($this->hasProperty(FetcherRawLocalPath::SRC_QUERY_PARAMETER)) {
985            return false;
986        }
987        return true;
988    }
989
990
991}
992