Completed
Push — master ( 5d4e1b...373de6 )
by Zaahid
11:37
created

MimePart   C

Complexity

Total Complexity 58

Size/Duplication

Total Lines 546
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 94.36%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 58
c 1
b 0
f 0
lcom 1
cbo 4
dl 0
loc 546
ccs 184
cts 195
cp 0.9436
rs 6.3005

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A __destruct() 0 6 2
A addPart() 0 8 3
A removePart() 0 11 3
A getPart() 0 7 2
A getAllParts() 0 4 1
A getPartCount() 0 4 1
A getPartByMimeType() 0 8 2
A hasContent() 0 7 2
A isMultiPart() 0 7 1
A isTextPart() 0 7 1
A attachContentResourceHandle() 0 7 3
A detachContentResourceHandle() 0 4 1
A setContent() 0 7 1
A getContentResourceHandle() 0 4 1
A getContent() 0 7 2
A setRawHeader() 0 4 1
A removeHeader() 0 4 1
A getHeader() 0 7 2
A getHeaderValue() 0 8 2
A getHeaders() 0 4 1
A getHeaderParameter() 0 8 3
A setParent() 0 4 1
A getParent() 0 4 1
A setCharsetStreamFilterOnStream() 0 16 2
B setTransferEncodingFilterOnStream() 0 37 3
A isUUEncoded() 0 5 1
A filterTextBeforeCopying() 0 15 3
A copyContentStream() 0 16 3
A writeHeadersTo() 0 7 2
A writeContentTo() 0 17 4
A writeTo() 0 5 1

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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;
8
9
use ZBateson\MailMimeParser\Header\HeaderFactory;
10
use ZBateson\MailMimeParser\Header\ParameterHeader;
11
use ZBateson\MailMimeParser\Stream\StreamLeftover;
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\MimePart parent part
40
     */
41
    protected $parent;
42
43
    /**
44
     * @var resource the content's resource handle
45
     */
46
    protected $handle;
47
48
    /**
49
     * @var \ZBateson\MailMimeParser\MimePart[] array of parts in this message
50
     */
51
    protected $parts = [];
52
53
    /**
54
     * @var \ZBateson\MailMimeParser\MimePart[] Maps mime types to parts for
55
     * looking up in getPartByMimeType
56
     */
57
    protected $mimeToPart = [];
58
59
    /**
60
     * Sets up class dependencies.
61
     *
62
     * @param HeaderFactory $headerFactory
63
     */
64 82
    public function __construct(HeaderFactory $headerFactory)
65
    {
66 82
        $this->headerFactory = $headerFactory;
67 82
    }
68
69
    /**
70
     * Closes the attached resource handle.
71
     */
72 82
    public function __destruct()
73
    {
74 82
        if ($this->handle !== null) {
75 8
            fclose($this->handle);
76 8
        }
77 82
    }
78
79
    /**
80
     * Adds the passed part to the parts array, and registers non-attachment/
81
     * non-multipart parts by their content type.
82
     *
83
     * @param \ZBateson\MailMimeParser\MimePart $part
84
     */
85 74
    public function addPart(MimePart $part)
86
    {
87 74
        $this->parts[] = $part;
88 74
        $key = strtolower($part->getHeaderValue('Content-Type', 'text/plain'));
89 74
        if ($part->getHeaderValue('Content-Disposition') === null && !$part->isMultiPart()) {
90 71
            $this->mimeToPart[$key] = $part;
91 71
        }
92 74
    }
93
94
    /**
95
     * Unregisters the child part from this part.
96
     *
97
     * @param \ZBateson\MailMimeParser\MimePart $part
98
     */
99 7
    public function removePart(MimePart $part)
100
    {
101 7
        $partsArray = [];
102 7
        foreach ($this->parts as $apart) {
103 7
            if ($apart !== $part) {
104 5
                $partsArray[] = $apart;
105 5
            }
106 7
        }
107 7
        unset($this->mimeToPart[$part->getHeaderValue('Content-Type', 'text/plain')]);
108 7
        $this->parts = $partsArray;
109 7
    }
110
111
    /**
112
     * Returns the non-text, non-HTML part at the given 0-based index, or null
113
     * if none is set.
114
     *
115
     * @param int $index
116
     * @return \ZBateson\MailMimeParser\MimePart
117
     */
118 2
    public function getPart($index)
119
    {
120 2
        if (!isset($this->parts[$index])) {
121
            return null;
122
        }
123 2
        return $this->parts[$index];
124
    }
125
126
    /**
127
     * Returns all attachment parts.
128
     *
129
     * @return \ZBateson\MailMimeParser\MimePart[]
130
     */
131 22
    public function getAllParts()
132
    {
133 22
        return $this->parts;
134
    }
135
136
    /**
137
     * Returns the number of attachments available.
138
     *
139
     * @return int
140
     */
141 1
    public function getPartCount()
142
    {
143 1
        return count($this->parts);
144
    }
145
146
    /**
147
     * Returns the part associated with the passed mime type if it exists.
148
     *
149
     * @param string $mimeType
150
     * @return \ZBateson\MailMimeParser\MimePart or null
151
     */
152 18
    public function getPartByMimeType($mimeType)
153
    {
154 18
        $key = strtolower($mimeType);
155 18
        if (isset($this->mimeToPart[$key])) {
156 18
            return $this->mimeToPart[$key];
157
        }
158
        return null;
159
    }
160
161
    /**
162
     * Returns true if there's a content stream associated with the part.
163
     *
164
     * @return boolean
165
     */
166 72
    public function hasContent()
167
    {
168 72
        if (!empty($this->handle)) {
169 72
            return true;
170
        }
171 22
        return false;
172
    }
173
174
    /**
175
     * Returns true if this part's mime type is multipart/*
176
     *
177
     * @return bool
178
     */
179 74
    public function isMultiPart()
180
    {
181 74
        return preg_match(
182 74
            '~multipart/\w+~i',
183 74
            $this->getHeaderValue('Content-Type', 'text/plain')
184 74
        );
185
    }
186
    
187
    /**
188
     * Returns true if this part's mime type is text/*
189
     * 
190
     * @return bool
191
     */
192 71
    public function isTextPart()
193
    {
194 71
        return preg_match(
195 71
            '~text/\w+~i',
196 71
            $this->getHeaderValue('Content-Type', 'text/plain')
197 71
        );
198
    }
199
200
    /**
201
     * Attaches the resource handle for the part's content.  The attached handle
202
     * is closed when the MimePart object is destroyed.
203
     *
204
     * @param resource $contentHandle
205
     */
206 76
    public function attachContentResourceHandle($contentHandle)
207
    {
208 76
        if ($this->handle !== null && $this->handle !== $contentHandle) {
209 2
            fclose($this->handle);
210 2
        }
211 76
        $this->handle = $contentHandle;
212 76
    }
213
214
    /**
215
     *
216
     */
217 8
    protected function detachContentResourceHandle()
218
    {
219 8
        $this->handle = null;
220 8
    }
221
222
    /**
223
     * Sets the content of the part to the passed string (effectively creates
224
     * a php://temp stream with the passed content and calls
225
     * attachContentResourceHandle with the opened stream).
226
     *
227
     * @param string $string
228
     */
229 6
    public function setContent($string)
230
    {
231 6
        $handle = fopen('php://temp', 'r+');
232 6
        fwrite($handle, $string);
233 6
        rewind($handle);
234 6
        $this->attachContentResourceHandle($handle);
235 6
    }
236
237
    /**
238
     * Returns the resource stream handle for the part's content.
239
     *
240
     * The resource is automatically closed by MimePart's destructor and should
241
     * not be closed otherwise.
242
     *
243
     * @return resource
244
     */
245 72
    public function getContentResourceHandle()
246
    {
247 72
        return $this->handle;
248
    }
249
250
    /**
251
     * Shortcut to reading stream content and assigning it to a string.  Returns
252
     * null if the part doesn't have a content stream.
253
     *
254
     * @return string
255
     */
256 2
    public function getContent()
257
    {
258 2
        if ($this->hasContent()) {
259 2
            return stream_get_contents($this->handle);
260
        }
261
        return null;
262
    }
263
264
    /**
265
     * Adds a header with the given $name and $value.
266
     *
267
     * Creates a new \ZBateson\MailMimeParser\Header\AbstractHeader object and
268
     * registers it as a header.
269
     *
270
     * @param string $name
271
     * @param string $value
272
     */
273 77
    public function setRawHeader($name, $value)
274
    {
275 77
        $this->headers[strtolower($name)] = $this->headerFactory->newInstance($name, $value);
276 77
    }
277
278
    /**
279
     * Removes the header with the given name
280
     *
281
     * @param string $name
282
     */
283 5
    public function removeHeader($name)
284
    {
285 5
        unset($this->headers[strtolower($name)]);
286 5
    }
287
288
    /**
289
     * Returns the AbstractHeader object for the header with the given $name
290
     *
291
     * Note that mime headers aren't case sensitive.
292
     *
293
     * @param string $name
294
     * @return \ZBateson\MailMimeParser\Header\AbstractHeader
295
     */
296 79
    public function getHeader($name)
297
    {
298 79
        if (isset($this->headers[strtolower($name)])) {
299 77
            return $this->headers[strtolower($name)];
300
        }
301 75
        return null;
302
    }
303
304
    /**
305
     * Returns the string value for the header with the given $name.
306
     *
307
     * Note that mime headers aren't case sensitive.
308
     *
309
     * @param string $name
310
     * @param string $defaultValue
311
     * @return string
312
     */
313 76
    public function getHeaderValue($name, $defaultValue = null)
314
    {
315 76
        $header = $this->getHeader($name);
316 76
        if (!empty($header)) {
317 75
            return $header->getValue();
318
        }
319 74
        return $defaultValue;
320
    }
321
322
    /**
323
     * Returns the full array of headers for this part.
324
     *
325
     * @return \ZBateson\MailMimeParser\Header\AbstractHeader[]
326
     */
327 1
    public function getHeaders()
328
    {
329 1
        return $this->headers;
330
    }
331
332
    /**
333
     * Returns a parameter of the header $header, given the parameter named
334
     * $param.
335
     *
336
     * Only headers of type
337
     * \ZBateson\MailMimeParser\Header\ParameterHeader have parameters.
338
     * Content-Type and Content-Disposition are examples of headers with
339
     * parameters. "Charset" is a common parameter of Content-Type.
340
     *
341
     * @param string $header
342
     * @param string $param
343
     * @param string $defaultValue
344
     * @return string
345
     */
346 76
    public function getHeaderParameter($header, $param, $defaultValue = null)
347
    {
348 76
        $obj = $this->getHeader($header);
349 76
        if ($obj && $obj instanceof ParameterHeader) {
350 75
            return $obj->getValueFor($param, $defaultValue);
351
        }
352 5
        return $defaultValue;
353
    }
354
355
    /**
356
     * Sets the parent part.
357
     *
358
     * @param \ZBateson\MailMimeParser\MimePart $part
359
     */
360 73
    public function setParent(MimePart $part)
361
    {
362 73
        $this->parent = $part;
363 73
    }
364
365
    /**
366
     * Returns this part's parent.
367
     *
368
     * @return \ZBateson\MailMimeParser\MimePart
369
     */
370 73
    public function getParent()
371
    {
372 73
        return $this->parent;
373
    }
374
375
    /**
376
     * Sets up a mailmimeparser-encode stream filter on the passed stream
377
     * resource handle if applicable and returns a reference to the filter.
378
     *
379
     * @param resource $handle
380
     * @return resource a reference to the appended stream filter or null
381
     */
382 71
    private function setCharsetStreamFilterOnStream($handle)
0 ignored issues
show
Unused Code introduced by
The parameter $handle is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
383
    {
384 71
        $contentType = strtolower($this->getHeaderValue('Content-Type', 'text/plain'));
385 71
        if (strpos($contentType, 'text/') === 0) {
386 69
            return stream_filter_append(
387 69
                $this->handle,
388 69
                'mailmimeparser-encode',
389 69
                STREAM_FILTER_READ,
390
                [
391 69
                    'charset' => 'UTF-8',
392 69
                    'to' => $this->getHeaderParameter('Content-Type', 'charset', 'ISO-8859-1')
393 69
                ]
394 69
            );
395
        }
396 41
        return null;
397
    }
398
399
    /**
400
     * Appends a stream filter the passed resource handle based on the type of
401
     * encoding for the current mime part.
402
     *
403
     * Unfortunately PHP seems to error out allocating memory for
404
     * stream_filter_make_writable in Base64EncodeStreamFilter using
405
     * STREAM_FILTER_WRITE, and HHVM doesn't seem to remove the filter properly
406
     * for STREAM_FILTER_READ, so the function appends a read filter on
407
     * $fromHandle if running through 'php', and a write filter on $toHandle if
408
     * using HHVM.
409
     *
410
     * @param resource $fromHandle
411
     * @param resource $toHandle
412
     * @param \ZBateson\MailMimeParser\Stream\StreamLeftover $leftovers
413
     * @return resource the stream filter
414
     */
415 71
    private function setTransferEncodingFilterOnStream($fromHandle, $toHandle, StreamLeftover $leftovers)
416
    {
417 71
        $encoding = strtolower($this->getHeaderValue('Content-Transfer-Encoding'));
418
        $params = [
419 71
            'line-length' => 76,
420 71
            'line-break-chars' => "\r\n",
421 71
            'leftovers' => $leftovers,
422 71
            'filename' => $this->getHeaderParameter(
423 71
                'Content-Type',
424 71
                'name',
425
                'null'
426 71
            )
427 71
        ];
428
        $typeToEncoding = [
429 71
            'quoted-printable' => 'convert.quoted-printable-encode',
430 71
            'base64' => 'convert.base64-encode',
431 71
            'x-uuencode' => 'mailmimeparser-uuencode',
432 71
        ];
433 71
        if (isset($typeToEncoding[$encoding])) {
434 61
            if (defined('HHVM_VERSION')) {
435
                return stream_filter_append(
436
                    $toHandle,
437
                    $typeToEncoding[$encoding],
438
                    STREAM_FILTER_WRITE,
439
                    $params
440
                );
441
            } else {
442 61
                return stream_filter_append(
443 61
                    $fromHandle,
444 61
                    $typeToEncoding[$encoding],
445 61
                    STREAM_FILTER_READ,
446
                    $params
447 61
                );
448
            }
449
        }
450 40
        return null;
451
    }
452
453
    /**
454
     * Returns true if the content-transfer-encoding header of the current part
455
     * is set to 'x-uuencode'.
456
     *
457
     * @return bool
458
     */
459
    private function isUUEncoded()
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
460
    {
461
        $encoding = strtolower($this->getHeaderValue('Content-Transfer-Encoding'));
462
        return ($encoding === 'x-uuencode');
463
    }
464
465
    /**
466
     * Filters out single line feed (CR or LF) characters from text input and
467
     * replaces them with CRLF, assigning the result to $read.  Also trims out
468
     * any starting and ending CRLF characters in the stream.
469
     *
470
     * @param string $read the read string, and where the result will be written
471
     *        to
472
     * @param bool $first set to true if this is the first set of read
473
     *        characters from the stream (ltrims CRLF)
474
     * @param string $lastChars contains any CRLF characters from the last $read
475
     *        line if it ended with a CRLF (because they're trimmed from the
476
     *        end, and get prepended to $read).
477
     */
478 69
    private function filterTextBeforeCopying(&$read, &$first, &$lastChars)
479
    {
480 69
        if ($first) {
481 69
            $first = false;
482 69
            $read = ltrim($read, "\r\n");
483 69
        }
484 69
        $read = $lastChars . $read;
485 69
        $read = preg_replace('/\r\n|\r|\n/', "\r\n", $read);
486 69
        $lastChars = '';
487 69
        $matches = null;
488 69
        if (preg_match('/[\r\n]+$/', $read, $matches)) {
489 65
            $lastChars = $matches[0];
490 65
            $read = rtrim($read, "\r\n");
491 65
        }
492 69
    }
493
494
    /**
495
     * Copies the content of the $fromHandle stream into the $toHandle stream,
496
     * maintaining the current read position in $fromHandle and writing
497
     * uuencode headers.
498
     *
499
     * @param resource $fromHandle
500
     * @param resource $toHandle
501
     */
502 71
    private function copyContentStream($fromHandle, $toHandle)
503
    {
504 71
        $pos = ftell($fromHandle);
505 71
        rewind($fromHandle);
506
        // changed from stream_copy_to_stream because hhvm seems to stop before
507
        // end of file for some reason
508 71
        $lastChars = '';
509 71
        $first = true;
510 71
        while (($read = fread($fromHandle, 1024)) != false) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $read = fread($fromHandle, 1024) of type string to the boolean false. If you are specifically checking for a non-empty string, consider using the more explicit !== '' instead.
Loading history...
511 71
            if ($this->isTextPart()) {
512 69
                $this->filterTextBeforeCopying($read, $first, $lastChars);
513 69
            }
514 71
            fwrite($toHandle, $read);
515 71
        }
516 71
        fseek($fromHandle, $pos);
517 71
    }
518
519
    /**
520
     * Writes out headers and follows them with an empty line.
521
     *
522
     * @param resource $handle
523
     */
524 71
    protected function writeHeadersTo($handle)
525
    {
526 71
        foreach ($this->headers as $header) {
527 71
            fwrite($handle, "$header\r\n");
1 ignored issue
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $header instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
528 71
        }
529 71
        fwrite($handle, "\r\n");
530 71
    }
531
532
    /**
533
     * Writes out the content portion of the mime part based on the headers that
534
     * are set on the part, taking care of character/content-transfer encoding.
535
     *
536
     * @param resource $handle
537
     */
538 71
    protected function writeContentTo($handle)
539
    {
540 71
        if (!empty($this->handle)) {
541 71
            $filter = $this->setCharsetStreamFilterOnStream($handle);
542 71
            $leftovers = new StreamLeftover();
543 71
            $encodingFilter = $this->setTransferEncodingFilterOnStream($this->handle, $handle, $leftovers);
544 71
            $this->copyContentStream($this->handle, $handle);
545 71
            if ($encodingFilter !== null) {
546 61
                fflush($handle);
547 61
                stream_filter_remove($encodingFilter);
548 61
                fwrite($handle, $leftovers->encodedValue);
549 61
            }
550 71
            if ($filter !== null) {
551 69
                stream_filter_remove($filter);
552 69
            }
553 71
        }
554 71
    }
555
556
    /**
557
     * Writes out the MimePart to the passed resource.
558
     *
559
     * Takes care of character and content transfer encoding on the output based
560
     * on what headers are set.
561
     *
562
     * @param resource $handle
563
     */
564 48
    protected function writeTo($handle)
565
    {
566 48
        $this->writeHeadersTo($handle);
567 48
        $this->writeContentTo($handle);
568 48
    }
569
}
570