3 * @see https://github.com/zendframework/zend-http for the canonical source repository
4 * @copyright Copyright (c) 2005-2017 Zend Technologies USA Inc. (http://www.zend.com)
5 * @license https://github.com/zendframework/zend-http/blob/master/LICENSE.md New BSD License
14 use Zend\Http\Header\Exception
;
15 use Zend\Http\Header\GenericHeader
;
16 use Zend\Loader\PluginClassLocator
;
19 * Basic HTTP headers collection functionality
20 * Handles aggregation of headers
22 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
24 class Headers
implements Countable
, Iterator
27 * @var PluginClassLocator
29 protected $pluginClassLoader;
32 * @var array key names for $headers array
34 protected $headersKeys = [];
37 * @var array Array of header array information or Header instances
39 protected $headers = [];
42 * Populates headers from string representation
44 * Parses a string for headers, and aggregates them, in order, in the
45 * current instance, primarily as strings until they are needed (they
46 * will be lazy loaded)
48 * @param string $string
50 * @throws Exception\RuntimeException
52 public static function fromString($string)
54 $headers = new static();
58 // iterate the header lines, some might be continuations
59 foreach (explode("\r\n", $string) as $line) {
60 // CRLF*2 is end of headers; an empty line by itself or between header lines
61 // is an attempt at CRLF injection.
62 if (preg_match('/^\s*$/', $line)) {
63 // empty line indicates end of headers
66 throw new Exception\
RuntimeException('Malformed header detected');
72 throw new Exception\
RuntimeException('Malformed header detected');
75 // check if a header name is present
76 if (preg_match('/^(?P<name>[^()><@,;:\"\\/\[\]?={} \t]+):.*$/', $line, $matches)) {
78 // a header name was present, then store the current complete line
79 $headers->headersKeys
[] = static::createKey($current['name']);
80 $headers->headers
[] = $current;
83 'name' => $matches['name'],
84 'line' => trim($line),
90 if (preg_match("/^[ \t][^\r\n]*$/", $line, $matches)) {
91 // continuation: append to current line
92 $current['line'] .= trim($line);
96 // Line does not match header format!
97 throw new Exception\
RuntimeException(sprintf(
98 'Line "%s" does not match header format!',
103 $headers->headersKeys
[] = static::createKey($current['name']);
104 $headers->headers
[] = $current;
110 * Set an alternate implementation for the PluginClassLoader
112 * @param \Zend\Loader\PluginClassLocator $pluginClassLoader
115 public function setPluginClassLoader(PluginClassLocator
$pluginClassLoader)
117 $this->pluginClassLoader
= $pluginClassLoader;
122 * Return an instance of a PluginClassLocator, lazyload and inject map if necessary
124 * @return PluginClassLocator
126 public function getPluginClassLoader()
128 if ($this->pluginClassLoader
=== null) {
129 $this->pluginClassLoader
= new HeaderLoader();
131 return $this->pluginClassLoader
;
135 * Add many headers at once
137 * Expects an array (or Traversable object) of type/value pairs.
139 * @param array|Traversable $headers
141 * @throws Exception\InvalidArgumentException
143 public function addHeaders($headers)
145 if (! is_array($headers) && ! $headers instanceof Traversable
) {
146 throw new Exception\
InvalidArgumentException(sprintf(
147 'Expected array or Traversable; received "%s"',
148 (is_object($headers) ?
get_class($headers) : gettype($headers))
152 foreach ($headers as $name => $value) {
154 if (is_string($value)) {
155 $this->addHeaderLine($value);
156 } elseif (is_array($value) && count($value) == 1) {
157 $this->addHeaderLine(key($value), current($value));
158 } elseif (is_array($value) && count($value) == 2) {
159 $this->addHeaderLine($value[0], $value[1]);
160 } elseif ($value instanceof Header\HeaderInterface
) {
161 $this->addHeader($value);
163 } elseif (is_string($name)) {
164 $this->addHeaderLine($name, $value);
172 * Add a raw header line, either in name => value, or as a single string 'name: value'
174 * This method allows for lazy-loading in that the parsing and instantiation of Header object
175 * will be delayed until they are retrieved by either get() or current()
177 * @throws Exception\InvalidArgumentException
178 * @param string $headerFieldNameOrLine
179 * @param string $fieldValue optional
182 public function addHeaderLine($headerFieldNameOrLine, $fieldValue = null)
185 if (preg_match('/^(?P<name>[^()><@,;:\"\\/\[\]?=}{ \t]+):.*$/', $headerFieldNameOrLine, $matches)
186 && $fieldValue === null) {
188 $headerName = $matches['name'];
189 $headerKey = static::createKey($matches['name']);
190 $line = $headerFieldNameOrLine;
191 } elseif ($fieldValue === null) {
192 throw new Exception\
InvalidArgumentException('A field name was provided without a field value');
194 $headerName = $headerFieldNameOrLine;
195 $headerKey = static::createKey($headerFieldNameOrLine);
196 if (is_array($fieldValue)) {
197 $fieldValue = implode(', ', $fieldValue);
199 $line = $headerFieldNameOrLine . ': ' . $fieldValue;
202 $this->headersKeys
[] = $headerKey;
203 $this->headers
[] = ['name' => $headerName, 'line' => $line];
209 * Add a Header to this container, for raw values @see addHeaderLine() and addHeaders()
211 * @param Header\HeaderInterface $header
214 public function addHeader(Header\HeaderInterface
$header)
216 $key = static::createKey($header->getFieldName());
217 $index = array_search($key, $this->headersKeys
);
219 // No header by that key presently; append key and header to list.
220 if ($index === false) {
221 $this->headersKeys
[] = $key;
222 $this->headers
[] = $header;
226 // Header exists, and is a multi-value header; append key and header to
227 // list (as multi-value headers are aggregated on retrieval)
228 $class = ($this->getPluginClassLoader()->load(str_replace('-', '', $key))) ?
: Header\GenericHeader
::class;
229 if (in_array(Header\MultipleHeaderInterface
::class, class_implements($class, true))) {
230 $this->headersKeys
[] = $key;
231 $this->headers
[] = $header;
235 // Otherwise, we replace the current instance.
236 $this->headers
[$index] = $header;
242 * Remove a Header from the container
244 * @param Header\HeaderInterface $header
247 public function removeHeader(Header\HeaderInterface
$header)
249 $index = array_search($header, $this->headers
, true);
250 if ($index !== false) {
251 unset($this->headersKeys
[$index]);
252 unset($this->headers
[$index]);
262 * Removes all headers from queue
266 public function clearHeaders()
268 $this->headers
= $this->headersKeys
= [];
273 * Get all headers of a certain name/type
275 * @param string $name
276 * @return bool|Header\HeaderInterface|ArrayIterator
278 public function get($name)
280 $key = static::createKey($name);
281 if (! $this->has($name)) {
285 $class = ($this->getPluginClassLoader()->load(str_replace('-', '', $key))) ?
: 'Zend\Http\Header\GenericHeader';
287 if (in_array('Zend\Http\Header\MultipleHeaderInterface', class_implements($class, true))) {
289 foreach (array_keys($this->headersKeys
, $key) as $index) {
290 if (is_array($this->headers
[$index])) {
291 $this->lazyLoadHeader($index);
294 foreach (array_keys($this->headersKeys
, $key) as $index) {
295 $headers[] = $this->headers
[$index];
297 return new ArrayIterator($headers);
300 $index = array_search($key, $this->headersKeys
);
301 if ($index === false) {
305 if (is_array($this->headers
[$index])) {
306 return $this->lazyLoadHeader($index);
309 return $this->headers
[$index];
313 * Test for existence of a type of header
315 * @param string $name
318 public function has($name)
320 return in_array(static::createKey($name), $this->headersKeys
);
324 * Advance the pointer for this object as an iterator
328 public function next()
330 next($this->headers
);
334 * Return the current key for this object as an iterator
338 public function key()
340 return (key($this->headers
));
344 * Is this iterator still valid?
348 public function valid()
350 return (current($this->headers
) !== false);
354 * Reset the internal pointer for this object as an iterator
358 public function rewind()
360 reset($this->headers
);
364 * Return the current value for this iterator, lazy loading it if need be
366 * @return array|Header\HeaderInterface
368 public function current()
370 $current = current($this->headers
);
371 if (is_array($current)) {
372 $current = $this->lazyLoadHeader(key($this->headers
));
378 * Return the number of headers in this contain, if all headers have not been parsed, actual count could
379 * increase if MultipleHeader objects exist in the Request/Response. If you need an exact count, iterate
381 * @return int count of currently known headers
383 public function count()
385 return count($this->headers
);
389 * Render all headers at once
391 * This method handles the normal iteration of headers; it is up to the
392 * concrete classes to prepend with the appropriate status/request line.
396 public function toString()
399 foreach ($this->toArray() as $fieldName => $fieldValue) {
400 if (is_array($fieldValue)) {
401 // Handle multi-value headers
402 foreach ($fieldValue as $value) {
403 $headers .= $fieldName . ': ' . $value . "\r\n";
407 // Handle single-value headers
408 $headers .= $fieldName . ': ' . $fieldValue . "\r\n";
414 * Return the headers container as an array
416 * @todo determine how to produce single line headers, if they are supported
419 public function toArray()
422 /* @var $header Header\HeaderInterface */
423 foreach ($this->headers
as $header) {
424 if ($header instanceof Header\MultipleHeaderInterface
) {
425 $name = $header->getFieldName();
426 if (! isset($headers[$name])) {
427 $headers[$name] = [];
429 $headers[$name][] = $header->getFieldValue();
430 } elseif ($header instanceof Header\HeaderInterface
) {
431 $headers[$header->getFieldName()] = $header->getFieldValue();
434 preg_match('/^(?P<name>[^()><@,;:\"\\/\[\]?=}{ \t]+):\s*(?P<value>.*)$/', $header['line'], $matches);
436 $headers[$matches['name']] = $matches['value'];
444 * By calling this, it will force parsing and loading of all headers, after this count() will be accurate
448 public function forceLoading()
450 foreach ($this as $item) {
451 // $item should now be loaded
458 * @param bool $isGeneric
461 protected function lazyLoadHeader($index, $isGeneric = false)
463 $current = $this->headers
[$index];
465 $key = $this->headersKeys
[$index];
466 /* @var $class Header\HeaderInterface */
467 $class = $this->getPluginClassLoader()->load(str_replace('-', '', $key));
468 if ($isGeneric ||
! $class) {
469 $class = GenericHeader
::class;
473 $headers = $class::fromString($current['line']);
474 } catch (Exception\InvalidArgumentException
$exception) {
475 return $this->lazyLoadHeader($index, true);
477 if (is_array($headers)) {
478 $this->headers
[$index] = $current = array_shift($headers);
479 foreach ($headers as $header) {
480 $this->headersKeys
[] = $key;
481 $this->headers
[] = $header;
486 $this->headers
[$index] = $current = $headers;
491 * Create array key from header name
493 * @param string $name
496 protected static function createKey($name)
498 return str_replace(['_', ' ', '.'], '-', strtolower($name));