Passed
Push — master ( 325143...ad3faa )
by Zaahid
03:10
created

PartFilter::setHeaders()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2.032

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 17
ccs 8
cts 10
cp 0.8
rs 9.9
c 0
b 0
f 0
cc 2
nc 1
nop 1
crap 2.032
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
namespace ZBateson\MailMimeParser\Message;
8
9
use ZBateson\MailMimeParser\Message\Part\MessagePart;
10
use ZBateson\MailMimeParser\Message\Part\MimePart;
11
use InvalidArgumentException;
12
13
/**
14
 * Provides a way to define a filter of MessagePart for use in various calls to
15
 * add/remove MessagePart.
16
 * 
17
 * A PartFilter is defined as a set of properties in the class, set to either be
18
 * 'included' or 'excluded'.  The filter is simplistic in that a property
19
 * defined as included must be set on a part for it to be passed, and an
20
 * excluded filter must not be set for the part to be passed.  There is no
21
 * provision for creating logical conditions.
22
 * 
23
 * The only property set by default is $signedpart, which defaults to
24
 * FILTER_EXCLUDE.
25
 * 
26
 * A PartFilter can be instantiated with an array of keys matching class
27
 * properties, and values to set them for convenience.
28
 * 
29
 * ```php
30
 * $inlineParts = $message->getAllParts(new PartFilter([
31
 *     'multipart' => PartFilter::FILTER_INCLUDE,
32
 *     'headers' => [ 
33
 *         FILTER_EXCLUDE => [
34
 *             'Content-Disposition': 'attachment'
35
 *         ]
36
 *     ]
37
 * ]));
38
 * 
39
 * $inlineTextPart = $message->getAllParts(PartFilter::fromInlineContentType('text/plain'));
40
 * ```
41
 *
42
 * @author Zaahid Bateson
43
 */
44
class PartFilter
45
{
46
    /**
47
     * @var int indicates a filter is not in use
48
     */
49
    const FILTER_OFF = 0;
50
    
51
    /**
52
     * @var int an excluded filter must not be included in a part
53
     */
54
    const FILTER_EXCLUDE = 1;
55
    
56
    /**
57
     * @var int an included filter must be included in a part
58
     */
59
    const FILTER_INCLUDE = 2;
60
61
    /**
62
     * @var int filters based on whether MessagePart::hasContent is true
63
     */
64
    private $hascontent = PartFilter::FILTER_OFF;
65
66
    /**
67
     * @var int filters based on whether MimePart::isMultiPart is true
68
     */
69
    private $multipart = PartFilter::FILTER_OFF;
70
    
71
    /**
72
     * @var int filters based on whether MessagePart::isTextPart is true
73
     */
74
    private $textpart = PartFilter::FILTER_OFF;
75
    
76
    /**
77
     * @var int filters based on whether the parent of a part is a
78
     *      multipart/signed part and this part has a content-type equal to its
79
     *      parent's 'protocol' parameter in its content-type header
80
     */
81
    private $signedpart = PartFilter::FILTER_EXCLUDE;
82
    
83
    /**
84
     * @var string calculated hash of the filter
85
     */
86
    private $hashCode;
87
    
88
    /**
89
     * @var string[][] array of header rules.  The top-level contains keys of
90
     * FILTER_INCLUDE and/or FILTER_EXCLUDE, which contain key => value mapping
91
     * of header names => values to search for.  Note that when searching
92
     * MimePart::getHeaderValue is used (so additional parameters need not be
93
     * matched) and strcasecmp is used.
94
     * 
95
     * ```php
96
     * $filter = new PartFilter();
97
     * $filter->headers = [ PartFilter::FILTER_INCLUDE => [ 'Content-Type' => 'text/plain' ] ];
98
     * ```
99
     */
100
    private $headers = [];
101
    
102
    /**
103
     * Convenience method to filter for a specific mime type.
104
     * 
105
     * @param string $mimeType
106
     * @return PartFilter
107
     */
108 3
    public static function fromContentType($mimeType)
109
    {
110 3
        return new static([
111
            'headers' => [
112 3
                static::FILTER_INCLUDE => [
113 3
                    'Content-Type' => $mimeType
114
                ]
115
            ]
116
        ]);
117
    }
118
    
119
    /**
120
     * Convenience method to look for parts of a specific mime-type, and that
121
     * do not specifically have a Content-Disposition equal to 'attachment'.
122
     * 
123
     * @param string $mimeType
124
     * @return PartFilter
125
     */
126 2
    public static function fromInlineContentType($mimeType)
127
    {
128 2
        return new static([
129 2
            'headers' => [
130 2
                static::FILTER_INCLUDE => [
131 2
                    'Content-Type' => $mimeType
132
                ],
133 2
                static::FILTER_EXCLUDE => [
134
                    'Content-Disposition' => 'attachment'
135
                ]
136
            ]
137
        ]);
138
    }
139
    
140
    /**
141
     * Convenience method to search for parts with a specific
142
     * Content-Disposition, optionally including multipart parts.
143
     * 
144
     * @param string $disposition
145
     * @param int $multipart
146
     * @return PartFilter
147
     */
148 6
    public static function fromDisposition($disposition, $multipart = PartFilter::FILTER_OFF)
149
    {
150 6
        return new static([
151 6
            'multipart' => $multipart,
152
            'headers' => [
153 6
                static::FILTER_INCLUDE => [
154 6
                    'Content-Disposition' => $disposition
155
                ]
156
            ]
157
        ]);
158
    }
159
    
160
    /**
161
     * Constructs a PartFilter, optionally instantiating member variables with
162
     * values in the passed array.
163
     * 
164
     * The passed array must use keys equal to member variable names, e.g.
165
     * 'multipart', 'textpart', 'signedpart' and 'headers'.
166
     * 
167
     * @param array $filter
168
     */
169 20
    public function __construct(array $filter = [])
170
    {
171 20
        $params = [ 'hascontent', 'multipart', 'textpart', 'signedpart', 'headers' ];
172 20
        foreach ($params as $param) {
173 20
            if (isset($filter[$param])) {
174 20
                $this->__set($param, $filter[$param]);
175
            }
176
        }
177 20
    }
178
    
179
    /**
180
     * Validates an argument passed to __set to insure it's set to a value in
181
     * $valid.
182
     * 
183
     * @param string $name Name of the member variable
184
     * @param string $value The value to test
185
     * @param array $valid an array of valid values
186
     * @throws InvalidArgumentException
187
     */
188 20
    private function validateArgument($name, $value, array $valid)
189
    {
190 20
        if (!in_array($value, $valid)) {
191
            $last = array_pop($valid);
192
            throw new InvalidArgumentException(
193
                '$value parameter for ' . $name . ' must be one of '
194
                . join(', ', $valid) . ' or ' . $last . ' - "' . $value
195
                . '" provided'
196
            );
197
        }
198 20
    }
199
    
200
    /**
201
     * Sets the PartFilter's headers filter to the passed array after validating
202
     * it.
203
     * 
204
     * @param array $headers
205
     * @throws InvalidArgumentException
206
     */
207
    public function setHeaders(array $headers)
208
    {
209 17
        array_walk($headers, function ($v, $k) {
210 17
            $this->validateArgument(
211 17
                'headers',
212 17
                $k,
213 17
                [ static::FILTER_EXCLUDE, static::FILTER_INCLUDE ]
214
            );
215 17
            if (!is_array($v)) {
216
                throw new InvalidArgumentException(
217
                    '$value must be an array with keys set to FILTER_EXCLUDE, '
218
                    . 'FILTER_INCLUDE and values set to an array of header '
219
                    . 'name => values'
220
                );
221
            }
222 17
        });
223 17
        $this->headers = $headers;
224 17
    }
225
    
226
    /**
227
     * Sets the member variable denoted by $name to the passed $value after
228
     * validating it.
229
     * 
230
     * @param string $name
231
     * @param int|array $value
232
     * @throws InvalidArgumentException
233
     */
234 20
    public function __set($name, $value)
235
    {
236 20
        if ($name === 'hascontent' || $name === 'multipart'
237 20
            || $name === 'textpart' || $name === 'signedpart') {
238 15
            if (is_array($value)) {
239
                throw new InvalidArgumentException('$value must be not be an array');
240
            }
241 15
            $this->validateArgument(
242 15
                $name,
243 15
                $value,
244 15
                [ static::FILTER_OFF, static::FILTER_EXCLUDE, static::FILTER_INCLUDE ]
245
            );
246 15
            $this->$name = $value;
247 17
        } elseif ($name === 'headers') {
248 17
            if (!is_array($value)) {
249
                throw new InvalidArgumentException('$value must be an array');
250
            }
251 17
            $this->setHeaders($value);
252
        }
253 20
    }
254
    
255
    /**
256
     * Returns true if the variable denoted by $name is a member variable of
257
     * PartFilter.
258
     * 
259
     * @param string $name
260
     * @return bool
261
     */
262
    public function __isset($name)
263
    {
264
        return isset($this->$name);
265
    }
266
    
267
    /**
268
     * Returns the value of the member variable denoted by $name
269
     * 
270
     * @param string $name
271
     * @return mixed
272
     */
273 20
    public function __get($name)
274
    {
275 20
        return $this->$name;
276
    }
277
278
    /**
279
     * Returns true if the passed MessagePart fails the filter's hascontent
280
     * filter settings.
281
     *
282
     * @param MessagePart $part
283
     * @return bool
284
     */
285
    private function failsHasContentFilter(MessagePart $part)
286
    {
287
        return ($this->hascontent === static::FILTER_EXCLUDE && $part->hasContent())
288
            || ($this->hascontent === static::FILTER_INCLUDE && !$part->hasContent());
289
    }
290
    
291
    /**
292
     * Returns true if the passed MessagePart fails the filter's multipart
293
     * filter settings.
294
     * 
295
     * @param MessagePart $part
296
     * @return bool
297
     */
298 16
    private function failsMultiPartFilter(MessagePart $part)
299
    {
300 16
        if (!($part instanceof MimePart)) {
301
            return $this->multipart !== static::FILTER_EXCLUDE;
302
        }
303 16
        return ($this->multipart === static::FILTER_EXCLUDE && $part->isMultiPart())
304 16
            || ($this->multipart === static::FILTER_INCLUDE && !$part->isMultiPart());
305
    }
306
    
307
    /**
308
     * Returns true if the passed MessagePart fails the filter's textpart filter
309
     * settings.
310
     * 
311
     * @param MessagePart $part
312
     * @return bool
313
     */
314 16
    private function failsTextPartFilter(MessagePart $part)
315
    {
316 16
        return ($this->textpart === static::FILTER_EXCLUDE && $part->isTextPart())
317 16
            || ($this->textpart === static::FILTER_INCLUDE && !$part->isTextPart());
318
    }
319
    
320
    /**
321
     * Returns true if the passed MessagePart fails the filter's signedpart
322
     * filter settings.
323
     * 
324
     * @param MessagePart $part
325
     * @return boolean
326
     */
327 16
    private function failsSignedPartFilter(MessagePart $part)
328
    {
329 16
        if ($this->signedpart === static::FILTER_OFF) {
330 1
            return false;
331 15
        } elseif (!$part->isMime() || $part->getParent() === null) {
332 15
            return ($this->signedpart === static::FILTER_INCLUDE);
333
        }
334 10
        $partMimeType = $part->getContentType();
335 10
        $parentMimeType = $part->getParent()->getContentType();
336 10
        $parentProtocol = $part->getParent()->getHeaderParameter('Content-Type', 'protocol');
337 10
        if (strcasecmp($parentMimeType, 'multipart/signed') === 0 && strcasecmp($partMimeType, $parentProtocol) === 0) {
338 10
            return ($this->signedpart === static::FILTER_EXCLUDE);
339
        }
340
        return ($this->signedpart === static::FILTER_INCLUDE);
341
    }
342
    
343
    /**
344
     * Tests a single header value against $part, and returns true if the test
345
     * fails.
346
     * 
347
     * @staticvar array $map
348
     * @param MessagePart $part
349
     * @param int $type
350
     * @param string $name
351
     * @param string $header
352
     * @return boolean
353
     */
354 13
    private function failsHeaderFor(MessagePart $part, $type, $name, $header)
355
    {
356 13
        $headerValue = null;
357
        
358 13
        static $map = [
359
            'content-type' => 'getContentType',
360
            'content-disposition' => 'getContentDisposition',
361
            'content-transfer-encoding' => 'getContentTransferEncoding'
362
        ];
363 13
        $lower = strtolower($name);
364 13
        if (isset($map[$lower])) {
365 13
            $headerValue = call_user_func([$part, $map[$lower]]);
366
        } elseif (!($part instanceof MimePart)) {
367
            return ($type === static::FILTER_INCLUDE);
368
        } else {
369
            $headerValue = $part->getHeaderValue($name);
370
        }
371
        
372 13
        return (($type === static::FILTER_EXCLUDE && strcasecmp($headerValue, $header) === 0)
373 13
            || ($type === static::FILTER_INCLUDE && strcasecmp($headerValue, $header) !== 0));
374
    }
375
    
376
    /**
377
     * Returns true if the passed MessagePart fails the filter's header filter
378
     * settings.
379
     * 
380
     * @param MessagePart $part
381
     * @return boolean
382
     */
383 16
    private function failsHeaderPartFilter(MessagePart $part)
384
    {
385 16
        foreach ($this->headers as $type => $values) {
386 13
            foreach ($values as $name => $header) {
387 13
                if ($this->failsHeaderFor($part, $type, $name, $header)) {
388 13
                    return true;
389
                }
390
            }
391
        }
392 14
        return false;
393
    }
394
    
395
    /**
396
     * Determines if the passed MessagePart should be filtered out or not.
397
     * If the MessagePart passes all filter tests, true is returned.  Otherwise
398
     * false is returned.
399
     * 
400
     * @param MessagePart $part
401
     * @return boolean
402
     */
403 16
    public function filter(MessagePart $part)
404
    {
405 16
        return !($this->failsMultiPartFilter($part)
406 16
            || $this->failsTextPartFilter($part)
407 16
            || $this->failsSignedPartFilter($part)
408 16
            || $this->failsHeaderPartFilter($part));
409
    }
410
}
411