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