Add object type-based captcha system (WIP)
authorMatthias Schmidt <gravatronics@live.com>
Sat, 14 Jun 2014 13:04:07 +0000 (15:04 +0200)
committerMatthias Schmidt <gravatronics@live.com>
Sat, 14 Jun 2014 13:04:07 +0000 (15:04 +0200)
48 files changed:
com.woltlab.wcf/acpMenu.xml
com.woltlab.wcf/objectType.xml
com.woltlab.wcf/objectTypeDefinition.xml
com.woltlab.wcf/option.xml
com.woltlab.wcf/templates/__commentJavaScript.tpl
com.woltlab.wcf/templates/captcha.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/captchaQuestion.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/commentAddGuestDialog.tpl
com.woltlab.wcf/templates/lostPassword.tpl
com.woltlab.wcf/templates/mail.tpl
com.woltlab.wcf/templates/recaptcha.tpl
com.woltlab.wcf/templates/register.tpl
com.woltlab.wcf/templates/search.tpl
com.woltlab.wcf/userGroupOption.xml
wcfsetup/install/files/acp/templates/captchaQuestionAdd.tpl [new file with mode: 0644]
wcfsetup/install/files/acp/templates/captchaQuestionList.tpl [new file with mode: 0644]
wcfsetup/install/files/js/WCF.Comment.js
wcfsetup/install/files/js/WCF.js
wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/form/CaptchaQuestionEditForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/acp/page/CaptchaQuestionListPage.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/action/FacebookAuthAction.class.php
wcfsetup/install/files/lib/action/GithubAuthAction.class.php
wcfsetup/install/files/lib/action/GoogleAuthAction.class.php
wcfsetup/install/files/lib/action/TwitterAuthAction.class.php
wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionEditor.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/data/comment/CommentAction.class.php
wcfsetup/install/files/lib/form/AbstractCaptchaForm.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/form/LostPasswordForm.class.php
wcfsetup/install/files/lib/form/MailForm.class.php
wcfsetup/install/files/lib/form/MessageForm.class.php
wcfsetup/install/files/lib/form/RecaptchaForm.class.php
wcfsetup/install/files/lib/form/RegisterForm.class.php
wcfsetup/install/files/lib/form/SearchForm.class.php
wcfsetup/install/files/lib/form/SignatureEditForm.class.php
wcfsetup/install/files/lib/system/cache/builder/CaptchaQuestionCacheBuilder.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/captcha/CaptchaHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/captcha/CaptchaQuestionHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/captcha/ICaptchaHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/captcha/RecaptchaHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/option/CaptchaSelectOptionType.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/recaptcha/RecaptchaHandler.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index e633b3fc945955eae0c736528960d775afd9d671..29df7909ae7eace4447bf84613faa7da6ae43825 100644 (file)
                        <parent>wcf.acp.menu.link.language.server</parent>
                        <permissions>admin.language.canManageLanguage</permissions>
                </acpmenuitem>-->
+               
+               <acpmenuitem name="wcf.acp.menu.link.captcha">
+                       <parent>wcf.acp.menu.link.display</parent>
+                       <showorder>5</showorder>
+               </acpmenuitem>
+               <acpmenuitem name="wcf.acp.menu.link.captcha.question.list">
+                       <parent>wcf.acp.menu.link.captcha</parent>
+                       <controller><![CDATA[wcf\acp\page\CaptchaQuestionListPage]]></controller>
+                       <permissions>admin.captcha.canManageCaptchaQuestion</permissions>
+               </acpmenuitem>
+               <acpmenuitem name="wcf.acp.menu.link.captcha.question.add">
+                       <parent>wcf.acp.menu.link.captcha</parent>
+                       <controller><![CDATA[wcf\acp\form\CaptchaQuestionAddForm]]></controller>
+                       <permissions>admin.captcha.canManageCaptchaQuestion</permissions>
+               </acpmenuitem>
                <!-- /language -->
                
                <acpmenuitem name="wcf.acp.menu.link.content">
index 335de6c268b6e747846f7669c0f2d434a7be890b..eceb72f287eccf3646361287fd49fa77780fa7db 100644 (file)
                        <conditiongroup>userOptions</conditiongroup>
                </type>
                <!-- /ad conditions -->
+               
+               <!-- captcha types -->
+               <type>
+                       <name>com.woltlab.wcf.recaptcha</name>
+                       <definitionname>com.woltlab.wcf.captcha</definitionname>
+                       <classname><![CDATA[wcf\system\captcha\RecaptchaHandler]]></classname>
+               </type>
+               <type>
+                       <name>com.woltlab.wcf.captchaQuestion</name>
+                       <definitionname>com.woltlab.wcf.captcha</definitionname>
+                       <classname><![CDATA[wcf\system\captcha\CaptchaQuestionHandler]]></classname>
+               </type>
+               <!-- captcha types -->
        </import>
 </data>
index 8cfd6268ca3021a65a4d8756da7a82fa7f261f06..daebce74ab36b3e686424dbfea9de5b013f406cb 100644 (file)
                        <name>com.woltlab.wcf.condition.ad</name>
                        <interfacename><![CDATA[wcf\system\condition\IContentCondition]]></interfacename>
                </definition>
+               
+               <definition>
+                       <name>com.woltlab.wcf.captcha</name>
+                       <interfacename><![CDATA[wcf\system\captcha\ICaptchaHandler]]></interfacename>
+               </definition>
        </import>
 </data>
index 50883b1d6263af7d9aff0400494f11e70c6a8a0b..d83bc75e537343ed39c4a1d69ee67c9a4d5d4e2d 100644 (file)
                                <category name="security.antispam">
                                        <parent>security</parent>
                                </category>
+                                       <category name="security.antispam.captcha">
+                                               <parent>security.antispam</parent>
+                                       </category>
                                        <category name="security.antispam.recaptcha">
                                                <parent>security.antispam</parent>
                                                <options>module_system_recaptcha</options>
@@ -524,6 +527,41 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]>
                        </option>
                        <!-- /security.blacklist -->
                        
+                       <!-- security.antispam -->
+                       <option name="register_captcha_type">
+                               <categoryname>security.antispam.captcha</categoryname>
+                               <optiontype>captchaSelect</optiontype>
+                               <defaultvalue>com.woltlab.wcf.recaptcha</defaultvalue>
+                               <allowemptyvalue>1</allowemptyvalue>
+                       </option>
+                       <option name="lost_password_captcha_type">
+                               <categoryname>security.antispam.captcha</categoryname>
+                               <optiontype>captchaSelect</optiontype>
+                               <defaultvalue>com.woltlab.wcf.recaptcha</defaultvalue>
+                               <allowemptyvalue>1</allowemptyvalue>
+                       </option>
+                       <option name="profile_mail_captcha_type">
+                               <categoryname>security.antispam.captcha</categoryname>
+                               <optiontype>captchaSelect</optiontype>
+                               <defaultvalue>com.woltlab.wcf.recaptcha</defaultvalue>
+                               <allowemptyvalue>1</allowemptyvalue>
+                       </option>
+                       <option name="search_captcha_type">
+                               <categoryname>security.antispam.captcha</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <optiontype>captchaSelect</optiontype>
+                               <defaultvalue>com.woltlab.wcf.recaptcha</defaultvalue>
+                               <allowemptyvalue>1</allowemptyvalue>
+                       </option>
+                       <option name="message_captcha_type">
+                               <categoryname>security.antispam.captcha</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <optiontype>captchaSelect</optiontype>
+                               <defaultvalue>com.woltlab.wcf.recaptcha</defaultvalue>
+                               <allowemptyvalue>1</allowemptyvalue>
+                       </option>
+                       <!-- /security.antispam -->
+                       
                        <option name="recaptcha_publickey">
                                <categoryname>security.antispam.recaptcha</categoryname>
                                <optiontype>text</optiontype>
@@ -532,7 +570,6 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]>
                                <defaultvalue>6LfOlMYSAAAAADvo3s4puBAYDqI-6YK2ybe7BJE5</defaultvalue>
                                <showorder>1</showorder>
                        </option>
-                       
                        <option name="recaptcha_privatekey">
                                <categoryname>security.antispam.recaptcha</categoryname>
                                <optiontype>text</optiontype>
@@ -932,27 +969,6 @@ no:!cache_source_memcached_host]]></enableoptions>
                        </option>
                        <!-- /user.3rdPartyAuth -->
                        
-                       <!-- user.security -->
-                       <option name="register_use_captcha">
-                               <categoryname>security.antispam</categoryname>
-                               <optiontype>boolean</optiontype>
-                               <defaultvalue><![CDATA[1]]></defaultvalue>
-                               <options>module_system_recaptcha</options>
-                       </option>
-                       <option name="lost_password_use_captcha">
-                               <categoryname>security.antispam</categoryname>
-                               <optiontype>boolean</optiontype>
-                               <defaultvalue><![CDATA[1]]></defaultvalue>
-                               <options>module_system_recaptcha</options>
-                       </option>
-                       <option name="profile_mail_use_captcha">
-                               <categoryname>security.antispam</categoryname>
-                               <optiontype>boolean</optiontype>
-                               <defaultvalue><![CDATA[1]]></defaultvalue>
-                               <options>module_system_recaptcha</options>
-                       </option>
-                       <!-- /user.security -->
-                       
                        <!-- user.avatar -->
                        <option name="max_avatar_width">
                                <categoryname>user.avatar</categoryname>
@@ -1210,11 +1226,6 @@ DESC:wcf.global.sortOrder.descending]]></selectoptions>
                                <minvalue>5</minvalue>
                                <maxvalue>40</maxvalue>
                        </option>
-                       <option name="search_use_captcha">
-                               <categoryname>security.antispam</categoryname>
-                               <optiontype>boolean</optiontype>
-                               <options>module_system_recaptcha</options>
-                       </option>
                        
                        <option name="search_default_sort_field">
                                <categoryname>message.search</categoryname>
@@ -1242,6 +1253,36 @@ DESC:wcf.global.sortOrder.descending]]></selectoptions>
                                <maxvalue>100</maxvalue>
                        </option>
                        <!-- /message.general.poll -->
+                       
+                       <!-- deprecated -->
+                       <option name="register_use_captcha">
+                               <categoryname>security.antispam</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue><![CDATA[1]]></defaultvalue>
+                               <options>module_system_recaptcha</options>
+                               <hidden>1</hidden>
+                       </option>
+                       <option name="lost_password_use_captcha">
+                               <categoryname>security.antispam</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue><![CDATA[1]]></defaultvalue>
+                               <options>module_system_recaptcha</options>
+                               <hidden>1</hidden>
+                       </option>
+                       <option name="profile_mail_use_captcha">
+                               <categoryname>security.antispam</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue><![CDATA[1]]></defaultvalue>
+                               <options>module_system_recaptcha</options>
+                               <hidden>1</hidden>
+                       </option>
+                       <option name="search_use_captcha">
+                               <categoryname>security.antispam</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <options>module_system_recaptcha</options>
+                               <hidden>1</hidden>
+                       </option>
+                       <!-- deprecated -->
                </options>
        </import>
        
index 2d2d936817563bd1dc240dfcb25ec0d1a6305bd5..04337db0aaeaeb7ccf6b1068d2e11c2dc25655ef 100644 (file)
@@ -9,6 +9,7 @@
                        'wcf.comment.button.response.add': '{lang}wcf.comment.button.response.add{/lang}',
                        'wcf.comment.delete.confirmMessage': '{lang}wcf.comment.delete.confirmMessage{/lang}',
                        'wcf.comment.description': '{lang}wcf.comment.description{/lang}',
+                       'wcf.comment.guestDialog.title': '{lang}wcf.comment.guestDialog.title{/lang}',
                        'wcf.comment.more': '{lang}wcf.comment.more{/lang}',
                        'wcf.comment.response.add': '{lang}wcf.comment.response.add{/lang}',
                        'wcf.comment.response.more': '{lang}wcf.comment.response.more{/lang}',
diff --git a/com.woltlab.wcf/templates/captcha.tpl b/com.woltlab.wcf/templates/captcha.tpl
new file mode 100644 (file)
index 0000000..9ad50b2
--- /dev/null
@@ -0,0 +1,3 @@
+{if $captchaObjectType}
+       {@$captchaObjectType->getProcessor()->getFormElement()}
+{/if}
diff --git a/com.woltlab.wcf/templates/captchaQuestion.tpl b/com.woltlab.wcf/templates/captchaQuestion.tpl
new file mode 100644 (file)
index 0000000..06e0a3f
--- /dev/null
@@ -0,0 +1,43 @@
+<input type="hidden" name="captchaQuestion" value="{$captchaQuestion}" />
+
+{if !$captchaQuestionAnswered}
+       <fieldset>
+               <legend>{lang}wcf.captcha.question.captcha{/lang}</legend>
+               <small>{lang}wcf.captcha.question.captcha.description{/lang}</small>
+               
+               <dl{if (($errorType|isset && $errorType|is_array && $errorType[captchaAnswer]|isset) || ($errorField|isset && $errorField == 'captchaAnswer'))} class="formError"{/if}>
+                       <dt><label for="captchaAnswer">{lang}{$captchaQuestionObject->question}{/lang}</label></dt>
+                       <dd>
+                               <input type="text" id="captchaAnswer" name="captchaAnswer" class="medium" />
+                               {if (($errorType|isset && $errorType|is_array && $errorType[captchaAnswer]|isset) || ($errorField|isset && $errorField == 'captchaAnswer'))}
+                                       {if $errorType|is_array && $errorType[captchaAnswer]|isset}
+                                               {assign var='__errorType' value=$errorType[captchaAnswer]}
+                                       {else}
+                                               {assign var='__errorType' value=$errorType}
+                                       {/if}
+                                       
+                                       {if $__errorType == 'empty'}
+                                               <small class="innerError">{lang}wcf.global.form.error.empty{/lang}</small>
+                                       {else}
+                                               <small class="innerError">{lang}wcf.captcha.question.answer.error.{$__errorType}{/lang}</small>
+                                       {/if}
+                               {/if}
+                       </dd>
+               </dl>
+       </fieldset>
+       
+       {if !$ajaxCaptcha|empty}
+               <script data-relocate="true">
+                       //<![CDATA[
+                       $(function() {
+                               WCF.System.Captcha.addCallback('{$captchaID}', function() {
+                                       return {
+                                               captchaAnswer: $('#captchaAnswer').val(),
+                                               captchaQuestion: '{$captchaQuestion}'
+                                       };
+                               });
+                       });
+                       //]]>
+               </script>
+       {/if}
+{/if}
index f0a4465cf947f51d43f11359b08421abff976f42..1f758870f1b06de0b7644dd8e390ae84d0e7a491 100644 (file)
@@ -1,16 +1,23 @@
 <div>
        <fieldset>
-               <dl>
+               <dl{if $errorType[username]|isset} class="formError"{/if}>
                        <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" />
+                               {if $errorType[username]|isset}
+                                       <small class="innerError">
+                                               {if $errorType[username] == 'empty'}
+                                                       {lang}wcf.global.form.error.empty{/lang}
+                                               {else}
+                                                       {lang}wcf.user.username.error.$errorType[username]{/lang}
+                                               {/if}
+                                       </small>
+                               {/if}
                        </dd>
                </dl>
        </fieldset>
        
-       {if MODULE_SYSTEM_RECAPTCHA}
-               {include file='recaptcha'}
-       {/if}
+       {include file='captcha'}
 </div>
 
 <div class="formSubmit">
index 2a37999878045294c6dcec7dda0b1e705df8474d..d8d4929f765ab291858246d2514e7ac16ecd96ec 100644 (file)
@@ -80,9 +80,9 @@
                
                {event name='fieldsets'}
                
-               {if $useCaptcha}{include file='recaptcha'}{/if}
+               {include file='captcha'}
        </div>
-               
+       
        <div class="formSubmit">
                <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
                {@SECURITY_TOKEN_INPUT_TAG}
index 5b994a82106b3f904d4799b1909bb7b29752726b..71b2beecc807a336d5f05b2a2b5ec3ae1cb3d79f 100644 (file)
                
                {event name='fieldsets'}
                
-               {if $useCaptcha}
-                       {include file='recaptcha'}
-               {/if}
+               {include file='captcha'}
        </div>
        
        <div class="formSubmit">
index eff2b03eac44969b8f689f01f10bb2fab6f3e51d..c538a1157fe5ebb546c8a0659f5c42f6edb8dbde 100644 (file)
@@ -1,68 +1,95 @@
-<fieldset>
-       <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|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|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}
-                               </small>
-                       {/if}
-               </dd>
-               
-               {event name='fields'}
-               
-               <dd class="jsOnly">
-                       <ul class="buttonList smallButtons">
-                               <li><a href="javascript:Recaptcha.reload()" class="button small"><span class="icon icon16 icon-repeat"></span> <span>{lang}wcf.recaptcha.reload{/lang}</span></a></li>
-                               <li class="recaptcha_only_if_image"><a href="javascript:Recaptcha.switch_type('audio')" class="button small"><span class="icon icon16 icon-volume-up"></span> <span>{lang}wcf.recaptcha.audio{/lang}</span></a></li>
-                               <li class="recaptcha_only_if_audio"><a href="javascript:Recaptcha.switch_type('image')" class="button small"><span class="icon icon16 icon-eye-open"></span> <span>{lang}wcf.recaptcha.image{/lang}</span></a></li>
-                               <li><a href="javascript:Recaptcha.showhelp()" class="button small"><span class="icon icon16 icon-question-sign"></span> <span>{lang}wcf.recaptcha.help{/lang}</span></a></li>
-                               {event name='buttons'}
-                       </ul>
-               </dd>
+{if $recaptchaLegacyMode|empty}
+       {include file='captcha'}
+{else}
+       <fieldset>
+               <legend><label for="recaptcha_response_field">{lang}wcf.recaptcha.title{/lang}</label></legend>
+               <small>{lang}wcf.recaptcha.description{/lang}</small>
                
-               {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'}
+               <dl class="wide reCaptcha{if $errorField|isset && $errorField == 'recaptchaString'} formError{/if}">
+                       {if !$ajaxCaptcha|isset || !$ajaxCaptcha}
+                               <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 (($errorType|isset && $errorType|is_array && $errorType[recaptchaString]|isset) || ($errorField|isset && $errorField == 'recaptchaString'))}
+                                       {if $errorType|is_array && $errorType[recaptchaString]|isset}
+                                               {assign var='__errorType' value=$errorType[recaptchaString]}
+                                       {else}
+                                               {assign var='__errorType' value=$errorType}
+                                       {/if}
                                        <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}
+                                               {if $__errorType == 'empty'}
+                                                       {lang}wcf.global.form.error.empty{/lang}
+                                               {else}
+                                                       {lang}wcf.recaptcha.error.recaptchaString.{$__errorType}{/lang}
+                                               {/if}
                                        </small>
                                {/if}
-                       </noscript>
-               {else}
-                       <script data-relocate="true">
-                               //<![CDATA[
-                               Recaptcha.create("{$recaptchaPublicKey}", "recaptcha_image", {
-                                       lang: '{@$recaptchaLanguageCode}',
-                                       theme : 'custom'
-                               });
-                               //]]>
-                       </script>
-               {/if}
-       </dl>
-</fieldset>
+                       </dd>
+                       
+                       {event name='fields'}
+                       
+                       <dd class="jsOnly">
+                               <ul class="buttonList smallButtons">
+                                       <li><a href="javascript:Recaptcha.reload()" class="button small"><span class="icon icon16 icon-repeat"></span> <span>{lang}wcf.recaptcha.reload{/lang}</span></a></li>
+                                       <li class="recaptcha_only_if_image"><a href="javascript:Recaptcha.switch_type('audio')" class="button small"><span class="icon icon16 icon-volume-up"></span> <span>{lang}wcf.recaptcha.audio{/lang}</span></a></li>
+                                       <li class="recaptcha_only_if_audio"><a href="javascript:Recaptcha.switch_type('image')" class="button small"><span class="icon icon16 icon-eye-open"></span> <span>{lang}wcf.recaptcha.image{/lang}</span></a></li>
+                                       <li><a href="javascript:Recaptcha.showhelp()" class="button small"><span class="icon icon16 icon-question-sign"></span> <span>{lang}wcf.recaptcha.help{/lang}</span></a></li>
+                                       {event name='buttons'}
+                               </ul>
+                       </dd>
+                       
+                       {if !$ajaxCaptcha|isset || !$ajaxCaptcha}
+                               <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 (($errorType|isset && $errorType|is_array && $errorType[recaptchaString]|isset) || ($errorField|isset && $errorField == 'recaptchaString'))}
+                                               {if $errorType|is_array && $errorType[recaptchaString]|isset}
+                                                       {assign var='__errorType' value=$errorType[recaptchaString]}
+                                               {else}
+                                                       {assign var='__errorType' value=$errorType}
+                                               {/if}
+                                               <small class="innerError">
+                                                       {if $errorType == 'empty'}
+                                                               {lang}wcf.global.form.error.empty{/lang}
+                                                       {else}
+                                                               {lang}wcf.recaptcha.error.recaptchaString.{$__errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                               </noscript>
+                       {else}
+                               <script data-relocate="true">
+                                       //<![CDATA[
+                                       Recaptcha.create("{$recaptchaPublicKey}", "recaptcha_image", {
+                                               lang: '{@$recaptchaLanguageCode}',
+                                               theme : 'custom'
+                                       });
+                                       
+                                       WCF.System.Captcha.addCallback('{$captchaID}', function() {
+                                               return {
+                                                       recaptcha_challenge_field: Recaptcha.get_challenge(),
+                                                       recaptcha_response_field: Recaptcha.get_response()
+                                               };
+                                       });
+                                       //]]>
+                               </script>
+                       {/if}
+               </dl>
+       </fieldset>
+{/if}
index f806ca097f368406c41166d9060a22ce9c6bd300..6ebc9c6c0ab9a77c611abcbb41b1461f3237b4a4 100644 (file)
                
                {event name='fieldsets'}
                
-               {if $useCaptcha}
-                       {if $errorType.recaptchaString|isset}
-                               {assign var=errorField value='recaptchaString'}
-                               {assign var=errorType value=$errorType.recaptchaString}
-                       {/if}
-                       {include file='recaptcha'}
-               {/if}
+               {include file='captcha'}
        </div>
        
        <div class="formSubmit">
 {include file='footer'}
 
 </body>
-</html>
\ No newline at end of file
+</html>
index 11ecc2ba36f2922c35a0ba5db631bb001ea734da..b91643c86fd049bafb754e483a018e8ae12d01f4 100644 (file)
@@ -98,7 +98,8 @@
                </fieldset>
                
                {event name='fieldsets'}
-               {if $useCaptcha}{include file='recaptcha'}{/if}
+               
+               {include file='captcha'}
                
                {foreach from=$objectTypes key=objectTypeName item=objectType}
                        {if $objectType->isAccessible() && $objectType->getFormTemplateName()}
 </script>
 
 </body>
-</html>
\ No newline at end of file
+</html>
index a9eea1a904447125a727854ed4d7b92785d0e90a..311e95d76dacf8fba8bd2f251ebc8781e84a52dc 100644 (file)
@@ -78,6 +78,9 @@
                        <category name="admin.template">
                                <parent>admin.display</parent>
                        </category>
+                       <category name="admin.captcha">
+                               <parent>admin.display</parent>
+                       </category>
                        
                        <category name="admin.content">
                                <parent>admin</parent>
@@ -654,6 +657,13 @@ png]]></defaultvalue>
                                <defaultvalue>0</defaultvalue>
                                <admindefaultvalue>1</admindefaultvalue>
                        </option>
+                       
+                       <option name="admin.captcha.canManageCaptchaQuestion">
+                               <categoryname>admin.display.captcha</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                               <admindefaultvalue>1</admindefaultvalue>
+                       </option>
                </options>
        </import>
 </data>
diff --git a/wcfsetup/install/files/acp/templates/captchaQuestionAdd.tpl b/wcfsetup/install/files/acp/templates/captchaQuestionAdd.tpl
new file mode 100644 (file)
index 0000000..28746c4
--- /dev/null
@@ -0,0 +1,86 @@
+{include file='header' pageTitle='wcf.acp.captcha.question.'|concat:$action}
+
+<header class="boxHeadline">
+       <h1>{lang}wcf.acp.captcha.question.{$action}{/lang}</h1>
+</header>
+
+{include file='formError'}
+
+{if $success|isset}
+       <p class="success">{lang}wcf.global.success.{$action}{/lang}</p>
+{/if}
+
+<div class="contentNavigation">
+       <nav>
+               <ul>
+                       <li><a href="{link controller='CaptchaQuestionList'}{/link}" class="button"><span class="icon icon16 icon-list"></span> <span>{lang}wcf.acp.menu.link.captcha.question.list{/lang}</span></a></li>
+                       
+                       {event name='contentNavigationButtons'}
+               </ul>
+       </nav>
+</div>
+
+<form id="adForm" method="post" action="{if $action == 'add'}{link controller='CaptchaQuestionAdd'}{/link}{else}{link controller='CaptchaQuestionEdit' id=$captchaQuestion->questionID}{/link}{/if}">
+       <div class="container containerPadding marginTop">
+               <fieldset>
+                       <legend>{lang}wcf.global.form.data{/lang}</legend>
+                       
+                       <dl{if $errorField == 'question'} class="formError"{/if}>
+                               <dt><label for="question">{lang}wcf.acp.captcha.question.question{/lang}</label></dt>
+                               <dd>
+                                       <input type="text" id="question" name="question" value="{$i18nPlainValues[question]}" required="required" autofocus="autofocus" class="long" />
+                                       {if $errorField == 'question'}
+                                               <small class="innerError">
+                                                       {if $errorType == 'empty'}
+                                                               {lang}wcf.global.form.error.empty{/lang}
+                                                       {elseif $errorType == 'multilingual'}
+                                                               {lang}wcf.global.form.error.multilingual{/lang}
+                                                       {else}
+                                                               {lang}wcf.acp.captcha.question.question.error.{$errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                               </dd>
+                       </dl>
+                       {include file='multipleLanguageInputJavascript' elementIdentifier='question' forceSelection=false}
+                       
+                       <dl{if $errorField == 'answers'} class="formError"{/if}>
+                               <dt><label for="answers">{lang}wcf.acp.captcha.question.answers{/lang}</label></dt>
+                               <dd>
+                                       <textarea id="answers" name="answers" cols="40" rows="10">{$i18nPlainValues[answers]}</textarea>
+                                       <small>{lang}wcf.acp.captcha.question.answers.description{/lang}</small>
+                                       {if $errorField == 'answers'}
+                                               <small class="innerError">
+                                                       {if $errorType == 'empty'}
+                                                               {lang}wcf.global.form.error.empty{/lang}
+                                                       {elseif $errorType == 'multilingual'}
+                                                               {lang}wcf.global.form.error.multilingual{/lang}
+                                                       {else}
+                                                               {lang}wcf.acp.captcha.question.answers.error.{$errorType}{/lang}
+                                                       {/if}
+                                               </small>
+                                       {/if}
+                               </dd>
+                       </dl>
+                       {include file='multipleLanguageInputJavascript' elementIdentifier='answers' forceSelection=false}
+                       
+                       <dl>
+                               <dt></dt>
+                               <dd>
+                                       <label><input type="checkbox" name="isDisabled" value="1"{if $isDisabled} checked="checked"{/if} /> {lang}wcf.acp.captcha.question.isDisabled{/lang}</label>
+                               </dd>
+                       </dl>
+                       
+                       {event name='dataFields'}
+               </fieldset>
+               
+               {event name='fieldsets'}
+       </div>
+       
+       <div class="formSubmit">
+               <input type="submit" value="{lang}wcf.global.button.submit{/lang}" accesskey="s" />
+               {@SECURITY_TOKEN_INPUT_TAG}
+       </div>
+</form>
+
+{include file='footer'}
diff --git a/wcfsetup/install/files/acp/templates/captchaQuestionList.tpl b/wcfsetup/install/files/acp/templates/captchaQuestionList.tpl
new file mode 100644 (file)
index 0000000..1f41b88
--- /dev/null
@@ -0,0 +1,94 @@
+{include file='header' pageTitle='wcf.acp.captcha.question.list'}
+
+<header class="boxHeadline">
+       <h1>{lang}wcf.acp.captcha.question.list{/lang}</h1>
+</header>
+
+<script data-relocate="true">
+       //<![CDATA[
+       $(function() {
+               new WCF.Action.Delete('wcf\\data\\captcha\\question\\CaptchaQuestionAction', '.jsQuestionRow');
+               new WCF.Action.Toggle('wcf\\data\\captcha\\question\\CaptchaQuestionAction', '.jsQuestionRow');
+               
+               var options = { };
+               {if $pages > 1}
+                       options.refreshPage = true;
+                       {if $pages == $pageNo}
+                               options.updatePageNumber = -1;
+                       {/if}
+               {else}
+                       options.emptyMessage = '{lang}wcf.global.noItems{/lang}';
+               {/if}
+               
+               new WCF.Table.EmptyTableHandler($('#captchaQuestionTabelContainer'), 'jsQuestionRow', options);
+       });
+       //]]>
+</script>
+
+<div class="contentNavigation">
+       {pages print=true assign=pagesLinks controller="CaptchaQuestionList" link="pageNo=%d"}
+       
+       <nav>
+               <ul>
+                       <li><a href="{link controller='CaptchaQuestionAdd'}{/link}" class="button"><span class="icon icon16 icon-plus"></span> <span>{lang}wcf.acp.captcha.question.add{/lang}</span></a></li>
+                       
+                       {event name='contentNavigationButtonsTop'}
+               </ul>
+       </nav>
+</div>
+
+{hascontent}
+       <div id="captchaQuestionTabelContainer" class="tabularBox tabularBoxTitle marginTop">
+               <header>
+                       <h2>{lang}wcf.acp.captcha.question.list{/lang} <span class="badge badgeInverse">{#$items}</span></h2>
+               </header>
+               
+               <table class="table">
+                       <thead>
+                               <tr>
+                                       <th class="columnID columnQuestionID active ASC" colspan="2">{lang}wcf.global.objectID{/lang}</th>
+                                       <th class="columnText columnQuestion">{lang}wcf.acp.captcha.question.question{/lang}</th>
+                                       
+                                       {event name='columnHeads'}
+                               </tr>
+                       </thead>
+                       
+                       <tbody>
+                               {content}
+                                       {foreach from=$objects item='question'}
+                                               <tr class="jsQuestionRow">
+                                                       <td class="columnIcon">
+                                                               <span class="icon icon16 icon-check{if $question->isDisabled}-empty{/if} jsToggleButton jsTooltip pointer" title="{lang}wcf.global.button.{if $question->isDisabled}enable{else}disable{/if}{/lang}" data-object-id="{@$question->questionID}" data-disable-message="{lang}wcf.global.button.disable{/lang}" data-enable-message="{lang}wcf.global.button.enable{/lang}"></span>
+                                                               <a href="{link controller='CaptchaQuestionEdit' id=$question->questionID}{/link}" title="{lang}wcf.global.button.edit{/lang}" class="jsTooltip"><span class="icon icon16 icon-pencil"></span></a>
+                                                               <span class="icon icon16 icon-remove jsDeleteButton jsTooltip pointer" title="{lang}wcf.global.button.delete{/lang}" data-object-id="{@$question->questionID}" data-confirm-message="{lang}wcf.acp.captcha.question.delete.confirmMessage{/lang}"></span>
+                                                               
+                                                               {event name='rowButtons'}
+                                                       </td>
+                                                       <td class="columnID columnQuestionID">{$question->questionID}</td>
+                                                       <td class="columnText columnQuestion"><a href="{link controller='CaptchaQuestionEdit' id=$question->questionID}{/link}">{lang}{$question->question}{/lang}</a></td>
+                                                       
+                                                       {event name='columns'}
+                                               </tr>
+                                       {/foreach}
+                               {/content}
+                       </tbody>
+               </table>
+       </div>
+{hascontentelse}
+       <p class="info">{lang}wcf.global.noItems{/lang}</p>
+{/hascontent}
+
+<div class="contentNavigation">
+       {@$pagesLinks}
+       
+       <nav>
+               <ul>
+                       <li><a href="{link controller='CaptchaQuestionAdd'}{/link}" class="button"><span class="icon icon16 icon-plus"></span> <span>{lang}wcf.acp.captcha.question.add{/lang}</span></a></li>
+                       
+                       {event name='contentNavigationButtonsBottom'}
+               </ul>
+       </nav>
+</div>
+
+{include file='footer'}
\ No newline at end of file
index 9092c85ed34937c22b514ce974388739408ec18d..1d1ab8214753afaa23b05ccb11fcceac4e97915e 100644 (file)
@@ -88,12 +88,6 @@ WCF.Comment.Handler = Class.extend({
         * @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.
@@ -132,6 +126,8 @@ WCF.Comment.Handler = Class.extend({
                
                WCF.DOMNodeInsertedHandler.execute();
                WCF.DOMNodeInsertedHandler.addCallback('WCF.Comment.Handler', $.proxy(this._domNodeInserted, this));
+               
+               WCF.System.ObjectStore.add('WCF.Comment.Handler', this);
        },
        
        /**
@@ -204,7 +200,6 @@ WCF.Comment.Handler = Class.extend({
         */
        _loadResponses: function(event) {
                this._loadResponsesExecute($(event.currentTarget).disable().data('commentID'), false);
-               
        },
        
        /**
@@ -460,30 +455,18 @@ WCF.Comment.Handler = Class.extend({
                        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.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();
-                       }
-                       else {
-                               // request a new recaptcha
-                               if (this._useRecaptcha) {
-                                       Recaptcha.reload();
                                }
-                               
-                               this._guestDialog.find('input[type="submit"]').enable();
-                               
-                               this._guestDialog.wcfDialog('open');
-                       }
+                       });
+                       this._proxy.sendRequest();
                }
                else {
                        this._proxy.setOption('data', {
@@ -557,8 +540,8 @@ WCF.Comment.Handler = Class.extend({
        _success: function(data, textStatus, jqXHR) {
                switch (data.actionName) {
                        case 'addComment':
-                               if (data.returnValues.errors) {
-                                       this._handleGuestDialogErrors(data.returnValues.errors);
+                               if (data.returnValues.guestDialog) {
+                                       this._createGuestDialog(data.returnValues.guestDialog, data.returnValues.useCaptcha);
                                }
                                else {
                                        this._commentAdd.find('textarea').val('').blur().trigger('updateHeight');
@@ -571,8 +554,8 @@ WCF.Comment.Handler = Class.extend({
                        break;
                        
                        case 'addResponse':
-                               if (data.returnValues.errors) {
-                                       this._handleGuestDialogErrors(data.returnValues.errors);
+                               if (data.returnValues.guestDialog) {
+                                       this._createGuestDialog(data.returnValues.guestDialog, data.returnValues.useCaptcha);
                                }
                                else {
                                        var $comment = this._comments[data.returnValues.commentID];
@@ -609,7 +592,7 @@ WCF.Comment.Handler = Class.extend({
                        break;
                        
                        case 'getGuestDialog':
-                               this._createGuestDialog(data);
+                               this._createGuestDialog(data.returnValues.template, data.returnValues.useCaptcha);
                        break;
                }
                
@@ -716,27 +699,29 @@ WCF.Comment.Handler = Class.extend({
        },
        
        /**
-        * Creates the guest dialog based on the given return data from the AJAX
-        * request.
+        * Creates the guest dialog using the given template.
         * 
-        * @param       object          data
+        * @param       string          template
+        * @param       boolean         useCaptcha
         */
-       _createGuestDialog: function(data) {
-               this._guestDialog = $('<div id="commentAddGuestDialog" />').append(data.returnValues.template).hide().appendTo(document.body);
+       _createGuestDialog: function(template, useCaptcha) {
+               var $refreshGuestDialog = !!this._guestDialog;
+               if (!this._guestDialog) {
+                       this._guestDialog = $('<div id="commentAddGuestDialog" />').hide().appendTo(document.body);
+               }
+               
+               this._guestDialog.html(template);
+               this._guestDialog.data('useCaptcha', useCaptcha);
                
                // 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.
@@ -748,40 +733,6 @@ WCF.Comment.Handler = Class.extend({
                        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.
@@ -789,70 +740,22 @@ WCF.Comment.Handler = Class.extend({
         * @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();
-               }
+               var $requestData = {
+                       actionName: this._commentData.commentID ? 'addResponse' : 'addComment',
+                       className: 'wcf\\data\\comment\\CommentAction'
+               };
+               
+               var $data = this._commentData;
+               $data.username = this._guestDialog.find('input[name="username"]').val();
+               
+               $requestData.parameters = {
+                       data: $data
+               };
+               
+               $requestData = $.extend(WCF.System.Captcha.getData('commentAdd'), $requestData);
+               
+               this._proxy.setOption('data', $requestData);
+               this._proxy.sendRequest();
        },
        
        /**
index fe9cd49c67f4216c5e12bdf067b29dfc4550e0ad..66f6b82ab5200aed5d804a3c466d37a7bf77d1ef 100755 (executable)
@@ -6951,6 +6951,53 @@ WCF.System.ObjectStore = {
        }
 };
 
+/**
+ * Stores captcha callbacks used for captchas in AJAX contexts.
+ */
+WCF.System.Captcha = {
+       /**
+        * adds call
+        * @var object<function>
+        */
+       _captchas: { },
+       
+       /**
+        * Adds a callback for a certain captcha.
+        * 
+        * @param       string          captchaID
+        * @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;
+       },
+       
+       /**
+        * Returns the captcha data for the captcha with the given id.
+        * 
+        * @return      object
+        */
+       getData: function(captchaID) {
+               if (this._captchas[captchaID] === undefined) {
+                       console.debug('[WCF.System.Captcha] Unknow captcha id "' + captchaID + '"');
+                       return;
+               }
+               
+               return this._captchas[captchaID]();
+       },
+       
+       /**
+        * Removes the callback with the given captcha id.
+        */
+       removeCallback: function(captchaID) {
+               delete this._captchas[captchaID];
+       }
+};
+
 WCF.System.Page = { };
 
 WCF.System.Page.Multiple = Class.extend({
diff --git a/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php
new file mode 100644 (file)
index 0000000..afa092d
--- /dev/null
@@ -0,0 +1,178 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\captcha\question\CaptchaQuestionAction;
+use wcf\data\captcha\question\CaptchaQuestionEditor;
+use wcf\form\AbstractForm;
+use wcf\system\exception\UserInputException;
+use wcf\system\language\I18nHandler;
+use wcf\system\Regex;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Shows the form to create a new captcha question.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage acp.form
+ * @category   Community Framework
+ */
+class CaptchaQuestionAddForm extends AbstractForm {
+       /**
+        * @see \wcf\page\AbstractPage::$activeMenuItem
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.captcha.question.add';
+       
+       /**
+        * invalid regex in answers
+        * @var string
+        */
+       public $invalidRegex = '';
+       
+       /**
+        * 1 if the question is disabled
+        * @var integer
+        */
+       public $isDisabled = 0;
+       
+       /**
+        * @see \wcf\page\AbstractPage::$neededPermissions
+        */
+       public $neededPermissions = array('admin.display.canManageCaptchaQuestion');
+       
+       /**
+        * @see \wcf\page\IPage::assignVariables()
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               I18nHandler::getInstance()->assignVariables();
+               
+               WCF::getTPL()->assign(array(
+                       'action' => 'add',
+                       'isDisabled' => $this->isDisabled,
+                       'invalidRegex' => $this->invalidRegex
+               ));
+       }
+       
+       /**
+        * @see \wcf\form\IForm::readFormParameters()
+        */
+       public function readFormParameters() {
+               parent::readFormParameters();
+               
+               I18nHandler::getInstance()->readValues();
+               
+               if (isset($_POST['isDisabled'])) $this->isDisabled = 1;
+       }
+       
+       /**
+        * @see \wcf\page\IPage::readParameters()
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               I18nHandler::getInstance()->register('question');
+               I18nHandler::getInstance()->register('answers');
+       }
+       
+       /**
+        * @see \wcf\form\IForm::save()
+        */
+       public function save() {
+               parent::save();
+               
+               $this->objectAction = new CaptchaQuestionAction(array(), 'create', array(
+                       'data' => array_merge($this->additionalFields, array(
+                               'answers' => I18nHandler::getInstance()->isPlainValue('answers') ? I18nHandler::getInstance()->getValue('answers') : '',
+                               'isDisabled' => $this->isDisabled,
+                               'question' => I18nHandler::getInstance()->isPlainValue('question') ? I18nHandler::getInstance()->getValue('question') : ''
+                       ))
+               ));
+               $returnValues = $this->objectAction->executeAction();
+               $questionID = $returnValues['returnValues']->questionID;
+               
+               // set i18n values
+               $questionUpdates = array();
+               if (!I18nHandler::getInstance()->isPlainValue('question')) {
+                       I18nHandler::getInstance()->save('question', 'wcf.captcha.question.question.question'.$questionID, 'wcf.captcha.question', 1);
+                       
+                       $questionUpdates['question'] = 'wcf.captcha.question.question.question'.$questionID;
+               }
+               if (!I18nHandler::getInstance()->isPlainValue('answers')) {
+                       I18nHandler::getInstance()->save('answers', 'wcf.captcha.question.answers.question'.$questionID, 'wcf.captcha.question', 1);
+                       
+                       $questionUpdates['answers'] = 'wcf.captcha.question.answers.question'.$questionID;
+               }
+               
+               if (!empty($questionUpdates)) {
+                       $questionEditor = new CaptchaQuestionEditor($returnValues['returnValues']);
+                       $questionEditor->update($questionUpdates);
+               }
+               
+               $this->saved();
+               
+               // reset values
+               I18nHandler::getInstance()->reset();
+               $this->isDisabled = 0;
+               
+               // show success message
+               WCF::getTPL()->assign('success', true);
+       }
+       
+       /**
+        * @see \wcf\form\IForm::validate()
+        */
+       public function validate() {
+               parent::validate();
+               
+               // validate question
+               if (!I18nHandler::getInstance()->validateValue('question')) {
+                       if (I18nHandler::getInstance()->isPlainValue('question')) {
+                               throw new UserInputException('question');
+                       }
+                       else {
+                               throw new UserInputException('question', 'multilingual');
+                       }
+               }
+               
+               // validate answers
+               if (!I18nHandler::getInstance()->validateValue('answers')) {
+                       if (I18nHandler::getInstance()->isPlainValue('answers')) {
+                               throw new UserInputException('answers');
+                       }
+                       else {
+                               throw new UserInputException('answers', 'multilingual');
+                       }
+               }
+               
+               if (I18nHandler::getInstance()->isPlainValue('answers')) {
+                       $answers = explode("\n", StringUtil::unifyNewlines(I18nHandler::getInstance()->getValue('answers')));
+                       foreach ($answers as $answer) {
+                               if (mb_substr($answer, 0, 1) == '~' && mb_substr($answer, -1, 1) == '~') {
+                                       $regexLength = mb_strlen($answer) - 2;
+                                       if (!$regexLength || !Regex::compile(mb_substr($answer, 1, $regexLength))->isValid()) {
+                                               $this->invalidRegex = $answer;
+                                               
+                                               throw new UserInputException('answers', 'regexNotValid');
+                                       }
+                               }
+                       }
+               }
+               foreach (I18nHandler::getInstance()->getValues('answers') as $languageAnswers) {
+                       $answers = explode("\n", StringUtil::unifyNewlines($languageAnswers));
+                       foreach ($answers as $answer) {
+                               if (mb_substr($answer, 0, 1) == '~' && mb_substr($answer, -1, 1) == '~') {
+                                       $regexLength = mb_strlen($answer) - 2;
+                                       if (!$regexLength || !Regex::compile(mb_substr($answer, 1, $regexLength))->isValid()) {
+                                               $this->invalidRegex = $answer;
+                                               
+                                               throw new UserInputException('answers', 'regexNotValid');
+                                       }
+                               }
+                       }
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/form/CaptchaQuestionEditForm.class.php b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionEditForm.class.php
new file mode 100644 (file)
index 0000000..bbdd8aa
--- /dev/null
@@ -0,0 +1,118 @@
+<?php
+namespace wcf\acp\form;
+use wcf\data\captcha\question\CaptchaQuestion;
+use wcf\data\captcha\question\CaptchaQuestionAction;
+use wcf\data\captcha\question\CaptchaQuestionEditor;
+use wcf\form\AbstractForm;
+use wcf\system\exception\IllegalLinkException;
+use wcf\system\language\I18nHandler;
+use wcf\system\WCF;
+
+/**
+ * Shows the form to edit an existing captcha question.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage acp.form
+ * @category   Community Framework
+ */
+class CaptchaQuestionEditForm extends CaptchaQuestionAddForm {
+       /**
+        * @see \wcf\page\AbstractPage::$activeMenuItem
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.captcha';
+       
+       /**
+        * edited captcha question
+        * @var \wcf\data\captcha\question\CaptchaQuestion
+        */
+       public $captchaQuestion = null;
+       
+       /**
+        * id of the edited captcha question
+        * @var integer
+        */
+       public $captchaQuestionID = 0;
+       
+       /**
+        * @see \wcf\page\IPage::assignVariables()
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               I18nHandler::getInstance()->assignVariables(!empty($_POST));
+               
+               WCF::getTPL()->assign(array(
+                       'action' => 'edit',
+                       'captchaQuestion' => $this->captchaQuestion
+               ));
+       }
+       
+       /**
+        * @see \wcf\page\IPage::readData()
+        */
+       public function readData() {
+               parent::readData();
+               
+               if (empty($_POST)) {
+                       I18nHandler::getInstance()->setOptions('question', 1, $this->captchaQuestion->question, 'wcf.captcha.question.question.question\d+');
+                       I18nHandler::getInstance()->setOptions('answers', 1, $this->captchaQuestion->answers, 'wcf.captcha.question.question.answers\d+');
+                       
+                       $this->isDisabled = $this->captchaQuestion->isDisabled;
+               }
+       }
+       
+       /**
+        * @see \wcf\page\IPage::readParameters()
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if (isset($_REQUEST['id'])) $this->captchaQuestionID = intval($_REQUEST['id']);
+               $this->captchaQuestion = new CaptchaQuestion($this->captchaQuestionID);
+               if (!$this->captchaQuestion->questionID) {
+                       throw new IllegalLinkException();
+               }
+       }
+       
+       /**
+        * @see \wcf\form\IForm::save()
+        */
+       public function save() {
+               AbstractForm::save();
+               
+               if (I18nHandler::getInstance()->isPlainValue('question')) {
+                       if ($this->captchaQuestion->question == 'wcf.captcha.question.question.question'.$this->captchaQuestion->questionID) {
+                               I18nHandler::getInstance()->remove($this->captchaQuestion->question);
+                       }
+               }
+               else {
+                       I18nHandler::getInstance()->save('question', 'wcf.captcha.question.question.question'.$this->captchaQuestion->questionID, 'wcf.captcha.question', 1);
+               }
+               
+               if (I18nHandler::getInstance()->isPlainValue('answers')) {
+                       if ($this->captchaQuestion->answers == 'wcf.captcha.question.question.answers'.$this->captchaQuestion->questionID) {
+                               I18nHandler::getInstance()->remove($this->captchaQuestion->answers);
+                       }
+               }
+               else {
+                       I18nHandler::getInstance()->save('answers', 'wcf.captcha.question.question.answers'.$this->captchaQuestion->questionID, 'wcf.captcha.question', 1);
+               }
+               
+               $this->objectAction = new CaptchaQuestionAction(array($this->captchaQuestion), 'update', array(
+                       'data' => array_merge($this->additionalFields, array(
+                               'answers' => I18nHandler::getInstance()->isPlainValue('answers') ? I18nHandler::getInstance()->getValue('answers') : 'wcf.captcha.question.question.answers'.$this->captchaQuestion->questionID,
+                               'isDisabled' => $this->isDisabled,
+                               'question' => I18nHandler::getInstance()->isPlainValue('question') ? I18nHandler::getInstance()->getValue('question') : 'wcf.captcha.question.question.question'.$this->captchaQuestion->questionID
+                       ))
+               ));
+               $this->objectAction->executeAction();
+               
+               $this->saved();
+               
+               // show success message
+               WCF::getTPL()->assign('success', true);
+       }
+}
diff --git a/wcfsetup/install/files/lib/acp/page/CaptchaQuestionListPage.class.php b/wcfsetup/install/files/lib/acp/page/CaptchaQuestionListPage.class.php
new file mode 100644 (file)
index 0000000..064df47
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+namespace wcf\acp\page;
+use wcf\page\MultipleLinkPage;
+
+/**
+ * Lists the available captcha questions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage acp.page
+ * @category   Community Framework
+ */
+class CaptchaQuestionListPage extends MultipleLinkPage {
+       /**
+        * @see \wcf\page\AbstractPage::$activeMenuItem
+        */
+       public $activeMenuItem = 'wcf.acp.menu.link.captcha.question.list';
+       
+       /**
+        * @see \wcf\page\AbstractPage::$neededPermissions
+        */
+       public $neededPermissions = array('admin.captcha.canManageCaptchaQuestion');
+       
+       /**
+        * @see \wcf\page\MultipleLinkPage::$objectListClassName
+        */
+       public $objectListClassName = 'wcf\data\captcha\question\CaptchaQuestionList';
+       
+       /**
+        * @see \wcf\page\MultipleLinkPage::$sortField
+        */
+       public $sortField = 'questionID';
+       
+       /**
+        * @see \wcf\page\MultipleLinkPage::$sortOrder
+        */
+       public $sortOrder = 'ASC';
+}
index d26e00ba6911b59d25a20508f9e5b2e167c80f21..a82912943c870edef7f4e6aff7233eb3fae7c573 100644 (file)
@@ -116,6 +116,7 @@ class FacebookAuthAction extends AbstractAction {
                                        WCF::getSession()->register('__facebookData', $userData);
                                        
                                        // we assume that bots won't register on facebook first
+                                       // todo: captcha
                                        WCF::getSession()->register('recaptchaDone', true);
                                        
                                        WCF::getSession()->update();
index fec9eca2c6c50f9bead3fbadec9c61a8afc40a93..bcf0c93f2bb77ca6f40f3ef42f21da1f4f3f1dac 100644 (file)
@@ -148,6 +148,7 @@ class GithubAuthAction extends AbstractAction {
                                        WCF::getSession()->register('__githubToken', $data['access_token']);
                                        
                                        // we assume that bots won't register on github first
+                                       // todo: captcha
                                        WCF::getSession()->register('recaptchaDone', true);
                                        
                                        WCF::getSession()->update();
index e5708e3543ac756a720e12b319358fabb53dbea5..62042513c5ce1051ba17f94dfdd5820a437e4ce7 100644 (file)
@@ -129,6 +129,7 @@ class GoogleAuthAction extends AbstractAction {
                                        WCF::getSession()->register('__googleData', $userData);
                                        
                                        // we assume that bots won't register on facebook first
+                                       // todo: captcha
                                        WCF::getSession()->register('recaptchaDone', true);
                                        
                                        WCF::getSession()->update();
index 74ba55fd5b2a671ff57b416cd7ed1226a92c369e..fd40437a885ca80e85b11ef19520364cf9a050f6 100644 (file)
@@ -128,6 +128,7 @@ class TwitterAuthAction extends AbstractAction {
                                        WCF::getSession()->register('__twitterData', $data);
                                        
                                        // we assume that bots won't register on twitter first
+                                       // todo: captcha
                                        WCF::getSession()->register('recaptchaDone', true);
                                        
                                        WCF::getSession()->update();
diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php
new file mode 100644 (file)
index 0000000..98781eb
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+namespace wcf\data\captcha\question;
+use wcf\data\DatabaseObject;
+use wcf\system\Regex;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Represents a captcha question.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.captcha.question
+ * @category   Community Framework
+ */
+class CaptchaQuestion extends DatabaseObject {
+       /**
+        * @see \wcf\data\DatabaseObject::$databaseTableName
+        */
+       protected static $databaseTableName = 'captcha_question';
+       
+       /**
+        * @see \wcf\data\DatabaseObject::$databaseTableIndexName
+        */
+       protected static $databaseTableIndexName = 'questionID';
+       
+       /**
+        * Returns true if the given user input is an answer to this question.
+        * 
+        * @param       string          $answer
+        * @return      boolean
+        */
+       public function isAnswer($answer) {
+               $answers = explode("\n", StringUtil::unifyNewlines(WCF::getLanguage()->get($this->answers)));
+               foreach ($answers as $__answer) {
+                       if (mb_substr($__answer, 0, 1) == '~' && mb_substr($__answer, -1, 1) == '~') {
+                               if (Regex::compile(mb_substr($__answer, 1, mb_strlen($__answer) - 2))->match($answer)) {
+                                       return true;
+                               }
+                               
+                               continue;
+                       }
+                       else if ($__answer == $answer) {
+                               return true;
+                       }
+               }
+               
+               return false;
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php
new file mode 100644 (file)
index 0000000..6382a23
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+namespace wcf\data\captcha\question;
+use wcf\data\AbstractDatabaseObjectAction;
+use wcf\data\IToggleAction;
+
+/**
+ * Executes captcha question-related actions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.captcha.question
+ * @category   Community Framework
+ */
+class CaptchaQuestionAction extends AbstractDatabaseObjectAction implements IToggleAction {
+       /**
+        * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsDelete
+        */
+       protected $permissionsDelete = array('admin.captcha.canManageCaptchaQuestion');
+       
+       /**
+        * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsUpdate
+       */
+       protected $permissionsUpdate = array('admin.captcha.canManageCaptchaQuestion');
+       
+       /**
+        * @see \wcf\data\IToggleAction::toggle()
+        */
+       public function toggle() {
+               foreach ($this->objects as $question) {
+                       $question->update(array(
+                               'isDisabled' => $question->isDisabled ? 0 : 1
+                       ));
+               }
+       }
+       
+       /**
+        * @see \wcf\data\IToggleAction::validateToggle()
+        */
+       public function validateToggle() {
+               parent::validateUpdate();
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionEditor.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionEditor.class.php
new file mode 100644 (file)
index 0000000..0cd804f
--- /dev/null
@@ -0,0 +1,29 @@
+<?php
+namespace wcf\data\captcha\question;
+use wcf\data\DatabaseObjectEditor;
+use wcf\data\IEditableCachedObject;
+use wcf\system\cache\builder\CaptchaQuestionCacheBuilder;
+
+/**
+ * Provides functions to edit captcha questions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.captcha.question
+ * @category   Community Framework
+ */
+class CaptchaQuestionEditor extends DatabaseObjectEditor implements IEditableCachedObject {
+       /**
+        * @see \wcf\data\DatabaseObjectDecorator::$baseClass
+        */
+       protected static $baseClass = 'wcf\data\captcha\question\CaptchaQuestion';
+       
+       /**
+        * @see \wcf\data\IEditableCachedObject::resetCache()
+        */
+       public static function resetCache() {
+               CaptchaQuestionCacheBuilder::getInstance()->reset();
+       }
+}
diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php
new file mode 100644 (file)
index 0000000..a97fd7d
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+namespace wcf\data\captcha\question;
+use wcf\data\DatabaseObjectList;
+
+/**
+ * Represents a list of captcha questions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage data.captcha.question
+ * @category   Community Framework
+ */
+class CaptchaQuestionList extends DatabaseObjectList { }
index 70af4a95cf31984213c4b7f14631bbcfa0e94886..37a592a68f75b02ae4a323d4b751c84ddcddaa78 100644 (file)
@@ -8,11 +8,11 @@ use wcf\data\comment\response\StructuredCommentResponse;
 use wcf\data\object\type\ObjectTypeCache;
 use wcf\data\user\UserProfile;
 use wcf\data\AbstractDatabaseObjectAction;
+use wcf\system\captcha\CaptchaHandler;
 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;
@@ -37,6 +37,12 @@ class CommentAction extends AbstractDatabaseObjectAction {
         */
        protected $allowGuestAccess = array('addComment', 'addResponse', 'loadComments', 'getGuestDialog');
        
+       /**
+        * captcha object type used for comments
+        * @var \wcf\data\object\type\ObjectType
+        */
+       public $captchaObjectType = null;
+       
        /**
         * @see \wcf\data\AbstractDatabaseObjectAction::$className
         */
@@ -184,7 +190,7 @@ class CommentAction extends AbstractDatabaseObjectAction {
                $this->readInteger('objectID', false, 'data');
                
                $this->validateUsername();
-               $this->validateRecaptcha();
+               $this->validateCaptcha();
                
                $this->validateMessage();
                $objectType = $this->validateObjectType();
@@ -203,8 +209,15 @@ class CommentAction extends AbstractDatabaseObjectAction {
         */
        public function addComment() {
                if (!empty($this->validationErrors)) {
+                       if (!empty($this->parameters['data']['username'])) {
+                               WCF::getSession()->register('username', $this->parameters['data']['username']);
+                       }
+                       WCF::getTPL()->assign('errorType', $this->validationErrors);
+                       
+                       $guestDialog = $this->getGuestDialog();
                        return array(
-                               'errors' => $this->validationErrors
+                               'useCaptcha' => $guestDialog['useCaptcha'],
+                               'guestDialog' => $guestDialog['template']
                        );
                }
                
@@ -247,8 +260,10 @@ class CommentAction extends AbstractDatabaseObjectAction {
                        // 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');
+                       // reset captcha for future requests
+                       if ($this->captchaObjectType) {
+                               $this->captchaObjectType->getProcessor()->reset();
+                       }
                }
                
                return array(
@@ -266,7 +281,7 @@ class CommentAction extends AbstractDatabaseObjectAction {
                $this->validateMessage();
                
                $this->validateUsername();
-               $this->validateRecaptcha();
+               $this->validateCaptcha();
                
                // validate comment id
                $this->validateCommentID();
@@ -287,8 +302,15 @@ class CommentAction extends AbstractDatabaseObjectAction {
         */
        public function addResponse() {
                if (!empty($this->validationErrors)) {
+                       if (!empty($this->parameters['data']['username'])) {
+                               WCF::getSession()->register('username', $this->parameters['data']['username']);
+                       }
+                       WCF::getTPL()->assign('errorType', $this->validationErrors);
+                       
+                       $guestDialog = $this->getGuestDialog();
                        return array(
-                               'errors' => $this->validationErrors
+                               'useCaptcha' => $guestDialog['useCaptcha'],
+                               'guestDialog' => $guestDialog['template']
                        );
                }
                
@@ -349,8 +371,10 @@ class CommentAction extends AbstractDatabaseObjectAction {
                        // 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');
+                       // reset captcha for future requests
+                       if ($this->captchaObjectType) {
+                               $this->captchaObjectType->getProcessor()->reset();
+                       }
                }
                
                return array(
@@ -553,11 +577,23 @@ class CommentAction extends AbstractDatabaseObjectAction {
         * @return      array
         */
        public function getGuestDialog() {
-               RecaptchaHandler::getInstance()->assignVariables();
+               if (MESSAGE_CAPTCHA_TYPE) {
+                       $captchaObjectType = CaptchaHandler::getInstance()->getObjectTypeByName(MESSAGE_CAPTCHA_TYPE);
+                       if ($captchaObjectType === null) {
+                               throw new SystemException("Unknown captcha object type with name '".MESSAGE_CAPTCHA_TYPE."'");
+                       }
+                       
+                       if (!$captchaObjectType->getProcessor()->isAvailable()) {
+                               $captchaObjectType = null;
+                       }
+               }
                
                return array(
+                       'useCaptcha' => $captchaObjectType !== null,
                        'template' => WCF::getTPL()->fetch('commentAddGuestDialog', 'wcf', array(
-                               'ajaxRecaptcha' => true,
+                               'ajaxCaptcha' => true,
+                               'captchaID' => 'commentAdd',
+                               'captchaObjectType' => $captchaObjectType,
                                'username' => WCF::getSession()->getVar('username')
                        ))
                );
@@ -679,29 +715,39 @@ class CommentAction extends AbstractDatabaseObjectAction {
                        }
                }
                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());
-                       }
+                       $this->validationErrors['username'] = $e->getType();
                }
        }
-
+       
        /**
-        * Validates the recaptcha challenge.
+        * Validates the captcha challenge.
         */
-       protected function validateRecaptcha() {
-               if (WCF::getUser()->userID || !MODULE_SYSTEM_RECAPTCHA || WCF::getSession()->getVar('recaptchaDone')) return;
+       protected function validateCaptcha() {
+               if (WCF::getUser()->userID) return;
                
-               $this->readString('recaptchaChallenge');
-               $this->readString('recaptchaResponse');
+               if (MESSAGE_CAPTCHA_TYPE) {
+                       $this->captchaObjectType = CaptchaHandler::getInstance()->getObjectTypeByName(MESSAGE_CAPTCHA_TYPE);
+                       if ($this->captchaObjectType === null) {
+                               throw new SystemException("Unknown captcha object type with name '".MESSAGE_CAPTCHA_TYPE."'");
+                       }
+                       
+                       if (!$this->captchaObjectType->getProcessor()->isAvailable()) {
+                               $this->captchaObjectType = null;
+                       }
+               }
+               
+               if ($this->captchaObjectType === null) return;
                
                try {
-                       RecaptchaHandler::getInstance()->validate($this->parameters['recaptchaChallenge'], $this->parameters['recaptchaResponse']);
+                       $this->captchaObjectType->getProcessor()->readFormParameters();
+                       $this->captchaObjectType->getProcessor()->validate();
                }
                catch (UserInputException $e) {
-                       $this->validationErrors['recaptcha'] = WCF::getLanguage()->get('wcf.recaptcha.error.recaptchaString.false');
+                       $this->validationErrors = array_merge($this->validationErrors,
+                               array(
+                                       $e->getField() => $e->getType()
+                               )
+                       );
                }
        }
        
diff --git a/wcfsetup/install/files/lib/form/AbstractCaptchaForm.class.php b/wcfsetup/install/files/lib/form/AbstractCaptchaForm.class.php
new file mode 100644 (file)
index 0000000..ad3d784
--- /dev/null
@@ -0,0 +1,147 @@
+<?php
+namespace wcf\form;
+use wcf\system\captcha\CaptchaHandler;
+use wcf\system\exception\SystemException;
+use wcf\system\recaptcha\RecaptchaHandler;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Abstract implementation of a form using captcha.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage form
+ * @category   Community Framework
+ */
+abstract class AbstractCaptchaForm extends AbstractForm {
+       /**
+        * captcha object type object
+        * @var \wcf\data\object\type\ObjectType
+        */
+       public $captchaObjectType = null;
+       
+       /**
+        * name of the captcha object type; if empty, captcha is disabled
+        * @var string
+        */
+       public $captchaObjectTypeName = '';
+       
+       /**
+        * challenge (legacy property from RecaptchaForm, do not use!)
+        * @var string
+        */
+       public $challenge = '';
+       
+       /**
+        * response (legacy property from RecaptchaForm, do not use!)
+        * @var string
+        */
+       public $response = '';
+       
+       /**
+        * true if recaptcha is used (legacy property from RecaptchaForm, do not use!)
+        * @var boolean
+        */
+       public $useCaptcha = true;
+       
+       /**
+        * @see \wcf\page\IPage::assignVariables()
+        */
+       public function assignVariables() {
+               parent::assignVariables();
+               
+               WCF::getTPL()->assign(array(
+                       'captchaObjectType' => $this->captchaObjectType
+               ));
+               
+               if (!$this->captchaObjectType) {
+                       RecaptchaHandler::getInstance()->assignVariables();
+                       WCF::getTPL()->assign(array(
+                               'useCaptcha' => $this->useCaptcha
+                       ));
+               }
+       }
+       
+       /**
+        * @see \wcf\page\IPage::readData()
+        */
+       public function readData() {
+               if ($this->captchaObjectTypeName) {
+                       $this->captchaObjectType = CaptchaHandler::getInstance()->getObjectTypeByName($this->captchaObjectTypeName);
+                       if ($this->captchaObjectType === null) {
+                               throw new SystemException("Unknown captcha object type with name '".$this->captchaObjectTypeName."'");
+                       }
+                       
+                       if (!$this->captchaObjectType->getProcessor()->isAvailable()) {
+                               $this->captchaObjectType = null;
+                       }
+               }
+               
+               parent::readData();
+       }
+       
+       /**
+        * @see \wcf\form\IForm::readFormParameters()
+        */
+       public function readFormParameters() {
+               parent::readFormParameters();
+               
+               if ($this->captchaObjectType) {
+                       $this->captchaObjectType->getProcessor()->readFormParameters();
+               }
+               else if ($this->useCaptcha) {
+                       if (isset($_POST['recaptcha_challenge_field'])) $this->challenge = StringUtil::trim($_POST['recaptcha_challenge_field']);
+                       if (isset($_POST['recaptcha_response_field'])) $this->response = StringUtil::trim($_POST['recaptcha_response_field']);
+               }
+       }
+       
+       /**
+        * @see \wcf\page\IPage::readParameters()
+        */
+       public function readParameters() {
+               parent::readParameters();
+               
+               if ($this->captchaObjectType === null && (!MODULE_SYSTEM_RECAPTCHA || WCF::getUser()->userID || WCF::getSession()->getVar('recaptchaDone'))) {
+                       $this->useCaptcha = false;
+               }
+       }
+       
+       /**
+        * @see \wcf\form\IForm::save()
+        */
+       public function save() {
+               parent::save();
+               
+               if ($this->captchaObjectType) {
+                       $this->captchaObjectType->getProcessor()->reset();
+               }
+               else {
+                       WCF::getSession()->unregister('recaptchaDone');
+               }
+       }
+       
+       /**
+        * @see \wcf\form\IForm::validate()
+        */
+       public function validate() {
+               parent::validate();
+               
+               $this->validateCaptcha();
+       }
+       
+       /**
+        * Validates the captcha.
+        */
+       protected function validateCaptcha() {
+               if ($this->captchaObjectType) {
+                       $this->captchaObjectType->getProcessor()->validate();
+               }
+               else if ($this->useCaptcha) {
+                       RecaptchaHandler::getInstance()->validate($this->challenge, $this->response);
+                       $this->useCaptcha = false;
+               }
+       }
+}
index a5355dc2f32eb7c51857de14812e31e755e0dc47..f0eec4d85fabd8b7d0caec6934282c0ccc1083c5 100644 (file)
@@ -20,7 +20,7 @@ use wcf\util\StringUtil;
  * @subpackage form
  * @category   Community Framework
  */
-class LostPasswordForm extends RecaptchaForm {
+class LostPasswordForm extends AbstractCaptchaForm {
        const AVAILABLE_DURING_OFFLINE_MODE = true;
        
        /**
@@ -47,9 +47,9 @@ class LostPasswordForm extends RecaptchaForm {
        public $user;
        
        /**
-        * @see \wcf\form\RecaptchaForm::$useCaptcha
+        * @see \wcf\form\CaptchaForm::$captchaObjectTypeName
         */
-       public $useCaptcha = LOST_PASSWORD_USE_CAPTCHA;
+       public $captchaObjectTypeName = LOST_PASSWORD_CAPTCHA_TYPE;
        
        /**
         * @see \wcf\form\IForm::readFormParameters()
index dc801ae8227eb141f0e650af17bda30368e836b6..e1dc8ab83b1278a997cc8ab1d91fb83eacb3bf81 100644 (file)
@@ -21,11 +21,11 @@ use wcf\util\UserUtil;
  * @subpackage form
  * @category   Community Framework
  */
-class MailForm extends RecaptchaForm {
+class MailForm extends AbstractCaptchaForm {
        /**
-        * @see \wcf\form\RecaptchaForm::$useCaptcha
+        * @see \wcf\form\AbstractCaptchaForm::$captchaObjectTypeName
         */
-       public $useCaptcha = PROFILE_MAIL_USE_CAPTCHA;
+       public $captchaObjectTypeName = PROFILE_MAIL_CAPTCHA_TYPE;
        
        /**
         * recipient's user id
index 964dc8ddefae5086b23d965bc61f46f9bc88c74a..260966242fc240331241e0d107a734b8e1d5835d 100644 (file)
@@ -23,7 +23,7 @@ use wcf\util\StringUtil;
  * @subpackage form
  * @category   Community Framework
  */
-abstract class MessageForm extends RecaptchaForm {
+abstract class MessageForm extends AbstractCaptchaForm {
        /**
         * name of the permission which contains the allowed BBCodes
         * @var string
index d12c32ca1c44b7fef9577f3b223f329d475865b3..12e49384a3912d64da8b9f151e539769514c479c 100644 (file)
@@ -13,6 +13,7 @@ use wcf\util\StringUtil;
  * @package    com.woltlab.wcf
  * @subpackage form
  * @category   Community Framework
+ * @deprecated since 2.1
  */
 abstract class RecaptchaForm extends AbstractForm {
        /**
index 81f9c98dd1f63475be04aebc4366946175ec3d31..17f2dac5d068e0c964c38291585724452ac6772d 100644 (file)
@@ -9,12 +9,13 @@ use wcf\data\user\UserAction;
 use wcf\data\user\UserEditor;
 use wcf\data\user\UserProfile;
 use wcf\data\user\UserProfileAction;
+use wcf\system\captcha\CaptchaHandler;
 use wcf\system\exception\NamedUserException;
 use wcf\system\exception\PermissionDeniedException;
+use wcf\system\exception\SystemException;
 use wcf\system\exception\UserInputException;
 use wcf\system\language\LanguageFactory;
 use wcf\system\mail\Mail;
-use wcf\system\recaptcha\RecaptchaHandler;
 use wcf\system\request\LinkHandler;
 use wcf\system\user\authentication\UserAuthenticationFactory;
 use wcf\system\Regex;
@@ -34,12 +35,6 @@ use wcf\util\UserRegistrationUtil;
  * @category   Community Framework
  */
 class RegisterForm extends UserAddForm {
-       /**
-        * recaptcha challenge
-        * @var string
-        */
-       public $challenge = '';
-       
        /**
         * @see \wcf\page\AbstractPage::$enableTracking
         */
@@ -64,16 +59,16 @@ class RegisterForm extends UserAddForm {
        public $message = '';
        
        /**
-        * recaptcha response
-        * @var string
+        * captcha object type object
+        * @var \wcf\data\object\type\ObjectType
         */
-       public $response = '';
+       public $captchaObjectType = null;
        
        /**
-        * enable recaptcha
-        * @var boolean
+        * name of the captcha object type; if empty, captcha is disabled
+        * @var string
         */
-       public $useCaptcha = REGISTER_USE_CAPTCHA;
+       public $captchaObjectTypeName = REGISTER_CAPTCHA_TYPE;
        
        /**
         * field names
@@ -109,10 +104,6 @@ class RegisterForm extends UserAddForm {
                        exit;
                }
                
-               if (!MODULE_SYSTEM_RECAPTCHA || WCF::getSession()->getVar('recaptchaDone')) {
-                       $this->useCaptcha = false;
-               }
-               
                if (WCF::getSession()->getVar('__3rdPartyProvider')) {
                        $this->isExternalAuthentication = true;
                }
@@ -140,8 +131,10 @@ class RegisterForm extends UserAddForm {
                if (isset($_POST[$this->randomFieldNames['confirmPassword']])) $this->confirmPassword = $_POST[$this->randomFieldNames['confirmPassword']];
                
                $this->groupIDs = array();
-               if (isset($_POST['recaptcha_challenge_field'])) $this->challenge = StringUtil::trim($_POST['recaptcha_challenge_field']);
-               if (isset($_POST['recaptcha_response_field'])) $this->response = StringUtil::trim($_POST['recaptcha_response_field']);
+               
+               if ($this->captchaObjectType) {
+                       $this->captchaObjectType->getProcessor()->readFormParameters();
+               }
        }
        
        /**
@@ -157,9 +150,7 @@ class RegisterForm extends UserAddForm {
         */
        public function validate() {
                // validate captcha first
-               if ($this->useCaptcha) {
-                       $this->validateCaptcha();
-               }
+               $this->validateCaptcha();
                
                parent::validate();
                
@@ -173,6 +164,17 @@ class RegisterForm extends UserAddForm {
         * @see \wcf\page\IPage::readData()
         */
        public function readData() {
+               if ($this->captchaObjectTypeName) {
+                       $this->captchaObjectType = CaptchaHandler::getInstance()->getObjectTypeByName($this->captchaObjectTypeName);
+                       if ($this->captchaObjectType === null) {
+                               throw new SystemException("Unknown captcha object type with id '".$this->captchaObjectTypeName."'");
+                       }
+                       
+                       if (!$this->captchaObjectType->getProcessor()->isAvailable()) {
+                               $this->captchaObjectType = null;
+                       }
+               }
+               
                parent::readData();
                
                if (empty($_POST)) {
@@ -215,10 +217,9 @@ class RegisterForm extends UserAddForm {
        public function assignVariables() {
                parent::assignVariables();
                
-               RecaptchaHandler::getInstance()->assignVariables();
                WCF::getTPL()->assign(array(
+                       'captchaObjectType' => $this->captchaObjectType,
                        'isExternalAuthentication' => $this->isExternalAuthentication,
-                       'useCaptcha' => $this->useCaptcha,
                        'randomFieldNames' => $this->randomFieldNames
                ));
        }
@@ -234,10 +235,9 @@ class RegisterForm extends UserAddForm {
         * Validates the captcha.
         */
        protected function validateCaptcha() {
-               if ($this->useCaptcha) {
+               if ($this->captchaObjectType) {
                        try {
-                               RecaptchaHandler::getInstance()->validate($this->challenge, $this->response);
-                               $this->useCaptcha = false;
+                               $this->captchaObjectType->getProcessor()->validate();
                        }
                        catch (UserInputException $e) {
                                $this->errorType[$e->getField()] = $e->getType();
@@ -487,9 +487,12 @@ class RegisterForm extends UserAddForm {
                        $mail->send();
                }
                
+               if ($this->captchaObjectType) {
+                       $this->captchaObjectType->getProcessor()->reset();
+               }
+               
                // login user
                UserAuthenticationFactory::getInstance()->getUserAuthentication()->storeAccessData($user, $this->username, $this->password);
-               WCF::getSession()->unregister('recaptchaDone');
                WCF::getSession()->unregister('registrationRandomFieldNames');
                WCF::getSession()->unregister('registrationStartTime');
                $this->saved();
index 03104a0b264fde7391cc399703ef7463380d9b5e..e5dc4d836ddb93e8244b4379f1f035a2b2f4dc7b 100644 (file)
@@ -27,7 +27,7 @@ use wcf\util\StringUtil;
  * @subpackage form
  * @category   Community Framework
  */
-class SearchForm extends RecaptchaForm {
+class SearchForm extends AbstractCaptchaForm {
        /**
         * list of additional conditions
         * @var array<string>
@@ -93,9 +93,9 @@ class SearchForm extends RecaptchaForm {
        public $username = '';
        
        /**
-        * @see \wcf\form\RecaptchaForm::$useCaptcha
+        * @see \wcf\form\AbstractCaptchaForm::$captchaObjectTypeName
         */
-       public $useCaptcha = SEARCH_USE_CAPTCHA;
+       public $captchaObjectTypeName = SEARCH_CAPTCHA_TYPE;
        
        /**
         * parameters used for previous search
@@ -473,7 +473,7 @@ class SearchForm extends RecaptchaForm {
         */
        public function getUserIDs() {
                $userIDs = array();
-                       
+               
                // username
                if (!empty($this->username)) {
                        $sql = "SELECT  userID
index 62a5a1612e385ab2f14e5497d869609a700a541c..f1fb11f74789e661791b7aeb485a374f418e6042 100644 (file)
@@ -44,11 +44,6 @@ class SignatureEditForm extends MessageForm {
         */
        public $signatureCache = null;
        
-       /**
-        * @see \wcf\form\RecaptchaForm::$useCaptcha
-        */
-       public $useCaptacha = false;
-       
        /**
         * @see \wcf\form\MessageForm::$allowedBBCodesPermission
         */
diff --git a/wcfsetup/install/files/lib/system/cache/builder/CaptchaQuestionCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/CaptchaQuestionCacheBuilder.class.php
new file mode 100644 (file)
index 0000000..d93208b
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+namespace wcf\system\cache\builder;
+use wcf\data\captcha\question\CaptchaQuestionList;
+
+/**
+ * Caches the enabled captcha questions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.cache.builder
+ * @category   Community Framework
+ */
+class CaptchaQuestionCacheBuilder extends AbstractCacheBuilder {
+       /**
+        * @see \wcf\system\cache\builder\AbstractCacheBuilder::rebuild()
+        */
+       public function rebuild(array $parameters) {
+               $questionList = new CaptchaQuestionList();
+               $questionList->getConditionBuilder()->add('isDisabled = ?', array(0));
+               $questionList->readObjects();
+               
+               return $questionList->getObjects();
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/captcha/CaptchaHandler.class.php b/wcfsetup/install/files/lib/system/captcha/CaptchaHandler.class.php
new file mode 100644 (file)
index 0000000..1220873
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+namespace wcf\system\captcha;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\SingletonFactory;
+use wcf\system\WCF;
+
+/**
+ * Handles captchas.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.captcha
+ * @category   Community Framework
+ */
+class CaptchaHandler extends SingletonFactory {
+       /**
+        * Returns the available captcha types for selection.
+        * 
+        * @return      array<string>
+        */
+       public function getCaptchaSelection() {
+               $selection = array();
+               foreach ($this->objectTypes as $objectType) {
+                       if ($objectType->getProcessor()->isAvailable()) {
+                               $selection[$objectType->objectType] = WCF::getLanguage()->get('wcf.captcha.'.$objectType->objectType);
+                       }
+               }
+               
+               return $selection;
+       }
+       
+       /**
+        * Returns the captcha object type with the given id or null if no such
+        * object type exists.
+        * 
+        * @param       integer         $objectTypeID
+        * @return      \wcf\data\object\type\ObjectType
+        */
+       public function getObjectType($objectTypeID) {
+               if (isset($this->objectTypes[$objectTypeID])) {
+                       return $this->objectTypes[$objectTypeID];
+               }
+               
+               return null;
+       }
+       
+       /**
+        * Returns the captcha object type with the given name or null if no such
+        * object type exists.
+        * 
+        * @param       string          $objectType
+        * @return      \wcf\data\object\type\ObjectType
+        */
+       public function getObjectTypeByName($objectType) {
+               return ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.captcha', $objectType);
+       }
+       
+       /**
+        * @see \wcf\system\SingletonFactory::init()
+        */
+       protected function init() {
+               $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.captcha');
+               foreach ($objectTypes as $objectType) {
+                       $this->objectTypes[$objectType->objectTypeID] = $objectType;
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/captcha/CaptchaQuestionHandler.class.php b/wcfsetup/install/files/lib/system/captcha/CaptchaQuestionHandler.class.php
new file mode 100644 (file)
index 0000000..a22db58
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+namespace wcf\system\captcha;
+use wcf\system\cache\builder\CaptchaQuestionCacheBuilder;
+use wcf\system\exception\UserInputException;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Captcha handler for captcha questions.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.captcha
+ * @category   Community Framework
+ */
+class CaptchaQuestionHandler implements ICaptchaHandler {
+       /**
+        * answer to the captcha question
+        * @var string
+        */
+       protected $captchaAnswer = '';
+       
+       /**
+        * unique identifier of the captcha question
+        * @var string
+        */
+       protected $captchaQuestion = '';
+       
+       /**
+        * captcha question to answer
+        * @var \wcf\data\captcha\question\CaptchaQuestion
+        */
+       protected $question = null;
+       
+       /**
+        * Creates a new instance of CaptchaQuestionHandler.
+        */
+       public function __construct() {
+               $this->questions = CaptchaQuestionCacheBuilder::getInstance()->getData();
+       }
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::isAvailable()
+        */
+       public function isAvailable() {
+               return count($this->questions) > 0;
+       }
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::getFormElement()
+        */
+       public function getFormElement() {
+               if ($this->question === null) {
+                       $this->readCaptchaQuestion();
+               }
+               
+               return WCF::getTPL()->fetch('captchaQuestion', 'wcf', array(
+                       'captchaQuestion' => $this->captchaQuestion,
+                       'captchaQuestionAnswered' => WCF::getSession()->getVar('captchaQuestionSolved_'.$this->captchaQuestion) !== null,
+                       'captchaQuestionObject' => $this->question
+               ));
+       }
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::readFormParameters()
+        */
+       public function readFormParameters() {
+               if (isset($_POST['captchaQuestion'])) $this->captchaQuestion = StringUtil::trim($_POST['captchaQuestion']);
+               if (isset($_POST['captchaAnswer'])) $this->captchaAnswer = StringUtil::trim($_POST['captchaAnswer']);
+       }
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::reset()
+        */
+       public function reset() {
+               WCF::getSession()->unregister('captchaQuestion_'.$this->captchaQuestion);
+               WCF::getSession()->unregister('captchaQuestionSolved_'.$this->captchaQuestion);
+       }
+       
+       /**
+        * Reads a random captcha question.
+        */
+       protected function readCaptchaQuestion() {
+               $questionID = array_rand($this->questions);
+               $this->question = $this->questions[$questionID];
+               
+               do {
+                       $this->captchaQuestion = StringUtil::getRandomID();
+               }
+               while (WCF::getSession()->getVar('captchaQuestion_'.$this->captchaQuestion) !== null);
+               
+               WCF::getSession()->register('captchaQuestion_'.$this->captchaQuestion, $questionID);
+       }
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::validate()
+        */
+       public function validate() {
+               $questionID = WCF::getSession()->getVar('captchaQuestion_'.$this->captchaQuestion);
+               
+               if ($questionID === null || !isset($this->questions[$questionID])) {
+                       throw new UserInputException('captchaQuestion');
+               }
+               
+               $this->question = $this->questions[$questionID];
+               
+               // check if question has already been answered
+               if (WCF::getSession()->getVar('captchaQuestionSolved_'.$this->captchaQuestion) !== null) return;
+               
+               if ($this->captchaAnswer == '') {
+                       throw new UserInputException('captchaAnswer');
+               }
+               else if (!$this->question->isAnswer($this->captchaAnswer)) {
+                       throw new UserInputException('captchaAnswer', 'false');
+               }
+               
+               WCF::getSession()->register('captchaQuestionSolved_'.$this->captchaQuestion, true);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/captcha/ICaptchaHandler.class.php b/wcfsetup/install/files/lib/system/captcha/ICaptchaHandler.class.php
new file mode 100644 (file)
index 0000000..8fa27e8
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+namespace wcf\system\captcha;
+
+/**
+ * Every captcha type has to implement this interface.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.captcha
+ * @category   Community Framework
+ */
+interface ICaptchaHandler {
+       /**
+        * Returns the form element.
+        * 
+        * @return      string
+        */
+       public function getFormElement();
+       
+       /**
+        * Returns true if this kind of captcha is available.
+        * 
+        * @return      boolean
+        */
+       public function isAvailable();
+       
+       /**
+        * Reads the parameters of the captcha form element.
+        */
+       public function readFormParameters();
+       
+       /**
+        * Resets the captcha after it is no longer needed.
+        */
+       public function reset();
+       
+       /**
+        * Validates the response to the challenge and marks the captcha as done.
+        */
+       public function validate();
+}
diff --git a/wcfsetup/install/files/lib/system/captcha/RecaptchaHandler.class.php b/wcfsetup/install/files/lib/system/captcha/RecaptchaHandler.class.php
new file mode 100644 (file)
index 0000000..42eb32c
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+namespace wcf\system\captcha;
+use wcf\system\WCF;
+use wcf\util\StringUtil;
+
+/**
+ * Captcha handler for reCAPTCHA.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.captcha
+ * @category   Community Framework
+ */
+class RecaptchaHandler implements ICaptchaHandler {
+       /**
+        * recaptcha challenge
+        * @var string
+        */
+       public $challenge = '';
+       
+       /**
+        * response to the challenge
+        * @var string
+        */
+       public $response = '';
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::getFormElement()
+        */
+       public function getFormElement() {
+               \wcf\system\recaptcha\RecaptchaHandler::getInstance()->assignVariables();
+               
+               return WCF::getTPL()->fetch('recaptcha');
+       }
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::isAvailable()
+        */
+       public function isAvailable() {
+               return MODULE_SYSTEM_RECAPTCHA && RECAPTCHA_PUBLICKEY && RECAPTCHA_PRIVATEKEY;
+       }
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::readFormParameters()
+        */
+       public function readFormParameters() {
+               if (isset($_POST['recaptcha_challenge_field'])) $this->challenge = StringUtil::trim($_POST['recaptcha_challenge_field']);
+               if (isset($_POST['recaptcha_response_field'])) $this->response = StringUtil::trim($_POST['recaptcha_response_field']);
+       }
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::reset()
+        */
+       public function reset() {
+               WCF::getSession()->unregister('recaptchaDone');
+       }
+       
+       /**
+        * @see \wcf\system\captcha\ICaptchaHandler::validate()
+        */
+       public function validate() {
+               \wcf\system\recaptcha\RecaptchaHandler::getInstance()->validate($this->challenge, $this->response);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/option/CaptchaSelectOptionType.class.php b/wcfsetup/install/files/lib/system/option/CaptchaSelectOptionType.class.php
new file mode 100644 (file)
index 0000000..e49febd
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+namespace wcf\system\option;
+use wcf\data\option\Option;
+use wcf\system\captcha\CaptchaHandler;
+use wcf\system\WCF;
+
+/**
+ * Option type implementation for selecting a captcha type.
+ * 
+ * @author     Matthias Schmidt
+ * @copyright  2001-2014 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage system.option
+ * @category   Community Framework
+ */
+class CaptchaSelectOptionType extends AbstractOptionType {
+       /**
+        * @see \wcf\system\option\IOptionType::getFormElement()
+        */
+       public function getFormElement(Option $option, $value) {
+               $selectOptions = CaptchaHandler::getInstance()->getCaptchaSelection();
+               if ($option->allowemptyvalue) {
+                       $selectOptions = array_merge(
+                               array(
+                                       '' => WCF::getLanguage()->get('wcf.captcha.useNoCaptcha')
+                               ),
+                               $selectOptions
+                       );
+               }
+               
+               return WCF::getTPL()->fetch('selectOptionType', 'wcf', array(
+                       'selectOptions' => $selectOptions,
+                       'option' => $option,
+                       'value' => $value
+               ));
+       }
+       
+       /**
+        * @see \wcf\system\option\IOptionType::validate()
+        */
+       public function validate(Option $option, $newValue) {
+               if (!$newValue) return;
+               
+               $selection = CaptchaHandler::getInstance()->getCaptchaSelection();
+               if (!isset($selection[$newValue])) {
+                       throw new UserInputException($option->optionName);
+               }
+       }
+}
index 4e69ff49affd7fff1b471f47545eb7599733b262..837593cd0b8e3e735f3eb19fb8039805d7b3e8d6 100644 (file)
@@ -174,7 +174,8 @@ class RecaptchaHandler extends SingletonFactory {
                WCF::getTPL()->assign(array(
                        'recaptchaLanguageCode' => $this->languageCode,
                        'recaptchaPublicKey' => $this->publicKey,
-                       'recaptchaUseSSL' => RouteHandler::secureConnection()
+                       'recaptchaUseSSL' => RouteHandler::secureConnection(),
+                       'recaptchaLegacyMode' => true
                ));
        }
 }
index 5dc43d9913c24482f7884ad9f04a00ac57dd0d04..3ba64121781c24bc70201b88269c6940525fdd7d 100644 (file)
                <item name="wcf.acp.cache.type.template"><![CDATA[Templates]]></item>
        </category>
        
+       <category name="wcf.acp.captcha">
+               <item name="wcf.acp.captcha.question.add"><![CDATA[Captcha-Frage hinzufügen]]></item>
+               <item name="wcf.acp.captcha.question.answers"><![CDATA[Antworten]]></item>
+               <item name="wcf.acp.captcha.question.answers.description"><![CDATA[Geben Sie pro Zeile eine mögliche Antwort ein. Antworten, die mit „~“ beginnen und enden, werden als reguläre Ausdrücke interpretiert.]]></item>
+               <item name="wcf.acp.captcha.question.answers.error.regexNotValid"><![CDATA[Der reguläre Ausdruck „{$invalidRegex}“ ist ungültig.]]></item>
+               <item name="wcf.acp.captcha.question.delete.confirmMessage"><![CDATA[Wollen Sie diese Frage wirklich löschen?]]></item>
+               <item name="wcf.acp.captcha.question.edit"><![CDATA[Captcha-Frage bearbeiten]]></item>
+               <item name="wcf.acp.captcha.question.isDisabled"><![CDATA[Fragen deaktivieren]]></item>
+               <item name="wcf.acp.captcha.question.list"><![CDATA[Captcha-Fragen]]></item>
+               <item name="wcf.acp.captcha.question.question"><![CDATA[Frage]]></item>
+       </category>
+       
        <category name="wcf.acp.cronjob">
                <item name="wcf.acp.cronjob.list"><![CDATA[Cronjobs]]></item>
                <item name="wcf.acp.cronjob.add"><![CDATA[Cronjob hinzufügen]]></item>
                <item name="wcf.acp.group.option.admin.content.ad.canManageAd"><![CDATA[Kann Werbung verwalten]]></item>
                <item name="wcf.acp.group.option.user.profile.aboutMeMaxLength"><![CDATA[Maximallänge „Über mich“]]></item>
                <item name="wcf.acp.group.option.user.profile.canReportContent"><![CDATA[Kann Inhalte melden]]></item>
+               <item name="wcf.acp.group.option.admin.captcha.canManageCaptchaQuestion"><![CDATA[Kann Captcha-Fragen verwalten]]></item>
+               <item name="wcf.acp.group.category.admin.display.captcha"><![CDATA[Captchas]]></item>
        </category>
        
        <category name="wcf.acp.index">
                <item name="wcf.acp.menu.link.ad"><![CDATA[Werbung]]></item>
                <item name="wcf.acp.menu.link.ad.add"><![CDATA[Werbung hinzufügen]]></item>
                <item name="wcf.acp.menu.link.ad.list"><![CDATA[Werbung auflisten]]></item>
+               <item name="wcf.acp.menu.link.captcha"><![CDATA[Captchas]]></item>
+               <item name="wcf.acp.menu.link.captcha.question.add"><![CDATA[Frage hinzufügen]]></item>
+               <item name="wcf.acp.menu.link.captcha.question.list"><![CDATA[Fragen auflisten]]></item>
        </category>
        
        <category name="wcf.acp.notice">
                <item name="wcf.acp.option.register_activation_method.byAdmin"><![CDATA[Aktivierung erfolgt durch Administrator]]></item>
                <item name="wcf.acp.option.register_activation_method.byUser"><![CDATA[Benutzer aktiviert sich durch E-Mail-Bestätigung]]></item>
                <item name="wcf.acp.option.register_activation_method.disabled"><![CDATA[Keine Aktivierung notwendig]]></item>
-               <item name="wcf.acp.option.register_use_captcha"><![CDATA[reCAPTCHA in Registrierung aktivieren]]></item>
-               <item name="wcf.acp.option.lost_password_use_captcha"><![CDATA[reCAPTCHA in „Kennwort vergessen“ aktivieren]]></item>
-               <item name="wcf.acp.option.profile_mail_use_captcha"><![CDATA[reCAPTCHA in „E-Mail an Benutzer schicken“ aktivieren]]></item>
                <item name="wcf.acp.option.signature_max_image_height"><![CDATA[Maximale Höhe von Signatur-Bildern]]></item>
                <item name="wcf.acp.option.user_title_max_length"><![CDATA[Maximale Länge des Benutzertitels]]></item>
                <item name="wcf.acp.option.user_forbidden_titles"><![CDATA[Reservierte Benutzertitel]]></item>
                <item name="wcf.acp.option.search_results_per_page"><![CDATA[Ergebnisse pro Seite]]></item>
                <item name="wcf.acp.option.search_default_sort_field"><![CDATA[Standardsortierung]]></item>
                <item name="wcf.acp.option.search_default_sort_order"><![CDATA[Standardreihenfolge]]></item>
-               <item name="wcf.acp.option.search_use_captcha"><![CDATA[reCAPTCHA in Suchfunktion aktivieren]]></item>
                <item name="wcf.acp.option.category.message.general.poll"><![CDATA[Umfragen]]></item>
                <item name="wcf.acp.option.module_poll"><![CDATA[Umfragen]]></item>
                <item name="wcf.acp.option.poll_max_options"><![CDATA[Maximale Anzahl an Antworten]]></item>
@@ -967,6 +980,12 @@ GmbH=Gesellschaft mit beschränkter Haftung]]></item>
                <item name="wcf.acp.option.url_omit_index_php"><![CDATA[„index.php“ aus URLs entfernen]]></item>
                <item name="wcf.acp.option.url_omit_index_php.description"><![CDATA[TODO]]></item>
                <item name="wcf.acp.option.module_ad"><![CDATA[Werbung]]></item>
+               <item name="wcf.acp.option.register_captcha_type"><![CDATA[Registierung]]></item>
+               <item name="wcf.acp.option.lost_password_captcha_type"><![CDATA[Kennwort vergessen]]></item>
+               <item name="wcf.acp.option.profile_mail_captcha_type"><![CDATA[E-Mail an Benutzer schicken]]></item>
+               <item name="wcf.acp.option.search_captcha_type"><![CDATA[Suche]]></item>
+               <item name="wcf.acp.option.message_captcha_type"><![CDATA[Nachrichten]]></item>
+               <item name="wcf.acp.option.category.security.antispam.captcha"><![CDATA[Captchas]]></item>
        </category>
        
        <category name="wcf.acp.package">
@@ -1606,6 +1625,18 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt
                <item name="wcf.bbcode.spoiler.text"><![CDATA[(Versteckter Text)]]></item>
        </category>
        
+       <category name="wcf.captcha">
+               <item name="wcf.captcha.useNoCaptcha"><![CDATA[(Kein Captcha)]]></item>
+               <item name="wcf.captcha.com.woltlab.wcf.recaptcha"><![CDATA[reCAPTCHA]]></item>
+               <item name="wcf.captcha.com.woltlab.wcf.captchaQuestion"><![CDATA[Frage]]></item>
+       </category>
+       
+       <category name="wcf.captcha.question">
+               <item name="wcf.captcha.question.answer.error.false"><![CDATA[Sie haben eine falsche Antwort angegeben.]]></item>
+               <item name="wcf.captcha.question.captcha"><![CDATA[Frage]]></item>
+               <item name="wcf.captcha.question.captcha.description"><![CDATA[Bitte beantworten Sie die untenstehende Frage bzw. lösen Sie die untenstehende Aufgabe.]]></item>
+       </category>
+       
        <category name="wcf.category">
                <item name="wcf.category.add"><![CDATA[Kategorie hinzufügen]]></item>
                <item name="wcf.category.button.list"><![CDATA[Kategorien auflisten]]></item>
index 16d57c15b326a53e753b92e50cff794e6350cae7..bf4341374751565d28b7f1f9bb5c05382c5f81f6 100644 (file)
@@ -140,6 +140,18 @@ Examples for medium ID detection:
                <item name="wcf.acp.cache.type.template"><![CDATA[Templates]]></item>
        </category>
        
+       <category name="wcf.acp.captcha">
+               <item name="wcf.acp.captcha.question.add"><![CDATA[Add Captcha Question]]></item>
+               <item name="wcf.acp.captcha.question.answers"><![CDATA[Answers]]></item>
+               <item name="wcf.acp.captcha.question.answers.description"><![CDATA[Enter one possible answer per line. Answers beginning and ending with “~” will be interpreted as regular expressions.]]></item>
+               <item name="wcf.acp.captcha.question.answers.error.regexNotValid"><![CDATA[The regular expression “{$invalidRegex}” is not valid.]]></item>
+               <item name="wcf.acp.captcha.question.delete.confirmMessage"><![CDATA[Do you really want to delete this question?]]></item>
+               <item name="wcf.acp.captcha.question.edit"><![CDATA[Edit Captcha Question]]></item>
+               <item name="wcf.acp.captcha.question.isDisabled"><![CDATA[Disable Question]]></item>
+               <item name="wcf.acp.captcha.question.list"><![CDATA[Captcha Questions]]></item>
+               <item name="wcf.acp.captcha.question.question"><![CDATA[Question]]></item>
+       </category>
+       
        <category name="wcf.acp.cronjob">
                <item name="wcf.acp.cronjob.list"><![CDATA[Cronjobs]]></item>
                <item name="wcf.acp.cronjob.add"><![CDATA[Add Cronjob]]></item>
@@ -419,6 +431,8 @@ Examples for medium ID detection:
                <item name="wcf.acp.group.option.admin.content.ad.canManageAd"><![CDATA[Can manage ads]]></item>
                <item name="wcf.acp.group.option.user.profile.aboutMeMaxLength"><![CDATA[“About Me” maximum length]]></item>
                <item name="wcf.acp.group.option.user.profile.canReportContent"><![CDATA[Can report content]]></item>
+               <item name="wcf.acp.group.option.admin.captcha.canManageCaptchaQuestion"><![CDATA[Can manage captcha questions]]></item>
+               <item name="wcf.acp.group.category.admin.display.captcha"><![CDATA[Captchas]]></item>
        </category>
        
        <category name="wcf.acp.index">
@@ -617,6 +631,9 @@ Examples for medium ID detection:
                <item name="wcf.acp.menu.link.ad"><![CDATA[Ad]]></item>
                <item name="wcf.acp.menu.link.ad.add"><![CDATA[Add Ad]]></item>
                <item name="wcf.acp.menu.link.ad.list"><![CDATA[List Ads]]></item>
+               <item name="wcf.acp.menu.link.captcha"><![CDATA[Captchas]]></item>
+               <item name="wcf.acp.menu.link.captcha.question.add"><![CDATA[Add Question]]></item>
+               <item name="wcf.acp.menu.link.captcha.question.list"><![CDATA[List Questions]]></item>
        </category>
        
        <category name="wcf.acp.notice">
@@ -880,9 +897,6 @@ Examples for medium ID detection:
                <item name="wcf.acp.option.register_activation_method.byAdmin"><![CDATA[Only administrators can approve]]></item>
                <item name="wcf.acp.option.register_activation_method.byUser"><![CDATA[Approval through email confirmation]]></item>
                <item name="wcf.acp.option.register_activation_method.disabled"><![CDATA[No approval required]]></item>
-               <item name="wcf.acp.option.register_use_captcha"><![CDATA[Enable reCAPTCHA protection during registration]]></item>
-               <item name="wcf.acp.option.lost_password_use_captcha"><![CDATA[Enable reCAPTCHA protection for “Lost Password”]]></item>
-               <item name="wcf.acp.option.profile_mail_use_captcha"><![CDATA[Enable reCAPTCHA protection for “Send Email to User”]]></item>
                <item name="wcf.acp.option.signature_max_image_height"><![CDATA[Maximum Height of Images in Signatures]]></item>
                <item name="wcf.acp.option.user_title_max_length"><![CDATA[Maximum User Title Length]]></item>
                <item name="wcf.acp.option.user_forbidden_titles"><![CDATA[Reserved User Titles]]></item>
@@ -928,7 +942,6 @@ Examples for medium ID detection:
                <item name="wcf.acp.option.search_results_per_page"><![CDATA[Results Per Page]]></item>
                <item name="wcf.acp.option.search_default_sort_field"><![CDATA[Default Sort Field]]></item>
                <item name="wcf.acp.option.search_default_sort_order"><![CDATA[Default Sort Order]]></item>
-               <item name="wcf.acp.option.search_use_captcha"><![CDATA[Enable reCAPTCHA protection for search]]></item>
                <item name="wcf.acp.option.category.message.general.poll"><![CDATA[Polls]]></item>
                <item name="wcf.acp.option.module_poll"><![CDATA[Polls]]></item>
                <item name="wcf.acp.option.poll_max_options"><![CDATA[Maximum Number of Options]]></item>
@@ -967,6 +980,12 @@ GmbH=Gesellschaft mit beschränkter Haftung]]></item>
                <item name="wcf.acp.option.url_omit_index_php"><![CDATA[TODO: „index.php“ aus URLs entfernen]]></item>
                <item name="wcf.acp.option.url_omit_index_php.description"><![CDATA[TODO]]></item>
                <item name="wcf.acp.option.module_ad"><![CDATA[Ads]]></item>
+               <item name="wcf.acp.option.register_captcha_type"><![CDATA[Registration]]></item>
+               <item name="wcf.acp.option.lost_password_captcha_type"><![CDATA[Lost Password]]></item>
+               <item name="wcf.acp.option.profile_mail_captcha_type"><![CDATA[Send Email to User]]></item>
+               <item name="wcf.acp.option.search_captcha_type"><![CDATA[Search]]></item>
+               <item name="wcf.acp.option.message_captcha_type"><![CDATA[Messages]]></item>
+               <item name="wcf.acp.option.category.security.antispam.captcha"><![CDATA[Captchas]]></item>
        </category>
        
        <category name="wcf.acp.package">
@@ -1575,6 +1594,18 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi
                <item name="wcf.bbcode.spoiler.text"><![CDATA[(Hidden Content)]]></item>
        </category>
        
+       <category name="wcf.captcha">
+               <item name="wcf.captcha.useNoCaptcha"><![CDATA[(No Captcha)]]></item>
+               <item name="wcf.captcha.com.woltlab.wcf.recaptcha"><![CDATA[reCAPTCHA]]></item>
+               <item name="wcf.captcha.com.woltlab.wcf.captchaQuestion"><![CDATA[Question]]></item>
+       </category>
+       
+       <category name="wcf.captcha.question">
+               <item name="wcf.captcha.question.answer.error.false"><![CDATA[You have entered an incorrect answer.]]></item>
+               <item name="wcf.captcha.question.captcha"><![CDATA[Question]]></item>
+               <item name="wcf.captcha.question.captcha.description"><![CDATA[Please answer the question below or solve the problem below.]]></item>
+       </category>
+       
        <category name="wcf.category">
                <item name="wcf.category.add"><![CDATA[Add Category]]></item>
                <item name="wcf.category.button.list"><![CDATA[List Categories]]></item>
index 05569c1a724f57914c2524d431c15db368fcf8f5..681158ccc1efe6b15465c8c8de029983df3b83bb 100644 (file)
@@ -203,6 +203,14 @@ CREATE TABLE wcf1_bbcode_media_provider (
        html TEXT NOT NULL
 );
 
+DROP TABLE IF EXISTS wcf1_captcha_question;
+CREATE TABLE wcf1_captcha_question (
+       questionID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
+       question VARCHAR(255) NOT NULL,
+       answers MEDIUMTEXT,
+       isDisabled TINYINT(1) NOT NULL DEFAULT 0
+);
+
 DROP TABLE IF EXISTS wcf1_category;
 CREATE TABLE wcf1_category (
        categoryID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,