composer package updates
[openemr.git] / vendor / zendframework / zend-http / src / Headers.php
blobe3897c1a0f8419e4a921275ad51c0dc6a73d7fad
1 <?php
2 /**
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
6 */
8 namespace Zend\Http;
10 use ArrayIterator;
11 use Countable;
12 use Iterator;
13 use Traversable;
14 use Zend\Http\Header\Exception;
15 use Zend\Http\Header\GenericHeader;
16 use Zend\Loader\PluginClassLocator;
18 /**
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
26 /**
27 * @var PluginClassLocator
29 protected $pluginClassLoader;
31 /**
32 * @var array key names for $headers array
34 protected $headersKeys = [];
36 /**
37 * @var array Array of header array information or Header instances
39 protected $headers = [];
41 /**
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
49 * @return Headers
50 * @throws Exception\RuntimeException
52 public static function fromString($string)
54 $headers = new static();
55 $current = [];
56 $emptyLine = 0;
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
64 $emptyLine += 1;
65 if ($emptyLine > 2) {
66 throw new Exception\RuntimeException('Malformed header detected');
68 continue;
71 if ($emptyLine) {
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)) {
77 if ($current) {
78 // a header name was present, then store the current complete line
79 $headers->headersKeys[] = static::createKey($current['name']);
80 $headers->headers[] = $current;
82 $current = [
83 'name' => $matches['name'],
84 'line' => trim($line),
87 continue;
90 if (preg_match("/^[ \t][^\r\n]*$/", $line, $matches)) {
91 // continuation: append to current line
92 $current['line'] .= trim($line);
93 continue;
96 // Line does not match header format!
97 throw new Exception\RuntimeException(sprintf(
98 'Line "%s" does not match header format!',
99 $line
102 if ($current) {
103 $headers->headersKeys[] = static::createKey($current['name']);
104 $headers->headers[] = $current;
106 return $headers;
110 * Set an alternate implementation for the PluginClassLoader
112 * @param \Zend\Loader\PluginClassLocator $pluginClassLoader
113 * @return Headers
115 public function setPluginClassLoader(PluginClassLocator $pluginClassLoader)
117 $this->pluginClassLoader = $pluginClassLoader;
118 return $this;
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
140 * @return 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) {
153 if (is_int($name)) {
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);
168 return $this;
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
180 * @return Headers
182 public function addHeaderLine($headerFieldNameOrLine, $fieldValue = null)
184 $matches = null;
185 if (preg_match('/^(?P<name>[^()><@,;:\"\\/\[\]?=}{ \t]+):.*$/', $headerFieldNameOrLine, $matches)
186 && $fieldValue === null) {
187 // is a header
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');
193 } else {
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];
205 return $this;
209 * Add a Header to this container, for raw values @see addHeaderLine() and addHeaders()
211 * @param Header\HeaderInterface $header
212 * @return Headers
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;
223 return $this;
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;
232 return $this;
235 // Otherwise, we replace the current instance.
236 $this->headers[$index] = $header;
238 return $this;
242 * Remove a Header from the container
244 * @param Header\HeaderInterface $header
245 * @return bool
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]);
254 return true;
256 return false;
260 * Clear all headers
262 * Removes all headers from queue
264 * @return Headers
266 public function clearHeaders()
268 $this->headers = $this->headersKeys = [];
269 return $this;
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)) {
282 return false;
285 $class = ($this->getPluginClassLoader()->load(str_replace('-', '', $key))) ?: 'Zend\Http\Header\GenericHeader';
287 if (in_array('Zend\Http\Header\MultipleHeaderInterface', class_implements($class, true))) {
288 $headers = [];
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) {
302 return 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
316 * @return bool
318 public function has($name)
320 return in_array(static::createKey($name), $this->headersKeys);
324 * Advance the pointer for this object as an iterator
326 * @return void
328 public function next()
330 next($this->headers);
334 * Return the current key for this object as an iterator
336 * @return mixed
338 public function key()
340 return (key($this->headers));
344 * Is this iterator still valid?
346 * @return bool
348 public function valid()
350 return (current($this->headers) !== false);
354 * Reset the internal pointer for this object as an iterator
356 * @return void
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));
374 return $current;
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.
394 * @return string
396 public function toString()
398 $headers = '';
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";
405 continue;
407 // Handle single-value headers
408 $headers .= $fieldName . ': ' . $fieldValue . "\r\n";
410 return $headers;
414 * Return the headers container as an array
416 * @todo determine how to produce single line headers, if they are supported
417 * @return array
419 public function toArray()
421 $headers = [];
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();
432 } else {
433 $matches = null;
434 preg_match('/^(?P<name>[^()><@,;:\"\\/\[\]?=}{ \t]+):\s*(?P<value>.*)$/', $header['line'], $matches);
435 if ($matches) {
436 $headers[$matches['name']] = $matches['value'];
440 return $headers;
444 * By calling this, it will force parsing and loading of all headers, after this count() will be accurate
446 * @return bool
448 public function forceLoading()
450 foreach ($this as $item) {
451 // $item should now be loaded
453 return true;
457 * @param $index
458 * @param bool $isGeneric
459 * @return mixed|void
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;
472 try {
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;
483 return $current;
486 $this->headers[$index] = $current = $headers;
487 return $current;
491 * Create array key from header name
493 * @param string $name
494 * @return string
496 protected static function createKey($name)
498 return str_replace(['_', ' ', '.'], '-', strtolower($name));