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