mirror of
https://github.com/itflow-org/itflow
synced 2026-09-14 10:45:12 +00:00
323 lines
10 KiB
PHP
323 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* This file is part of the ZBateson\MailMimeParser project.
|
|
*
|
|
* @license http://opensource.org/licenses/bsd-license.php BSD
|
|
*/
|
|
|
|
namespace ZBateson\MailMimeParser\Message;
|
|
|
|
use ArrayIterator;
|
|
use IteratorAggregate;
|
|
use Psr\Log\LoggerInterface;
|
|
use Traversable;
|
|
use ZBateson\MailMimeParser\ErrorBag;
|
|
use ZBateson\MailMimeParser\Header\HeaderFactory;
|
|
use ZBateson\MailMimeParser\Header\IHeader;
|
|
|
|
/**
|
|
* Maintains a collection of headers for a part.
|
|
*
|
|
* @implements IteratorAggregate<int, array<string>>
|
|
*
|
|
* @author Zaahid Bateson
|
|
*/
|
|
class PartHeaderContainer extends ErrorBag implements IteratorAggregate
|
|
{
|
|
/**
|
|
* @var HeaderFactory the HeaderFactory object used for created headers
|
|
*/
|
|
protected HeaderFactory $headerFactory;
|
|
|
|
/**
|
|
* @var string[][] Each element in the array is an array with its first
|
|
* element set to the header's name, and the second its value.
|
|
*/
|
|
private array $headers = [];
|
|
|
|
/**
|
|
* @var array<int, ?IHeader> Each element is an IHeader
|
|
* representing the header at the same index in the $headers array. If
|
|
* an IHeader has not been constructed for the header at that index,
|
|
* the element would be set to null.
|
|
*/
|
|
private array $headerObjects = [];
|
|
|
|
/**
|
|
* @var array<string, int[]> Maps header names by their "normalized" (lower-cased,
|
|
* non-alphanumeric characters stripped) name to an array of indexes in
|
|
* the $headers array. For example:
|
|
* $headerMap['contenttype'] = [ 1, 4 ]
|
|
* would indicate that the headers in $headers[1] and $headers[4] are
|
|
* both headers with the name 'Content-Type' or 'contENTtype'.
|
|
*/
|
|
private array $headerMap = [];
|
|
|
|
/**
|
|
* @var int the next index to use for $headers and $headerObjects.
|
|
*/
|
|
private int $nextIndex = 0;
|
|
|
|
/**
|
|
* Pass a PartHeaderContainer as the second parameter. This is useful when
|
|
* creating a new MimePart with this PartHeaderContainer and the original
|
|
* container is needed for parsing and changes to the header in the part
|
|
* should not affect parsing.
|
|
*
|
|
* @param PartHeaderContainer $cloneSource the original container to clone
|
|
* from
|
|
*/
|
|
public function __construct(
|
|
LoggerInterface $logger,
|
|
HeaderFactory $headerFactory,
|
|
?PartHeaderContainer $cloneSource = null
|
|
) {
|
|
parent::__construct($logger);
|
|
$this->headerFactory = $headerFactory;
|
|
if ($cloneSource !== null) {
|
|
$this->headers = $cloneSource->headers;
|
|
$this->headerObjects = $cloneSource->headerObjects;
|
|
$this->headerMap = $cloneSource->headerMap;
|
|
$this->nextIndex = $cloneSource->nextIndex;
|
|
$this->copyErrorsFrom($cloneSource);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if the passed header exists in this collection.
|
|
*/
|
|
public function exists(string $name, int $offset = 0) : bool
|
|
{
|
|
$s = $this->headerFactory->getNormalizedHeaderName($name);
|
|
return isset($this->headerMap[$s][$offset]);
|
|
}
|
|
|
|
/**
|
|
* Returns an array of header indexes with names that more closely match
|
|
* the passed $name if available: for instance if there are two headers in
|
|
* an email, "Content-Type" and "ContentType", and the query is for a header
|
|
* with the name "Content-Type", only headers that match exactly
|
|
* "Content-Type" would be returned.
|
|
*
|
|
* @return int[]|null
|
|
*/
|
|
private function getAllWithOriginalHeaderNameIfSet(string $name) : ?array
|
|
{
|
|
$s = $this->headerFactory->getNormalizedHeaderName($name);
|
|
if (isset($this->headerMap[$s])) {
|
|
$filtered = \array_filter($this->headerMap[$s], fn($h) => \strcasecmp($this->headers[$h][0], $name) === 0);
|
|
return (!empty($filtered)) ? $filtered : $this->headerMap[$s];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the IHeader object for the header with the given $name, or null
|
|
* if none exist.
|
|
*
|
|
* An optional offset can be provided, which defaults to the first header in
|
|
* the collection when more than one header with the same name exists.
|
|
*
|
|
* Note that mime headers aren't case sensitive.
|
|
*/
|
|
public function get(string $name, int $offset = 0) : ?IHeader
|
|
{
|
|
$a = $this->getAllWithOriginalHeaderNameIfSet($name);
|
|
if (!empty($a) && isset($a[$offset])) {
|
|
return $this->getByIndex($a[$offset]);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns the IHeader object for the header with the given $name, or null
|
|
* if none exist, using the passed $iHeaderClass to construct it.
|
|
*
|
|
* An optional offset can be provided, which defaults to the first header in
|
|
* the collection when more than one header with the same name exists.
|
|
*
|
|
* Note that mime headers aren't case sensitive.
|
|
*/
|
|
public function getAs(string $name, string $iHeaderClass, int $offset = 0) : ?IHeader
|
|
{
|
|
$a = $this->getAllWithOriginalHeaderNameIfSet($name);
|
|
if (!empty($a) && isset($a[$offset])) {
|
|
return $this->getByIndexAs($a[$offset], $iHeaderClass);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns all headers with the passed name.
|
|
*
|
|
* @return IHeader[]
|
|
*/
|
|
public function getAll(string $name) : array
|
|
{
|
|
$a = $this->getAllWithOriginalHeaderNameIfSet($name);
|
|
if (!empty($a)) {
|
|
return \array_map(fn($index) => $this->getByIndex($index), $a);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Returns the header in the headers array at the passed 0-based integer
|
|
* index or null if one doesn't exist.
|
|
*/
|
|
private function getByIndex(int $index) : ?IHeader
|
|
{
|
|
if (!isset($this->headers[$index])) {
|
|
return null;
|
|
}
|
|
if ($this->headerObjects[$index] === null) {
|
|
$this->headerObjects[$index] = $this->headerFactory->newInstance(
|
|
$this->headers[$index][0],
|
|
$this->headers[$index][1]
|
|
);
|
|
}
|
|
return $this->headerObjects[$index];
|
|
}
|
|
|
|
/**
|
|
* Returns the header in the headers array at the passed 0-based integer
|
|
* index or null if one doesn't exist, using the passed $iHeaderClass to
|
|
* construct it.
|
|
*/
|
|
private function getByIndexAs(int $index, string $iHeaderClass) : ?IHeader
|
|
{
|
|
if (!isset($this->headers[$index])) {
|
|
return null;
|
|
}
|
|
if ($this->headerObjects[$index] !== null && \get_class($this->headerObjects[$index]) === $iHeaderClass) {
|
|
return $this->headerObjects[$index];
|
|
}
|
|
return $this->headerFactory->newInstanceOf(
|
|
$this->headers[$index][0],
|
|
$this->headers[$index][1],
|
|
$iHeaderClass
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Removes the header from the collection with the passed name. Defaults to
|
|
* removing the first instance of the header for a collection that contains
|
|
* more than one with the same passed name.
|
|
*
|
|
* @return bool true if a header was found and removed.
|
|
*/
|
|
public function remove(string $name, int $offset = 0) : bool
|
|
{
|
|
$s = $this->headerFactory->getNormalizedHeaderName($name);
|
|
if (isset($this->headerMap[$s][$offset])) {
|
|
$index = $this->headerMap[$s][$offset];
|
|
\array_splice($this->headerMap[$s], $offset, 1);
|
|
unset($this->headers[$index], $this->headerObjects[$index]);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Removes all headers that match the passed name.
|
|
*
|
|
* @return bool true if one or more headers were removed.
|
|
*/
|
|
public function removeAll(string $name) : bool
|
|
{
|
|
$s = $this->headerFactory->getNormalizedHeaderName($name);
|
|
if (!empty($this->headerMap[$s])) {
|
|
foreach ($this->headerMap[$s] as $i) {
|
|
unset($this->headers[$i], $this->headerObjects[$i]);
|
|
}
|
|
$this->headerMap[$s] = [];
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Adds the header to the collection.
|
|
*/
|
|
public function add(string $name, string $value) : static
|
|
{
|
|
$s = $this->headerFactory->getNormalizedHeaderName($name);
|
|
$this->headers[$this->nextIndex] = [$name, $value];
|
|
$this->headerObjects[$this->nextIndex] = null;
|
|
if (!isset($this->headerMap[$s])) {
|
|
$this->headerMap[$s] = [];
|
|
}
|
|
$this->headerMap[$s][] = $this->nextIndex;
|
|
$this->nextIndex++;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* If a header exists with the passed name, and at the passed offset if more
|
|
* than one exists, its value is updated.
|
|
*
|
|
* If a header with the passed name doesn't exist at the passed offset, it
|
|
* is created at the next available offset (offset is ignored when adding).
|
|
*/
|
|
public function set(string $name, string $value, int $offset = 0) : static
|
|
{
|
|
$s = $this->headerFactory->getNormalizedHeaderName($name);
|
|
if (!isset($this->headerMap[$s][$offset])) {
|
|
$this->add($name, $value);
|
|
return $this;
|
|
}
|
|
$i = $this->headerMap[$s][$offset];
|
|
$this->headers[$i] = [$name, $value];
|
|
$this->headerObjects[$i] = null;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of IHeader objects representing all headers in this
|
|
* collection.
|
|
*
|
|
* @return IHeader[]
|
|
*/
|
|
public function getHeaderObjects() : array
|
|
{
|
|
return \array_filter(\array_map($this->getByIndex(...), \array_keys($this->headers)));
|
|
}
|
|
|
|
/**
|
|
* Returns an array of headers in this collection. Each returned element in
|
|
* the array is an array with the first element set to the name, and the
|
|
* second its value:
|
|
*
|
|
* [
|
|
* [ 'Header-Name', 'Header Value' ],
|
|
* [ 'Second-Header-Name', 'Second-Header-Value' ],
|
|
* // etc...
|
|
* ]
|
|
*
|
|
* @return string[][]
|
|
*/
|
|
public function getHeaders() : array
|
|
{
|
|
return \array_values(\array_filter($this->headers));
|
|
}
|
|
|
|
/**
|
|
* Returns an iterator to the headers in this collection. Each returned
|
|
* element is an array with its first element set to the header's name, and
|
|
* the second to its value:
|
|
*
|
|
* [ 'Header-Name', 'Header Value' ]
|
|
*
|
|
* @return Traversable<int, array<string>>
|
|
*/
|
|
public function getIterator() : Traversable
|
|
{
|
|
return new ArrayIterator($this->getHeaders());
|
|
}
|
|
|
|
public function getErrorBagChildren() : array
|
|
{
|
|
return \array_values(\array_filter($this->headerObjects));
|
|
}
|
|
}
|