Encoder.php 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  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 Closure;
  13. use League\Uri\Contracts\UriComponentInterface;
  14. use League\Uri\Exceptions\SyntaxError;
  15. use SensitiveParameter;
  16. use Stringable;
  17. use function preg_match;
  18. use function preg_replace_callback;
  19. use function rawurldecode;
  20. use function rawurlencode;
  21. use function strtoupper;
  22. final class Encoder
  23. {
  24. private const REGEXP_CHARS_INVALID = '/[\x00-\x1f\x7f]/';
  25. private const REGEXP_CHARS_ENCODED = ',%[A-Fa-f0-9]{2},';
  26. private const REGEXP_CHARS_PREVENTS_DECODING = ',%
  27. 2[A-F|1-2|4-9]|
  28. 3[0-9|B|D]|
  29. 4[1-9|A-F]|
  30. 5[0-9|A|F]|
  31. 6[1-9|A-F]|
  32. 7[0-9|E]
  33. ,ix';
  34. private const REGEXP_PART_SUBDELIM = "\!\$&'\(\)\*\+,;\=%";
  35. private const REGEXP_PART_UNRESERVED = 'A-Za-z\d_\-.~';
  36. private const REGEXP_PART_ENCODED = '%(?![A-Fa-f\d]{2})';
  37. /**
  38. * Encode User.
  39. *
  40. * All generic delimiters MUST be encoded
  41. */
  42. public static function encodeUser(Stringable|string|null $component): ?string
  43. {
  44. static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.']+|'.self::REGEXP_PART_ENCODED.'/';
  45. return self::encode($component, $pattern);
  46. }
  47. /**
  48. * Encode Password.
  49. *
  50. * Generic delimiters ":" MUST NOT be encoded
  51. */
  52. public static function encodePassword(#[SensitiveParameter] Stringable|string|null $component): ?string
  53. {
  54. static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':]+|'.self::REGEXP_PART_ENCODED.'/';
  55. return self::encode($component, $pattern);
  56. }
  57. /**
  58. * Encode Path.
  59. *
  60. * Generic delimiters ":", "@", and "/" MUST NOT be encoded
  61. */
  62. public static function encodePath(Stringable|string|null $component): string
  63. {
  64. static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/]+|'.self::REGEXP_PART_ENCODED.'/';
  65. return (string) self::encode($component, $pattern);
  66. }
  67. /**
  68. * Encode Query or Fragment.
  69. *
  70. * Generic delimiters ":", "@", "?", and "/" MUST NOT be encoded
  71. */
  72. public static function encodeQueryOrFragment(Stringable|string|null $component): ?string
  73. {
  74. static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.self::REGEXP_PART_SUBDELIM.':@\/?]+|'.self::REGEXP_PART_ENCODED.'/';
  75. return self::encode($component, $pattern);
  76. }
  77. public static function encodeQueryKeyValue(mixed $component): ?string
  78. {
  79. static $pattern = '/[^'.self::REGEXP_PART_UNRESERVED.']+|'.self::REGEXP_PART_ENCODED.'/';
  80. $encodeMatches = static fn (array $matches): string => match (1) {
  81. preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]),
  82. default => $matches[0],
  83. };
  84. $component = self::filterComponent($component);
  85. return match (true) {
  86. !is_scalar($component) => throw new SyntaxError(sprintf('A pair key/value must be a scalar value `%s` given.', gettype($component))),
  87. 1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => rawurlencode($component),
  88. 1 === preg_match($pattern, $component) => (string) preg_replace_callback($pattern, $encodeMatches(...), $component),
  89. default => $component,
  90. };
  91. }
  92. /**
  93. * Decodes the URI component without decoding the unreserved characters which are already encoded.
  94. */
  95. public static function decodePartial(Stringable|string|int|null $component): ?string
  96. {
  97. $decodeMatches = static fn (array $matches): string => match (1) {
  98. preg_match(self::REGEXP_CHARS_PREVENTS_DECODING, $matches[0]) => strtoupper($matches[0]),
  99. default => rawurldecode($matches[0]),
  100. };
  101. return self::decode($component, $decodeMatches);
  102. }
  103. /**
  104. * Decodes all the URI component characters.
  105. */
  106. public static function decodeAll(Stringable|string|int|null $component): ?string
  107. {
  108. $decodeMatches = static fn (array $matches): string => rawurldecode($matches[0]);
  109. return self::decode($component, $decodeMatches);
  110. }
  111. private static function filterComponent(mixed $component): ?string
  112. {
  113. return match (true) {
  114. true === $component => '1',
  115. false === $component => '0',
  116. $component instanceof UriComponentInterface => $component->value(),
  117. $component instanceof Stringable,
  118. is_scalar($component) => (string) $component,
  119. null === $component => null,
  120. default => throw new SyntaxError(sprintf('The component must be a scalar value `%s` given.', gettype($component))),
  121. };
  122. }
  123. private static function encode(Stringable|string|int|bool|null $component, string $pattern): ?string
  124. {
  125. $component = self::filterComponent($component);
  126. $encodeMatches = static fn (array $matches): string => match (1) {
  127. preg_match('/[^'.self::REGEXP_PART_UNRESERVED.']/', rawurldecode($matches[0])) => rawurlencode($matches[0]),
  128. default => $matches[0],
  129. };
  130. return match (true) {
  131. null === $component,
  132. '' === $component => $component,
  133. default => (string) preg_replace_callback($pattern, $encodeMatches(...), $component),
  134. };
  135. }
  136. /**
  137. * Decodes all the URI component characters.
  138. */
  139. private static function decode(Stringable|string|int|null $component, Closure $decodeMatches): ?string
  140. {
  141. $component = self::filterComponent($component);
  142. return match (true) {
  143. null === $component => null,
  144. 1 === preg_match(self::REGEXP_CHARS_INVALID, $component) => throw new SyntaxError('Invalid component string: '.$component.'.'),
  145. 1 === preg_match(self::REGEXP_CHARS_ENCODED, $component) => preg_replace_callback(self::REGEXP_CHARS_ENCODED, $decodeMatches(...), $component),
  146. default => $component,
  147. };
  148. }
  149. }