/src/Data/URI.php

https://github.com/ILIAS-eLearning/ILIAS · PHP · 384 lines · 188 code · 36 blank · 160 comment · 10 complexity · b825c827959b88ee6979e604d0c38879 MD5 · raw file

  1. <?php
  2. declare(strict_types=1);
  3. namespace ILIAS\Data;
  4. /**
  5. * The scope of this class is split ilias-conform URI's into components.
  6. * Please refer to RFC 3986 for details.
  7. * Notice, ilias-confor URI's will form a SUBSET of RFC 3986:
  8. * - Notice the restrictions on baseuri-subdelims.
  9. * - We require a schema and an authority to be present.
  10. * - If any part is located and it is invalid an exception will be thrown
  11. * instead of just omiting it.
  12. * - IPv6 is currently not supported.
  13. */
  14. class URI
  15. {
  16. /**
  17. * @var string
  18. */
  19. protected $schema;
  20. /**
  21. * @var string
  22. */
  23. protected $host;
  24. /**
  25. * @var int|null
  26. */
  27. protected $port;
  28. /**
  29. * @var string|null
  30. */
  31. protected $path;
  32. /**
  33. * @var string|null
  34. */
  35. protected $query;
  36. /**
  37. * @var string|null
  38. */
  39. protected $fragment;
  40. const PATH_DELIM = '/';
  41. /**
  42. * Relevant character-groups as defined in RFC 3986 Appendix 1
  43. */
  44. const ALPHA = '[A-Za-z]';
  45. const DIGIT = '[0-9]';
  46. const ALPHA_DIGIT = '[A-Za-z0-9]';
  47. const HEXDIG = '[0-9A-F]';
  48. const PCTENCODED = '%' . self::HEXDIG . self::HEXDIG;
  49. /**
  50. * point|minus|plus to be used in schema.
  51. */
  52. const PIMP = '[\\+\\-\\.]';
  53. /**
  54. * valid subdelims according to RFC 3986 Appendix 1:
  55. * "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "="
  56. */
  57. const SUBDELIMS = '[\\$,;=!&\'\\(\\)\\*\\+]';
  58. /**
  59. * subdelims without jsf**k characters +!() and =
  60. */
  61. const BASEURI_SUBDELIMS = '[\\$,;&\'\\*]';
  62. const UNRESERVED = self::ALPHA_DIGIT . '|[\\-\\._~]';
  63. const UNRESERVED_NO_DOT = self::ALPHA_DIGIT . '|[\\-_~]';
  64. const PCHAR = self::UNRESERVED . '|' . self::SUBDELIMS . '|' . self::PCTENCODED . '|:|@';
  65. const BASEURI_PCHAR = self::UNRESERVED . '|' . self::BASEURI_SUBDELIMS . '|' . self::PCTENCODED . '|:|@';
  66. const SCHEMA = '#^' . self::ALPHA . '(' . self::ALPHA_DIGIT . '|' . self::PIMP . ')*$#';
  67. const DOMAIN_LABEL = self::ALPHA_DIGIT . '((' . self::UNRESERVED_NO_DOT . '|' . self::PCTENCODED . '|' . self::BASEURI_SUBDELIMS . ')*' . self::ALPHA_DIGIT . ')*';
  68. const HOST_REG_NAME = '^' . self::DOMAIN_LABEL . '(\\.' . self::DOMAIN_LABEL . ')*$';
  69. const HOST_IPV4 = '^(' . self::DIGIT . '{1,3})(\\.' . self::DIGIT . '{1,3}){3}$';
  70. const HOST = '#' . self::HOST_IPV4 . '|' . self::HOST_REG_NAME . '#';
  71. const PORT = '#^' . self::DIGIT . '+$#';
  72. const PATH = '#^(?!//)(?!:)(' . self::PCHAR . '|' . self::PATH_DELIM . ')+$#';
  73. const QUERY = '#^(' . self::PCHAR . '|' . self::PATH_DELIM . '|\\?)+$#';
  74. const FRAGMENT = '#^(' . self::PCHAR . '|' . self::PATH_DELIM . '|\\?|\\#)+$#';
  75. public function __construct(string $uri_string)
  76. {
  77. $this->schema = $this->digestSchema(parse_url($uri_string, PHP_URL_SCHEME));
  78. $this->host = $this->digestHost(parse_url($uri_string, PHP_URL_HOST));
  79. $this->port = $this->digestPort(parse_url($uri_string, PHP_URL_PORT));
  80. $this->path = $this->digestPath(parse_url($uri_string, PHP_URL_PATH));
  81. $this->query = $this->digestQuery(parse_url($uri_string, PHP_URL_QUERY));
  82. $this->fragment = $this->digestFragment(parse_url($uri_string, PHP_URL_FRAGMENT));
  83. }
  84. /**
  85. * Check schema formating. Return it in case of success.
  86. *
  87. * @param string $schema
  88. * @throws \InvalidArgumentException
  89. * @return string
  90. */
  91. protected function digestSchema(string $schema) : string
  92. {
  93. return $this->checkCorrectFormatOrThrow(self::SCHEMA, (string) $schema);
  94. }
  95. /**
  96. * Check host formating. Return it in case of success.
  97. *
  98. * @param string $host
  99. * @throws \InvalidArgumentException
  100. * @return string
  101. */
  102. protected function digestHost(string $host) : string
  103. {
  104. return $this->checkCorrectFormatOrThrow(self::HOST, (string) $host);
  105. }
  106. /**
  107. * Check port formating. Return it in case of success.
  108. *
  109. * @param int|null $port
  110. * @return int|null
  111. */
  112. protected function digestPort(int $port = null)
  113. {
  114. if ($port === null) {
  115. return null;
  116. }
  117. return $port;
  118. }
  119. /**
  120. * Check path formating. Return it in case of success.
  121. *
  122. * @param string|null $path
  123. * @throws \InvalidArgumentException
  124. * @return string|null
  125. */
  126. protected function digestPath(string $path = null)
  127. {
  128. if ($path === null) {
  129. return null;
  130. }
  131. $path = trim($this->checkCorrectFormatOrThrow(self::PATH, $path), self::PATH_DELIM);
  132. if ($path === '') {
  133. $path = null;
  134. }
  135. return $path;
  136. }
  137. /**
  138. * Check query formating. Return it in case of success.
  139. *
  140. * @param string|null $query
  141. * @throws \InvalidArgumentException
  142. * @return string|null
  143. */
  144. protected function digestQuery(string $query = null)
  145. {
  146. if ($query === null) {
  147. return null;
  148. }
  149. return $this->checkCorrectFormatOrThrow(self::QUERY, $query);
  150. }
  151. /**
  152. * Check fragment formating. Return it in case of success.
  153. *
  154. * @param string|null $fragment
  155. * @throws \InvalidArgumentException
  156. * @return string|null
  157. */
  158. protected function digestFragment(string $fragment = null)
  159. {
  160. if ($fragment === null) {
  161. return null;
  162. }
  163. return $this->checkCorrectFormatOrThrow(self::FRAGMENT, $fragment);
  164. }
  165. /**
  166. * Check wether a string fits a regexp. Return it, if so,
  167. * throw otherwise.
  168. *
  169. * @param string $regexp
  170. * @param string $string
  171. * @throws \InvalidArgumentException
  172. * @return string|null
  173. */
  174. protected function checkCorrectFormatOrThrow(string $regexp, string $string)
  175. {
  176. if (preg_match($regexp, (string) $string) === 1) {
  177. return $string;
  178. }
  179. throw new \InvalidArgumentException('ill-formated component "' . $string . '" expected "' . $regexp . '"');
  180. }
  181. /**
  182. * @return string
  183. */
  184. public function getSchema() : string
  185. {
  186. return $this->schema;
  187. }
  188. /**
  189. * Get URI with modified schema
  190. *
  191. * @param string $schema
  192. * @return URI
  193. */
  194. public function withSchema(string $schema) : URI
  195. {
  196. $shema = $this->digestSchema($schema);
  197. $other = clone $this;
  198. $other->schema = $schema;
  199. return $other;
  200. }
  201. /**
  202. * @return string
  203. */
  204. public function getAuthority() : string
  205. {
  206. $port = $this->getPort();
  207. if ($port === null) {
  208. return $this->getHost();
  209. }
  210. return $this->getHost() . ':' . $port;
  211. }
  212. /**
  213. * Get URI with modified authority
  214. *
  215. * @param string $authority
  216. * @return URI
  217. */
  218. public function withAuthority(string $authority) : URI
  219. {
  220. $parts = explode(':', $authority);
  221. if (count($parts) > 2) {
  222. throw new \InvalidArgumentException('ill-formated component ' . $authority);
  223. }
  224. $host = $this->digestHost($parts[0]);
  225. $port = null;
  226. if (array_key_exists(1, $parts)) {
  227. $port = (int) $this->checkCorrectFormatOrThrow(self::PORT, (string) $parts[1]);
  228. }
  229. $other = clone $this;
  230. $other->host = $host;
  231. $other->port = $port;
  232. return $other;
  233. }
  234. /**
  235. * @return int|null
  236. */
  237. public function getPort()
  238. {
  239. return $this->port;
  240. }
  241. /**
  242. * Get URI with modified port
  243. *
  244. * @param int|null $port
  245. * @return URI
  246. */
  247. public function withPort(int $port = null) : URI
  248. {
  249. $port = $this->digestPort($port);
  250. $other = clone $this;
  251. $other->port = $port;
  252. return $other;
  253. }
  254. /**
  255. * @return string
  256. */
  257. public function getHost() : string
  258. {
  259. return $this->host;
  260. }
  261. /**
  262. * Get URI with modified host
  263. *
  264. * @param string $host
  265. * @return URI
  266. */
  267. public function withHost(string $host) : URI
  268. {
  269. $host = $this->digestHost($host);
  270. $other = clone $this;
  271. $other->host = $host;
  272. return $other;
  273. }
  274. /**
  275. * @return string|null
  276. */
  277. public function getPath()
  278. {
  279. return $this->path;
  280. }
  281. /**
  282. * Get URI with modified path
  283. *
  284. * @param string|null $path
  285. * @return URI
  286. */
  287. public function withPath(string $path = null) : URI
  288. {
  289. $path = $this->digestPath($path);
  290. $other = clone $this;
  291. $other->path = $path;
  292. return $other;
  293. }
  294. /**
  295. * @return string|null
  296. */
  297. public function getQuery()
  298. {
  299. return $this->query;
  300. }
  301. /**
  302. * Get URI with modified query
  303. *
  304. * @param string|null $query
  305. * @return URI
  306. */
  307. public function withQuery(string $query = null) : URI
  308. {
  309. $query = $this->digestQuery($query);
  310. $other = clone $this;
  311. $other->query = $query;
  312. return $other;
  313. }
  314. /**
  315. * @return string|null
  316. */
  317. public function getFragment()
  318. {
  319. return $this->fragment;
  320. }
  321. /**
  322. * Get URI with modified fragment
  323. *
  324. * @param string|null $fragment
  325. * @return URI
  326. */
  327. public function withFragment(string $fragment = null) : URI
  328. {
  329. $fragment = $this->digestFragment($fragment);
  330. $other = clone $this;
  331. $other->fragment = $fragment;
  332. return $other;
  333. }
  334. /**
  335. * Get a well-formed URI consisting only out of
  336. * schema, authority and port.
  337. *
  338. * @return string
  339. */
  340. public function getBaseURI() : string
  341. {
  342. $path = $this->getPath();
  343. if ($path === null) {
  344. return $this->getSchema() . '://' . $this->getAuthority();
  345. }
  346. return $this->getSchema() . '://' . $this->getAuthority() . '/' . $path;
  347. }
  348. }