composer package updates
[openemr.git] / vendor / zendframework / zend-mail / src / Headers.php
blobb8058b97a36af1c1726518486924f0e35d7c9876
1 <?php
2 /**
3 * @see https://github.com/zendframework/zend-mail for the canonical source repository
4 * @copyright Copyright (c) 2005-2018 Zend Technologies USA Inc. (https://www.zend.com)
5 * @license https://github.com/zendframework/zend-mail/blob/master/LICENSE.md New BSD License
6 */
8 namespace Zend\Mail;
10 use ArrayIterator;
11 use Countable;
12 use Iterator;
13 use Traversable;
14 use Zend\Loader\PluginClassLocator;
15 use Zend\Mail\Header\GenericHeader;
16 use Zend\Mail\Header\HeaderInterface;
18 /**
19 * Basic mail headers collection functionality
21 * Handles aggregation of headers
23 class Headers implements Countable, Iterator
25 /** @var string End of Line for fields */
26 const EOL = "\r\n";
28 /** @var string Start of Line when folding */
29 const FOLDING = "\r\n ";
31 /**
32 * @var \Zend\Loader\PluginClassLoader
34 protected $pluginClassLoader = null;
36 /**
37 * @var array key names for $headers array
39 protected $headersKeys = [];
41 /**
42 * @var Header\HeaderInterface[] instances
44 protected $headers = [];
46 /**
47 * Header encoding; defaults to ASCII
49 * @var string
51 protected $encoding = 'ASCII';
53 /**
54 * Populates headers from string representation
56 * Parses a string for headers, and aggregates them, in order, in the
57 * current instance, primarily as strings until they are needed (they
58 * will be lazy loaded)
60 * @param string $string
61 * @param string $EOL EOL string; defaults to {@link EOL}
62 * @throws Exception\RuntimeException
63 * @return Headers
65 public static function fromString($string, $EOL = self::EOL)
67 $headers = new static();
68 $currentLine = '';
69 $emptyLine = 0;
71 // iterate the header lines, some might be continuations
72 $lines = explode($EOL, $string);
73 $total = count($lines);
74 for ($i = 0; $i < $total; $i += 1) {
75 $line = $lines[$i];
77 // Empty line indicates end of headers
78 // EXCEPT if there are more lines, in which case, there's a possible error condition
79 if (preg_match('/^\s*$/', $line)) {
80 $emptyLine += 1;
81 if ($emptyLine > 2) {
82 throw new Exception\RuntimeException('Malformed header detected');
84 continue;
87 if ($emptyLine > 1) {
88 throw new Exception\RuntimeException('Malformed header detected');
91 // check if a header name is present
92 if (preg_match('/^[\x21-\x39\x3B-\x7E]+:.*$/', $line)) {
93 if ($currentLine) {
94 // a header name was present, then store the current complete line
95 $headers->addHeaderLine($currentLine);
97 $currentLine = trim($line);
98 continue;
101 // continuation: append to current line
102 // recover the whitespace that break the line (unfolding, rfc2822#section-2.2.3)
103 if (preg_match('/^\s+.*$/', $line)) {
104 $currentLine .= ' ' . trim($line);
105 continue;
108 // Line does not match header format!
109 throw new Exception\RuntimeException(sprintf(
110 'Line "%s" does not match header format!',
111 $line
114 if ($currentLine) {
115 $headers->addHeaderLine($currentLine);
117 return $headers;
121 * Set an alternate implementation for the PluginClassLoader
123 * @param PluginClassLocator $pluginClassLoader
124 * @return Headers
126 public function setPluginClassLoader(PluginClassLocator $pluginClassLoader)
128 $this->pluginClassLoader = $pluginClassLoader;
129 return $this;
133 * Return an instance of a PluginClassLocator, lazyload and inject map if necessary
135 * @return PluginClassLocator
137 public function getPluginClassLoader()
139 if ($this->pluginClassLoader === null) {
140 $this->pluginClassLoader = new Header\HeaderLoader();
142 return $this->pluginClassLoader;
146 * Set the header encoding
148 * @param string $encoding
149 * @return Headers
151 public function setEncoding($encoding)
153 $this->encoding = $encoding;
154 foreach ($this as $header) {
155 $header->setEncoding($encoding);
157 return $this;
161 * Get the header encoding
163 * @return string
165 public function getEncoding()
167 return $this->encoding;
171 * Add many headers at once
173 * Expects an array (or Traversable object) of type/value pairs.
175 * @param array|Traversable $headers
176 * @throws Exception\InvalidArgumentException
177 * @return Headers
179 public function addHeaders($headers)
181 if (! is_array($headers) && ! $headers instanceof Traversable) {
182 throw new Exception\InvalidArgumentException(sprintf(
183 'Expected array or Traversable; received "%s"',
184 (is_object($headers) ? get_class($headers) : gettype($headers))
188 foreach ($headers as $name => $value) {
189 if (is_int($name)) {
190 if (is_string($value)) {
191 $this->addHeaderLine($value);
192 } elseif (is_array($value) && count($value) == 1) {
193 $this->addHeaderLine(key($value), current($value));
194 } elseif (is_array($value) && count($value) == 2) {
195 $this->addHeaderLine($value[0], $value[1]);
196 } elseif ($value instanceof Header\HeaderInterface) {
197 $this->addHeader($value);
199 } elseif (is_string($name)) {
200 $this->addHeaderLine($name, $value);
204 return $this;
208 * Add a raw header line, either in name => value, or as a single string 'name: value'
210 * This method allows for lazy-loading in that the parsing and instantiation of HeaderInterface object
211 * will be delayed until they are retrieved by either get() or current()
213 * @throws Exception\InvalidArgumentException
214 * @param string $headerFieldNameOrLine
215 * @param string $fieldValue optional
216 * @return Headers
218 public function addHeaderLine($headerFieldNameOrLine, $fieldValue = null)
220 if (! is_string($headerFieldNameOrLine)) {
221 throw new Exception\InvalidArgumentException(sprintf(
222 '%s expects its first argument to be a string; received "%s"',
223 __METHOD__,
224 (is_object($headerFieldNameOrLine)
225 ? get_class($headerFieldNameOrLine)
226 : gettype($headerFieldNameOrLine))
230 if ($fieldValue === null) {
231 $headers = $this->loadHeader($headerFieldNameOrLine);
232 $headers = is_array($headers) ? $headers : [$headers];
233 foreach ($headers as $header) {
234 $this->addHeader($header);
236 } elseif (is_array($fieldValue)) {
237 foreach ($fieldValue as $i) {
238 $this->addHeader(Header\GenericMultiHeader::fromString($headerFieldNameOrLine . ':' . $i));
240 } else {
241 $this->addHeader(Header\GenericHeader::fromString($headerFieldNameOrLine . ':' . $fieldValue));
244 return $this;
248 * Add a Header\Interface to this container, for raw values see {@link addHeaderLine()} and {@link addHeaders()}
250 * @param Header\HeaderInterface $header
251 * @return Headers
253 public function addHeader(Header\HeaderInterface $header)
255 $key = $this->normalizeFieldName($header->getFieldName());
256 $this->headersKeys[] = $key;
257 $this->headers[] = $header;
258 if ($this->getEncoding() !== 'ASCII') {
259 $header->setEncoding($this->getEncoding());
261 return $this;
265 * Remove a Header from the container
267 * @param string|Header\HeaderInterface field name or specific header instance to remove
268 * @return bool
270 public function removeHeader($instanceOrFieldName)
272 if ($instanceOrFieldName instanceof Header\HeaderInterface) {
273 $indexes = array_keys($this->headers, $instanceOrFieldName, true);
274 } else {
275 $key = $this->normalizeFieldName($instanceOrFieldName);
276 $indexes = array_keys($this->headersKeys, $key, true);
279 if (! empty($indexes)) {
280 foreach ($indexes as $index) {
281 unset($this->headersKeys[$index]);
282 unset($this->headers[$index]);
284 return true;
287 return false;
291 * Clear all headers
293 * Removes all headers from queue
295 * @return Headers
297 public function clearHeaders()
299 $this->headers = $this->headersKeys = [];
300 return $this;
304 * Get all headers of a certain name/type
306 * @param string $name
307 * @return bool|ArrayIterator|Header\HeaderInterface Returns false if there is no headers with $name in this
308 * contain, an ArrayIterator if the header is a MultipleHeadersInterface instance and finally returns
309 * HeaderInterface for the rest of cases.
311 public function get($name)
313 $key = $this->normalizeFieldName($name);
314 $results = [];
316 foreach (array_keys($this->headersKeys, $key) as $index) {
317 if ($this->headers[$index] instanceof Header\GenericHeader) {
318 $results[] = $this->lazyLoadHeader($index);
319 } else {
320 $results[] = $this->headers[$index];
324 switch (count($results)) {
325 case 0:
326 return false;
327 case 1:
328 if ($results[0] instanceof Header\MultipleHeadersInterface) {
329 return new ArrayIterator($results);
330 } else {
331 return $results[0];
333 //fall-trough
334 default:
335 return new ArrayIterator($results);
340 * Test for existence of a type of header
342 * @param string $name
343 * @return bool
345 public function has($name)
347 $name = $this->normalizeFieldName($name);
348 return in_array($name, $this->headersKeys);
352 * Advance the pointer for this object as an iterator
355 public function next()
357 next($this->headers);
361 * Return the current key for this object as an iterator
363 * @return mixed
365 public function key()
367 return key($this->headers);
371 * Is this iterator still valid?
373 * @return bool
375 public function valid()
377 return (current($this->headers) !== false);
381 * Reset the internal pointer for this object as an iterator
384 public function rewind()
386 reset($this->headers);
390 * Return the current value for this iterator, lazy loading it if need be
392 * @return Header\HeaderInterface
394 public function current()
396 $current = current($this->headers);
397 if ($current instanceof Header\GenericHeader) {
398 $current = $this->lazyLoadHeader(key($this->headers));
400 return $current;
404 * Return the number of headers in this contain, if all headers have not been parsed, actual count could
405 * increase if MultipleHeader objects exist in the Request/Response. If you need an exact count, iterate
407 * @return int count of currently known headers
409 public function count()
411 return count($this->headers);
415 * Render all headers at once
417 * This method handles the normal iteration of headers; it is up to the
418 * concrete classes to prepend with the appropriate status/request line.
420 * @return string
422 public function toString()
424 $headers = '';
425 foreach ($this as $header) {
426 if ($str = $header->toString()) {
427 $headers .= $str . self::EOL;
431 return $headers;
435 * Return the headers container as an array
437 * @param bool $format Return the values in Mime::Encoded or in Raw format
438 * @return array
439 * @todo determine how to produce single line headers, if they are supported
441 public function toArray($format = Header\HeaderInterface::FORMAT_RAW)
443 $headers = [];
444 /* @var $header Header\HeaderInterface */
445 foreach ($this->headers as $header) {
446 if ($header instanceof Header\MultipleHeadersInterface) {
447 $name = $header->getFieldName();
448 if (! isset($headers[$name])) {
449 $headers[$name] = [];
451 $headers[$name][] = $header->getFieldValue($format);
452 } else {
453 $headers[$header->getFieldName()] = $header->getFieldValue($format);
456 return $headers;
460 * By calling this, it will force parsing and loading of all headers, after this count() will be accurate
462 * @return bool
464 public function forceLoading()
466 foreach ($this as $item) {
467 // $item should now be loaded
469 return true;
473 * Create Header object from header line
475 * @param string $headerLine
476 * @return Header\HeaderInterface|Header\HeaderInterface[]
478 public function loadHeader($headerLine)
480 list($name, ) = Header\GenericHeader::splitHeaderLine($headerLine);
482 /** @var HeaderInterface $class */
483 $class = $this->getPluginClassLoader()->load($name) ?: Header\GenericHeader::class;
484 return $class::fromString($headerLine);
488 * @param $index
489 * @return mixed
491 protected function lazyLoadHeader($index)
493 $current = $this->headers[$index];
495 $key = $this->headersKeys[$index];
497 /** @var GenericHeader $class */
498 $class = ($this->getPluginClassLoader()->load($key)) ?: 'Zend\Mail\Header\GenericHeader';
500 $encoding = $current->getEncoding();
501 $headers = $class::fromString($current->toString());
502 if (is_array($headers)) {
503 $current = array_shift($headers);
504 $current->setEncoding($encoding);
505 $this->headers[$index] = $current;
506 foreach ($headers as $header) {
507 $header->setEncoding($encoding);
508 $this->headersKeys[] = $key;
509 $this->headers[] = $header;
511 return $current;
514 $current = $headers;
515 $current->setEncoding($encoding);
516 $this->headers[$index] = $current;
517 return $current;
521 * Normalize a field name
523 * @param string $fieldName
524 * @return string
526 protected function normalizeFieldName($fieldName)
528 return str_replace(['-', '_', ' ', '.'], '', strtolower($fieldName));