You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

288 lines
7.2 KiB

3 years ago
  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. * HeaderBag is a container for HTTP headers.
  13. *
  14. * @author Fabien Potencier <fabien@symfony.com>
  15. */
  16. class HeaderBag implements \IteratorAggregate, \Countable
  17. {
  18. protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  19. protected const LOWER = '-abcdefghijklmnopqrstuvwxyz';
  20. protected $headers = [];
  21. protected $cacheControl = [];
  22. public function __construct(array $headers = [])
  23. {
  24. foreach ($headers as $key => $values) {
  25. $this->set($key, $values);
  26. }
  27. }
  28. /**
  29. * Returns the headers as a string.
  30. *
  31. * @return string The headers
  32. */
  33. public function __toString()
  34. {
  35. if (!$headers = $this->all()) {
  36. return '';
  37. }
  38. ksort($headers);
  39. $max = max(array_map('strlen', array_keys($headers))) + 1;
  40. $content = '';
  41. foreach ($headers as $name => $values) {
  42. $name = ucwords($name, '-');
  43. foreach ($values as $value) {
  44. $content .= sprintf("%-{$max}s %s\r\n", $name.':', $value);
  45. }
  46. }
  47. return $content;
  48. }
  49. /**
  50. * Returns the headers.
  51. *
  52. * @param string|null $key The name of the headers to return or null to get them all
  53. *
  54. * @return array An array of headers
  55. */
  56. public function all(string $key = null)
  57. {
  58. if (null !== $key) {
  59. return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? [];
  60. }
  61. return $this->headers;
  62. }
  63. /**
  64. * Returns the parameter keys.
  65. *
  66. * @return array An array of parameter keys
  67. */
  68. public function keys()
  69. {
  70. return array_keys($this->all());
  71. }
  72. /**
  73. * Replaces the current HTTP headers by a new set.
  74. */
  75. public function replace(array $headers = [])
  76. {
  77. $this->headers = [];
  78. $this->add($headers);
  79. }
  80. /**
  81. * Adds new headers the current HTTP headers set.
  82. */
  83. public function add(array $headers)
  84. {
  85. foreach ($headers as $key => $values) {
  86. $this->set($key, $values);
  87. }
  88. }
  89. /**
  90. * Returns a header value by name.
  91. *
  92. * @return string|null The first header value or default value
  93. */
  94. public function get(string $key, string $default = null)
  95. {
  96. $headers = $this->all($key);
  97. if (!$headers) {
  98. return $default;
  99. }
  100. if (null === $headers[0]) {
  101. return null;
  102. }
  103. return (string) $headers[0];
  104. }
  105. /**
  106. * Sets a header by name.
  107. *
  108. * @param string|string[] $values The value or an array of values
  109. * @param bool $replace Whether to replace the actual value or not (true by default)
  110. */
  111. public function set(string $key, $values, bool $replace = true)
  112. {
  113. $key = strtr($key, self::UPPER, self::LOWER);
  114. if (\is_array($values)) {
  115. $values = array_values($values);
  116. if (true === $replace || !isset($this->headers[$key])) {
  117. $this->headers[$key] = $values;
  118. } else {
  119. $this->headers[$key] = array_merge($this->headers[$key], $values);
  120. }
  121. } else {
  122. if (true === $replace || !isset($this->headers[$key])) {
  123. $this->headers[$key] = [$values];
  124. } else {
  125. $this->headers[$key][] = $values;
  126. }
  127. }
  128. if ('cache-control' === $key) {
  129. $this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key]));
  130. }
  131. }
  132. /**
  133. * Returns true if the HTTP header is defined.
  134. *
  135. * @return bool true if the parameter exists, false otherwise
  136. */
  137. public function has(string $key)
  138. {
  139. return \array_key_exists(strtr($key, self::UPPER, self::LOWER), $this->all());
  140. }
  141. /**
  142. * Returns true if the given HTTP header contains the given value.
  143. *
  144. * @return bool true if the value is contained in the header, false otherwise
  145. */
  146. public function contains(string $key, string $value)
  147. {
  148. return \in_array($value, $this->all($key));
  149. }
  150. /**
  151. * Removes a header.
  152. */
  153. public function remove(string $key)
  154. {
  155. $key = strtr($key, self::UPPER, self::LOWER);
  156. unset($this->headers[$key]);
  157. if ('cache-control' === $key) {
  158. $this->cacheControl = [];
  159. }
  160. }
  161. /**
  162. * Returns the HTTP header value converted to a date.
  163. *
  164. * @return \DateTimeInterface|null The parsed DateTime or the default value if the header does not exist
  165. *
  166. * @throws \RuntimeException When the HTTP header is not parseable
  167. */
  168. public function getDate(string $key, \DateTime $default = null)
  169. {
  170. if (null === $value = $this->get($key)) {
  171. return $default;
  172. }
  173. if (false === $date = \DateTime::createFromFormat(\DATE_RFC2822, $value)) {
  174. throw new \RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value));
  175. }
  176. return $date;
  177. }
  178. /**
  179. * Adds a custom Cache-Control directive.
  180. *
  181. * @param mixed $value The Cache-Control directive value
  182. */
  183. public function addCacheControlDirective(string $key, $value = true)
  184. {
  185. $this->cacheControl[$key] = $value;
  186. $this->set('Cache-Control', $this->getCacheControlHeader());
  187. }
  188. /**
  189. * Returns true if the Cache-Control directive is defined.
  190. *
  191. * @return bool true if the directive exists, false otherwise
  192. */
  193. public function hasCacheControlDirective(string $key)
  194. {
  195. return \array_key_exists($key, $this->cacheControl);
  196. }
  197. /**
  198. * Returns a Cache-Control directive value by name.
  199. *
  200. * @return mixed The directive value if defined, null otherwise
  201. */
  202. public function getCacheControlDirective(string $key)
  203. {
  204. return \array_key_exists($key, $this->cacheControl) ? $this->cacheControl[$key] : null;
  205. }
  206. /**
  207. * Removes a Cache-Control directive.
  208. */
  209. public function removeCacheControlDirective(string $key)
  210. {
  211. unset($this->cacheControl[$key]);
  212. $this->set('Cache-Control', $this->getCacheControlHeader());
  213. }
  214. /**
  215. * Returns an iterator for headers.
  216. *
  217. * @return \ArrayIterator An \ArrayIterator instance
  218. */
  219. public function getIterator()
  220. {
  221. return new \ArrayIterator($this->headers);
  222. }
  223. /**
  224. * Returns the number of headers.
  225. *
  226. * @return int The number of headers
  227. */
  228. public function count()
  229. {
  230. return \count($this->headers);
  231. }
  232. protected function getCacheControlHeader()
  233. {
  234. ksort($this->cacheControl);
  235. return HeaderUtils::toString($this->cacheControl, ',');
  236. }
  237. /**
  238. * Parses a Cache-Control HTTP header.
  239. *
  240. * @return array An array representing the attribute values
  241. */
  242. protected function parseCacheControl(string $header)
  243. {
  244. $parts = HeaderUtils::split($header, ',=');
  245. return HeaderUtils::combine($parts);
  246. }
  247. }