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