Completed
Push — master ( 74fe50...0f19ea )
by Zaahid
09:04
created

MimePartWriter::writePartContentTo()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 24
ccs 0
cts 20
cp 0
rs 8.6845
c 0
b 0
f 0
cc 4
eloc 16
nc 5
nop 2
crap 20
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\Writer;
8
9
use ZBateson\MailMimeParser\Message\MimePart;
10
use ZBateson\MailMimeParser\Stream\StreamLeftover;
11
12
/**
13
 * Writes a MimePart to a resource handle.
14
 * 
15
 * The class is responsible for writing out the headers and content of a
16
 * MimePart to an output stream buffer, taking care of encoding and filtering.
17
 * 
18
 * @author Zaahid Bateson
19
 */
20
class MimePartWriter
21
{
22
    /**
23
     * @var array default params for stream filters in
24
     *      setTransferEncodingFilterOnStream
25
     */
26
    private static $defaultStreamFilterParams = [
27
        'line-length' => 76,
28
        'line-break-chars' => "\r\n",
29
    ];
30
    
31
    /**
32
     * @var array map of transfer-encoding types to registered stream filter
33
     *      names used in setTransferEncodingFilterOnStream
34
     */
35
    private static $typeToEncodingMap = [
36
        'quoted-printable' => 'convert.quoted-printable-encode',
37
        'base64' => 'convert.base64-encode',
38
        'x-uuencode' => 'mailmimeparser-uuencode',
39
    ];
40
    
41
    /**
42
     * Returns the singleton instance for the class, instantiating it if not
43
     * already created.
44
     */
45 View Code Duplication
    public static function getInstance()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
46
    {
47
        static $instances = [];
48
        $class = get_called_class();
49
        if (!isset($instances[$class])) {
50
            $instances[$class] = new static();
51
        }
52
        return $instances[$class];
53
    }
54
    
55
    /**
56
     * Writes out the headers of the passed MimePart and follows them with an
57
     * empty line.
58
     *
59
     * @param MimePart $part
60
     * @param resource $handle
61
     */
62
    public function writePartHeadersTo(MimePart $part, $handle)
63
    {
64
        $headers = $part->getHeaders();
65
        foreach ($headers as $header) {
66
            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...
67
        }
68
        fwrite($handle, "\r\n");
69
    }
70
    
71
    /**
72
     * Sets up a mailmimeparser-encode stream filter on the content resource 
73
     * handle of the passed MimePart if applicable and returns a reference to
74
     * the filter.
75
     *
76
     * @param MimePart $part
77
     * @return resource a reference to the appended stream filter or null
78
     */
79
    private function setCharsetStreamFilterOnPartStream(MimePart $part)
80
    {
81
        $handle = $part->getContentResourceHandle();
82
        if ($part->isTextPart()) {
83
            return stream_filter_append(
84
                $handle,
85
                'mailmimeparser-encode',
86
                STREAM_FILTER_READ,
87
                [
88
                    'charset' => 'UTF-8',
89
                    'to' => $part->getHeaderParameter(
90
                        'Content-Type',
91
                        'charset',
92
                        'ISO-8859-1'
93
                    )
94
                ]
95
            );
96
        }
97
        return null;
98
    }
99
    
100
    /**
101
     * Appends a stream filter on the passed MimePart's content resource handle
102
     * based on the type of encoding for the passed part.
103
     *
104
     * Unfortunately PHP seems to error out allocating memory for
105
     * stream_filter_make_writable in Base64EncodeStreamFilter using
106
     * STREAM_FILTER_WRITE, and HHVM doesn't seem to remove the filter properly
107
     * for STREAM_FILTER_READ, so the function appends a read filter on
108
     * $fromHandle if running through 'php', and a write filter on $toHandle if
109
     * using HHVM.
110
     *
111
     * @param MimePart $part
112
     * @param resource $handle
113
     * @param StreamLeftover $leftovers
114
     * @return resource the stream filter
115
     */
116
    private function setTransferEncodingFilterOnStream(MimePart $part, $handle, StreamLeftover $leftovers)
117
    {
118
        $contentHandle = $part->getContentResourceHandle();
119
        $encoding = strtolower($part->getHeaderValue('Content-Transfer-Encoding'));
120
        $params = array_merge(self::$defaultStreamFilterParams, [
121
            'leftovers' => $leftovers,
122
            'filename' => $part->getHeaderParameter(
123
                'Content-Type',
124
                'name',
125
                'null'
126
            )
127
        ]);
128
        if (isset(self::$typeToEncodingMap[$encoding])) {
129
            if (defined('HHVM_VERSION')) {
130
                return stream_filter_append(
131
                    $handle,
132
                    self::$typeToEncodingMap[$encoding],
133
                    STREAM_FILTER_WRITE,
134
                    $params
135
                );
136
            } else {
137
                return stream_filter_append(
138
                    $contentHandle,
139
                    self::$typeToEncodingMap[$encoding],
140
                    STREAM_FILTER_READ,
141
                    $params
142
                );
143
            }
144
        }
145
        return null;
146
    }
147
148
    /**
149
     * Filters out single line feed (CR or LF) characters from text input and
150
     * replaces them with CRLF, assigning the result to $read.  Also trims out
151
     * any starting and ending CRLF characters in the stream.
152
     *
153
     * @param string $read the read string, and where the result will be written
154
     *        to
155
     * @param bool $first set to true if this is the first set of read
156
     *        characters from the stream (ltrims CRLF)
157
     * @param string $lastChars contains any CRLF characters from the last $read
158
     *        line if it ended with a CRLF (because they're trimmed from the
159
     *        end, and get prepended to $read).
160
     */
161
    private function filterTextBeforeCopying(&$read, &$first, &$lastChars)
162
    {
163
        if ($first) {
164
            $first = false;
165
            $read = ltrim($read, "\r\n");
166
        }
167
        $read = $lastChars . $read;
168
        $read = preg_replace('/\r\n|\r|\n/', "\r\n", $read);
169
        $lastChars = '';
170
        $matches = null;
171
        if (preg_match('/[\r\n]+$/', $read, $matches)) {
172
            $lastChars = $matches[0];
173
            $read = rtrim($read, "\r\n");
174
        }
175
    }
176
177
    /**
178
     * Copies the content of the $fromHandle stream into the $toHandle stream,
179
     * maintaining the current read position in $fromHandle.  The passed
180
     * MimePart is where $fromHandle originated after setting up filters on
181
     * $fromHandle.
182
     *
183
     * @param MimePart $part
184
     * @param resource $fromHandle
185
     * @param resource $toHandle
186
     */
187
    private function copyContentStream(MimePart $part, $fromHandle, $toHandle)
188
    {
189
        $pos = ftell($fromHandle);
190
        rewind($fromHandle);
191
        // changed from stream_copy_to_stream because hhvm seems to stop before
192
        // end of file for some reason
193
        $lastChars = '';
194
        $first = true;
195
        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...
196
            if ($part->isTextPart()) {
197
                $this->filterTextBeforeCopying($read, $first, $lastChars);
198
            }
199
            fwrite($toHandle, $read);
200
        }
201
        fseek($fromHandle, $pos);
202
    }
203
204
    /**
205
     * Writes out the content portion of the mime part based on the headers that
206
     * are set on the part, taking care of character/content-transfer encoding.
207
     *
208
     * @param MimePart $part
209
     * @param resource $handle
210
     */
211
    public function writePartContentTo(MimePart $part, $handle)
212
    {
213
        $contentHandle = $part->getContentResourceHandle();
214
        if ($contentHandle !== null) {
215
            
216
            $filter = $this->setCharsetStreamFilterOnPartStream($part);
217
            $leftovers = new StreamLeftover();
218
            $encodingFilter = $this->setTransferEncodingFilterOnStream(
219
                $part,
220
                $handle,
221
                $leftovers
222
            );
223
            $this->copyContentStream($part, $contentHandle, $handle);
224
            
225
            if ($encodingFilter !== null) {
226
                fflush($handle);
227
                stream_filter_remove($encodingFilter);
228
                fwrite($handle, $leftovers->encodedValue);
229
            }
230
            if ($filter !== null) {
231
                stream_filter_remove($filter);
232
            }
233
        }
234
    }
235
236
    /**
237
     * Writes out the MimePart to the passed resource.
238
     *
239
     * Takes care of character and content transfer encoding on the output based
240
     * on what headers are set.
241
     *
242
     * @param MimePart $part
243
     * @param resource $handle
244
     */
245
    public function writePartTo(MimePart $part, $handle)
246
    {
247
        $this->writePartHeadersTo($part, $handle);
248
        $this->writePartContentTo($part, $handle);
249
    }
250
}
251