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 $https = $_SERVER['HTTPS'] ?? null; 409 if (empty($https)) { 410 $this->setScheme("http"); 411 } else { 412 $this->setScheme("https"); 413 } 414 } 415 try { 416 $this->getHost(); 417 } catch (ExceptionNotFound $e) { 418 $remoteHost = Site::getServerHost(); 419 $this->setHost($remoteHost); 420 421 } 422 return $this; 423 } 424 425 /** 426 * @return string - utility function that call {@link Url::toAbsoluteUrl()} absolute and {@link Url::toString()} 427 */ 428 public function toAbsoluteUrlString(): string 429 { 430 $this->toAbsoluteUrl(); 431 return $this->toString(); 432 } 433 434 /** 435 * @throws ExceptionNotFound 436 */ 437 public function getHost(): string 438 { 439 if ($this->host === null) { 440 throw new ExceptionNotFound("No host"); 441 } 442 return $this->host; 443 } 444 445 /** 446 * @throws ExceptionNotFound 447 */ 448 public function getPath(): string 449 { 450 if ($this->path === null || $this->path === '/') { 451 throw new ExceptionNotFound("The path was not found"); 452 } 453 return $this->path; 454 } 455 456 /** 457 * @throws ExceptionNotFound 458 */ 459 public function getFragment(): string 460 { 461 if ($this->fragment === null) { 462 throw new ExceptionNotFound("The fragment was not set"); 463 } 464 return $this->fragment; 465 } 466 467 468 public function __toString() 469 { 470 return $this->toString(); 471 } 472 473 public function getQueryPropertyValueOrDefault(string $key, string $defaultIfNull) 474 { 475 try { 476 return $this->getQueryPropertyValue($key); 477 } catch (ExceptionNotFound $e) { 478 return $defaultIfNull; 479 } 480 } 481 482 /** 483 * Actual vs expected 484 * 485 * We use this vocabulary (actual/expected) and not (internal/external or left/right) because this function 486 * is mostly used in a test framework. 487 * 488 * @throws ExceptionNotEquals 489 */ 490 public function equals(Url $expectedUrl) 491 { 492 /** 493 * Scheme 494 */ 495 try { 496 $actualScheme = $this->getScheme(); 497 } catch (ExceptionNotFound $e) { 498 $actualScheme = ""; 499 } 500 try { 501 $expectedScheme = $expectedUrl->getScheme(); 502 } catch (ExceptionNotFound $e) { 503 $expectedScheme = ""; 504 } 505 if ($actualScheme !== $expectedScheme) { 506 throw new ExceptionNotEquals("The scheme are not equals ($actualScheme vs $expectedScheme)"); 507 } 508 /** 509 * Host 510 */ 511 try { 512 $actualHost = $this->getHost(); 513 } catch (ExceptionNotFound $e) { 514 $actualHost = ""; 515 } 516 try { 517 $expectedHost = $expectedUrl->getHost(); 518 } catch (ExceptionNotFound $e) { 519 $expectedHost = ""; 520 } 521 if ($actualHost !== $expectedHost) { 522 throw new ExceptionNotEquals("The host are not equals ($actualHost vs $expectedHost)"); 523 } 524 /** 525 * Query 526 */ 527 $actualQuery = $this->getQueryProperties(); 528 $expectedQuery = $expectedUrl->getQueryProperties(); 529 foreach ($actualQuery as $key => $value) { 530 $expectedValue = $expectedQuery[$key]; 531 if ($expectedValue === null) { 532 throw new ExceptionNotEquals("The expected url does not have the $key property"); 533 } 534 if ($expectedValue !== $value) { 535 throw new ExceptionNotEquals("The $key property does not have the same value ($value vs $expectedValue)"); 536 } 537 unset($expectedQuery[$key]); 538 } 539 foreach ($expectedQuery as $key => $value) { 540 throw new ExceptionNotEquals("The expected URL has an extra property ($key=$value)"); 541 } 542 543 /** 544 * Fragment 545 */ 546 try { 547 $actualFragment = $this->getFragment(); 548 } catch (ExceptionNotFound $e) { 549 $actualFragment = ""; 550 } 551 try { 552 $expectedFragment = $expectedUrl->getFragment(); 553 } catch (ExceptionNotFound $e) { 554 $expectedFragment = ""; 555 } 556 if ($actualFragment !== $expectedFragment) { 557 throw new ExceptionNotEquals("The fragment are not equals ($actualFragment vs $expectedFragment)"); 558 } 559 560 } 561 562 public function setScheme(string $scheme): Url 563 { 564 $this->scheme = $scheme; 565 return $this; 566 } 567 568 public function setHost($host): Url 569 { 570 $this->host = $host; 571 return $this; 572 } 573 574 /** 575 * @param string $fragment 576 * @return $this 577 * Example `#step:11:24728`, this fragment is valid! 578 */ 579 public function setFragment(string $fragment): Url 580 { 581 $this->fragment = $fragment; 582 return $this; 583 } 584 585 /** 586 * @throws ExceptionNotFound 587 */ 588 public function getQueryString($ampersand = Url::AMPERSAND_CHARACTER): string 589 { 590 if (sizeof($this->query) === 0) { 591 throw new ExceptionNotFound("No Query string"); 592 } 593 /** 594 * To be able to diff them 595 */ 596 $originalArray = $this->query->getOriginalArray(); 597 ksort($originalArray); 598 599 /** 600 * We don't use {@link http_build_query} because: 601 * * it does not the follow the array format (ie s[]=searchword1+seachword2) 602 * * it output 'key=' instead of `key` when the value is null 603 */ 604 $queryString = null; 605 foreach ($originalArray as $key => $value) { 606 if ($queryString !== null) { 607 /** 608 * HTML encoding (ie {@link self::AMPERSAND_URL_ENCODED_FOR_HTML} 609 * happens only when outputing to HTML 610 * The url may also be used elsewhere where & is unknown or not wanted such as css ... 611 * 612 * In test, we may ask the url HTML encoded 613 */ 614 $queryString .= $ampersand; 615 } 616 if ($value === null) { 617 $queryString .= urlencode($key); 618 } else { 619 if (is_array($value)) { 620 for ($i = 0; $i < sizeof($value); $i++) { 621 $val = $value[$i]; 622 if ($i > 0) { 623 $queryString .= self::AMPERSAND_CHARACTER; 624 } 625 $queryString .= urlencode($key) . "[]=" . urlencode($val); 626 } 627 } else { 628 $queryString .= urlencode($key) . "=" . urlencode($value); 629 } 630 } 631 } 632 return $queryString; 633 634 635 } 636 637 /** 638 * @throws ExceptionNotFound 639 */ 640 public function getQueryPropertyValueAndRemoveIfPresent(string $key) 641 { 642 $value = $this->getQueryPropertyValue($key); 643 unset($this->query[$key]); 644 return $value; 645 } 646 647 648 /** 649 * @throws ExceptionNotFound 650 */ 651 function getLastName(): string 652 { 653 $names = $this->getNames(); 654 $namesCount = count($names); 655 if ($namesCount === 0) { 656 throw new ExceptionNotFound("No last name"); 657 } 658 return $names[$namesCount - 1]; 659 660 } 661 662 /** 663 * @return string 664 * @throws ExceptionNotFound 665 */ 666 public function getExtension(): string 667 { 668 if ($this->hasProperty(MediaMarkup::$MEDIA_QUERY_PARAMETER)) { 669 670 try { 671 return FetcherSystem::createPathFetcherFromUrl($this)->getMime()->getExtension(); 672 } catch (ExceptionCompile $e) { 673 LogUtility::internalError("Build error from a Media Fetch URL. We were unable to get the mime. Error: {$e->getMessage()}"); 674 } 675 676 } 677 return parent::getExtension(); 678 } 679 680 681 function getNames() 682 { 683 684 try { 685 $names = explode(self::PATH_SEP, $this->getPath()); 686 return array_slice($names, 1); 687 } catch (ExceptionNotFound $e) { 688 return []; 689 } 690 691 } 692 693 /** 694 * @throws ExceptionNotFound 695 */ 696 function getParent(): Url 697 { 698 $names = $this->getNames(); 699 $count = count($names); 700 if ($count === 0) { 701 throw new ExceptionNotFound("No Parent"); 702 } 703 $parentPath = implode(self::PATH_SEP, array_splice($names, 0, $count - 1)); 704 return $this->setPath($parentPath); 705 } 706 707 function toAbsoluteId(): string 708 { 709 try { 710 return $this->getPath(); 711 } catch (ExceptionNotFound $e) { 712 return ""; 713 } 714 } 715 716 function toAbsolutePath(): Url 717 { 718 return $this->toAbsoluteUrl(); 719 } 720 721 function resolve(string $name): Url 722 { 723 try { 724 $path = $this->getPath(); 725 if ($this->path[strlen($path) - 1] === URL::PATH_SEP) { 726 $this->path .= $name; 727 } else { 728 $this->path .= URL::PATH_SEP . $name; 729 } 730 return $this; 731 } catch (ExceptionNotFound $e) { 732 $this->setPath($name); 733 return $this; 734 } 735 736 } 737 738 /** 739 * @param string $ampersand 740 * @return string 741 */ 742 public function toString(string $ampersand = Url::AMPERSAND_CHARACTER): string 743 { 744 745 try { 746 $scheme = $this->getScheme(); 747 } catch (ExceptionNotFound $e) { 748 $scheme = null; 749 } 750 751 752 switch ($scheme) { 753 case LocalFileSystem::SCHEME: 754 /** 755 * file://host/path 756 */ 757 $base = "$scheme://"; 758 try { 759 $base = "$base{$this->getHost()}"; 760 } catch (ExceptionNotFound $e) { 761 // no host 762 } 763 try { 764 $path = $this->getAbsolutePath(); 765 // linux, network share (file://host/path) 766 $base = "$base{$path}"; 767 } catch (ExceptionNotFound $e) { 768 // no path 769 } 770 return $base; 771 case "mailto": 772 case "whatsapp": 773 case "skype": 774 /** 775 * Skype. Example: skype:echo123?call 776 * https://docs.microsoft.com/en-us/skype-sdk/skypeuris/skypeuris 777 * Mailto: Example: mailto:java-net@java.sun.com?subject=yolo 778 * https://datacadamia.com/marketing/email/mailto 779 */ 780 $base = "$scheme:"; 781 try { 782 $base = "$base{$this->getPath()}"; 783 } catch (ExceptionNotFound $e) { 784 // no path 785 } 786 try { 787 $base = "$base?{$this->getQueryString()}"; 788 } catch (ExceptionNotFound $e) { 789 // no query string 790 } 791 try { 792 $base = "$base#{$this->getFragment()}"; 793 } catch (ExceptionNotFound $e) { 794 // no fragment 795 } 796 return $base; 797 case "http": 798 case "https": 799 case "ftp": 800 default: 801 /** 802 * Url Rewrite 803 * Absolute vs Relative, __media, ... 804 */ 805 if ($this->withRewrite) { 806 UrlRewrite::rewrite($this); 807 } 808 /** 809 * Rewrite may have set a default scheme 810 * We read it again 811 */ 812 try { 813 $scheme = $this->getScheme(); 814 } catch (ExceptionNotFound $e) { 815 $scheme = null; 816 } 817 try { 818 $host = $this->getHost(); 819 } catch (ExceptionNotFound $e) { 820 $host = null; 821 } 822 /** 823 * Absolute/Relative Uri 824 */ 825 $base = ""; 826 if ($host !== null) { 827 if ($scheme !== null) { 828 $base = "{$scheme}://"; 829 } 830 $base = "$base{$host}"; 831 try { 832 $base = "$base:{$this->getPort()}"; 833 } catch (ExceptionNotFound $e) { 834 // no port 835 } 836 } else { 837 if (!in_array($scheme, self::RELATIVE_URL_SCHEMES) && $scheme !== null) { 838 $base = "{$scheme}:"; 839 } 840 } 841 842 try { 843 $base = "$base{$this->getAbsolutePath()}"; 844 } catch (ExceptionNotFound $e) { 845 // ok 846 } 847 848 try { 849 $base = "$base?{$this->getQueryString($ampersand)}"; 850 } catch (ExceptionNotFound $e) { 851 // ok 852 } 853 854 try { 855 $base = "$base#{$this->getFragment()}"; 856 } catch (ExceptionNotFound $e) { 857 // ok 858 } 859 return $base; 860 } 861 862 863 } 864 865 /** 866 * Query parameter can have several values 867 * This function makes sure that there is only one value for one key 868 * if the value are different, the value will be added 869 * @param string $key 870 * @param string $value 871 * @return Url 872 */ 873 public function addQueryParameterIfNotActualSameValue(string $key, string $value): Url 874 { 875 try { 876 $actualValue = $this->getQueryPropertyValue($key); 877 if ($actualValue !== $value) { 878 $this->addQueryParameter($key, $value); 879 } 880 } catch (ExceptionNotFound $e) { 881 $this->addQueryParameter($key, $value); 882 } 883 884 return $this; 885 886 } 887 888 function getUrl(): Url 889 { 890 return $this; 891 } 892 893 public function toHtmlString(): string 894 { 895 return $this->toString(Url::AMPERSAND_URL_ENCODED_FOR_HTML); 896 } 897 898 /** 899 * @throws ExceptionNotFound 900 */ 901 private function getPort(): int 902 { 903 if ($this->port === null) { 904 throw new ExceptionNotFound("No port specified"); 905 } 906 return $this->port; 907 } 908 909 public function addQueryParameterIfNotPresent(string $key, string $value) 910 { 911 if (!$this->hasProperty($key)) { 912 $this->addQueryParameterIfNotActualSameValue($key, $value); 913 } 914 } 915 916 /** 917 * Set/replace a query parameter with the new value 918 * @param string $key 919 * @param string $value 920 * @return Url 921 */ 922 public function setQueryParameter(string $key, string $value): Url 923 { 924 $this->deleteQueryParameter($key); 925 $this->addQueryParameter($key, $value); 926 return $this; 927 } 928 929 public function deleteQueryParameter(string $key) 930 { 931 unset($this->query[$key]); 932 } 933 934 /** 935 * @return string - An url in the DOM use the ampersand character 936 * If you want to check the value of a DOM attribute, you need to check it with this value 937 */ 938 public function toDomString(): string 939 { 940 // ampersand for dom string 941 return $this->toString(); 942 } 943 944 public function toCssString(): string 945 { 946 // ampersand for css 947 return $this->toString(); 948 } 949 950 /** 951 * @return bool - if the url points to the same website than the host 952 */ 953 public function isExternal(): bool 954 { 955 try { 956 // We set the path, otherwise it's seen as a local url 957 $localHost = Url::createEmpty()->setPath("/")->toAbsoluteUrl()->getHost(); 958 return $localHost !== $this->getHost(); 959 } catch (ExceptionNotFound $e) { 960 // no host meaning that the url is relative and then local 961 return false; 962 } 963 } 964 965 /** 966 * In a url, in a case, the path should be absolute 967 * This function makes it absolute if not. 968 * In case of messaging scheme (mailto, whatsapp, ...), this is not the case 969 * @throws ExceptionNotFound 970 */ 971 private function getAbsolutePath(): string 972 { 973 $pathString = $this->getPath(); 974 if ($pathString[0] !== "/") { 975 return "/{$pathString}"; 976 } 977 return $pathString; 978 } 979 980 981 /** 982 * @throws ExceptionBadSyntax 983 * @throws ExceptionBadArgument 984 */ 985 public static function createFromUri(string $uri): Path 986 { 987 return new Url($uri); 988 } 989 990 public function deleteQueryProperties(): Url 991 { 992 $this->query = new ArrayCaseInsensitive();; 993 return $this; 994 } 995 996 public function withoutRewrite(): Url 997 { 998 $this->withRewrite = false; 999 return $this; 1000 } 1001 1002 /** 1003 * Dokuwiki utility to check if the URL is local 1004 * (ie has not path, only a fragment such as #id) 1005 * @return bool 1006 */ 1007 public function isLocal(): bool 1008 { 1009 if ($this->path !== null) { 1010 return false; 1011 } 1012 /** 1013 * The path paramater of Dokuwiki 1014 */ 1015 if ($this->hasProperty(DokuwikiId::DOKUWIKI_ID_ATTRIBUTE)) { 1016 return false; 1017 } 1018 if ($this->hasProperty(MediaMarkup::$MEDIA_QUERY_PARAMETER)) { 1019 return false; 1020 } 1021 if ($this->hasProperty(FetcherRawLocalPath::SRC_QUERY_PARAMETER)) { 1022 return false; 1023 } 1024 return true; 1025 } 1026 1027 1028} 1029