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