Passed
Pull Request — master (#171)
by Zaahid
03:23
created

PartStreamContainer::isCharsetFilterChanged()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 2
crap 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;
8
9
use Psr\Http\Message\StreamInterface;
10
use GuzzleHttp\Psr7\CachingStream;
11
use ZBateson\MailMimeParser\Stream\StreamFactory;
12
13
/**
14
 * Holds the stream and content stream objects for a part.
15
 *
16
 * Note that streams are not explicitly closed or detached on destruction of the
17
 * PartSreamContainer by design: the passed StreamInterfaces will be closed on
18
 * their destruction when no references to them remain, which is useful when the
19
 * streams are passed around.
20
 *
21
 * In addition, all the streams passed to PartStreamContainer should be wrapping
22
 * a ZBateson\StreamDecorators\NonClosingStream unless attached to a part by a
23
 * user, this is because MMP uses a single seekable stream for content and wraps
24
 * it in ZBateson\StreamDecorators\SeekingLimitStream objects for each part.
25
 *
26
 * @author Zaahid Bateson
27
 */
28
class PartStreamContainer
29
{
30
    /**
31
     * @var StreamFactory used to apply psr7 stream decorators to the
32
     *      attached StreamInterface based on encoding.
33
     */
34
    protected $streamFactory;
35
36
    /**
37
     * @var StreamInterface stream containing the part's headers, content and
38
     *      children
39
     */
40
    protected $stream;
41
42
    /**
43
     * @var StreamInterface a stream containing this part's content
44
     */
45
    protected $contentStream;
46
47
    /**
48
     * @var StreamInterface the content stream after attaching transfer encoding
49
     *      streams to $contentStream.
50
     */
51
    protected $decodedStream;
52
53
    /**
54
     * @var StreamInterface attached charset stream to $decodedStream
55
     */
56
    protected $charsetStream;
57
58
    /**
59
     * @var bool true if the stream should be detached when this container is
60
     *      destroyed.
61
     */
62
    protected $detachParsedStream;
63
64
    /**
65
     * @var array map of the active encoding filter on the current handle.
66
     */
67
    private $encoding = [
68
        'type' => null,
69
        'filter' => null
70
    ];
71
72
    /**
73
     * @var array map of the active charset filter on the current handle.
74
     */
75
    private $charset = [
76
        'from' => null,
77
        'to' => null,
78
        'filter' => null
79
    ];
80
81 114
    public function __construct(StreamFactory $streamFactory)
82
    {
83 114
        $this->streamFactory = $streamFactory;
84 114
    }
85
86
    /**
87
     * Sets the part's stream containing the part's headers, content, and
88
     * children.
89
     *
90
     * @param StreamInterface $stream
91
     */
92 105
    public function setStream(StreamInterface $stream)
93
    {
94 105
        $this->stream = $stream;
95 105
    }
96
97
    /**
98
     * Returns the part's stream containing the part's headers, content, and
99
     * children.
100
     *
101
     * @return StreamInterface
102
     */
103 93
    public function getStream()
104
    {
105
        // error out if called before setStream, getStream should never return
106
        // null.
107 93
        $this->stream->rewind();
108 93
        return $this->stream;
109
    }
110
111
    /**
112
     * Returns true if there's a content stream associated with the part.
113
     *
114
     * @return boolean
115
     */
116 103
    public function hasContent()
117
    {
118 103
        return ($this->contentStream !== null);
119
    }
120
121
    /**
122
     * Attaches the passed stream as the content portion of this
123
     * StreamContainer.
124
     *
125
     * The content stream would represent the content portion of $this->stream.
126
     *
127
     * If the content is overridden, $this->stream should point to a dynamic
128
     * {@see ZBateson\Stream\MessagePartStream} that dynamically creates the
129
     * RFC822 formatted message based on the IMessagePart this
130
     * PartStreamContainer belongs to.
131
     *
132
     * setContentStream can be called with 'null' to indicate the IMessagePart
133
     * does not contain any content.
134
     *
135
     * @param StreamInterface $contentStream
136
     */
137 111
    public function setContentStream(StreamInterface $contentStream = null)
138
    {
139 111
        $this->contentStream = $contentStream;
140 111
        $this->decodedStream = null;
141 111
        $this->charsetStream = null;
142 111
    }
143
144
    /**
145
     * Returns true if the attached stream filter used for decoding the content
146
     * on the current handle is different from the one passed as an argument.
147
     *
148
     * @param string $transferEncoding
149
     * @return boolean
150
     */
151 82
    private function isTransferEncodingFilterChanged($transferEncoding)
152
    {
153 82
        return ($transferEncoding !== $this->encoding['type']);
154
    }
155
156
    /**
157
     * Returns true if the attached stream filter used for charset conversion on
158
     * the current handle is different from the one needed based on the passed
159
     * arguments.
160
     *
161
     * @param string $fromCharset
162
     * @param string $toCharset
163
     * @return boolean
164
     */
165 70
    private function isCharsetFilterChanged($fromCharset, $toCharset)
166
    {
167 70
        return ($fromCharset !== $this->charset['from']
168 70
            || $toCharset !== $this->charset['to']);
169
    }
170
171
    /**
172
     * Attaches a decoding filter to the attached content handle, for the passed
173
     * $transferEncoding.
174
     *
175
     * @param string $transferEncoding
176
     */
177 110
    protected function attachTransferEncodingFilter($transferEncoding)
178
    {
179 110
        if ($this->decodedStream !== null) {
180 110
            $this->encoding['type'] = $transferEncoding;
181 110
            $assign = null;
182 110
            switch ($transferEncoding) {
183 110
                case 'base64':
184 48
                    $assign = $this->streamFactory->newBase64Stream($this->decodedStream);
185 48
                    break;
186 104
                case 'x-uuencode':
187 11
                    $assign = $this->streamFactory->newUUStream($this->decodedStream);
188 11
                    break;
189 103
                case 'quoted-printable':
190 62
                    $assign = $this->streamFactory->newQuotedPrintableStream($this->decodedStream);
191 62
                    break;
192
            }
193 110
            if ($assign !== null) {
194 87
                $this->decodedStream = new CachingStream($assign);
195
            }
196
        }
197 110
    }
198
199
    /**
200
     * Attaches a charset conversion filter to the attached content handle, for
201
     * the passed arguments.
202
     *
203
     * @param string $fromCharset the character set the content is encoded in
204
     * @param string $toCharset the target encoding to return
205
     */
206 90
    protected function attachCharsetFilter($fromCharset, $toCharset)
207
    {
208 90
        if ($this->charsetStream !== null) {
209 90
            $this->charsetStream = new CachingStream($this->streamFactory->newCharsetStream(
210 90
                $this->charsetStream,
211 90
                $fromCharset,
212 90
                $toCharset
213
            ));
214 90
            $this->charset['from'] = $fromCharset;
215 90
            $this->charset['to'] = $toCharset;
216
        }
217 90
    }
218
219
    /**
220
     * Resets just the charset stream, and rewinds the decodedStream.
221
     */
222 90
    private function resetCharsetStream()
223
    {
224 90
        $this->charset = [
225
            'from' => null,
226
            'to' => null,
227
            'filter' => null
228
        ];
229 90
        $this->decodedStream->rewind();
230 90
        $this->charsetStream = $this->decodedStream;
231 90
    }
232
233
    /**
234
     * Resets cached encoding and charset streams, and rewinds the stream.
235
     */
236 110
    public function reset()
237
    {
238 110
        $this->encoding = [
239
            'type' => null,
240
            'filter' => null
241
        ];
242 110
        $this->charset = [
243
            'from' => null,
244
            'to' => null,
245
            'filter' => null
246
        ];
247 110
        $this->contentStream->rewind();
248 110
        $this->decodedStream = $this->contentStream;
249 110
        $this->charsetStream = $this->contentStream;
250 110
    }
251
252
    /**
253
     * Checks what transfer-encoding decoder stream and charset conversion
254
     * stream are currently attached on the underlying contentStream, and resets
255
     * them if the requested arguments differ from the currently assigned ones.
256
     *
257
     * @param string $transferEncoding the transfer encoding
258
     * @param string $fromCharset the character set the content is encoded in
259
     * @param string $toCharset the target encoding to return
260
     * @return StreamInterface
261
     */
262 109
    public function getContentStream($transferEncoding, $fromCharset, $toCharset)
263
    {
264 109
        if ($this->contentStream === null) {
265 2
            return null;
266
        }
267 108
        if (empty($fromCharset) || empty($toCharset)) {
268 71
            return $this->getBinaryContentStream($transferEncoding);
269
        }
270 90
        if ($this->charsetStream === null
271 70
            || $this->isTransferEncodingFilterChanged($transferEncoding)
272 90
            || $this->isCharsetFilterChanged($fromCharset, $toCharset)) {
273 90
            if ($this->charsetStream === null
274 90
                || $this->isTransferEncodingFilterChanged($transferEncoding)) {
275 90
                $this->reset();
276 90
                $this->attachTransferEncodingFilter($transferEncoding);
277
            }
278 90
            $this->resetCharsetStream();
279 90
            $this->attachCharsetFilter($fromCharset, $toCharset);
280
        }
281 90
        $this->charsetStream->rewind();
282 90
        return $this->charsetStream;
283
    }
284
285
    /**
286
     * Checks what transfer-encoding decoder stream is attached on the
287
     * underlying stream, and resets it if the requested arguments differ.
288
     *
289
     * @param string $transferEncoding
290
     * @return StreamInterface
291
     */
292 74
    public function getBinaryContentStream($transferEncoding)
293
    {
294 74
        if ($this->contentStream === null) {
295 1
            return null;
296
        }
297 73
        if ($this->decodedStream === null
298 73
            || $this->isTransferEncodingFilterChanged($transferEncoding)) {
299 73
            $this->reset();
300 73
            $this->attachTransferEncodingFilter($transferEncoding);
301
        }
302 73
        $this->decodedStream->rewind();
303 73
        return $this->decodedStream;
304
    }
305
}
306