zbateson /
mail-mime-parser
| 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
Loading history...
|
|||
| 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 |