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