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 /** 543 * @param string $fragment 544 * @return $this 545 * Example `#step:11:24728`, this fragment is valid! 546 */ 547 public function setFragment(string $fragment): Url 548 { 549 $this->fragment = $fragment; 550 return $this; 551 } 552 553 /** 554 * @throws ExceptionNotFound 555 */ 556 public function getQueryString($ampersand = Url::AMPERSAND_CHARACTER): string 557 { 558 if (sizeof($this->query) === 0) { 559 throw new ExceptionNotFound("No Query string"); 560 } 561 /** 562 * To be able to diff them 563 */ 564 $originalArray = $this->query->getOriginalArray(); 565 ksort($originalArray); 566 567 /** 568 * We don't use {@link http_build_query} because: 569 * * it does not the follow the array format (ie s[]=searchword1+seachword2) 570 * * it output 'key=' instead of `key` when the value is null 571 */ 572 $queryString = null; 573 foreach ($originalArray as $key => $value) { 574 if ($queryString !== null) { 575 /** 576 * HTML encoding (ie {@link self::AMPERSAND_URL_ENCODED_FOR_HTML} 577 * happens only when outputing to HTML 578 * The url may also be used elsewhere where & is unknown or not wanted such as css ... 579 * 580 * In test, we may ask the url HTML encoded 581 */ 582 $queryString .= $ampersand; 583 } 584 if ($value === null) { 585 $queryString .= urlencode($key); 586 } else { 587 if (is_array($value)) { 588 for ($i = 0; $i < sizeof($value); $i++) { 589 $val = $value[$i]; 590 if ($i > 0) { 591 $queryString .= self::AMPERSAND_CHARACTER; 592 } 593 $queryString .= urlencode($key) . "[]=" . urlencode($val); 594 } 595 } else { 596 $queryString .= urlencode($key) . "=" . urlencode($value); 597 } 598 } 599 } 600 return $queryString; 601 602 603 } 604 605 /** 606 * @throws ExceptionNotFound 607 */ 608 public function getQueryPropertyValueAndRemoveIfPresent(string $key) 609 { 610 $value = $this->getQueryPropertyValue($key); 611 unset($this->query[$key]); 612 return $value; 613 } 614 615 616 /** 617 * @throws ExceptionNotFound 618 */ 619 function getLastName(): string 620 { 621 $names = $this->getNames(); 622 $namesCount = count($names); 623 if ($namesCount === 0) { 624 throw new ExceptionNotFound("No last name"); 625 } 626 return $names[$namesCount - 1]; 627 628 } 629 630 /** 631 * @return string 632 * @throws ExceptionNotFound 633 */ 634 public function getExtension(): string 635 { 636 if ($this->hasProperty(MediaMarkup::$MEDIA_QUERY_PARAMETER)) { 637 638 try { 639 return FetcherSystem::createPathFetcherFromUrl($this)->getMime()->getExtension(); 640 } catch (ExceptionCompile $e) { 641 LogUtility::internalError("Build error from a Media Fetch URL. We were unable to get the mime. Error: {$e->getMessage()}"); 642 } 643 644 } 645 return parent::getExtension(); 646 } 647 648 649 function getNames() 650 { 651 652 try { 653 $names = explode(self::PATH_SEP, $this->getPath()); 654 return array_slice($names, 1); 655 } catch (ExceptionNotFound $e) { 656 return []; 657 } 658 659 } 660 661 /** 662 * @throws ExceptionNotFound 663 */ 664 function getParent(): Url 665 { 666 $names = $this->getNames(); 667 $count = count($names); 668 if ($count === 0) { 669 throw new ExceptionNotFound("No Parent"); 670 } 671 $parentPath = implode(self::PATH_SEP, array_splice($names, 0, $count - 1)); 672 return $this->setPath($parentPath); 673 } 674 675 function toAbsoluteId(): string 676 { 677 try { 678 return $this->getPath(); 679 } catch (ExceptionNotFound $e) { 680 return ""; 681 } 682 } 683 684 function toAbsolutePath(): Url 685 { 686 return $this->toAbsoluteUrl(); 687 } 688 689 function resolve(string $name): Url 690 { 691 try { 692 $path = $this->getPath(); 693 if ($this->path[strlen($path) - 1] === URL::PATH_SEP) { 694 $this->path .= $name; 695 } else { 696 $this->path .= URL::PATH_SEP . $name; 697 } 698 return $this; 699 } catch (ExceptionNotFound $e) { 700 $this->setPath($name); 701 return $this; 702 } 703 704 } 705 706 /** 707 * @param string $ampersand 708 * @return string 709 */ 710 public function toString(string $ampersand = Url::AMPERSAND_CHARACTER): string 711 { 712 713 try { 714 $scheme = $this->getScheme(); 715 } catch (ExceptionNotFound $e) { 716 $scheme = null; 717 } 718 719 720 switch ($scheme) { 721 case LocalFileSystem::SCHEME: 722 /** 723 * file://host/path 724 */ 725 $base = "$scheme://"; 726 try { 727 $base = "$base{$this->getHost()}"; 728 } catch (ExceptionNotFound $e) { 729 // no host 730 } 731 try { 732 $path = $this->getAbsolutePath(); 733 // linux, network share (file://host/path) 734 $base = "$base{$path}"; 735 } catch (ExceptionNotFound $e) { 736 // no path 737 } 738 return $base; 739 case "mailto": 740 case "whatsapp": 741 case "skype": 742 /** 743 * Skype. Example: skype:echo123?call 744 * https://docs.microsoft.com/en-us/skype-sdk/skypeuris/skypeuris 745 * Mailto: Example: mailto:java-net@java.sun.com?subject=yolo 746 * https://datacadamia.com/marketing/email/mailto 747 */ 748 $base = "$scheme:"; 749 try { 750 $base = "$base{$this->getPath()}"; 751 } catch (ExceptionNotFound $e) { 752 // no path 753 } 754 try { 755 $base = "$base?{$this->getQueryString()}"; 756 } catch (ExceptionNotFound $e) { 757 // no query string 758 } 759 try { 760 $base = "$base#{$this->getFragment()}"; 761 } catch (ExceptionNotFound $e) { 762 // no fragment 763 } 764 return $base; 765 case "http": 766 case "https": 767 case "ftp": 768 default: 769 /** 770 * Url Rewrite 771 * Absolute vs Relative, __media, ... 772 */ 773 if ($this->withRewrite) { 774 UrlRewrite::rewrite($this); 775 } 776 /** 777 * Rewrite may have set a default scheme 778 * We read it again 779 */ 780 try { 781 $scheme = $this->getScheme(); 782 } catch (ExceptionNotFound $e) { 783 $scheme = null; 784 } 785 try { 786 $host = $this->getHost(); 787 } catch (ExceptionNotFound $e) { 788 $host = null; 789 } 790 /** 791 * Absolute/Relative Uri 792 */ 793 $base = ""; 794 if ($host !== null) { 795 if ($scheme !== null) { 796 $base = "{$scheme}://"; 797 } 798 $base = "$base{$host}"; 799 try { 800 $base = "$base:{$this->getPort()}"; 801 } catch (ExceptionNotFound $e) { 802 // no port 803 } 804 } else { 805 if (!in_array($scheme, self::RELATIVE_URL_SCHEMES) && $scheme !== null) { 806 $base = "{$scheme}:"; 807 } 808 } 809 810 try { 811 $base = "$base{$this->getAbsolutePath()}"; 812 } catch (ExceptionNotFound $e) { 813 // ok 814 } 815 816 try { 817 $base = "$base?{$this->getQueryString($ampersand)}"; 818 } catch (ExceptionNotFound $e) { 819 // ok 820 } 821 822 try { 823 $base = "$base#{$this->getFragment()}"; 824 } catch (ExceptionNotFound $e) { 825 // ok 826 } 827 return $base; 828 } 829 830 831 } 832 833 /** 834 * Query parameter can have several values 835 * This function makes sure that there is only one value for one key 836 * if the value are different, the value will be added 837 * @param string $key 838 * @param string $value 839 * @return Url 840 */ 841 public function addQueryParameterIfNotActualSameValue(string $key, string $value): Url 842 { 843 try { 844 $actualValue = $this->getQueryPropertyValue($key); 845 if ($actualValue !== $value) { 846 $this->addQueryParameter($key, $value); 847 } 848 } catch (ExceptionNotFound $e) { 849 $this->addQueryParameter($key, $value); 850 } 851 852 return $this; 853 854 } 855 856 function getUrl(): Url 857 { 858 return $this; 859 } 860 861 public function toHtmlString(): string 862 { 863 return $this->toString(Url::AMPERSAND_URL_ENCODED_FOR_HTML); 864 } 865 866 /** 867 * @throws ExceptionNotFound 868 */ 869 private function getPort(): int 870 { 871 if ($this->port === null) { 872 throw new ExceptionNotFound("No port specified"); 873 } 874 return $this->port; 875 } 876 877 public function addQueryParameterIfNotPresent(string $key, string $value) 878 { 879 if (!$this->hasProperty($key)) { 880 $this->addQueryParameterIfNotActualSameValue($key, $value); 881 } 882 } 883 884 /** 885 * Set/replace a query parameter with the new value 886 * @param string $key 887 * @param string $value 888 * @return Url 889 */ 890 public function setQueryParameter(string $key, string $value): Url 891 { 892 $this->deleteQueryParameter($key); 893 $this->addQueryParameter($key, $value); 894 return $this; 895 } 896 897 public function deleteQueryParameter(string $key) 898 { 899 unset($this->query[$key]); 900 } 901 902 /** 903 * @return string - An url in the DOM use the ampersand character 904 * If you want to check the value of a DOM attribute, you need to check it with this value 905 */ 906 public function toDomString(): string 907 { 908 // ampersand for dom string 909 return $this->toString(); 910 } 911 912 public function toCssString(): string 913 { 914 // ampersand for css 915 return $this->toString(); 916 } 917 918 /** 919 * @return bool - if the url points to the same website than the host 920 */ 921 public function isExternal(): bool 922 { 923 try { 924 // We set the path, otherwise it's seen as a local url 925 $localHost = Url::createEmpty()->setPath("/")->toAbsoluteUrl()->getHost(); 926 return $localHost !== $this->getHost(); 927 } catch (ExceptionNotFound $e) { 928 // no host meaning that the url is relative and then local 929 return false; 930 } 931 } 932 933 /** 934 * In a url, in a case, the path should be absolute 935 * This function makes it absolute if not. 936 * In case of messaging scheme (mailto, whatsapp, ...), this is not the case 937 * @throws ExceptionNotFound 938 */ 939 private function getAbsolutePath(): string 940 { 941 $pathString = $this->getPath(); 942 if ($pathString[0] !== "/") { 943 return "/{$pathString}"; 944 } 945 return $pathString; 946 } 947 948 949 /** 950 * @throws ExceptionBadSyntax 951 * @throws ExceptionBadArgument 952 */ 953 public static function createFromUri(string $uri): Path 954 { 955 return new Url($uri); 956 } 957 958 public function deleteQueryProperties(): Url 959 { 960 $this->query = new ArrayCaseInsensitive();; 961 return $this; 962 } 963 964 public function withoutRewrite(): Url 965 { 966 $this->withRewrite = false; 967 return $this; 968 } 969 970 /** 971 * Dokuwiki utility to check if the URL is local 972 * (ie has not path, only a fragment such as #id) 973 * @return bool 974 */ 975 public function isLocal(): bool 976 { 977 if ($this->path !== null) { 978 return false; 979 } 980 /** 981 * The path paramater of Dokuwiki 982 */ 983 if ($this->hasProperty(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE)) { 984 return false; 985 } 986 if ($this->hasProperty(MediaMarkup::$MEDIA_QUERY_PARAMETER)) { 987 return false; 988 } 989 if ($this->hasProperty(FetcherRawLocalPath::SRC_QUERY_PARAMETER)) { 990 return false; 991 } 992 return true; 993 } 994 995 996} 997