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