Merge branch '5.3'
authorAlexander Ebert <ebert@woltlab.com>
Wed, 24 Feb 2021 17:22:45 +0000 (18:22 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 24 Feb 2021 17:22:45 +0000 (18:22 +0100)
1  2 
wcfsetup/install/files/lib/system/html/output/HtmlOutputProcessor.class.php
wcfsetup/install/files/lib/system/message/embedded/object/MessageEmbeddedObjectManager.class.php
wcfsetup/install/files/lib/system/user/signature/SignatureCache.class.php

index 6d2194c617b4f1f85513ecfb3da2876b58a359b4,547c38f2b04597dd19dbccdc4ccc892c66e9d176..821d8371ee75169e736030d7897b6d6f70afb86e
@@@ -16,117 -14,125 +16,131 @@@ use wcf\system\message\embedded\object\
   * @package     WoltLabSuite\Core\System\Html\Output
   * @since       3.0
   */
 -class HtmlOutputProcessor extends AbstractHtmlProcessor {
 -      /**
 -       * Generate the table of contents, implicitly enable this for certain object types on demand.
 -       * @var bool|null
 -       * @since       5.2
 -       */
 -      public $enableToc;
 -      
 -      /**
 -       * Removes any link contained inside the message text.
 -       * @var bool
 -       * @since 5.2
 -       */
 -      public $removeLinks = false;
 -      
 -      /**
 -       * output node processor instance
 -       * @var HtmlOutputNodeProcessor
 -       */
 -      protected $htmlOutputNodeProcessor;
 -      
 -      /**
 -       * content language id
 -       * @var integer
 -       */
 -      protected $languageID;
 -      
 -      /**
 -       * desired output type
 -       * @var string
 -       */
 -      protected $outputType = 'text/html';
 -      
 -      /**
 -       * enables rel=ugc for external links
 -       * @var bool
 -       */
 -      public $enableUgc = true;
 -      
 -      /**
 -       * Processes the input html string.
 -       *
 -       * @param       string          $html                           html string
 -       * @param       string          $objectType                     object type identifier
 -       * @param       integer         $objectID                       object id
 -       * @param       boolean         $doKeywordHighlighting          enable keyword highlighting
 -       * @param       integer         $languageID                     content language id
 -       */
 -      public function process($html, $objectType, $objectID, $doKeywordHighlighting = true, $languageID = null) {
 -              $this->languageID = $languageID;
 -              $this->setContext($objectType, $objectID);
 -
 -              MessageEmbeddedObjectManager::getInstance()->setActiveMessage($objectType, $objectID, $this->languageID);
 -
 -              try {
 -                      $this->getHtmlOutputNodeProcessor()->setOutputType($this->outputType);
 -                      $this->getHtmlOutputNodeProcessor()->enableKeywordHighlighting($doKeywordHighlighting);
 -                      $this->getHtmlOutputNodeProcessor()->load($this, $html);
 -                      $this->getHtmlOutputNodeProcessor()->process();
 -              }
 -              finally {
 -                      MessageEmbeddedObjectManager::getInstance()->reset();
 -              }
 -      }
 -      
 -      /**
 -       * Sets the desired output type.
 -       * 
 -       * @param       string          $outputType     desired output type
 -       * @throws      \InvalidArgumentException
 -       */
 -      public function setOutputType($outputType) {
 -              if (!in_array($outputType, ['text/html', 'text/simplified-html', 'text/plain'])) {
 -                      throw new \InvalidArgumentException("Expected 'text/html', 'text/simplified-html' or 'text/plain', but received '" . $outputType . "'");
 -              }
 -              
 -              $this->outputType = $outputType;
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function getHtml() {
 -              $context = $this->getContext();
 -              MessageEmbeddedObjectManager::getInstance()->setActiveMessage($context['objectType'], $context['objectID'], $this->languageID);
 -
 -              try {
 -                      $html = $this->getHtmlOutputNodeProcessor()->getHtml();
 -              }
 -              finally {
 -                      MessageEmbeddedObjectManager::getInstance()->reset();
 -              }
 -
 -              return $html;
 -      }
 -      
 -      /**
 -       * @inheritdoc
 -       * @throws \InvalidArgumentException
 -       */
 -      public function setContext($objectType, $objectID) {
 -              parent::setContext($objectType, $objectID);
 -              
 -              $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.message', $objectType);
 -              if ($this->enableToc === null) {
 -                      $this->enableToc = (!empty($objectType->additionalData['enableToc']));
 -              }
 -      }
 -      
 -      /**
 -       * Returns the output node processor instance.
 -       * 
 -       * @return      HtmlOutputNodeProcessor         output node processor instance
 -       */
 -      protected function getHtmlOutputNodeProcessor() {
 -              if ($this->htmlOutputNodeProcessor === null) {
 -                      $this->htmlOutputNodeProcessor = new HtmlOutputNodeProcessor();
 -              }
 -              
 -              return $this->htmlOutputNodeProcessor;
 -      }
 +class HtmlOutputProcessor extends AbstractHtmlProcessor
 +{
 +    /**
 +     * Generate the table of contents, implicitly enable this for certain object types on demand.
 +     * @var bool|null
 +     * @since   5.2
 +     */
 +    public $enableToc;
 +
 +    /**
 +     * Removes any link contained inside the message text.
 +     * @var bool
 +     * @since 5.2
 +     */
 +    public $removeLinks = false;
 +
 +    /**
 +     * output node processor instance
 +     * @var HtmlOutputNodeProcessor
 +     */
 +    protected $htmlOutputNodeProcessor;
 +
 +    /**
 +     * content language id
 +     * @var int
 +     */
 +    protected $languageID;
 +
 +    /**
 +     * desired output type
 +     * @var string
 +     */
 +    protected $outputType = 'text/html';
 +
 +    /**
 +     * enables rel=ugc for external links
 +     * @var bool
 +     */
 +    public $enableUgc = true;
 +
 +    /**
 +     * Processes the input html string.
 +     *
 +     * @param string $html html string
 +     * @param string $objectType object type identifier
 +     * @param int $objectID object id
 +     * @param bool $doKeywordHighlighting enable keyword highlighting
 +     * @param int $languageID content language id
 +     */
 +    public function process($html, $objectType, $objectID, $doKeywordHighlighting = true, $languageID = null)
 +    {
 +        $this->languageID = $languageID;
 +        $this->setContext($objectType, $objectID);
 +
-         $this->getHtmlOutputNodeProcessor()->setOutputType($this->outputType);
-         $this->getHtmlOutputNodeProcessor()->enableKeywordHighlighting($doKeywordHighlighting);
-         $this->getHtmlOutputNodeProcessor()->load($this, $html);
-         $this->getHtmlOutputNodeProcessor()->process();
++        MessageEmbeddedObjectManager::getInstance()->setActiveMessage($objectType, $objectID, $this->languageID);
++
++        try {
++            $this->getHtmlOutputNodeProcessor()->setOutputType($this->outputType);
++            $this->getHtmlOutputNodeProcessor()->enableKeywordHighlighting($doKeywordHighlighting);
++            $this->getHtmlOutputNodeProcessor()->load($this, $html);
++            $this->getHtmlOutputNodeProcessor()->process();
++        } finally {
++            MessageEmbeddedObjectManager::getInstance()->reset();
++        }
 +    }
 +
 +    /**
 +     * Sets the desired output type.
 +     *
 +     * @param string $outputType desired output type
 +     * @throws      \InvalidArgumentException
 +     */
 +    public function setOutputType($outputType)
 +    {
 +        if (!\in_array($outputType, ['text/html', 'text/simplified-html', 'text/plain'])) {
 +            throw new \InvalidArgumentException(
 +                "Expected 'text/html', 'text/simplified-html' or 'text/plain', but received '" . $outputType . "'"
 +            );
 +        }
 +
 +        $this->outputType = $outputType;
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function getHtml()
 +    {
-         return $this->getHtmlOutputNodeProcessor()->getHtml();
++        $context = $this->getContext();
++        MessageEmbeddedObjectManager::getInstance()->setActiveMessage($context['objectType'], $context['objectID'], $this->languageID);
++
++        try {
++            $html = $this->getHtmlOutputNodeProcessor()->getHtml();
++        } finally {
++            MessageEmbeddedObjectManager::getInstance()->reset();
++        }
++
++        return $html;
 +    }
 +
 +    /**
 +     * @inheritdoc
 +     * @throws \InvalidArgumentException
 +     */
 +    public function setContext($objectType, $objectID)
 +    {
 +        parent::setContext($objectType, $objectID);
 +
-         MessageEmbeddedObjectManager::getInstance()->setActiveMessage($objectType, $objectID, $this->languageID);
 +        $objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.message', $objectType);
 +        if ($this->enableToc === null) {
 +            $this->enableToc = (!empty($objectType->additionalData['enableToc']));
 +        }
 +    }
 +
 +    /**
 +     * Returns the output node processor instance.
 +     *
 +     * @return      HtmlOutputNodeProcessor         output node processor instance
 +     */
 +    protected function getHtmlOutputNodeProcessor()
 +    {
 +        if ($this->htmlOutputNodeProcessor === null) {
 +            $this->htmlOutputNodeProcessor = new HtmlOutputNodeProcessor();
 +        }
 +
 +        return $this->htmlOutputNodeProcessor;
 +    }
  }
index 80b8e22702f7eeffb231fd242dbe44ac63a392d0,d9af6697608b1a767a0f10e7869d2ca2881b9871..d4e04b597ebca7ec21b6ed16923991e446e798d0
@@@ -11,443 -9,443 +11,476 @@@ use wcf\system\WCF
  
  /**
   * Default interface of embedded object handler.
 - * 
 - * @author    Marcel Werk
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Message\Embedded\Object
 + *
 + * @author  Marcel Werk
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Message\Embedded\Object
   */
 -class MessageEmbeddedObjectManager extends SingletonFactory {
 -      /**
 -       * caches message to embedded object assignments
 -       * @var array
 -       */
 -      protected $messageEmbeddedObjects = [];
 -      
 -      /**
 -       * caches embedded objects
 -       * @var array
 -       */
 -      protected $embeddedObjects = [];
 -      
 -      /**
 -       * object type of the active message
 -       * @var integer
 -       */
 -      protected $activeMessageObjectTypeID;
 -      
 -      /**
 -       * id of the active message
 -       * @var integer
 -       */
 -      protected $activeMessageID;
 -      
 -      /**
 -       * language id of the active message
 -       * @var integer
 -       */
 -      protected $activeMessageLanguageID;
 -      
 -      /**
 -       * list of embedded object handlers
 -       * @var array
 -       */
 -      protected $embeddedObjectHandlers;
 -      
 -      /**
 -       * content language id
 -       * @var integer
 -       */
 -      protected $contentLanguageID;
 -      
 -      /**
 -       * local cache for bulk operations
 -       * @var mixed[][]
 -       */
 -      protected $bulkData = [
 -              'insert' => [],
 -              'remove' => []
 -      ];
 -
 -      /**
 -       * A list of previous active message settings used to restore
 -       * the internal state in case of nested message processing.
 -       */
 -      protected $activeMessageHistory = [];
 -      
 -      /**
 -       * Registers the embedded objects found in given message.
 -       * 
 -       * @param       HtmlInputProcessor      $htmlInputProcessor     html input processor instance holding embedded object data
 -       * @param       boolean                 $isBulk                 true for bulk operations
 -       * @return      boolean                 true if at least one embedded object was found
 -       */
 -      public function registerObjects(HtmlInputProcessor $htmlInputProcessor, $isBulk = false) {
 -              $context = $htmlInputProcessor->getContext();
 -              
 -              $messageObjectType = $context['objectType'];
 -              $messageObjectTypeID = $context['objectTypeID'];
 -              $messageID = $context['objectID'];
 -              
 -              // delete existing assignments
 -              if ($isBulk) {
 -                      if (!isset($this->bulkData['remove'][$messageObjectType])) $this->bulkData['remove'][$messageObjectType] = [];
 -                      $this->bulkData['remove'][$messageObjectType][] = $messageID;
 -              }
 -              else {
 -                      $this->removeObjects($messageObjectType, [$messageID]);
 -              }
 -              
 -              $statement = null;
 -              if (!$isBulk) {
 -                      // prepare statement
 -                      $sql = "INSERT INTO     wcf".WCF_N."_message_embedded_object
 -                                              (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID)
 -                              VALUES          (?, ?, ?, ?)";
 -                      $statement = WCF::getDB()->prepareStatement($sql);
 -                      
 -                      WCF::getDB()->beginTransaction();
 -              }
 -              
 -              $embeddedData = $htmlInputProcessor->getEmbeddedContent();
 -              $returnValue = false;
 -              
 -              /** @var IMessageEmbeddedObjectHandler $handler */
 -              foreach ($this->getEmbeddedObjectHandlers() as $handler) {
 -                      $objectIDs = $handler->parse($htmlInputProcessor, $embeddedData);
 -                      
 -                      if (!empty($objectIDs)) {
 -                              foreach ($objectIDs as $objectID) {
 -                                      $parameters = [$messageObjectTypeID, $messageID, $handler->objectTypeID, $objectID];
 -                                      if ($isBulk) {
 -                                              $this->bulkData['insert'][] = $parameters;
 -                                      }
 -                                      else {
 -                                              $statement->execute($parameters);
 -                                      }
 -                              }
 -                              
 -                              $returnValue = true;
 -                      }
 -              }
 -              
 -              if (!$isBulk) {
 -                      WCF::getDB()->commitTransaction();
 -              }
 -              
 -              return $returnValue;
 -      }
 -      
 -      /**
 -       * Commits the bulk operation by performing all deletes and inserts
 -       * in one big transaction to save performance.
 -       */
 -      public function commitBulkOperation() {
 -              // delete existing data
 -              WCF::getDB()->beginTransaction();
 -              foreach ($this->bulkData['remove'] as $objectType => $objectIDs) {
 -                      $this->removeObjects($objectType, $objectIDs);
 -              }
 -              WCF::getDB()->commitTransaction();
 -              
 -              // prepare statement
 -              $sql = "INSERT INTO     wcf".WCF_N."_message_embedded_object
 -                                      (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID)
 -                      VALUES          (?, ?, ?, ?)";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              
 -              WCF::getDB()->beginTransaction();
 -              foreach ($this->bulkData['insert'] as $parameters) {
 -                      $statement->execute($parameters);
 -              }
 -              WCF::getDB()->commitTransaction();
 -              
 -              // reset cache
 -              $this->bulkData = [
 -                      'insert' => [],
 -                      'remove' => []
 -              ];
 -      }
 -      
 -      /**
 -       * Registers the embedded objects found in a message using the simplified syntax.
 -       * 
 -       * @param       string          $messageObjectType      object type identifier
 -       * @param       integer         $messageID              object id
 -       * @param       integer[][]     $embeddedContent        list of object ids for embedded objects by object type id
 -       * @return      boolean         true if at least one embedded object was found
 -       */
 -      public function registerSimpleObjects($messageObjectType, $messageID, array $embeddedContent) {
 -              $messageObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType);
 -              
 -              // delete existing assignments
 -              $this->removeObjects($messageObjectType, [$messageID]);
 -              
 -              if (empty($embeddedContent)) {
 -                      return false;
 -              }
 -              
 -              // prepare statement
 -              $sql = "INSERT INTO     wcf".WCF_N."_message_embedded_object
 -                                      (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID)
 -                      VALUES          (?, ?, ?, ?)";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              
 -              // call embedded object handlers
 -              WCF::getDB()->beginTransaction();
 -              foreach ($embeddedContent as $objectTypeID => $objectIDs) {
 -                      foreach ($objectIDs as $objectID) {
 -                              $statement->execute([$messageObjectTypeID, $messageID, $objectTypeID, $objectID]);
 -                      }
 -              }
 -              WCF::getDB()->commitTransaction();
 -              
 -              return true;
 -      }
 -      
 -      /**
 -       * Removes embedded object assignments for given messages.
 -       * 
 -       * @param       string                  $messageObjectType
 -       * @param       integer[]               $messageIDs
 -       */
 -      public function removeObjects($messageObjectType, array $messageIDs) {
 -              $conditionBuilder = new PreparedStatementConditionBuilder();
 -              $conditionBuilder->add('messageObjectTypeID = ?', [ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType)]);
 -              $conditionBuilder->add('messageID IN (?)', [$messageIDs]);
 -              
 -              $sql = "DELETE FROM     wcf".WCF_N."_message_embedded_object
 -                      ".$conditionBuilder;
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute($conditionBuilder->getParameters());
 -      }
 -      
 -      /**
 -       * Loads the embedded objects for given messages.
 -       * 
 -       * @param       string          $messageObjectType
 -       * @param       integer[]       $messageIDs
 -       * @param       integer         $contentLanguageID
 -       * @throws      InvalidObjectTypeException
 -       */
 -      public function loadObjects($messageObjectType, array $messageIDs, $contentLanguageID = null) {
 -              $messageObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType);
 -              if ($messageObjectTypeID === null) {
 -                      throw new InvalidObjectTypeException($messageObjectType, 'com.woltlab.wcf.message');
 -              }
 -              
 -              $conditionBuilder = new PreparedStatementConditionBuilder();
 -              $conditionBuilder->add('messageObjectTypeID = ?', [$messageObjectTypeID]);
 -              $conditionBuilder->add('messageID IN (?)', [$messageIDs]);
 -              
 -              // get object ids
 -              $sql = "SELECT  *
 -                      FROM    wcf".WCF_N."_message_embedded_object
 -                      ".$conditionBuilder;
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute($conditionBuilder->getParameters());
 -              $embeddedObjects = [];
 -              while ($row = $statement->fetchArray()) {
 -                      if (!isset($this->embeddedObjects[$row['embeddedObjectTypeID']][$row['embeddedObjectID']])) {
 -                              // group objects by object type
 -                              if (!isset($embeddedObjects[$row['embeddedObjectTypeID']])) $embeddedObjects[$row['embeddedObjectTypeID']] = [];
 -                              $embeddedObjects[$row['embeddedObjectTypeID']][] = $row['embeddedObjectID'];
 -                      }
 -                      
 -                      // store message to embedded object assignment
 -                      if (!isset($this->messageEmbeddedObjects[$row['messageObjectTypeID']][$row['messageID']][$row['embeddedObjectTypeID']])) {
 -                              $this->messageEmbeddedObjects[$row['messageObjectTypeID']][$row['messageID']][$row['embeddedObjectTypeID']] = [];
 -                      }
 -                      $this->messageEmbeddedObjects[$row['messageObjectTypeID']][$row['messageID']][$row['embeddedObjectTypeID']][] = $row['embeddedObjectID'];
 -              }
 -              
 -              $this->contentLanguageID = $contentLanguageID;
 -              
 -              // load objects
 -              foreach ($embeddedObjects as $embeddedObjectTypeID => $objectIDs) {
 -                      if (!isset($this->embeddedObjects[$embeddedObjectTypeID])) $this->embeddedObjects[$embeddedObjectTypeID] = [];
 -                      foreach ($this->getEmbeddedObjectHandler($embeddedObjectTypeID)->loadObjects(array_unique($objectIDs)) as $objectID => $object) {
 -                              $this->embeddedObjects[$embeddedObjectTypeID][$objectID] = $object;
 -                      }
 -              }
 -              
 -              $this->contentLanguageID = null;
 -      }
 -      
 -      /**
 -       * Returns the content language id or null.
 -       * 
 -       * @return      integer
 -       */
 -      public function getContentLanguageID() {
 -              return $this->contentLanguageID;
 -      }
 -      
 -      /**
 -       * Sets active message information.
 -       * 
 -       * @param       string          $messageObjectType
 -       * @param       integer         $messageID
 -       * @param       integer         $languageID
 -       */
 -      public function setActiveMessage($messageObjectType, $messageID, $languageID = null) {
 -              if ($this->activeMessageObjectTypeID) {
 -                      $this->activeMessageHistory[] = [
 -                              'activeMessageID' => $this->activeMessageID,
 -                              'activeMessageLanguageID' => $this->activeMessageLanguageID,
 -                              'activeMessageObjectTypeID' => $this->activeMessageObjectTypeID,
 -                      ];
 -              }
 -
 -              $this->activeMessageObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType);
 -              $this->activeMessageID = $messageID;
 -              $this->activeMessageLanguageID = $languageID;
 -      }
 -
 -      /**
 -       * Restores the internal state in case of nested message processing.
 -       */
 -      public function reset() {
 -              $newState = \array_pop($this->activeMessageHistory);
 -              if ($newState === null) {
 -                      $newState = [
 -                              'activeMessageID' => null,
 -                              'activeMessageLanguageID' => null,
 -                              'activeMessageObjectTypeID' => null,
 -                      ];
 -              }
 -
 -              $this->activeMessageID = $newState['activeMessageID'];
 -              $this->activeMessageLanguageID = $newState['activeMessageLanguageID'];
 -              $this->activeMessageObjectTypeID = $newState['activeMessageObjectTypeID'];
 -      }
 -      
 -      /**
 -       * Returns the language id of the active message.
 -       * 
 -       * @return      integer
 -       */
 -      public function getActiveMessageLanguageID() {
 -              return $this->activeMessageLanguageID;
 -      }
 -      
 -      /**
 -       * Returns all embedded objects of a specific type.
 -       * 
 -       * @param       string          $embeddedObjectType
 -       * @return      array
 -       */
 -      public function getObjects($embeddedObjectType) {
 -              $embeddedObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message.embeddedObject', $embeddedObjectType);
 -              $returnValue = [];
 -              if (!empty($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID])) {
 -                      foreach ($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID] as $embeddedObjectID) {
 -                              if (isset($this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID])) {
 -                                      $returnValue[] = $this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID];
 -                              }
 -                      }
 -              }
 -              
 -              return $returnValue;
 -      }
 -      
 -      /**
 -       * Returns a specific embedded object.
 -       * 
 -       * @param       string          $embeddedObjectType
 -       * @param       integer         $objectID
 -       * @return      \wcf\data\DatabaseObject
 -       */
 -      public function getObject($embeddedObjectType, $objectID) {
 -              $embeddedObjectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message.embeddedObject', $embeddedObjectType);
 -              if (!empty($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID])) {
 -                      foreach ($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID] as $embeddedObjectID) {
 -                              if ($embeddedObjectID == $objectID) {
 -                                      if (isset($this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID])) {
 -                                              return $this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID];
 -                                      }
 -                              }
 -                      }
 -              }
 -              
 -              return null;
 -      }
 -      
 -      /**
 -       * Temporarily registers a message, the parsed data will not be stored.
 -       * 
 -       * @param       HtmlInputProcessor      $htmlInputProcessor     html input processor
 -       */
 -      public function registerTemporaryMessage(HtmlInputProcessor $htmlInputProcessor) {
 -              $context = $htmlInputProcessor->getContext();
 -              
 -              // set active message information
 -              $this->activeMessageObjectTypeID = $context['objectTypeID'];
 -              $this->activeMessageID = $context['objectID'];
 -              
 -              $embeddedData = $htmlInputProcessor->getEmbeddedContent();
 -              
 -              /** @var IMessageEmbeddedObjectHandler $handler */
 -              foreach ($this->getEmbeddedObjectHandlers() as $handler) {
 -                      $objectIDs = $handler->parse($htmlInputProcessor, $embeddedData);
 -                      
 -                      if (!empty($objectIDs)) {
 -                              // save assignments
 -                              $this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$handler->objectTypeID] = $objectIDs;
 -                              
 -                              // loads objects
 -                              $this->embeddedObjects[$handler->objectTypeID] = $handler->loadObjects($objectIDs);
 -                      }
 -              }
 -      }
 -      
 -      /**
 -       * @return      ISimpleMessageEmbeddedObjectHandler[];
 -       */
 -      public function getSimpleMessageEmbeddedObjectHandlers() {
 -              $handlers = [];
 -              foreach ($this->getEmbeddedObjectHandlers() as $handler) {
 -                      if ($handler instanceof ISimpleMessageEmbeddedObjectHandler) {
 -                              $name = lcfirst(preg_replace('~^.*\\\\([A-Z][a-zA-Z]+)MessageEmbeddedObjectHandler$~', '$1', get_class($handler)));
 -                              $handlers[$name] = $handler;
 -                      }
 -              }
 -              
 -              return $handlers;
 -      }
 -      
 -      /**
 -       * Returns all embedded object handlers.
 -       * 
 -       * @return      IMessageEmbeddedObjectHandler[]
 -       */
 -      protected function getEmbeddedObjectHandlers() {
 -              if ($this->embeddedObjectHandlers === null) {
 -                      $this->embeddedObjectHandlers = [];
 -                      foreach (ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.message.embeddedObject') as $objectType) {
 -                              $this->embeddedObjectHandlers[$objectType->objectTypeID] = $objectType->getProcessor();
 -                      }
 -              }
 -              
 -              return $this->embeddedObjectHandlers;
 -      }
 -      
 -      /**
 -       * Returns a specific embedded object handler.
 -       * 
 -       * @param       integer         $objectTypeID
 -       * @return      IMessageEmbeddedObjectHandler
 -       */
 -      protected function getEmbeddedObjectHandler($objectTypeID) {
 -              $this->getEmbeddedObjectHandlers();
 -              
 -              return $this->embeddedObjectHandlers[$objectTypeID];
 -      }
 -      
 -      /**
 -       * @deprecated  3.0
 -       */
 -      public function parseTemporaryMessage() {
 -              throw new \BadMethodCallException("parseTemporaryMessage() has been removed, please use registerTemporaryMessage() instead.");
 -      }
 +class MessageEmbeddedObjectManager extends SingletonFactory
 +{
 +    /**
 +     * caches message to embedded object assignments
 +     * @var array
 +     */
 +    protected $messageEmbeddedObjects = [];
 +
 +    /**
 +     * caches embedded objects
 +     * @var array
 +     */
 +    protected $embeddedObjects = [];
 +
 +    /**
 +     * object type of the active message
 +     * @var int
 +     */
 +    protected $activeMessageObjectTypeID;
 +
 +    /**
 +     * id of the active message
 +     * @var int
 +     */
 +    protected $activeMessageID;
 +
 +    /**
 +     * language id of the active message
 +     * @var int
 +     */
 +    protected $activeMessageLanguageID;
 +
 +    /**
 +     * list of embedded object handlers
 +     * @var array
 +     */
 +    protected $embeddedObjectHandlers;
 +
 +    /**
 +     * content language id
 +     * @var int
 +     */
 +    protected $contentLanguageID;
 +
 +    /**
 +     * local cache for bulk operations
 +     * @var mixed[][]
 +     */
 +    protected $bulkData = [
 +        'insert' => [],
 +        'remove' => [],
 +    ];
 +
++    /**
++     * A list of previous active message settings used to restore
++     * the internal state in case of nested message processing.
++     */
++    protected $activeMessageHistory = [];
++
 +    /**
 +     * Registers the embedded objects found in given message.
 +     *
 +     * @param HtmlInputProcessor $htmlInputProcessor html input processor instance holding embedded object data
 +     * @param bool $isBulk true for bulk operations
 +     * @return      bool                 true if at least one embedded object was found
 +     */
 +    public function registerObjects(HtmlInputProcessor $htmlInputProcessor, $isBulk = false)
 +    {
 +        $context = $htmlInputProcessor->getContext();
 +
 +        $messageObjectType = $context['objectType'];
 +        $messageObjectTypeID = $context['objectTypeID'];
 +        $messageID = $context['objectID'];
 +
 +        // delete existing assignments
 +        if ($isBulk) {
 +            if (!isset($this->bulkData['remove'][$messageObjectType])) {
 +                $this->bulkData['remove'][$messageObjectType] = [];
 +            }
 +            $this->bulkData['remove'][$messageObjectType][] = $messageID;
 +        } else {
 +            $this->removeObjects($messageObjectType, [$messageID]);
 +        }
 +
 +        $statement = null;
 +        if (!$isBulk) {
 +            // prepare statement
 +            $sql = "INSERT INTO wcf" . WCF_N . "_message_embedded_object
 +                                (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID)
 +                    VALUES      (?, ?, ?, ?)";
 +            $statement = WCF::getDB()->prepareStatement($sql);
 +
 +            WCF::getDB()->beginTransaction();
 +        }
 +
 +        $embeddedData = $htmlInputProcessor->getEmbeddedContent();
 +        $returnValue = false;
 +
 +        /** @var IMessageEmbeddedObjectHandler $handler */
 +        foreach ($this->getEmbeddedObjectHandlers() as $handler) {
 +            $objectIDs = $handler->parse($htmlInputProcessor, $embeddedData);
 +
 +            if (!empty($objectIDs)) {
 +                foreach ($objectIDs as $objectID) {
 +                    $parameters = [$messageObjectTypeID, $messageID, $handler->objectTypeID, $objectID];
 +                    if ($isBulk) {
 +                        $this->bulkData['insert'][] = $parameters;
 +                    } else {
 +                        $statement->execute($parameters);
 +                    }
 +                }
 +
 +                $returnValue = true;
 +            }
 +        }
 +
 +        if (!$isBulk) {
 +            WCF::getDB()->commitTransaction();
 +        }
 +
 +        return $returnValue;
 +    }
 +
 +    /**
 +     * Commits the bulk operation by performing all deletes and inserts
 +     * in one big transaction to save performance.
 +     */
 +    public function commitBulkOperation()
 +    {
 +        // delete existing data
 +        WCF::getDB()->beginTransaction();
 +        foreach ($this->bulkData['remove'] as $objectType => $objectIDs) {
 +            $this->removeObjects($objectType, $objectIDs);
 +        }
 +        WCF::getDB()->commitTransaction();
 +
 +        // prepare statement
 +        $sql = "INSERT INTO wcf" . WCF_N . "_message_embedded_object
 +                            (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID)
 +                VALUES      (?, ?, ?, ?)";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +
 +        WCF::getDB()->beginTransaction();
 +        foreach ($this->bulkData['insert'] as $parameters) {
 +            $statement->execute($parameters);
 +        }
 +        WCF::getDB()->commitTransaction();
 +
 +        // reset cache
 +        $this->bulkData = [
 +            'insert' => [],
 +            'remove' => [],
 +        ];
 +    }
 +
 +    /**
 +     * Registers the embedded objects found in a message using the simplified syntax.
 +     *
 +     * @param string $messageObjectType object type identifier
 +     * @param int $messageID object id
 +     * @param int[][] $embeddedContent list of object ids for embedded objects by object type id
 +     * @return      bool         true if at least one embedded object was found
 +     */
 +    public function registerSimpleObjects($messageObjectType, $messageID, array $embeddedContent)
 +    {
 +        $messageObjectTypeID = ObjectTypeCache::getInstance()
 +            ->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType);
 +
 +        // delete existing assignments
 +        $this->removeObjects($messageObjectType, [$messageID]);
 +
 +        if (empty($embeddedContent)) {
 +            return false;
 +        }
 +
 +        // prepare statement
 +        $sql = "INSERT INTO wcf" . WCF_N . "_message_embedded_object
 +                            (messageObjectTypeID, messageID, embeddedObjectTypeID, embeddedObjectID)
 +                VALUES      (?, ?, ?, ?)";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +
 +        // call embedded object handlers
 +        WCF::getDB()->beginTransaction();
 +        foreach ($embeddedContent as $objectTypeID => $objectIDs) {
 +            foreach ($objectIDs as $objectID) {
 +                $statement->execute([$messageObjectTypeID, $messageID, $objectTypeID, $objectID]);
 +            }
 +        }
 +        WCF::getDB()->commitTransaction();
 +
 +        return true;
 +    }
 +
 +    /**
 +     * Removes embedded object assignments for given messages.
 +     *
 +     * @param string $messageObjectType
 +     * @param int[] $messageIDs
 +     */
 +    public function removeObjects($messageObjectType, array $messageIDs)
 +    {
 +        $conditionBuilder = new PreparedStatementConditionBuilder();
 +        $conditionBuilder->add(
 +            'messageObjectTypeID = ?',
 +            [ObjectTypeCache::getInstance()->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType)]
 +        );
 +        $conditionBuilder->add('messageID IN (?)', [$messageIDs]);
 +
 +        $sql = "DELETE FROM wcf" . WCF_N . "_message_embedded_object
 +                " . $conditionBuilder;
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute($conditionBuilder->getParameters());
 +    }
 +
 +    /**
 +     * Loads the embedded objects for given messages.
 +     *
 +     * @param string $messageObjectType
 +     * @param int[] $messageIDs
 +     * @param int $contentLanguageID
 +     * @throws  InvalidObjectTypeException
 +     */
 +    public function loadObjects($messageObjectType, array $messageIDs, $contentLanguageID = null)
 +    {
 +        $messageObjectTypeID = ObjectTypeCache::getInstance()
 +            ->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType);
 +        if ($messageObjectTypeID === null) {
 +            throw new InvalidObjectTypeException($messageObjectType, 'com.woltlab.wcf.message');
 +        }
 +
 +        $conditionBuilder = new PreparedStatementConditionBuilder();
 +        $conditionBuilder->add('messageObjectTypeID = ?', [$messageObjectTypeID]);
 +        $conditionBuilder->add('messageID IN (?)', [$messageIDs]);
 +
 +        // get object ids
 +        $sql = "SELECT  *
 +                FROM    wcf" . WCF_N . "_message_embedded_object
 +                " . $conditionBuilder;
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute($conditionBuilder->getParameters());
 +        $embeddedObjects = [];
 +        while ($row = $statement->fetchArray()) {
 +            if (!isset($this->embeddedObjects[$row['embeddedObjectTypeID']][$row['embeddedObjectID']])) {
 +                // group objects by object type
 +                if (!isset($embeddedObjects[$row['embeddedObjectTypeID']])) {
 +                    $embeddedObjects[$row['embeddedObjectTypeID']] = [];
 +                }
 +                $embeddedObjects[$row['embeddedObjectTypeID']][] = $row['embeddedObjectID'];
 +            }
 +
 +            // store message to embedded object assignment
 +            if (!isset($this->messageEmbeddedObjects[$row['messageObjectTypeID']][$row['messageID']][$row['embeddedObjectTypeID']])) {
 +                $this->messageEmbeddedObjects[$row['messageObjectTypeID']][$row['messageID']][$row['embeddedObjectTypeID']] = [];
 +            }
 +            $this->messageEmbeddedObjects[$row['messageObjectTypeID']][$row['messageID']][$row['embeddedObjectTypeID']][] = $row['embeddedObjectID'];
 +        }
 +
 +        $this->contentLanguageID = $contentLanguageID;
 +
 +        // load objects
 +        foreach ($embeddedObjects as $embeddedObjectTypeID => $objectIDs) {
 +            if (!isset($this->embeddedObjects[$embeddedObjectTypeID])) {
 +                $this->embeddedObjects[$embeddedObjectTypeID] = [];
 +            }
 +            foreach ($this->getEmbeddedObjectHandler($embeddedObjectTypeID)->loadObjects(\array_unique($objectIDs)) as $objectID => $object) {
 +                $this->embeddedObjects[$embeddedObjectTypeID][$objectID] = $object;
 +            }
 +        }
 +
 +        $this->contentLanguageID = null;
 +    }
 +
 +    /**
 +     * Returns the content language id or null.
 +     *
 +     * @return      int
 +     */
 +    public function getContentLanguageID()
 +    {
 +        return $this->contentLanguageID;
 +    }
 +
 +    /**
 +     * Sets active message information.
 +     *
 +     * @param string $messageObjectType
 +     * @param int $messageID
 +     * @param int $languageID
 +     */
 +    public function setActiveMessage($messageObjectType, $messageID, $languageID = null)
 +    {
++        if ($this->activeMessageObjectTypeID) {
++            $this->activeMessageHistory[] = [
++                'activeMessageID' => $this->activeMessageID,
++                'activeMessageLanguageID' => $this->activeMessageLanguageID,
++                'activeMessageObjectTypeID' => $this->activeMessageObjectTypeID,
++            ];
++        }
++
 +        $this->activeMessageObjectTypeID = ObjectTypeCache::getInstance()
 +            ->getObjectTypeIDByName('com.woltlab.wcf.message', $messageObjectType);
 +        $this->activeMessageID = $messageID;
 +        $this->activeMessageLanguageID = $languageID;
 +    }
 +
 +    /**
++     * Restores the internal state in case of nested message processing.
++     */
++    public function reset()
++    {
++        $newState = \array_pop($this->activeMessageHistory);
++        if ($newState === null) {
++            $newState = [
++                'activeMessageID' => null,
++                'activeMessageLanguageID' => null,
++                'activeMessageObjectTypeID' => null,
++            ];
++        }
++
++        $this->activeMessageID = $newState['activeMessageID'];
++        $this->activeMessageLanguageID = $newState['activeMessageLanguageID'];
++        $this->activeMessageObjectTypeID = $newState['activeMessageObjectTypeID'];
++    }
++
++    /**
 +     * Returns the language id of the active message.
 +     *
 +     * @return      int
 +     */
 +    public function getActiveMessageLanguageID()
 +    {
 +        return $this->activeMessageLanguageID;
 +    }
 +
 +    /**
 +     * Returns all embedded objects of a specific type.
 +     *
 +     * @param string $embeddedObjectType
 +     * @return  array
 +     */
 +    public function getObjects($embeddedObjectType)
 +    {
 +        $embeddedObjectTypeID = ObjectTypeCache::getInstance()
 +            ->getObjectTypeIDByName('com.woltlab.wcf.message.embeddedObject', $embeddedObjectType);
 +        $returnValue = [];
 +        if (!empty($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID])) {
 +            foreach ($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID] as $embeddedObjectID) {
 +                if (isset($this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID])) {
 +                    $returnValue[] = $this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID];
 +                }
 +            }
 +        }
 +
 +        return $returnValue;
 +    }
 +
 +    /**
 +     * Returns a specific embedded object.
 +     *
 +     * @param string $embeddedObjectType
 +     * @param int $objectID
 +     * @return  \wcf\data\DatabaseObject
 +     */
 +    public function getObject($embeddedObjectType, $objectID)
 +    {
 +        $embeddedObjectTypeID = ObjectTypeCache::getInstance()
 +            ->getObjectTypeIDByName('com.woltlab.wcf.message.embeddedObject', $embeddedObjectType);
 +        if (!empty($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID])) {
 +            foreach ($this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$embeddedObjectTypeID] as $embeddedObjectID) {
 +                if ($embeddedObjectID == $objectID) {
 +                    if (isset($this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID])) {
 +                        return $this->embeddedObjects[$embeddedObjectTypeID][$embeddedObjectID];
 +                    }
 +                }
 +            }
 +        }
 +    }
 +
 +    /**
 +     * Temporarily registers a message, the parsed data will not be stored.
 +     *
 +     * @param HtmlInputProcessor $htmlInputProcessor html input processor
 +     */
 +    public function registerTemporaryMessage(HtmlInputProcessor $htmlInputProcessor)
 +    {
 +        $context = $htmlInputProcessor->getContext();
 +
 +        // set active message information
 +        $this->activeMessageObjectTypeID = $context['objectTypeID'];
 +        $this->activeMessageID = $context['objectID'];
 +
 +        $embeddedData = $htmlInputProcessor->getEmbeddedContent();
 +
 +        /** @var IMessageEmbeddedObjectHandler $handler */
 +        foreach ($this->getEmbeddedObjectHandlers() as $handler) {
 +            $objectIDs = $handler->parse($htmlInputProcessor, $embeddedData);
 +
 +            if (!empty($objectIDs)) {
 +                // save assignments
 +                $this->messageEmbeddedObjects[$this->activeMessageObjectTypeID][$this->activeMessageID][$handler->objectTypeID] = $objectIDs;
 +
 +                // loads objects
 +                $this->embeddedObjects[$handler->objectTypeID] = $handler->loadObjects($objectIDs);
 +            }
 +        }
 +    }
 +
 +    /**
 +     * @return      ISimpleMessageEmbeddedObjectHandler[];
 +     */
 +    public function getSimpleMessageEmbeddedObjectHandlers()
 +    {
 +        $handlers = [];
 +        foreach ($this->getEmbeddedObjectHandlers() as $handler) {
 +            if ($handler instanceof ISimpleMessageEmbeddedObjectHandler) {
 +                $name = \lcfirst(\preg_replace(
 +                    '~^.*\\\\([A-Z][a-zA-Z]+)MessageEmbeddedObjectHandler$~',
 +                    '$1',
 +                    \get_class($handler)
 +                ));
 +                $handlers[$name] = $handler;
 +            }
 +        }
 +
 +        return $handlers;
 +    }
 +
 +    /**
 +     * Returns all embedded object handlers.
 +     *
 +     * @return  IMessageEmbeddedObjectHandler[]
 +     */
 +    protected function getEmbeddedObjectHandlers()
 +    {
 +        if ($this->embeddedObjectHandlers === null) {
 +            $this->embeddedObjectHandlers = [];
 +            foreach (ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.message.embeddedObject') as $objectType) {
 +                $this->embeddedObjectHandlers[$objectType->objectTypeID] = $objectType->getProcessor();
 +            }
 +        }
 +
 +        return $this->embeddedObjectHandlers;
 +    }
 +
 +    /**
 +     * Returns a specific embedded object handler.
 +     *
 +     * @param int $objectTypeID
 +     * @return  IMessageEmbeddedObjectHandler
 +     */
 +    protected function getEmbeddedObjectHandler($objectTypeID)
 +    {
 +        $this->getEmbeddedObjectHandlers();
 +
 +        return $this->embeddedObjectHandlers[$objectTypeID];
 +    }
 +
 +    /**
 +     * @deprecated  3.0
 +     */
 +    public function parseTemporaryMessage()
 +    {
 +        throw new \BadMethodCallException(
 +            "parseTemporaryMessage() has been removed, please use registerTemporaryMessage() instead."
 +        );
 +    }
  }
index 669356a47382783231f999f7e40b127d0de6823d,85b52fb7c37ddaeffe5054251fb9d01f4fff50c9..6259192e398ba44dd82bba682fc72b756cf94be3
@@@ -9,66 -7,62 +9,65 @@@ use wcf\system\SingletonFactory
  
  /**
   * Caches parsed user signatures.
 - * 
 - * @author    Marcel Werk
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\User\Signature
 + *
 + * @author  Marcel Werk
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\User\Signature
   */
 -class SignatureCache extends SingletonFactory {
 -      /**
 -       * @var HtmlOutputProcessor
 -       */
 -      protected $htmlOutputProcessor;
 -      
 -      /**
 -       * cached signatures
 -       * @var string
 -       */
 -      protected $signatures = [];
 -      
 -      /**
 -       * The userIDs which are cached by the message embedded object manager. 
 -       * @var integer[]
 -       */
 -      protected $cachedUserIDs = [];
 -      
 -      /**
 -       * Returns a parsed user signature.
 -       * 
 -       * @param       User            $user           user object
 -       * @return      string          parsed signature
 -       */
 -      public function getSignature(User $user) {
 -              if (!isset($this->signatures[$user->userID])) {
 -                      if ($this->htmlOutputProcessor === null) {
 -                              $this->htmlOutputProcessor = new HtmlOutputProcessor();
 -                      }
 -                      
 -                      if (!in_array($user->userID, $this->cachedUserIDs)) {
 -                              $this->cacheUserSignature([$user->userID]);
 -                      }
 -                      
 -                      $this->htmlOutputProcessor->process($user->signature, 'com.woltlab.wcf.user.signature', $user->userID);
 -                      $this->signatures[$user->userID] = $this->htmlOutputProcessor->getHtml();
 -              }
 -              
 -              return $this->signatures[$user->userID];
 -      }
 -      
 -      /**
 -       * Loads the embedded objects for the given users. 
 -       * 
 -       * @param       integer[]       $userIDs
 -       * @since       5.2
 -       */
 -      public function cacheUserSignature(array $userIDs) {
 -              $this->cachedUserIDs = array_merge($this->cachedUserIDs, $userIDs);
 -              
 -              MessageEmbeddedObjectManager::getInstance()->loadObjects('com.woltlab.wcf.user.signature', $userIDs);
 -      }
 +class SignatureCache extends SingletonFactory
 +{
 +    /**
 +     * @var HtmlOutputProcessor
 +     */
 +    protected $htmlOutputProcessor;
 +
 +    /**
 +     * cached signatures
 +     * @var string
 +     */
 +    protected $signatures = [];
 +
 +    /**
 +     * The userIDs which are cached by the message embedded object manager.
 +     * @var int[]
 +     */
 +    protected $cachedUserIDs = [];
 +
 +    /**
 +     * Returns a parsed user signature.
 +     *
 +     * @param User $user user object
 +     * @return  string          parsed signature
 +     */
 +    public function getSignature(User $user)
 +    {
 +        if (!isset($this->signatures[$user->userID])) {
 +            if ($this->htmlOutputProcessor === null) {
 +                $this->htmlOutputProcessor = new HtmlOutputProcessor();
 +            }
 +
 +            if (!\in_array($user->userID, $this->cachedUserIDs)) {
 +                $this->cacheUserSignature([$user->userID]);
 +            }
 +
-             $this->htmlOutputProcessor->setContext('com.woltlab.wcf.user.signature', $user->userID);
 +            $this->htmlOutputProcessor->process($user->signature, 'com.woltlab.wcf.user.signature', $user->userID);
 +            $this->signatures[$user->userID] = $this->htmlOutputProcessor->getHtml();
 +        }
 +
 +        return $this->signatures[$user->userID];
 +    }
 +
 +    /**
 +     * Loads the embedded objects for the given users.
 +     *
 +     * @param int[] $userIDs
 +     * @since       5.2
 +     */
 +    public function cacheUserSignature(array $userIDs)
 +    {
 +        $this->cachedUserIDs = \array_merge($this->cachedUserIDs, $userIDs);
 +
 +        MessageEmbeddedObjectManager::getInstance()->loadObjects('com.woltlab.wcf.user.signature', $userIDs);
 +    }
  }