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