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
|
|||||||
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
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
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. ![]() |
|||||||
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
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
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. ![]() |
|||||||
307 | 92 | return $this->streamFactory->newDecoratedMessagePartStream( |
|||||
308 | 92 | $part, |
|||||
309 | 92 | $this->charsetStream |
|||||
0 ignored issues
–
show
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
![]() |
|||||||
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 |
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.