PartStreamContainer::attachCharsetFilter()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 27
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 4

Importance

Changes 0
Metric Value
eloc 20
c 0
b 0
f 0
dl 0
loc 27
ccs 21
cts 21
cp 1
rs 9.6
cc 4
nc 5
nop 2
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
8
namespace ZBateson\MailMimeParser\Message;
9
10
use GuzzleHttp\Psr7\CachingStream;
11
use Psr\Http\Message\StreamInterface;
12
use Psr\Log\LoggerInterface;
13
use Psr\Log\LogLevel;
14
use ZBateson\MailMimeParser\ErrorBag;
15
use ZBateson\MailMimeParser\Stream\MessagePartStreamDecorator;
16
use ZBateson\MailMimeParser\Stream\StreamFactory;
17
use ZBateson\MbWrapper\MbWrapper;
18
use ZBateson\MbWrapper\UnsupportedCharsetException;
19
20
/**
21
 * Holds the stream and content stream objects for a part.
22
 *
23
 * Note that streams are not explicitly closed or detached on destruction of the
24
 * PartSreamContainer by design: the passed StreamInterfaces will be closed on
25
 * their destruction when no references to them remain, which is useful when the
26
 * streams are passed around.
27
 *
28
 * In addition, all the streams passed to PartStreamContainer should be wrapping
29
 * a ZBateson\StreamDecorators\NonClosingStream unless attached to a part by a
30
 * user, this is because MMP uses a single seekable stream for content and wraps
31
 * it in ZBateson\StreamDecorators\SeekingLimitStream objects for each part.
32
 *
33
 * @author Zaahid Bateson
34
 */
35
class PartStreamContainer extends ErrorBag
36
{
37
    /**
38
     * @var MbWrapper to test charsets and see if they're supported.
39
     */
40
    protected MbWrapper $mbWrapper;
41
42
    /**
43
     * @var bool if false, reading from a content stream with an unsupported
44
     *      charset will be tried with the default charset, otherwise the stream
45
     *      created with the unsupported charset, and an exception will be
46
     *      thrown when read from.
47
     */
48
    protected bool $throwExceptionReadingPartContentFromUnsupportedCharsets;
49
50
    /**
51
     * @var StreamFactory used to apply psr7 stream decorators to the
52
     *      attached StreamInterface based on encoding.
53
     */
54
    protected StreamFactory $streamFactory;
55
56
    /**
57
     * @var MessagePartStreamDecorator stream containing the part's headers,
58
     *      content and children wrapped in a MessagePartStreamDecorator
59
     */
60
    protected MessagePartStreamDecorator $stream;
61
62
    /**
63
     * @var StreamInterface a stream containing this part's content
64
     */
65
    protected ?StreamInterface $contentStream = null;
66
67
    /**
68
     * @var StreamInterface the content stream after attaching transfer encoding
69
     *      streams to $contentStream.
70
     */
71
    protected ?StreamInterface $decodedStream = null;
72
73
    /**
74
     * @var StreamInterface attached charset stream to $decodedStream
75
     */
76
    protected ?StreamInterface $charsetStream = null;
77
78
    /**
79
     * @var bool true if the stream should be detached when this container is
80
     *      destroyed.
81
     */
82
    protected bool $detachParsedStream = false;
83
84
    /**
85
     * @var array<string, null> map of the active encoding filter on the current handle.
86
     */
87
    private array $encoding = [
88
        'type' => null,
89
        'filter' => null
90
    ];
91
92
    /**
93
     * @var array<string, null> map of the active charset filter on the current handle.
94
     */
95
    private array $charset = [
96
        'from' => null,
97
        'to' => null,
98
        'filter' => null
99
    ];
100
101 117
    public function __construct(
102
        LoggerInterface $logger,
103
        StreamFactory $streamFactory,
104
        MbWrapper $mbWrapper,
105
        bool $throwExceptionReadingPartContentFromUnsupportedCharsets
106
    ) {
107 117
        parent::__construct($logger);
108 117
        $this->streamFactory = $streamFactory;
109 117
        $this->mbWrapper = $mbWrapper;
110 117
        $this->throwExceptionReadingPartContentFromUnsupportedCharsets = $throwExceptionReadingPartContentFromUnsupportedCharsets;
111
    }
112
113
    /**
114
     * Sets the part's stream containing the part's headers, content, and
115
     * children.
116
     */
117 108
    public function setStream(MessagePartStreamDecorator $stream) : static
118
    {
119 108
        $this->stream = $stream;
120 108
        return $this;
121
    }
122
123
    /**
124
     * Returns the part's stream containing the part's headers, content, and
125
     * children.
126
     */
127 95
    public function getStream() : MessagePartStreamDecorator
128
    {
129
        // error out if called before setStream, getStream should never return
130
        // null.
131 95
        $this->stream->rewind();
132 95
        return $this->stream;
133
    }
134
135
    /**
136
     * Returns true if there's a content stream associated with the part.
137
     */
138 106
    public function hasContent() : bool
139
    {
140 106
        return ($this->contentStream !== null);
141
    }
142
143
    /**
144
     * Attaches the passed stream as the content portion of this
145
     * StreamContainer.
146
     *
147
     * The content stream would represent the content portion of $this->stream.
148
     *
149
     * If the content is overridden, $this->stream should point to a dynamic
150
     * {@see ZBateson\Stream\MessagePartStream} that dynamically creates the
151
     * RFC822 formatted message based on the IMessagePart this
152
     * PartStreamContainer belongs to.
153
     *
154
     * setContentStream can be called with 'null' to indicate the IMessagePart
155
     * does not contain any content.
156
     */
157 114
    public function setContentStream(?StreamInterface $contentStream = null) : static
158
    {
159 114
        $this->contentStream = $contentStream;
160 114
        $this->decodedStream = null;
161 114
        $this->charsetStream = null;
162 114
        return $this;
163
    }
164
165
    /**
166
     * Returns true if the attached stream filter used for decoding the content
167
     * on the current handle is different from the one passed as an argument.
168
     */
169 83
    private function isTransferEncodingFilterChanged(?string $transferEncoding) : bool
170
    {
171 83
        return ($transferEncoding !== $this->encoding['type']);
172
    }
173
174
    /**
175
     * Returns true if the attached stream filter used for charset conversion on
176
     * the current handle is different from the one needed based on the passed
177
     * arguments.
178
     *
179
     */
180 71
    private function isCharsetFilterChanged(string $fromCharset, string $toCharset) : bool
181
    {
182 71
        return ($fromCharset !== $this->charset['from']
183 71
            || $toCharset !== $this->charset['to']);
184
    }
185
186
    /**
187
     * Attaches a decoding filter to the attached content handle, for the passed
188
     * $transferEncoding.
189
     */
190 113
    protected function attachTransferEncodingFilter(?string $transferEncoding) : static
191
    {
192 113
        if ($this->decodedStream !== null) {
193 113
            $this->encoding['type'] = $transferEncoding;
194 113
            $this->decodedStream = new CachingStream($this->streamFactory->getTransferEncodingDecoratedStream(
195 113
                $this->decodedStream,
196 113
                $transferEncoding
197 113
            ));
198
        }
199 113
        return $this;
200
    }
201
202
    /**
203
     * Attaches a charset conversion filter to the attached content handle, for
204
     * the passed arguments.
205
     *
206
     * @param string $fromCharset the character set the content is encoded in
207
     * @param string $toCharset the target encoding to return
208
     */
209 92
    protected function attachCharsetFilter(string $fromCharset, string $toCharset) : static
210
    {
211 92
        if ($this->charsetStream !== null) {
212 92
            if (!$this->throwExceptionReadingPartContentFromUnsupportedCharsets) {
213
                try {
214 91
                    $this->mbWrapper->convert('t', $fromCharset, $toCharset);
215 90
                    $this->charsetStream = new CachingStream($this->streamFactory->newCharsetStream(
216 90
                        $this->charsetStream,
217 90
                        $fromCharset,
218 90
                        $toCharset
219 90
                    ));
220 1
                } catch (UnsupportedCharsetException $ex) {
221 1
                    $this->addError('Unsupported character set found', LogLevel::ERROR, $ex);
222 1
                    $this->charsetStream = new CachingStream($this->charsetStream);
223
                }
224
            } else {
225 1
                $this->charsetStream = new CachingStream($this->streamFactory->newCharsetStream(
226 1
                    $this->charsetStream,
227 1
                    $fromCharset,
228 1
                    $toCharset
229 1
                ));
230
            }
231 92
            $this->charsetStream->rewind();
232 92
            $this->charset['from'] = $fromCharset;
233 92
            $this->charset['to'] = $toCharset;
234
        }
235 92
        return $this;
236
    }
237
238
    /**
239
     * Resets just the charset stream, and rewinds the decodedStream.
240
     */
241 92
    private function resetCharsetStream() : static
242
    {
243 92
        $this->charset = [
244 92
            'from' => null,
245 92
            'to' => null,
246 92
            'filter' => null
247 92
        ];
248 92
        $this->decodedStream->rewind();
0 ignored issues
show
Bug introduced by
The method rewind() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

248
        $this->decodedStream->/** @scrutinizer ignore-call */ 
249
                              rewind();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
249 92
        $this->charsetStream = $this->decodedStream;
250 92
        return $this;
251
    }
252
253
    /**
254
     * Resets cached encoding and charset streams, and rewinds the stream.
255
     */
256 113
    public function reset() : static
257
    {
258 113
        $this->encoding = [
259 113
            'type' => null,
260 113
            'filter' => null
261 113
        ];
262 113
        $this->charset = [
263 113
            'from' => null,
264 113
            'to' => null,
265 113
            'filter' => null
266 113
        ];
267 113
        $this->contentStream->rewind();
0 ignored issues
show
Bug introduced by
The method rewind() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

267
        $this->contentStream->/** @scrutinizer ignore-call */ 
268
                              rewind();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
268 113
        $this->decodedStream = $this->contentStream;
269 113
        $this->charsetStream = $this->contentStream;
270 113
        return $this;
271
    }
272
273
    /**
274
     * Checks what transfer-encoding decoder stream and charset conversion
275
     * stream are currently attached on the underlying contentStream, and resets
276
     * them if the requested arguments differ from the currently assigned ones.
277
     *
278
     * @param IMessagePart $part the part the stream belongs to
279
     * @param string $transferEncoding the transfer encoding
280
     * @param string $fromCharset the character set the content is encoded in
281
     * @param string $toCharset the target encoding to return
282
     */
283 112
    public function getContentStream(
284
        IMessagePart $part,
285
        ?string $transferEncoding,
286
        ?string $fromCharset,
287
        ?string $toCharset
288
    ) : ?MessagePartStreamDecorator {
289 112
        if ($this->contentStream === null) {
290 2
            return null;
291
        }
292 111
        if (empty($fromCharset) || empty($toCharset)) {
293 72
            return $this->getBinaryContentStream($part, $transferEncoding);
294
        }
295 92
        if ($this->charsetStream === null
296 71
            || $this->isTransferEncodingFilterChanged($transferEncoding)
297 92
            || $this->isCharsetFilterChanged($fromCharset, $toCharset)) {
298 92
            if ($this->charsetStream === null
299 92
                || $this->isTransferEncodingFilterChanged($transferEncoding)) {
300 92
                $this->reset();
301 92
                $this->attachTransferEncodingFilter($transferEncoding);
302
            }
303 92
            $this->resetCharsetStream();
304 92
            $this->attachCharsetFilter($fromCharset, $toCharset);
305
        }
306 92
        $this->charsetStream->rewind();
0 ignored issues
show
Bug introduced by
The method rewind() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

306
        $this->charsetStream->/** @scrutinizer ignore-call */ 
307
                              rewind();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
307 92
        return $this->streamFactory->newDecoratedMessagePartStream(
308 92
            $part,
309 92
            $this->charsetStream
0 ignored issues
show
Bug introduced by
It seems like $this->charsetStream can also be of type null; however, parameter $stream of ZBateson\MailMimeParser\...atedMessagePartStream() does only seem to accept Psr\Http\Message\StreamInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

309
            /** @scrutinizer ignore-type */ $this->charsetStream
Loading history...
310 92
        );
311
    }
312
313
    /**
314
     * Checks what transfer-encoding decoder stream is attached on the
315
     * underlying stream, and resets it if the requested arguments differ.
316
     */
317 75
    public function getBinaryContentStream(IMessagePart $part, ?string $transferEncoding = null) : ?MessagePartStreamDecorator
318
    {
319 75
        if ($this->contentStream === null) {
320 1
            return null;
321
        }
322 74
        if ($this->decodedStream === null
323 74
            || $this->isTransferEncodingFilterChanged($transferEncoding)) {
324 74
            $this->reset();
325 74
            $this->attachTransferEncodingFilter($transferEncoding);
326
        }
327 74
        $this->decodedStream->rewind();
328 74
        return $this->streamFactory->newDecoratedMessagePartStream($part, $this->decodedStream);
329
    }
330
331 1
    protected function getErrorBagChildren() : array
332
    {
333 1
        return [];
334
    }
335
}
336