Passed
Push — 1.0.0 ( 1b586d...185e59 )
by Zaahid
03:51
created

PartFilter::failsHasContentFilter()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 2
nc 5
nop 1
dl 0
loc 4
ccs 0
cts 3
cp 0
crap 20
rs 10
c 0
b 0
f 0
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 MimeParts for use in various calls to
15
 * add/remove MimeParts.
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
            $this->validateArgument(
239 15
                $name,
240 15
                $value,
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type array; however, parameter $value of ZBateson\MailMimeParser\...ter::validateArgument() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

240
                /** @scrutinizer ignore-type */ $value,
Loading history...
241 15
                [ static::FILTER_OFF, static::FILTER_EXCLUDE, static::FILTER_INCLUDE ]
242
            );
243 15
            $this->$name = $value;
244 17
        } elseif ($name === 'headers') {
245 17
            if (!is_array($value)) {
246
                throw new InvalidArgumentException('$value must be an array');
247
            }
248 17
            $this->setHeaders($value);
249
        }
250 20
    }
251
    
252
    /**
253
     * Returns true if the variable denoted by $name is a member variable of
254
     * PartFilter.
255
     * 
256
     * @param string $name
257
     * @return bool
258
     */
259
    public function __isset($name)
260
    {
261
        return isset($this->$name);
262
    }
263
    
264
    /**
265
     * Returns the value of the member variable denoted by $name
266
     * 
267
     * @param string $name
268
     * @return mixed
269
     */
270 20
    public function __get($name)
271
    {
272 20
        return $this->$name;
273
    }
274
275
    /**
276
     * Returns true if the passed MessagePart fails the filter's hascontent
277
     * filter settings.
278
     *
279
     * @param MessagePart $part
280
     * @return bool
281
     */
282
    private function failsHasContentFilter(MessagePart $part)
283
    {
284
        return ($this->hascontent === static::FILTER_EXCLUDE && $part->hasContent())
285
            || ($this->hascontent === static::FILTER_INCLUDE && !$part->hasContent());
286
    }
287
    
288
    /**
289
     * Returns true if the passed MessagePart fails the filter's multipart filter
290
     * settings.
291
     * 
292
     * @param MessagePart $part
293
     * @return bool
294
     */
295 16
    private function failsMultiPartFilter(MessagePart $part)
296
    {
297 16
        if (!($part instanceof MimePart)) {
298
            return $this->multipart !== static::FILTER_EXCLUDE;
299
        }
300 16
        return ($this->multipart === static::FILTER_EXCLUDE && $part->isMultiPart())
301 16
            || ($this->multipart === static::FILTER_INCLUDE && !$part->isMultiPart());
302
    }
303
    
304
    /**
305
     * Returns true if the passed MessagePart fails the filter's textpart filter
306
     * settings.
307
     * 
308
     * @param MessagePart $part
309
     * @return bool
310
     */
311 16
    private function failsTextPartFilter(MessagePart $part)
312
    {
313 16
        return ($this->textpart === static::FILTER_EXCLUDE && $part->isTextPart())
314 16
            || ($this->textpart === static::FILTER_INCLUDE && !$part->isTextPart());
315
    }
316
    
317
    /**
318
     * Returns true if the passed MessagePart fails the filter's signedpart
319
     * filter settings.
320
     * 
321
     * @param MessagePart $part
322
     * @return boolean
323
     */
324 16
    private function failsSignedPartFilter(MessagePart $part)
325
    {
326 16
        if ($this->signedpart === static::FILTER_OFF) {
327 1
            return false;
328 15
        } elseif (!$part->isMime() || $part->getParent() === null) {
329 15
            return ($this->signedpart === static::FILTER_INCLUDE);
330
        }
331 10
        $partMimeType = $part->getContentType();
332 10
        $parentMimeType = $part->getParent()->getContentType();
333 10
        $parentProtocol = $part->getParent()->getHeaderParameter('Content-Type', 'protocol');
334 10
        if (strcasecmp($parentMimeType, 'multipart/signed') === 0 && strcasecmp($partMimeType, $parentProtocol) === 0) {
335 10
            return ($this->signedpart === static::FILTER_EXCLUDE);
336
        }
337
        return ($this->signedpart === static::FILTER_INCLUDE);
338
    }
339
    
340
    /**
341
     * Tests a single header value against $part, and returns true if the test
342
     * fails.
343
     * 
344
     * @staticvar array $map
345
     * @param MimePart $part
346
     * @param int $type
347
     * @param string $name
348
     * @param string $header
349
     * @return boolean
350
     */
351 13
    private function failsHeaderFor($part, $type, $name, $header)
352
    {
353 13
        $headerValue = null;
354
        
355 13
        static $map = [
356
            'content-type' => 'getContentType',
357
            'content-disposition' => 'getContentDisposition',
358
            'content-transfer-encoding' => 'getContentTransferEncoding'
359
        ];
360 13
        $lower = strtolower($name);
361 13
        if (isset($map[$lower])) {
362 13
            $headerValue = call_user_func([$part, $map[$lower]]);
363
        } elseif (!($part instanceof MimePart)) {
0 ignored issues
show
introduced by
$part is always a sub-type of ZBateson\MailMimeParser\Message\Part\MimePart.
Loading history...
364
            return ($type === static::FILTER_INCLUDE);
365
        } else {
366
            $headerValue = $part->getHeaderValue($name);
367
        }
368
        
369 13
        return (($type === static::FILTER_EXCLUDE && strcasecmp($headerValue, $header) === 0)
370 13
            || ($type === static::FILTER_INCLUDE && strcasecmp($headerValue, $header) !== 0));
371
    }
372
    
373
    /**
374
     * Returns true if the passed MimePart fails the filter's header filter
375
     * settings.
376
     * 
377
     * @param \ZBateson\MailMimeParser\Message\Part\MimePart $part
378
     * @return boolean
379
     */
380 16
    private function failsHeaderPartFilter(MessagePart $part)
381
    {
382 16
        foreach ($this->headers as $type => $values) {
383 13
            foreach ($values as $name => $header) {
384 13
                if ($this->failsHeaderFor($part, $type, $name, $header)) {
385 13
                    return true;
386
                }
387
            }
388
        }
389 14
        return false;
390
    }
391
    
392
    /**
393
     * Determines if the passed MimePart should be filtered out or not.  If the
394
     * MimePart passes all filter tests, true is returned.  Otherwise false is
395
     * returned.
396
     * 
397
     * @param \ZBateson\MailMimeParser\Message\Part\MimePart $part
398
     * @return boolean
399
     */
400 16
    public function filter(MessagePart $part)
401
    {
402 16
        return !($this->failsMultiPartFilter($part)
403 16
            || $this->failsTextPartFilter($part)
404 16
            || $this->failsSignedPartFilter($part)
405 16
            || $this->failsHeaderPartFilter($part));
406
    }
407
}
408