[ Index ]

MailPress 7.2

[ Index ]     [ Classes ]     [ Functions ]     [ Variables ]     [ Constants ]     [ Statistics ]    

title

Body

[close]

/mp-includes/composer/vendor/swiftmailer/swiftmailer/lib/classes/Swift/Mime/ -> SimpleMimeEntity.php (source)

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


Generated: Tue May 19 15:55:14 2020 Cross-referenced by PHPXref 0.7.1