HeaderUtils.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.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. namespace Symfony\Component\HttpFoundation;
  11. /**
  12. * HTTP header utility functions.
  13. *
  14. * @author Christian Schmidt <github@chsc.dk>
  15. */
  16. class HeaderUtils
  17. {
  18. public const DISPOSITION_ATTACHMENT = 'attachment';
  19. public const DISPOSITION_INLINE = 'inline';
  20. /**
  21. * This class should not be instantiated.
  22. */
  23. private function __construct()
  24. {
  25. }
  26. /**
  27. * Splits an HTTP header by one or more separators.
  28. *
  29. * Example:
  30. *
  31. * HeaderUtils::split("da, en-gb;q=0.8", ",;")
  32. * // => ['da'], ['en-gb', 'q=0.8']]
  33. *
  34. * @param string $separators List of characters to split on, ordered by
  35. * precedence, e.g. ",", ";=", or ",;="
  36. *
  37. * @return array Nested array with as many levels as there are characters in
  38. * $separators
  39. */
  40. public static function split(string $header, string $separators): array
  41. {
  42. $quotedSeparators = preg_quote($separators, '/');
  43. preg_match_all('
  44. /
  45. (?!\s)
  46. (?:
  47. # quoted-string
  48. "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$)
  49. |
  50. # token
  51. [^"'.$quotedSeparators.']+
  52. )+
  53. (?<!\s)
  54. |
  55. # separator
  56. \s*
  57. (?<separator>['.$quotedSeparators.'])
  58. \s*
  59. /x', trim($header), $matches, \PREG_SET_ORDER);
  60. return self::groupParts($matches, $separators);
  61. }
  62. /**
  63. * Combines an array of arrays into one associative array.
  64. *
  65. * Each of the nested arrays should have one or two elements. The first
  66. * value will be used as the keys in the associative array, and the second
  67. * will be used as the values, or true if the nested array only contains one
  68. * element. Array keys are lowercased.
  69. *
  70. * Example:
  71. *
  72. * HeaderUtils::combine([["foo", "abc"], ["bar"]])
  73. * // => ["foo" => "abc", "bar" => true]
  74. */
  75. public static function combine(array $parts): array
  76. {
  77. $assoc = [];
  78. foreach ($parts as $part) {
  79. $name = strtolower($part[0]);
  80. $value = $part[1] ?? true;
  81. $assoc[$name] = $value;
  82. }
  83. return $assoc;
  84. }
  85. /**
  86. * Joins an associative array into a string for use in an HTTP header.
  87. *
  88. * The key and value of each entry are joined with "=", and all entries
  89. * are joined with the specified separator and an additional space (for
  90. * readability). Values are quoted if necessary.
  91. *
  92. * Example:
  93. *
  94. * HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",")
  95. * // => 'foo=abc, bar, baz="a b c"'
  96. */
  97. public static function toString(array $assoc, string $separator): string
  98. {
  99. $parts = [];
  100. foreach ($assoc as $name => $value) {
  101. if (true === $value) {
  102. $parts[] = $name;
  103. } else {
  104. $parts[] = $name.'='.self::quote($value);
  105. }
  106. }
  107. return implode($separator.' ', $parts);
  108. }
  109. /**
  110. * Encodes a string as a quoted string, if necessary.
  111. *
  112. * If a string contains characters not allowed by the "token" construct in
  113. * the HTTP specification, it is backslash-escaped and enclosed in quotes
  114. * to match the "quoted-string" construct.
  115. */
  116. public static function quote(string $s): string
  117. {
  118. if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) {
  119. return $s;
  120. }
  121. return '"'.addcslashes($s, '"\\"').'"';
  122. }
  123. /**
  124. * Decodes a quoted string.
  125. *
  126. * If passed an unquoted string that matches the "token" construct (as
  127. * defined in the HTTP specification), it is passed through verbatimly.
  128. */
  129. public static function unquote(string $s): string
  130. {
  131. return preg_replace('/\\\\(.)|"/', '$1', $s);
  132. }
  133. /**
  134. * Generates an HTTP Content-Disposition field-value.
  135. *
  136. * @param string $disposition One of "inline" or "attachment"
  137. * @param string $filename A unicode string
  138. * @param string $filenameFallback A string containing only ASCII characters that
  139. * is semantically equivalent to $filename. If the filename is already ASCII,
  140. * it can be omitted, or just copied from $filename
  141. *
  142. * @throws \InvalidArgumentException
  143. *
  144. * @see RFC 6266
  145. */
  146. public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string
  147. {
  148. if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) {
  149. throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE));
  150. }
  151. if ('' === $filenameFallback) {
  152. $filenameFallback = $filename;
  153. }
  154. // filenameFallback is not ASCII.
  155. if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) {
  156. throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.');
  157. }
  158. // percent characters aren't safe in fallback.
  159. if (str_contains($filenameFallback, '%')) {
  160. throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.');
  161. }
  162. // path separators aren't allowed in either.
  163. if (str_contains($filename, '/') || str_contains($filename, '\\') || str_contains($filenameFallback, '/') || str_contains($filenameFallback, '\\')) {
  164. throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.');
  165. }
  166. $params = ['filename' => $filenameFallback];
  167. if ($filename !== $filenameFallback) {
  168. $params['filename*'] = "utf-8''".rawurlencode($filename);
  169. }
  170. return $disposition.'; '.self::toString($params, ';');
  171. }
  172. /**
  173. * Like parse_str(), but preserves dots in variable names.
  174. */
  175. public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array
  176. {
  177. $q = [];
  178. foreach (explode($separator, $query) as $v) {
  179. if (false !== $i = strpos($v, "\0")) {
  180. $v = substr($v, 0, $i);
  181. }
  182. if (false === $i = strpos($v, '=')) {
  183. $k = urldecode($v);
  184. $v = '';
  185. } else {
  186. $k = urldecode(substr($v, 0, $i));
  187. $v = substr($v, $i);
  188. }
  189. if (false !== $i = strpos($k, "\0")) {
  190. $k = substr($k, 0, $i);
  191. }
  192. $k = ltrim($k, ' ');
  193. if ($ignoreBrackets) {
  194. $q[$k][] = urldecode(substr($v, 1));
  195. continue;
  196. }
  197. if (false === $i = strpos($k, '[')) {
  198. $q[] = bin2hex($k).$v;
  199. } else {
  200. $q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v;
  201. }
  202. }
  203. if ($ignoreBrackets) {
  204. return $q;
  205. }
  206. parse_str(implode('&', $q), $q);
  207. $query = [];
  208. foreach ($q as $k => $v) {
  209. if (false !== $i = strpos($k, '_')) {
  210. $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v;
  211. } else {
  212. $query[hex2bin($k)] = $v;
  213. }
  214. }
  215. return $query;
  216. }
  217. private static function groupParts(array $matches, string $separators, bool $first = true): array
  218. {
  219. $separator = $separators[0];
  220. $partSeparators = substr($separators, 1);
  221. $i = 0;
  222. $partMatches = [];
  223. $previousMatchWasSeparator = false;
  224. foreach ($matches as $match) {
  225. if (!$first && $previousMatchWasSeparator && isset($match['separator']) && $match['separator'] === $separator) {
  226. $previousMatchWasSeparator = true;
  227. $partMatches[$i][] = $match;
  228. } elseif (isset($match['separator']) && $match['separator'] === $separator) {
  229. $previousMatchWasSeparator = true;
  230. ++$i;
  231. } else {
  232. $previousMatchWasSeparator = false;
  233. $partMatches[$i][] = $match;
  234. }
  235. }
  236. $parts = [];
  237. if ($partSeparators) {
  238. foreach ($partMatches as $matches) {
  239. $parts[] = self::groupParts($matches, $partSeparators, false);
  240. }
  241. } else {
  242. foreach ($partMatches as $matches) {
  243. $parts[] = self::unquote($matches[0][0]);
  244. }
  245. if (!$first && 2 < \count($parts)) {
  246. $parts = [
  247. $parts[0],
  248. implode($separator, \array_slice($parts, 1)),
  249. ];
  250. }
  251. }
  252. return $parts;
  253. }
  254. }