Passed
Push — 1.0.0 ( 4505d9...06b3ad )
by Zaahid
04:10
created

PartStreamFilterManager   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Test Coverage

Coverage 87.3%

Importance

Changes 0
Metric Value
dl 0
loc 216
ccs 55
cts 63
cp 0.873
rs 10
c 0
b 0
f 0
wmc 27

9 Methods

Rating   Name   Duplication   Size   Complexity  
A isTransferEncodingFilterChanged() 0 3 1
A __destruct() 0 7 3
A getContentStream() 0 11 4
B attachTransferEncodingFilter() 0 14 5
A setStream() 0 10 4
A attachCharsetFilter() 0 12 4
A __construct() 0 9 1
A reset() 0 15 3
A isCharsetFilterChanged() 0 4 2
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
use Psr\Http\Message\StreamInterface;
10
use ZBateson\MailMimeParser\Stream\StreamDecoratorFactory;
11
12
/**
13
 * Manages attached stream filters for a MessagePart's content resource handle.
14
 * 
15
 * The attached stream filters are:
16
 *  o Content-Transfer-Encoding filter to manage decoding from a supported
17
 *    encoding: quoted-printable, base64 and x-uuencode.
18
 *  o Charset conversion filter to convert to UTF-8
19
 *
20
 * @author Zaahid Bateson
21
 */
22
class PartStreamFilterManager
23
{
24
    /**
25
     * @var StreamInterface the content stream after attaching encoding filters
26
     */
27
    protected $filteredStream;
28
    
29
    /**
30
     * @var StreamInterface the underlying content stream without filters
31
     *      applied
32
     */
33
    protected $stream;
34
    
35
    /**
36
     * @var array map of the active encoding filter on the current handle.
37
     */
38
    private $encoding = [
39
        'type' => null,
40
        'filter' => null
41
    ];
42
    
43
    /**
44
     * @var array map of the active charset filter on the current handle.
45
     */
46
    private $charset = [
47
        'from' => null,
48
        'to' => null,
49
        'filter' => null
50
    ];
51
52
    /**
53
     * @var StreamDecoratorFactory used to apply psr7 stream decorators to the
54
     *      attached StreamInterface based on encoding.
55
     */
56
    private $streamDecoratorFactory;
57
    
58
    /**
59
     * @var string name of stream filter handling character set conversion
60
     */
61
    private $charsetConversionFilter;
62
    
63
    /**
64
     * Sets up filter names used for stream_filter_append
65
     * 
66
     * @param StreamDecoratorFactory $streamDecoratorFactory
67
     */
68 7
    public function __construct(StreamDecoratorFactory $streamDecoratorFactory)
69
    {
70 7
        $this->streamDecoratorFactory = $streamDecoratorFactory;
71 7
        $this->encodingEncoderMap = [
0 ignored issues
show
Bug Best Practice introduced by
The property encodingEncoderMap does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
72
            'quoted-printable' => '',
73
            'base64' => '',
74
            'x-uuencode' => ''
75
        ];
76 7
        $this->charsetConversionFilter = '';
77 7
    }
78
    
79
    /**
80
     * Closes the contentHandle if one is attached.
81
     */
82
    public function __destruct()
83
    {
84
        if ($this->filteredStream !== null) {
85
            $this->filteredStream->close();
86
        }
87
        if ($this->stream !== null) {
88
            $this->stream->close();
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 StreamInterface $stream
98
     */
99 7
    public function setStream(StreamInterface $stream)
100
    {
101 7
        if ($this->filteredStream !== null && $this->filteredStream !== $this->stream) {
102
            $this->filteredStream->close();
103
        }
104 7
        if ($this->stream !== null) {
105
            $this->stream->close();
106
        }
107 7
        $this->stream = $stream;
108 7
        $this->filteredStream = null;
109 7
    }
110
    
111
    /**
112
     * Returns true if the attached stream filter used for decoding the content
113
     * on the current handle is different from the one passed as an argument.
114
     * 
115
     * @param string $transferEncoding
116
     * @return boolean
117
     */
118 3
    private function isTransferEncodingFilterChanged($transferEncoding)
119
    {
120 3
        return ($transferEncoding !== $this->encoding['type']);
121
    }
122
    
123
    /**
124
     * Returns true if the attached stream filter used for charset conversion on
125
     * the current handle is different from the one needed based on the passed 
126
     * arguments.
127
     * 
128
     * @param string $fromCharset
129
     * @param string $toCharset
130
     * @return boolean
131
     */
132 3
    private function isCharsetFilterChanged($fromCharset, $toCharset)
133
    {
134 3
        return ($fromCharset !== $this->charset['from']
135 3
            || $toCharset !== $this->charset['to']);
136
    }
137
    
138
    /**
139
     * Attaches a decoding filter to the attached content handle, for the passed
140
     * $transferEncoding.
141
     * 
142
     * @param string $transferEncoding
143
     */
144 7
    protected function attachTransferEncodingFilter($transferEncoding)
145
    {
146 7
        if ($this->filteredStream !== null) {
147 7
            $this->encoding['type'] = $transferEncoding;
148
            switch ($transferEncoding) {
149 7
                case 'base64':
150 1
                    $this->filteredStream = $this->streamDecoratorFactory->newBase64StreamDecorator($this->filteredStream);
151 1
                    break;
152 6
                case 'x-uuencode':
153 2
                    $this->filteredStream = $this->streamDecoratorFactory->newUUStreamDecorator($this->filteredStream);
154 2
                    break;
155 5
                case 'quoted-printable':
156 3
                    $this->filteredStream = $this->streamDecoratorFactory->newQuotedPrintableStreamDecorator($this->filteredStream);
157 3
                    break;
158
            }
159
        }
160 7
    }
161
    
162
    /**
163
     * Attaches a charset conversion filter to the attached content handle, for
164
     * the passed arguments.
165
     * 
166
     * @param string $fromCharset the character set the content is encoded in
167
     * @param string $toCharset the target encoding to return
168
     */
169 7
    protected function attachCharsetFilter($fromCharset, $toCharset)
170
    {
171 7
        if ($this->filteredStream !== null) {
172 7
            if (!empty($fromCharset) && !empty($toCharset)) {
173 3
                $this->filteredStream = $this->streamDecoratorFactory->newCharsetStreamDecorator(
174 3
                    $this->filteredStream,
175 3
                    $fromCharset,
176 3
                    $toCharset
177
                );
178
            }
179 7
            $this->charset['from'] = $fromCharset;
180 7
            $this->charset['to'] = $toCharset;
181
        }
182 7
    }
183
    
184
    /**
185
     * Closes the attached resource handle, resets mapped encoding and charset
186
     * filters, and reopens the handle seeking back to the current position.
187
     * 
188
     * Note that closing/reopening is done because of the following differences
189
     * discovered between hhvm (up to 3.18 at least) and php:
190
     * 
191
     *  o stream_filter_remove wasn't triggering php_user_filter's onClose
192
     *    callback
193
     *  o read operations performed after stream_filter_remove weren't calling
194
     *    filter on php_user_filter
195
     * 
196
     * It seems stream_filter_remove doesn't work on hhvm, or isn't implemented
197
     * in the same way -- so closing and reopening seems to solve that.
198
     */
199 7
    public function reset()
200
    {
201 7
        if ($this->filteredStream !== null && $this->filteredStream !== $this->stream) {
202 1
            $this->filteredStream->close();
203
        }
204 7
        $this->encoding = [
205
            'type' => null,
206
            'filter' => null
207
        ];
208 7
        $this->charset = [
209
            'from' => null,
210
            'to' => null,
211
            'filter' => null
212
        ];
213 7
        $this->filteredStream = $this->stream;
214 7
    }
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 StreamInterface.
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
     * @return StreamInterface
226
     */
227 7
    public function getContentStream($transferEncoding, $fromCharset, $toCharset)
228
    {
229 7
        if ($this->filteredStream === null
230 3
            || $this->isTransferEncodingFilterChanged($transferEncoding)
231 7
            || $this->isCharsetFilterChanged($fromCharset, $toCharset)) {
232 7
            $this->reset();
233 7
            $this->attachTransferEncodingFilter($transferEncoding);
234 7
            $this->attachCharsetFilter($fromCharset, $toCharset);
235
        }
236 7
        $this->filteredStream->rewind();
237 7
        return $this->filteredStream;
238
    }
239
}
240