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