HeaderUtils.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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 $header HTTP header value
  35. * @param string $separators List of characters to split on, ordered by
  36. * precedence, e.g. ",", ";=", or ",;="
  37. *
  38. * @return array Nested array with as many levels as there are characters in
  39. * $separators
  40. */
  41. public static function split(string $header, string $separators): array
  42. {
  43. $quotedSeparators = preg_quote($separators, '/');
  44. preg_match_all('
  45. /
  46. (?!\s)
  47. (?:
  48. # quoted-string
  49. "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$)
  50. |
  51. # token
  52. [^"'.$quotedSeparators.']+
  53. )+
  54. (?<!\s)
  55. |
  56. # separator
  57. \s*
  58. (?<separator>['.$quotedSeparators.'])
  59. \s*
  60. /x', trim($header), $matches, PREG_SET_ORDER);
  61. return self::groupParts($matches, $separators);
  62. }
  63. /**
  64. * Combines an array of arrays into one associative array.
  65. *
  66. * Each of the nested arrays should have one or two elements. The first
  67. * value will be used as the keys in the associative array, and the second
  68. * will be used as the values, or true if the nested array only contains one
  69. * element. Array keys are lowercased.
  70. *
  71. * Example:
  72. *
  73. * HeaderUtils::combine([["foo", "abc"], ["bar"]])
  74. * // => ["foo" => "abc", "bar" => true]
  75. */
  76. public static function combine(array $parts): array
  77. {
  78. $assoc = [];
  79. foreach ($parts as $part) {
  80. $name = strtolower($part[0]);
  81. $value = $part[1] ?? true;
  82. $assoc[$name] = $value;
  83. }
  84. return $assoc;
  85. }
  86. /**
  87. * Joins an associative array into a string for use in an HTTP header.
  88. *
  89. * The key and value of each entry are joined with "=", and all entries
  90. * are joined with the specified separator and an additional space (for
  91. * readability). Values are quoted if necessary.
  92. *
  93. * Example:
  94. *
  95. * HeaderUtils::toString(["foo" => "abc", "bar" => true, "baz" => "a b c"], ",")
  96. * // => 'foo=abc, bar, baz="a b c"'
  97. */
  98. public static function toString(array $assoc, string $separator): string
  99. {
  100. $parts = [];
  101. foreach ($assoc as $name => $value) {
  102. if (true === $value) {
  103. $parts[] = $name;
  104. } else {
  105. $parts[] = $name.'='.self::quote($value);
  106. }
  107. }
  108. return implode($separator.' ', $parts);
  109. }
  110. /**
  111. * Encodes a string as a quoted string, if necessary.
  112. *
  113. * If a string contains characters not allowed by the "token" construct in
  114. * the HTTP specification, it is backslash-escaped and enclosed in quotes
  115. * to match the "quoted-string" construct.
  116. */
  117. public static function quote(string $s): string
  118. {
  119. if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) {
  120. return $s;
  121. }
  122. return '"'.addcslashes($s, '"\\"').'"';
  123. }
  124. /**
  125. * Decodes a quoted string.
  126. *
  127. * If passed an unquoted string that matches the "token" construct (as
  128. * defined in the HTTP specification), it is passed through verbatimly.
  129. */
  130. public static function unquote(string $s): string
  131. {
  132. return preg_replace('/\\\\(.)|"/', '$1', $s);
  133. }
  134. /**
  135. * Generates a HTTP Content-Disposition field-value.
  136. *
  137. * @param string $disposition One of "inline" or "attachment"
  138. * @param string $filename A unicode string
  139. * @param string $filenameFallback A string containing only ASCII characters that
  140. * is semantically equivalent to $filename. If the filename is already ASCII,
  141. * it can be omitted, or just copied from $filename
  142. *
  143. * @return string A string suitable for use as a Content-Disposition field-value
  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 (false !== strpos($filenameFallback, '%')) {
  163. throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.');
  164. }
  165. // path separators aren't allowed in either.
  166. if (false !== strpos($filename, '/') || false !== strpos($filename, '\\') || false !== strpos($filenameFallback, '/') || false !== strpos($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. private static function groupParts(array $matches, string $separators): array
  176. {
  177. $separator = $separators[0];
  178. $partSeparators = substr($separators, 1);
  179. $i = 0;
  180. $partMatches = [];
  181. foreach ($matches as $match) {
  182. if (isset($match['separator']) && $match['separator'] === $separator) {
  183. ++$i;
  184. } else {
  185. $partMatches[$i][] = $match;
  186. }
  187. }
  188. $parts = [];
  189. if ($partSeparators) {
  190. foreach ($partMatches as $matches) {
  191. $parts[] = self::groupParts($matches, $partSeparators);
  192. }
  193. } else {
  194. foreach ($partMatches as $matches) {
  195. $parts[] = self::unquote($matches[0][0]);
  196. }
  197. }
  198. return $parts;
  199. }
  200. }