Passed
Branch 1.0.0 (a1adee)
by Zaahid
08:34
created

PartFilter::failsHeaderPartFilter()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 8
cts 8
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 6
nc 4
nop 1
crap 4
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 MimePart::isMultiPart is set
63
     */
64
    private $multipart = PartFilter::FILTER_OFF;
65
    
66
    /**
67
     * @var int filters based on whether MimePart::isTextPart is set
68
     */
69
    private $textpart = PartFilter::FILTER_OFF;
70
    
71
    /**
72
     * @var int filters based on whether the parent of a part is a
73
     *      multipart/signed part and this part has a content-type equal to its
74
     *      parent's 'protocol' parameter in its content-type header
75
     */
76
    private $signedpart = PartFilter::FILTER_EXCLUDE;
77
    
78
    /**
79
     * @var string calculated hash of the filter
80
     */
81
    private $hashCode;
0 ignored issues
show
Unused Code introduced by
The property $hashCode is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
82
    
83
    /**
84
     * @var string[][] array of header rules.  The top-level contains keys of
85
     * FILTER_INCLUDE and/or FILTER_EXCLUDE, which contain key => value mapping
86
     * of header names => values to search for.  Note that when searching
87
     * MimePart::getHeaderValue is used (so additional parameters need not be
88
     * matched) and strcasecmp is used.
89
     * 
90
     * ```php
91
     * $filter = new PartFilter();
92
     * $filter->headers = [ PartFilter::FILTER_INCLUDE => [ 'Content-Type' => 'text/plain' ] ];
93
     * ```
94
     */
95
    private $headers = [];
96
    
97
    /**
98
     * Convenience method to filter for a specific mime type.
99
     * 
100
     * @param string $mimeType
101
     * @return PartFilter
102
     */
103 3
    public static function fromContentType($mimeType)
104
    {
105 3
        return new static([
106
            'headers' => [
107 3
                static::FILTER_INCLUDE => [
108
                    'Content-Type' => $mimeType
109 3
                ]
110 3
            ]
111 3
        ]);
112
    }
113
    
114
    /**
115
     * Convenience method to look for parts of a specific mime-type, and that
116
     * do not specifically have a Content-Disposition equal to 'attachment'.
117
     * 
118
     * @param string $mimeType
119
     * @return PartFilter
120
     */
121 2
    public static function fromInlineContentType($mimeType)
122
    {
123 2
        return new static([
124
            'headers' => [
125 2
                static::FILTER_INCLUDE => [
126
                    'Content-Type' => $mimeType
127 2
                ],
128 2
                static::FILTER_EXCLUDE => [
129
                    'Content-Disposition' => 'attachment'
130 2
                ]
131 2
            ]
132 2
        ]);
133
    }
134
    
135
    /**
136
     * Convenience method to search for parts with a specific
137
     * Content-Disposition, optionally including multipart parts.
138
     * 
139
     * @param string $disposition
140
     * @param int $multipart
141
     * @return PartFilter
142
     */
143 6
    public static function fromDisposition($disposition, $multipart = PartFilter::FILTER_OFF)
144
    {
145 6
        return new static([
146 6
            'multipart' => $multipart,
147
            'headers' => [
148 6
                static::FILTER_INCLUDE => [
149
                    'Content-Disposition' => $disposition
150 6
                ]
151 6
            ]
152 6
        ]);
153
    }
154
    
155
    /**
156
     * Constructs a PartFilter, optionally instantiating member variables with
157
     * values in the passed array.
158
     * 
159
     * The passed array must use keys equal to member variable names, e.g.
160
     * 'multipart', 'textpart', 'signedpart' and 'headers'.
161
     * 
162
     * @param array $filter
163
     */
164 20
    public function __construct(array $filter = [])
165
    {
166 20
        $params = [ 'multipart', 'textpart', 'signedpart', 'headers' ];
167 20
        foreach ($params as $param) {
168 20
            if (isset($filter[$param])) {
169 20
                $this->__set($param, $filter[$param]);
170 20
            }
171 20
        }
172 20
    }
173
    
174
    /**
175
     * Validates an argument passed to __set to insure it's set to a value in
176
     * $valid.
177
     * 
178
     * @param string $name Name of the member variable
179
     * @param string $value The value to test
180
     * @param array $valid an array of valid values
181
     * @throws InvalidArgumentException
182
     */
183 20
    private function validateArgument($name, $value, array $valid)
184
    {
185 20
        if (!in_array($value, $valid)) {
186
            $last = array_pop($valid);
187
            throw new InvalidArgumentException(
188
                '$value parameter for ' . $name . ' must be one of '
189
                . join(', ', $valid) . ' or ' . $last . ' - "' . $value
190
                . '" provided'
191
            );
192
        }
193 20
    }
194
    
195
    /**
196
     * Sets the PartFilter's headers filter to the passed array after validating
197
     * it.
198
     * 
199
     * @param array $headers
200
     * @throws InvalidArgumentException
201
     */
202
    public function setHeaders(array $headers)
203
    {
204 17
        array_walk($headers, function ($v, $k) {
205 17
            $this->validateArgument(
206 17
                'headers',
207 17
                $k,
208 17
                [ static::FILTER_EXCLUDE, static::FILTER_INCLUDE ]
209 17
            );
210 17
            if (!is_array($v)) {
211
                throw new InvalidArgumentException(
212
                    '$value must be an array with keys set to FILTER_EXCLUDE, '
213
                    . 'FILTER_INCLUDE and values set to an array of header '
214
                    . 'name => values'
215
                );
216
            }
217 17
        });
218 17
        $this->headers = $headers;
219 17
    }
220
    
221
    /**
222
     * Sets the member variable denoted by $name to the passed $value after
223
     * validating it.
224
     * 
225
     * @param string $name
226
     * @param int|array $value
227
     * @throws InvalidArgumentException
228
     */
229 20
    public function __set($name, $value)
230
    {
231 20
        if ($name === 'multipart' || $name === 'textpart' || $name === 'signedpart') {
232 15
            $this->validateArgument(
233 15
                $name,
234 15
                $value,
0 ignored issues
show
Documentation introduced by
$value is of type integer|array, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
235 15
                [ static::FILTER_OFF, static::FILTER_EXCLUDE, static::FILTER_INCLUDE ]
236 15
            );
237 15
            $this->$name = $value;
238 20
        } elseif ($name === 'headers') {
239 17
            if (!is_array($value)) {
240
                throw new InvalidArgumentException('$value must be an array');
241
            }
242 17
            $this->setHeaders($value);
243 17
        }
244 20
    }
245
    
246
    /**
247
     * Returns true if the variable denoted by $name is a member variable of
248
     * PartFilter.
249
     * 
250
     * @param string $name
251
     * @return bool
252
     */
253
    public function __isset($name)
254
    {
255
        return isset($this->$name);
256
    }
257
    
258
    /**
259
     * Returns the value of the member variable denoted by $name
260
     * 
261
     * @param string $name
262
     * @return mixed
263
     */
264 20
    public function __get($name)
265
    {
266 20
        return $this->$name;
267
    }
268
    
269
    /**
270
     * Returns true if the passed MimePart fails the filter's multipart filter
271
     * settings.
272
     * 
273
     * @param \ZBateson\MailMimeParser\Message\Part\MimePart $part
274
     * @return bool
275
     */
276 16
    private function failsMultiPartFilter(MessagePart $part)
277
    {
278 16
        if (!($part instanceof MimePart)) {
279
            return $this->multipart !== static::FILTER_EXCLUDE;
280
        }
281 16
        return ($this->multipart === static::FILTER_EXCLUDE && $part->isMultiPart())
282 16
            || ($this->multipart === static::FILTER_INCLUDE && !$part->isMultiPart());
283
    }
284
    
285
    /**
286
     * Returns true if the passed MimePart fails the filter's textpart filter
287
     * settings.
288
     * 
289
     * @param \ZBateson\MailMimeParser\Message\Part\MimePart $part
290
     * @return bool
291
     */
292 16
    private function failsTextPartFilter(MessagePart $part)
293
    {
294 16
        return ($this->textpart === static::FILTER_EXCLUDE && $part->isTextPart())
295 16
            || ($this->textpart === static::FILTER_INCLUDE && !$part->isTextPart());
296
    }
297
    
298
    /**
299
     * Returns true if the passed MimePart fails the filter's signedpart filter
300
     * settings.
301
     * 
302
     * @param \ZBateson\MailMimeParser\Message\Part\MimePart $part
303
     * @return boolean
304
     */
305 16
    private function failsSignedPartFilter(MessagePart $part)
306
    {
307 16
        if ($this->signedpart === static::FILTER_OFF) {
308 1
            return false;
309 15
        } elseif (!$part->isMime() || $part->getParent() === null) {
310 15
            return ($this->signedpart === static::FILTER_INCLUDE);
311
        }
312 10
        $partMimeType = $part->getContentType();
313 10
        $parentMimeType = $part->getParent()->getContentType();
314 10
        $parentProtocol = $part->getParent()->getHeaderParameter('Content-Type', 'protocol');
315 10
        if (strcasecmp($parentMimeType, 'multipart/signed') === 0 && strcasecmp($partMimeType, $parentProtocol) === 0) {
316 10
            return ($this->signedpart === static::FILTER_EXCLUDE);
317
        }
318
        return ($this->signedpart === static::FILTER_INCLUDE);
319
    }
320
    
321
    /**
322
     * Tests a single header value against $part, and returns true if the test
323
     * fails.
324
     * 
325
     * @staticvar array $map
326
     * @param MimePart $part
327
     * @param int $type
328
     * @param string $name
329
     * @param string $header
330
     * @return boolean
331
     */
332 13
    private function failsHeaderFor($part, $type, $name, $header)
333
    {
334 13
        $headerValue = null;
0 ignored issues
show
Unused Code introduced by
$headerValue is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
335
        
336
        static $map = [
337
            'content-type' => 'getContentType',
338
            'content-disposition' => 'getContentDisposition',
339
            'content-transfer-encoding' => 'getContentTransferEncoding'
340 13
        ];
341 13
        $lower = strtolower($name);
342 13
        if (isset($map[$lower])) {
343 13
            $headerValue = call_user_func([$part, $map[$lower]]);
344 13
        } elseif (!($part instanceof MimePart)) {
345
            return ($type === static::FILTER_INCLUDE);
346
        } else {
347
            $headerValue = $part->getHeaderValue($name);
348
        }
349
        
350 13
        return (($type === static::FILTER_EXCLUDE && strcasecmp($headerValue, $header) === 0)
351 13
            || ($type === static::FILTER_INCLUDE && strcasecmp($headerValue, $header) !== 0));
352
    }
353
    
354
    /**
355
     * Returns true if the passed MimePart fails the filter's header filter
356
     * settings.
357
     * 
358
     * @param \ZBateson\MailMimeParser\Message\Part\MimePart $part
359
     * @return boolean
360
     */
361 16
    private function failsHeaderPartFilter(MessagePart $part)
362
    {
363 16
        foreach ($this->headers as $type => $values) {
364 13
            foreach ($values as $name => $header) {
365 13
                if ($this->failsHeaderFor($part, $type, $name, $header)) {
0 ignored issues
show
Compatibility introduced by
$part of type object<ZBateson\MailMime...ssage\Part\MessagePart> is not a sub-type of object<ZBateson\MailMime...\Message\Part\MimePart>. It seems like you assume a child class of the class ZBateson\MailMimeParser\Message\Part\MessagePart to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
366 13
                    return true;
367
                }
368 11
            }
369 14
        }
370 14
        return false;
371
    }
372
    
373
    /**
374
     * Determines if the passed MimePart should be filtered out or not.  If the
375
     * MimePart passes all filter tests, true is returned.  Otherwise false is
376
     * returned.
377
     * 
378
     * @param \ZBateson\MailMimeParser\Message\Part\MimePart $part
379
     * @return boolean
380
     */
381 16
    public function filter(MessagePart $part)
382
    {
383 16
        return !($this->failsMultiPartFilter($part)
384 16
            || $this->failsTextPartFilter($part)
385 16
            || $this->failsSignedPartFilter($part)
386 16
            || $this->failsHeaderPartFilter($part));
387
    }
388
}
389