Passed
Push — master ( 158433...e258e4 )
by Zaahid
03:43
created

PartHeaderContainer::getByIndex()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3.0123

Importance

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