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\Parser\Proxy; |
||
9 | |||
10 | use Psr\Log\LogLevel; |
||
11 | use ZBateson\MailMimeParser\Header\HeaderConsts; |
||
12 | use ZBateson\MailMimeParser\Header\ParameterHeader; |
||
13 | use ZBateson\MailMimeParser\Message\IMessagePart; |
||
14 | |||
15 | /** |
||
16 | * A bi-directional parser-to-part proxy for MimeParser and IMimeParts. |
||
17 | * |
||
18 | * @author Zaahid Bateson |
||
19 | */ |
||
20 | class ParserMimePartProxy extends ParserPartProxy |
||
21 | { |
||
22 | /** |
||
23 | * @var bool set to true once the end boundary of the currently-parsed |
||
24 | * part is found. |
||
25 | */ |
||
26 | protected bool $endBoundaryFound = false; |
||
27 | |||
28 | /** |
||
29 | * @var bool set to true once a boundary belonging to this parent's part |
||
30 | * is found. |
||
31 | */ |
||
32 | protected bool $parentBoundaryFound = false; |
||
33 | |||
34 | /** |
||
35 | * @var bool true once all children of this part have been parsed. |
||
36 | */ |
||
37 | protected bool $allChildrenParsed = false; |
||
38 | |||
39 | /** |
||
40 | * @var ParserPartProxy[] Array of all parsed children. |
||
41 | */ |
||
42 | protected array $children = []; |
||
43 | |||
44 | /** |
||
45 | * @var ParserPartProxy[] Parsed children used as a 'first-in-first-out' |
||
46 | * stack as children are parsed. |
||
47 | */ |
||
48 | protected array $childrenStack = []; |
||
49 | |||
50 | /** |
||
51 | * @var ParserPartProxy Reference to the last child added to this part. |
||
52 | */ |
||
53 | protected ?ParserPartProxy $lastAddedChild = null; |
||
54 | |||
55 | /** |
||
56 | * @var ?string NULL if the current part does not have a boundary, and |
||
57 | * otherwise contains the value of the boundary parameter of the |
||
58 | * content-type header if the part contains one. |
||
59 | */ |
||
60 | private ?string $mimeBoundary = null; |
||
61 | |||
62 | /** |
||
63 | * @var bool FALSE if not queried for in the content-type header of this |
||
64 | * part and set in $mimeBoundary. |
||
65 | */ |
||
66 | private bool $mimeBoundaryQueried = false; |
||
67 | |||
68 | /** |
||
69 | * Ensures that the last child added to this part is fully parsed (content |
||
70 | * and children). |
||
71 | */ |
||
72 | 108 | protected function ensureLastChildParsed() : static |
|
73 | { |
||
74 | 108 | if ($this->lastAddedChild !== null) { |
|
75 | 77 | $this->lastAddedChild->parseAll(); |
|
76 | } |
||
77 | 108 | return $this; |
|
78 | } |
||
79 | |||
80 | /** |
||
81 | * Parses the next child of this part and adds it to the 'stack' of |
||
82 | * children. |
||
83 | */ |
||
84 | 108 | protected function parseNextChild() : static |
|
85 | { |
||
86 | 108 | if ($this->allChildrenParsed) { |
|
87 | 20 | return $this; |
|
88 | } |
||
89 | 108 | $this->parseContent(); |
|
90 | 108 | $this->ensureLastChildParsed(); |
|
91 | 108 | $next = $this->parser->parseNextChild($this); |
|
92 | 108 | if ($next !== null) { |
|
93 | 77 | $this->children[] = $next; |
|
94 | 77 | $this->childrenStack[] = $next; |
|
95 | 77 | $this->lastAddedChild = $next; |
|
96 | } else { |
||
97 | 108 | $this->allChildrenParsed = true; |
|
98 | } |
||
99 | 108 | return $this; |
|
100 | } |
||
101 | |||
102 | /** |
||
103 | * Returns the next child part if one exists, popping it from the internal |
||
104 | * 'stack' of children, attempting to parse a new one if the stack is empty, |
||
105 | * and returning null if there are no more children. |
||
106 | * |
||
107 | * @return ?IMessagePart the child part. |
||
108 | */ |
||
109 | 106 | public function popNextChild() : ?IMessagePart |
|
110 | { |
||
111 | 106 | if (empty($this->childrenStack)) { |
|
112 | 106 | $this->parseNextChild(); |
|
113 | } |
||
114 | 106 | $proxy = \array_shift($this->childrenStack); |
|
115 | 106 | return ($proxy !== null) ? $proxy->getPart() : null; |
|
116 | } |
||
117 | |||
118 | /** |
||
119 | * Parses all content and children for this part. |
||
120 | */ |
||
121 | 106 | public function parseAll() : static |
|
122 | { |
||
123 | 106 | $this->parseContent(); |
|
124 | 106 | while (!$this->allChildrenParsed) { |
|
125 | 27 | $this->parseNextChild(); |
|
126 | } |
||
127 | 106 | return $this; |
|
128 | } |
||
129 | |||
130 | /** |
||
131 | * Returns a ParameterHeader representing the parsed Content-Type header for |
||
132 | * this part. |
||
133 | */ |
||
134 | 79 | public function getContentType() : ?ParameterHeader |
|
135 | { |
||
136 | 79 | return $this->getHeaderContainer()->get(HeaderConsts::CONTENT_TYPE); |
|
137 | } |
||
138 | |||
139 | /** |
||
140 | * Returns the parsed boundary parameter of the Content-Type header if set |
||
141 | * for a multipart message part. |
||
142 | * |
||
143 | */ |
||
144 | 78 | public function getMimeBoundary() : ?string |
|
145 | { |
||
146 | 78 | if ($this->mimeBoundaryQueried === false) { |
|
147 | 78 | $this->mimeBoundaryQueried = true; |
|
148 | 78 | $contentType = $this->getContentType(); |
|
149 | 78 | if ($contentType !== null) { |
|
150 | 76 | $this->mimeBoundary = $contentType->getValueFor('boundary'); |
|
151 | } |
||
152 | } |
||
153 | 78 | return $this->mimeBoundary; |
|
154 | } |
||
155 | |||
156 | /** |
||
157 | * Returns true if the passed $line of read input matches this part's mime |
||
158 | * boundary, or any of its parent's mime boundaries for a multipart message. |
||
159 | * |
||
160 | * If the passed $line is the ending boundary for the current part, |
||
161 | * $this->isEndBoundaryFound will return true after. |
||
162 | */ |
||
163 | 76 | public function setEndBoundaryFound(string $line) : bool |
|
164 | { |
||
165 | 76 | $boundary = $this->getMimeBoundary(); |
|
166 | 76 | if ($this->getParent() !== null && $this->getParent()->setEndBoundaryFound($line)) { |
|
167 | 74 | $this->parentBoundaryFound = true; |
|
168 | 74 | return true; |
|
169 | 76 | } elseif ($boundary !== null) { |
|
170 | 74 | if ($line === "--$boundary--") { |
|
171 | 74 | $this->endBoundaryFound = true; |
|
172 | 74 | return true; |
|
173 | 74 | } elseif ($line === "--$boundary") { |
|
174 | 74 | return true; |
|
175 | } |
||
176 | } |
||
177 | 38 | return false; |
|
178 | } |
||
179 | |||
180 | /** |
||
181 | * Returns true if the parser passed an input line to setEndBoundary that |
||
182 | * matches a parent's mime boundary, and the following input belongs to a |
||
183 | * new part under its parent. |
||
184 | * |
||
185 | */ |
||
186 | 104 | public function isParentBoundaryFound() : bool |
|
187 | { |
||
188 | 104 | return ($this->parentBoundaryFound); |
|
189 | } |
||
190 | |||
191 | /** |
||
192 | * Returns true if an end boundary was found for this part. |
||
193 | * |
||
194 | */ |
||
195 | 76 | public function isEndBoundaryFound() : bool |
|
196 | { |
||
197 | 76 | return ($this->endBoundaryFound); |
|
198 | } |
||
199 | |||
200 | /** |
||
201 | * Called once EOF is reached while reading content. The method sets the |
||
202 | * flag used by isParentBoundaryFound() to true on this part and all parent |
||
203 | * parts. |
||
204 | * |
||
205 | */ |
||
206 | 99 | public function setEof() : static |
|
207 | { |
||
208 | 99 | $this->parentBoundaryFound = true; |
|
209 | 99 | if ($this->getParent() !== null) { |
|
210 | 66 | $this->getParent()->setEof(); |
|
211 | } |
||
212 | 99 | return $this; |
|
213 | } |
||
214 | |||
215 | /** |
||
216 | * Overridden to set a 0-length content length, and a stream end pos of -2 |
||
217 | * if the passed end pos is before the start pos (can happen if a mime |
||
218 | * end boundary doesn't have an empty line before the next parent start |
||
219 | * boundary). |
||
220 | */ |
||
221 | 106 | public function setStreamPartAndContentEndPos(int $streamContentEndPos) : static |
|
222 | { |
||
223 | // check if we're expecting a boundary and didn't find one |
||
224 | 106 | if (!$this->endBoundaryFound && !$this->parentBoundaryFound) { |
|
225 | 106 | if (!empty($this->mimeBoundary) || ($this->getParent() !== null && !empty($this->getParent()->mimeBoundary))) { |
|
226 | 73 | $this->addError('End boundary for part not found', LogLevel::WARNING); |
|
227 | } |
||
228 | } |
||
229 | 106 | $start = $this->getStreamContentStartPos(); |
|
230 | 106 | if ($streamContentEndPos - $start < 0) { |
|
231 | 26 | parent::setStreamPartAndContentEndPos($start); |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
232 | 26 | $this->setStreamPartEndPos($streamContentEndPos); |
|
233 | } else { |
||
234 | 106 | parent::setStreamPartAndContentEndPos($streamContentEndPos); |
|
235 | } |
||
236 | 106 | return $this; |
|
237 | } |
||
238 | |||
239 | /** |
||
240 | * Sets the length of the last line ending read by MimeParser (e.g. 2 for |
||
241 | * '\r\n', or 1 for '\n'). |
||
242 | * |
||
243 | * The line ending may not belong specifically to this part, so |
||
244 | * ParserMimePartProxy simply calls setLastLineEndingLength on its parent, |
||
245 | * which must eventually reach a ParserMessageProxy which actually stores |
||
246 | * the length. |
||
247 | */ |
||
248 | 74 | public function setLastLineEndingLength(int $length) : static |
|
249 | { |
||
250 | 74 | $this->getParent()->setLastLineEndingLength($length); |
|
251 | 74 | return $this; |
|
252 | } |
||
253 | |||
254 | /** |
||
255 | * Returns the length of the last line ending read by MimeParser (e.g. 2 for |
||
256 | * '\r\n', or 1 for '\n'). |
||
257 | * |
||
258 | * The line ending may not belong specifically to this part, so |
||
259 | * ParserMimePartProxy simply calls getLastLineEndingLength on its parent, |
||
260 | * which must eventually reach a ParserMessageProxy which actually keeps |
||
261 | * the length and returns it. |
||
262 | * |
||
263 | * @return int the length of the last line ending read |
||
264 | */ |
||
265 | 74 | public function getLastLineEndingLength() : int |
|
266 | { |
||
267 | 74 | return $this->getParent()->getLastLineEndingLength(); |
|
268 | } |
||
269 | |||
270 | /** |
||
271 | * Returns the last part that was added. |
||
272 | */ |
||
273 | 2 | public function getLastAddedChild() : ?ParserPartProxy |
|
274 | { |
||
275 | 2 | return $this->lastAddedChild; |
|
276 | } |
||
277 | |||
278 | /** |
||
279 | * Returns the added child at the provided index, useful for looking at |
||
280 | * previously parsed children. |
||
281 | */ |
||
282 | 2 | public function getAddedChildAt(int $index) : ?ParserPartProxy |
|
283 | { |
||
284 | 2 | return $this->children[$index] ?? null; |
|
285 | } |
||
286 | } |
||
287 |