Passed
Push — master ( 16bd50...cbcb00 )
by Zaahid
03:19
created

PartHeaderContainer::getByIndexAs()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.1755

Importance

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