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