Passed
Branch 1.0.0 (a1adee)
by Zaahid
08:34
created

attachTransferEncodingFilter()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 13
ccs 11
cts 11
cp 1
rs 9.2
c 0
b 0
f 0
cc 4
eloc 8
nc 3
nop 1
crap 4
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\Part;
8
9
/**
10
 * Manages attached stream filters for a MessagePart's content resource handle.
11
 * 
12
 * The attached stream filters are:
13
 *  o Content-Transfer-Encoding filter to manage decoding from a supported
14
 *    encoding: quoted-printable, base64 and x-uuencode.
15
 *  o Charset conversion filter to convert to UTF-8
16
 *
17
 * @author Zaahid Bateson
18
 */
19
class PartStreamFilterManager
20
{
21
    /**
22
     * @var handle current opened handle if any
23
     */
24
    protected $contentHandle;
25
    
26
    /**
27
     * @var string the URL to open the content stream
28
     */
29
    protected $url;
30
    
31
    /**
32
     * @var array map of the active encoding filter on the current handle.
33
     */
34
    private $encoding = [
35
        'type' => null,
36
        'filter' => null
37
    ];
38
    
39
    /**
40
     * @var array map of the active charset filter on the current handle.
41
     */
42
    private $charset = [
43
        'from' => null,
44
        'to' => null,
45
        'filter' => null
46
    ];
47
    
48
    /**
49
     * @var array mapping Content-Transfer-Encoding header values to available
50
     *      stream filters.
51
     */
52
    private $encodingEncoderMap = [];
53
    
54
    /**
55
     * @var string name of stream filter handling character set conversion
56
     */
57
    private $charsetConversionFilter;
58
    
59
    /**
60
     * Sets up filter names used for stream_filter_append
61
     * 
62
     * @param string $quotedPrintableDecodeFilter
63
     * @param string $base64DecodeFilter
64
     * @param string $uudecodeFilter
65
     * @param string $charsetConversionFilter
66
     */
67 9
    public function __construct(
68
        $quotedPrintableDecodeFilter,
69
        $base64DecodeFilter,
70
        $uudecodeFilter,
71
        $charsetConversionFilter
72
    ) {
73 9
        $this->encodingEncoderMap = [
74 9
            'quoted-printable' => $quotedPrintableDecodeFilter,
75 9
            'base64' => $base64DecodeFilter,
76
            'x-uuencode' => $uudecodeFilter
77 9
        ];
78 9
        $this->charsetConversionFilter = $charsetConversionFilter;
79 9
    }
80
    
81
    /**
82
     * Closes the contentHandle if one is attached.
83
     */
84
    public function __destruct()
85
    {
86
        $this->url = null;
87
        if (is_resource($this->contentHandle)) {
88
            fclose($this->contentHandle);
89
        }
90
    }
91
    
92
    /**
93
     * Sets the URL used to open the content resource handle.
94
     * 
95
     * The function also closes the currently attached handle if any.
96
     * 
97
     * @param string $url
98
     */
99 9
    public function setContentUrl($url)
100
    {
101 9
        $this->url = $url;
102 9
        if (is_resource($this->contentHandle)) {
103 1
            fclose($this->contentHandle);
104 1
            $this->contentHandle = null;
105 1
        }
106 9
    }
107
    
108
    /**
109
     * Returns true if the attached stream filter used for decoding the content
110
     * on the current handle is different from the one passed as an argument.
111
     * 
112
     * @param string $transferEncoding
113
     * @return boolean
114
     */
115 4
    private function isTransferEncodingFilterChanged($transferEncoding)
116
    {
117 4
        return ($transferEncoding !== $this->encoding['type']);
118
    }
119
    
120
    /**
121
     * Returns true if the attached stream filter used for charset conversion on
122
     * the current handle is different from the one needed based on the passed 
123
     * arguments.
124
     * 
125
     * @param string $fromCharset
126
     * @param string $toCharset
127
     * @return boolean
128
     */
129 3
    private function isCharsetFilterChanged($fromCharset, $toCharset)
130
    {
131 3
        return ($fromCharset !== $this->charset['from']
132 3
            || $toCharset !== $this->charset['to']);
133
    }
134
    
135
    /**
136
     * Attaches a decoding filter to the attached content handle, for the passed
137
     * $transferEncoding.
138
     * 
139
     * @param string $transferEncoding
140
     */
141 9
    protected function attachTransferEncodingFilter($transferEncoding)
142
    {
143 9
        if ($this->contentHandle !== null) {
144 9
            if (!empty($transferEncoding) && isset($this->encodingEncoderMap[$transferEncoding])) {
145 7
                $this->encoding['filter'] = stream_filter_append(
146 7
                    $this->contentHandle,
147 7
                    $this->encodingEncoderMap[$transferEncoding],
148
                    STREAM_FILTER_READ
149 7
                );
150 7
            }
151 9
            $this->encoding['type'] = $transferEncoding;
152 9
        }
153 9
    }
154
    
155
    /**
156
     * Attaches a charset conversion filter to the attached content handle, for
157
     * the passed arguments.
158
     * 
159
     * @param string $fromCharset the character set the content is encoded in
160
     * @param string $toCharset the target encoding to return
161
     */
162 9
    protected function attachCharsetFilter($fromCharset, $toCharset)
163
    {
164 9
        if ($this->contentHandle !== null) {
165 9
            if (!empty($fromCharset) && !empty($toCharset)) {
166 5
                $this->charset['filter'] = stream_filter_append(
167 5
                    $this->contentHandle,
168 5
                    $this->charsetConversionFilter,
169 5
                    STREAM_FILTER_READ,
170 5
                    [ 'from' => $fromCharset, 'to' => $toCharset ]
171 5
                );
172 5
            }
173 9
            $this->charset['from'] = $fromCharset;
174 9
            $this->charset['to'] = $toCharset;
175 9
        }
176 9
    }
177
    
178
    /**
179
     * Closes the attached resource handle, resets mapped encoding and charset
180
     * filters, and reopens the handle seeking back to the current position.
181
     * 
182
     * Note that closing/reopening is done because of the following differences
183
     * discovered between hhvm (up to 3.18 at least) and php:
184
     * 
185
     *  o stream_filter_remove wasn't triggering php_user_filter's onClose
186
     *    callback
187
     *  o read operations performed after stream_filter_remove weren't calling
188
     *    filter on php_user_filter
189
     * 
190
     * It seems stream_filter_remove doesn't work on hhvm, or isn't implemented
191
     * in the same way -- so closing and reopening seems to solve that.
192
     */
193 9
    public function reset()
194
    {
195 9
        $pos = 0;
196 9
        if (is_resource($this->contentHandle)) {
197 3
            $pos = ftell($this->contentHandle);
198 3
            fclose($this->contentHandle);
199 3
            $this->contentHandle = null;
200 3
        }
201 9
        $this->encoding = [
202 9
            'type' => null,
203
            'filter' => null
204 9
        ];
205 9
        $this->charset = [
206 9
            'from' => null,
207 9
            'to' => null,
208
            'filter' => null
209 9
        ];
210 9
        if (!empty($this->url)) {
211 9
            $this->contentHandle = fopen($this->url, 'r');
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen($this->url, 'r') of type resource is incompatible with the declared type object<ZBateson\MailMime...er\Message\Part\handle> of property $contentHandle.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
212 9
            fseek($this->contentHandle, $pos);
213 9
        }
214 9
    }
215
    
216
    /**
217
     * Checks what transfer-encoding decoder filters and charset conversion
218
     * filters are attached on the handle, closing/reopening the handle if
219
     * different, before attaching relevant filters for the passed
220
     * $transferEncoding and charset arguments, and returning a resource handle.
221
     * 
222
     * @param string $transferEncoding
223
     * @param string $fromCharset the character set the content is encoded in
224
     * @param string $toCharset the target encoding to return
225
     */
226 9
    public function getContentHandle($transferEncoding, $fromCharset, $toCharset)
227
    {
228 9
        if (!is_resource($this->contentHandle)
229 9
            || $this->isTransferEncodingFilterChanged($transferEncoding)
230 9
            || $this->isCharsetFilterChanged($fromCharset, $toCharset)) {
231 9
            $this->reset();
232 9
            $this->attachTransferEncodingFilter($transferEncoding);
233 9
            $this->attachCharsetFilter($fromCharset, $toCharset);
234 9
        }
235 9
        return $this->contentHandle;
236
    }
237
}
238