Passed
Push — master ( 2ea88e...404a7f )
by Zaahid
03:05
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' => 'mmp-convert.quoted-printable-encode',
37
        'base64' => 'mmp-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
    public static function getInstance()
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");
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
     * @param MimePart $part
105
     * @param resource $handle
106
     * @param StreamLeftover $leftovers
107
     * @return resource the stream filter
108
     */
109
    private function setTransferEncodingFilterOnStream(MimePart $part, $handle, StreamLeftover $leftovers)
110
    {
111
        $contentHandle = $part->getContentResourceHandle();
112
        $encoding = strtolower($part->getHeaderValue('Content-Transfer-Encoding'));
113
        $params = array_merge(self::$defaultStreamFilterParams, [
114
            'leftovers' => $leftovers,
115
            'filename' => $part->getHeaderParameter(
116
                'Content-Type',
117
                'name',
118
                'null'
119
            )
120
        ]);
121
        if (isset(self::$typeToEncodingMap[$encoding])) {
122
            if (defined('HHVM_VERSION') && HHVM_VERSION < '3.18.0') {
123
                return stream_filter_append(
124
                    $handle,
125
                    self::$typeToEncodingMap[$encoding],
126
                    STREAM_FILTER_WRITE,
127
                    $params
128
                );
129
            } else {
130
                return stream_filter_append(
131
                    $contentHandle,
132
                    self::$typeToEncodingMap[$encoding],
133
                    STREAM_FILTER_READ,
134
                    $params
135
                );
136
            }
137
        }
138
        return null;
139
    }
140
141
    /**
142
     * Trims out any starting and ending CRLF characters in the stream.
143
     *
144
     * @param string $read the read string, and where the result will be written
145
     *        to
146
     * @param bool $first set to true if this is the first set of read
147
     *        characters from the stream (ltrims CRLF)
148
     * @param string $lastChars contains any CRLF characters from the last $read
149
     *        line if it ended with a CRLF (because they're trimmed from the
150
     *        end, and get prepended to $read).
151
     */
152
    private function trimTextBeforeCopying(&$read, &$first, &$lastChars)
153
    {
154
        if ($first) {
155
            $first = false;
156
            $read = ltrim($read, "\r\n");
157
        }
158
        $read = $lastChars . $read;
159
        $lastChars = '';
160
        $matches = null;
161
        if (preg_match('/[\r\n]+$/', $read, $matches)) {
162
            $lastChars = $matches[0];
163
            $read = rtrim($read, "\r\n");
164
        }
165
    }
166
167
    /**
168
     * Copies the content of the $fromHandle stream into the $toHandle stream,
169
     * maintaining the current read position in $fromHandle.  The passed
170
     * MimePart is where $fromHandle originated after setting up filters on
171
     * $fromHandle.
172
     *
173
     * @param MimePart $part
174
     * @param resource $fromHandle
175
     * @param resource $toHandle
176
     */
177
    private function copyContentStream(MimePart $part, $fromHandle, $toHandle)
178
    {
179
        $pos = ftell($fromHandle);
180
        rewind($fromHandle);
181
        // changed from stream_copy_to_stream because hhvm seems to stop before
182
        // end of file for some reason
183
        $lastChars = '';
184
        $first = true;
185
        while (!feof($fromHandle)) {
186
            $read = fread($fromHandle, 1024);
187
            if (strcasecmp($part->getHeaderValue('Content-Encoding'), '8bit') !== 0) {
188
                $read = preg_replace('/\r\n|\r|\n/', "\r\n", $read);
189
            }
190
            if ($part->isTextPart()) {
191
                $this->trimTextBeforeCopying($read, $first, $lastChars);
192
            }
193
            fwrite($toHandle, $read);
194
        }
195
        fseek($fromHandle, $pos);
196
    }
197
198
    /**
199
     * Writes out the content portion of the mime part based on the headers that
200
     * are set on the part, taking care of character/content-transfer encoding.
201
     *
202
     * @param MimePart $part
203
     * @param resource $handle
204
     */
205
    public function writePartContentTo(MimePart $part, $handle)
206
    {
207
        $contentHandle = $part->getContentResourceHandle();
208
        if ($contentHandle !== null) {
209
            
210
            $filter = $this->setCharsetStreamFilterOnPartStream($part);
211
            $leftovers = new StreamLeftover();
212
            $encodingFilter = $this->setTransferEncodingFilterOnStream(
213
                $part,
214
                $handle,
215
                $leftovers
216
            );
217
            $this->copyContentStream($part, $contentHandle, $handle);
218
            
219
            if ($encodingFilter !== null) {
220
                fflush($handle);
221
                stream_filter_remove($encodingFilter);
222
                fwrite($handle, $leftovers->encodedValue);
223
            }
224
            if ($filter !== null) {
225
                stream_filter_remove($filter);
226
            }
227
        }
228
    }
229
230
    /**
231
     * Writes out the MimePart to the passed resource.
232
     *
233
     * Takes care of character and content transfer encoding on the output based
234
     * on what headers are set.
235
     *
236
     * @param MimePart $part
237
     * @param resource $handle
238
     */
239
    public function writePartTo(MimePart $part, $handle)
240
    {
241
        $this->writePartHeadersTo($part, $handle);
242
        $this->writePartContentTo($part, $handle);
243
    }
244
}
245