Passed
Branch master (ba441e)
by Zaahid
03:56
created

MimePart   D

Complexity

Total Complexity 60

Size/Duplication

Total Lines 541
Duplicated Lines 0 %

Test Coverage

Coverage 95.62%

Importance

Changes 0
Metric Value
wmc 60
dl 0
loc 541
ccs 131
cts 137
cp 0.9562
rs 4.2857
c 0
b 0
f 0

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __destruct() 0 7 3
A setParent() 0 3 1
A setContent() 0 6 1
A getChildParts() 0 6 2
A isTextPart() 0 10 4
A detachContentResourceHandle() 0 3 1
A removePart() 0 13 4
A getAllParts() 0 13 3
A hasContent() 0 6 2
A getChild() 0 7 2
A attachOriginalStreamHandle() 0 6 3
A getHeader() 0 6 2
A removeHeader() 0 3 1
A getPartByMimeType() 0 3 1
A removeAllParts() 0 4 2
A attachContentResourceHandle() 0 6 3
A getOriginalStreamHandle() 0 6 2
A getAllPartsByMimeType() 0 3 1
A getHeaderValue() 0 7 2
A getContentResourceHandle() 0 6 2
A __construct() 0 4 1
A getParent() 0 3 1
A isMultiPart() 0 6 1
A getContent() 0 8 2
A setRawHeader() 0 3 1
A getPartCount() 0 3 1
A getCountOfPartsByMimeType() 0 3 1
A getPart() 0 7 2
A getHeaderParameter() 0 7 3
A getHeaders() 0 3 1
A getChildCount() 0 3 1
A addPart() 0 5 3

How to fix   Complexity   

Complex Class

Complex classes like MimePart often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MimePart, and based on these observations, apply Extract Interface, too.

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\Header\HeaderFactory;
10
use ZBateson\MailMimeParser\Header\ParameterHeader;
11
use ZBateson\MailMimeParser\Message\Writer\MimePartWriter;
12
13
/**
14
 * Represents a single part of a multi-part mime message.
15
 *
16
 * A MimePart object may have any number of child parts, or may be a child
17
 * itself with its own parent or parents.
18
 *
19
 * The content of the part can be read from its PartStream resource handle,
20
 * accessible via MimePart::getContentResourceHanlde.
21
 *
22
 * @author Zaahid Bateson
23
 */
24
class MimePart
25
{
26
    /**
27
     * @var \ZBateson\MailMimeParser\Header\HeaderFactory the HeaderFactory
28
     *      object used for created headers
29
     */
30
    protected $headerFactory;
31
32
    /**
33
     * @var \ZBateson\MailMimeParser\Header\AbstractHeader[] array of header
34
     * objects
35
     */
36
    protected $headers;
37
38
    /**
39
     * @var \ZBateson\MailMimeParser\Message\MimePart parent part
40
     */
41
    protected $parent;
42
43
    /**
44
     * @var resource the content's resource handle
45
     */
46
    protected $handle;
47
    
48
    /**
49
     * 
50
     */
51
    protected $originalStreamHandle;
52
53
    /**
54
     * @var \ZBateson\MailMimeParser\Message\MimePart[] array of parts in this
55
     *      message
56
     */
57
    protected $parts = [];
58
59
    /**
60
     * @var \ZBateson\MailMimeParser\Message\Writer\MimePartWriter the part
61
     *      writer for this MimePart
62
     */
63
    protected $partWriter = null;
64
65
    /**
66
     * Sets up class dependencies.
67
     *
68
     * @param HeaderFactory $headerFactory
69
     * @param MimePartWriter $partWriter
70
     */
71 113
    public function __construct(HeaderFactory $headerFactory, MimePartWriter $partWriter)
72
    {
73 113
        $this->headerFactory = $headerFactory;
74 113
        $this->partWriter = $partWriter;
75 113
    }
76
77
    /**
78
     * Closes the attached resource handle.
79
     */
80 105
    public function __destruct()
81
    {
82 105
        if (is_resource($this->handle)) {
83 35
            fclose($this->handle);
84
        }
85 105
        if (is_resource($this->originalStreamHandle)) {
86 31
            fclose($this->originalStreamHandle);
87
        }
88 105
    }
89
90
    /**
91
     * Registers the passed part as a child of the current part.
92
     * 
93
     * If the $position parameter is non-null, adds the part at the passed
94
     * position index.
95
     *
96
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
97
     * @param int $position
98
     */
99 79
    public function addPart(MimePart $part, $position = null)
100
    {
101 79
        if ($part !== $this) {
102 79
            $part->setParent($this);
103 79
            array_splice($this->parts, ($position === null) ? count($this->parts) : $position, 0, [ $part ]);
104
        }
105 79
    }
106
    
107
    /**
108
     * Removes the child part from this part and returns its position or
109
     * null if it wasn't found.
110
     * 
111
     * Note that if the part is not a direct child of this part, the returned
112
     * position is its index within its parent (calls removePart on its direct
113
     * parent).
114
     *
115
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
116
     * @return int or null if not found
117
     */
118 15
    public function removePart(MimePart $part)
119
    {
120 15
        $parent = $part->getParent();
121 15
        if ($this !== $parent && $parent !== null) {
122 6
            return $parent->removePart($part);
123
        } else {
124 15
            $position = array_search($part, $this->parts, true);
125 15
            if ($position !== false) {
126 15
                array_splice($this->parts, $position, 1);
0 ignored issues
show
Bug introduced by
It seems like $position can also be of type string; however, parameter $offset of array_splice() does only seem to accept integer, 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

126
                array_splice($this->parts, /** @scrutinizer ignore-type */ $position, 1);
Loading history...
127 15
                return $position;
128
            }
129
        }
130
        return null;
131
    }
132
    
133
    /**
134
     * Removes all parts that are matched by the passed PartFilter.
135
     * 
136
     * @param \ZBateson\MailMimeParser\Message\PartFilter $filter
137
     */
138
    public function removeAllParts(PartFilter $filter = null)
139
    {
140
        foreach ($this->getAllParts($filter) as $part) {
141
            $this->removePart($part);
142
        }
143
    }
144
145
    /**
146
     * Returns the part at the given 0-based index, or null if none is set.
147
     * 
148
     * Note that the first part returned is the current part itself.  This is
149
     * often desirable for queries with a PartFilter, e.g. looking for a
150
     * MimePart with a specific Content-Type that may be satisfied by the
151
     * current part.
152
     *
153
     * @param int $index
154
     * @param PartFilter $filter
155
     * @return \ZBateson\MailMimeParser\Message\MimePart
156
     */
157 84
    public function getPart($index, PartFilter $filter = null)
158
    {
159 84
        $parts = $this->getAllParts($filter);
160 84
        if (!isset($parts[$index])) {
161 14
            return null;
162
        }
163 82
        return $parts[$index];
164
    }
165
166
    /**
167
     * Returns the current part, all child parts, and child parts of all
168
     * children optionally filtering them with the provided PartFilter.
169
     * 
170
     * The first part returned is always the current MimePart.  This is often
171
     * desirable as it may be a valid MimePart for the provided PartFilter.
172
     * 
173
     * @param PartFilter $filter an optional filter
174
     * @return \ZBateson\MailMimeParser\Message\MimePart[]
175
     */
176 94
    public function getAllParts(PartFilter $filter = null)
177
    {
178 94
        $aParts = [ $this ];
179 94
        foreach ($this->parts as $part) {
180 69
            $aParts = array_merge($aParts, $part->getAllParts(null, true));
0 ignored issues
show
Unused Code introduced by
The call to ZBateson\MailMimeParser\...MimePart::getAllParts() has too many arguments starting with true. ( Ignorable by Annotation )

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

180
            $aParts = array_merge($aParts, $part->/** @scrutinizer ignore-call */ getAllParts(null, true));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
181
        }
182 94
        if (!empty($filter)) {
183 93
            return array_values(array_filter(
184 93
                $aParts,
185 93
                [ $filter, 'filter' ]
186
            ));
187
        }
188 69
        return $aParts;
189
    }
190
191
    /**
192
     * Returns the total number of parts in this and all children.
193
     * 
194
     * Note that the current part is considered, so the minimum getPartCount is
195
     * 1 without a filter.
196
     *
197
     * @param PartFilter $filter
198
     * @return int
199
     */
200 5
    public function getPartCount(PartFilter $filter = null)
201
    {
202 5
        return count($this->getAllParts($filter));
203
    }
204
    
205
    /**
206
     * Returns the direct child at the given 0-based index, or null if none is
207
     * set.
208
     *
209
     * @param int $index
210
     * @param PartFilter $filter
211
     * @return \ZBateson\MailMimeParser\Message\MimePart
212
     */
213 27
    public function getChild($index, PartFilter $filter = null)
214
    {
215 27
        $parts = $this->getChildParts($filter);
216 27
        if (!isset($parts[$index])) {
217 8
            return null;
218
        }
219 27
        return $parts[$index];
220
    }
221
    
222
    /**
223
     * Returns all direct child parts.
224
     * 
225
     * If a PartFilter is provided, the PartFilter is applied before returning.
226
     * 
227
     * @param PartFilter $filter
228
     * @return \ZBateson\MailMimeParser\Message\MimePart[]
229
     */
230 97
    public function getChildParts(PartFilter $filter = null)
231
    {
232 97
        if ($filter !== null) {
233 1
            return array_values(array_filter($this->parts, [ $filter, 'filter' ]));
234
        }
235 96
        return $this->parts;
236
    }
237
    
238
    /**
239
     * Returns the number of direct children under this part.
240
     * 
241
     * @param PartFilter $filter
242
     * @return int
243
     */
244 9
    public function getChildCount(PartFilter $filter = null)
245
    {
246 9
        return count($this->getChildParts($filter));
247
    }
248
249
    /**
250
     * Returns the part associated with the passed mime type if it exists.
251
     *
252
     * @param string $mimeType
253
     * @return \ZBateson\MailMimeParser\Message\MimePart or null
254
     */
255 11
    public function getPartByMimeType($mimeType, $index = 0)
256
    {
257 11
        return $this->getPart($index, PartFilter::fromContentType($mimeType));
258
    }
259
    
260
    /**
261
     * Returns an array of all parts associated with the passed mime type if any
262
     * exist or null otherwise.
263
     *
264
     * @param string $mimeType
265
     * @return \ZBateson\MailMimeParser\Message\MimePart[] or null
266
     */
267 1
    public function getAllPartsByMimeType($mimeType)
268
    {
269 1
        return $this->getAllParts(PartFilter::fromContentType($mimeType));
270
    }
271
    
272
    /**
273
     * Returns the number of parts matching the passed $mimeType
274
     * 
275
     * @param string $mimeType
276
     * @return int
277
     */
278 1
    public function getCountOfPartsByMimeType($mimeType)
279
    {
280 1
        return $this->getPartCount(PartFilter::fromContentType($mimeType));
281
    }
282
283
    /**
284
     * Returns true if there's a content stream associated with the part.
285
     *
286
     * @return boolean
287
     */
288 26
    public function hasContent()
289
    {
290 26
        if ($this->handle !== null) {
0 ignored issues
show
introduced by
The condition $this->handle !== null can never be false.
Loading history...
291 26
            return true;
292
        }
293 1
        return false;
294
    }
295
296
    /**
297
     * Returns true if this part's mime type is multipart/*
298
     *
299
     * @return bool
300
     */
301 76
    public function isMultiPart()
302
    {
303
        // casting to bool, preg_match returns 1 for true
304 76
        return (bool) (preg_match(
305 76
            '~multipart/\w+~i',
306 76
            $this->getHeaderValue('Content-Type', 'text/plain')
307
        ));
308
    }
309
    
310
    /**
311
     * Returns true if this part's mime type is text/plain, text/html or has a
312
     * text/* and has a defined 'charset' attribute.
313
     * 
314
     * @return bool
315
     */
316 100
    public function isTextPart()
317
    {
318 100
        $type = $this->getHeaderValue('Content-Type', 'text/plain');
319 100
        if ($type === 'text/html' || $type === 'text/plain') {
320 89
            return true;
321
        }
322 77
        $charset = $this->getHeaderParameter('Content-Type', 'charset');
323 77
        return ($charset !== null && preg_match(
324 4
            '~text/\w+~i',
325 77
            $this->getHeaderValue('Content-Type', 'text/plain')
326
        ));
327
    }
328
329
    /**
330
     * Attaches the resource handle for the part's content.  The attached handle
331
     * is closed when the MimePart object is destroyed.
332
     *
333
     * @param resource $contentHandle
334
     */
335 100
    public function attachContentResourceHandle($contentHandle)
336
    {
337 100
        if ($this->handle !== null && $this->handle !== $contentHandle) {
338 11
            fclose($this->handle);
339
        }
340 100
        $this->handle = $contentHandle;
341 100
    }
342
    
343
    /**
344
     * Attaches the resource handle representing the original stream that
345
     * created this part (including any sub-parts).  The attached handle is
346
     * closed when the MimePart object is destroyed.
347
     * 
348
     * This stream is not modified or changed as the part is changed and is only
349
     * set during parsing in MessageParser.
350
     *
351
     * @param resource $handle
352
     */
353 95
    public function attachOriginalStreamHandle($handle)
354
    {
355 95
        if ($this->originalStreamHandle !== null && $this->originalStreamHandle !== $handle) {
356 70
            fclose($this->originalStreamHandle);
357
        }
358 95
        $this->originalStreamHandle = $handle;
359 95
    }
360
    
361
    /**
362
     * Returns a resource stream handle allowing a user to read the original
363
     * stream (including headers and child parts) that was used to create the
364
     * current part.
365
     * 
366
     * The part contains an original stream handle only if it was explicitly set
367
     * by a call to MimePart::attachOriginalStreamHandle.  MailMimeParser only
368
     * sets this during the parsing phase in MessageParser, and is not otherwise
369
     * changed or updated.  New parts added below this part, changed headers,
370
     * etc... would not be reflected in the returned stream handle.
371
     * 
372
     * @return resource the resource handle or null if not set
373
     */
374 14
    public function getOriginalStreamHandle()
375
    {
376 14
        if (is_resource($this->originalStreamHandle)) {
377 14
            rewind($this->originalStreamHandle);
378
        }
379 14
        return $this->originalStreamHandle;
380
    }
381
382
    /**
383
     * Detaches the content resource handle from this part but does not close
384
     * it.
385
     */
386 18
    protected function detachContentResourceHandle()
387
    {
388 18
        $this->handle = null;
389 18
    }
390
391
    /**
392
     * Sets the content of the part to the passed string (effectively creates
393
     * a php://temp stream with the passed content and calls
394
     * attachContentResourceHandle with the opened stream).
395
     *
396
     * @param string $string
397
     */
398 11
    public function setContent($string)
399
    {
400 11
        $handle = fopen('php://temp', 'r+');
401 11
        fwrite($handle, $string);
0 ignored issues
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, 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

401
        fwrite(/** @scrutinizer ignore-type */ $handle, $string);
Loading history...
402 11
        rewind($handle);
0 ignored issues
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $handle of rewind() does only seem to accept resource, 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

402
        rewind(/** @scrutinizer ignore-type */ $handle);
Loading history...
403 11
        $this->attachContentResourceHandle($handle);
0 ignored issues
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $contentHandle of ZBateson\MailMimeParser\...ContentResourceHandle() does only seem to accept resource, 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

403
        $this->attachContentResourceHandle(/** @scrutinizer ignore-type */ $handle);
Loading history...
404 11
    }
405
406
    /**
407
     * Returns the resource stream handle for the part's content or null if not
408
     * set.  rewind() is called on the stream before returning it.
409
     *
410
     * The resource is automatically closed by MimePart's destructor and should
411
     * not be closed otherwise.
412
     *
413
     * The returned resource handle is a stream with decoding filters appended
414
     * to it.  The attached filters are determined by looking at the part's
415
     * Content-Encoding header.  The following encodings are currently
416
     * supported:
417
     *
418
     * - Quoted-Printable
419
     * - Base64
420
     * - X-UUEncode
421
     *
422
     * UUEncode may be automatically attached for a message without a defined
423
     * Content-Encoding and Content-Type if it has a UUEncoded part to support
424
     * older non-mime message attachments.
425
     *
426
     * In addition, character encoding for text streams is converted to UTF-8
427
     * if {@link \ZBateson\MailMimeParser\Message\MimePart::isTextPart
428
     * MimePart::isTextPart} returns true.
429
     *
430
     * @return resource
431
     */
432 94
    public function getContentResourceHandle()
433
    {
434 94
        if (is_resource($this->handle)) {
435 94
            rewind($this->handle);
436
        }
437 94
        return $this->handle;
438
    }
439
440
    /**
441
     * Shortcut to reading stream content and assigning it to a string.  Returns
442
     * null if the part doesn't have a content stream.
443
     *
444
     * @return string
445
     */
446 25
    public function getContent()
447
    {
448 25
        if ($this->hasContent()) {
449 25
            $text = stream_get_contents($this->handle);
450 25
            rewind($this->handle);
451 25
            return $text;
452
        }
453
        return null;
454
    }
455
456
    /**
457
     * Adds a header with the given $name and $value.
458
     *
459
     * Creates a new \ZBateson\MailMimeParser\Header\AbstractHeader object and
460
     * registers it as a header.
461
     *
462
     * @param string $name
463
     * @param string $value
464
     */
465 105
    public function setRawHeader($name, $value)
466
    {
467 105
        $this->headers[strtolower($name)] = $this->headerFactory->newInstance($name, $value);
468 105
    }
469
470
    /**
471
     * Removes the header with the given name
472
     *
473
     * @param string $name
474
     */
475 18
    public function removeHeader($name)
476
    {
477 18
        unset($this->headers[strtolower($name)]);
478 18
    }
479
480
    /**
481
     * Returns the AbstractHeader object for the header with the given $name
482
     *
483
     * Note that mime headers aren't case sensitive.
484
     *
485
     * @param string $name
486
     * @return \ZBateson\MailMimeParser\Header\AbstractHeader
487
     */
488 107
    public function getHeader($name)
489
    {
490 107
        if (isset($this->headers[strtolower($name)])) {
491 105
            return $this->headers[strtolower($name)];
492
        }
493 100
        return null;
494
    }
495
496
    /**
497
     * Returns the string value for the header with the given $name.
498
     *
499
     * Note that mime headers aren't case sensitive.
500
     *
501
     * @param string $name
502
     * @param string $defaultValue
503
     * @return string
504
     */
505 104
    public function getHeaderValue($name, $defaultValue = null)
506
    {
507 104
        $header = $this->getHeader($name);
508 104
        if ($header !== null) {
509 103
            return $header->getValue();
510
        }
511 99
        return $defaultValue;
512
    }
513
514
    /**
515
     * Returns the full array of headers for this part.
516
     *
517
     * @return \ZBateson\MailMimeParser\Header\AbstractHeader[]
518
     */
519 90
    public function getHeaders()
520
    {
521 90
        return $this->headers;
522
    }
523
524
    /**
525
     * Returns a parameter of the header $header, given the parameter named
526
     * $param.
527
     *
528
     * Only headers of type
529
     * \ZBateson\MailMimeParser\Header\ParameterHeader have parameters.
530
     * Content-Type and Content-Disposition are examples of headers with
531
     * parameters. "Charset" is a common parameter of Content-Type.
532
     *
533
     * @param string $header
534
     * @param string $param
535
     * @param string $defaultValue
536
     * @return string
537
     */
538 103
    public function getHeaderParameter($header, $param, $defaultValue = null)
539
    {
540 103
        $obj = $this->getHeader($header);
541 103
        if ($obj && $obj instanceof ParameterHeader) {
0 ignored issues
show
introduced by
The condition $obj && $obj instanceof ...\Header\ParameterHeader can never be true.
Loading history...
542 101
            return $obj->getValueFor($param, $defaultValue);
543
        }
544 7
        return $defaultValue;
545
    }
546
547
    /**
548
     * Sets the parent part.
549
     *
550
     * @param \ZBateson\MailMimeParser\Message\MimePart $part
551
     */
552 104
    public function setParent(MimePart $part)
553
    {
554 104
        $this->parent = $part;
555 104
    }
556
557
    /**
558
     * Returns this part's parent.
559
     *
560
     * @return \ZBateson\MailMimeParser\Message\MimePart
561
     */
562 103
    public function getParent()
563
    {
564 103
        return $this->parent;
565
    }
566
}
567