Uri.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. <?php
  2. /**
  3. * File for Net\Uri class.
  4. * @package Phrity > Net > Uri
  5. * @see https://www.rfc-editor.org/rfc/rfc3986
  6. * @see https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface
  7. */
  8. namespace Phrity\Net;
  9. use InvalidArgumentException;
  10. use Psr\Http\Message\UriInterface;
  11. /**
  12. * Net\Uri class.
  13. */
  14. class Uri implements UriInterface
  15. {
  16. public const REQUIRE_PORT = 1; // Always include port, explicit or default
  17. public const ABSOLUTE_PATH = 2; // Enforce absolute path
  18. public const NORMALIZE_PATH = 4; // Normalize path
  19. public const IDNA = 8; // IDNA-convert host
  20. private const RE_MAIN = '!^(?P<schemec>(?P<scheme>[^:/?#]+):)?(?P<authorityc>//(?P<authority>[^/?#]*))?'
  21. . '(?P<path>[^?#]*)(?P<queryc>\?(?P<query>[^#]*))?(?P<fragmentc>#(?P<fragment>.*))?$!';
  22. private const RE_AUTH = '!^(?P<userinfoc>(?P<user>[^:/?#]+)(?P<passc>:(?P<pass>[^:/?#]+))?@)?'
  23. . '(?P<host>[^:/?#]*|\[[^/?#]*\])(?P<portc>:(?P<port>[0-9]*))?$!';
  24. private static $port_defaults = [
  25. 'acap' => 674,
  26. 'afp' => 548,
  27. 'dict' => 2628,
  28. 'dns' => 53,
  29. 'ftp' => 21,
  30. 'git' => 9418,
  31. 'gopher' => 70,
  32. 'http' => 80,
  33. 'https' => 443,
  34. 'imap' => 143,
  35. 'ipp' => 631,
  36. 'ipps' => 631,
  37. 'irc' => 194,
  38. 'ircs' => 6697,
  39. 'ldap' => 389,
  40. 'ldaps' => 636,
  41. 'mms' => 1755,
  42. 'msrp' => 2855,
  43. 'mtqp' => 1038,
  44. 'nfs' => 111,
  45. 'nntp' => 119,
  46. 'nntps' => 563,
  47. 'pop' => 110,
  48. 'prospero' => 1525,
  49. 'redis' => 6379,
  50. 'rsync' => 873,
  51. 'rtsp' => 554,
  52. 'rtsps' => 322,
  53. 'rtspu' => 5005,
  54. 'sftp' => 22,
  55. 'smb' => 445,
  56. 'snmp' => 161,
  57. 'ssh' => 22,
  58. 'svn' => 3690,
  59. 'telnet' => 23,
  60. 'ventrilo' => 3784,
  61. 'vnc' => 5900,
  62. 'wais' => 210,
  63. 'ws' => 80,
  64. 'wss' => 443,
  65. ];
  66. private $scheme;
  67. private $authority;
  68. private $host;
  69. private $port;
  70. private $user;
  71. private $pass;
  72. private $path;
  73. private $query;
  74. private $fragment;
  75. /**
  76. * Create new URI instance using a string
  77. * @param string $uri_string URI as string
  78. * @throws \InvalidArgumentException If the given URI cannot be parsed
  79. */
  80. public function __construct(string $uri_string = '', int $flags = 0)
  81. {
  82. $this->parse($uri_string);
  83. }
  84. // ---------- PSR-7 getters ---------------------------------------------------------------------------------------
  85. /**
  86. * Retrieve the scheme component of the URI.
  87. * @return string The URI scheme
  88. */
  89. public function getScheme(int $flags = 0): string
  90. {
  91. return $this->getComponent('scheme') ?? '';
  92. }
  93. /**
  94. * Retrieve the authority component of the URI.
  95. * @return string The URI authority, in "[user-info@]host[:port]" format
  96. */
  97. public function getAuthority(int $flags = 0): string
  98. {
  99. $host = $this->formatComponent($this->getHost($flags));
  100. if ($this->isEmpty($host)) {
  101. return '';
  102. }
  103. $userinfo = $this->formatComponent($this->getUserInfo(), '', '@');
  104. $port = $this->formatComponent($this->getPort($flags), ':');
  105. return "{$userinfo}{$host}{$port}";
  106. }
  107. /**
  108. * Retrieve the user information component of the URI.
  109. * @return string The URI user information, in "username[:password]" format
  110. */
  111. public function getUserInfo(int $flags = 0): string
  112. {
  113. $user = $this->formatComponent($this->getComponent('user'));
  114. $pass = $this->formatComponent($this->getComponent('pass'), ':');
  115. return $this->isEmpty($user) ? '' : "{$user}{$pass}";
  116. }
  117. /**
  118. * Retrieve the host component of the URI.
  119. * @return string The URI host
  120. */
  121. public function getHost(int $flags = 0): string
  122. {
  123. $host = $this->getComponent('host') ?? '';
  124. if ($flags & self::IDNA) {
  125. $host = $this->idna($host);
  126. }
  127. return $host;
  128. }
  129. /**
  130. * Retrieve the port component of the URI.
  131. * @return null|int The URI port
  132. */
  133. public function getPort(int $flags = 0): ?int
  134. {
  135. $port = $this->getComponent('port');
  136. $scheme = $this->getComponent('scheme');
  137. $default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null;
  138. if ($flags & self::REQUIRE_PORT) {
  139. return !$this->isEmpty($port) ? $port : $default;
  140. }
  141. return $this->isEmpty($port) || $port === $default ? null : $port;
  142. }
  143. /**
  144. * Retrieve the path component of the URI.
  145. * @return string The URI path
  146. */
  147. public function getPath(int $flags = 0): string
  148. {
  149. $path = $this->getComponent('path') ?? '';
  150. if ($flags & self::NORMALIZE_PATH) {
  151. $path = $this->normalizePath($path);
  152. }
  153. if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
  154. $path = "/{$path}";
  155. }
  156. return $path;
  157. }
  158. /**
  159. * Retrieve the query string of the URI.
  160. * @return string The URI query string
  161. */
  162. public function getQuery(int $flags = 0): string
  163. {
  164. return $this->getComponent('query') ?? '';
  165. }
  166. /**
  167. * Retrieve the fragment component of the URI.
  168. * @return string The URI fragment
  169. */
  170. public function getFragment(int $flags = 0): string
  171. {
  172. return $this->getComponent('fragment') ?? '';
  173. }
  174. // ---------- PSR-7 setters ---------------------------------------------------------------------------------------
  175. /**
  176. * Return an instance with the specified scheme.
  177. * @param string $scheme The scheme to use with the new instance
  178. * @return static A new instance with the specified scheme
  179. * @throws \InvalidArgumentException for invalid schemes
  180. * @throws \InvalidArgumentException for unsupported schemes
  181. */
  182. public function withScheme($scheme, int $flags = 0): UriInterface
  183. {
  184. $clone = clone $this;
  185. if ($flags & self::REQUIRE_PORT) {
  186. $clone->setComponent('port', $this->getPort(self::REQUIRE_PORT));
  187. $default = isset(self::$port_defaults[$scheme]) ? self::$port_defaults[$scheme] : null;
  188. }
  189. $clone->setComponent('scheme', $scheme);
  190. return $clone;
  191. }
  192. /**
  193. * Return an instance with the specified user information.
  194. * @param string $user The user name to use for authority
  195. * @param null|string $password The password associated with $user
  196. * @return static A new instance with the specified user information
  197. */
  198. public function withUserInfo($user, $password = null, int $flags = 0): UriInterface
  199. {
  200. $clone = clone $this;
  201. $clone->setComponent('user', $user);
  202. $clone->setComponent('pass', $password);
  203. return $clone;
  204. }
  205. /**
  206. * Return an instance with the specified host.
  207. * @param string $host The hostname to use with the new instance
  208. * @return static A new instance with the specified host
  209. * @throws \InvalidArgumentException for invalid hostnames
  210. */
  211. public function withHost($host, int $flags = 0): UriInterface
  212. {
  213. $clone = clone $this;
  214. if ($flags & self::IDNA) {
  215. $host = $this->idna($host);
  216. }
  217. $clone->setComponent('host', $host);
  218. return $clone;
  219. }
  220. /**
  221. * Return an instance with the specified port.
  222. * @param null|int $port The port to use with the new instance
  223. * @return static A new instance with the specified port
  224. * @throws \InvalidArgumentException for invalid ports
  225. */
  226. public function withPort($port, int $flags = 0): UriInterface
  227. {
  228. $clone = clone $this;
  229. $clone->setComponent('port', $port);
  230. return $clone;
  231. }
  232. /**
  233. * Return an instance with the specified path.
  234. * @param string $path The path to use with the new instance
  235. * @return static A new instance with the specified path
  236. * @throws \InvalidArgumentException for invalid paths
  237. */
  238. public function withPath($path, int $flags = 0): UriInterface
  239. {
  240. $clone = clone $this;
  241. if ($flags & self::NORMALIZE_PATH) {
  242. $path = $this->normalizePath($path);
  243. }
  244. if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
  245. $path = "/{$path}";
  246. }
  247. $clone->setComponent('path', $path);
  248. return $clone;
  249. }
  250. /**
  251. * Return an instance with the specified query string.
  252. * @param string $query The query string to use with the new instance
  253. * @return static A new instance with the specified query string
  254. * @throws \InvalidArgumentException for invalid query strings
  255. */
  256. public function withQuery($query, int $flags = 0): UriInterface
  257. {
  258. $clone = clone $this;
  259. $clone->setComponent('query', $query);
  260. return $clone;
  261. }
  262. /**
  263. * Return an instance with the specified URI fragment.
  264. * @param string $fragment The fragment to use with the new instance
  265. * @return static A new instance with the specified fragment
  266. */
  267. public function withFragment($fragment, int $flags = 0): UriInterface
  268. {
  269. $clone = clone $this;
  270. $clone->setComponent('fragment', $fragment);
  271. return $clone;
  272. }
  273. // ---------- PSR-7 string ----------------------------------------------------------------------------------------
  274. /**
  275. * Return the string representation as a URI reference.
  276. * @return string
  277. */
  278. public function __toString(): string
  279. {
  280. return $this->toString();
  281. }
  282. // ---------- Extensions ------------------------------------------------------------------------------------------
  283. /**
  284. * Return the string representation as a URI reference.
  285. * @return string
  286. */
  287. public function toString(int $flags = 0): string
  288. {
  289. $scheme = $this->formatComponent($this->getComponent('scheme'), '', ':');
  290. $authority = $this->authority ? "//{$this->formatComponent($this->getAuthority($flags))}" : '';
  291. $path_flags = ($this->authority && $this->path ? self::ABSOLUTE_PATH : 0) | $flags;
  292. $path = $this->formatComponent($this->getPath($path_flags));
  293. $query = $this->formatComponent($this->getComponent('query'), '?');
  294. $fragment = $this->formatComponent($this->getComponent('fragment'), '#');
  295. return "{$scheme}{$authority}{$path}{$query}{$fragment}";
  296. }
  297. // ---------- Private helper methods ------------------------------------------------------------------------------
  298. private function parse(string $uri_string = ''): void
  299. {
  300. if ($uri_string === '') {
  301. return;
  302. }
  303. preg_match(self::RE_MAIN, $uri_string, $main);
  304. $this->authority = !empty($main['authorityc']);
  305. $this->setComponent('scheme', isset($main['schemec']) ? $main['scheme'] : '');
  306. $this->setComponent('path', isset($main['path']) ? $main['path'] : '');
  307. $this->setComponent('query', isset($main['queryc']) ? $main['query'] : '');
  308. $this->setComponent('fragment', isset($main['fragmentc']) ? $main['fragment'] : '');
  309. if ($this->authority) {
  310. preg_match(self::RE_AUTH, $main['authority'], $auth);
  311. if (empty($auth) && $main['authority'] !== '') {
  312. throw new InvalidArgumentException("Invalid 'authority'.");
  313. }
  314. if ($this->isEmpty($auth['host']) && !$this->isEmpty($auth['user'])) {
  315. throw new InvalidArgumentException("Invalid 'authority'.");
  316. }
  317. $this->setComponent('user', isset($auth['user']) ? $auth['user'] : '');
  318. $this->setComponent('pass', isset($auth['passc']) ? $auth['pass'] : '');
  319. $this->setComponent('host', isset($auth['host']) ? $auth['host'] : '');
  320. $this->setComponent('port', isset($auth['portc']) ? $auth['port'] : '');
  321. }
  322. }
  323. private function encode(string $source, string $keep = ''): string
  324. {
  325. $exclude = "[^%\/:=&!\$'()*+,;@{$keep}]+";
  326. $exp = "/(%{$exclude})|({$exclude})/";
  327. return preg_replace_callback($exp, function ($matches) {
  328. if ($e = preg_match('/^(%[0-9a-fA-F]{2})/', $matches[0], $m)) {
  329. return substr($matches[0], 0, 3) . rawurlencode(substr($matches[0], 3));
  330. } else {
  331. return rawurlencode($matches[0]);
  332. }
  333. }, $source);
  334. }
  335. private function setComponent(string $component, $value): void
  336. {
  337. $value = $this->parseCompontent($component, $value);
  338. $this->$component = $value;
  339. }
  340. private function parseCompontent(string $component, $value)
  341. {
  342. if ($this->isEmpty($value)) {
  343. return null;
  344. }
  345. switch ($component) {
  346. case 'scheme':
  347. $this->assertString($component, $value);
  348. $this->assertpattern($component, $value, '/^[a-z][a-z0-9-+.]*$/i');
  349. return mb_strtolower($value);
  350. case 'host': // IP-literal / IPv4address / reg-name
  351. $this->assertString($component, $value);
  352. $this->authority = $this->authority || !$this->isEmpty($value);
  353. return mb_strtolower($value);
  354. case 'port':
  355. $this->assertInteger($component, $value);
  356. if ($value < 0 || $value > 65535) {
  357. throw new InvalidArgumentException("Invalid port number");
  358. }
  359. return (int)$value;
  360. case 'path':
  361. $this->assertString($component, $value);
  362. $value = $this->encode($value);
  363. return $value;
  364. case 'user':
  365. case 'pass':
  366. case 'query':
  367. case 'fragment':
  368. $this->assertString($component, $value);
  369. $value = $this->encode($value, '?');
  370. return $value;
  371. }
  372. }
  373. private function getComponent(string $component)
  374. {
  375. return isset($this->$component) ? $this->$component : null;
  376. }
  377. private function formatComponent($value, string $before = '', string $after = ''): string
  378. {
  379. return $this->isEmpty($value) ? '' : "{$before}{$value}{$after}";
  380. }
  381. private function isEmpty($value): bool
  382. {
  383. return is_null($value) || $value === '';
  384. }
  385. private function assertString(string $component, $value): void
  386. {
  387. if (!is_string($value)) {
  388. throw new InvalidArgumentException("Invalid '{$component}': Should be a string");
  389. }
  390. }
  391. private function assertInteger(string $component, $value): void
  392. {
  393. if (!is_numeric($value) || intval($value) != $value) {
  394. throw new InvalidArgumentException("Invalid '{$component}': Should be an integer");
  395. }
  396. }
  397. private function assertPattern(string $component, string $value, string $pattern): void
  398. {
  399. if (preg_match($pattern, $value) == 0) {
  400. throw new InvalidArgumentException("Invalid '{$component}': Should match {$pattern}");
  401. }
  402. }
  403. private function normalizePath(string $path): string
  404. {
  405. $result = [];
  406. preg_match_all('!([^/]*/|[^/]*$)!', $path, $items);
  407. foreach ($items[0] as $item) {
  408. switch ($item) {
  409. case '':
  410. case './':
  411. case '.':
  412. break; // just skip
  413. case '/':
  414. if (empty($result)) {
  415. array_push($result, $item); // add
  416. }
  417. break;
  418. case '..':
  419. case '../':
  420. if (empty($result) || end($result) == '../') {
  421. array_push($result, $item); // add
  422. } else {
  423. array_pop($result); // remove previous
  424. }
  425. break;
  426. default:
  427. array_push($result, $item); // add
  428. }
  429. }
  430. return implode('', $result);
  431. }
  432. private function idna(string $value): string
  433. {
  434. if ($value === '' || !is_callable('idn_to_ascii')) {
  435. return $value; // Can't convert, but don't cause exception
  436. }
  437. return idn_to_ascii($value, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46);
  438. }
  439. }