Uri.php 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967
  1. <?php
  2. /**
  3. * League.Uri (https://uri.thephpleague.com)
  4. *
  5. * (c) Ignace Nyamagana Butera <nyamsprod@gmail.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. declare(strict_types=1);
  11. namespace League\Uri;
  12. use BackedEnum;
  13. use Closure;
  14. use Deprecated;
  15. use finfo;
  16. use League\Uri\Contracts\Conditionable;
  17. use League\Uri\Contracts\FragmentDirective;
  18. use League\Uri\Contracts\Transformable;
  19. use League\Uri\Contracts\UriComponentInterface;
  20. use League\Uri\Contracts\UriException;
  21. use League\Uri\Contracts\UriInterface;
  22. use League\Uri\Exceptions\MissingFeature;
  23. use League\Uri\Exceptions\SyntaxError;
  24. use League\Uri\Idna\Converter as IdnaConverter;
  25. use League\Uri\IPv4\Converter as IPv4Converter;
  26. use League\Uri\IPv6\Converter as IPv6Converter;
  27. use League\Uri\UriTemplate\TemplateCanNotBeExpanded;
  28. use Psr\Http\Message\UriInterface as Psr7UriInterface;
  29. use RuntimeException;
  30. use SensitiveParameter;
  31. use SplFileInfo;
  32. use SplFileObject;
  33. use Stringable;
  34. use Throwable;
  35. use TypeError;
  36. use Uri\Rfc3986\Uri as Rfc3986Uri;
  37. use Uri\WhatWg\Url as WhatWgUrl;
  38. use function array_filter;
  39. use function array_key_last;
  40. use function array_map;
  41. use function array_pop;
  42. use function array_shift;
  43. use function base64_decode;
  44. use function base64_encode;
  45. use function basename;
  46. use function count;
  47. use function dirname;
  48. use function explode;
  49. use function fclose;
  50. use function feof;
  51. use function file_get_contents;
  52. use function filter_var;
  53. use function fopen;
  54. use function fread;
  55. use function fwrite;
  56. use function gettype;
  57. use function implode;
  58. use function in_array;
  59. use function is_bool;
  60. use function is_object;
  61. use function is_resource;
  62. use function is_string;
  63. use function preg_match;
  64. use function preg_replace;
  65. use function preg_replace_callback;
  66. use function rawurldecode;
  67. use function rawurlencode;
  68. use function restore_error_handler;
  69. use function set_error_handler;
  70. use function sprintf;
  71. use function str_contains;
  72. use function str_repeat;
  73. use function str_replace;
  74. use function str_starts_with;
  75. use function strlen;
  76. use function strpos;
  77. use function strspn;
  78. use function strtolower;
  79. use function substr;
  80. use function trim;
  81. use const FILEINFO_MIME;
  82. use const FILEINFO_MIME_TYPE;
  83. use const FILTER_FLAG_IPV4;
  84. use const FILTER_NULL_ON_FAILURE;
  85. use const FILTER_VALIDATE_BOOLEAN;
  86. use const FILTER_VALIDATE_EMAIL;
  87. use const FILTER_VALIDATE_IP;
  88. /**
  89. * @phpstan-import-type ComponentMap from UriString
  90. * @phpstan-import-type InputComponentMap from UriString
  91. */
  92. final class Uri implements Conditionable, UriInterface, Transformable
  93. {
  94. /**
  95. * RFC3986 invalid characters.
  96. *
  97. * @link https://tools.ietf.org/html/rfc3986#section-2.2
  98. *
  99. * @var string
  100. */
  101. private const REGEXP_INVALID_CHARS = '/[\x00-\x1f\x7f]/';
  102. /**
  103. * RFC3986 IPvFuture host and port component.
  104. *
  105. * @var string
  106. */
  107. private const REGEXP_HOST_PORT = ',^(?<host>(\[.*]|[^:])*)(:(?<port>[^/?#]*))?$,x';
  108. /**
  109. * Regular expression pattern to for file URI.
  110. * <volume> contains the volume but not the volume separator.
  111. * The volume separator may be URL-encoded (`|` as `%7C`) by formatPath(),
  112. * so we account for that here.
  113. *
  114. * @var string
  115. */
  116. private const REGEXP_FILE_PATH = ',^(?<delim>/)?(?<volume>[a-zA-Z])(?:[:|\|]|%7C)(?<rest>.*)?,';
  117. /**
  118. * Mimetype regular expression pattern.
  119. *
  120. * @link https://tools.ietf.org/html/rfc2397
  121. *
  122. * @var string
  123. */
  124. private const REGEXP_MIMETYPE = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
  125. /**
  126. * Base64 content regular expression pattern.
  127. *
  128. * @link https://tools.ietf.org/html/rfc2397
  129. *
  130. * @var string
  131. */
  132. private const REGEXP_BINARY = ',(;|^)base64$,';
  133. /**
  134. * Windows filepath regular expression pattern.
  135. * <root> contains both the volume and volume separator.
  136. *
  137. * @var string
  138. */
  139. private const REGEXP_WINDOW_PATH = ',^(?<root>[a-zA-Z][:|\|]),';
  140. /**
  141. * Maximum number of cached items.
  142. *
  143. * @var int
  144. */
  145. private const MAXIMUM_CACHED_ITEMS = 100;
  146. /**
  147. * All ASCII letters sorted by typical frequency of occurrence.
  148. *
  149. * @var string
  150. */
  151. private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F";
  152. private readonly ?string $scheme;
  153. private readonly ?string $user;
  154. private readonly ?string $pass;
  155. private readonly ?string $userInfo;
  156. private readonly ?string $host;
  157. private readonly ?int $port;
  158. private readonly ?string $authority;
  159. private readonly string $path;
  160. private readonly ?string $query;
  161. private readonly ?string $fragment;
  162. private readonly string $uriAsciiString;
  163. private readonly string $uriUnicodeString;
  164. private readonly ?string $origin;
  165. private function __construct(
  166. ?string $scheme,
  167. ?string $user,
  168. #[SensitiveParameter] ?string $pass,
  169. ?string $host,
  170. ?int $port,
  171. string $path,
  172. ?string $query,
  173. ?string $fragment
  174. ) {
  175. $this->scheme = $this->formatScheme($scheme);
  176. $this->user = Encoder::encodeUser($user);
  177. $this->pass = Encoder::encodePassword($pass);
  178. $this->host = $this->formatHost($host);
  179. $this->port = $this->formatPort($port);
  180. $this->authority = UriString::buildAuthority([
  181. 'scheme' => $this->scheme,
  182. 'user' => $this->user,
  183. 'pass' => $this->pass,
  184. 'host' => $this->host,
  185. 'port' => $this->port,
  186. ]);
  187. $this->path = $this->formatPath($path);
  188. $this->query = Encoder::encodeQueryOrFragment($query);
  189. $this->fragment = Encoder::encodeQueryOrFragment($fragment);
  190. $this->userInfo = null !== $this->pass ? $this->user.':'.$this->pass : $this->user;
  191. $this->uriAsciiString = UriString::buildUri($this->scheme, $this->authority, $this->path, $this->query, $this->fragment);
  192. $this->assertValidRfc3986Uri();
  193. $this->assertValidState();
  194. $this->origin = $this->setOrigin();
  195. $host = $this->getUnicodeHost();
  196. $this->uriUnicodeString = $host === $this->host
  197. ? $this->uriAsciiString
  198. : UriString::buildUri(
  199. $this->scheme,
  200. UriString::buildAuthority([...$this->toComponents(), ...['host' => $host]]),
  201. $this->path,
  202. $this->query,
  203. $this->fragment
  204. );
  205. }
  206. /**
  207. * Format the Scheme and Host component.
  208. *
  209. * @throws SyntaxError if the scheme is invalid
  210. */
  211. private function formatScheme(?string $scheme): ?string
  212. {
  213. if (null === $scheme) {
  214. return null;
  215. }
  216. $formattedScheme = strtolower($scheme);
  217. static $cache = [];
  218. if (isset($cache[$formattedScheme])) {
  219. return $formattedScheme;
  220. }
  221. null !== UriScheme::tryFrom($formattedScheme)
  222. || UriString::isValidScheme($formattedScheme)
  223. || throw new SyntaxError('The scheme `'.$scheme.'` is invalid.');
  224. $cache[$formattedScheme] = 1;
  225. if (self::MAXIMUM_CACHED_ITEMS < count($cache)) {
  226. array_shift($cache);
  227. }
  228. return $formattedScheme;
  229. }
  230. /**
  231. * Validate and Format the Host component.
  232. */
  233. private function formatHost(?string $host): ?string
  234. {
  235. return HostRecord::from($host)->toAscii();
  236. }
  237. /**
  238. * Format the Port component.
  239. *
  240. * @throws SyntaxError
  241. */
  242. private function formatPort(BackedEnum|int|null $port = null): ?int
  243. {
  244. if ($port instanceof BackedEnum) {
  245. $port = (string) $port->value;
  246. 1 === preg_match('/^\d+$/', $port) || throw new SyntaxError('The port `'.$port.'` is invalid.');
  247. $port = (int) $port;
  248. }
  249. $defaultPort = null !== $this->scheme
  250. ? UriScheme::tryFrom($this->scheme)?->port()
  251. : null;
  252. return match (true) {
  253. null === $port, $defaultPort === $port => null,
  254. 0 > $port => throw new SyntaxError('The port `'.$port.'` is invalid.'),
  255. default => $port,
  256. };
  257. }
  258. /**
  259. * Create a new instance from a string or a stringable structure or returns null on failure.
  260. */
  261. public static function tryNew(Rfc3986Uri|WhatWgUrl|Urn|Stringable|string $uri = ''): ?self
  262. {
  263. try {
  264. return self::new($uri);
  265. } catch (Throwable) {
  266. return null;
  267. }
  268. }
  269. /**
  270. * Create a new instance from a string.
  271. */
  272. public static function new(Rfc3986Uri|WhatWgUrl|Urn|BackedEnum|Stringable|string $uri = ''): self
  273. {
  274. if ($uri instanceof Rfc3986Uri) {
  275. return new self(
  276. $uri->getRawScheme(),
  277. $uri->getRawUsername(),
  278. $uri->getRawPassword(),
  279. $uri->getRawHost(),
  280. $uri->getPort(),
  281. $uri->getRawPath(),
  282. $uri->getRawQuery(),
  283. $uri->getRawFragment()
  284. );
  285. }
  286. if ($uri instanceof WhatWgUrl) {
  287. return new self(
  288. $uri->getScheme(),
  289. $uri->getUsername(),
  290. $uri->getPassword(),
  291. $uri->getAsciiHost(),
  292. $uri->getPort(),
  293. $uri->getPath(),
  294. $uri->getQuery(),
  295. $uri->getFragment(),
  296. );
  297. }
  298. if ($uri instanceof BackedEnum) {
  299. $uri = $uri->value;
  300. }
  301. $uri = (string) $uri;
  302. trim($uri) === $uri || throw new SyntaxError(sprintf('The uri `%s` contains invalid characters', $uri));
  303. return new self(...UriString::parse(str_replace(' ', '%20', $uri)));
  304. }
  305. /**
  306. * Returns a new instance from a URI and a Base URI.or null on failure.
  307. *
  308. * The returned URI must be absolute if a base URI is provided
  309. */
  310. public static function parse(Rfc3986Uri|WhatWgUrl|Urn|BackedEnum|Stringable|string $uri, Rfc3986Uri|WhatWgUrl|Urn|BackedEnum|Stringable|string|null $baseUri = null): ?self
  311. {
  312. try {
  313. if (null === $baseUri) {
  314. return self::new($uri);
  315. }
  316. if ($uri instanceof Rfc3986Uri) {
  317. $uri = $uri->toRawString();
  318. }
  319. if ($uri instanceof WhatWgUrl) {
  320. $uri = $uri->toAsciiString();
  321. }
  322. if ($baseUri instanceof Rfc3986Uri) {
  323. $baseUri = $baseUri->toRawString();
  324. }
  325. if ($baseUri instanceof WhatWgUrl) {
  326. $baseUri = $baseUri->toAsciiString();
  327. }
  328. return self::new(UriString::resolve($uri, $baseUri));
  329. } catch (Throwable) {
  330. return null;
  331. }
  332. }
  333. /**
  334. * Creates a new instance from a template.
  335. *
  336. * @throws TemplateCanNotBeExpanded if the variables are invalid or missing
  337. * @throws UriException if the resulting expansion cannot be converted to a UriInterface instance
  338. */
  339. public static function fromTemplate(BackedEnum|UriTemplate|Stringable|string $template, iterable $variables = []): self
  340. {
  341. return match (true) {
  342. $template instanceof UriTemplate => self::new($template->expand($variables)),
  343. $template instanceof UriTemplate\Template => self::new($template->expand($variables)),
  344. default => self::new(UriTemplate\Template::new($template)->expand($variables)),
  345. };
  346. }
  347. /**
  348. * Create a new instance from a hash representation of the URI similar
  349. * to PHP parse_url function result.
  350. *
  351. * @param InputComponentMap $components a hash representation of the URI similar to PHP parse_url function result
  352. */
  353. public static function fromComponents(array $components = []): self
  354. {
  355. $components += [
  356. 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
  357. 'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
  358. ];
  359. if (null === $components['path']) {
  360. $components['path'] = '';
  361. }
  362. return new self(
  363. $components['scheme'],
  364. $components['user'],
  365. $components['pass'],
  366. $components['host'],
  367. $components['port'],
  368. $components['path'],
  369. $components['query'],
  370. $components['fragment']
  371. );
  372. }
  373. /**
  374. * Create a new instance from a data file path.
  375. *
  376. * @param SplFileInfo|SplFileObject|resource|Stringable|string $path
  377. * @param ?resource $context
  378. *
  379. * @throws MissingFeature If ext/fileinfo is not installed
  380. * @throws SyntaxError If the file does not exist or is not readable
  381. */
  382. public static function fromFileContents(mixed $path, $context = null): self
  383. {
  384. FeatureDetection::supportsFileDetection();
  385. $finfo = new finfo(FILEINFO_MIME_TYPE);
  386. $bufferSize = 8192;
  387. /** @var Closure(SplFileobject): array{0:string, 1:string} $fromFileObject */
  388. $fromFileObject = function (SplFileObject $path) use ($finfo, $bufferSize): array {
  389. $raw = $path->fread($bufferSize);
  390. false !== $raw || throw new SyntaxError('The file `'.$path.'` does not exist or is not readable.');
  391. $mimetype = (string) $finfo->buffer($raw);
  392. while (!$path->eof()) {
  393. $raw .= $path->fread($bufferSize);
  394. }
  395. return [$mimetype, $raw];
  396. };
  397. /** @var Closure(resource): array{0:string, 1:string} $fromResource */
  398. $fromResource = function ($stream) use ($finfo, $path, $bufferSize): array {
  399. set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
  400. $raw = fread($stream, $bufferSize);
  401. false !== $raw || throw new SyntaxError('The file `'.$path.'` does not exist or is not readable.');
  402. $mimetype = (string) $finfo->buffer($raw);
  403. while (!feof($stream)) {
  404. $raw .= fread($stream, $bufferSize);
  405. }
  406. restore_error_handler();
  407. return [$mimetype, $raw];
  408. };
  409. /** @var Closure(Stringable|string, resource|null): array{0:string, 1:string} $fromPath */
  410. $fromPath = function (Stringable|string $path, $context) use ($finfo): array {
  411. $path = (string) $path;
  412. set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
  413. $raw = file_get_contents(filename: $path, context: $context);
  414. restore_error_handler();
  415. false !== $raw || throw new SyntaxError('The file `'.$path.'` does not exist or is not readable.');
  416. $mimetype = (string) $finfo->file(filename: $path, flags: FILEINFO_MIME, context: $context);
  417. return [$mimetype, $raw];
  418. };
  419. [$mimetype, $raw] = match (true) {
  420. $path instanceof SplFileObject => $fromFileObject($path),
  421. $path instanceof SplFileInfo => $fromFileObject($path->openFile(mode: 'rb', context: $context)),
  422. is_resource($path) => $fromResource($path),
  423. $path instanceof Stringable,
  424. is_string($path) => $fromPath($path, $context),
  425. default => throw new TypeError('The path `'.$path.'` is not a valid resource.'),
  426. };
  427. return Uri::fromComponents([
  428. 'scheme' => 'data',
  429. 'path' => str_replace(' ', '', $mimetype.';base64,'.base64_encode($raw)),
  430. ]);
  431. }
  432. /**
  433. * Create a new instance from a data URI string.
  434. *
  435. * @throws SyntaxError If the parameter syntax is invalid
  436. */
  437. public static function fromData(BackedEnum|Stringable|string $data, string $mimetype = '', string $parameters = ''): self
  438. {
  439. static $regexpMimetype = ',^\w+/[-.\w]+(?:\+[-.\w]+)?$,';
  440. $mimetype = match (true) {
  441. '' === $mimetype => 'text/plain',
  442. 1 === preg_match($regexpMimetype, $mimetype) => $mimetype,
  443. default => throw new SyntaxError('Invalid mimeType, `'.$mimetype.'`.'),
  444. };
  445. if ($data instanceof BackedEnum) {
  446. $data = $data->value;
  447. }
  448. $data = (string) $data;
  449. if ('' === $parameters) {
  450. return self::fromComponents([
  451. 'scheme' => 'data',
  452. 'path' => self::formatDataPath($mimetype.','.rawurlencode($data)),
  453. ]);
  454. }
  455. $isInvalidParameter = static function (string $parameter): bool {
  456. $properties = explode('=', $parameter);
  457. return 2 !== count($properties) || 'base64' === strtolower($properties[0]);
  458. };
  459. if (str_starts_with($parameters, ';')) {
  460. $parameters = substr($parameters, 1);
  461. }
  462. return match ([]) {
  463. array_filter(explode(';', $parameters), $isInvalidParameter) => self::fromComponents([
  464. 'scheme' => 'data',
  465. 'path' => self::formatDataPath($mimetype.';'.$parameters.','.rawurlencode($data)),
  466. ]),
  467. default => throw new SyntaxError(sprintf('Invalid mediatype parameters, `%s`.', $parameters))
  468. };
  469. }
  470. /**
  471. * Create a new instance from a Unix path string.
  472. */
  473. public static function fromUnixPath(BackedEnum|Stringable|string $path): self
  474. {
  475. if ($path instanceof BackedEnum) {
  476. $path = $path->value;
  477. }
  478. $path = implode('/', array_map(rawurlencode(...), explode('/', (string) $path)));
  479. return Uri::fromComponents(match (true) {
  480. '/' !== ($path[0] ?? '') => ['path' => $path],
  481. default => ['path' => $path, 'scheme' => 'file', 'host' => ''],
  482. });
  483. }
  484. /**
  485. * Create a new instance from a local Windows path string.
  486. */
  487. public static function fromWindowsPath(BackedEnum|Stringable|string $path): self
  488. {
  489. if ($path instanceof BackedEnum) {
  490. $path = $path->value;
  491. }
  492. $root = '';
  493. $path = (string) $path;
  494. if (1 === preg_match(self::REGEXP_WINDOW_PATH, $path, $matches)) {
  495. $root = substr($matches['root'], 0, -1).':';
  496. $path = substr($path, strlen($root));
  497. }
  498. $path = str_replace('\\', '/', $path);
  499. $path = implode('/', array_map(rawurlencode(...), explode('/', $path)));
  500. //Local Windows absolute path
  501. if ('' !== $root) {
  502. return Uri::fromComponents(['path' => '/'.$root.$path, 'scheme' => 'file', 'host' => '']);
  503. }
  504. //UNC Windows Path
  505. if (!str_starts_with($path, '//')) {
  506. return Uri::fromComponents(['path' => $path]);
  507. }
  508. [$host, $path] = explode('/', substr($path, 2), 2) + [1 => ''];
  509. return Uri::fromComponents(['host' => $host, 'path' => '/'.$path, 'scheme' => 'file']);
  510. }
  511. /**
  512. * Creates a new instance from a RFC8089 compatible URI.
  513. *
  514. * @see https://datatracker.ietf.org/doc/html/rfc8089
  515. */
  516. public static function fromRfc8089(BackedEnum|Stringable|string $uri): static
  517. {
  518. if ($uri instanceof BackedEnum) {
  519. $uri = $uri->value;
  520. }
  521. $fileUri = self::new((string) preg_replace(',^(file:/)([^/].*)$,i', 'file:///$2', (string) $uri));
  522. $scheme = $fileUri->getScheme();
  523. return match (true) {
  524. 'file' !== $scheme => throw new SyntaxError('As per RFC8089, the URI scheme must be `file`.'),
  525. 'localhost' === $fileUri->getAuthority() => $fileUri->withHost(''),
  526. default => $fileUri,
  527. };
  528. }
  529. /**
  530. * Create a new instance from the environment.
  531. */
  532. public static function fromServer(array $server): self
  533. {
  534. $components = ['scheme' => self::fetchScheme($server)];
  535. [$components['user'], $components['pass']] = self::fetchUserInfo($server);
  536. [$components['host'], $components['port']] = self::fetchHostname($server);
  537. [$components['path'], $components['query']] = self::fetchRequestUri($server);
  538. return Uri::fromComponents($components);
  539. }
  540. /**
  541. * Returns the environment scheme.
  542. */
  543. private static function fetchScheme(array $server): string
  544. {
  545. $server += ['HTTPS' => ''];
  546. return match (true) {
  547. false !== filter_var($server['HTTPS'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) => 'https',
  548. default => 'http',
  549. };
  550. }
  551. /**
  552. * Returns the environment user info.
  553. *
  554. * @return non-empty-array {0: ?string, 1: ?string}
  555. */
  556. private static function fetchUserInfo(array $server): array
  557. {
  558. $server += ['PHP_AUTH_USER' => null, 'PHP_AUTH_PW' => null, 'HTTP_AUTHORIZATION' => ''];
  559. $user = $server['PHP_AUTH_USER'];
  560. $pass = $server['PHP_AUTH_PW'];
  561. if (str_starts_with(strtolower($server['HTTP_AUTHORIZATION']), 'basic')) {
  562. $userinfo = base64_decode(substr($server['HTTP_AUTHORIZATION'], 6), true);
  563. false !== $userinfo || throw new SyntaxError('The user info could not be detected');
  564. [$user, $pass] = explode(':', $userinfo, 2) + [1 => null];
  565. }
  566. if (null !== $user) {
  567. $user = rawurlencode($user);
  568. }
  569. if (null !== $pass) {
  570. $pass = rawurlencode($pass);
  571. }
  572. return [$user, $pass];
  573. }
  574. /**
  575. * Returns the environment host.
  576. *
  577. * @throws SyntaxError If the host cannot be detected
  578. *
  579. * @return array{0:string|null, 1:int|null}
  580. */
  581. private static function fetchHostname(array $server): array
  582. {
  583. $server += ['SERVER_PORT' => null];
  584. if (null !== $server['SERVER_PORT']) {
  585. $server['SERVER_PORT'] = (int) $server['SERVER_PORT'];
  586. }
  587. if (isset($server['HTTP_HOST']) && 1 === preg_match(self::REGEXP_HOST_PORT, $server['HTTP_HOST'], $matches)) {
  588. $matches += ['host' => null, 'port' => null];
  589. if (null !== $matches['port']) {
  590. $matches['port'] = (int) $matches['port'];
  591. }
  592. return [$matches['host'], $matches['port'] ?? $server['SERVER_PORT']];
  593. }
  594. isset($server['SERVER_ADDR']) || throw new SyntaxError('The host could not be detected');
  595. if (false === filter_var($server['SERVER_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
  596. return ['['.$server['SERVER_ADDR'].']', $server['SERVER_PORT']];
  597. }
  598. return [$server['SERVER_ADDR'], $server['SERVER_PORT']];
  599. }
  600. /**
  601. * Returns the environment path.
  602. *
  603. * @return list<?string>
  604. */
  605. private static function fetchRequestUri(array $server): array
  606. {
  607. $server += ['IIS_WasUrlRewritten' => null, 'UNENCODED_URL' => '', 'PHP_SELF' => '', 'QUERY_STRING' => null];
  608. if ('1' === $server['IIS_WasUrlRewritten'] && '' !== $server['UNENCODED_URL']) {
  609. return explode('?', $server['UNENCODED_URL'], 2) + [1 => null];
  610. }
  611. if (isset($server['REQUEST_URI'])) {
  612. [$path] = explode('?', $server['REQUEST_URI'], 2);
  613. $query = ('' !== $server['QUERY_STRING']) ? $server['QUERY_STRING'] : null;
  614. return [$path, $query];
  615. }
  616. return [$server['PHP_SELF'], $server['QUERY_STRING']];
  617. }
  618. /**
  619. * Format the Path component.
  620. */
  621. private function formatPath(string $path): string
  622. {
  623. $path = match ($this->scheme) {
  624. 'data' => Encoder::encodePath(self::formatDataPath($path)),
  625. 'file' => self::formatFilePath(Encoder::encodePath($path)),
  626. default => Encoder::encodePath($path),
  627. };
  628. if ('' === $path) {
  629. return $path;
  630. }
  631. if (null !== $this->authority) {
  632. // If there is an authority, the path must start with a `/`
  633. return str_starts_with($path, '/') ? $path : '/'.$path;
  634. }
  635. // If there is no authority, the path cannot start with `//`
  636. if (str_starts_with($path, '//')) {
  637. return '/.'.$path;
  638. }
  639. $colonPos = strpos($path, ':');
  640. if (false !== $colonPos && null === $this->scheme) {
  641. // In the absence of a scheme and of an authority,
  642. // the first path segment cannot contain a colon (":") character.'
  643. $slashPos = strpos($path, '/');
  644. (false !== $slashPos && $colonPos > $slashPos) || throw new SyntaxError(
  645. 'In absence of the scheme and authority components, the first path segment cannot contain a colon (":") character.'
  646. );
  647. }
  648. return $path;
  649. }
  650. /**
  651. * Filter the Path component.
  652. *
  653. * @link https://tools.ietf.org/html/rfc2397
  654. *
  655. * @throws SyntaxError If the path is not compliant with RFC2397
  656. */
  657. private static function formatDataPath(string $path): string
  658. {
  659. if ('' == $path) {
  660. return 'text/plain;charset=us-ascii,';
  661. }
  662. if (strlen($path) !== strspn($path, self::ASCII) || !str_contains($path, ',')) {
  663. throw new SyntaxError('The path `'.$path.'` is invalid according to RFC2937.');
  664. }
  665. $parts = explode(',', $path, 2) + [1 => null];
  666. $mediatype = explode(';', (string) $parts[0], 2) + [1 => null];
  667. $data = (string) $parts[1];
  668. $mimetype = $mediatype[0];
  669. if (null === $mimetype || '' === $mimetype) {
  670. $mimetype = 'text/plain';
  671. }
  672. $parameters = $mediatype[1];
  673. if (null === $parameters || '' === $parameters) {
  674. $parameters = 'charset=us-ascii';
  675. }
  676. self::assertValidPath($mimetype, $parameters, $data);
  677. return $mimetype.';'.$parameters.','.$data;
  678. }
  679. /**
  680. * Assert the path is a compliant with RFC2397.
  681. *
  682. * @link https://tools.ietf.org/html/rfc2397
  683. *
  684. * @throws SyntaxError If the mediatype or the data are not compliant with the RFC2397
  685. */
  686. private static function assertValidPath(string $mimetype, string $parameters, string $data): void
  687. {
  688. 1 === preg_match(self::REGEXP_MIMETYPE, $mimetype) || throw new SyntaxError('The path mimetype `'.$mimetype.'` is invalid.');
  689. $isBinary = 1 === preg_match(self::REGEXP_BINARY, $parameters, $matches);
  690. if ($isBinary) {
  691. $parameters = substr($parameters, 0, - strlen($matches[0]));
  692. }
  693. $res = array_filter(array_filter(explode(';', $parameters), self::validateParameter(...)));
  694. [] === $res || throw new SyntaxError('The path parameters `'.$parameters.'` is invalid.');
  695. if (!$isBinary) {
  696. return;
  697. }
  698. $res = base64_decode($data, true);
  699. if (false === $res || $data !== base64_encode($res)) {
  700. throw new SyntaxError('The path data `'.$data.'` is invalid.');
  701. }
  702. }
  703. /**
  704. * Validate mediatype parameter.
  705. */
  706. private static function validateParameter(string $parameter): bool
  707. {
  708. $properties = explode('=', $parameter);
  709. return 2 != count($properties) || 'base64' === strtolower($properties[0]);
  710. }
  711. /**
  712. * Format the path component for the URI scheme file.
  713. */
  714. private static function formatFilePath(string $path): string
  715. {
  716. return (string) preg_replace_callback(
  717. self::REGEXP_FILE_PATH,
  718. static fn (array $matches): string => $matches['delim'].$matches['volume'].(isset($matches['rest']) ? ':'.$matches['rest'] : ''),
  719. $path
  720. );
  721. }
  722. /**
  723. * assert the URI internal state is valid.
  724. *
  725. * @link https://tools.ietf.org/html/rfc3986#section-3
  726. * @link https://tools.ietf.org/html/rfc3986#section-3.3
  727. *
  728. * @throws SyntaxError if the URI is in an invalid state, according to RFC3986
  729. */
  730. private function assertValidRfc3986Uri(): void
  731. {
  732. if (null !== $this->authority && ('' !== $this->path && '/' !== $this->path[0])) {
  733. throw new SyntaxError('If an authority is present the path must be empty or start with a `/`.');
  734. }
  735. if (null === $this->authority && str_starts_with($this->path, '//')) {
  736. throw new SyntaxError('If there is no authority the path `'.$this->path.'` cannot start with a `//`.');
  737. }
  738. $pos = strpos($this->path, ':');
  739. if (null === $this->authority
  740. && null === $this->scheme
  741. && false !== $pos
  742. && !str_contains(substr($this->path, 0, $pos), '/')
  743. ) {
  744. throw new SyntaxError('In absence of a scheme and an authority the first path segment cannot contain a colon (":") character.');
  745. }
  746. }
  747. /**
  748. * assert the URI scheme is valid.
  749. *
  750. * @link https://w3c.github.io/FileAPI/#url
  751. * @link https://datatracker.ietf.org/doc/html/rfc2397
  752. * @link https://tools.ietf.org/html/rfc3986#section-3
  753. * @link https://tools.ietf.org/html/rfc3986#section-3.3
  754. *
  755. * @throws SyntaxError if the URI is in an invalid state, according to scheme-specific rules
  756. */
  757. private function assertValidState(): void
  758. {
  759. $scheme = UriScheme::tryFrom((string) $this->scheme);
  760. if (null === $scheme) {
  761. return;
  762. }
  763. $schemeType = $scheme->type();
  764. match ($scheme) {
  765. UriScheme::Blob => $this->isValidBlob(),
  766. UriScheme::Mailto => $this->isValidMailto(),
  767. UriScheme::Data,
  768. UriScheme::About,
  769. UriScheme::Javascript => $this->isUriWithSchemeAndPathOnly(),
  770. UriScheme::File => $this->isUriWithSchemeHostAndPathOnly(),
  771. UriScheme::Ftp,
  772. UriScheme::Gopher,
  773. UriScheme::Afp,
  774. UriScheme::Dict,
  775. UriScheme::Msrps,
  776. UriScheme::Msrp,
  777. UriScheme::Mtqp,
  778. UriScheme::Rsync,
  779. UriScheme::Ssh,
  780. UriScheme::Svn,
  781. UriScheme::Snmp => $this->isNonEmptyHostUriWithoutFragmentAndQuery(),
  782. UriScheme::Https,
  783. UriScheme::Http => $this->isNonEmptyHostUri(),
  784. UriScheme::Ws,
  785. UriScheme::Wss,
  786. UriScheme::Ipp,
  787. UriScheme::Ipps => $this->isNonEmptyHostUriWithoutFragment(),
  788. UriScheme::Ldap,
  789. UriScheme::Ldaps,
  790. UriScheme::Acap,
  791. UriScheme::Imaps,
  792. UriScheme::Imap,
  793. UriScheme::Redis => null === $this->fragment,
  794. UriScheme::Prospero => null === $this->fragment && null === $this->query && null === $this->userInfo,
  795. UriScheme::Urn => null !== Urn::parse($this->uriAsciiString),
  796. UriScheme::Telnet,
  797. UriScheme::Tn3270 => null === $this->fragment && null === $this->query && in_array($this->path, ['', '/'], true),
  798. UriScheme::Vnc => null !== $this->authority && null === $this->fragment && '' === $this->path,
  799. default => $schemeType->isUnknown()
  800. || ($schemeType->isOpaque() && null === $this->authority)
  801. || ($schemeType->isHierarchical() && null !== $this->authority),
  802. } || throw new SyntaxError('The uri `'.$this->uriAsciiString.'` is invalid for the `'.$this->scheme.'` scheme.');
  803. }
  804. private function isValidBlob(): bool
  805. {
  806. static $regexpUuidRfc4122 = '/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i';
  807. if (!$this->isUriWithSchemeAndPathOnly()
  808. || '' === $this->path
  809. || !str_contains($this->path, '/')
  810. || str_ends_with($this->path, '/')
  811. || 1 !== preg_match($regexpUuidRfc4122, basename($this->path))
  812. ) {
  813. return false;
  814. }
  815. $origin = dirname($this->path);
  816. if ('null' === $origin) {
  817. return true;
  818. }
  819. try {
  820. $components = UriString::parse($origin);
  821. return '' === $components['path']
  822. && null === $components['query']
  823. && null === $components['fragment']
  824. && true === UriScheme::tryFrom((string) $components['scheme'])?->isWhatWgSpecial();
  825. } catch (UriException) {
  826. return false;
  827. }
  828. }
  829. private function isValidMailto(): bool
  830. {
  831. if (null !== $this->authority || null !== $this->fragment || str_contains((string) $this->query, '?')) {
  832. return false;
  833. }
  834. static $mailHeaders = [
  835. 'to', 'cc', 'bcc', 'reply-to', 'from', 'sender',
  836. 'resent-to', 'resent-cc', 'resent-bcc', 'resent-from', 'resent-sender',
  837. 'return-path', 'delivery-to', 'site-owner',
  838. ];
  839. static $headerRegexp = '/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D';
  840. $pairs = QueryString::parseFromValue($this->query);
  841. $hasTo = false;
  842. foreach ($pairs as [$name, $value]) {
  843. $headerName = strtolower($name);
  844. if (in_array($headerName, $mailHeaders, true)) {
  845. if (null === $value || !self::validateEmailList($value)) {
  846. return false;
  847. }
  848. if (!$hasTo && 'to' === $headerName) {
  849. $hasTo = true;
  850. }
  851. continue;
  852. }
  853. if (1 !== preg_match($headerRegexp, (string) Encoder::decodeAll($name))) {
  854. return false;
  855. }
  856. }
  857. return '' === $this->path ? $hasTo : self::validateEmailList($this->path);
  858. }
  859. private static function validateEmailList(string $emails): bool
  860. {
  861. foreach (explode(',', $emails) as $email) {
  862. if (false === filter_var((string) Encoder::decodeAll($email), FILTER_VALIDATE_EMAIL)) {
  863. return false;
  864. }
  865. }
  866. return '' !== $emails;
  867. }
  868. /**
  869. * Sets the URI origin.
  870. *
  871. * The origin read-only property of the URL interface returns a string containing
  872. * the Unicode serialization of the represented URL.
  873. */
  874. private function setOrigin(): ?string
  875. {
  876. try {
  877. if ('blob' !== $this->scheme) {
  878. if (!(UriScheme::tryFrom($this->scheme ?? '')?->isWhatWgSpecial() ?? false)) {
  879. return null;
  880. }
  881. $host = $this->host;
  882. $converted = $host;
  883. if (null !== $converted) {
  884. try {
  885. $converted = IPv4Converter::fromEnvironment()->toDecimal($host);
  886. } catch (MissingFeature) {
  887. $converted = null;
  888. }
  889. if (false === filter_var($converted, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
  890. $converted = IPv6Converter::compress($host);
  891. }
  892. /** @var string $converted */
  893. if ($converted !== $host) {
  894. $converted = Idna\Converter::toAscii($converted)->domain();
  895. }
  896. }
  897. return $this
  898. ->withFragment(null)
  899. ->withQuery(null)
  900. ->withPath('')
  901. ->withUserInfo(null)
  902. ->withHost($converted)
  903. ->toString();
  904. }
  905. $components = UriString::parse($this->path);
  906. $scheme = strtolower($components['scheme'] ?? '');
  907. if (! (UriScheme::tryFrom($scheme)?->isWhatWgSpecial() ?? false)) {
  908. return null;
  909. }
  910. return self::fromComponents($components)->origin;
  911. } catch (UriException) {
  912. return null;
  913. }
  914. }
  915. /**
  916. * URI validation for URI schemes which allows only scheme and path components.
  917. */
  918. private function isUriWithSchemeAndPathOnly(): bool
  919. {
  920. return null === $this->authority
  921. && null === $this->query
  922. && null === $this->fragment;
  923. }
  924. /**
  925. * URI validation for URI schemes which allows only scheme, host and path components.
  926. */
  927. private function isUriWithSchemeHostAndPathOnly(): bool
  928. {
  929. return null === $this->userInfo
  930. && null === $this->port
  931. && null === $this->query
  932. && null === $this->fragment
  933. && !('' != $this->scheme && null === $this->host);
  934. }
  935. /**
  936. * URI validation for URI schemes which disallow the empty '' host.
  937. */
  938. private function isNonEmptyHostUri(): bool
  939. {
  940. return '' !== $this->host
  941. && !(null !== $this->scheme && null === $this->host);
  942. }
  943. /**
  944. * URI validation for URIs schemes which disallow the empty '' host
  945. * and forbids the fragment component.
  946. */
  947. private function isNonEmptyHostUriWithoutFragment(): bool
  948. {
  949. return $this->isNonEmptyHostUri() && null === $this->fragment;
  950. }
  951. /**
  952. * URI validation for URIs schemes which disallow the empty '' host
  953. * and forbids fragment and query components.
  954. */
  955. private function isNonEmptyHostUriWithoutFragmentAndQuery(): bool
  956. {
  957. return $this->isNonEmptyHostUri() && null === $this->fragment && null === $this->query;
  958. }
  959. public function __toString(): string
  960. {
  961. return $this->toString();
  962. }
  963. /**
  964. * Returns the string representation as a URI reference.
  965. *
  966. * @see http://tools.ietf.org/html/rfc3986#section-4.1
  967. * @see ::toString
  968. */
  969. public function jsonSerialize(): string
  970. {
  971. return $this->toString();
  972. }
  973. /**
  974. * Returns the string representation as a URI reference.
  975. *
  976. * @see http://tools.ietf.org/html/rfc3986#section-4.1
  977. */
  978. public function toString(): string
  979. {
  980. return $this->toAsciiString();
  981. }
  982. /**
  983. * Returns the string representation as a URI reference.
  984. *
  985. * @see http://tools.ietf.org/html/rfc3986#section-4.1
  986. */
  987. public function toAsciiString(): string
  988. {
  989. return $this->uriAsciiString;
  990. }
  991. /**
  992. * Returns the string representation as a URI reference.
  993. *
  994. * The host is converted to its UNICODE representation if available
  995. */
  996. public function toUnicodeString(): string
  997. {
  998. return $this->uriUnicodeString;
  999. }
  1000. /**
  1001. * Returns the human-readable string representation of the URI as an IRI.
  1002. *
  1003. * @see https://datatracker.ietf.org/doc/html/rfc3987
  1004. */
  1005. public function toDisplayString(): string
  1006. {
  1007. return UriString::toIriString($this->toString());
  1008. }
  1009. /**
  1010. * Returns the Unix filesystem path.
  1011. *
  1012. * The method will return null if a scheme is present and is not the `file` scheme
  1013. */
  1014. public function toUnixPath(): ?string
  1015. {
  1016. return match ($this->scheme) {
  1017. 'file', null => rawurldecode($this->path),
  1018. default => null,
  1019. };
  1020. }
  1021. /**
  1022. * Returns the Windows filesystem path.
  1023. *
  1024. * The method will return null if a scheme is present and is not the `file` scheme
  1025. */
  1026. public function toWindowsPath(): ?string
  1027. {
  1028. static $regexpWindowsPath = ',^(?<root>[a-zA-Z]:),';
  1029. if (!in_array($this->scheme, ['file', null], true)) {
  1030. return null;
  1031. }
  1032. $originalPath = $this->path;
  1033. $path = $originalPath;
  1034. if ('/' === ($path[0] ?? '')) {
  1035. $path = substr($path, 1);
  1036. }
  1037. if (1 === preg_match($regexpWindowsPath, $path, $matches)) {
  1038. $root = $matches['root'];
  1039. $path = substr($path, strlen($root));
  1040. return $root.str_replace('/', '\\', rawurldecode($path));
  1041. }
  1042. $host = $this->host;
  1043. return match (null) {
  1044. $host => str_replace('/', '\\', rawurldecode($originalPath)),
  1045. default => '\\\\'.$host.'\\'.str_replace('/', '\\', rawurldecode($path)),
  1046. };
  1047. }
  1048. /**
  1049. * Returns a string representation of a File URI according to RFC8089.
  1050. *
  1051. * The method will return null if the URI scheme is not the `file` scheme
  1052. *
  1053. * @see https://datatracker.ietf.org/doc/html/rfc8089
  1054. */
  1055. public function toRfc8089(): ?string
  1056. {
  1057. $path = $this->path;
  1058. return match (true) {
  1059. 'file' !== $this->scheme => null,
  1060. in_array($this->authority, ['', null, 'localhost'], true) => 'file:'.match (true) {
  1061. '' === $path,
  1062. '/' === $path[0] => $path,
  1063. default => '/'.$path,
  1064. },
  1065. default => $this->toString(),
  1066. };
  1067. }
  1068. /**
  1069. * Save the data to a specific file.
  1070. *
  1071. * The method returns the number of bytes written to the file
  1072. * or null for any other scheme except the data scheme
  1073. *
  1074. * @param SplFileInfo|SplFileObject|resource|Stringable|string $destination
  1075. * @param ?resource $context
  1076. *
  1077. * @throws RuntimeException if the content cannot be stored.
  1078. */
  1079. public function toFileContents(mixed $destination, $context = null): ?int
  1080. {
  1081. if ('data' !== $this->scheme) {
  1082. return null;
  1083. }
  1084. [$mediaType, $document] = explode(',', $this->path, 2) + [0 => '', 1 => null];
  1085. null !== $document || throw new RuntimeException('Unable to extract the document part from the URI path.');
  1086. $data = match (true) {
  1087. str_ends_with((string) $mediaType, ';base64') => (string) base64_decode($document, true),
  1088. default => rawurldecode($document),
  1089. };
  1090. $res = match (true) {
  1091. $destination instanceof SplFileObject => $destination->fwrite($data),
  1092. $destination instanceof SplFileInfo => $destination->openFile(mode:'wb', context: $context)->fwrite($data),
  1093. is_resource($destination) => fwrite($destination, $data),
  1094. $destination instanceof Stringable,
  1095. is_string($destination) => (function () use ($destination, $data, $context): int|false {
  1096. set_error_handler(fn (int $errno, string $errstr, string $errfile, int $errline) => true);
  1097. $rsrc = fopen((string) $destination, mode:'wb', context: $context);
  1098. if (false === $rsrc) {
  1099. restore_error_handler();
  1100. throw new RuntimeException('Unable to open the destination file: '.$destination);
  1101. }
  1102. $bytes = fwrite($rsrc, $data);
  1103. fclose($rsrc);
  1104. restore_error_handler();
  1105. return $bytes;
  1106. })(),
  1107. default => throw new TypeError('Unsupported destination type; expected SplFileObject, SplFileInfo, resource or a string; '.(is_object($destination) ? $destination::class : gettype($destination)).' given.'),
  1108. };
  1109. false !== $res || throw new RuntimeException('Unable to write to the destination file.');
  1110. return $res;
  1111. }
  1112. /**
  1113. * Returns an associative array containing all the URI components.
  1114. *
  1115. * @return ComponentMap
  1116. */
  1117. public function toComponents(): array
  1118. {
  1119. return [
  1120. 'scheme' => $this->scheme,
  1121. 'user' => $this->user,
  1122. 'pass' => $this->pass,
  1123. 'host' => $this->host,
  1124. 'port' => $this->port,
  1125. 'path' => $this->path,
  1126. 'query' => $this->query,
  1127. 'fragment' => $this->fragment,
  1128. ];
  1129. }
  1130. public function getScheme(): ?string
  1131. {
  1132. return $this->scheme;
  1133. }
  1134. public function getAuthority(): ?string
  1135. {
  1136. return $this->authority;
  1137. }
  1138. /**
  1139. * Returns the user component encoded value.
  1140. *
  1141. * @see https://wiki.php.net/rfc/url_parsing_api
  1142. */
  1143. public function getUsername(): ?string
  1144. {
  1145. return $this->user;
  1146. }
  1147. public function getPassword(): ?string
  1148. {
  1149. return $this->pass;
  1150. }
  1151. public function getUserInfo(): ?string
  1152. {
  1153. return $this->userInfo;
  1154. }
  1155. public function getHost(): ?string
  1156. {
  1157. return $this->host;
  1158. }
  1159. public function getUnicodeHost(): ?string
  1160. {
  1161. if (null === $this->host) {
  1162. return null;
  1163. }
  1164. $host = IdnaConverter::toUnicode($this->host)->domain();
  1165. if ($host === $this->host) {
  1166. return $this->host;
  1167. }
  1168. return $host;
  1169. }
  1170. public function isIpv4Host(): bool
  1171. {
  1172. return HostRecord::isIpv4($this->host);
  1173. }
  1174. public function isIpv6Host(): bool
  1175. {
  1176. return HostRecord::isIpv6($this->host);
  1177. }
  1178. public function isIpvFutureHost(): bool
  1179. {
  1180. return HostRecord::isIpvFuture($this->host);
  1181. }
  1182. public function isIpHost(): bool
  1183. {
  1184. return HostRecord::isIp($this->host);
  1185. }
  1186. public function isRegisteredNameHost(): bool
  1187. {
  1188. return HostRecord::isRegisteredName($this->host);
  1189. }
  1190. public function isDomainHost(): bool
  1191. {
  1192. return HostRecord::isDomain($this->host);
  1193. }
  1194. public function getPort(): ?int
  1195. {
  1196. return $this->port;
  1197. }
  1198. public function getPath(): string
  1199. {
  1200. return $this->path;
  1201. }
  1202. public function getQuery(): ?string
  1203. {
  1204. return $this->query;
  1205. }
  1206. public function getFragment(): ?string
  1207. {
  1208. return $this->fragment;
  1209. }
  1210. public function getOrigin(): ?string
  1211. {
  1212. return $this->origin;
  1213. }
  1214. public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
  1215. {
  1216. if (!is_bool($condition)) {
  1217. $condition = $condition($this);
  1218. }
  1219. return match (true) {
  1220. $condition => $onSuccess($this),
  1221. null !== $onFail => $onFail($this),
  1222. default => $this,
  1223. } ?? $this;
  1224. }
  1225. public function transform(callable $callback): static
  1226. {
  1227. return $callback($this);
  1228. }
  1229. public function withScheme(BackedEnum|Stringable|string|null $scheme): static
  1230. {
  1231. $scheme = $this->formatScheme($this->filterString($scheme));
  1232. return match ($scheme) {
  1233. $this->scheme => $this,
  1234. default => new self($scheme, $this->user, $this->pass, $this->host, $this->port, $this->path, $this->query, $this->fragment),
  1235. };
  1236. }
  1237. /**
  1238. * Filter a string.
  1239. *
  1240. * @throws SyntaxError if the submitted data cannot be converted to string
  1241. */
  1242. private function filterString(BackedEnum|Stringable|string|null $str): ?string
  1243. {
  1244. $str = match (true) {
  1245. $str instanceof FragmentDirective => $str->toFragmentValue(),
  1246. $str instanceof UriComponentInterface => $str->value(),
  1247. $str instanceof BackedEnum => (string) $str->value,
  1248. null === $str => null,
  1249. default => (string) $str,
  1250. };
  1251. return match (true) {
  1252. null === $str => null,
  1253. 1 === preg_match(self::REGEXP_INVALID_CHARS, $str) => throw new SyntaxError('The component `'.$str.'` contains invalid characters.'),
  1254. default => $str,
  1255. };
  1256. }
  1257. public function withUserInfo(
  1258. BackedEnum|Stringable|string|null $user,
  1259. #[SensitiveParameter] BackedEnum|Stringable|string|null $password = null
  1260. ): static {
  1261. $user = Encoder::encodeUser($this->filterString($user));
  1262. $pass = Encoder::encodePassword($this->filterString($password));
  1263. $userInfo = $user;
  1264. if (null !== $password) {
  1265. $userInfo .= ':'.$pass;
  1266. }
  1267. return match ($userInfo) {
  1268. $this->userInfo => $this,
  1269. default => new self($this->scheme, $user, $pass, $this->host, $this->port, $this->path, $this->query, $this->fragment),
  1270. };
  1271. }
  1272. public function withUsername(BackedEnum|Stringable|string|null $user): static
  1273. {
  1274. return $this->withUserInfo($user, $this->pass);
  1275. }
  1276. public function withPassword(#[SensitiveParameter] BackedEnum|Stringable|string|null $password): static
  1277. {
  1278. return $this->withUserInfo($this->user, $password);
  1279. }
  1280. public function withHost(BackedEnum|Stringable|string|null $host): static
  1281. {
  1282. $host = $this->formatHost($this->filterString($host));
  1283. return match ($host) {
  1284. $this->host => $this,
  1285. default => new self($this->scheme, $this->user, $this->pass, $host, $this->port, $this->path, $this->query, $this->fragment),
  1286. };
  1287. }
  1288. public function withPort(BackedEnum|int|null $port): static
  1289. {
  1290. $port = $this->formatPort($port);
  1291. return match ($port) {
  1292. $this->port => $this,
  1293. default => new self($this->scheme, $this->user, $this->pass, $this->host, $port, $this->path, $this->query, $this->fragment),
  1294. };
  1295. }
  1296. public function withPath(BackedEnum|Stringable|string $path): static
  1297. {
  1298. $path = $this->formatPath($this->filterString($path) ?? throw new SyntaxError('The path component cannot be null.'));
  1299. return match ($path) {
  1300. $this->path => $this,
  1301. default => new self($this->scheme, $this->user, $this->pass, $this->host, $this->port, $path, $this->query, $this->fragment),
  1302. };
  1303. }
  1304. public function withQuery(BackedEnum|Stringable|string|null $query): static
  1305. {
  1306. $query = Encoder::encodeQueryOrFragment($this->filterString($query));
  1307. return match ($query) {
  1308. $this->query => $this,
  1309. default => new self($this->scheme, $this->user, $this->pass, $this->host, $this->port, $this->path, $query, $this->fragment),
  1310. };
  1311. }
  1312. public function withFragment(BackedEnum|Stringable|string|null $fragment): static
  1313. {
  1314. $fragment = Encoder::encodeQueryOrFragment($this->filterString($fragment));
  1315. return match ($fragment) {
  1316. $this->fragment => $this,
  1317. default => new self($this->scheme, $this->user, $this->pass, $this->host, $this->port, $this->path, $this->query, $fragment),
  1318. };
  1319. }
  1320. /**
  1321. * Tells whether the `file` scheme base URI represents a local file.
  1322. */
  1323. public function isLocalFile(): bool
  1324. {
  1325. return match (true) {
  1326. 'file' !== $this->scheme => false,
  1327. in_array($this->authority, ['', null, 'localhost'], true) => true,
  1328. default => false,
  1329. };
  1330. }
  1331. /**
  1332. * Tells whether the URI is opaque or not.
  1333. *
  1334. * A URI is opaque if and only if it is absolute
  1335. * and does not have an authority path.
  1336. */
  1337. public function isOpaque(): bool
  1338. {
  1339. return null === $this->authority
  1340. && null !== $this->scheme;
  1341. }
  1342. /**
  1343. * Tells whether two URI do not share the same origin.
  1344. */
  1345. public function isCrossOrigin(Rfc3986Uri|WhatWgUrl|Urn|Stringable|string $uri): bool
  1346. {
  1347. if (null === $this->origin) {
  1348. return true;
  1349. }
  1350. $uri = self::tryNew($uri);
  1351. if (null === $uri || null === ($origin = $uri->getOrigin())) {
  1352. return true;
  1353. }
  1354. return $this->origin !== $origin;
  1355. }
  1356. public function isSameOrigin(Rfc3986Uri|WhatWgUrl|Urn|Stringable|string $uri): bool
  1357. {
  1358. return ! $this->isCrossOrigin($uri);
  1359. }
  1360. /**
  1361. * Tells whether the URI is absolute.
  1362. */
  1363. public function isAbsolute(): bool
  1364. {
  1365. return null !== $this->scheme;
  1366. }
  1367. /**
  1368. * Tells whether the URI is a network path.
  1369. */
  1370. public function isNetworkPath(): bool
  1371. {
  1372. return null === $this->scheme
  1373. && null !== $this->authority;
  1374. }
  1375. /**
  1376. * Tells whether the URI is an absolute path.
  1377. */
  1378. public function isAbsolutePath(): bool
  1379. {
  1380. return null === $this->scheme
  1381. && null === $this->authority
  1382. && '/' === ($this->path[0] ?? '');
  1383. }
  1384. /**
  1385. * Tells whether the URI is a relative path.
  1386. */
  1387. public function isRelativePath(): bool
  1388. {
  1389. return null === $this->scheme
  1390. && null === $this->authority
  1391. && '/' !== ($this->path[0] ?? '');
  1392. }
  1393. /**
  1394. * Tells whether both URIs refer to the same document.
  1395. */
  1396. public function isSameDocument(Rfc3986Uri|WhatWgUrl|UriInterface|Stringable|Urn|string $uri): bool
  1397. {
  1398. return $this->equals($uri);
  1399. }
  1400. public function equals(Rfc3986Uri|WhatWgUrl|UriInterface|Stringable|Urn|string $uri, UriComparisonMode $uriComparisonMode = UriComparisonMode::ExcludeFragment): bool
  1401. {
  1402. if (!$uri instanceof UriInterface && !$uri instanceof Rfc3986Uri && !$uri instanceof WhatWgUrl) {
  1403. $uri = self::tryNew($uri);
  1404. }
  1405. if (null === $uri) {
  1406. return false;
  1407. }
  1408. $baseUri = $this;
  1409. if (UriComparisonMode::ExcludeFragment === $uriComparisonMode) {
  1410. $uri = $uri->withFragment(null);
  1411. $baseUri = $baseUri->withFragment(null);
  1412. }
  1413. return $baseUri->normalize()->toString() === match (true) {
  1414. $uri instanceof Rfc3986Uri => $uri->toString(),
  1415. $uri instanceof WhatWgUrl => $uri->toAsciiString(),
  1416. default => $uri->normalize()->toString(),
  1417. };
  1418. }
  1419. /**
  1420. * Normalize a URI by applying non-destructive and destructive normalization
  1421. * rules as defined in RFC3986 and RFC3987.
  1422. */
  1423. public function normalize(): static
  1424. {
  1425. $uriString = $this->toString();
  1426. if ('' === $uriString) {
  1427. return $this;
  1428. }
  1429. $normalizedUriString = UriString::normalize($uriString);
  1430. $normalizedUri = self::new($normalizedUriString);
  1431. if (null !== $normalizedUri->getAuthority() && ('' === $normalizedUri->getPath() && (UriScheme::tryFrom($normalizedUri->getScheme() ?? '')?->isWhatWgSpecial() ?? false))) {
  1432. $normalizedUri = $normalizedUri->withPath('/');
  1433. }
  1434. if ($normalizedUri->toString() === $uriString) {
  1435. return $this;
  1436. }
  1437. return $normalizedUri;
  1438. }
  1439. /**
  1440. * Resolves a URI against a base URI using RFC3986 rules.
  1441. *
  1442. * This method MUST retain the state of the submitted URI instance, and return
  1443. * a URI instance of the same type that contains the applied modifications.
  1444. *
  1445. * This method MUST be transparent when dealing with errors and exceptions.
  1446. * It MUST not alter or silence them apart from validating its own parameters.
  1447. */
  1448. public function resolve(Rfc3986Uri|WhatWgUrl|UriInterface|Stringable|Urn|BackedEnum|string $uri): static
  1449. {
  1450. return self::new(UriString::resolve(
  1451. match (true) {
  1452. $uri instanceof UriInterface,
  1453. $uri instanceof Rfc3986Uri => $uri->toString(),
  1454. $uri instanceof WhatWgUrl => $uri->toAsciiString(),
  1455. $uri instanceof BackedEnum => (string) $uri->value,
  1456. default => $uri,
  1457. },
  1458. $this->toString()
  1459. ));
  1460. }
  1461. /**
  1462. * Relativize a URI according to a base URI.
  1463. *
  1464. * This method MUST retain the state of the submitted URI instance, and return
  1465. * a URI instance of the same type that contains the applied modifications.
  1466. *
  1467. * This method MUST be transparent when dealing with error and exceptions.
  1468. * It MUST not alter of silence them apart from validating its own parameters.
  1469. */
  1470. public function relativize(Rfc3986Uri|WhatWgUrl|UriInterface|Stringable|Urn|BackedEnum|string $uri): static
  1471. {
  1472. $uri = self::new($uri);
  1473. if (
  1474. $this->scheme !== $uri->getScheme() ||
  1475. $this->authority !== $uri->getAuthority() ||
  1476. $uri->isRelativePath()) {
  1477. return $uri;
  1478. }
  1479. $targetPath = $uri->getPath();
  1480. $basePath = $this->path;
  1481. $uri = $uri
  1482. ->withScheme(null)
  1483. ->withUserInfo(null)
  1484. ->withPort(null)
  1485. ->withHost(null);
  1486. return match (true) {
  1487. $targetPath !== $basePath => $uri->withPath(self::relativizePath($targetPath, $basePath)),
  1488. $this->query === $uri->getQuery() => $uri->withPath('')->withQuery(null),
  1489. null === $uri->getQuery() => $uri->withPath(self::formatPathWithEmptyBaseQuery($targetPath)),
  1490. default => $uri->withPath(''),
  1491. };
  1492. }
  1493. /**
  1494. * Formatting the path to keep a resolvable URI.
  1495. */
  1496. private static function formatPathWithEmptyBaseQuery(string $path): string
  1497. {
  1498. $targetSegments = self::getSegments($path);
  1499. $basename = $targetSegments[array_key_last($targetSegments)];
  1500. return '' === $basename ? './' : $basename;
  1501. }
  1502. /**
  1503. * Relatives the URI for an authority-less target URI.
  1504. */
  1505. private static function relativizePath(string $path, string $basePath): string
  1506. {
  1507. $baseSegments = self::getSegments($basePath);
  1508. $targetSegments = self::getSegments($path);
  1509. $targetBasename = array_pop($targetSegments);
  1510. array_pop($baseSegments);
  1511. foreach ($baseSegments as $offset => $segment) {
  1512. if (!isset($targetSegments[$offset]) || $segment !== $targetSegments[$offset]) {
  1513. break;
  1514. }
  1515. unset($baseSegments[$offset], $targetSegments[$offset]);
  1516. }
  1517. $targetSegments[] = $targetBasename;
  1518. return static::formatRelativePath(
  1519. str_repeat('../', count($baseSegments)).implode('/', $targetSegments),
  1520. $basePath
  1521. );
  1522. }
  1523. /**
  1524. * Formatting the path to keep a valid URI.
  1525. */
  1526. private static function formatRelativePath(string $path, string $basePath): string
  1527. {
  1528. $colonPosition = strpos($path, ':');
  1529. $slashPosition = strpos($path, '/');
  1530. return match (true) {
  1531. '' === $path => match (true) {
  1532. '' === $basePath,
  1533. '/' === $basePath => $basePath,
  1534. default => './',
  1535. },
  1536. false === $colonPosition => $path,
  1537. false === $slashPosition,
  1538. $colonPosition < $slashPosition => "./$path",
  1539. default => $path,
  1540. };
  1541. }
  1542. /**
  1543. * returns the path segments.
  1544. *
  1545. * @return array<string>
  1546. */
  1547. private static function getSegments(string $path): array
  1548. {
  1549. return explode('/', match (true) {
  1550. '' === $path,
  1551. '/' !== $path[0] => $path,
  1552. default => substr($path, 1),
  1553. });
  1554. }
  1555. /**
  1556. * @return ComponentMap
  1557. */
  1558. public function __debugInfo(): array
  1559. {
  1560. return $this->toComponents();
  1561. }
  1562. /**
  1563. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1564. *
  1565. * @deprecated Since version 7.6.0
  1566. * @codeCoverageIgnore
  1567. * @see Uri::parse()
  1568. *
  1569. * Creates a new instance from a URI and a Base URI.
  1570. *
  1571. * The returned URI must be absolute.
  1572. */
  1573. #[Deprecated(message:'use League\Uri\Uri::parse() instead', since:'league/uri:7.6.0')]
  1574. public static function fromBaseUri(WhatWgUrl|Rfc3986Uri|Stringable|string $uri, WhatWgUrl|Rfc3986Uri|Stringable|string|null $baseUri = null): self
  1575. {
  1576. $formatter = fn (WhatWgUrl|Rfc3986Uri|Stringable|string $uri): string => match (true) {
  1577. $uri instanceof Rfc3986Uri => $uri->toRawString(),
  1578. $uri instanceof WhatWgUrl => $uri->toAsciiString(),
  1579. default => str_replace(' ', '%20', (string) $uri),
  1580. };
  1581. return self::new(
  1582. UriString::resolve(
  1583. uri: $formatter($uri),
  1584. baseUri: null !== $baseUri ? $formatter($baseUri) : $baseUri
  1585. )
  1586. );
  1587. }
  1588. /**
  1589. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1590. *
  1591. * @deprecated Since version 7.5.0
  1592. * @codeCoverageIgnore
  1593. * @see Uri::toComponents()
  1594. *
  1595. * @return ComponentMap
  1596. */
  1597. #[Deprecated(message:'use League\Uri\Uri::toComponents() instead', since:'league/uri:7.5.0')]
  1598. public function getComponents(): array
  1599. {
  1600. return $this->toComponents();
  1601. }
  1602. /**
  1603. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1604. *
  1605. * @deprecated Since version 7.0.0
  1606. * @codeCoverageIgnore
  1607. * @see Uri::new()
  1608. */
  1609. #[Deprecated(message:'use League\Uri\Uri::new() instead', since:'league/uri:7.0.0')]
  1610. public static function createFromString(Stringable|string $uri = ''): self
  1611. {
  1612. return self::new($uri);
  1613. }
  1614. /**
  1615. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1616. *
  1617. * @deprecated Since version 7.0.0
  1618. * @codeCoverageIgnore
  1619. * @see Uri::fromComponents()
  1620. *
  1621. * @param InputComponentMap $components a hash representation of the URI similar to PHP parse_url function result
  1622. */
  1623. #[Deprecated(message:'use League\Uri\Uri::fromComponents() instead', since:'league/uri:7.0.0')]
  1624. public static function createFromComponents(array $components = []): self
  1625. {
  1626. return self::fromComponents($components);
  1627. }
  1628. /**
  1629. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1630. *
  1631. * @param resource|null $context
  1632. *
  1633. * @throws MissingFeature If ext/fileinfo is not installed
  1634. * @throws SyntaxError If the file does not exist or is not readable
  1635. * @see Uri::fromFileContents()
  1636. *
  1637. * @deprecated Since version 7.0.0
  1638. * @codeCoverageIgnore
  1639. */
  1640. #[Deprecated(message:'use League\Uri\Uri::fromDataPath() instead', since:'league/uri:7.0.0')]
  1641. public static function createFromDataPath(string $path, $context = null): self
  1642. {
  1643. return self::fromFileContents($path, $context);
  1644. }
  1645. /**
  1646. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1647. *
  1648. * @deprecated Since version 7.0.0
  1649. * @codeCoverageIgnore
  1650. * @see Uri::fromBaseUri()
  1651. *
  1652. * Creates a new instance from a URI and a Base URI.
  1653. *
  1654. * The returned URI must be absolute.
  1655. */
  1656. #[Deprecated(message:'use League\Uri\Uri::fromBaseUri() instead', since:'league/uri:7.0.0')]
  1657. public static function createFromBaseUri(
  1658. Stringable|UriInterface|String $uri,
  1659. Stringable|UriInterface|String|null $baseUri = null
  1660. ): static {
  1661. return self::fromBaseUri($uri, $baseUri);
  1662. }
  1663. /**
  1664. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1665. *
  1666. * @deprecated Since version 7.0.0
  1667. * @codeCoverageIgnore
  1668. * @see Uri::fromUnixPath()
  1669. *
  1670. * Create a new instance from a Unix path string.
  1671. */
  1672. #[Deprecated(message:'use League\Uri\Uri::fromUnixPath() instead', since:'league/uri:7.0.0')]
  1673. public static function createFromUnixPath(string $uri = ''): self
  1674. {
  1675. return self::fromUnixPath($uri);
  1676. }
  1677. /**
  1678. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1679. *
  1680. * @deprecated Since version 7.0.0
  1681. * @codeCoverageIgnore
  1682. * @see Uri::fromWindowsPath()
  1683. *
  1684. * Create a new instance from a local Windows path string.
  1685. */
  1686. #[Deprecated(message:'use League\Uri\Uri::fromWindowsPath() instead', since:'league/uri:7.0.0')]
  1687. public static function createFromWindowsPath(string $uri = ''): self
  1688. {
  1689. return self::fromWindowsPath($uri);
  1690. }
  1691. /**
  1692. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1693. *
  1694. * @deprecated Since version 7.0.0
  1695. * @codeCoverageIgnore
  1696. * @see Uri::new()
  1697. *
  1698. * Create a new instance from a URI object.
  1699. */
  1700. #[Deprecated(message:'use League\Uri\Uri::new() instead', since:'league/uri:7.0.0')]
  1701. public static function createFromUri(Psr7UriInterface|UriInterface $uri): self
  1702. {
  1703. return self::new($uri);
  1704. }
  1705. /**
  1706. * DEPRECATION WARNING! This method will be removed in the next major point release.
  1707. *
  1708. * @deprecated Since version 7.0.0
  1709. * @codeCoverageIgnore
  1710. * @see Uri::fromServer()
  1711. *
  1712. * Create a new instance from the environment.
  1713. */
  1714. #[Deprecated(message:'use League\Uri\Uri::fromServer() instead', since:'league/uri:7.0.0')]
  1715. public static function createFromServer(array $server): self
  1716. {
  1717. return self::fromServer($server);
  1718. }
  1719. }