Builder.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  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 League\Uri\Contracts\Conditionable;
  14. use League\Uri\Contracts\FragmentDirective;
  15. use League\Uri\Contracts\Transformable;
  16. use League\Uri\Contracts\UriComponentInterface;
  17. use League\Uri\Exceptions\SyntaxError;
  18. use SensitiveParameter;
  19. use Stringable;
  20. use Throwable;
  21. use TypeError;
  22. use Uri\Rfc3986\Uri as Rfc3986Uri;
  23. use Uri\WhatWg\Url as WhatWgUrl;
  24. use function is_bool;
  25. use function str_replace;
  26. use function strpos;
  27. final class Builder implements Conditionable, Transformable
  28. {
  29. private ?string $scheme = null;
  30. private ?string $username = null;
  31. private ?string $password = null;
  32. private ?string $host = null;
  33. private ?int $port = null;
  34. private ?string $path = null;
  35. private ?string $query = null;
  36. private ?string $fragment = null;
  37. public function __construct(
  38. BackedEnum|Stringable|string|null $scheme = null,
  39. BackedEnum|Stringable|string|null $username = null,
  40. #[SensitiveParameter] BackedEnum|Stringable|string|null $password = null,
  41. BackedEnum|Stringable|string|null $host = null,
  42. BackedEnum|int|null $port = null,
  43. BackedEnum|Stringable|string|null $path = null,
  44. BackedEnum|Stringable|string|null $query = null,
  45. BackedEnum|Stringable|string|null $fragment = null,
  46. ) {
  47. $this
  48. ->scheme($scheme)
  49. ->userInfo($username, $password)
  50. ->host($host)
  51. ->port($port)
  52. ->path($path)
  53. ->query($query)
  54. ->fragment($fragment);
  55. }
  56. /**
  57. * @throws SyntaxError
  58. */
  59. public function scheme(BackedEnum|Stringable|string|null $scheme): self
  60. {
  61. $scheme = $this->filterString($scheme);
  62. if ($scheme !== $this->scheme) {
  63. UriString::isValidScheme($scheme) || throw new SyntaxError('The scheme `'.$scheme.'` is invalid.');
  64. $this->scheme = $scheme;
  65. }
  66. return $this;
  67. }
  68. /**
  69. * @throws SyntaxError
  70. */
  71. public function userInfo(
  72. BackedEnum|Stringable|string|null $user,
  73. #[SensitiveParameter] BackedEnum|Stringable|string|null $password = null
  74. ): static {
  75. $username = Encoder::encodeUser($this->filterString($user));
  76. $password = Encoder::encodePassword($this->filterString($password));
  77. if ($username !== $this->username || $password !== $this->password) {
  78. $this->username = $username;
  79. $this->password = $password;
  80. }
  81. return $this;
  82. }
  83. /**
  84. * @throws SyntaxError
  85. */
  86. public function host(BackedEnum|Stringable|string|null $host): self
  87. {
  88. $host = $this->filterString($host);
  89. if ($host !== $this->host) {
  90. null === $host
  91. || HostRecord::isValid($host)
  92. || throw new SyntaxError('The host `'.$host.'` is invalid.');
  93. $this->host = $host;
  94. }
  95. return $this;
  96. }
  97. /**
  98. * @throws SyntaxError
  99. * @throws TypeError
  100. */
  101. public function port(BackedEnum|int|null $port): self
  102. {
  103. if ($port instanceof BackedEnum) {
  104. 1 === preg_match('/^\d+$/', (string) $port->value)
  105. || throw new TypeError('The port must be a valid BackedEnum containing a number.');
  106. $port = (int) $port->value;
  107. }
  108. if ($port !== $this->port) {
  109. null === $port
  110. || ($port >= 0 && $port < 65535)
  111. || throw new SyntaxError('The port value must be null or an integer between 0 and 65535.');
  112. $this->port = $port;
  113. }
  114. return $this;
  115. }
  116. /**
  117. * @throws SyntaxError
  118. */
  119. public function authority(BackedEnum|Stringable|string|null $authority): self
  120. {
  121. ['user' => $user, 'pass' => $pass, 'host' => $host, 'port' => $port] = UriString::parseAuthority($authority);
  122. return $this
  123. ->userInfo($user, $pass)
  124. ->host($host)
  125. ->port($port);
  126. }
  127. /**
  128. * @throws SyntaxError
  129. */
  130. public function path(BackedEnum|Stringable|string|null $path): self
  131. {
  132. $path = $this->filterString($path);
  133. if ($path !== $this->path) {
  134. $this->path = null !== $path ? Encoder::encodePath($path) : null;
  135. }
  136. return $this;
  137. }
  138. /**
  139. * @throws SyntaxError
  140. */
  141. public function query(BackedEnum|Stringable|string|null $query): self
  142. {
  143. $query = $this->filterString($query);
  144. if ($query !== $this->query) {
  145. $this->query = Encoder::encodeQueryOrFragment($query);
  146. }
  147. return $this;
  148. }
  149. /**
  150. * @throws SyntaxError
  151. */
  152. public function fragment(BackedEnum|Stringable|string|null $fragment): self
  153. {
  154. $fragment = $this->filterString($fragment);
  155. if ($fragment !== $this->fragment) {
  156. $this->fragment = Encoder::encodeQueryOrFragment($fragment);
  157. }
  158. return $this;
  159. }
  160. /**
  161. * Puts back the Builder in a freshly created state.
  162. */
  163. public function reset(): self
  164. {
  165. $this->scheme = null;
  166. $this->username = null;
  167. $this->password = null;
  168. $this->host = null;
  169. $this->port = null;
  170. $this->path = null;
  171. $this->query = null;
  172. $this->fragment = null;
  173. return $this;
  174. }
  175. /**
  176. * Executes the given callback with the current instance
  177. * and returns the current instance.
  178. *
  179. * @param callable(self): self $callback
  180. */
  181. public function transform(callable $callback): static
  182. {
  183. return $callback($this);
  184. }
  185. public function when(callable|bool $condition, callable $onSuccess, ?callable $onFail = null): static
  186. {
  187. if (!is_bool($condition)) {
  188. $condition = $condition($this);
  189. }
  190. return match (true) {
  191. $condition => $onSuccess($this),
  192. null !== $onFail => $onFail($this),
  193. default => $this,
  194. } ?? $this;
  195. }
  196. /**
  197. * @throws SyntaxError if the URI can not be build with the current Builder state
  198. */
  199. public function guard(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): self
  200. {
  201. try {
  202. $this->build($baseUri);
  203. return $this;
  204. } catch (Throwable $exception) {
  205. throw new SyntaxError('The current builder cannot generate a valid URI.', previous: $exception);
  206. }
  207. }
  208. /**
  209. * Tells whether the URI can be built with the current Builder state.
  210. */
  211. public function validate(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): bool
  212. {
  213. try {
  214. $this->build($baseUri);
  215. return true;
  216. } catch (Throwable) {
  217. return false;
  218. }
  219. }
  220. public function build(Rfc3986Uri|WhatWgUrl|BackedEnum|Stringable|string|null $baseUri = null): Uri
  221. {
  222. $authority = $this->buildAuthority();
  223. $path = $this->buildPath($authority);
  224. $uriString = UriString::buildUri(
  225. $this->scheme,
  226. $authority,
  227. $path,
  228. Encoder::encodeQueryOrFragment($this->query),
  229. Encoder::encodeQueryOrFragment($this->fragment)
  230. );
  231. return Uri::new(null === $baseUri ? $uriString : UriString::resolve($uriString, match (true) {
  232. $baseUri instanceof Rfc3986Uri => $baseUri->toString(),
  233. $baseUri instanceof WhatWgUrl => $baseUri->toAsciiString(),
  234. default => $baseUri,
  235. }));
  236. }
  237. /**
  238. * @throws SyntaxError
  239. */
  240. private function buildAuthority(): ?string
  241. {
  242. if (null === $this->host) {
  243. (null === $this->username && null === $this->password && null === $this->port)
  244. || throw new SyntaxError('The User Information and/or the Port component(s) are set without a Host component being present.');
  245. return null;
  246. }
  247. $authority = $this->host;
  248. if (null !== $this->username || null !== $this->password) {
  249. $userInfo = Encoder::encodeUser($this->username);
  250. if (null !== $this->password) {
  251. $userInfo .= ':'.Encoder::encodePassword($this->password);
  252. }
  253. $authority = $userInfo.'@'.$authority;
  254. }
  255. if (null !== $this->port) {
  256. return $authority.':'.$this->port;
  257. }
  258. return $authority;
  259. }
  260. /**
  261. * @throws SyntaxError
  262. */
  263. private function buildPath(?string $authority): ?string
  264. {
  265. if (null === $this->path || '' === $this->path) {
  266. return $this->path;
  267. }
  268. $path = Encoder::encodePath($this->path);
  269. if (null !== $authority) {
  270. return str_starts_with($path, '/') ? $path : '/'.$path;
  271. }
  272. if (str_starts_with($path, '//')) {
  273. return '/.'.$path;
  274. }
  275. $colonPos = strpos($path, ':');
  276. if (false !== $colonPos && null === $this->scheme) {
  277. $slashPos = strpos($path, '/');
  278. (false !== $slashPos && $colonPos > $slashPos) || throw new SyntaxError('In absence of the scheme and authority components, the first path segment cannot contain a colon (":") character.');
  279. }
  280. return $path;
  281. }
  282. /**
  283. * Filter a string.
  284. *
  285. * @throws SyntaxError if the submitted data cannot be converted to string
  286. */
  287. private function filterString(BackedEnum|Stringable|string|null $str): ?string
  288. {
  289. $str = match (true) {
  290. $str instanceof FragmentDirective => $str->toFragmentValue(),
  291. $str instanceof UriComponentInterface => $str->value(),
  292. $str instanceof BackedEnum => (string) $str->value,
  293. null === $str => null,
  294. default => (string) $str,
  295. };
  296. if (null === $str) {
  297. return null;
  298. }
  299. $str = str_replace(' ', '%20', $str);
  300. return UriString::containsRfc3987Chars($str)
  301. ? $str
  302. : throw new SyntaxError('The component value `'.$str.'` contains invalid characters.');
  303. }
  304. }