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