Merge branch 'refs/heads/pr/90' into next
[GitHub/WoltLab/com.woltlab.wcf.conversation.git] / files / lib / data / conversation / message / ConversationMessageAction.class.php
index 7800a265693bfa0fae171bd20acc485592a69c7c..00f0b05a1e98986876b4b98350719b6e2c763929 100644 (file)
@@ -6,7 +6,7 @@ use wcf\data\conversation\ConversationEditor;
 use wcf\data\smiley\SmileyCache;
 use wcf\data\AbstractDatabaseObjectAction;
 use wcf\data\DatabaseObject;
-use wcf\data\IExtendedMessageQuickReplyAction;
+use wcf\data\IAttachmentMessageQuickReplyAction;
 use wcf\data\IMessageInlineEditorAction;
 use wcf\data\IMessageQuoteAction;
 use wcf\system\attachment\AttachmentHandler;
@@ -15,7 +15,9 @@ use wcf\system\bbcode\BBCodeParser;
 use wcf\system\bbcode\PreParser;
 use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\UserInputException;
+use wcf\system\html\input\HtmlInputProcessor;
 use wcf\system\message\censorship\Censorship;
+use wcf\system\message\embedded\object\MessageEmbeddedObjectManager;
 use wcf\system\message\quote\MessageQuoteManager;
 use wcf\system\message\QuickReplyManager;
 use wcf\system\moderation\queue\ModerationQueueManager;
@@ -25,40 +27,49 @@ use wcf\system\user\notification\object\ConversationMessageUserNotificationObjec
 use wcf\system\user\notification\UserNotificationHandler;
 use wcf\system\user\storage\UserStorageHandler;
 use wcf\system\WCF;
-use wcf\util\MessageUtil;
+use wcf\util\StringUtil;
 
 /**
  * Executes conversation message-related actions.
  * 
  * @author     Marcel Werk
- * @copyright  2001-2014 WoltLab GmbH
+ * @copyright  2001-2016 WoltLab GmbH
  * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    com.woltlab.wcf.conversation
- * @subpackage data.conversation.message
- * @category   Community Framework
+ * @package    WoltLabSuite\Core\Data\Conversation\Message
+ * 
+ * @method     ConversationMessageEditor[]     getObjects()
+ * @method     ConversationMessageEditor       getSingleObject()
  */
-class ConversationMessageAction extends AbstractDatabaseObjectAction implements IExtendedMessageQuickReplyAction, IMessageInlineEditorAction, IMessageQuoteAction {
+class ConversationMessageAction extends AbstractDatabaseObjectAction implements IAttachmentMessageQuickReplyAction, IMessageInlineEditorAction, IMessageQuoteAction {
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::$className
+        * @inheritDoc
         */
-       protected $className = 'wcf\data\conversation\message\ConversationMessageEditor';
+       protected $className = ConversationMessageEditor::class;
        
        /**
         * conversation object
-        * @var \wcf\data\conversation\Conversation
+        * @var Conversation
         */
-       public $conversation = null;
+       public $conversation;
+       
+       /**
+        * @var HtmlInputProcessor
+        */
+       public $htmlInputProcessor;
        
        /**
         * conversation message object
-        * @var \wcf\data\conversation\message\ConversationMessage
+        * @var ConversationMessage
         */
-       public $message = null;
+       public $message;
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::create()
+        * @inheritDoc
+        * @return      ConversationMessage
         */
        public function create() {
+               if (!isset($this->parameters['data']['enableHtml'])) $this->parameters['data']['enableHtml'] = 1;
+               
                // count attachments
                if (isset($this->parameters['attachmentHandler']) && $this->parameters['attachmentHandler'] !== null) {
                        $this->parameters['data']['attachments'] = count($this->parameters['attachmentHandler']);
@@ -77,11 +88,18 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
                        }
                }
                
+               if (!empty($this->parameters['htmlInputProcessor'])) {
+                       /** @noinspection PhpUndefinedMethodInspection */
+                       $this->parameters['data']['message'] = $this->parameters['htmlInputProcessor']->getHtml();
+               }
+               
                // create message
+               /** @var ConversationMessage $message */
                $message = parent::create();
+               $messageEditor = new ConversationMessageEditor($message);
                
                // get conversation
-               $conversation = (isset($this->parameters['converation']) ? $this->parameters['converation'] : new Conversation($message->conversationID));
+               $conversation = (isset($this->parameters['conversation']) ? $this->parameters['conversation'] : new Conversation($message->conversationID));
                $conversationEditor = new ConversationEditor($conversation);
                
                if (empty($this->parameters['isFirstPost'])) {
@@ -90,7 +108,7 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
                        
                        // fire notification event
                        if (!$conversation->isDraft) {
-                               $notificationRecipients = array_diff($conversation->getParticipantIDs(true), array($message->userID)); // don't notify message author
+                               $notificationRecipients = array_diff($conversation->getParticipantIDs(true), [$message->userID]); // don't notify message author
                                if (!empty($notificationRecipients)) {
                                        UserNotificationHandler::getInstance()->fireEvent('conversationMessage', 'com.woltlab.wcf.conversation.message.notification', new ConversationMessageUserNotificationObject($message), $notificationRecipients);
                                }
@@ -104,7 +122,7 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
                                        WHERE   participantID = ?
                                                AND conversationID = ?";
                                $statement = WCF::getDB()->prepareStatement($sql);
-                               $statement->execute(array($message->userID, $conversation->conversationID));
+                               $statement->execute([$message->userID, $conversation->conversationID]);
                                
                                $conversationEditor->updateParticipantSummary();
                                $conversationEditor->updateParticipantCount();
@@ -116,24 +134,35 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
                                WHERE   conversationID = ?
                                        AND hideConversation = ?";
                        $statement = WCF::getDB()->prepareStatement($sql);
-                       $statement->execute(array(
+                       $statement->execute([
                                Conversation::STATE_DEFAULT,
                                $conversation->conversationID,
                                Conversation::STATE_HIDDEN
-                       ));
+                       ]);
                }
                
                // reset storage
                UserStorageHandler::getInstance()->reset($conversation->getParticipantIDs(), 'unreadConversationCount');
                
                // update search index
-               SearchIndexManager::getInstance()->add('com.woltlab.wcf.conversation.message', $message->messageID, $message->message, (!empty($this->parameters['isFirstPost']) ? $conversation->subject : ''), $message->time, $message->userID, $message->username);
+               SearchIndexManager::getInstance()->set('com.woltlab.wcf.conversation.message', $message->messageID, $message->message, (!empty($this->parameters['isFirstPost']) ? $conversation->subject : ''), $message->time, $message->userID, $message->username);
                
                // update attachments
                if (isset($this->parameters['attachmentHandler']) && $this->parameters['attachmentHandler'] !== null) {
+                       /** @noinspection PhpUndefinedMethodInspection */
                        $this->parameters['attachmentHandler']->updateObjectID($message->messageID);
                }
                
+               // save embedded objects
+               if (!empty($this->parameters['htmlInputProcessor'])) {
+                       /** @noinspection PhpUndefinedMethodInspection */
+                       $this->parameters['htmlInputProcessor']->setObjectID($message->messageID);
+                       
+                       if (MessageEmbeddedObjectManager::getInstance()->registerObjects($this->parameters['htmlInputProcessor'])) {
+                               $messageEditor->update(['hasEmbeddedObjects' => 1]);
+                       }
+               }
+               
                // clear quotes
                if (isset($this->parameters['removeQuoteIDs']) && !empty($this->parameters['removeQuoteIDs'])) {
                        MessageQuoteManager::getInstance()->markQuotesForRemoval($this->parameters['removeQuoteIDs']);
@@ -145,7 +174,7 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
        }
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::update()
+        * @inheritDoc
         */
        public function update() {
                // count attachments
@@ -153,23 +182,39 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
                        $this->parameters['data']['attachments'] = count($this->parameters['attachmentHandler']);
                }
                
+               if (!empty($this->parameters['htmlInputProcessor'])) {
+                       /** @noinspection PhpUndefinedMethodInspection */
+                       $this->parameters['data']['message'] = $this->parameters['htmlInputProcessor']->getHtml();
+               }
+               
                parent::update();
                
-               // update search index
-               foreach ($this->objects as $message) {
-                       $conversation = $message->getConversation();
-                       SearchIndexManager::getInstance()->update('com.woltlab.wcf.conversation.message', $message->messageID, $message->message, ($conversation->firstMessageID == $message->messageID ? $conversation->subject : ''), $message->time, $message->userID, $message->username);
+               // update search index / embedded objects
+               if (isset($this->parameters['data']) && isset($this->parameters['data']['message'])) {
+                       foreach ($this->getObjects() as $message) {
+                               $conversation = $message->getConversation();
+                               SearchIndexManager::getInstance()->set('com.woltlab.wcf.conversation.message', $message->messageID, $this->parameters['data']['message'], ($conversation->firstMessageID == $message->messageID ? $conversation->subject : ''), $message->time, $message->userID, $message->username);
+                               
+                               if (!empty($this->parameters['htmlInputProcessor'])) {
+                                       /** @noinspection PhpUndefinedMethodInspection */
+                                       $this->parameters['htmlInputProcessor']->setObjectID($message->messageID);
+                                       
+                                       if ($message->hasEmbeddedObjects != MessageEmbeddedObjectManager::getInstance()->registerObjects($this->parameters['htmlInputProcessor'])) {
+                                               $message->update(['hasEmbeddedObjects' => ($message->hasEmbeddedObjects ? 0 : 1)]);
+                                       }
+                               }
+                       }
                }
        }
        
        /**
-        * @see \wcf\data\AbstractDatabaseObjectAction::delete()
+        * @inheritDoc
         */
        public function delete() {
                $count = parent::delete();
                
-               $attachmentMessageIDs = $conversationIDs = array();
-               foreach ($this->objects as $message) {
+               $attachmentMessageIDs = $conversationIDs = [];
+               foreach ($this->getObjects() as $message) {
                        if (!in_array($message->conversationID, $conversationIDs)) {
                                $conversationIDs[] = $message->conversationID;
                        }
@@ -187,11 +232,14 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
                
                if (!empty($this->objectIDs)) {
                        // delete notifications
-                       UserNotificationHandler::getInstance()->deleteNotifications('conversationMessage', 'com.woltlab.wcf.conversation.message.notification', array(), $this->objectIDs);
+                       UserNotificationHandler::getInstance()->removeNotifications('com.woltlab.wcf.conversation.message.notification', $this->objectIDs);
                        
                        // update search index
                        SearchIndexManager::getInstance()->delete('com.woltlab.wcf.conversation.message', $this->objectIDs);
                        
+                       // update embedded objects
+                       MessageEmbeddedObjectManager::getInstance()->removeObjects('com.woltlab.wcf.conversation.message', $this->objectIDs);
+
                        // remove moderation queues
                        ModerationQueueManager::getInstance()->removeQueues('com.woltlab.wcf.conversation.message', $this->objectIDs);
                }
@@ -205,38 +253,39 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
        }
        
        /**
-        * @see \wcf\data\IMessageQuickReply::validateQuickReply()
+        * @inheritDoc
         */
        public function validateQuickReply() {
-               QuickReplyManager::getInstance()->setAllowedBBCodes(explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')));
-               QuickReplyManager::getInstance()->validateParameters($this, $this->parameters, 'wcf\data\conversation\Conversation');
+               QuickReplyManager::getInstance()->setDisallowedBBCodes(explode(',', WCF::getSession()->getPermission('user.message.disallowedBBCodes')));
+               QuickReplyManager::getInstance()->validateParameters($this, $this->parameters, Conversation::class);
        }
        
        /**
-        * @see \wcf\data\IMessageQuickReply::quickReply()
+        * @inheritDoc
         */
        public function quickReply() {
                return QuickReplyManager::getInstance()->createMessage(
                        $this,
                        $this->parameters,
-                       'wcf\data\conversation\ConversationAction',
+                       ConversationAction::class,
                        CONVERSATION_LIST_DEFAULT_SORT_ORDER,
                        'conversationMessageList'
                );
        }
        
        /**
-        * @see \wcf\data\IExtendedMessageQuickReplyAction::validateJumpToExtended()
+        * @inheritDoc
         */
        public function validateJumpToExtended() {
                $this->readInteger('containerID');
                $this->readString('message', true);
+               $this->readString('tmpHash', true);
                
                $this->conversation = new Conversation($this->parameters['containerID']);
                if (!$this->conversation->conversationID) {
                        throw new UserInputException('containerID');
                }
-               else if ($this->conversation->isClosed || !Conversation::isParticipant(array($this->conversation->conversationID))) {
+               else if ($this->conversation->isClosed || !Conversation::isParticipant([$this->conversation->conversationID])) {
                        throw new PermissionDeniedException();
                }
                
@@ -254,28 +303,32 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
        }
        
        /**
-        * @see \wcf\data\IExtendedMessageQuickReplyAction::jumpToExtended()
+        * @inheritDoc
         */
        public function jumpToExtended() {
                // quick reply
                if ($this->message === null) {
                        QuickReplyManager::getInstance()->setMessage('conversation', $this->conversation->conversationID, $this->parameters['message']);
-                       $url = LinkHandler::getInstance()->getLink('ConversationMessageAdd', array('id' => $this->conversation->conversationID));
+                       $url = LinkHandler::getInstance()->getLink('ConversationMessageAdd', ['id' => $this->conversation->conversationID]);
                }
                else {
                        // editing message
                        QuickReplyManager::getInstance()->setMessage('conversationMessage', $this->message->messageID, $this->parameters['message']);
-                       $url = LinkHandler::getInstance()->getLink('ConversationMessageEdit', array('id' => $this->message->messageID));
+                       $url = LinkHandler::getInstance()->getLink('ConversationMessageEdit', ['id' => $this->message->messageID]);
+               }
+               
+               if (!empty($this->parameters['tmpHash'])) {
+                       QuickReplyManager::getInstance()->setTmpHash($this->parameters['tmpHash']);
                }
                
                // redirect
-               return array(
+               return [
                        'url' => $url
-               );
+               ];
        }
        
        /**
-        * @see \wcf\data\IMessageInlineEditorAction::validateBeginEdit()
+        * @inheritDoc
         */
        public function validateBeginEdit() {
                $this->readInteger('containerID');
@@ -286,7 +339,7 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
                        throw new UserInputException('containerID');
                }
                
-               if ($this->conversation->isClosed || !Conversation::isParticipant(array($this->conversation->conversationID))) {
+               if ($this->conversation->isClosed || !Conversation::isParticipant([$this->conversation->conversationID])) {
                        throw new PermissionDeniedException();
                }
                
@@ -298,29 +351,44 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
                if (!$this->message->canEdit()) {
                        throw new PermissionDeniedException();
                }
+               
+               BBCodeHandler::getInstance()->setDisallowedBBCodes(explode(',', WCF::getSession()->getPermission('user.message.disallowedBBCodes')));
        }
        
        /**
-        * @see \wcf\data\IMessageInlineEditorAction::beginEdit()
+        * @inheritDoc
         */
        public function beginEdit() {
-               BBCodeHandler::getInstance()->setAllowedBBCodes(explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')));
-               
-               WCF::getTPL()->assign(array(
+               WCF::getTPL()->assign([
                        'defaultSmilies' => SmileyCache::getInstance()->getCategorySmilies(),
                        'message' => $this->message,
                        'permissionCanUseSmilies' => 'user.message.canUseSmilies',
                        'wysiwygSelector' => 'messageEditor'.$this->message->messageID
-               ));
+               ]);
+               
+               if (MODULE_ATTACHMENT) {
+                       $tmpHash = StringUtil::getRandomID();
+                       $attachmentHandler = new AttachmentHandler('com.woltlab.wcf.conversation.message', $this->message->messageID, $tmpHash);
+                       $attachmentList = $attachmentHandler->getAttachmentList();
+                               
+                       WCF::getTPL()->assign([
+                               'attachmentHandler' => $attachmentHandler,
+                               'attachmentList' => $attachmentList->getObjects(),
+                               'attachmentObjectID' => $this->message->messageID,
+                               'attachmentObjectType' => 'com.woltlab.wcf.conversation.message',
+                               'attachmentParentObjectID' => 0,
+                               'tmpHash' => $tmpHash
+                       ]);
+               }
                
-               return array(
+               return [
                        'actionName' => 'beginEdit',
                        'template' => WCF::getTPL()->fetch('conversationMessageInlineEditor')
-               );
+               ];
        }
        
        /**
-        * @see \wcf\data\IMessageInlineEditorAction::validateSave()
+        * @inheritDoc
         */
        public function validateSave() {
                $this->readString('message', true, 'data');
@@ -330,72 +398,123 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
                }
                
                $this->validateBeginEdit();
-               $this->validateMessage($this->conversation, $this->parameters['data']['message']);
+               
+               $this->validateMessage($this->conversation, $this->getHtmlInputProcessor($this->parameters['data']['message'], $this->message->messageID));
        }
        
        /**
-        * @see \wcf\data\IMessageInlineEditorAction::save()
+        * @inheritDoc
         */
        public function save() {
-               $messageEditor = new ConversationMessageEditor($this->message);
-               $messageEditor->update(array(
-                       'message' => PreParser::getInstance()->parse(MessageUtil::stripCrap($this->parameters['data']['message']), explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')))
-               ));
+               $data = [];
+               
+               if (!$this->message->getConversation()->isDraft) {
+                       $data['lastEditTime'] = TIME_NOW;
+                       $data['editCount'] = $this->message->editCount + 1;
+               }
+               // execute update action
+               $action = new ConversationMessageAction([$this->message], 'update', [
+                       'data' => $data,
+                       'htmlInputProcessor' => $this->getHtmlInputProcessor()
+               ]);
+               $action->executeAction();
                
                // load new message
                $this->message = new ConversationMessage($this->message->messageID);
                $this->message->getAttachments();
                
-               return array(
+               $attachmentList = null;
+               if (MODULE_ATTACHMENT) {
+                       $attachmentList = $this->message->getAttachments(true);
+                       $count = 0;
+                       if ($attachmentList !== null) {
+                               // set permissions
+                               $attachmentList->setPermissions([
+                                       'canDownload' => true,
+                                       'canViewPreview' => true
+                               ]);
+                               
+                               $count = count($attachmentList);
+                       }
+                       
+                       // update count to reflect number of attachments after edit
+                       if ($count != $this->message->attachments) {
+                               $messageEditor = new ConversationMessageEditor($this->message);
+                               $messageEditor->update(['attachments' => $count]);
+                       }
+               }
+               
+               // load embedded objects
+               MessageEmbeddedObjectManager::getInstance()->loadObjects('com.woltlab.wcf.conversation.message', [$this->message->messageID]);
+               
+               $data = [
                        'actionName' => 'save',
                        'message' => $this->message->getFormattedMessage()
-               );
+               ];
+               
+               if (MODULE_ATTACHMENT) {
+                       WCF::getTPL()->assign([
+                               'attachmentList' => $attachmentList,
+                               'objectID' => $this->message->messageID
+                       ]);
+                       $data['attachmentList'] = WCF::getTPL()->fetch('attachments');
+               }
+               
+               return $data;
        }
        
        /**
-        * @see \wcf\data\IMessageQuickReply::validateContainer()
+        * @inheritDoc
         */
        public function validateContainer(DatabaseObject $conversation) {
+               /** @var Conversation $conversation */
+               
                if (!$conversation->conversationID) {
                        throw new UserInputException('objectID');
                }
-               else if ($conversation->isClosed || !Conversation::isParticipant(array($conversation->conversationID))) {
+               if ($conversation->isClosed) {
+                       throw new PermissionDeniedException();
+               }
+               $conversation->loadUserParticipation();
+               if (!$conversation->canRead()) {
                        throw new PermissionDeniedException();
                }
        }
        
        /**
-        * @see \wcf\data\IMessageQuickReplyAction::validateMessage()
+        * @inheritDoc
         */
-       public function validateMessage(DatabaseObject $container, $message) {
-               $message = MessageUtil::stripCrap($message);
-               
+       public function validateMessage(DatabaseObject $container, HtmlInputProcessor $htmlInputProcessor) {
+               $message = $htmlInputProcessor->getTextContent();
                if (mb_strlen($message) > WCF::getSession()->getPermission('user.conversation.maxLength')) {
-                       throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.tooLong', array('maxTextLength' => WCF::getSession()->getPermission('user.conversation.maxLength'))));
+                       throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.tooLong', ['maxTextLength' => WCF::getSession()->getPermission('user.conversation.maxLength')]));
                }
                
                // search for disallowed bbcodes
-               $disallowedBBCodes = BBCodeParser::getInstance()->validateBBCodes($message, explode(',', WCF::getSession()->getPermission('user.message.allowedBBCodes')));
+               $disallowedBBCodes = $htmlInputProcessor->validate();
                if (!empty($disallowedBBCodes)) {
-                       throw new UserInputException('text', WCF::getLanguage()->getDynamicVariable('wcf.message.error.disallowedBBCodes', array('disallowedBBCodes' => $disallowedBBCodes)));
+                       throw new UserInputException('text', WCF::getLanguage()->getDynamicVariable('wcf.message.error.disallowedBBCodes', ['disallowedBBCodes' => $disallowedBBCodes]));
                }
                
                // search for censored words
                if (ENABLE_CENSORSHIP) {
                        $result = Censorship::getInstance()->test($message);
                        if ($result) {
-                               throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.censoredWordsFound', array('censoredWords' => $result)));
+                               throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.censoredWordsFound', ['censoredWords' => $result]));
                        }
                }
        }
        
        /**
-        * @see \wcf\data\IMessageQuickReplyAction::getMessageList()
+        * @inheritDoc
         */
        public function getMessageList(DatabaseObject $conversation, $lastMessageTime) {
+               /** @var Conversation $conversation */
+               
                $messageList = new ViewableConversationMessageList();
-               $messageList->getConditionBuilder()->add("conversation_message.conversationID = ?", array($conversation->conversationID));
-               $messageList->getConditionBuilder()->add("conversation_message.time > ?", array($lastMessageTime));
+               $messageList->setConversation($conversation);
+               $messageList->getConditionBuilder()->add("conversation_message.conversationID = ?", [$conversation->conversationID]);
+               $messageList->getConditionBuilder()->add("conversation_message.time > ?", [$lastMessageTime]);
                $messageList->sqlOrderBy = "conversation_message.time ".CONVERSATION_LIST_DEFAULT_SORT_ORDER;
                $messageList->readObjects();
                
@@ -403,81 +522,105 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
        }
        
        /**
-        * @see \wcf\data\IMessageQuickReply::getPageNo()
+        * @inheritDoc
         */
        public function getPageNo(DatabaseObject $conversation) {
+               /** @var Conversation $conversation */
+               
                $sql = "SELECT  COUNT(*) AS count
                        FROM    wcf".WCF_N."_conversation_message
                        WHERE   conversationID = ?";
                $statement = WCF::getDB()->prepareStatement($sql);
-               $statement->execute(array($conversation->conversationID));
+               $statement->execute([$conversation->conversationID]);
                $count = $statement->fetchArray();
                
-               return array(intval(ceil($count['count'] / CONVERSATION_MESSAGES_PER_PAGE)), $count['count']);
+               return [intval(ceil($count['count'] / CONVERSATION_MESSAGES_PER_PAGE)), $count['count']];
        }
        
        /**
-        * @see \wcf\data\IMessageQuickReply::getRedirectUrl()
+        * @inheritDoc
         */
        public function getRedirectUrl(DatabaseObject $conversation, DatabaseObject $message) {
-               return LinkHandler::getInstance()->getLink('Conversation', array(
+               /** @var ConversationMessage $message */
+               return LinkHandler::getInstance()->getLink('Conversation', [
                        'object' => $conversation,
                        'messageID' => $message->messageID
-               )).'#message'.$message->messageID;
+               ]).'#message'.$message->messageID;
        }
        
        /**
-        * @see \wcf\data\IMessageQuoteAction::validateSaveFullQuote()
+        * @inheritDoc
         */
        public function validateSaveFullQuote() {
                $this->message = $this->getSingleObject();
                
-               if (!Conversation::isParticipant(array($this->message->conversationID))) {
+               if (!Conversation::isParticipant([$this->message->conversationID])) {
                        throw new PermissionDeniedException();
                }
        }
        
        /**
-        * @see \wcf\data\IMessageQuoteAction::saveFullQuote()
+        * @inheritDoc
         */
        public function saveFullQuote() {
-               if (!MessageQuoteManager::getInstance()->addQuote('com.woltlab.wcf.conversation.message', $this->message->conversationID, $this->message->messageID, $this->message->getExcerpt(), $this->message->getMessage())) {
-                       $quoteID = MessageQuoteManager::getInstance()->getQuoteID('com.woltlab.wcf.conversation.message', $this->message->conversationID, $this->message->messageID, $this->message->getExcerpt(), $this->message->getMessage());
-                       MessageQuoteManager::getInstance()->removeQuote($quoteID);
+               $quoteID = MessageQuoteManager::getInstance()->addQuote(
+                       'com.woltlab.wcf.conversation.message',
+                       $this->message->conversationID,
+                       $this->message->messageID,
+                       $this->message->getExcerpt(),
+                       $this->message->getMessage()
+               );
+               
+               if ($quoteID === false) {
+                       $removeQuoteID = MessageQuoteManager::getInstance()->getQuoteID('com.woltlab.wcf.conversation.message', $this->message->messageID, $this->message->getExcerpt(), $this->message->getMessage());
+                       MessageQuoteManager::getInstance()->removeQuote($removeQuoteID);
                }
                
-               return array(
+               $returnValues = [
                        'count' => MessageQuoteManager::getInstance()->countQuotes(),
-                       'fullQuoteMessageIDs' => MessageQuoteManager::getInstance()->getFullQuoteObjectIDs(array('com.woltlab.wcf.conversation.message'))
-               );
+                       'fullQuoteMessageIDs' => MessageQuoteManager::getInstance()->getFullQuoteObjectIDs(['com.woltlab.wcf.conversation.message'])
+               ];
+               
+               if ($quoteID) {
+                       $returnValues['renderedQuote'] = MessageQuoteManager::getInstance()->getQuoteComponents($quoteID);
+               }
+               
+               return $returnValues;
        }
        
        /**
-        * @see \wcf\data\IMessageQuoteAction::validateSaveQuote()
+        * @inheritDoc
         */
        public function validateSaveQuote() {
                $this->readString('message');
+               $this->readBoolean('renderQuote', true);
                $this->message = $this->getSingleObject();
                
-               if (!Conversation::isParticipant(array($this->message->conversationID))) {
+               if (!Conversation::isParticipant([$this->message->conversationID])) {
                        throw new PermissionDeniedException();
                }
        }
        
        /**
-        * @see \wcf\data\IMessageQuoteAction::saveQuote()
+        * @inheritDoc
         */
        public function saveQuote() {
-               MessageQuoteManager::getInstance()->addQuote('com.woltlab.wcf.conversation.message', $this->message->conversationID, $this->message->messageID, $this->parameters['message']);
+               $quoteID = MessageQuoteManager::getInstance()->addQuote('com.woltlab.wcf.conversation.message', $this->message->conversationID, $this->message->messageID, $this->parameters['message'], false);
                
-               return array(
+               $returnValues = [
                        'count' => MessageQuoteManager::getInstance()->countQuotes(),
-                       'fullQuoteMessageIDs' => MessageQuoteManager::getInstance()->getFullQuoteObjectIDs(array('com.woltlab.wcf.conversation.message'))
-               );
+                       'fullQuoteMessageIDs' => MessageQuoteManager::getInstance()->getFullQuoteObjectIDs(['com.woltlab.wcf.conversation.message'])
+               ];
+               
+               if ($this->parameters['renderQuote']) {
+                       $returnValues['renderedQuote'] = MessageQuoteManager::getInstance()->getQuoteComponents($quoteID);
+               }
+               
+               return $returnValues;
        }
        
        /**
-        * @see \wcf\data\IMessageQuoteAction::validateGetRenderedQuotes()
+        * @inheritDoc
         */
        public function validateGetRenderedQuotes() {
                $this->readInteger('parentObjectID');
@@ -489,13 +632,34 @@ class ConversationMessageAction extends AbstractDatabaseObjectAction implements
        }
        
        /**
-        * @see \wcf\data\IMessageQuoteAction::getRenderedQuotes()
+        * @inheritDoc
         */
        public function getRenderedQuotes() {
                $quotes = MessageQuoteManager::getInstance()->getQuotesByParentObjectID('com.woltlab.wcf.conversation.message', $this->conversation->conversationID);
                
-               return array(
+               return [
                        'template' => implode("\n\n", $quotes)
-               );
+               ];
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getAttachmentHandler(DatabaseObject $conversation) {
+               return new AttachmentHandler('com.woltlab.wcf.conversation.message', 0, $this->parameters['tmpHash']);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function getHtmlInputProcessor($message = null, $objectID = 0) {
+               if ($message === null) {
+                       return $this->htmlInputProcessor;
+               }
+               
+               $this->htmlInputProcessor = new HtmlInputProcessor();
+               $this->htmlInputProcessor->process($message, 'com.woltlab.wcf.conversation.message', $objectID);
+               
+               return $this->htmlInputProcessor;
        }
 }