Implemented input/output processing for comments
authorAlexander Ebert <ebert@woltlab.com>
Thu, 2 Mar 2017 11:03:32 +0000 (12:03 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Thu, 2 Mar 2017 11:03:38 +0000 (12:03 +0100)
See #2222

com.woltlab.wcf/userGroupOption.xml
wcfsetup/install/files/js/WCF.Comment.js
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Comment/Add.js [new file with mode: 0644]
wcfsetup/install/files/lib/data/comment/Comment.class.php
wcfsetup/install/files/lib/data/comment/CommentAction.class.php

index 0bf90ccde544f1cffc580e0a6b7a603fd50a6c13..56358406bc9f667f951d92779b6cb0b406dab43f 100644 (file)
@@ -568,6 +568,10 @@ pdf</defaultvalue>
                                <admindefaultvalue>0</admindefaultvalue>
                                <minvalue>0</minvalue>
                        </option>
+                       <option name="user.comment.disallowedBBCodes">
+                               <categoryname>user.message.comment</categoryname>
+                               <optiontype>BBCodeSelect</optiontype>
+                       </option>
                        <!-- /user.message.comment -->
                        
                        <!-- user.signature -->
index 5ca29ca3f4d0412ebffb7d4f5368e52a3e45f1ee..fa5b4c472e04c583e750351e2c8d5c09718dc2a7 100644 (file)
@@ -131,7 +131,16 @@ WCF.Comment.Handler = Class.extend({
                
                // add new comment
                if (this._container.data('canAdd')) {
-                       this._initAddComment();
+                       if (elBySel('.commentListAddComment .wysiwygTextarea', this._container[0]) === null) {
+                               console.debug("Missing WYSIWYG implementation, adding comments is not available.");
+                       }
+                       else {
+                               require(['WoltLabSuite/Core/Ui/Comment/Add'], (function (UICommentAdd) {
+                                       new UICommentAdd(elBySel('.jsCommentAdd',  this._container[0]), {
+                                               
+                                       });
+                               }).bind(this));
+                       }
                }
                
                WCF.DOMNodeInsertedHandler.execute();
@@ -328,14 +337,6 @@ WCF.Comment.Handler = Class.extend({
                }
        },
        
-       /**
-        * Initializes the UI components to add a comment.
-        */
-       _initAddComment: function() {
-               var button = elBySel('.jsCommentAdd .formSubmit button[data-type="save"]', this._commentAdd[0]);
-               if (button) button.addEventListener(WCF_CLICK_EVENT, this._save.bind(this));
-       },
-       
        /**
         * Initializes the UI elements to add a response.
         * 
@@ -438,6 +439,10 @@ WCF.Comment.Handler = Class.extend({
         * @param       jQuery          input
         */
        _save: function(event, isResponse, input) {
+               if (!isResponse) {
+                       throw new Error("Adding comments through `_save()` is no longer supported.");
+               }
+               
                var $input = (event === null) ? input : $(event.currentTarget).parent().children('textarea');
                $input.next('small.innerError').remove();
                var $value = $.trim($input.val());
@@ -447,16 +452,12 @@ WCF.Comment.Handler = Class.extend({
                        return;
                }
                
-               var $actionName = 'addComment';
                var $data = {
+                       commentID: $input.data('commentID'),
                        message: $value,
                        objectID: this._container.data('objectID'),
                        objectTypeID: this._container.data('objectTypeID')
                };
-               if (isResponse === true) {
-                       $actionName = 'addResponse';
-                       $data.commentID = $input.data('commentID');
-               }
                
                if (!WCF.User.userID) {
                        this._commentData = $data;
@@ -479,7 +480,7 @@ WCF.Comment.Handler = Class.extend({
                        new WCF.Action.Proxy({
                                autoSend: true,
                                data: {
-                                       actionName: $actionName,
+                                       actionName: 'addResponse',
                                        className: 'wcf\\data\\comment\\CommentAction',
                                        parameters: {
                                                data: $data
@@ -561,20 +562,6 @@ WCF.Comment.Handler = Class.extend({
         */
        _success: function(data, textStatus, jqXHR) {
                switch (data.actionName) {
-                       case 'addComment':
-                               if (data.returnValues.guestDialog) {
-                                       this._createGuestDialog(data.returnValues.guestDialog, data.returnValues.useCaptcha);
-                               }
-                               else {
-                                       this._commentAdd.find('textarea').val('').blur().trigger('updateHeight');
-                                       $(data.returnValues.template).insertAfter(this._commentAdd).wcfFadeIn();
-                                       
-                                       if (!WCF.User.userID) {
-                                               this._guestDialog.wcfDialog('close');
-                                       }
-                               }
-                       break;
-                       
                        case 'addResponse':
                                if (data.returnValues.guestDialog) {
                                        this._createGuestDialog(data.returnValues.guestDialog, data.returnValues.useCaptcha);
@@ -782,7 +769,7 @@ WCF.Comment.Handler = Class.extend({
         */
        _submit: function(event) {
                var $requestData = {
-                       actionName: this._commentData.commentID ? 'addResponse' : 'addComment',
+                       actionName: 'addResponse',
                        className: 'wcf\\data\\comment\\CommentAction'
                };
                
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Comment/Add.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Comment/Add.js
new file mode 100644 (file)
index 0000000..cf6816d
--- /dev/null
@@ -0,0 +1,318 @@
+/**
+ * Handles the comment add feature.
+ * 
+ * @author     Alexander Ebert
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module     WoltLabSuite/Core/Ui/Comment/Add
+ */
+define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Dom/Traverse', 'Ui/Dialog', 'Ui/Notification', 'WoltLabSuite/Core/Ui/Scroll', 'EventKey', 'User', 'WoltLabSuite/Core/Controller/Captcha'],
+       function(Ajax, Core, EventHandler, Language, DomChangeListener, DomUtil, DomTraverse, UiDialog, UiNotification, UiScroll, EventKey, User, ControllerCaptcha) {
+       "use strict";
+       
+       /**
+        * @constructor
+        */
+       function UiCommentAdd(container, options) { this.init(container, options); }
+       UiCommentAdd.prototype = {
+               /**
+                * Initializes a new quick reply field.
+                * 
+                * @param       {Element}       container       container element
+                * @param       {Object}        options         configuration options
+                */
+               init: function(container, options) {
+                       this._options = Core.extend({
+                               ajax: {
+                                       className: ''
+                               },
+                               successMessage: 'wcf.global.success.add'
+                       }, options);
+                       
+                       this._container = container;
+                       this._content = elBySel('.commentListAddComment', this._container);
+                       this._textarea = elBySel('.wysiwygTextarea', this._container);
+                       this._editor = null;
+                       this._loadingOverlay = null;
+                       
+                       // handle submit button
+                       var submitButton = elBySel('button[data-type="save"]', this._container);
+                       submitButton.addEventListener(WCF_CLICK_EVENT, this._submit.bind(this));
+               },
+               
+               /**
+                * Submits the guest dialog.
+                * 
+                * @param       {Event}         event
+                * @protected
+                */
+               _submitGuestDialog: function(event) {
+                       // only submit when enter key is pressed
+                       if (event.type === 'keypress' && !EventKey.Enter(event)) {
+                               return;
+                       }
+                       
+                       var usernameInput = elBySel('input[name=username]', event.currentTarget.closest('.dialogContent'));
+                       if (usernameInput.value === '') {
+                               var error = DomTraverse.nextByClass(usernameInput, 'innerError');
+                               if (!error) {
+                                       error = elCreate('small');
+                                       error.className = 'innerError';
+                                       error.innerText = Language.get('wcf.global.form.error.empty');
+                                       
+                                       DomUtil.insertAfter(error, usernameInput);
+                                       
+                                       usernameInput.closest('dl').classList.add('formError');
+                               }
+                               
+                               return;
+                       }
+                       
+                       var parameters = {
+                               parameters: {
+                                       data: {
+                                               username: usernameInput.value
+                                       }
+                               }
+                       };
+                       
+                       //noinspection JSCheckFunctionSignatures
+                       var captchaId = elData(event.currentTarget, 'captcha-id');
+                       if (ControllerCaptcha.has(captchaId)) {
+                               parameters = Core.extend(parameters, ControllerCaptcha.getData(captchaId));
+                       }
+                       
+                       this._submit(undefined, parameters);
+               },
+               
+               /**
+                * Validates the message and submits it to the server.
+                * 
+                * @param       {Event?}        event                   event object
+                * @param       {Object?}       additionalParameters    additional parameters sent to the server
+                * @protected
+                */
+               _submit: function(event, additionalParameters) {
+                       if (event) {
+                               event.preventDefault();
+                       }
+                       
+                       if (!this._validate()) {
+                               // validation failed, bail out
+                               return;
+                       }
+                       
+                       this._showLoadingOverlay();
+                       
+                       // build parameters
+                       var commentList = this._container.closest('.commentList');
+                       var parameters = {
+                               data: {
+                                       message: this._getEditor().code.get(),
+                                       objectID: elData(commentList, 'object-id'),
+                                       objectTypeID: elData(commentList, 'object-type-id')
+                               }
+                       };
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_text', parameters.data);
+                       
+                       if (!User.userId && !additionalParameters) {
+                               parameters.requireGuestDialog = true;
+                       }
+                       
+                       Ajax.api(this, Core.extend({
+                               parameters: parameters
+                       }, additionalParameters));
+               },
+               
+               /**
+                * Validates the message and invokes listeners to perform additional validation.
+                * 
+                * @return      {boolean}       validation result
+                * @protected
+                */
+               _validate: function() {
+                       // remove all existing error elements
+                       var errorMessages = elByClass('innerError', this._container);
+                       while (errorMessages.length) {
+                               elRemove(errorMessages[0]);
+                       }
+                       
+                       // check if editor contains actual content
+                       if (this._getEditor().utils.isEmpty()) {
+                               this.throwError(this._textarea, Language.get('wcf.global.form.error.empty'));
+                               return false;
+                       }
+                       
+                       var data = {
+                               api: this,
+                               editor: this._getEditor(),
+                               message: this._getEditor().code.get(),
+                               valid: true
+                       };
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'validate_text', data);
+                       
+                       return (data.valid !== false);
+               },
+               
+               /**
+                * Throws an error by adding an inline error to target element.
+                * 
+                * @param       {Element}       element         erroneous element
+                * @param       {string}        message         error message
+                */
+               throwError: function(element, message) {
+                       var error = elCreate('small');
+                       error.className = 'innerError';
+                       error.textContent = (message === 'empty' ? Language.get('wcf.global.form.error.empty') : message);
+                       
+                       DomUtil.insertAfter(error, element);
+               },
+               
+               /**
+                * Displays a loading spinner while the request is processed by the server.
+                * 
+                * @protected
+                */
+               _showLoadingOverlay: function() {
+                       if (this._loadingOverlay === null) {
+                               this._loadingOverlay = elCreate('div');
+                               this._loadingOverlay.className = 'commentLoadingOverlay';
+                               this._loadingOverlay.innerHTML = '<span class="icon icon96 fa-spinner"></span>';
+                       }
+                       
+                       this._content.classList.add('loading');
+                       this._content.appendChild(this._loadingOverlay);
+               },
+               
+               /**
+                * Hides the loading spinner.
+                * 
+                * @protected
+                */
+               _hideLoadingOverlay: function() {
+                       this._content.classList.remove('loading');
+                       
+                       var loadingOverlay = elBySel('.commentLoadingOverlay', this._content);
+                       if (loadingOverlay !== null) {
+                               loadingOverlay.parentNode.removeChild(loadingOverlay);
+                       }
+               },
+               
+               /**
+                * Resets the editor contents and notifies event listeners.
+                * 
+                * @protected
+                */
+               _reset: function() {
+                       this._getEditor().code.set('<p>\u200b</p>');
+                       
+                       EventHandler.fire('com.woltlab.wcf.redactor2', 'reset_text');
+               },
+               
+               /**
+                * Handles errors occured during server processing.
+                * 
+                * @param       {Object}        data    response data
+                * @protected
+                */
+               _handleError: function(data) {
+                       //noinspection JSUnresolvedVariable
+                       this.throwError(this._textarea, data.returnValues.errorType);
+               },
+               
+               /**
+                * Returns the current editor instance.
+                * 
+                * @return      {Object}       editor instance
+                * @protected
+                */
+               _getEditor: function() {
+                       if (this._editor === null) {
+                               if (typeof window.jQuery === 'function') {
+                                       this._editor = window.jQuery(this._textarea).data('redactor');
+                               }
+                               else {
+                                       throw new Error("Unable to access editor, jQuery has not been loaded yet.");
+                               }
+                       }
+                       
+                       return this._editor;
+               },
+               
+               /**
+                * Inserts the rendered message into the post list, unless the post is on the next
+                * page in which case a redirect will be performed instead.
+                * 
+                * @param       {Object}        data    response data
+                * @protected
+                */
+               _insertMessage: function(data) {
+                       // insert HTML
+                       //noinspection JSCheckFunctionSignatures
+                       DomUtil.insertHtml(data.returnValues.template, this._container, 'after');
+                                       
+                       UiNotification.show(Language.get(this._options.successMessage));
+                       
+                       DomChangeListener.trigger();
+               },
+               
+               /**
+                * @param {{returnValues:{guestDialog:string,guestDialogID:string}}} data
+                * @protected
+                */
+               _ajaxSuccess: function(data) {
+                       if (!User.userId && !data.returnValues.guestDialogID) {
+                               throw new Error("Missing 'guestDialogID' return value for guest.");
+                       }
+                       
+                       if (!User.userId && data.returnValues.guestDialog) {
+                               UiDialog.openStatic(data.returnValues.guestDialogID, data.returnValues.guestDialog, {
+                                       closable: false,
+                                       title: Language.get('wcf.global.confirmation.title')
+                               });
+                               
+                               var dialog = UiDialog.getDialog(data.returnValues.guestDialogID);
+                               elBySel('input[type=submit]', dialog.content).addEventListener(WCF_CLICK_EVENT, this._submitGuestDialog.bind(this));
+                               elBySel('input[type=text]', dialog.content).addEventListener('keypress', this._submitGuestDialog.bind(this));
+                       }
+                       else {
+                               this._insertMessage(data);
+                               
+                               if (!User.userId) {
+                                       UiDialog.close(data.returnValues.guestDialogID);
+                               }
+                               
+                               this._reset();
+                               
+                               this._hideLoadingOverlay();
+                       }
+               },
+               
+               _ajaxFailure: function(data) {
+                       this._hideLoadingOverlay();
+                       
+                       //noinspection JSUnresolvedVariable
+                       if (data === null || data.returnValues === undefined || data.returnValues.errorType === undefined) {
+                               return true;
+                       }
+                       
+                       this._handleError(data);
+                       
+                       return false;
+               },
+               
+               _ajaxSetup: function() {
+                       return {
+                               data: {
+                                       actionName: 'addComment',
+                                       className: 'wcf\\data\\comment\\CommentAction'
+                               },
+                               silent: true
+                       };
+               }
+       };
+       
+       return UiCommentAdd;
+});
index ab8b9163c67068086107fcfd64e0a9a35e5e6404..2452116218235f6489362e2cb908fe4aec3a2d49 100644 (file)
@@ -5,6 +5,7 @@ use wcf\data\IMessage;
 use wcf\data\TUserContent;
 use wcf\system\bbcode\SimpleMessageParser;
 use wcf\system\comment\CommentHandler;
+use wcf\system\html\output\HtmlOutputProcessor;
 use wcf\util\StringUtil;
 
 /**
@@ -51,14 +52,30 @@ class Comment extends DatabaseObject implements IMessage {
         * @inheritDoc
         */
        public function getFormattedMessage() {
-               return SimpleMessageParser::getInstance()->parse($this->message);
+               $processor = new HtmlOutputProcessor();
+               $processor->process($this->message, 'com.woltlab.wcf.comment', $this->commentID);
+               
+               return $processor->getHtml();
+       }
+       
+       /**
+        * Returns a simplified version of the formatted message.
+        * 
+        * @return      string
+        */
+       public function getSimplifiedFormattedMessage() {
+               $processor = new HtmlOutputProcessor();
+               $processor->setOutputType('text/simplified-html');
+               $processor->process($this->message, 'com.woltlab.wcf.comment', $this->commentID);
+               
+               return $processor->getHtml();
        }
        
        /**
         * @inheritDoc
         */
        public function getExcerpt($maxLength = 255) {
-               return StringUtil::truncateHTML($this->getFormattedMessage(), $maxLength);
+               return StringUtil::truncateHTML($this->getSimplifiedFormattedMessage(), $maxLength);
        }
        
        /**
index d9c65eab36b25a219c584b7ed548062c0fa8ca7b..afc33d0940f05356d7475db319d9885ffa568100 100644 (file)
@@ -8,12 +8,14 @@ use wcf\data\comment\response\StructuredCommentResponse;
 use wcf\data\object\type\ObjectType;
 use wcf\data\object\type\ObjectTypeCache;
 use wcf\data\AbstractDatabaseObjectAction;
+use wcf\system\bbcode\BBCodeHandler;
 use wcf\system\captcha\CaptchaHandler;
 use wcf\system\comment\manager\ICommentManager;
 use wcf\system\comment\CommentHandler;
 use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\SystemException;
 use wcf\system\exception\UserInputException;
+use wcf\system\html\input\HtmlInputProcessor;
 use wcf\system\like\LikeHandler;
 use wcf\system\user\activity\event\UserActivityEventHandler;
 use wcf\system\user\notification\object\type\ICommentUserNotificationObjectType;
@@ -66,6 +68,11 @@ class CommentAction extends AbstractDatabaseObjectAction {
         */
        protected $commentProcessor = null;
        
+       /**
+        * @var HtmlInputProcessor
+        */
+       protected $htmlInputProcessor;
+       
        /**
         * response object
         * @var CommentResponse
@@ -203,7 +210,7 @@ class CommentAction extends AbstractDatabaseObjectAction {
                $this->validateUsername();
                $this->validateCaptcha();
                
-               $this->validateMessage();
+               $this->validateMessage(true);
                $objectType = $this->validateObjectType();
                
                // validate object id and permissions
@@ -232,6 +239,9 @@ class CommentAction extends AbstractDatabaseObjectAction {
                        ];
                }
                
+               /** @var HtmlInputProcessor $htmlInputProcessor */
+               $htmlInputProcessor = $this->parameters['htmlInputProcessor'];
+               
                // create comment
                $this->createdComment = CommentEditor::create([
                        'objectTypeID' => $this->parameters['data']['objectTypeID'],
@@ -239,7 +249,7 @@ class CommentAction extends AbstractDatabaseObjectAction {
                        'time' => TIME_NOW,
                        'userID' => WCF::getUser()->userID ?: null,
                        'username' => WCF::getUser()->userID ? WCF::getUser()->username : $this->parameters['data']['username'],
-                       'message' => $this->parameters['data']['message'],
+                       'message' => $htmlInputProcessor->getHtml(),
                        'responses' => 0,
                        'responseIDs' => serialize([])
                ]);
@@ -533,7 +543,7 @@ class CommentAction extends AbstractDatabaseObjectAction {
        public function validateEdit() {
                $this->validatePrepareEdit();
                
-               $this->validateMessage();
+               $this->validateMessage($this->comment !== null);
        }
        
        /**
@@ -717,7 +727,7 @@ class CommentAction extends AbstractDatabaseObjectAction {
        /**
         * Validates message parameter.
         */
-       protected function validateMessage() {
+       protected function validateMessage($isComment = false) {
                $this->readString('message', false, 'data');
                $this->parameters['data']['message'] = MessageUtil::stripCrap($this->parameters['data']['message']);
                
@@ -726,6 +736,26 @@ class CommentAction extends AbstractDatabaseObjectAction {
                }
                
                CommentHandler::enforceCensorship($this->parameters['data']['message']);
+               
+               if ($isComment) {
+                       $this->setDisallowedBBCodes();
+                       $htmlInputProcessor = $this->getHtmlInputProcessor($this->parameters['data']['message']);
+                       
+                       // search for disallowed bbcodes
+                       $disallowedBBCodes = $htmlInputProcessor->validate();
+                       if (!empty($disallowedBBCodes)) {
+                               throw new UserInputException('text', WCF::getLanguage()->getDynamicVariable('wcf.message.error.disallowedBBCodes', ['disallowedBBCodes' => $disallowedBBCodes]));
+                       }
+                       
+                       if ($htmlInputProcessor->appearsToBeEmpty()) {
+                               throw new UserInputException('message');
+                       }
+                       
+                       $this->parameters['htmlInputProcessor'] = $htmlInputProcessor;
+               }
+               else {
+                       unset($this->parameters['htmlInputProcessor']);
+               }
        }
        
        /**
@@ -818,6 +848,24 @@ class CommentAction extends AbstractDatabaseObjectAction {
                }
        }
        
+       /**
+        * Sets the list of disallowed bbcodes for comments.
+        */
+       protected function setDisallowedBBCodes() {
+               BBCodeHandler::getInstance()->setDisallowedBBCodes(explode(',', WCF::getSession()->getPermission('user.comment.disallowedBBCodes')));
+       }
+       
+       public function getHtmlInputProcessor($message = null, $objectID = 0) {
+               if ($message === null) {
+                       return $this->htmlInputProcessor;
+               }
+               
+               $this->htmlInputProcessor = new HtmlInputProcessor();
+               $this->htmlInputProcessor->process($message, 'com.woltlab.wcf.comment', $objectID);
+               
+               return $this->htmlInputProcessor;
+       }
+       
        /**
         * Returns the comment object.
         *