HeaderUtils.php 8.9 KB

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