Allow guests to use the comment system
authorMatthias Schmidt <gravatronics@live.com>
Thu, 27 Mar 2014 15:16:14 +0000 (16:16 +0100)
committerMatthias Schmidt <gravatronics@live.com>
Thu, 27 Mar 2014 15:16:14 +0000 (16:16 +0100)
13 files changed:
com.woltlab.wcf/templates/__commentJavaScript.tpl
com.woltlab.wcf/templates/commentAddGuestDialog.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/recaptcha.tpl
com.woltlab.wcf/userOption.xml
wcfsetup/install/files/js/WCF.Comment.js
wcfsetup/install/files/lib/data/comment/CommentAction.class.php
wcfsetup/install/files/lib/data/comment/StructuredCommentList.class.php
wcfsetup/install/files/lib/system/WCF.class.php
wcfsetup/install/files/lib/system/comment/CommentHandler.class.php
wcfsetup/install/files/lib/system/user/notification/event/UserProfileCommentResponseOwnerUserNotificationEvent.class.php
wcfsetup/install/files/lib/system/user/notification/event/UserProfileCommentResponseUserNotificationEvent.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 307ea7b42b24f2fd77cd6301a40b42959685e1a9..55f3a12fd7d7612e59709df6f4b04eb714d5c120 100644 (file)
@@ -1,5 +1,8 @@
 <script data-relocate="true" src="{@$__wcf->getPath()}js/WCF.Comment{if !ENABLE_DEBUG_MODE}.min{/if}.js?v={@$__wcfVersion}"></script>
 <script data-relocate="true" src="{@$__wcf->getPath()}js/WCF.Moderation{if !ENABLE_DEBUG_MODE}.min{/if}.js?v={@$__wcfVersion}"></script>
+{if !$__wcf->user->userID}
+       <script type="text/javascript" src="http{if $__wcf->secureConnection()}s{/if}://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
+{/if}
 <script data-relocate="true">
        //<![CDATA[
        $(function() {
diff --git a/com.woltlab.wcf/templates/commentAddGuestDialog.tpl b/com.woltlab.wcf/templates/commentAddGuestDialog.tpl
new file mode 100644 (file)
index 0000000..f0a4465
--- /dev/null
@@ -0,0 +1,18 @@
+<div>
+       <fieldset>
+               <dl>
+                       <dt><label for="username">{lang}wcf.user.username{/lang}</label></dt>
+                       <dd>
+                               <input type="text" id="username" name="username" value="{$username}" required="required" class="long" autofocus="true" />
+                       </dd>
+               </dl>
+       </fieldset>
+       
+       {if MODULE_SYSTEM_RECAPTCHA}
+               {include file='recaptcha'}
+       {/if}
+</div>
+
+<div class="formSubmit">
+       <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
+</div>
index 22ef413416163aa9f2ca77646254d5472755a71d..eff2b03eac44969b8f689f01f10bb2fab6f3e51d 100644 (file)
@@ -2,22 +2,24 @@
        <legend><label for="recaptcha_response_field">{lang}wcf.recaptcha.title{/lang}</label></legend>
        <small>{lang}wcf.recaptcha.description{/lang}</small>
        
-       <dl class="wide reCaptcha{if $errorField == 'recaptchaString'} formError{/if}">
-               <script data-relocate="true">
-                       //<![CDATA[
-                       var RecaptchaOptions = {
-                               lang: '{@$recaptchaLanguageCode}',
-                               theme : 'custom'
-                       }
-                       //]]>
-               </script>
+       <dl class="wide reCaptcha{if $errorField|isset && $errorField == 'recaptchaString'} formError{/if}">
+               {if !$ajaxRecaptcha|isset || !$ajaxRecaptcha}
+                       <script data-relocate="true">
+                               //<![CDATA[
+                               var RecaptchaOptions = {
+                                       lang: '{@$recaptchaLanguageCode}',
+                                       theme : 'custom'
+                               }
+                               //]]>
+                       </script>
+               {/if}
                <dt class="jsOnly">
                        <label for="recaptcha_response_field">reCAPTCHA</label>
                </dt>
                <dd class="jsOnly">
                        <div id="recaptcha_image" class="framed"></div>
                        <input type="text" id="recaptcha_response_field" name="recaptcha_response_field" class="medium marginTop" />
-                       {if $errorField == 'recaptchaString'}
+                       {if $errorField|isset && $errorField == 'recaptchaString'}
                                <small class="innerError">
                                        {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if}
                                        {if $errorType == 'false'}{lang}wcf.recaptcha.error.recaptchaString.false{/lang}{/if}
                        </ul>
                </dd>
                
-               <script data-relocate="true" src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/challenge?k={$recaptchaPublicKey}"></script>
-               <noscript>
-                       <dd>
-                               <iframe src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/noscript?k={$recaptchaPublicKey}" height="300" width="500" seamless="seamless"></iframe><br />
-                               <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
-                               <input type="hidden" name="recaptcha_response_field" value="manual_challenge" />
-                       </dd>
-                       {if $errorField == 'recaptchaString'}
-                               <small class="innerError">
-                                       {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if}
-                                       {if $errorType == 'false'}{lang}wcf.recaptcha.error.recaptchaString.false{/lang}{/if}
-                               </small>
-                       {/if}
-               </noscript>
+               {if !$ajaxRecaptcha|isset || !$ajaxRecaptcha}
+                       <script data-relocate="true" src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/challenge?k={$recaptchaPublicKey}"></script>
+                       <noscript>
+                               <dd>
+                                       <iframe src="http{if $recaptchaUseSSL}s{/if}://www.google.com/recaptcha/api/noscript?k={$recaptchaPublicKey}" height="300" width="500" seamless="seamless"></iframe><br />
+                                       <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
+                                       <input type="hidden" name="recaptcha_response_field" value="manual_challenge" />
+                               </dd>
+                               {if $errorField == 'recaptchaString'}
+                                       <small class="innerError">
+                                               {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if}
+                                               {if $errorType == 'false'}{lang}wcf.recaptcha.error.recaptchaString.false{/lang}{/if}
+                                       </small>
+                               {/if}
+                       </noscript>
+               {else}
+                       <script data-relocate="true">
+                               //<![CDATA[
+                               Recaptcha.create("{$recaptchaPublicKey}", "recaptcha_image", {
+                                       lang: '{@$recaptchaLanguageCode}',
+                                       theme : 'custom'
+                               });
+                               //]]>
+                       </script>
+               {/if}
        </dl>
 </fieldset>
index 683bd4321f5910b2d5b30035c9a50b77be13ca80..08a21e0feb04d8676edf5f947b86c46560a263ec 100644 (file)
                                <categoryname>settings.privacy.messaging</categoryname>
                                <optiontype>select</optiontype>
                                <editable>3</editable>
-                               <selectoptions><![CDATA[1:wcf.user.access.registered
+                               <selectoptions><![CDATA[0:wcf.user.access.everyone
+1:wcf.user.access.registered
 2:wcf.user.access.following
 3:wcf.user.access.nobody]]></selectoptions>
                                <defaultvalue>1</defaultvalue>
index 6a70cbfd42819dd8275c76be89e775e6c4962682..60aafbaa3cd0d7354edcb6f39692206ac26e35aa 100644 (file)
@@ -1,7 +1,7 @@
 /**
  * Namespace for comments
  */
-WCF.Comment = {};
+WCF.Comment = { };
 
 /**
  * Comment support for WCF
@@ -77,6 +77,24 @@ WCF.Comment.Handler = Class.extend({
         */
        _userAvatar: '',
        
+       /**
+        * data of the comment the active guest user is about to create
+        * @var object
+        */
+       _commentData: { },
+       
+       /**
+        * guest dialog with username input field and recaptcha
+        * @var jQuery
+        */
+       _guestDialog: null,
+
+       /**
+        * true if the guest has to solve a recaptcha challenge to save the comment
+        * @var boolean
+        */
+       _useRecaptcha: true,
+       
        /**
         * Initializes the WCF.Comment.Handler class.
         * 
@@ -100,6 +118,7 @@ WCF.Comment.Handler = Class.extend({
                }
                
                this._proxy = new WCF.Action.Proxy({
+                       failure: $.proxy(this._failure, this),
                        success: $.proxy(this._success, this)
                });
                
@@ -437,17 +456,45 @@ WCF.Comment.Handler = Class.extend({
                        $data.commentID = $input.data('commentID');
                }
                
-               this._proxy.setOption('data', {
-                       actionName: $actionName,
-                       className: 'wcf\\data\\comment\\CommentAction',
-                       parameters: {
-                               data: $data
+               if (!WCF.User.userID) {
+                       this._commentData = $data;
+                       
+                       // check if guest dialog has already been loaded
+                       if (this._guestDialog === null) {
+                               this._proxy.setOption('data', {
+                                       actionName: 'getGuestDialog',
+                                       className: 'wcf\\data\\comment\\CommentAction',
+                                       parameters: {
+                                               data: {
+                                                       message: $value,
+                                                       objectID: this._container.data('objectID'),
+                                                       objectTypeID: this._container.data('objectTypeID')
+                                               }
+                                       }
+                               });
+                               this._proxy.sendRequest();
                        }
-               });
-               this._proxy.sendRequest();
-               
-               // reset input
-               //$input.val('').blur();
+                       else {
+                               // request a new recaptcha
+                               if (this._useRecaptcha) {
+                                       Recaptcha.reload();
+                               }
+                               
+                               this._guestDialog.find('input[type="submit"]').enable();
+                               
+                               this._guestDialog.wcfDialog('open');
+                       }
+               }
+               else {
+                       this._proxy.setOption('data', {
+                               actionName: $actionName,
+                               className: 'wcf\\data\\comment\\CommentAction',
+                               parameters: {
+                                       data: $data
+                               }
+                       });
+                       this._proxy.sendRequest();
+               }
        },
        
        /**
@@ -482,6 +529,24 @@ WCF.Comment.Handler = Class.extend({
                }, this));
        },
        
+       /**
+        * Handles a failed AJAX request.
+        * 
+        * @param       object          data
+        * @param       object          jqXHR
+        * @param       string          textStatus
+        * @param       string          errorThrown
+        * @return      boolean
+        */
+       _failure: function(data, jqXHR, textStatus, errorThrown) {
+               if (!WCF.User.userID && this._guestDialog) {
+                       // enable submit button again
+                       this._guestDialog.find('input[type="submit"]').enable();
+               }
+               
+               return true;
+       },
+       
        /**
         * Handles successful AJAX requests.
         * 
@@ -492,17 +557,35 @@ WCF.Comment.Handler = Class.extend({
        _success: function(data, textStatus, jqXHR) {
                switch (data.actionName) {
                        case 'addComment':
-                               this._commentAdd.find('input').val('').blur();
-                               $(data.returnValues.template).insertAfter(this._commentAdd).wcfFadeIn();
+                               if (data.returnValues.errors) {
+                                       this._handleGuestDialogErrors(data.returnValues.errors);
+                               }
+                               else {
+                                       this._commentAdd.find('input').val('').blur();
+                                       $(data.returnValues.template).insertAfter(this._commentAdd).wcfFadeIn();
+                                       
+                                       if (!WCF.User.userID) {
+                                               this._guestDialog.wcfDialog('close');
+                                       }
+                               }
                        break;
                        
                        case 'addResponse':
-                               var $comment = this._comments[data.returnValues.commentID];
-                               $comment.find('.jsCommentResponseAdd input').val('').blur();
+                               if (data.returnValues.errors) {
+                                       this._handleGuestDialogErrors(data.returnValues.errors);
+                               }
+                               else {
+                                       var $comment = this._comments[data.returnValues.commentID];
+                                       $comment.find('.jsCommentResponseAdd input').val('').blur();
+                                       
+                                       var $responseList = $comment.find('ul.commentResponseList');
+                                       if (!$responseList.length) $responseList = $('<ul class="commentResponseList" />').insertBefore($comment.find('.commentOptionContainer'));
+                                       $(data.returnValues.template).appendTo($responseList).wcfFadeIn();
+                               }
                                
-                               var $responseList = $comment.find('ul.commentResponseList');
-                               if (!$responseList.length) $responseList = $('<ul class="commentResponseList" />').insertBefore($comment.find('.commentOptionContainer'));
-                               $(data.returnValues.template).appendTo($responseList).wcfFadeIn();
+                               if (!WCF.User.userID) {
+                                       this._guestDialog.wcfDialog('close');
+                               }
                        break;
                        
                        case 'edit':
@@ -524,6 +607,10 @@ WCF.Comment.Handler = Class.extend({
                        case 'remove':
                                this._remove(data);
                        break;
+                       
+                       case 'getGuestDialog':
+                               this._createGuestDialog(data);
+                       break;
                }
                
                WCF.DOMNodeInsertedHandler.execute();
@@ -628,6 +715,146 @@ WCF.Comment.Handler = Class.extend({
                this._cancelEdit($input);
        },
        
+       /**
+        * Creates the guest dialog based on the given return data from the AJAX
+        * request.
+        * 
+        * @param       object          data
+        */
+       _createGuestDialog: function(data) {
+               this._guestDialog = $('<div id="commentAddGuestDialog" />').append(data.returnValues.template).hide().appendTo(document.body);
+               
+               // bind submit event listeners
+               this._guestDialog.find('input[type="submit"]').click($.proxy(this._submit, this));
+
+               this._guestDialog.find('input[type="text"]').keydown($.proxy(this._keyDown, this));
+
+               // check if recaptcha is used
+               this._useRecaptcha = this._guestDialog.find('dl.reCaptcha').length > 0;
+               
+               this._guestDialog.wcfDialog({
+                       'title': WCF.Language.get('wcf.comment.guestDialog.title')
+               });
+       },
+
+       /**
+        * Handles clicking enter in the input fields of the guest dialog by
+        * submitting it.
+        * 
+        * @param       Event           event
+        */
+       _keyDown: function(event) {
+               if (event.which === $.ui.keyCode.ENTER) {
+                       this._submit();
+               }
+       },
+
+       /**
+        * Handles errors during creation of a comment or response due to the input
+        * in the guest dialog.
+        * 
+        * @param       object          errors
+        */
+       _handleGuestDialogErrors: function(errors) {
+               if (errors.username) {
+                       var $usernameInput = this._guestDialog.find('input[name="username"]');
+                       var $errorMessage = $usernameInput.next('.innerError');
+                       if (!$errorMessage.length) {
+                               $errorMessage = $('<small class="innerError" />').text(errors.username).insertAfter($usernameInput);
+                       }
+                       else {
+                               $errorMessage.text(errors.username).show();
+                       }
+               }
+               
+               if (errors.recaptcha) {
+                       Recaptcha.reload();
+
+                       var $recaptchaInput = this._guestDialog.find('input[name="recaptcha_response_field"]');
+                       var $errorMessage = $recaptchaInput.next('.innerError');
+                       if (!$errorMessage.length) {
+                               $errorMessage = $('<small class="innerError" />').text(errors.recaptcha).insertAfter($recaptchaInput);
+                       }
+                       else {
+                               $errorMessage.text(errors.recaptcha).show();
+                       }
+               }
+
+               this._guestDialog.find('input[type="submit"]').enable();
+       },
+       
+       /**
+        * Handles submitting the guest dialog.
+        * 
+        * @param       Event           event
+        */
+       _submit: function(event) {
+               var $submit = true;
+
+               this._guestDialog.find('input[type="submit"]').enable();
+
+               // validate username
+               var $usernameInput = this._guestDialog.find('input[name="username"]');
+               var $username = $usernameInput.val();
+               var $usernameErrorMessage = $usernameInput.next('.innerError');
+               if (!$username) {
+                       $submit = false;
+                       if (!$usernameErrorMessage.length) {
+                               $usernameErrorMessage = $('<small class="innerError" />').text(WCF.Language.get('wcf.global.form.error.empty')).insertAfter($usernameInput);
+                       }
+                       else {
+                               $usernameErrorMessage.text(WCF.Language.get('wcf.global.form.error.empty')).show();
+                       }
+               }
+
+               // validate recaptcha
+               if (this._useRecaptcha) {
+                       var $recaptchaInput = this._guestDialog.find('input[name="recaptcha_response_field"]');
+                       var $recaptchaResponse = $recaptchaInput.val();
+                       var $recaptchaErrorMessage = $recaptchaInput.next('.innerError');
+                       if (!$recaptchaResponse) {
+                               $submit = false;
+                               if (!$recaptchaErrorMessage.length) {
+                                       $recaptchaErrorMessage = $('<small class="innerError" />').text(WCF.Language.get('wcf.global.form.error.empty')).insertAfter($recaptchaInput);
+                               }
+                               else {
+                                       $recaptchaErrorMessage.text(WCF.Language.get('wcf.global.form.error.empty')).show();
+                               }
+                       }
+               }
+
+               if ($submit) {
+                       if ($usernameErrorMessage.length) {
+                               $usernameErrorMessage.hide();
+                       }
+
+                       if (this._useRecaptcha && $recaptchaErrorMessage.length) {
+                               $recaptchaErrorMessage.hide();
+                       }
+
+                       var $data = this._commentData;
+                       $data.username = $username;
+
+                       var $parameters = {
+                               data: $data
+                       };
+
+                       if (this._useRecaptcha) {
+                               $parameters.recaptchaChallenge = Recaptcha.get_challenge();
+                               $parameters.recaptchaResponse = Recaptcha.get_response();
+                       }
+                       
+                       this._proxy.setOption('data', {
+                               actionName: this._commentData.commentID ? 'addResponse' : 'addComment',
+                               className: 'wcf\\data\\comment\\CommentAction',
+                               parameters: $parameters
+                       });
+                       this._proxy.sendRequest();
+
+                       this._guestDialog.find('input[type="submit"]').disable();
+               }
+       },
+       
        /**
         * Saves editing of a comment or response.
         * 
@@ -779,4 +1006,4 @@ WCF.Comment.Response.Like = WCF.Like.extend({
         * @see WCF.Like._getWidgetContainer()
         */
        _getWidgetContainer: function(containerID) { }
-});
\ No newline at end of file
+});
index d7d1f59a26cbb890dead2ca20fd52646809981b6..70af4a95cf31984213c4b7f14631bbcfa0e94886 100644 (file)
@@ -12,12 +12,14 @@ use wcf\system\comment\CommentHandler;
 use wcf\system\exception\PermissionDeniedException;
 use wcf\system\exception\UserInputException;
 use wcf\system\like\LikeHandler;
+use wcf\system\recaptcha\RecaptchaHandler;
 use wcf\system\user\activity\event\UserActivityEventHandler;
 use wcf\system\user\notification\object\CommentResponseUserNotificationObject;
 use wcf\system\user\notification\object\CommentUserNotificationObject;
 use wcf\system\user\notification\UserNotificationHandler;
 use wcf\system\WCF;
 use wcf\util\MessageUtil;
+use wcf\util\UserUtil;
 
 /**
  * Executes comment-related actions.
@@ -33,7 +35,7 @@ class CommentAction extends AbstractDatabaseObjectAction {
        /**
         * @see \wcf\data\AbstractDatabaseObjectAction::$allowGuestAccess
         */
-       protected $allowGuestAccess = array('loadComments');
+       protected $allowGuestAccess = array('addComment', 'addResponse', 'loadComments', 'getGuestDialog');
        
        /**
         * @see \wcf\data\AbstractDatabaseObjectAction::$className
@@ -69,6 +71,12 @@ class CommentAction extends AbstractDatabaseObjectAction {
         * @var \wcf\data\comment\response\CommentResponse
         */
        public $createdResponse = null;
+
+       /**
+        * errors occuring durch the validation of addComment or addResponse
+        * @var array
+        */
+       public $validationErrors = array();
        
        /**
         * @see \wcf\data\AbstractDatabaseObjectAction::delete()
@@ -174,6 +182,10 @@ class CommentAction extends AbstractDatabaseObjectAction {
                CommentHandler::enforceFloodControl();
                
                $this->readInteger('objectID', false, 'data');
+               
+               $this->validateUsername();
+               $this->validateRecaptcha();
+               
                $this->validateMessage();
                $objectType = $this->validateObjectType();
                
@@ -190,13 +202,19 @@ class CommentAction extends AbstractDatabaseObjectAction {
         * @return      array
         */
        public function addComment() {
+               if (!empty($this->validationErrors)) {
+                       return array(
+                               'errors' => $this->validationErrors
+                       );
+               }
+               
                // create comment
                $this->createdComment = CommentEditor::create(array(
                        'objectTypeID' => $this->parameters['data']['objectTypeID'],
                        'objectID' => $this->parameters['data']['objectID'],
                        'time' => TIME_NOW,
-                       'userID' => WCF::getUser()->userID,
-                       'username' => WCF::getUser()->username,
+                       'userID' => WCF::getUser()->userID ?: null,
+                       'username' => WCF::getUser()->userID ? WCF::getUser()->username : $this->parameters['data']['username'],
                        'message' => $this->parameters['data']['message'],
                        'responses' => 0,
                        'responseIDs' => serialize(array())
@@ -207,7 +225,7 @@ class CommentAction extends AbstractDatabaseObjectAction {
                
                // fire activity event
                $objectType = ObjectTypeCache::getInstance()->getObjectType($this->parameters['data']['objectTypeID']);
-               if (UserActivityEventHandler::getInstance()->getObjectTypeID($objectType->objectType.'.recentActivityEvent')) {
+               if ($this->createdComment->userID && UserActivityEventHandler::getInstance()->getObjectTypeID($objectType->objectType.'.recentActivityEvent')) {
                        UserActivityEventHandler::getInstance()->fireEvent($objectType->objectType.'.recentActivityEvent', $this->createdComment->commentID);
                }
                
@@ -222,6 +240,17 @@ class CommentAction extends AbstractDatabaseObjectAction {
                        }
                }
                
+               if (!$this->createdComment->userID) {
+                       // save user name is session
+                       WCF::getSession()->register('username', $this->createdComment->username);
+                       
+                       // save last comment time for flood control
+                       WCF::getSession()->register('lastCommentTime', $this->createdComment->time);
+                       
+                       // unmark recaptcha as done for furture requests
+                       WCF::getSession()->unregister('recaptchaDone');
+               }
+               
                return array(
                        'template' => $this->renderComment($this->createdComment)
                );
@@ -236,10 +265,12 @@ class CommentAction extends AbstractDatabaseObjectAction {
                $this->readInteger('objectID', false, 'data');
                $this->validateMessage();
                
+               $this->validateUsername();
+               $this->validateRecaptcha();
+               
                // validate comment id
                $this->validateCommentID();
                
-               // validate object type id
                $objectType = $this->validateObjectType();
                
                // validate object id and permissions
@@ -255,12 +286,18 @@ class CommentAction extends AbstractDatabaseObjectAction {
         * @return      array
         */
        public function addResponse() {
+               if (!empty($this->validationErrors)) {
+                       return array(
+                               'errors' => $this->validationErrors
+                       );
+               }
+               
                // create response
                $this->createdResponse = CommentResponseEditor::create(array(
                        'commentID' => $this->comment->commentID,
                        'time' => TIME_NOW,
-                       'userID' => WCF::getUser()->userID,
-                       'username' => WCF::getUser()->username,
+                       'userID' => WCF::getUser()->userID ?: null,
+                       'username' => WCF::getUser()->userID ? WCF::getUser()->username : $this->parameters['data']['username'],
                        'message' => $this->parameters['data']['message']
                ));
                
@@ -283,7 +320,7 @@ class CommentAction extends AbstractDatabaseObjectAction {
                
                // fire activity event
                $objectType = ObjectTypeCache::getInstance()->getObjectType($this->comment->objectTypeID);
-               if (UserActivityEventHandler::getInstance()->getObjectTypeID($objectType->objectType.'.response.recentActivityEvent')) {
+               if ($this->createdResponse->userID && UserActivityEventHandler::getInstance()->getObjectTypeID($objectType->objectType.'.response.recentActivityEvent')) {
                        UserActivityEventHandler::getInstance()->fireEvent($objectType->objectType.'.response.recentActivityEvent', $this->createdResponse->responseID);
                }
                
@@ -305,6 +342,17 @@ class CommentAction extends AbstractDatabaseObjectAction {
                        }
                }
                
+               if (!$this->createdResponse->userID) {
+                       // save user name is session
+                       WCF::getSession()->register('username', $this->createdResponse->username);
+                       
+                       // save last comment time for flood control
+                       WCF::getSession()->register('lastCommentTime', $this->createdResponse->time);
+                       
+                       // unmark recaptcha as done for furture requests
+                       WCF::getSession()->unregister('recaptchaDone');
+               }
+               
                return array(
                        'commentID' => $this->comment->commentID,
                        'template' => $this->renderResponse($this->createdResponse),
@@ -473,6 +521,48 @@ class CommentAction extends AbstractDatabaseObjectAction {
                }
        }
        
+       /**
+        * Validates the 'getGuestDialog' action.
+        */
+       public function validateGetGuestDialog() {
+               if (WCF::getUser()->userID) {
+                       throw new PermissionDeniedException();
+               }
+               
+               CommentHandler::enforceFloodControl();
+               
+               $this->readInteger('objectID', false, 'data');
+               $objectType = $this->validateObjectType();
+               
+               // validate object id and permissions
+               $this->commentProcessor = $objectType->getProcessor();
+               if (!$this->commentProcessor->canAdd($this->parameters['data']['objectID'])) {
+                       throw new PermissionDeniedException();
+               }
+               
+               // validate message already at this point to make sure that the
+               // message is valid when submitting the dialog to avoid having to
+               // go back to the message to fix it
+               $this->validateMessage();
+       }
+       
+       /**
+        * Returns the dialog for guests when they try to write a comment letting
+        * them enter a username and solving a captcha.
+        * 
+        * @return      array
+        */
+       public function getGuestDialog() {
+               RecaptchaHandler::getInstance()->assignVariables();
+               
+               return array(
+                       'template' => WCF::getTPL()->fetch('commentAddGuestDialog', 'wcf', array(
+                               'ajaxRecaptcha' => true,
+                               'username' => WCF::getSession()->getVar('username')
+                       ))
+               );
+       }
+       
        /**
         * Renders a comment.
         * 
@@ -485,8 +575,10 @@ class CommentAction extends AbstractDatabaseObjectAction {
                $comment->setIsEditable($this->commentProcessor->canEditComment($comment->getDecoratedObject()));
                
                // set user profile
-               $userProfile = UserProfile::getUserProfile($comment->userID);
-               $comment->setUserProfile($userProfile);
+               if ($comment->userID) {
+                       $userProfile = UserProfile::getUserProfile($comment->userID);
+                       $comment->setUserProfile($userProfile);
+               }
                
                WCF::getTPL()->assign(array(
                        'commentList' => array($comment)
@@ -506,8 +598,10 @@ class CommentAction extends AbstractDatabaseObjectAction {
                $response->setIsEditable($this->commentProcessor->canEditResponse($response->getDecoratedObject()));
                
                // set user profile
-               $userProfile = UserProfile::getUserProfile($response->userID);
-               $response->setUserProfile($userProfile);
+               if ($response->userID) {
+                       $userProfile = UserProfile::getUserProfile($response->userID);
+                       $response->setUserProfile($userProfile);
+               }
                
                // render response
                WCF::getTPL()->assign(array(
@@ -568,6 +662,49 @@ class CommentAction extends AbstractDatabaseObjectAction {
                }
        }
        
+       /**
+        * Validates the username parameter.
+        */
+       protected function validateUsername() {
+               if (WCF::getUser()->userID) return;
+               
+               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) {
+                       if ($e->getType() == 'empty') {
+                               $this->validationErrors['username'] = WCF::getLanguage()->get('wcf.global.form.error.empty');
+                       }
+                       else {
+                               $this->validationErrors['username'] = WCF::getLanguage()->get('wcf.user.username.error.'.$e->getType());
+                       }
+               }
+       }
+
+       /**
+        * Validates the recaptcha challenge.
+        */
+       protected function validateRecaptcha() {
+               if (WCF::getUser()->userID || !MODULE_SYSTEM_RECAPTCHA || WCF::getSession()->getVar('recaptchaDone')) return;
+               
+               $this->readString('recaptchaChallenge');
+               $this->readString('recaptchaResponse');
+               
+               try {
+                       RecaptchaHandler::getInstance()->validate($this->parameters['recaptchaChallenge'], $this->parameters['recaptchaResponse']);
+               }
+               catch (UserInputException $e) {
+                       $this->validationErrors['recaptcha'] = WCF::getLanguage()->get('wcf.recaptcha.error.recaptchaString.false');
+               }
+       }
+       
        /**
         * Returns the comment object.
         * 
index 68c41df571e1bceca3eef11ffb330c2497059cd1..3857ea6bf1787b43628e7ffc119b60225611bdfe 100644 (file)
@@ -91,7 +91,10 @@ class StructuredCommentList extends CommentList {
                                $this->responseIDs[] = $responseID;
                                $responseIDs[$responseID] = $comment->commentID;
                        }
-                       $userIDs[] = $comment->userID;
+
+                       if ($comment->userID) {
+                               $userIDs[] = $comment->userID;
+                       }
                        
                        $comment = new StructuredComment($comment);
                        $comment->setIsDeletable($this->commentManager->canDeleteComment($comment->getDecoratedObject()));
@@ -113,7 +116,9 @@ class StructuredCommentList extends CommentList {
                                $commentID = $responseIDs[$response->responseID];
                                $this->objects[$commentID]->addResponse($response);
                                
-                               $userIDs[] = $response->userID;
+                               if ($response->userID) {
+                                       $userIDs[] = $response->userID;
+                               }
                        }
                }
                
@@ -123,12 +128,12 @@ class StructuredCommentList extends CommentList {
                        
                        $users = UserProfile::getUserProfiles($userIDs);
                        foreach ($this->objects as $comment) {
-                               if (isset($users[$comment->userID])) {
+                               if ($comment->userID && isset($users[$comment->userID])) {
                                        $comment->setUserProfile($users[$comment->userID]);
                                }
                                
                                foreach ($comment as $response) {
-                                       if (isset($users[$response->userID])) {
+                                       if ($response->userID && isset($users[$response->userID])) {
                                                $response->setUserProfile($users[$response->userID]);
                                        }
                                }
index 07a8af2c064e842cd804b61d7280559df189335a..263d43d842210212fe45a6055526e2ac22c85c5c 100644 (file)
@@ -802,6 +802,13 @@ class WCF {
                return $data['updates'];
        }
        
+       /**
+        * @see \wcf\system\request\RouteHandler::secureConnection()
+        */
+       public function secureConnection() {
+               return RouteHandler::secureConnection();
+       }
+       
        /**
         * Initialises the cronjobs.
         */
index 81d5f85b99c48f89c998c698015f15c87bd46df5..53f2bff343c9bfb069b8be5ec02d5b4a9505bbab 100644 (file)
@@ -174,6 +174,19 @@ class CommentHandler extends SingletonFactory {
                        return;
                }
                
+               // flood control for guests is session based
+               if (!WCF::getUser()->userID) {
+                       $lastCommentTime = WCF::getSession()->getVar('lastCommentTime');
+                       
+                       if ($lastCommentTime && $lastCommentTime + WCF::getSession()->getPermission('user.comment.floodControlTime') > TIME_NOW) {
+                               throw new NamedUserException(WCF::getLanguage()->getDynamicVariable('wcf.comment.error.floodControl', array(
+                                       'lastCommentTime' => $lastCommentTime
+                               )));
+                       }
+                       
+                       return;
+               }
+               
                // check for comments
                $sql = "SELECT          time
                        FROM            wcf".WCF_N."_comment
index edcdb5b31bb1a0a989865921e15d56b6043d5341..363d9ae29da0c5ff26395723a088c708eef2a35c 100644 (file)
@@ -30,7 +30,14 @@ class UserProfileCommentResponseOwnerUserNotificationEvent extends AbstractUserN
        public function getMessage() {
                // @todo: use cache or a single query to retrieve required data
                $comment = new Comment($this->userNotificationObject->commentID);
-               $commentAuthor = new User($comment->userID);
+               if ($comment->userID) {
+                       $commentAuthor = new User($comment->userID);
+               }
+               else {
+                       $commentAuthor = new User(null, array(
+                               'username' => $comment->username
+                       ));
+               }
                
                return $this->getLanguage()->getDynamicVariable('wcf.user.notification.commentResponseOwner.message', array(
                        'author' => $this->author,
@@ -43,8 +50,15 @@ class UserProfileCommentResponseOwnerUserNotificationEvent extends AbstractUserN
         */
        public function getEmailMessage($notificationType = 'instant') {
                $comment = new Comment($this->userNotificationObject->commentID);
-               $commentAuthor = new User($comment->userID);
                $owner = new User($comment->objectID);
+               if ($comment->userID) {
+                       $commentAuthor = new User($comment->userID);
+               }
+               else {
+                       $commentAuthor = new User(null, array(
+                               'username' => $comment->username
+                       ));
+               }
                
                return $this->getLanguage()->getDynamicVariable('wcf.user.notification.commentResponseOwner.mail', array(
                        'response' => $this->userNotificationObject,
index 9432ce939a0bad707fea500959b019b8a7a2033c..335d6461b3c8956ce48c02cc6005a6fbb399f45e 100644 (file)
@@ -29,7 +29,14 @@ class UserProfileCommentResponseUserNotificationEvent extends AbstractUserNotifi
        public function getMessage() {
                // @todo: use cache or a single query to retrieve required data
                $comment = new Comment($this->userNotificationObject->commentID);
-               $user = new User($comment->objectID);
+               if ($comment->userID) {
+                       $commentAuthor = new User($comment->userID);
+               }
+               else {
+                       $commentAuthor = new User(null, array(
+                               'username' => $comment->username
+                       ));
+               }
                
                return $this->getLanguage()->getDynamicVariable('wcf.user.notification.commentResponse.message', array(
                        'author' => $this->author,
index c9e3f87ae4ba56ce3a968530c02a019655555f3e..ecf286198dba827bbffa989ebbdfff18c2c4a51a 100644 (file)
@@ -1513,6 +1513,7 @@ Fehler sind beispielsweise:
                <item name="wcf.comment.response.add"><![CDATA[Antworten …]]></item>
                <item name="wcf.comment.response.more"><![CDATA[{literal}{if $count == 1}Eine weitere Antwort{else}{#$count} weitere Antworten{/if}{/literal}]]></item>
                <item name="wcf.comment.button.response.add"><![CDATA[Antworten]]></item>
+               <item name="wcf.comment.guestDialog.title"><![CDATA[TODO]]></item>
        </category>
        
        <category name="wcf.dashboard">
index 67cdc8a78e9fa1a3005097a27476cd268f4a3df0..e53ad25ace1e1a3bdb387ec1cb1ba104bcbbad4f 100644 (file)
@@ -1512,6 +1512,7 @@ Errors are:
                <item name="wcf.comment.response.add"><![CDATA[Reply …]]></item>
                <item name="wcf.comment.response.more"><![CDATA[{literal}{if $count == 1}One more reply{else}{#$count} more replies{/if}{/literal}]]></item>
                <item name="wcf.comment.button.response.add"><![CDATA[Reply]]></item>
+               <item name="wcf.comment.guestDialog.title"><![CDATA[TODO]]></item>
        </category>
        
        <category name="wcf.dashboard">