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