Passed
Push — master ( 71d2a0...ed9bb7 )
by Zaahid
03:02
created

setCharsetStreamFilterOnPartStream()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 20
ccs 0
cts 15
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 13
nc 2
nop 1
crap 6
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 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");
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)
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...
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
            return stream_filter_append(
123
                $contentHandle,
124
                self::$typeToEncodingMap[$encoding],
125
                STREAM_FILTER_READ,
126
                $params
127
            );
128
        }
129
        return null;
130
    }
131
132
    /**
133
     * Trims out any starting and ending CRLF characters in the stream.
134
     *
135
     * @param string $read the read string, and where the result will be written
136
     *        to
137
     * @param bool $first set to true if this is the first set of read
138
     *        characters from the stream (ltrims CRLF)
139
     * @param string $lastChars contains any CRLF characters from the last $read
140
     *        line if it ended with a CRLF (because they're trimmed from the
141
     *        end, and get prepended to $read).
142
     */
143
    private function trimTextBeforeCopying(&$read, &$first, &$lastChars)
144
    {
145
        if ($first) {
146
            $first = false;
147
            $read = ltrim($read, "\r\n");
148
        }
149
        $read = $lastChars . $read;
150
        $lastChars = '';
151
        $matches = null;
152
        if (preg_match('/[\r\n]+$/', $read, $matches)) {
153
            $lastChars = $matches[0];
154
            $read = rtrim($read, "\r\n");
155
        }
156
    }
157
158
    /**
159
     * Copies the content of the $fromHandle stream into the $toHandle stream,
160
     * maintaining the current read position in $fromHandle.  The passed
161
     * MimePart is where $fromHandle originated after setting up filters on
162
     * $fromHandle.
163
     *
164
     * @param MimePart $part
165
     * @param resource $fromHandle
166
     * @param resource $toHandle
167
     */
168
    private function copyContentStream(MimePart $part, $fromHandle, $toHandle)
169
    {
170
        $pos = ftell($fromHandle);
171
        rewind($fromHandle);
172
        // changed from stream_copy_to_stream because hhvm seems to stop before
173
        // end of file for some reason
174
        $lastChars = '';
175
        $first = true;
176
        while (!feof($fromHandle)) {
177
            $read = fread($fromHandle, 1024);
178
            if (strcasecmp($part->getHeaderValue('Content-Encoding'), '8bit') !== 0) {
179
                $read = preg_replace('/\r\n|\r|\n/', "\r\n", $read);
180
            }
181
            if ($part->isTextPart()) {
182
                $this->trimTextBeforeCopying($read, $first, $lastChars);
183
            }
184
            fwrite($toHandle, $read);
185
        }
186
        fseek($fromHandle, $pos);
187
    }
188
189
    /**
190
     * Writes out the content portion of the mime part based on the headers that
191
     * are set on the part, taking care of character/content-transfer encoding.
192
     *
193
     * @param MimePart $part
194
     * @param resource $handle
195
     */
196
    public function writePartContentTo(MimePart $part, $handle)
197
    {
198
        $contentHandle = $part->getContentResourceHandle();
199
        if ($contentHandle !== null) {
200
            
201
            $filter = $this->setCharsetStreamFilterOnPartStream($part);
202
            $leftovers = new StreamLeftover();
203
            $encodingFilter = $this->setTransferEncodingFilterOnStream(
204
                $part,
205
                $handle,
206
                $leftovers
207
            );
208
            $this->copyContentStream($part, $contentHandle, $handle);
209
            
210
            if ($encodingFilter !== null) {
211
                fflush($handle);
212
                stream_filter_remove($encodingFilter);
213
                fwrite($handle, $leftovers->encodedValue);
214
            }
215
            if ($filter !== null) {
216
                stream_filter_remove($filter);
217
            }
218
        }
219
    }
220
221
    /**
222
     * Writes out the MimePart to the passed resource.
223
     *
224
     * Takes care of character and content transfer encoding on the output based
225
     * on what headers are set.
226
     *
227
     * @param MimePart $part
228
     * @param resource $handle
229
     */
230
    public function writePartTo(MimePart $part, $handle)
231
    {
232
        $this->writePartHeadersTo($part, $handle);
233
        $this->writePartContentTo($part, $handle);
234
    }
235
}
236