Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
 
3
/*
4
 * This file is part of SwiftMailer.
5
 * (c) 2004-2009 Chris Corbyn
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
 
11
//@require 'Swift/Mime/HeaderSet.php';
12
//@require 'Swift/OutputByteStream.php';
13
//@require 'Swift/Mime/ContentEncoder.php';
14
//@require 'Swift/KeyCache.php';
15
 
16
/**
17
 * A MIME entity, in a multipart message.
18
 * @package Swift
19
 * @subpackage Mime
20
 * @author Chris Corbyn
21
 */
22
class Swift_Mime_SimpleMimeEntity implements Swift_Mime_MimeEntity
23
{
24
 
25
  /** A collection of Headers for this mime entity */
26
  private $_headers;
27
 
28
  /** The body as a string, or a stream */
29
  private $_body;
30
 
31
  /** The encoder that encodes the body into a streamable format */
32
  private $_encoder;
33
 
34
  /** A mime bounary, if any is used */
35
  private $_boundary;
36
 
37
  /** Mime types to be used based on the nesting level */
38
  private $_compositeRanges = array(
39
    'multipart/mixed' => array(self::LEVEL_TOP, self::LEVEL_MIXED),
40
    'multipart/alternative' => array(self::LEVEL_MIXED, self::LEVEL_ALTERNATIVE),
41
    'multipart/related' => array(self::LEVEL_ALTERNATIVE, self::LEVEL_RELATED)
42
    );
43
 
44
  /** A set of filter rules to define what level an entity should be nested at */
45
  private $_compoundLevelFilters = array();
46
 
47
  /** The nesting level of this entity */
48
  private $_nestingLevel = self::LEVEL_ALTERNATIVE;
49
 
50
  /** A KeyCache instance used during encoding and streaming */
51
  private $_cache;
52
 
53
  /** Direct descendants of this entity */
54
  private $_immediateChildren = array();
55
 
56
  /** All descendants of this entity */
57
  private $_children = array();
58
 
59
  /** The maximum line length of the body of this entity */
60
  private $_maxLineLength = 78;
61
 
62
  /** The order in which alternative mime types should appear */
63
  private $_alternativePartOrder = array(
64
    'text/plain' => 1,
65
    'text/html' => 2,
66
    'multipart/related' => 3
67
    );
68
 
69
  /** The CID of this entity */
70
  private $_id;
71
 
72
  /** The key used for accessing the cache */
73
  private $_cacheKey;
74
 
75
  protected $_userContentType;
76
 
77
  /**
78
   * Create a new SimpleMimeEntity with $headers, $encoder and $cache.
79
   * @param Swift_Mime_HeaderSet $headers
80
   * @param Swift_Mime_ContentEncoder $encoder
81
   * @param Swift_KeyCache $cache
82
   */
83
  public function __construct(Swift_Mime_HeaderSet $headers,
84
    Swift_Mime_ContentEncoder $encoder, Swift_KeyCache $cache)
85
  {
86
    $this->_cacheKey = uniqid();
87
    $this->_cache = $cache;
88
    $this->_headers = $headers;
89
    $this->setEncoder($encoder);
90
    $this->_headers->defineOrdering(
91
      array('Content-Type', 'Content-Transfer-Encoding')
92
      );
93
 
94
    // This array specifies that, when the entire MIME document contains
95
    // $compoundLevel, then for each child within $level, if its Content-Type
96
    // is $contentType then it should be treated as if it's level is
97
    // $neededLevel instead.  I tried to write that unambiguously! :-\
98
    // Data Structure:
99
    // array (
100
    //   $compoundLevel => array(
101
    //     $level => array(
102
    //       $contentType => $neededLevel
103
    //     )
104
    //   )
105
    // )
106
 
107
    $this->_compoundLevelFilters = array(
108
      (self::LEVEL_ALTERNATIVE + self::LEVEL_RELATED) => array(
109
        self::LEVEL_ALTERNATIVE => array(
110
          'text/plain' => self::LEVEL_ALTERNATIVE,
111
          'text/html' => self::LEVEL_RELATED
112
          )
113
        )
114
      );
115
    $this->generateId();
116
  }
117
 
118
  /**
119
   * Generate a new Content-ID or Message-ID for this MIME entity.
120
   * @return string
121
   */
122
  public function generateId()
123
  {
124
    $idLeft = time() . '.' . uniqid();
125
    $idRight = !empty($_SERVER['SERVER_NAME'])
126
      ? $_SERVER['SERVER_NAME']
127
      : 'swift.generated';
128
    $this->_id = $idLeft . '@' . $idRight;
129
    return $this->getId();
130
  }
131
 
132
  /**
133
   * Get the {@link Swift_Mime_HeaderSet} for this entity.
134
   * @return Swift_Mime_HeaderSet
135
   */
136
  public function getHeaders()
137
  {
138
    return $this->_headers;
139
  }
140
 
141
  /**
142
   * Get the nesting level of this entity.
143
   * @return int
144
   * @see LEVEL_TOP, LEVEL_MIXED, LEVEL_RELATED, LEVEL_ALTERNATIVE
145
   */
146
  public function getNestingLevel()
147
  {
148
    return $this->_nestingLevel;
149
  }
150
 
151
  /**
152
   * Get the Content-type of this entity.
153
   * @return string
154
   */
155
  public function getContentType()
156
  {
157
    return $this->_getHeaderFieldModel('Content-Type');
158
  }
159
 
160
  /**
161
   * Set the Content-type of this entity.
162
   * @param string $type
163
   */
164
  public function setContentType($type)
165
  {
166
    $this->_setContentTypeInHeaders($type);
167
    // Keep track of the value so that if the content-type changes automatically
168
    // due to added child entities, it can be restored if they are later removed
169
    $this->_userContentType = $type;
170
    return $this;
171
  }
172
 
173
  /**
174
   * Get the CID of this entity.
175
   * The CID will only be present in headers if a Content-ID header is present.
176
   * @return string
177
   */
178
  public function getId()
179
  {
180
    return $this->_headers->has($this->_getIdField())
181
      ? current((array) $this->_getHeaderFieldModel($this->_getIdField()))
182
      : $this->_id;
183
  }
184
 
185
  /**
186
   * Set the CID of this entity.
187
   * @param string $id
188
   */
189
  public function setId($id)
190
  {
191
    if (!$this->_setHeaderFieldModel($this->_getIdField(), $id))
192
    {
193
      $this->_headers->addIdHeader($this->_getIdField(), $id);
194
    }
195
    $this->_id = $id;
196
    return $this;
197
  }
198
 
199
  /**
200
   * Get the description of this entity.
201
   * This value comes from the Content-Description header if set.
202
   * @return string
203
   */
204
  public function getDescription()
205
  {
206
    return $this->_getHeaderFieldModel('Content-Description');
207
  }
208
 
209
  /**
210
   * Set the description of this entity.
211
   * This method sets a value in the Content-ID header.
212
   * @param string $description
213
   */
214
  public function setDescription($description)
215
  {
216
    if (!$this->_setHeaderFieldModel('Content-Description', $description))
217
    {
218
      $this->_headers->addTextHeader('Content-Description', $description);
219
    }
220
    return $this;
221
  }
222
 
223
  /**
224
   * Get the maximum line length of the body of this entity.
225
   * @return int
226
   */
227
  public function getMaxLineLength()
228
  {
229
    return $this->_maxLineLength;
230
  }
231
 
232
  /**
233
   * Set the maximum line length of lines in this body.
234
   * Though not enforced by the library, lines should not exceed 1000 chars.
235
   * @param int $length
236
   */
237
  public function setMaxLineLength($length)
238
  {
239
    $this->_maxLineLength = $length;
240
    return $this;
241
  }
242
 
243
  /**
244
   * Get all children added to this entity.
245
   * @return array of Swift_Mime_Entity
246
   */
247
  public function getChildren()
248
  {
249
    return $this->_children;
250
  }
251
 
252
  /**
253
   * Set all children of this entity.
254
   * @param array $children Swiift_Mime_Entity instances
255
   * @param int $compoundLevel For internal use only
256
   */
257
  public function setChildren(array $children, $compoundLevel = null)
258
  {
259
    //TODO: Try to refactor this logic
260
 
261
    $compoundLevel = isset($compoundLevel)
262
      ? $compoundLevel
263
      : $this->_getCompoundLevel($children)
264
      ;
265
 
266
    $immediateChildren = array();
267
    $grandchildren = array();
268
    $newContentType = $this->_userContentType;
269
 
270
    foreach ($children as $child)
271
    {
272
      $level = $this->_getNeededChildLevel($child, $compoundLevel);
273
      if (empty($immediateChildren)) //first iteration
274
      {
275
        $immediateChildren = array($child);
276
      }
277
      else
278
      {
279
        $nextLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel);
280
        if ($nextLevel == $level)
281
        {
282
          $immediateChildren[] = $child;
283
        }
284
        elseif ($level < $nextLevel)
285
        {
286
          //Re-assign immediateChildren to grandchilden
287
          $grandchildren = array_merge($grandchildren, $immediateChildren);
288
          //Set new children
289
          $immediateChildren = array($child);
290
        }
291
        else
292
        {
293
          $grandchildren[] = $child;
294
        }
295
      }
296
    }
297
 
298
    if (!empty($immediateChildren))
299
    {
300
      $lowestLevel = $this->_getNeededChildLevel($immediateChildren[0], $compoundLevel);
301
 
302
      //Determine which composite media type is needed to accomodate the
303
      // immediate children
304
      foreach ($this->_compositeRanges as $mediaType => $range)
305
      {
306
        if ($lowestLevel > $range[0]
307
          && $lowestLevel <= $range[1])
308
        {
309
          $newContentType = $mediaType;
310
          break;
311
        }
312
      }
313
 
314
      //Put any grandchildren in a subpart
315
      if (!empty($grandchildren))
316
      {
317
        $subentity = $this->_createChild();
318
        $subentity->_setNestingLevel($lowestLevel);
319
        $subentity->setChildren($grandchildren, $compoundLevel);
320
        array_unshift($immediateChildren, $subentity);
321
      }
322
    }
323
 
324
    $this->_immediateChildren = $immediateChildren;
325
    $this->_children = $children;
326
    $this->_setContentTypeInHeaders($newContentType);
327
    $this->_fixHeaders();
328
    $this->_sortChildren();
329
 
330
    return $this;
331
  }
332
 
333
  /**
334
   * Get the body of this entity as a string.
335
   * @return string
336
   */
337
  public function getBody()
338
  {
339
    return ($this->_body instanceof Swift_OutputByteStream)
340
      ? $this->_readStream($this->_body)
341
      : $this->_body;
342
  }
343
 
344
  /**
345
   * Set the body of this entity, either as a string, or as an instance of
346
   * {@link Swift_OutputByteStream}.
347
   * @param mixed $body
348
   * @param string $contentType optional
349
   */
350
  public function setBody($body, $contentType = null)
351
  {
352
    if ($body !== $this->_body)
353
    {
354
      $this->_clearCache();
355
    }
356
 
357
    $this->_body = $body;
358
    if (isset($contentType))
359
    {
360
      $this->setContentType($contentType);
361
    }
362
    return $this;
363
  }
364
 
365
  /**
366
   * Get the encoder used for the body of this entity.
367
   * @return Swift_Mime_ContentEncoder
368
   */
369
  public function getEncoder()
370
  {
371
    return $this->_encoder;
372
  }
373
 
374
  /**
375
   * Set the encoder used for the body of this entity.
376
   * @param Swift_Mime_ContentEncoder $encoder
377
   */
378
  public function setEncoder(Swift_Mime_ContentEncoder $encoder)
379
  {
380
    if ($encoder !== $this->_encoder)
381
    {
382
      $this->_clearCache();
383
    }
384
 
385
    $this->_encoder = $encoder;
386
    $this->_setEncoding($encoder->getName());
387
    $this->_notifyEncoderChanged($encoder);
388
    return $this;
389
  }
390
 
391
  /**
392
   * Get the boundary used to separate children in this entity.
393
   * @return string
394
   */
395
  public function getBoundary()
396
  {
397
    if (!isset($this->_boundary))
398
    {
399
      $this->_boundary = '_=_swift_v4_' . time() . uniqid() . '_=_';
400
    }
401
    return $this->_boundary;
402
  }
403
 
404
  /**
405
   * Set the boundary used to separate children in this entity.
406
   * @param string $boundary
407
   * @throws Swift_RfcComplianceException
408
   */
409
  public function setBoundary($boundary)
410
  {
411
    $this->_assertValidBoundary($boundary);
412
    $this->_boundary = $boundary;
413
    return $this;
414
  }
415
 
416
  /**
417
   * Receive notification that the charset of this entity, or a parent entity
418
   * has changed.
419
   * @param string $charset
420
   */
421
  public function charsetChanged($charset)
422
  {
423
    $this->_notifyCharsetChanged($charset);
424
  }
425
 
426
  /**
427
   * Receive notification that the encoder of this entity or a parent entity
428
   * has changed.
429
   * @param Swift_Mime_ContentEncoder $encoder
430
   */
431
  public function encoderChanged(Swift_Mime_ContentEncoder $encoder)
432
  {
433
    $this->_notifyEncoderChanged($encoder);
434
  }
435
 
436
  /**
437
   * Get this entire entity as a string.
438
   * @return string
439
   */
440
  public function toString()
441
  {
442
    $string = $this->_headers->toString();
443
    if (isset($this->_body) && empty($this->_immediateChildren))
444
    {
445
      if ($this->_cache->hasKey($this->_cacheKey, 'body'))
446
      {
447
        $body = $this->_cache->getString($this->_cacheKey, 'body');
448
      }
449
      else
450
      {
451
        $body = "\r\n" . $this->_encoder->encodeString($this->getBody(), 0,
452
          $this->getMaxLineLength()
453
          );
454
        $this->_cache->setString($this->_cacheKey, 'body', $body,
455
          Swift_KeyCache::MODE_WRITE
456
          );
457
      }
458
      $string .= $body;
459
    }
460
 
461
    if (!empty($this->_immediateChildren))
462
    {
463
      foreach ($this->_immediateChildren as $child)
464
      {
465
        $string .= "\r\n\r\n--" . $this->getBoundary() . "\r\n";
466
        $string .= $child->toString();
467
      }
468
      $string .= "\r\n\r\n--" . $this->getBoundary() . "--\r\n";
469
    }
470
 
471
    return $string;
472
  }
473
 
474
  /**
475
   * Returns a string representation of this object.
476
   *
477
   * @return string
478
   *
479
   * @see toString()
480
   */
481
  public function __toString()
482
  {
483
    return $this->toString();
484
  }
485
 
486
  /**
487
   * Write this entire entity to a {@link Swift_InputByteStream}.
488
   * @param Swift_InputByteStream
489
   */
490
  public function toByteStream(Swift_InputByteStream $is)
491
  {
492
    $is->write($this->_headers->toString());
493
    $is->commit();
494
 
495
    if (empty($this->_immediateChildren))
496
    {
497
      if (isset($this->_body))
498
      {
499
        if ($this->_cache->hasKey($this->_cacheKey, 'body'))
500
        {
501
          $this->_cache->exportToByteStream($this->_cacheKey, 'body', $is);
502
        }
503
        else
504
        {
505
          $cacheIs = $this->_cache->getInputByteStream($this->_cacheKey, 'body');
506
          if ($cacheIs)
507
          {
508
            $is->bind($cacheIs);
509
          }
510
 
511
          $is->write("\r\n");
512
 
513
          if ($this->_body instanceof Swift_OutputByteStream)
514
          {
515
            $this->_body->setReadPointer(0);
516
 
517
            $this->_encoder->encodeByteStream($this->_body, $is, 0,
518
              $this->getMaxLineLength()
519
              );
520
          }
521
          else
522
          {
523
            $is->write($this->_encoder->encodeString(
524
              $this->getBody(), 0, $this->getMaxLineLength()
525
              ));
526
          }
527
 
528
          if ($cacheIs)
529
          {
530
            $is->unbind($cacheIs);
531
          }
532
        }
533
      }
534
    }
535
 
536
    if (!empty($this->_immediateChildren))
537
    {
538
      foreach ($this->_immediateChildren as $child)
539
      {
540
        $is->write("\r\n\r\n--" . $this->getBoundary() . "\r\n");
541
        $child->toByteStream($is);
542
      }
543
      $is->write("\r\n\r\n--" . $this->getBoundary() . "--\r\n");
544
    }
545
  }
546
 
547
  // -- Protected methods
548
 
549
  /**
550
   * Get the name of the header that provides the ID of this entity */
551
  protected function _getIdField()
552
  {
553
    return 'Content-ID';
554
  }
555
 
556
  /**
557
   * Get the model data (usually an array or a string) for $field.
558
   */
559
  protected function _getHeaderFieldModel($field)
560
  {
561
    if ($this->_headers->has($field))
562
    {
563
      return $this->_headers->get($field)->getFieldBodyModel();
564
    }
565
  }
566
 
567
  /**
568
   * Set the model data for $field.
569
   */
570
  protected function _setHeaderFieldModel($field, $model)
571
  {
572
    if ($this->_headers->has($field))
573
    {
574
      $this->_headers->get($field)->setFieldBodyModel($model);
575
      return true;
576
    }
577
    else
578
    {
579
      return false;
580
    }
581
  }
582
 
583
  /**
584
   * Get the parameter value of $parameter on $field header.
585
   */
586
  protected function _getHeaderParameter($field, $parameter)
587
  {
588
    if ($this->_headers->has($field))
589
    {
590
      return $this->_headers->get($field)->getParameter($parameter);
591
    }
592
  }
593
 
594
  /**
595
   * Set the parameter value of $parameter on $field header.
596
   */
597
  protected function _setHeaderParameter($field, $parameter, $value)
598
  {
599
    if ($this->_headers->has($field))
600
    {
601
      $this->_headers->get($field)->setParameter($parameter, $value);
602
      return true;
603
    }
604
    else
605
    {
606
      return false;
607
    }
608
  }
609
 
610
  /**
611
   * Re-evaluate what content type and encoding should be used on this entity.
612
   */
613
  protected function _fixHeaders()
614
  {
615
    if (count($this->_immediateChildren))
616
    {
617
      $this->_setHeaderParameter('Content-Type', 'boundary',
618
        $this->getBoundary()
619
        );
620
      $this->_headers->remove('Content-Transfer-Encoding');
621
    }
622
    else
623
    {
624
      $this->_setHeaderParameter('Content-Type', 'boundary', null);
625
      $this->_setEncoding($this->_encoder->getName());
626
    }
627
  }
628
 
629
  /**
630
   * Get the KeyCache used in this entity.
631
   */
632
  protected function _getCache()
633
  {
634
    return $this->_cache;
635
  }
636
 
637
  /**
638
   * Empty the KeyCache for this entity.
639
   */
640
  protected function _clearCache()
641
  {
642
    $this->_cache->clearKey($this->_cacheKey, 'body');
643
  }
644
 
645
  // -- Private methods
646
 
647
  private function _readStream(Swift_OutputByteStream $os)
648
  {
649
    $string = '';
650
    while (false !== $bytes = $os->read(8192))
651
    {
652
      $string .= $bytes;
653
    }
654
    return $string;
655
  }
656
 
657
  private function _setEncoding($encoding)
658
  {
659
    if (!$this->_setHeaderFieldModel('Content-Transfer-Encoding', $encoding))
660
    {
661
      $this->_headers->addTextHeader('Content-Transfer-Encoding', $encoding);
662
    }
663
  }
664
 
665
  private function _assertValidBoundary($boundary)
666
  {
667
    if (!preg_match(
668
      '/^[a-z0-9\'\(\)\+_\-,\.\/:=\?\ ]{0,69}[a-z0-9\'\(\)\+_\-,\.\/:=\?]$/Di',
669
      $boundary))
670
    {
671
      throw new Exception('Mime boundary set is not RFC 2046 compliant.');
672
    }
673
  }
674
 
675
  private function _setContentTypeInHeaders($type)
676
  {
677
    if (!$this->_setHeaderFieldModel('Content-Type', $type))
678
    {
679
      $this->_headers->addParameterizedHeader('Content-Type', $type);
680
    }
681
  }
682
 
683
  private function _setNestingLevel($level)
684
  {
685
    $this->_nestingLevel = $level;
686
  }
687
 
688
  private function _getCompoundLevel($children)
689
  {
690
    $level = 0;
691
    foreach ($children as $child)
692
    {
693
      $level |= $child->getNestingLevel();
694
    }
695
    return $level;
696
  }
697
 
698
  private function _getNeededChildLevel($child, $compoundLevel)
699
  {
700
    $filter = array();
701
    foreach ($this->_compoundLevelFilters as $bitmask => $rules)
702
    {
703
      if (($compoundLevel & $bitmask) === $bitmask)
704
      {
705
        $filter = $rules + $filter;
706
      }
707
    }
708
 
709
    $realLevel = $child->getNestingLevel();
710
    $lowercaseType = strtolower($child->getContentType());
711
 
712
    if (isset($filter[$realLevel])
713
      && isset($filter[$realLevel][$lowercaseType]))
714
    {
715
      return $filter[$realLevel][$lowercaseType];
716
    }
717
    else
718
    {
719
      return $realLevel;
720
    }
721
  }
722
 
723
  private function _createChild()
724
  {
725
    return new self($this->_headers->newInstance(),
726
      $this->_encoder, $this->_cache);
727
  }
728
 
729
  private function _notifyEncoderChanged(Swift_Mime_ContentEncoder $encoder)
730
  {
731
    foreach ($this->_immediateChildren as $child)
732
    {
733
      $child->encoderChanged($encoder);
734
    }
735
  }
736
 
737
  private function _notifyCharsetChanged($charset)
738
  {
739
    $this->_encoder->charsetChanged($charset);
740
    $this->_headers->charsetChanged($charset);
741
    foreach ($this->_immediateChildren as $child)
742
    {
743
      $child->charsetChanged($charset);
744
    }
745
  }
746
 
747
  private function _sortChildren()
748
  {
749
    $shouldSort = false;
750
    foreach ($this->_immediateChildren as $child)
751
    {
752
      //NOTE: This include alternative parts moved into a related part
753
      if ($child->getNestingLevel() == self::LEVEL_ALTERNATIVE)
754
      {
755
        $shouldSort = true;
756
        break;
757
      }
758
    }
759
 
760
    //Sort in order of preference, if there is one
761
    if ($shouldSort)
762
    {
763
      usort($this->_immediateChildren, array($this, '_childSortAlgorithm'));
764
    }
765
  }
766
 
767
  private function _childSortAlgorithm($a, $b)
768
  {
769
    $typePrefs = array();
770
    $types = array(
771
      strtolower($a->getContentType()),
772
      strtolower($b->getContentType())
773
      );
774
    foreach ($types as $type)
775
    {
776
      $typePrefs[] = (array_key_exists($type, $this->_alternativePartOrder))
777
        ? $this->_alternativePartOrder[$type]
778
        : (max($this->_alternativePartOrder) + 1);
779
    }
780
    return ($typePrefs[0] >= $typePrefs[1]) ? 1 : -1;
781
  }
782
 
783
  // -- Destructor
784
 
785
  /**
786
   * Empties it's own contents from the cache.
787
   */
788
  public function __destruct()
789
  {
790
    $this->_cache->clearAll($this->_cacheKey);
791
  }
792
 
793
}