From 1ea2604169a41e6aa0831bcccc84e52edb5ad7c0 Mon Sep 17 00:00:00 2001 From: Matthias Schmidt Date: Sun, 24 Jul 2016 09:52:50 +0200 Subject: [PATCH] Add guest dialog support for quick replies --- .../templates/headIncludeJavaScript.tpl | 4 +- .../messageQuickReplyGuestDialog.tpl | 23 +++ .../install/files/acp/templates/header.tpl | 140 +++++++++--------- wcfsetup/install/files/js/WCF.js | 51 ++++--- .../js/WoltLab/WCF/Controller/Captcha.js | 73 +++++++++ .../install/files/js/WoltLab/WCF/Ui/Dialog.js | 4 +- .../files/js/WoltLab/WCF/Ui/Message/Reply.js | 92 ++++++++++-- wcfsetup/install/files/js/WoltLab/WCF/User.js | 42 ++++++ wcfsetup/install/files/js/require.config.js | 3 +- ...ssageQuickReplyGuestDialogAction.class.php | 115 ++++++++++++++ .../message/QuickReplyManager.class.php | 10 +- 11 files changed, 454 insertions(+), 103 deletions(-) create mode 100644 com.woltlab.wcf/templates/messageQuickReplyGuestDialog.tpl create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/Controller/Captcha.js create mode 100644 wcfsetup/install/files/js/WoltLab/WCF/User.js create mode 100644 wcfsetup/install/files/lib/data/TMessageQuickReplyGuestDialogAction.class.php diff --git a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl index f149194801..dbe61852bc 100644 --- a/com.woltlab.wcf/templates/headIncludeJavaScript.tpl +++ b/com.woltlab.wcf/templates/headIncludeJavaScript.tpl @@ -22,7 +22,7 @@ requirejs.config({ }); {js application='wcf' lib='jquery'} diff --git a/wcfsetup/install/files/js/WCF.js b/wcfsetup/install/files/js/WCF.js index bf19636a53..201cb6afef 100755 --- a/wcfsetup/install/files/js/WCF.js +++ b/wcfsetup/install/files/js/WCF.js @@ -3517,6 +3517,8 @@ WCF.Collapsible.SimpleRemote = WCF.Collapsible.Remote.extend({ /** * Holds userdata of the current user + * + * @deprecated use WCF/WoltLab/User */ WCF.User = { /** @@ -4964,12 +4966,6 @@ WCF.System.ObjectStore = { * Stores captcha callbacks used for captchas in AJAX contexts. */ WCF.System.Captcha = { - /** - * adds call - * @var object - */ - _captchas: { }, - /** * Adds a callback for a certain captcha. * @@ -4977,12 +4973,19 @@ WCF.System.Captcha = { * @param function callback */ addCallback: function(captchaID, callback) { - if (!$.isFunction(callback)) { - console.debug('[WCF.System.Captcha] Given callback is no function'); - return; - } - - this._captchas[captchaID] = callback; + require(['WoltLab/WCF/Controller/Captcha'], function(ControllerCaptcha) { + try { + ControllerCaptcha.add(captchaID, callback); + } + catch (e) { + if (e instanceof TypeError) { + console.debug('[WCF.System.Captcha] Given callback is no function'); + return; + } + + // ignore other errors + } + }); }, /** @@ -4991,19 +4994,31 @@ WCF.System.Captcha = { * @return object */ getData: function(captchaID) { - if (this._captchas[captchaID] === undefined) { - console.debug('[WCF.System.Captcha] Unknow captcha id "' + captchaID + '"'); - return; - } + var returnValue; + require(['WoltLab/WCF/Controller/Captcha'], function(ControllerCaptcha) { + try { + returnValue = ControllerCaptcha.getData(captchaID); + } + catch (e) { + console.debug('[WCF.System.Captcha] Unknow captcha id "' + captchaID + '"'); + } + }); - return this._captchas[captchaID](); + return returnValue; }, /** * Removes the callback with the given captcha id. */ removeCallback: function(captchaID) { - delete this._captchas[captchaID]; + require(['WoltLab/WCF/Controller/Captcha'], function(ControllerCaptcha) { + try { + ControllerCaptcha.delete(captchaID); + } + catch (e) { + // ignore errors for unknown captchas + } + }); } }; diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Controller/Captcha.js b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Captcha.js new file mode 100644 index 0000000000..252c698898 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/Controller/Captcha.js @@ -0,0 +1,73 @@ +/** + * Provides data of the active user. + * + * @author Matthias Schmidt + * @copyright 2001-2016 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLab/WCF/Controller/Captcha + */ +define(['Dictionary'], function(Dictionary) { + "use strict"; + + var _captchas = new Dictionary(); + + /** + * @exports WoltLab/WCF/Controller/Captcha + */ + return { + /** + * Registers a captcha with the given identifier and callback used to get captcha data. + * + * @param {string} captchaId captcha identifier + * @param {function} callback callback to get captcha data + */ + add: function(captchaId, callback) { + if (_captchas.has(captchaId)) { + throw new Error("Captcha with id '" + captchaId + "' is already registered.") + } + + if (typeof callback !== 'function') { + throw new TypeError("Expected a valid callback for parameter 'callback'."); + } + + _captchas.set(captchaId, callback); + }, + + /** + * Deletes the captcha with the given identifier. + * + * @param {string} captchaId identifier of the captcha to be deleted + */ + 'delete': function(captchaId) { + if (!_captchas.has(captchaId)) { + throw new Error("Unknown captcha with id '" + captchaId + "'.") + } + + _captchas.delete(captchaId)(); + }, + + /** + * Returns true if a captcha with the given identifier exists. + * + * @param {string} captchaId captcha identifier + * @return {boolean} + */ + has: function(captchaId) { + return _captchas.has(captchaId); + }, + + /** + * Returns the data of the captcha with the given identifier. + * + * @param {string} captchaId captcha identifier + * @return {Object} captcha data + */ + getData: function(captchaId) { + if (!_captchas.has(captchaId)) { + throw new Error("Unknown captcha with id '" + captchaId + "'.") + } + + return _captchas.get(captchaId)(); + } + }; +}); diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js index e2e077592d..01dad2b0d3 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Dialog.js @@ -383,8 +383,6 @@ define( elData(_container, 'close-on-click', (data.backdropCloseOnClick ? 'true' : 'false')); _activeDialog = id; - this.rebuild(id); - // set focus on first applicable element var focusElement = elBySel('.jsDialogAutoFocus', data.dialog); if (focusElement !== null && focusElement.offsetParent !== null) { @@ -396,6 +394,8 @@ define( } } + this.rebuild(id); + DomChangeListener.trigger(); }, diff --git a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js index 84934df5cf..72cae533a0 100644 --- a/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js +++ b/wcfsetup/install/files/js/WoltLab/WCF/Ui/Message/Reply.js @@ -6,7 +6,8 @@ * @license GNU Lesser General Public License * @module WoltLab/WCF/Ui/Message/Reply */ -define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Notification', '../Scroll'], function(Ajax, Core, EventHandler, Language, DomChangeListener, DomUtil, UiNotification, UiScroll) { +define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/Util', 'Dom/Traverse', 'Ui/Dialog', 'Ui/Notification', '../Scroll', 'EventKey', 'User', 'WoltLab/WCF/Controller/Captcha'], + function(Ajax, Core, EventHandler, Language, DomChangeListener, DomUtil, DomTraverse, UiDialog, UiNotification, UiScroll, EventKey, User, ControllerCaptcha) { "use strict"; /** @@ -57,14 +58,61 @@ define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/U // TODO: add event listener for submit through keyboard in Redactor }, + /** + * 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) { + var 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 + } + } + }; + + 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 {Event?} event event object + * @param {Object?} additionalParameters additional parameters sent to the server * @protected */ - _submit: function(event) { - event.preventDefault(); + _submit: function(event, additionalParameters) { + if (event) { + event.preventDefault(); + } if (!this._validate()) { // validation failed, bail out @@ -80,9 +128,13 @@ define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/U EventHandler.fire('com.woltlab.wcf.redactor2', 'submit_text', parameters.data); - Ajax.api(this, { + if (!User.userId && !additionalParameters) { + parameters.requireGuestDialog = true; + } + + Ajax.api(this, Core.extend({ parameters: parameters - }); + }, additionalParameters)); }, /** @@ -255,11 +307,31 @@ define(['Ajax', 'Core', 'EventHandler', 'Language', 'Dom/ChangeListener', 'Dom/U }, _ajaxSuccess: function(data) { - this._insertMessage(data); - - this._reset(); + if (!User.userId && !data.returnValues.guestDialogID) { + throw new Error("Missing 'guestDialogID' return value for guest."); + } - this._hideLoadingOverlay(); + 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) { diff --git a/wcfsetup/install/files/js/WoltLab/WCF/User.js b/wcfsetup/install/files/js/WoltLab/WCF/User.js new file mode 100644 index 0000000000..17f16626a1 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLab/WCF/User.js @@ -0,0 +1,42 @@ +/** + * Provides data of the active user. + * + * @author Matthias Schmidt + * @copyright 2001-2016 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLab/WCF/User + */ +define([], function() { + "use strict"; + + var _didInit = false; + + /** + * @exports WoltLab/WCF/User + */ + return { + /** + * Initializes the user object. + * + * @param {int} userId id of the user, `0` for guests + * @param {string} username name of the user, empty for guests + */ + init: function(userId, username) { + if (_didInit) { + throw new Error('User has already been initialized.'); + } + + // define non-writeable properties for userId and username + Object.defineProperty(this, 'userId', { + value: userId, + writable: false + }); + Object.defineProperty(this, 'username', { + value: username, + writable: false + }); + + _didInit = true; + } + }; +}); diff --git a/wcfsetup/install/files/js/require.config.js b/wcfsetup/install/files/js/require.config.js index 88110584a9..56b0daba34 100644 --- a/wcfsetup/install/files/js/require.config.js +++ b/wcfsetup/install/files/js/require.config.js @@ -38,7 +38,8 @@ requirejs.config({ 'Ui/Screen': 'WoltLab/WCF/Ui/Screen', 'Ui/SimpleDropdown': 'WoltLab/WCF/Ui/Dropdown/Simple', 'Ui/TabMenu': 'WoltLab/WCF/Ui/TabMenu', - 'Upload': 'WoltLab/WCF/Upload' + 'Upload': 'WoltLab/WCF/Upload', + 'User': 'WoltLab/WCF/User' } } }); diff --git a/wcfsetup/install/files/lib/data/TMessageQuickReplyGuestDialogAction.class.php b/wcfsetup/install/files/lib/data/TMessageQuickReplyGuestDialogAction.class.php new file mode 100644 index 0000000000..fdac4730d7 --- /dev/null +++ b/wcfsetup/install/files/lib/data/TMessageQuickReplyGuestDialogAction.class.php @@ -0,0 +1,115 @@ + + * @package WoltLabSuite\Core\Data + * @since 3.0 + */ +trait TMessageQuickReplyGuestDialogAction { + /** + * captcha object type used for guests or `null` if no captcha is used or available + * @var ObjectType + */ + public $guestDialogCaptchaObjectType; + + /** + * list of errors in the guest dialog with field as key and error type as value + * @var string[] + */ + public $guestDialogErrors = []; + + /** + * @see AbstractDatabaseObjectAction::$parameters + */ + protected $parameters = []; + + /** + * @see AbstractDatabaseObjectAction::readString() + */ + abstract protected function readString($variableName, $allowEmpty = false, $arrayIndex = ''); + + /** + * Sets the guest dialog captcha. + * + * Needs to be called before all other methods in this trait. + */ + protected function setGuestDialogCaptcha() { + $this->guestDialogCaptchaObjectType = CaptchaHandler::getInstance()->getObjectTypeByName(CAPTCHA_TYPE); + if ($this->guestDialogCaptchaObjectType === null) { + throw new \LogicException("Unknown captcha object type with name '".CAPTCHA_TYPE."'"); + } + + /** @var ICaptchaHandler $processor */ + $processor = $this->guestDialogCaptchaObjectType->getProcessor(); + + if (!$processor->isAvailable()) { + $this->guestDialogCaptchaObjectType = null; + } + } + + /** + * Validates the captcha in the guest dialog. + * + * @throws \BadMethodCallException + * @throws \LogicException + */ + protected function validateGuestDialogCaptcha() { + // only relevant for guests + if (WCF::getUser()->userID) { + throw new \BadMethodCallException("Guest dialogs are only relevant for guests"); + } + + if (CAPTCHA_TYPE) { + /** @var ICaptchaHandler $processor */ + $processor = $this->guestDialogCaptchaObjectType->getProcessor(); + + if ($processor->isAvailable()) { + try { + $processor->readFormParameters(); + $processor->validate(); + } + catch (UserInputException $e) { + $this->guestDialogErrors[$e->getField()] = $e->getType(); + } + } + } + } + + /** + * Validates the entered username in the guest dialog. + * + * @return string type of the validation error or empty if no error occured + * @throws \BadMethodCallException + */ + protected function validateGuestDialogUsername() { + // only relevant for guests + if (WCF::getUser()->userID) { + throw new \BadMethodCallException("Guest dialogs are only relevant for guests"); + } + + try { + $this->readString('username', false, 'data'); + + if (!UserUtil::isValidUsername($this->parameters['data']['username'])) { + throw new UserInputException('username', 'notValid'); + } + if (!UserUtil::isAvailableUsername($this->parameters['data']['username'])) { + throw new UserInputException('username', 'notUnique'); + } + } + catch (UserInputException $e) { + $this->guestDialogErrors['username'] = $e->getType(); + } + } +} diff --git a/wcfsetup/install/files/lib/system/message/QuickReplyManager.class.php b/wcfsetup/install/files/lib/system/message/QuickReplyManager.class.php index 59c98fbae4..96e1ea3ad0 100644 --- a/wcfsetup/install/files/lib/system/message/QuickReplyManager.class.php +++ b/wcfsetup/install/files/lib/system/message/QuickReplyManager.class.php @@ -179,8 +179,14 @@ class QuickReplyManager extends SingletonFactory { $tableIndexName = call_user_func([$this->container, 'getDatabaseTableIndexName']); $parameters['data'][$tableIndexName] = $parameters['objectID']; $parameters['data']['time'] = TIME_NOW; - $parameters['data']['userID'] = WCF::getUser()->userID ?: null; - $parameters['data']['username'] = WCF::getUser()->username; + + if (!isset($parameters['data']['userID'])) { + $parameters['data']['userID'] = WCF::getUser()->userID ?: null; + } + + if (!isset($parameters['data']['username'])) { + $parameters['data']['username'] = WCF::getUser()->username; + } // pre-parse message text /*if ($parameters['data']['preParse']) { -- 2.20.1