Test Failed
Pull Request — master (#171)
by Zaahid
04:55
created

PartBuilder::createMessagePart()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 14
rs 9.6111
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\Parser;
8
9
use ZBateson\MailMimeParser\Message\PartHeaderContainer;
10
use ZBateson\MailMimeParser\Message\IMessagePart;
11
use ZBateson\MailMimeParser\Message\IMimePart;
12
use ZBateson\MailMimeParser\Parser\Part\ParsedMessagePartFactory;
13
use ZBateson\MailMimeParser\Parser\Part\ParsedPartChildrenContainer;
14
use ZBateson\MailMimeParser\Parser\Part\ParsedPartStreamContainer;
15
use ZBateson\MailMimeParser\Stream\StreamFactory;
16
use GuzzleHttp\Psr7\StreamWrapper;
17
use Psr\Http\Message\StreamInterface;
18
19
/**
20
 * Holds information about a part while it's being parsed, proxies calls between
21
 * parsed part containers (ParsedPartChildrenContainer,
22
 * ParsedPartStreamContainer) and the parser as more parts need to be parsed.
23
 *
24
 * The class holds:
25
 *  - a HeaderContainer to hold headers
26
 *  - stream positions (part start/end positions, content start/end)
27
 *  - parser markers, e.g. 'mimeBoundary, 'endBoundaryFound',
28
 *    'parentBoundaryFound', 'canHaveHeaders', 'isNonMimePart'
29
 *  - properties for UUEncoded parts (filename, mode)
30
 *  - the message's psr7 stream and a resource handle created from it (held
31
 *    only for a top-level PartBuilder representing the message, child
32
 *    PartBuilders do not duplicate/hold a separate stream).
33
 *  - ParsedPartChildrenContainer, ParsedPartStreamContainer to update children
34
 *    and streams dynamically as a part is parsed.
35
 * @author Zaahid Bateson
36
 */
37
class PartBuilder
38
{
39
    /**
40
     * @var MessagePartFactory the factory needed for creating the Message or
0 ignored issues
show
Bug introduced by
The type ZBateson\MailMimeParser\Parser\MessagePartFactory was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
41
     *      MessagePart for the parsed part.
42
     */
43
    protected $messagePartFactory;
44
45
    /**
46
     * @var StreamFactory Used for creating streams for IMessageParts when
47
     *      creating them.
48
     */
49
    protected $streamFactory;
50
51
    /**
52
     * @var BaseParser Used for parsing parts as needed.
53
     */
54
    protected $baseParser;
55
56
    /**
57
     * @var int The offset read start position for this part (beginning of
58
     * headers) in the message's stream.
59
     */
60
    protected $streamPartStartPos = null;
61
    
62
    /**
63
     * @var int The offset read end position for this part.  If the part is a
64
     * multipart mime part, the end position is after all of this parts
65
     * children.
66
     */
67
    protected $streamPartEndPos = null;
68
    
69
    /**
70
     * @var int The offset read start position in the message's stream for the
71
     * beginning of this part's content (body).
72
     */
73
    protected $streamContentStartPos = null;
74
    
75
    /**
76
     * @var int The offset read end position in the message's stream for the
77
     * end of this part's content (body).
78
     */
79
    protected $streamContentEndPos = null;
80
81
    /**
82
     * @var boolean set to true once the end boundary of the currently-parsed
83
     *      part is found.
84
     */
85
    protected $endBoundaryFound = false;
86
    
87
    /**
88
     * @var boolean set to true once a boundary belonging to this parent's part
89
     *      is found.
90
     */
91
    protected $parentBoundaryFound = false;
92
    
93
    /**
94
     * @var boolean|null|string FALSE if not queried for in the content-type
95
     *      header of this part, NULL if the current part does not have a
96
     *      boundary, and otherwise contains the value of the boundary parameter
97
     *      of the content-type header if the part contains one.
98
     */
99
    protected $mimeBoundary = false;
100
101
    /**
102
     * @var bool true if the part can have headers (i.e. a top-level part, or a
103
     *      child part if the parent's end boundary hasn't been found and is not
104
     *      a discardable part).
105
     */
106
    protected $canHaveHeaders = true;
107
108
    /**
109
     * @var bool set to true when creating a PartBuilder for a non-mime message.
110
     */
111
    protected $isNonMimePart = false;
112
113
    /**
114
     * @var PartHeaderContainer a container for found and parsed headers.
115
     */
116
    protected $headerContainer;
117
118
    /**
119
     * @var PartBuilder the parent part.
120
     */
121
    protected $parent = null;
122
    
123
    /**
124
     * @var string[] key => value pairs of properties passed on to the 
125
     *      $messagePartFactory when constructing the Message and its children.
126
     */
127
    protected $properties = [];
128
129
    /**
130
     * @var StreamInterface the raw message input stream for a message, or null
131
     *      for a child part.
132
     */
133
    protected $messageStream;
134
135
    /**
136
     * @var resource the raw message input stream handle constructed from
137
     *      $messageStream or null for a child part
138
     */
139
    protected $messageHandle;
140
141
    /**
142
     * @var ParsedPartChildrenContainer
143
     */
144
    protected $partChildrenContainer;
145
146
    /**
147
     * @var ParsedPartStreamContainer
148
     */
149
    protected $partStreamContainer;
150
151
    /**
152
     * @var IMessagePart the last child that was added maintained to ensure all
153
     *      of the last added part was parsed before parsing the next part.
154
     */
155
    private $lastAddedChild;
156
157
    /**
158
     * @var IMessagePart the created part from this PartBuilder.
159
     */
160
    private $part;
161
162
    public function __construct(
163
        ParsedMessagePartFactory $mpf,
164
        StreamFactory $streamFactory,
165
        BaseParser $parser,
166
        PartHeaderContainer $headerContainer,
167
        StreamInterface $messageStream = null,
168
        PartBuilder $parent = null
169
    ) {
170
        $this->messagePartFactory = $mpf;
0 ignored issues
show
Documentation Bug introduced by
It seems like $mpf of type ZBateson\MailMimeParser\...arsedMessagePartFactory is incompatible with the declared type ZBateson\MailMimeParser\Parser\MessagePartFactory of property $messagePartFactory.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
171
        $this->headerContainer = $headerContainer;
172
        $this->messageStream = $messageStream;
173
        $this->streamFactory = $streamFactory;
174
        $this->baseParser = $parser;
175
        if ($messageStream !== null) {
176
            $this->messageHandle = StreamWrapper::getResource($messageStream);
177
        }
178
        if ($parent !== null) {
179
            $this->parent = $parent;
180
            $this->canHaveHeaders = (!$parent->endBoundaryFound);
181
        }
182
        $this->setStreamPartStartPos($this->getMessageResourceHandlePos());
183
    }
184
185
    public function __destruct()
186
    {
187
        if ($this->messageHandle !== null) {
188
            fclose($this->messageHandle);
189
        }
190
    }
191
192
    public function setContainers(ParsedPartStreamContainer $streamContainer, ParsedPartChildrenContainer $childrenContainer = null)
193
    {
194
        $this->partStreamContainer = $streamContainer;
195
        $this->partChildrenContainer = $childrenContainer;
196
    }
197
198
    private function ensurePreviousSiblingRead()
199
    {
200
        if ($this->lastAddedChild !== null) {
201
            $this->lastAddedChild->hasContent();
202
            if ($this->lastAddedChild instanceof IMimePart) {
203
                $this->lastAddedChild->getAllParts();
204
            }
205
        }
206
    }
207
208
    public function parseContent()
209
    {
210
        if ($this->isContentParsed()) {
211
            return;
212
        }
213
        $this->baseParser->parseContent($this);
214
        $this->partStreamContainer->setContentStream(
215
            $this->streamFactory->getLimitedContentStream(
216
                $this->getStream(),
0 ignored issues
show
Bug introduced by
It seems like $this->getStream() can also be of type mixed; however, parameter $stream of ZBateson\MailMimeParser\...tLimitedContentStream() 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

216
                /** @scrutinizer ignore-type */ $this->getStream(),
Loading history...
217
                $this
218
            )
219
        );
220
    }
221
222
    public function parseAll()
223
    {
224
        $part = $this->createMessagePart();
225
        $part->hasContent();
226
        if ($part instanceof IMimePart) {
227
            $part->getAllParts();
228
        }
229
    }
230
231
    public function parseNextChild()
232
    {
233
        $this->ensurePreviousSiblingRead();
234
        $this->parseContent();
235
        return $this->baseParser->parseNextChild($this);
236
    }
237
238
    public function addChildToContainer(IMessagePart $part)
239
    {
240
        $this->partChildrenContainer->add($part);
241
        $this->lastAddedChild = $part;
242
    }
243
    
244
    /**
245
     * Adds a header with the given $name and $value to the headers array.
246
     *
247
     * Removes non-alphanumeric characters from $name, and sets it to lower-case
248
     * to use as a key in the private headers array.  Sets the original $name
249
     * and $value as elements in the headers' array value for the calculated
250
     * key.
251
     *
252
     * @param string $name
253
     * @param string $value
254
     */
255
    public function addHeader($name, $value)
256
    {
257
        $this->headerContainer->add($name, $value);
258
    }
259
    
260
    /**
261
     * Returns the HeaderContainer object containing parsed headers.
262
     * 
263
     * @return PartHeaderContainer
264
     */
265
    public function getHeaderContainer()
266
    {
267
        return $this->headerContainer;
268
    }
269
    
270
    /**
271
     * Sets the specified property denoted by $name to $value.
272
     * 
273
     * @param string $name
274
     * @param mixed $value
275
     */
276
    public function setProperty($name, $value)
277
    {
278
        $this->properties[$name] = $value;
279
    }
280
    
281
    /**
282
     * Returns the value of the property with the given $name.
283
     * 
284
     * @param string $name
285
     * @return mixed
286
     */
287
    public function getProperty($name)
288
    {
289
        if (!isset($this->properties[$name])) {
290
            return null;
291
        }
292
        return $this->properties[$name];
293
    }
294
295
    /**
296
     * Returns this PartBuilder's parent.
297
     * 
298
     * @return PartBuilder
299
     */
300
    public function getParent()
301
    {
302
        return $this->parent;
303
    }
304
305
    public function getChild($offset)
306
    {
307
        return $this->partChildrenContainer[$offset];
308
    }
309
310
    public function setNonMimePart($bool)
311
    {
312
        $this->isNonMimePart = $bool;
313
    }
314
315
    public function isNonMimePart()
316
    {
317
        return $this->isNonMimePart;
318
    }
319
320
    /**
321
     * Returns true if either a Content-Type or Mime-Version header are defined
322
     * in this PartBuilder's headers.
323
     * 
324
     * @return boolean
325
     */
326
    public function isMimeMessagePart()
327
    {
328
        return ($this->headerContainer->exists('Content-Type') ||
329
            $this->headerContainer->exists('Mime-Version'));
330
    }
331
    
332
    /**
333
     * Returns a ParameterHeader representing the parsed Content-Type header for
334
     * this PartBuilder.
335
     * 
336
     * @return \ZBateson\MailMimeParser\Header\ParameterHeader
337
     */
338
    public function getContentType()
339
    {
340
        return $this->headerContainer->get('Content-Type');
341
    }
342
    
343
    /**
344
     * Returns the parsed boundary parameter of the Content-Type header if set
345
     * for a multipart message part.
346
     * 
347
     * @return string
348
     */
349
    public function getMimeBoundary()
350
    {
351
        if ($this->mimeBoundary === false) {
352
            $this->mimeBoundary = null;
353
            $contentType = $this->getContentType();
354
            if ($contentType !== null) {
355
                $this->mimeBoundary = $contentType->getValueFor('boundary');
356
            }
357
        }
358
        return $this->mimeBoundary;
359
    }
360
    
361
    /**
362
     * Returns true if this part's content-type is multipart/*
363
     *
364
     * @return boolean
365
     */
366
    public function isMultiPart()
367
    {
368
        $contentType = $this->getContentType();
369
        if ($contentType !== null) {
370
            // casting to bool, preg_match returns 1 for true
371
            return (bool) (preg_match(
372
                '~multipart/.*~i',
373
                $contentType->getValue()
374
            ));
375
        }
376
        return false;
377
    }
378
    
379
    /**
380
     * Returns true if the passed $line of read input matches this PartBuilder's
381
     * mime boundary, or any of its parent's mime boundaries for a multipart
382
     * message.
383
     * 
384
     * If the passed $line is the ending boundary for the current PartBuilder,
385
     * $this->isEndBoundaryFound will return true after.
386
     * 
387
     * @param string $line
388
     * @return boolean
389
     */
390
    public function setEndBoundaryFound($line)
391
    {
392
        $boundary = $this->getMimeBoundary();
393
        if ($this->parent !== null && $this->parent->setEndBoundaryFound($line)) {
394
            $this->parentBoundaryFound = true;
395
            return true;
396
        } elseif ($boundary !== null) {
0 ignored issues
show
introduced by
The condition $boundary !== null is always true.
Loading history...
397
            if ($line === "--$boundary--") {
398
                $this->endBoundaryFound = true;
399
                return true;
400
            } elseif ($line === "--$boundary") {
401
                return true;
402
            }
403
        }
404
        return false;
405
    }
406
    
407
    /**
408
     * Returns true if MessageParser passed an input line to setEndBoundary that
409
     * matches a parent's mime boundary, and the following input belongs to a
410
     * new part under its parent.
411
     * 
412
     * @return boolean
413
     */
414
    public function isParentBoundaryFound()
415
    {
416
        return ($this->parentBoundaryFound);
417
    }
418
419
    public function isEndBoundaryFound()
420
    {
421
        return ($this->endBoundaryFound);
422
    }
423
    
424
    /**
425
     * Called once EOF is reached while reading content.  The method sets the
426
     * flag used by PartBuilder::isParentBoundaryFound to true on this part and
427
     * all parent PartBuilders.
428
     */
429
    public function setEof()
430
    {
431
        $this->parentBoundaryFound = true;
432
        if ($this->parent !== null) {
433
            $this->parent->parentBoundaryFound = true;
434
        }
435
    }
436
    
437
    /**
438
     * Returns true if the part's content contains headers.
439
     *
440
     * Top-level or non-discardable (i.e. parts before the end-boundary of the
441
     * parent) will return true;
442
     * 
443
     * @return boolean
444
     */
445
    public function canHaveHeaders()
446
    {
447
        return $this->canHaveHeaders;
448
    }
449
450
    public function getStream()
451
    {
452
        return ($this->messageStream !== null) ? $this->messageStream :
453
            $this->parent->getStream();
454
    }
455
456
    public function getMessageResourceHandle()
457
    {
458
        if ($this->messageStream === null) {
459
            return $this->parent->getMessageResourceHandle();
460
        }
461
        return $this->messageHandle;
462
    }
463
464
    public function getMessageResourceHandlePos()
465
    {
466
        return ftell($this->getMessageResourceHandle());
467
    }
468
469
    /**
470
     * Returns the offset for this part's stream within its parent stream.
471
     *
472
     * @return int
473
     */
474
    public function getStreamPartStartOffset()
475
    {
476
        return $this->streamPartStartPos;
477
    }
478
479
    /**
480
     * Returns the length of this part's stream.
481
     *
482
     * @return int
483
     */
484
    public function getStreamPartLength()
485
    {
486
        return $this->streamPartEndPos - $this->streamPartStartPos;
487
    }
488
489
    /**
490
     * Returns the offset for this part's content within its part stream.
491
     *
492
     * @return int
493
     */
494
    public function getStreamContentStartOffset()
495
    {
496
        return $this->streamContentStartPos;
497
    }
498
499
    /**
500
     * Returns the length of this part's content stream.
501
     *
502
     * @return int
503
     */
504
    public function getStreamContentLength()
505
    {
506
        return $this->streamContentEndPos - $this->streamContentStartPos;
507
    }
508
509
    /**
510
     * Sets the start position of the part in the input stream.
511
     * 
512
     * @param int $streamPartStartPos
513
     */
514
    public function setStreamPartStartPos($streamPartStartPos)
515
    {
516
        $this->streamPartStartPos = $streamPartStartPos;
517
    }
518
519
    /**
520
     * Sets the end position of the part in the input stream, and also calls
521
     * parent->setParentStreamPartEndPos to expand to parent parts.
522
     * 
523
     * @param int $streamPartEndPos
524
     */
525
    public function setStreamPartEndPos($streamPartEndPos)
526
    {
527
        $this->streamPartEndPos = $streamPartEndPos;
528
        if ($this->parent !== null) {
529
            $this->parent->setStreamPartEndPos($streamPartEndPos);
530
        }
531
    }
532
533
    /**
534
     * Sets the start position of the content in the input stream.
535
     * 
536
     * @param int $streamContentStartPos
537
     */
538
    public function setStreamContentStartPos($streamContentStartPos)
539
    {
540
        $this->streamContentStartPos = $streamContentStartPos;
541
    }
542
543
    /**
544
     * Sets the end position of the content and part in the input stream.
545
     * 
546
     * @param int $streamContentEndPos
547
     */
548
    public function setStreamPartAndContentEndPos($streamContentEndPos)
549
    {
550
        $this->streamContentEndPos = $streamContentEndPos;
551
        $this->setStreamPartEndPos($streamContentEndPos);
552
    }
553
554
    public function isContentParsed()
555
    {
556
        return ($this->streamContentEndPos !== null);
557
    }
558
559
    /**
560
     * Creates a MessagePart and returns it using the PartBuilder's
561
     * MessagePartFactory passed in during construction.
562
     *
563
     * @return IMessagePart
564
     */
565
    public function createMessagePart()
566
    {
567
        if (!$this->part) {
568
            $this->part = $this->messagePartFactory->newInstance(
569
                $this,
570
                ($this->parent !== null) ? $this->parent->createMessagePart() : null
571
            );
572
            if ($this->parent !== null && !$this->parent->endBoundaryFound) {
573
                // endBoundaryFound would indicate this is a discardable part
574
                // after the end boundary (some mailers seem to add identifiers)
575
                $this->parent->addChildToContainer($this->part);
576
            }
577
        }
578
        return $this->part;
579
    }
580
}
581