Urn.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  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 JsonSerializable;
  15. use League\Uri\Contracts\Conditionable;
  16. use League\Uri\Contracts\Transformable;
  17. use League\Uri\Contracts\UriComponentInterface;
  18. use League\Uri\Contracts\UriInterface;
  19. use League\Uri\Exceptions\SyntaxError;
  20. use League\Uri\UriTemplate\Template;
  21. use Stringable;
  22. use Uri\Rfc3986\Uri as Rfc3986Uri;
  23. use Uri\WhatWg\Url as WhatWgUrl;
  24. use function is_bool;
  25. use function preg_match;
  26. use function str_replace;
  27. use function strtolower;
  28. /**
  29. * @phpstan-type UrnSerialize array{0: array{urn: non-empty-string}, 1: array{}}
  30. * @phpstan-import-type InputComponentMap from UriString
  31. * @phpstan-type UrnMap array{
  32. * scheme: 'urn',
  33. * nid: string,
  34. * nss: string,
  35. * r_component: ?string,
  36. * q_component: ?string,
  37. * f_component: ?string,
  38. * }
  39. */
  40. final class Urn implements Conditionable, Stringable, JsonSerializable, Transformable
  41. {
  42. /**
  43. * RFC8141 regular expression URN splitter.
  44. *
  45. * The regexp does not perform any look-ahead.
  46. * Not all invalid URN are caught. Some
  47. * post-regexp-validation checks
  48. * are mandatory.
  49. *
  50. * @link https://datatracker.ietf.org/doc/html/rfc8141#section-2
  51. *
  52. * @var string
  53. */
  54. private const REGEXP_URN_PARTS = '/^
  55. urn:
  56. (?<nid>[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?): # NID
  57. (?<nss>.*?) # NSS
  58. (?<frc>\?\+(?<rcomponent>.*?))? # r-component
  59. (?<fqc>\?\=(?<qcomponent>.*?))? # q-component
  60. (?:\#(?<fcomponent>.*))? # f-component
  61. $/xi';
  62. /**
  63. * RFC8141 namespace identifier regular expression.
  64. *
  65. * @link https://datatracker.ietf.org/doc/html/rfc8141#section-2
  66. *
  67. * @var string
  68. */
  69. private const REGEX_NID_SEQUENCE = '/^[a-z0-9]([a-z0-9-]{0,30})[a-z0-9]$/xi';
  70. /** @var non-empty-string */
  71. private readonly string $uriString;
  72. /** @var non-empty-string */
  73. private readonly string $nid;
  74. /** @var non-empty-string */
  75. private readonly string $nss;
  76. /** @var non-empty-string|null */
  77. private readonly ?string $rComponent;
  78. /** @var non-empty-string|null */
  79. private readonly ?string $qComponent;
  80. /** @var non-empty-string|null */
  81. private readonly ?string $fComponent;
  82. /**
  83. * @param Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $urn the percent-encoded URN
  84. */
  85. public static function parse(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $urn): ?Urn
  86. {
  87. try {
  88. return self::fromString($urn);
  89. } catch (SyntaxError) {
  90. return null;
  91. }
  92. }
  93. /**
  94. * @param Rfc3986Uri|WhatWgUrl|Stringable|string $urn the percent-encoded URN
  95. * @see self::fromString()
  96. *
  97. * @throws SyntaxError if the URN is invalid
  98. */
  99. public static function new(Rfc3986Uri|WhatWgUrl|Stringable|string $urn): self
  100. {
  101. return self::fromString($urn);
  102. }
  103. /**
  104. * @param Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $urn the percent-encoded URN
  105. *
  106. * @throws SyntaxError if the URN is invalid
  107. */
  108. public static function fromString(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $urn): self
  109. {
  110. $urn = match (true) {
  111. $urn instanceof Rfc3986Uri => $urn->toRawString(),
  112. $urn instanceof WhatWgUrl => $urn->toAsciiString(),
  113. $urn instanceof BackedEnum => (string) $urn->value,
  114. default => (string) $urn,
  115. };
  116. UriString::containsRfc3986Chars($urn) || throw new SyntaxError('The URN is malformed, it contains invalid characters.');
  117. 1 === preg_match(self::REGEXP_URN_PARTS, $urn, $matches) || throw new SyntaxError('The URN string is invalid.');
  118. return new self(
  119. nid: $matches['nid'],
  120. nss: $matches['nss'],
  121. rComponent: (isset($matches['frc']) && '' !== $matches['frc']) ? $matches['rcomponent'] : null,
  122. qComponent: (isset($matches['fqc']) && '' !== $matches['fqc']) ? $matches['qcomponent'] : null,
  123. fComponent: $matches['fcomponent'] ?? null,
  124. );
  125. }
  126. /**
  127. * Create a new instance from a hash representation of the URI similar
  128. * to PHP parse_url function result.
  129. *
  130. * @param InputComponentMap $components a hash representation of the URI similar to PHP parse_url function result
  131. */
  132. public static function fromComponents(array $components = []): self
  133. {
  134. $components += [
  135. 'scheme' => null, 'user' => null, 'pass' => null, 'host' => null,
  136. 'port' => null, 'path' => '', 'query' => null, 'fragment' => null,
  137. ];
  138. return self::fromString(UriString::build($components));
  139. }
  140. /**
  141. * @param Stringable|string $nss the percent-encoded NSS
  142. *
  143. * @throws SyntaxError if the URN is invalid
  144. */
  145. public static function fromRfc2141(BackedEnum|Stringable|string $nid, BackedEnum|Stringable|string $nss): self
  146. {
  147. if ($nid instanceof BackedEnum) {
  148. $nid = $nid->value;
  149. }
  150. if ($nss instanceof BackedEnum) {
  151. $nss = $nss->value;
  152. }
  153. return new self((string) $nid, (string) $nss);
  154. }
  155. /**
  156. * @param string $nss the percent-encoded NSS
  157. * @param ?string $rComponent the percent-encoded r-component
  158. * @param ?string $qComponent the percent-encoded q-component
  159. * @param ?string $fComponent the percent-encoded f-component
  160. *
  161. * @throws SyntaxError if one of the URN part is invalid
  162. */
  163. private function __construct(
  164. string $nid,
  165. string $nss,
  166. ?string $rComponent = null,
  167. ?string $qComponent = null,
  168. ?string $fComponent = null,
  169. ) {
  170. ('' !== $nid && 1 === preg_match(self::REGEX_NID_SEQUENCE, $nid)) || throw new SyntaxError('The URN is malformed, the NID is invalid.');
  171. ('' !== $nss && Encoder::isPathEncoded($nss)) || throw new SyntaxError('The URN is malformed, the NSS is invalid.');
  172. /** @param Closure(string): ?non-empty-string $closure */
  173. $validateComponent = static fn (?string $value, Closure $closure, string $name): ?string => match (true) {
  174. null === $value,
  175. ('' !== $value && 1 !== preg_match('/[#?]/', $value) && $closure($value)) => $value,
  176. default => throw new SyntaxError('The URN is malformed, the `'.$name.'` component is invalid.'),
  177. };
  178. $this->nid = $nid;
  179. $this->nss = $nss;
  180. $this->rComponent = $validateComponent($rComponent, Encoder::isPathEncoded(...), 'r-component');
  181. $this->qComponent = $validateComponent($qComponent, Encoder::isQueryEncoded(...), 'q-component');
  182. $this->fComponent = $validateComponent($fComponent, Encoder::isFragmentEncoded(...), 'f-component');
  183. $this->uriString = $this->setUriString();
  184. }
  185. /**
  186. * @return non-empty-string
  187. */
  188. private function setUriString(): string
  189. {
  190. $str = $this->toRfc2141();
  191. if (null !== $this->rComponent) {
  192. $str .= '?+'.$this->rComponent;
  193. }
  194. if (null !== $this->qComponent) {
  195. $str .= '?='.$this->qComponent;
  196. }
  197. if (null !== $this->fComponent) {
  198. $str .= '#'.$this->fComponent;
  199. }
  200. return $str;
  201. }
  202. /**
  203. * Returns the NID.
  204. *
  205. * @return non-empty-string
  206. */
  207. public function getNid(): string
  208. {
  209. return $this->nid;
  210. }
  211. /**
  212. * Returns the percent-encoded NSS.
  213. *
  214. * @return non-empty-string
  215. */
  216. public function getNss(): string
  217. {
  218. return $this->nss;
  219. }
  220. /**
  221. * Returns the percent-encoded r-component string or null if it is not set.
  222. *
  223. * @return ?non-empty-string
  224. */
  225. public function getRComponent(): ?string
  226. {
  227. return $this->rComponent;
  228. }
  229. /**
  230. * Returns the percent-encoded q-component string or null if it is not set.
  231. *
  232. * @return ?non-empty-string
  233. */
  234. public function getQComponent(): ?string
  235. {
  236. return $this->qComponent;
  237. }
  238. /**
  239. * Returns the percent-encoded f-component string or null if it is not set.
  240. *
  241. * @return ?non-empty-string
  242. */
  243. public function getFComponent(): ?string
  244. {
  245. return $this->fComponent;
  246. }
  247. /**
  248. * Returns the RFC8141 URN string representation.
  249. *
  250. * @return non-empty-string
  251. */
  252. public function toString(): string
  253. {
  254. return $this->uriString;
  255. }
  256. /**
  257. * Returns the RFC2141 URN string representation.
  258. *
  259. * @return non-empty-string
  260. */
  261. public function toRfc2141(): string
  262. {
  263. return 'urn:'.$this->nid.':'.$this->nss;
  264. }
  265. /**
  266. * Returns the human-readable string representation of the URN as an IRI.
  267. *
  268. * @see https://datatracker.ietf.org/doc/html/rfc3987
  269. */
  270. public function toDisplayString(): string
  271. {
  272. return UriString::toIriString($this->uriString);
  273. }
  274. /**
  275. * Returns the RFC8141 URN string representation.
  276. *
  277. * @see self::toString()
  278. *
  279. * @return non-empty-string
  280. */
  281. public function __toString(): string
  282. {
  283. return $this->toString();
  284. }
  285. /**
  286. * Returns the RFC8141 URN string representation.
  287. * @see self::toString()
  288. *
  289. * @return non-empty-string
  290. */
  291. public function jsonSerialize(): string
  292. {
  293. return $this->toString();
  294. }
  295. /**
  296. * Returns the RFC3986 representation of the current URN.
  297. *
  298. * If a template URI is used the following variables as present
  299. * {nid} for the namespace identifier
  300. * {nss} for the namespace specific string
  301. * {r_component} for the r-component without its delimiter
  302. * {q_component} for the q-component without its delimiter
  303. * {f_component} for the f-component without its delimiter
  304. */
  305. public function resolve(UriTemplate|Template|BackedEnum|string|null $template = null): UriInterface
  306. {
  307. return null !== $template ? Uri::fromTemplate($template, $this->toComponents()) : Uri::new($this->uriString);
  308. }
  309. public function hasRComponent(): bool
  310. {
  311. return null !== $this->rComponent;
  312. }
  313. public function hasQComponent(): bool
  314. {
  315. return null !== $this->qComponent;
  316. }
  317. public function hasFComponent(): bool
  318. {
  319. return null !== $this->fComponent;
  320. }
  321. public function hasOptionalComponent(): bool
  322. {
  323. return null !== $this->rComponent
  324. || null !== $this->qComponent
  325. || null !== $this->fComponent;
  326. }
  327. /**
  328. * Return an instance with the specified NID.
  329. *
  330. * This method MUST retain the state of the current instance, and return
  331. * an instance that contains the specified NID.
  332. *
  333. * @throws SyntaxError for invalid component or transformations
  334. * that would result in an object in invalid state.
  335. */
  336. public function withNid(BackedEnum|Stringable|string $nid): self
  337. {
  338. if ($nid instanceof BackedEnum) {
  339. $nid = $nid->value;
  340. }
  341. $nid = (string) $nid;
  342. return $this->nid === $nid ? $this : new self(
  343. nid: $nid,
  344. nss: $this->nss,
  345. rComponent: $this->rComponent,
  346. qComponent: $this->qComponent,
  347. fComponent: $this->fComponent,
  348. );
  349. }
  350. /**
  351. * Return an instance with the specified NSS.
  352. *
  353. * This method MUST retain the state of the current instance, and return
  354. * an instance that contains the specified NSS.
  355. *
  356. * @throws SyntaxError for invalid component or transformations
  357. * that would result in an object in invalid state.
  358. */
  359. public function withNss(BackedEnum|Stringable|string $nss): self
  360. {
  361. $nss = Encoder::encodePath($nss);
  362. return $this->nss === $nss ? $this : new self(
  363. nid: $this->nid,
  364. nss: $nss,
  365. rComponent: $this->rComponent,
  366. qComponent: $this->qComponent,
  367. fComponent: $this->fComponent,
  368. );
  369. }
  370. /**
  371. * Return an instance with the specified r-component.
  372. *
  373. * This method MUST retain the state of the current instance, and return
  374. * an instance that contains the specified r-component.
  375. *
  376. * The component is removed if the value is null.
  377. *
  378. * @throws SyntaxError for invalid component or transformations
  379. * that would result in an object in invalid state.
  380. */
  381. public function withRComponent(BackedEnum|Stringable|string|null $component): self
  382. {
  383. if ($component instanceof BackedEnum) {
  384. $component = (string) $component->value;
  385. }
  386. if ($component instanceof UriComponentInterface) {
  387. $component = $component->value();
  388. }
  389. if (null !== $component) {
  390. $component = self::formatComponent(Encoder::encodePath($component));
  391. }
  392. return $this->rComponent === $component ? $this : new self(
  393. nid: $this->nid,
  394. nss: $this->nss,
  395. rComponent: $component,
  396. qComponent: $this->qComponent,
  397. fComponent: $this->fComponent,
  398. );
  399. }
  400. private static function formatComponent(?string $component): ?string
  401. {
  402. return null === $component ? null : str_replace(['?', '#'], ['%3F', '%23'], $component);
  403. }
  404. /**
  405. * Return an instance with the specified q-component.
  406. *
  407. * This method MUST retain the state of the current instance, and return
  408. * an instance that contains the specified q-component.
  409. *
  410. * The component is removed if the value is null.
  411. *
  412. * @throws SyntaxError for invalid component or transformations
  413. * that would result in an object in invalid state.
  414. */
  415. public function withQComponent(BackedEnum|Stringable|string|null $component): self
  416. {
  417. if ($component instanceof UriComponentInterface) {
  418. $component = $component->value();
  419. }
  420. $component = self::formatComponent(Encoder::encodeQueryOrFragment($component));
  421. return $this->qComponent === $component ? $this : new self(
  422. nid: $this->nid,
  423. nss: $this->nss,
  424. rComponent: $this->rComponent,
  425. qComponent: $component,
  426. fComponent: $this->fComponent,
  427. );
  428. }
  429. /**
  430. * Return an instance with the specified f-component.
  431. *
  432. * This method MUST retain the state of the current instance, and return
  433. * an instance that contains the specified f-component.
  434. *
  435. * The component is removed if the value is null.
  436. *
  437. * @throws SyntaxError for invalid component or transformations
  438. * that would result in an object in invalid state.
  439. */
  440. public function withFComponent(BackedEnum|Stringable|string|null $component): self
  441. {
  442. if ($component instanceof UriComponentInterface) {
  443. $component = $component->value();
  444. }
  445. $component = self::formatComponent(Encoder::encodeQueryOrFragment($component));
  446. return $this->fComponent === $component ? $this : new self(
  447. nid: $this->nid,
  448. nss: $this->nss,
  449. rComponent: $this->rComponent,
  450. qComponent: $this->qComponent,
  451. fComponent: $component,
  452. );
  453. }
  454. public function normalize(): self
  455. {
  456. $copy = new self(
  457. nid: strtolower($this->nid),
  458. nss: (string) Encoder::normalizePath($this->nss),
  459. rComponent: null === $this->rComponent ? $this->rComponent : Encoder::normalizePath($this->rComponent),
  460. qComponent: Encoder::normalizeQuery($this->qComponent),
  461. fComponent: Encoder::normalizeFragment($this->fComponent),
  462. );
  463. return $copy->uriString === $this->uriString ? $this : $copy;
  464. }
  465. public function equals(Urn|Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string $other, UrnComparisonMode $urnComparisonMode = UrnComparisonMode::ExcludeComponents): bool
  466. {
  467. if (!$other instanceof Urn) {
  468. $other = self::parse($other);
  469. }
  470. return (null !== $other) && match ($urnComparisonMode) {
  471. UrnComparisonMode::ExcludeComponents => $other->normalize()->toRfc2141() === $this->normalize()->toRfc2141(),
  472. UrnComparisonMode::IncludeComponents => $other->normalize()->toString() === $this->normalize()->toString(),
  473. };
  474. }
  475. public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
  476. {
  477. if (!is_bool($condition)) {
  478. $condition = $condition($this);
  479. }
  480. return match (true) {
  481. $condition => $onSuccess($this),
  482. null !== $onFail => $onFail($this),
  483. default => $this,
  484. } ?? $this;
  485. }
  486. public function transform(callable $callback): static
  487. {
  488. return $callback($this);
  489. }
  490. /**
  491. * @return UrnSerialize
  492. */
  493. public function __serialize(): array
  494. {
  495. return [['urn' => $this->toString()], []];
  496. }
  497. /**
  498. * @param UrnSerialize $data
  499. *
  500. * @throws SyntaxError
  501. */
  502. public function __unserialize(array $data): void
  503. {
  504. [$properties] = $data;
  505. $uri = self::fromString($properties['urn'] ?? throw new SyntaxError('The `urn` property is missing from the serialized object.'));
  506. $this->nid = $uri->nid;
  507. $this->nss = $uri->nss;
  508. $this->rComponent = $uri->rComponent;
  509. $this->qComponent = $uri->qComponent;
  510. $this->fComponent = $uri->fComponent;
  511. $this->uriString = $uri->uriString;
  512. }
  513. /**
  514. * @return UrnMap
  515. */
  516. public function toComponents(): array
  517. {
  518. return [
  519. 'scheme' => 'urn',
  520. 'nid' => $this->nid,
  521. 'nss' => $this->nss,
  522. 'r_component' => $this->rComponent,
  523. 'q_component' => $this->qComponent,
  524. 'f_component' => $this->fComponent,
  525. ];
  526. }
  527. /**
  528. * @return UrnMap
  529. */
  530. public function __debugInfo(): array
  531. {
  532. return $this->toComponents();
  533. }
  534. }