1*04fd306cSNickeau<?php 2*04fd306cSNickeau 3*04fd306cSNickeau 4*04fd306cSNickeaunamespace ComboStrap\Web; 5*04fd306cSNickeau 6*04fd306cSNickeauuse ComboStrap\ArrayCaseInsensitive; 7*04fd306cSNickeauuse ComboStrap\DataType; 8*04fd306cSNickeauuse ComboStrap\ExceptionBadArgument; 9*04fd306cSNickeauuse ComboStrap\ExceptionBadSyntax; 10*04fd306cSNickeauuse ComboStrap\ExceptionCompile; 11*04fd306cSNickeauuse ComboStrap\ExceptionNotEquals; 12*04fd306cSNickeauuse ComboStrap\ExceptionNotExists; 13*04fd306cSNickeauuse ComboStrap\ExceptionNotFound; 14*04fd306cSNickeauuse ComboStrap\ExceptionRuntimeInternal; 15*04fd306cSNickeauuse ComboStrap\FetcherRawLocalPath; 16*04fd306cSNickeauuse ComboStrap\FetcherSystem; 17*04fd306cSNickeauuse ComboStrap\LocalFileSystem; 18*04fd306cSNickeauuse ComboStrap\LogUtility; 19*04fd306cSNickeauuse ComboStrap\Path; 20*04fd306cSNickeauuse ComboStrap\PathAbs; 21*04fd306cSNickeauuse ComboStrap\Site; 22*04fd306cSNickeauuse ComboStrap\Web\UrlRewrite; 23*04fd306cSNickeauuse ComboStrap\WikiPath; 24*04fd306cSNickeau 25*04fd306cSNickeau/** 26*04fd306cSNickeau * Class Url 27*04fd306cSNickeau * @package ComboStrap 28*04fd306cSNickeau * There is no URL class in php 29*04fd306cSNickeau * Only function 30*04fd306cSNickeau * https://www.php.net/manual/en/ref.url.php 31*04fd306cSNickeau */ 32*04fd306cSNickeauclass Url extends PathAbs 33*04fd306cSNickeau{ 34*04fd306cSNickeau 35*04fd306cSNickeau 36*04fd306cSNickeau public const PATH_SEP = "/"; 37*04fd306cSNickeau /** 38*04fd306cSNickeau * In HTML (not in css) 39*04fd306cSNickeau * 40*04fd306cSNickeau * Because ampersands are used to denote HTML entities, 41*04fd306cSNickeau * if you want to use them as literal characters, you must escape them as entities, 42*04fd306cSNickeau * e.g. &. 43*04fd306cSNickeau * 44*04fd306cSNickeau * In HTML, Browser will do the translation for you if you give an URL 45*04fd306cSNickeau * not encoded but testing library may not and refuse them 46*04fd306cSNickeau * 47*04fd306cSNickeau * This URL encoding is mandatory for the {@link ml} function 48*04fd306cSNickeau * when there is a width and use them not otherwise 49*04fd306cSNickeau * 50*04fd306cSNickeau * Thus, if you want to link to: 51*04fd306cSNickeau * http://images.google.com/images?num=30&q=larry+bird 52*04fd306cSNickeau * you need to encode (ie pass this parameter to the {@link ml} function: 53*04fd306cSNickeau * http://images.google.com/images?num=30&q=larry+bird 54*04fd306cSNickeau * 55*04fd306cSNickeau * https://daringfireball.net/projects/markdown/syntax#autoescape 56*04fd306cSNickeau * 57*04fd306cSNickeau */ 58*04fd306cSNickeau public const AMPERSAND_URL_ENCODED_FOR_HTML = '&'; 59*04fd306cSNickeau /** 60*04fd306cSNickeau * Used in dokuwiki syntax & in CSS attribute 61*04fd306cSNickeau * (Css attribute value are then HTML encoded as value of the attribute) 62*04fd306cSNickeau */ 63*04fd306cSNickeau public const AMPERSAND_CHARACTER = "&"; 64*04fd306cSNickeau 65*04fd306cSNickeau const CANONICAL = "url"; 66*04fd306cSNickeau /** 67*04fd306cSNickeau * The schemes that are relative (normallu only URL ? ie http, https) 68*04fd306cSNickeau * This class is much more an URI 69*04fd306cSNickeau */ 70*04fd306cSNickeau const RELATIVE_URL_SCHEMES = ["http", "https"]; 71*04fd306cSNickeau 72*04fd306cSNickeau 73*04fd306cSNickeau private ArrayCaseInsensitive $query; 74*04fd306cSNickeau private ?string $path = null; 75*04fd306cSNickeau private ?string $scheme = null; 76*04fd306cSNickeau private ?string $host = null; 77*04fd306cSNickeau private ?string $fragment = null; 78*04fd306cSNickeau /** 79*04fd306cSNickeau * @var string - original url string 80*04fd306cSNickeau */ 81*04fd306cSNickeau private $url; 82*04fd306cSNickeau private ?int $port = null; 83*04fd306cSNickeau /** 84*04fd306cSNickeau * @var bool - does the URL rewrite occurs 85*04fd306cSNickeau */ 86*04fd306cSNickeau private bool $withRewrite = true; 87*04fd306cSNickeau 88*04fd306cSNickeau 89*04fd306cSNickeau /** 90*04fd306cSNickeau * UrlUtility constructor. 91*04fd306cSNickeau * @throws ExceptionBadSyntax 92*04fd306cSNickeau * @throws ExceptionBadArgument 93*04fd306cSNickeau */ 94*04fd306cSNickeau public function __construct(string $url = null) 95*04fd306cSNickeau { 96*04fd306cSNickeau 97*04fd306cSNickeau $this->url = $url; 98*04fd306cSNickeau $this->query = new ArrayCaseInsensitive(); 99*04fd306cSNickeau if ($this->url !== null) { 100*04fd306cSNickeau /** 101*04fd306cSNickeau * 102*04fd306cSNickeau * @var false 103*04fd306cSNickeau * 104*04fd306cSNickeau * Note: Url validation is hard with regexp 105*04fd306cSNickeau * for instance: 106*04fd306cSNickeau * - http://example.lan/utility/a-combostrap-component-to-render-web-code-in-a-web-page-javascript-html-...-u8fe6ahw 107*04fd306cSNickeau * - does not pass return preg_match('|^http(s)?://[a-z0-9-]+(.[a-z0-9-]+)*(:[0-9]+)?(/.*)?$|i', $url); 108*04fd306cSNickeau * of preg_match('/^https?:\/\//',$url) ? from redirect plugin 109*04fd306cSNickeau * 110*04fd306cSNickeau * We try to create the object, the object use the {@link parse_url()} 111*04fd306cSNickeau * method to validate or send an exception if it can be parsed 112*04fd306cSNickeau */ 113*04fd306cSNickeau $urlComponents = parse_url($url); 114*04fd306cSNickeau if ($urlComponents === false) { 115*04fd306cSNickeau throw new ExceptionBadSyntax("The url ($url) is not valid"); 116*04fd306cSNickeau } 117*04fd306cSNickeau parse_str($urlComponents['query'], $queryKeys); 118*04fd306cSNickeau $this->query = new ArrayCaseInsensitive($queryKeys); 119*04fd306cSNickeau $this->scheme = $urlComponents["scheme"]; 120*04fd306cSNickeau $this->host = $urlComponents["host"]; 121*04fd306cSNickeau $port = $urlComponents["port"]; 122*04fd306cSNickeau try { 123*04fd306cSNickeau if ($port !== null) { 124*04fd306cSNickeau $this->port = DataType::toInteger($port); 125*04fd306cSNickeau } 126*04fd306cSNickeau } catch (ExceptionBadArgument $e) { 127*04fd306cSNickeau throw new ExceptionBadArgument("The port ($port) in ($url) is not an integer. Error: {$e->getMessage()}"); 128*04fd306cSNickeau } 129*04fd306cSNickeau $pathUrlComponent = $urlComponents["path"]; 130*04fd306cSNickeau if ($pathUrlComponent !== null) { 131*04fd306cSNickeau $this->setPath($pathUrlComponent); 132*04fd306cSNickeau } 133*04fd306cSNickeau $this->fragment = $urlComponents["fragment"]; 134*04fd306cSNickeau } 135*04fd306cSNickeau } 136*04fd306cSNickeau 137*04fd306cSNickeau 138*04fd306cSNickeau const RESERVED_WORDS = [':', '!', '#', '$', '&', '\'', '(', ')', '*', '+', ',', '/', ';', '=', '?', '@', '[', ']']; 139*04fd306cSNickeau 140*04fd306cSNickeau /** 141*04fd306cSNickeau * A text to an encoded url 142*04fd306cSNickeau * @param $string - a string 143*04fd306cSNickeau * @param string $separator - the path separator in the string 144*04fd306cSNickeau */ 145*04fd306cSNickeau public static function encodeToUrlPath($string, string $separator = WikiPath::NAMESPACE_SEPARATOR_DOUBLE_POINT): string 146*04fd306cSNickeau { 147*04fd306cSNickeau $parts = explode($separator, $string); 148*04fd306cSNickeau $encodedParts = array_map(function ($e) { 149*04fd306cSNickeau return urlencode($e); 150*04fd306cSNickeau }, $parts); 151*04fd306cSNickeau return implode("/", $encodedParts); 152*04fd306cSNickeau } 153*04fd306cSNickeau 154*04fd306cSNickeau public static function createEmpty(): Url 155*04fd306cSNickeau { 156*04fd306cSNickeau return new Url(); 157*04fd306cSNickeau } 158*04fd306cSNickeau 159*04fd306cSNickeau /** 160*04fd306cSNickeau * 161*04fd306cSNickeau */ 162*04fd306cSNickeau public static function createFromGetOrPostGlobalVariable(): Url 163*04fd306cSNickeau { 164*04fd306cSNickeau /** 165*04fd306cSNickeau * $_REQUEST is a merge between get and post property 166*04fd306cSNickeau * Shared check between post and get HTTP method 167*04fd306cSNickeau * {@link \TestRequest} is using it 168*04fd306cSNickeau */ 169*04fd306cSNickeau $url = Url::createEmpty(); 170*04fd306cSNickeau foreach ($_REQUEST as $key => $value) { 171*04fd306cSNickeau if (is_array($value)) { 172*04fd306cSNickeau foreach ($value as $subkey => $subval) { 173*04fd306cSNickeau if (is_array($subval)) { 174*04fd306cSNickeau if ($key !== "config") { 175*04fd306cSNickeau // dokuwiki things 176*04fd306cSNickeau LogUtility::warning("The key ($key) is an array of an array and was not taken into account in the request url."); 177*04fd306cSNickeau } 178*04fd306cSNickeau continue; 179*04fd306cSNickeau } 180*04fd306cSNickeau if ($key == "do") { 181*04fd306cSNickeau // for whatever reason, dokuwiki puts the value in the key 182*04fd306cSNickeau $url->addQueryParameter($key, $subkey); 183*04fd306cSNickeau continue; 184*04fd306cSNickeau } 185*04fd306cSNickeau $url->addQueryParameter($key, $subval); 186*04fd306cSNickeau 187*04fd306cSNickeau } 188*04fd306cSNickeau } else { 189*04fd306cSNickeau /** 190*04fd306cSNickeau * Bad URL format test 191*04fd306cSNickeau * In the `src` attribute of `script`, the url should not be encoded 192*04fd306cSNickeau * with {@link Url::AMPERSAND_URL_ENCODED_FOR_HTML} 193*04fd306cSNickeau * otherwise we get `amp;` as prefix 194*04fd306cSNickeau * in Chrome 195*04fd306cSNickeau */ 196*04fd306cSNickeau if (strpos($key, "amp;") === 0) { 197*04fd306cSNickeau /** 198*04fd306cSNickeau * We don't advertise this error, it should not happen 199*04fd306cSNickeau * and there is nothing to do to get back on its feet 200*04fd306cSNickeau */ 201*04fd306cSNickeau $message = "The url in src has a bad encoding (the attribute have a amp; prefix. Infinite cache will not work."; 202*04fd306cSNickeau throw new ExceptionRuntimeInternal($message); 203*04fd306cSNickeau } 204*04fd306cSNickeau $url->addQueryParameter($key, $value); 205*04fd306cSNickeau } 206*04fd306cSNickeau } 207*04fd306cSNickeau return $url; 208*04fd306cSNickeau } 209*04fd306cSNickeau 210*04fd306cSNickeau /** 211*04fd306cSNickeau * Utility class to transform windows separator to url path separator 212*04fd306cSNickeau * @param string $pathString 213*04fd306cSNickeau * @return array|string|string[] 214*04fd306cSNickeau */ 215*04fd306cSNickeau public static function toUrlSeparator(string $pathString) 216*04fd306cSNickeau { 217*04fd306cSNickeau return str_replace('\\', '/', $pathString); 218*04fd306cSNickeau } 219*04fd306cSNickeau 220*04fd306cSNickeau 221*04fd306cSNickeau function getQueryProperties(): array 222*04fd306cSNickeau { 223*04fd306cSNickeau return $this->query->getOriginalArray(); 224*04fd306cSNickeau } 225*04fd306cSNickeau 226*04fd306cSNickeau /** 227*04fd306cSNickeau * @throws ExceptionNotFound 228*04fd306cSNickeau */ 229*04fd306cSNickeau function getQueryPropertyValue($key) 230*04fd306cSNickeau { 231*04fd306cSNickeau $value = $this->query[$key]; 232*04fd306cSNickeau if ($value === null) { 233*04fd306cSNickeau throw new ExceptionNotFound("The key ($key) was not found"); 234*04fd306cSNickeau } 235*04fd306cSNickeau return $value; 236*04fd306cSNickeau } 237*04fd306cSNickeau 238*04fd306cSNickeau /** 239*04fd306cSNickeau * Extract the value of a property 240*04fd306cSNickeau * @param $propertyName 241*04fd306cSNickeau * @return string - the value of the property 242*04fd306cSNickeau * @throws ExceptionNotFound 243*04fd306cSNickeau */ 244*04fd306cSNickeau public function getPropertyValue($propertyName): string 245*04fd306cSNickeau { 246*04fd306cSNickeau if (!isset($this->query[$propertyName])) { 247*04fd306cSNickeau throw new ExceptionNotFound("The property ($propertyName) was not found", self::CANONICAL); 248*04fd306cSNickeau } 249*04fd306cSNickeau return $this->query[$propertyName]; 250*04fd306cSNickeau } 251*04fd306cSNickeau 252*04fd306cSNickeau 253*04fd306cSNickeau /** 254*04fd306cSNickeau * @throws ExceptionBadSyntax|ExceptionBadArgument 255*04fd306cSNickeau */ 256*04fd306cSNickeau public static function createFromString(string $url): Url 257*04fd306cSNickeau { 258*04fd306cSNickeau return new Url($url); 259*04fd306cSNickeau } 260*04fd306cSNickeau 261*04fd306cSNickeau /** 262*04fd306cSNickeau * @throws ExceptionNotFound 263*04fd306cSNickeau */ 264*04fd306cSNickeau public function getScheme(): string 265*04fd306cSNickeau { 266*04fd306cSNickeau if ($this->scheme === null) { 267*04fd306cSNickeau throw new ExceptionNotFound("The scheme was not found"); 268*04fd306cSNickeau } 269*04fd306cSNickeau return $this->scheme; 270*04fd306cSNickeau } 271*04fd306cSNickeau 272*04fd306cSNickeau /** 273*04fd306cSNickeau * @param string $path 274*04fd306cSNickeau * @return $this 275*04fd306cSNickeau * in a https scheme: Not the path has a leading `/` that makes the path absolute 276*04fd306cSNickeau * in a email scheme: the path is the email (without /) then 277*04fd306cSNickeau */ 278*04fd306cSNickeau public function setPath(string $path): Url 279*04fd306cSNickeau { 280*04fd306cSNickeau 281*04fd306cSNickeau /** 282*04fd306cSNickeau * Normalization hack 283*04fd306cSNickeau */ 284*04fd306cSNickeau if (strpos($path, "/./") === 0) { 285*04fd306cSNickeau $path = substr($path, 2); 286*04fd306cSNickeau } 287*04fd306cSNickeau $this->path = $path; 288*04fd306cSNickeau return $this; 289*04fd306cSNickeau } 290*04fd306cSNickeau 291*04fd306cSNickeau /** 292*04fd306cSNickeau * @return bool - true if http, https scheme 293*04fd306cSNickeau */ 294*04fd306cSNickeau public function isHttpUrl(): bool 295*04fd306cSNickeau { 296*04fd306cSNickeau try { 297*04fd306cSNickeau return in_array($this->getScheme(), ["http", "https"]); 298*04fd306cSNickeau } catch (ExceptionNotFound $e) { 299*04fd306cSNickeau return false; 300*04fd306cSNickeau } 301*04fd306cSNickeau } 302*04fd306cSNickeau 303*04fd306cSNickeau /** 304*04fd306cSNickeau * Multiple parameter can be set to form an array 305*04fd306cSNickeau * 306*04fd306cSNickeau * Example: s=word1&s=word2 307*04fd306cSNickeau * 308*04fd306cSNickeau * https://stackoverflow.com/questions/24059773/correct-way-to-pass-multiple-values-for-same-parameter-name-in-get-request 309*04fd306cSNickeau */ 310*04fd306cSNickeau public function addQueryParameter(string $key, ?string $value = null): Url 311*04fd306cSNickeau { 312*04fd306cSNickeau /** 313*04fd306cSNickeau * Php Array syntax 314*04fd306cSNickeau */ 315*04fd306cSNickeau if (substr($key, -2) === "[]") { 316*04fd306cSNickeau $key = substr($key, 0, -2); 317*04fd306cSNickeau $actualValue = $this->query[$key]; 318*04fd306cSNickeau if ($actualValue === null || is_array($actualValue)) { 319*04fd306cSNickeau $this->query[$key] = [$value]; 320*04fd306cSNickeau } else { 321*04fd306cSNickeau $actualValue[] = $value; 322*04fd306cSNickeau $this->query[$key] = $actualValue; 323*04fd306cSNickeau } 324*04fd306cSNickeau return $this; 325*04fd306cSNickeau } 326*04fd306cSNickeau if (isset($this->query[$key])) { 327*04fd306cSNickeau $actualValue = $this->query[$key]; 328*04fd306cSNickeau if (is_array($actualValue)) { 329*04fd306cSNickeau $this->query[$key][] = $value; 330*04fd306cSNickeau } else { 331*04fd306cSNickeau $this->query[$key] = [$actualValue, $value]; 332*04fd306cSNickeau } 333*04fd306cSNickeau } else { 334*04fd306cSNickeau $this->query[$key] = $value; 335*04fd306cSNickeau } 336*04fd306cSNickeau return $this; 337*04fd306cSNickeau } 338*04fd306cSNickeau 339*04fd306cSNickeau 340*04fd306cSNickeau public function hasProperty(string $key): bool 341*04fd306cSNickeau { 342*04fd306cSNickeau if (isset($this->query[$key])) { 343*04fd306cSNickeau return true; 344*04fd306cSNickeau } 345*04fd306cSNickeau return false; 346*04fd306cSNickeau } 347*04fd306cSNickeau 348*04fd306cSNickeau /** 349*04fd306cSNickeau * @return Url - add the scheme and the host based on the request if not present 350*04fd306cSNickeau */ 351*04fd306cSNickeau public function toAbsoluteUrl(): Url 352*04fd306cSNickeau { 353*04fd306cSNickeau try { 354*04fd306cSNickeau $this->getScheme(); 355*04fd306cSNickeau } catch (ExceptionNotFound $e) { 356*04fd306cSNickeau /** 357*04fd306cSNickeau * See {@link getBaseURL()} 358*04fd306cSNickeau */ 359*04fd306cSNickeau $https = $_SERVER['HTTPS']; 360*04fd306cSNickeau if (empty($https)) { 361*04fd306cSNickeau $this->setScheme("http"); 362*04fd306cSNickeau } else { 363*04fd306cSNickeau $this->setScheme("https"); 364*04fd306cSNickeau } 365*04fd306cSNickeau } 366*04fd306cSNickeau try { 367*04fd306cSNickeau $this->getHost(); 368*04fd306cSNickeau } catch (ExceptionNotFound $e) { 369*04fd306cSNickeau $remoteHost = Site::getServerHost(); 370*04fd306cSNickeau $this->setHost($remoteHost); 371*04fd306cSNickeau 372*04fd306cSNickeau } 373*04fd306cSNickeau return $this; 374*04fd306cSNickeau } 375*04fd306cSNickeau 376*04fd306cSNickeau /** 377*04fd306cSNickeau * @return string - utility function that call {@link Url::toAbsoluteUrl()} absolute and {@link Url::toString()} 378*04fd306cSNickeau */ 379*04fd306cSNickeau public function toAbsoluteUrlString(): string 380*04fd306cSNickeau { 381*04fd306cSNickeau $this->toAbsoluteUrl(); 382*04fd306cSNickeau return $this->toString(); 383*04fd306cSNickeau } 384*04fd306cSNickeau 385*04fd306cSNickeau /** 386*04fd306cSNickeau * @throws ExceptionNotFound 387*04fd306cSNickeau */ 388*04fd306cSNickeau public function getHost(): string 389*04fd306cSNickeau { 390*04fd306cSNickeau if ($this->host === null) { 391*04fd306cSNickeau throw new ExceptionNotFound("No host"); 392*04fd306cSNickeau } 393*04fd306cSNickeau return $this->host; 394*04fd306cSNickeau } 395*04fd306cSNickeau 396*04fd306cSNickeau /** 397*04fd306cSNickeau * @throws ExceptionNotFound 398*04fd306cSNickeau */ 399*04fd306cSNickeau public function getPath(): string 400*04fd306cSNickeau { 401*04fd306cSNickeau if ($this->path === null) { 402*04fd306cSNickeau throw new ExceptionNotFound("The path was not found"); 403*04fd306cSNickeau } 404*04fd306cSNickeau return $this->path; 405*04fd306cSNickeau } 406*04fd306cSNickeau 407*04fd306cSNickeau /** 408*04fd306cSNickeau * @throws ExceptionNotFound 409*04fd306cSNickeau */ 410*04fd306cSNickeau public function getFragment(): string 411*04fd306cSNickeau { 412*04fd306cSNickeau if ($this->fragment === null) { 413*04fd306cSNickeau throw new ExceptionNotFound("The fragment was not set"); 414*04fd306cSNickeau } 415*04fd306cSNickeau return $this->fragment; 416*04fd306cSNickeau } 417*04fd306cSNickeau 418*04fd306cSNickeau 419*04fd306cSNickeau public function __toString() 420*04fd306cSNickeau { 421*04fd306cSNickeau return $this->toString(); 422*04fd306cSNickeau } 423*04fd306cSNickeau 424*04fd306cSNickeau public function getQueryPropertyValueOrDefault(string $key, string $defaultIfNull) 425*04fd306cSNickeau { 426*04fd306cSNickeau try { 427*04fd306cSNickeau return $this->getQueryPropertyValue($key); 428*04fd306cSNickeau } catch (ExceptionNotFound $e) { 429*04fd306cSNickeau return $defaultIfNull; 430*04fd306cSNickeau } 431*04fd306cSNickeau } 432*04fd306cSNickeau 433*04fd306cSNickeau /** 434*04fd306cSNickeau * Actual vs expected 435*04fd306cSNickeau * 436*04fd306cSNickeau * We use this vocabulary (actual/expected) and not (internal/external or left/right) because this function 437*04fd306cSNickeau * is mostly used in a test framework. 438*04fd306cSNickeau * 439*04fd306cSNickeau * @throws ExceptionNotEquals 440*04fd306cSNickeau */ 441*04fd306cSNickeau public function equals(Url $expectedUrl) 442*04fd306cSNickeau { 443*04fd306cSNickeau /** 444*04fd306cSNickeau * Scheme 445*04fd306cSNickeau */ 446*04fd306cSNickeau try { 447*04fd306cSNickeau $actualScheme = $this->getScheme(); 448*04fd306cSNickeau } catch (ExceptionNotFound $e) { 449*04fd306cSNickeau $actualScheme = ""; 450*04fd306cSNickeau } 451*04fd306cSNickeau try { 452*04fd306cSNickeau $expectedScheme = $expectedUrl->getScheme(); 453*04fd306cSNickeau } catch (ExceptionNotFound $e) { 454*04fd306cSNickeau $expectedScheme = ""; 455*04fd306cSNickeau } 456*04fd306cSNickeau if ($actualScheme !== $expectedScheme) { 457*04fd306cSNickeau throw new ExceptionNotEquals("The scheme are not equals ($actualScheme vs $expectedScheme)"); 458*04fd306cSNickeau } 459*04fd306cSNickeau /** 460*04fd306cSNickeau * Host 461*04fd306cSNickeau */ 462*04fd306cSNickeau try { 463*04fd306cSNickeau $actualHost = $this->getHost(); 464*04fd306cSNickeau } catch (ExceptionNotFound $e) { 465*04fd306cSNickeau $actualHost = ""; 466*04fd306cSNickeau } 467*04fd306cSNickeau try { 468*04fd306cSNickeau $expectedHost = $expectedUrl->getHost(); 469*04fd306cSNickeau } catch (ExceptionNotFound $e) { 470*04fd306cSNickeau $expectedHost = ""; 471*04fd306cSNickeau } 472*04fd306cSNickeau if ($actualHost !== $expectedHost) { 473*04fd306cSNickeau throw new ExceptionNotEquals("The host are not equals ($actualHost vs $expectedHost)"); 474*04fd306cSNickeau } 475*04fd306cSNickeau /** 476*04fd306cSNickeau * Query 477*04fd306cSNickeau */ 478*04fd306cSNickeau $actualQuery = $this->getQueryProperties(); 479*04fd306cSNickeau $expectedQuery = $expectedUrl->getQueryProperties(); 480*04fd306cSNickeau foreach ($actualQuery as $key => $value) { 481*04fd306cSNickeau $expectedValue = $expectedQuery[$key]; 482*04fd306cSNickeau if ($expectedValue === null) { 483*04fd306cSNickeau throw new ExceptionNotEquals("The expected url does not have the $key property"); 484*04fd306cSNickeau } 485*04fd306cSNickeau if ($expectedValue !== $value) { 486*04fd306cSNickeau throw new ExceptionNotEquals("The $key property does not have the same value ($value vs $expectedValue)"); 487*04fd306cSNickeau } 488*04fd306cSNickeau unset($expectedQuery[$key]); 489*04fd306cSNickeau } 490*04fd306cSNickeau foreach ($expectedQuery as $key => $value) { 491*04fd306cSNickeau throw new ExceptionNotEquals("The expected URL has an extra property ($key=$value)"); 492*04fd306cSNickeau } 493*04fd306cSNickeau 494*04fd306cSNickeau /** 495*04fd306cSNickeau * Fragment 496*04fd306cSNickeau */ 497*04fd306cSNickeau try { 498*04fd306cSNickeau $actualFragment = $this->getFragment(); 499*04fd306cSNickeau } catch (ExceptionNotFound $e) { 500*04fd306cSNickeau $actualFragment = ""; 501*04fd306cSNickeau } 502*04fd306cSNickeau try { 503*04fd306cSNickeau $expectedFragment = $expectedUrl->getFragment(); 504*04fd306cSNickeau } catch (ExceptionNotFound $e) { 505*04fd306cSNickeau $expectedFragment = ""; 506*04fd306cSNickeau } 507*04fd306cSNickeau if ($actualFragment !== $expectedFragment) { 508*04fd306cSNickeau throw new ExceptionNotEquals("The fragment are not equals ($actualFragment vs $expectedFragment)"); 509*04fd306cSNickeau } 510*04fd306cSNickeau 511*04fd306cSNickeau } 512*04fd306cSNickeau 513*04fd306cSNickeau public function setScheme(string $scheme): Url 514*04fd306cSNickeau { 515*04fd306cSNickeau $this->scheme = $scheme; 516*04fd306cSNickeau return $this; 517*04fd306cSNickeau } 518*04fd306cSNickeau 519*04fd306cSNickeau public function setHost($host): Url 520*04fd306cSNickeau { 521*04fd306cSNickeau $this->host = $host; 522*04fd306cSNickeau return $this; 523*04fd306cSNickeau } 524*04fd306cSNickeau 525*04fd306cSNickeau public function setFragment(string $fragment): Url 526*04fd306cSNickeau { 527*04fd306cSNickeau $this->fragment = $fragment; 528*04fd306cSNickeau return $this; 529*04fd306cSNickeau } 530*04fd306cSNickeau 531*04fd306cSNickeau /** 532*04fd306cSNickeau * @throws ExceptionNotFound 533*04fd306cSNickeau */ 534*04fd306cSNickeau public function getQueryString($ampersand = Url::AMPERSAND_CHARACTER): string 535*04fd306cSNickeau { 536*04fd306cSNickeau if (sizeof($this->query) === 0) { 537*04fd306cSNickeau throw new ExceptionNotFound("No Query string"); 538*04fd306cSNickeau } 539*04fd306cSNickeau /** 540*04fd306cSNickeau * To be able to diff them 541*04fd306cSNickeau */ 542*04fd306cSNickeau $originalArray = $this->query->getOriginalArray(); 543*04fd306cSNickeau ksort($originalArray); 544*04fd306cSNickeau 545*04fd306cSNickeau /** 546*04fd306cSNickeau * We don't use {@link http_build_query} because: 547*04fd306cSNickeau * * it does not the follow the array format (ie s[]=searchword1+seachword2) 548*04fd306cSNickeau * * it output 'key=' instead of `key` when the value is null 549*04fd306cSNickeau */ 550*04fd306cSNickeau $queryString = null; 551*04fd306cSNickeau foreach ($originalArray as $key => $value) { 552*04fd306cSNickeau if ($queryString !== null) { 553*04fd306cSNickeau /** 554*04fd306cSNickeau * HTML encoding (ie {@link self::AMPERSAND_URL_ENCODED_FOR_HTML} 555*04fd306cSNickeau * happens only when outputing to HTML 556*04fd306cSNickeau * The url may also be used elsewhere where & is unknown or not wanted such as css ... 557*04fd306cSNickeau * 558*04fd306cSNickeau * In test, we may ask the url HTML encoded 559*04fd306cSNickeau */ 560*04fd306cSNickeau $queryString .= $ampersand; 561*04fd306cSNickeau } 562*04fd306cSNickeau if ($value === null) { 563*04fd306cSNickeau $queryString .= urlencode($key); 564*04fd306cSNickeau } else { 565*04fd306cSNickeau if (is_array($value)) { 566*04fd306cSNickeau for ($i = 0; $i < sizeof($value); $i++) { 567*04fd306cSNickeau $val = $value[$i]; 568*04fd306cSNickeau if ($i > 0) { 569*04fd306cSNickeau $queryString .= self::AMPERSAND_CHARACTER; 570*04fd306cSNickeau } 571*04fd306cSNickeau $queryString .= urlencode($key) . "[]=" . urlencode($val); 572*04fd306cSNickeau } 573*04fd306cSNickeau } else { 574*04fd306cSNickeau $queryString .= urlencode($key) . "=" . urlencode($value); 575*04fd306cSNickeau } 576*04fd306cSNickeau } 577*04fd306cSNickeau } 578*04fd306cSNickeau return $queryString; 579*04fd306cSNickeau 580*04fd306cSNickeau 581*04fd306cSNickeau } 582*04fd306cSNickeau 583*04fd306cSNickeau /** 584*04fd306cSNickeau * @throws ExceptionNotFound 585*04fd306cSNickeau */ 586*04fd306cSNickeau public function getQueryPropertyValueAndRemoveIfPresent(string $key) 587*04fd306cSNickeau { 588*04fd306cSNickeau $value = $this->getQueryPropertyValue($key); 589*04fd306cSNickeau unset($this->query[$key]); 590*04fd306cSNickeau return $value; 591*04fd306cSNickeau } 592*04fd306cSNickeau 593*04fd306cSNickeau 594*04fd306cSNickeau /** 595*04fd306cSNickeau * @throws ExceptionNotFound 596*04fd306cSNickeau */ 597*04fd306cSNickeau function getLastName(): string 598*04fd306cSNickeau { 599*04fd306cSNickeau $names = $this->getNames(); 600*04fd306cSNickeau $namesCount = count($names); 601*04fd306cSNickeau if ($namesCount === 0) { 602*04fd306cSNickeau throw new ExceptionNotFound("No last name"); 603*04fd306cSNickeau } 604*04fd306cSNickeau return $names[$namesCount - 1]; 605*04fd306cSNickeau 606*04fd306cSNickeau } 607*04fd306cSNickeau 608*04fd306cSNickeau /** 609*04fd306cSNickeau * @return string 610*04fd306cSNickeau * @throws ExceptionNotFound 611*04fd306cSNickeau */ 612*04fd306cSNickeau public function getExtension(): string 613*04fd306cSNickeau { 614*04fd306cSNickeau if ($this->hasProperty(FetcherRawLocalPath::$MEDIA_QUERY_PARAMETER)) { 615*04fd306cSNickeau 616*04fd306cSNickeau try { 617*04fd306cSNickeau return FetcherSystem::createPathFetcherFromUrl($this)->getMime()->getExtension(); 618*04fd306cSNickeau } catch (ExceptionCompile $e) { 619*04fd306cSNickeau LogUtility::internalError("Build error from a Media Fetch URL. We were unable to get the mime. Error: {$e->getMessage()}"); 620*04fd306cSNickeau } 621*04fd306cSNickeau 622*04fd306cSNickeau } 623*04fd306cSNickeau return parent::getExtension(); 624*04fd306cSNickeau } 625*04fd306cSNickeau 626*04fd306cSNickeau 627*04fd306cSNickeau function getNames() 628*04fd306cSNickeau { 629*04fd306cSNickeau 630*04fd306cSNickeau try { 631*04fd306cSNickeau $names = explode(self::PATH_SEP, $this->getPath()); 632*04fd306cSNickeau return array_slice($names, 1); 633*04fd306cSNickeau } catch (ExceptionNotFound $e) { 634*04fd306cSNickeau return []; 635*04fd306cSNickeau } 636*04fd306cSNickeau 637*04fd306cSNickeau } 638*04fd306cSNickeau 639*04fd306cSNickeau /** 640*04fd306cSNickeau * @throws ExceptionNotFound 641*04fd306cSNickeau */ 642*04fd306cSNickeau function getParent(): Url 643*04fd306cSNickeau { 644*04fd306cSNickeau $names = $this->getNames(); 645*04fd306cSNickeau $count = count($names); 646*04fd306cSNickeau if ($count === 0) { 647*04fd306cSNickeau throw new ExceptionNotFound("No Parent"); 648*04fd306cSNickeau } 649*04fd306cSNickeau $parentPath = implode(self::PATH_SEP, array_splice($names, 0, $count - 1)); 650*04fd306cSNickeau return $this->setPath($parentPath); 651*04fd306cSNickeau } 652*04fd306cSNickeau 653*04fd306cSNickeau function toAbsoluteId(): string 654*04fd306cSNickeau { 655*04fd306cSNickeau try { 656*04fd306cSNickeau return $this->getPath(); 657*04fd306cSNickeau } catch (ExceptionNotFound $e) { 658*04fd306cSNickeau return ""; 659*04fd306cSNickeau } 660*04fd306cSNickeau } 661*04fd306cSNickeau 662*04fd306cSNickeau function toAbsolutePath(): Url 663*04fd306cSNickeau { 664*04fd306cSNickeau return $this->toAbsoluteUrl(); 665*04fd306cSNickeau } 666*04fd306cSNickeau 667*04fd306cSNickeau function resolve(string $name): Url 668*04fd306cSNickeau { 669*04fd306cSNickeau try { 670*04fd306cSNickeau $path = $this->getPath(); 671*04fd306cSNickeau if ($this->path[strlen($path) - 1] === URL::PATH_SEP) { 672*04fd306cSNickeau $this->path .= $name; 673*04fd306cSNickeau } else { 674*04fd306cSNickeau $this->path .= URL::PATH_SEP . $name; 675*04fd306cSNickeau } 676*04fd306cSNickeau return $this; 677*04fd306cSNickeau } catch (ExceptionNotFound $e) { 678*04fd306cSNickeau $this->setPath($name); 679*04fd306cSNickeau return $this; 680*04fd306cSNickeau } 681*04fd306cSNickeau 682*04fd306cSNickeau } 683*04fd306cSNickeau 684*04fd306cSNickeau /** 685*04fd306cSNickeau * @param string $ampersand 686*04fd306cSNickeau * @return string 687*04fd306cSNickeau */ 688*04fd306cSNickeau public function toString(string $ampersand = Url::AMPERSAND_CHARACTER): string 689*04fd306cSNickeau { 690*04fd306cSNickeau 691*04fd306cSNickeau try { 692*04fd306cSNickeau $scheme = $this->getScheme(); 693*04fd306cSNickeau } catch (ExceptionNotFound $e) { 694*04fd306cSNickeau $scheme = null; 695*04fd306cSNickeau } 696*04fd306cSNickeau 697*04fd306cSNickeau 698*04fd306cSNickeau switch ($scheme) { 699*04fd306cSNickeau case LocalFileSystem::SCHEME: 700*04fd306cSNickeau /** 701*04fd306cSNickeau * file://host/path 702*04fd306cSNickeau */ 703*04fd306cSNickeau $base = "$scheme://"; 704*04fd306cSNickeau try { 705*04fd306cSNickeau $base = "$base{$this->getHost()}"; 706*04fd306cSNickeau } catch (ExceptionNotFound $e) { 707*04fd306cSNickeau // no host 708*04fd306cSNickeau } 709*04fd306cSNickeau try { 710*04fd306cSNickeau $path = $this->getAbsolutePath(); 711*04fd306cSNickeau // linux, network share (file://host/path) 712*04fd306cSNickeau $base = "$base{$path}"; 713*04fd306cSNickeau } catch (ExceptionNotFound $e) { 714*04fd306cSNickeau // no path 715*04fd306cSNickeau } 716*04fd306cSNickeau return $base; 717*04fd306cSNickeau case "mailto": 718*04fd306cSNickeau case "whatsapp": 719*04fd306cSNickeau case "skype": 720*04fd306cSNickeau /** 721*04fd306cSNickeau * Skype. Example: skype:echo123?call 722*04fd306cSNickeau * https://docs.microsoft.com/en-us/skype-sdk/skypeuris/skypeuris 723*04fd306cSNickeau * Mailto: Example: mailto:java-net@java.sun.com?subject=yolo 724*04fd306cSNickeau * https://datacadamia.com/marketing/email/mailto 725*04fd306cSNickeau */ 726*04fd306cSNickeau $base = "$scheme:"; 727*04fd306cSNickeau try { 728*04fd306cSNickeau $base = "$base{$this->getPath()}"; 729*04fd306cSNickeau } catch (ExceptionNotFound $e) { 730*04fd306cSNickeau // no path 731*04fd306cSNickeau } 732*04fd306cSNickeau try { 733*04fd306cSNickeau $base = "$base?{$this->getQueryString()}"; 734*04fd306cSNickeau } catch (ExceptionNotFound $e) { 735*04fd306cSNickeau // no query string 736*04fd306cSNickeau } 737*04fd306cSNickeau try { 738*04fd306cSNickeau $base = "$base#{$this->getFragment()}"; 739*04fd306cSNickeau } catch (ExceptionNotFound $e) { 740*04fd306cSNickeau // no fragment 741*04fd306cSNickeau } 742*04fd306cSNickeau return $base; 743*04fd306cSNickeau case "http": 744*04fd306cSNickeau case "https": 745*04fd306cSNickeau case "ftp": 746*04fd306cSNickeau default: 747*04fd306cSNickeau /** 748*04fd306cSNickeau * Url Rewrite 749*04fd306cSNickeau * Absolute vs Relative, __media, ... 750*04fd306cSNickeau */ 751*04fd306cSNickeau if ($this->withRewrite) { 752*04fd306cSNickeau UrlRewrite::rewrite($this); 753*04fd306cSNickeau } 754*04fd306cSNickeau /** 755*04fd306cSNickeau * Rewrite may have set a default scheme 756*04fd306cSNickeau * We read it again 757*04fd306cSNickeau */ 758*04fd306cSNickeau try { 759*04fd306cSNickeau $scheme = $this->getScheme(); 760*04fd306cSNickeau } catch (ExceptionNotFound $e) { 761*04fd306cSNickeau $scheme = null; 762*04fd306cSNickeau } 763*04fd306cSNickeau try { 764*04fd306cSNickeau $host = $this->getHost(); 765*04fd306cSNickeau } catch (ExceptionNotFound $e) { 766*04fd306cSNickeau $host = null; 767*04fd306cSNickeau } 768*04fd306cSNickeau /** 769*04fd306cSNickeau * Absolute/Relative Uri 770*04fd306cSNickeau */ 771*04fd306cSNickeau $base = ""; 772*04fd306cSNickeau if ($host !== null) { 773*04fd306cSNickeau if ($scheme !== null) { 774*04fd306cSNickeau $base = "{$scheme}://"; 775*04fd306cSNickeau } 776*04fd306cSNickeau $base = "$base{$host}"; 777*04fd306cSNickeau try { 778*04fd306cSNickeau $base = "$base:{$this->getPort()}"; 779*04fd306cSNickeau } catch (ExceptionNotFound $e) { 780*04fd306cSNickeau // no port 781*04fd306cSNickeau } 782*04fd306cSNickeau } else { 783*04fd306cSNickeau if (!in_array($scheme, self::RELATIVE_URL_SCHEMES) && $scheme !== null) { 784*04fd306cSNickeau $base = "{$scheme}:"; 785*04fd306cSNickeau } 786*04fd306cSNickeau } 787*04fd306cSNickeau 788*04fd306cSNickeau try { 789*04fd306cSNickeau $base = "$base{$this->getAbsolutePath()}"; 790*04fd306cSNickeau } catch (ExceptionNotFound $e) { 791*04fd306cSNickeau // ok 792*04fd306cSNickeau } 793*04fd306cSNickeau 794*04fd306cSNickeau try { 795*04fd306cSNickeau $base = "$base?{$this->getQueryString($ampersand)}"; 796*04fd306cSNickeau } catch (ExceptionNotFound $e) { 797*04fd306cSNickeau // ok 798*04fd306cSNickeau } 799*04fd306cSNickeau 800*04fd306cSNickeau try { 801*04fd306cSNickeau $base = "$base#{$this->getFragment()}"; 802*04fd306cSNickeau } catch (ExceptionNotFound $e) { 803*04fd306cSNickeau // ok 804*04fd306cSNickeau } 805*04fd306cSNickeau return $base; 806*04fd306cSNickeau } 807*04fd306cSNickeau 808*04fd306cSNickeau 809*04fd306cSNickeau } 810*04fd306cSNickeau 811*04fd306cSNickeau /** 812*04fd306cSNickeau * Query parameter can have several values 813*04fd306cSNickeau * This function makes sure that there is only one value for one key 814*04fd306cSNickeau * if the value are different, the value will be added 815*04fd306cSNickeau * @param string $key 816*04fd306cSNickeau * @param string $value 817*04fd306cSNickeau * @return Url 818*04fd306cSNickeau */ 819*04fd306cSNickeau public function addQueryParameterIfNotActualSameValue(string $key, string $value): Url 820*04fd306cSNickeau { 821*04fd306cSNickeau try { 822*04fd306cSNickeau $actualValue = $this->getQueryPropertyValue($key); 823*04fd306cSNickeau if ($actualValue !== $value) { 824*04fd306cSNickeau $this->addQueryParameter($key, $value); 825*04fd306cSNickeau } 826*04fd306cSNickeau } catch (ExceptionNotFound $e) { 827*04fd306cSNickeau $this->addQueryParameter($key, $value); 828*04fd306cSNickeau } 829*04fd306cSNickeau 830*04fd306cSNickeau return $this; 831*04fd306cSNickeau 832*04fd306cSNickeau } 833*04fd306cSNickeau 834*04fd306cSNickeau function getUrl(): Url 835*04fd306cSNickeau { 836*04fd306cSNickeau return $this; 837*04fd306cSNickeau } 838*04fd306cSNickeau 839*04fd306cSNickeau public function toHtmlString(): string 840*04fd306cSNickeau { 841*04fd306cSNickeau return $this->toString(Url::AMPERSAND_URL_ENCODED_FOR_HTML); 842*04fd306cSNickeau } 843*04fd306cSNickeau 844*04fd306cSNickeau /** 845*04fd306cSNickeau * @throws ExceptionNotFound 846*04fd306cSNickeau */ 847*04fd306cSNickeau private function getPort(): int 848*04fd306cSNickeau { 849*04fd306cSNickeau if ($this->port === null) { 850*04fd306cSNickeau throw new ExceptionNotFound("No port specified"); 851*04fd306cSNickeau } 852*04fd306cSNickeau return $this->port; 853*04fd306cSNickeau } 854*04fd306cSNickeau 855*04fd306cSNickeau public function addQueryParameterIfNotPresent(string $key, string $value) 856*04fd306cSNickeau { 857*04fd306cSNickeau if (!$this->hasProperty($key)) { 858*04fd306cSNickeau $this->addQueryParameterIfNotActualSameValue($key, $value); 859*04fd306cSNickeau } 860*04fd306cSNickeau } 861*04fd306cSNickeau 862*04fd306cSNickeau /** 863*04fd306cSNickeau * Set/replace a query parameter with the new value 864*04fd306cSNickeau * @param string $key 865*04fd306cSNickeau * @param string $value 866*04fd306cSNickeau * @return Url 867*04fd306cSNickeau */ 868*04fd306cSNickeau public function setQueryParameter(string $key, string $value): Url 869*04fd306cSNickeau { 870*04fd306cSNickeau $this->deleteQueryParameter($key); 871*04fd306cSNickeau $this->addQueryParameter($key, $value); 872*04fd306cSNickeau return $this; 873*04fd306cSNickeau } 874*04fd306cSNickeau 875*04fd306cSNickeau public function deleteQueryParameter(string $key) 876*04fd306cSNickeau { 877*04fd306cSNickeau unset($this->query[$key]); 878*04fd306cSNickeau } 879*04fd306cSNickeau 880*04fd306cSNickeau /** 881*04fd306cSNickeau * @return string - An url in the DOM use the ampersand character 882*04fd306cSNickeau * If you want to check the value of a DOM attribute, you need to check it with this value 883*04fd306cSNickeau */ 884*04fd306cSNickeau public function toDomString(): string 885*04fd306cSNickeau { 886*04fd306cSNickeau // ampersand for dom string 887*04fd306cSNickeau return $this->toString(); 888*04fd306cSNickeau } 889*04fd306cSNickeau 890*04fd306cSNickeau public function toCssString(): string 891*04fd306cSNickeau { 892*04fd306cSNickeau // ampersand for css 893*04fd306cSNickeau return $this->toString(); 894*04fd306cSNickeau } 895*04fd306cSNickeau 896*04fd306cSNickeau /** 897*04fd306cSNickeau * @return bool - if the url points to the same website than the host 898*04fd306cSNickeau */ 899*04fd306cSNickeau public function isExternal(): bool 900*04fd306cSNickeau { 901*04fd306cSNickeau try { 902*04fd306cSNickeau $localHost = Url::createEmpty()->toAbsoluteUrl()->getHost(); 903*04fd306cSNickeau return $localHost !== $this->getHost(); 904*04fd306cSNickeau } catch (ExceptionNotFound $e) { 905*04fd306cSNickeau // no host meaning that the url is relative and then local 906*04fd306cSNickeau return false; 907*04fd306cSNickeau } 908*04fd306cSNickeau } 909*04fd306cSNickeau 910*04fd306cSNickeau /** 911*04fd306cSNickeau * In a url, in a case, the path should be absolute 912*04fd306cSNickeau * This function makes it absolute if not. 913*04fd306cSNickeau * In case of messaging scheme (mailto, whatsapp, ...), this is not the case 914*04fd306cSNickeau * @throws ExceptionNotFound 915*04fd306cSNickeau */ 916*04fd306cSNickeau private function getAbsolutePath(): string 917*04fd306cSNickeau { 918*04fd306cSNickeau $pathString = $this->getPath(); 919*04fd306cSNickeau if ($pathString[0] !== "/") { 920*04fd306cSNickeau return "/{$pathString}"; 921*04fd306cSNickeau } 922*04fd306cSNickeau return $pathString; 923*04fd306cSNickeau } 924*04fd306cSNickeau 925*04fd306cSNickeau 926*04fd306cSNickeau /** 927*04fd306cSNickeau * @throws ExceptionBadSyntax 928*04fd306cSNickeau * @throws ExceptionBadArgument 929*04fd306cSNickeau */ 930*04fd306cSNickeau public static function createFromUri(string $uri): Path 931*04fd306cSNickeau { 932*04fd306cSNickeau return new Url($uri); 933*04fd306cSNickeau } 934*04fd306cSNickeau 935*04fd306cSNickeau public function deleteQueryProperties(): Url 936*04fd306cSNickeau { 937*04fd306cSNickeau $this->query = new ArrayCaseInsensitive();; 938*04fd306cSNickeau return $this; 939*04fd306cSNickeau } 940*04fd306cSNickeau 941*04fd306cSNickeau public function withoutRewrite(): Url 942*04fd306cSNickeau { 943*04fd306cSNickeau $this->withRewrite = false; 944*04fd306cSNickeau return $this; 945*04fd306cSNickeau } 946*04fd306cSNickeau 947*04fd306cSNickeau} 948