Passed
Push — master ( a626ba...b864c0 )
by Zaahid
15:44 queued 12:03
created

PartHeaderContainer::getAll()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.0078

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 10
ccs 7
cts 8
cp 0.875
rs 10
cc 2
nc 2
nop 1
crap 2.0078
1
<?php
2
/**
3
 * This file is part of the ZBateson\MailMimeParser project.
4
 *
5
 * @license http://opensource.org/licenses/bsd-license.php BSD
6
 */
7
8
namespace ZBateson\MailMimeParser\Message;
9
10
use ArrayIterator;
11
use IteratorAggregate;
12
use Traversable;
13
use Psr\Log\LoggerInterface;
14
use ZBateson\MailMimeParser\ErrorBag;
15
use ZBateson\MailMimeParser\Header\HeaderFactory;
16
use ZBateson\MailMimeParser\Header\IHeader;
17
18
/**
19
 * Maintains a collection of headers for a part.
20
 *
21
 * @author Zaahid Bateson
22
 */
23
class PartHeaderContainer extends ErrorBag implements IteratorAggregate
24
{
25
    /**
26
     * @var HeaderFactory the HeaderFactory object used for created headers
27
     */
28
    protected $headerFactory;
29
30
    /**
31
     * @var string[][] Each element in the array is an array with its first
32
     * element set to the header's name, and the second its value.
33
     */
34
    private $headers = [];
35
36
    /**
37
     * @var \ZBateson\MailMimeParser\Header\IHeader[] Each element is an IHeader
38
     *      representing the header at the same index in the $headers array.  If
39
     *      an IHeader has not been constructed for the header at that index,
40
     *      the element would be set to null.
41
     */
42
    private $headerObjects = [];
43
44
    /**
45
     * @var array Maps header names by their "normalized" (lower-cased,
46
     *      non-alphanumeric characters stripped) name to an array of indexes in
47
     *      the $headers array.  For example:
48
     *      $headerMap['contenttype'] = [ 1, 4 ]
49
     *      would indicate that the headers in $headers[1] and $headers[4] are
50
     *      both headers with the name 'Content-Type' or 'contENTtype'.
51
     */
52
    private $headerMap = [];
53
54
    /**
55
     * @var int the next index to use for $headers and $headerObjects.
56
     */
57
    private $nextIndex = 0;
58
59
    /**
60
     * Pass a PartHeaderContainer as the second parameter.  This is useful when
61
     * creating a new MimePart with this PartHeaderContainer and the original
62
     * container is needed for parsing and changes to the header in the part
63
     * should not affect parsing.
64
     *
65
     * @param PartHeaderContainer $cloneSource the original container to clone
66
     *        from
67
     */
68 112
    public function __construct(
69
        LoggerInterface $logger,
70
        HeaderFactory $headerFactory,
71
        ?PartHeaderContainer $cloneSource = null
72
    ) {
73 112
        parent::__construct($logger);
74 112
        $this->headerFactory = $headerFactory;
75 112
        if ($cloneSource !== null) {
76 107
            $this->headers = $cloneSource->headers;
77 107
            $this->headerObjects = $cloneSource->headerObjects;
78 107
            $this->headerMap = $cloneSource->headerMap;
79 107
            $this->nextIndex = $cloneSource->nextIndex;
80
        }
81
    }
82
83
    /**
84
     * Returns true if the passed header exists in this collection.
85
     */
86 112
    public function exists(string $name, int $offset = 0) : bool
87
    {
88 112
        $s = $this->headerFactory->getNormalizedHeaderName($name);
89 112
        return isset($this->headerMap[$s][$offset]);
90
    }
91
92
    /**
93
     * Returns an array of header indexes with names that more closely match
94
     * the passed $name if available: for instance if there are two headers in
95
     * an email, "Content-Type" and "ContentType", and the query is for a header
96
     * with the name "Content-Type", only headers that match exactly
97
     * "Content-Type" would be returned.
98
     *
99
     * @return int[]|null
100
     */
101 112
    private function getAllWithOriginalHeaderNameIfSet(string $name) : ?array
102
    {
103 112
        $s = $this->headerFactory->getNormalizedHeaderName($name);
104 112
        if (isset($this->headerMap[$s])) {
105 112
            $self = $this;
106 112
            $filtered = \array_filter($this->headerMap[$s], function($h) use ($name, $self) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
107 112
                return (\strcasecmp($self->headers[$h][0], $name) === 0);
108 112
            });
109 112
            return (!empty($filtered)) ? $filtered : $this->headerMap[$s];
110
        }
111 106
        return null;
112
    }
113
114
    /**
115
     * Returns the IHeader object for the header with the given $name, or null
116
     * if none exist.
117
     *
118
     * An optional offset can be provided, which defaults to the first header in
119
     * the collection when more than one header with the same name exists.
120
     *
121
     * Note that mime headers aren't case sensitive.
122
     */
123 112
    public function get(string $name, int $offset = 0) : ?IHeader
124
    {
125 112
        $a = $this->getAllWithOriginalHeaderNameIfSet($name);
126 112
        if (!empty($a) && isset($a[$offset])) {
127 112
            return $this->getByIndex($a[$offset]);
128
        }
129 108
        return null;
130
    }
131
132
    /**
133
     * Returns the IHeader object for the header with the given $name, or null
134
     * if none exist, using the passed $iHeaderClass to construct it.
135
     *
136
     * An optional offset can be provided, which defaults to the first header in
137
     * the collection when more than one header with the same name exists.
138
     *
139
     * Note that mime headers aren't case sensitive.
140
     */
141 1
    public function getAs(string $name, string $iHeaderClass, int $offset = 0) : ?IHeader
142
    {
143 1
        $a = $this->getAllWithOriginalHeaderNameIfSet($name);
144 1
        if (!empty($a) && isset($a[$offset])) {
145 1
            return $this->getByIndexAs($a[$offset], $iHeaderClass);
146
        }
147
        return null;
148
    }
149
150
    /**
151
     * Returns all headers with the passed name.
152
     *
153
     * @return IHeader[]
154
     */
155 2
    public function getAll(string $name) : array
156
    {
157 2
        $a = $this->getAllWithOriginalHeaderNameIfSet($name);
158 2
        if (!empty($a)) {
159 2
            $self = $this;
160 2
            return \array_map(function($index) use ($self) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FUNCTION keyword; 0 found
Loading history...
161 2
                return $self->getByIndex($index);
162 2
            }, $a);
163
        }
164
        return [];
165
    }
166
167
    /**
168
     * Returns the header in the headers array at the passed 0-based integer
169
     * index or null if one doesn't exist.
170
     */
171 112
    private function getByIndex(int $index) : ?IHeader
172
    {
173 112
        if (!isset($this->headers[$index])) {
174
            return null;
175
        }
176 112
        if ($this->headerObjects[$index] === null) {
177 112
            $this->headerObjects[$index] = $this->headerFactory->newInstance(
178 112
                $this->headers[$index][0],
179 112
                $this->headers[$index][1]
180 112
            );
181
        }
182 112
        return $this->headerObjects[$index];
183
    }
184
185
    /**
186
     * Returns the header in the headers array at the passed 0-based integer
187
     * index or null if one doesn't exist, using the passed $iHeaderClass to
188
     * construct it.
189
     */
190 1
    private function getByIndexAs(int $index, string $iHeaderClass) : ?IHeader
191
    {
192 1
        if (!isset($this->headers[$index])) {
193
            return null;
194
        }
195 1
        if ($this->headerObjects[$index] !== null && \get_class($this->headerObjects[$index]) === $iHeaderClass) {
196
            return $this->headerObjects[$index];
197
        }
198 1
        return $this->headerFactory->newInstanceOf(
199 1
            $this->headers[$index][0],
200 1
            $this->headers[$index][1],
201 1
            $iHeaderClass
202 1
        );
203
    }
204
205
    /**
206
     * Removes the header from the collection with the passed name.  Defaults to
207
     * removing the first instance of the header for a collection that contains
208
     * more than one with the same passed name.
209
     *
210
     * @return bool true if a header was found and removed.
211
     */
212 2
    public function remove(string $name, int $offset = 0) : bool
213
    {
214 2
        $s = $this->headerFactory->getNormalizedHeaderName($name);
215 2
        if (isset($this->headerMap[$s][$offset])) {
216 2
            $index = $this->headerMap[$s][$offset];
217 2
            \array_splice($this->headerMap[$s], $offset, 1);
218 2
            unset($this->headers[$index], $this->headerObjects[$index]);
219 2
            return true;
220
        }
221
        return false;
222
    }
223
224
    /**
225
     * Removes all headers that match the passed name.
226
     *
227
     * @return bool true if one or more headers were removed.
228
     */
229 19
    public function removeAll(string $name) : bool
230
    {
231 19
        $s = $this->headerFactory->getNormalizedHeaderName($name);
232 19
        if (!empty($this->headerMap[$s])) {
233 19
            foreach ($this->headerMap[$s] as $i) {
234 19
                unset($this->headers[$i], $this->headerObjects[$i]);
235
            }
236 19
            $this->headerMap[$s] = [];
237 19
            return true;
238
        }
239
        return false;
240
    }
241
242
    /**
243
     * Adds the header to the collection.
244
     */
245 112
    public function add(string $name, string $value) : static
246
    {
247 112
        $s = $this->headerFactory->getNormalizedHeaderName($name);
248 112
        $this->headers[$this->nextIndex] = [$name, $value];
249 112
        $this->headerObjects[$this->nextIndex] = null;
250 112
        if (!isset($this->headerMap[$s])) {
251 112
            $this->headerMap[$s] = [];
252
        }
253 112
        $this->headerMap[$s][] = $this->nextIndex;
254 112
        $this->nextIndex++;
255 112
        return $this;
256
    }
257
258
    /**
259
     * If a header exists with the passed name, and at the passed offset if more
260
     * than one exists, its value is updated.
261
     *
262
     * If a header with the passed name doesn't exist at the passed offset, it
263
     * is created at the next available offset (offset is ignored when adding).
264
     */
265 25
    public function set(string $name, string $value, int $offset = 0) : static
266
    {
267 25
        $s = $this->headerFactory->getNormalizedHeaderName($name);
268 25
        if (!isset($this->headerMap[$s][$offset])) {
269 22
            $this->add($name, $value);
270 22
            return $this;
271
        }
272 15
        $i = $this->headerMap[$s][$offset];
273 15
        $this->headers[$i] = [$name, $value];
274 15
        $this->headerObjects[$i] = null;
275 15
        return $this;
276
    }
277
278
    /**
279
     * Returns an array of IHeader objects representing all headers in this
280
     * collection.
281
     *
282
     * @return IHeader[]
283
     */
284 19
    public function getHeaderObjects() : array
285
    {
286 19
        return \array_filter(\array_map([$this, 'getByIndex'], \array_keys($this->headers)));
287
    }
288
289
    /**
290
     * Returns an array of headers in this collection.  Each returned element in
291
     * the array is an array with the first element set to the name, and the
292
     * second its value:
293
     *
294
     * [
295
     *     [ 'Header-Name', 'Header Value' ],
296
     *     [ 'Second-Header-Name', 'Second-Header-Value' ],
297
     *     // etc...
298
     * ]
299
     *
300
     * @return string[][]
301
     */
302 99
    public function getHeaders() : array
303
    {
304 99
        return \array_values(\array_filter($this->headers));
305
    }
306
307
    /**
308
     * Returns an iterator to the headers in this collection.  Each returned
309
     * element is an array with its first element set to the header's name, and
310
     * the second to its value:
311
     *
312
     * [ 'Header-Name', 'Header Value' ]
313
     *
314
     * return Traversable<array<string>>
315
     */
316 94
    public function getIterator() : Traversable
317
    {
318 94
        return new ArrayIterator($this->getHeaders());
319
    }
320
321 1
    public function getErrorBagChildren() : array
322
    {
323 1
        return \array_values(\array_filter($this->headerObjects));
324
    }
325
}
326