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