From: Matthias Schmidt Date: Sat, 14 Jun 2014 13:04:07 +0000 (+0200) Subject: Add object type-based captcha system (WIP) X-Git-Tag: 2.1.0_Alpha_1~705 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=96714cabde03935ae3a5a76b091c44fbeb363b74;p=GitHub%2FWoltLab%2FWCF.git Add object type-based captcha system (WIP) --- diff --git a/com.woltlab.wcf/acpMenu.xml b/com.woltlab.wcf/acpMenu.xml index e633b3fc94..29df7909ae 100644 --- a/com.woltlab.wcf/acpMenu.xml +++ b/com.woltlab.wcf/acpMenu.xml @@ -384,6 +384,21 @@ wcf.acp.menu.link.language.server admin.language.canManageLanguage --> + + + wcf.acp.menu.link.display + 5 + + + wcf.acp.menu.link.captcha + + admin.captcha.canManageCaptchaQuestion + + + wcf.acp.menu.link.captcha + + admin.captcha.canManageCaptchaQuestion + diff --git a/com.woltlab.wcf/objectType.xml b/com.woltlab.wcf/objectType.xml index 335de6c268..eceb72f287 100644 --- a/com.woltlab.wcf/objectType.xml +++ b/com.woltlab.wcf/objectType.xml @@ -860,5 +860,18 @@ userOptions + + + + com.woltlab.wcf.recaptcha + com.woltlab.wcf.captcha + + + + com.woltlab.wcf.captchaQuestion + com.woltlab.wcf.captcha + + + diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index 8cfd6268ca..daebce74ab 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -171,5 +171,10 @@ com.woltlab.wcf.condition.ad + + + com.woltlab.wcf.captcha + + diff --git a/com.woltlab.wcf/option.xml b/com.woltlab.wcf/option.xml index 50883b1d62..d83bc75e53 100644 --- a/com.woltlab.wcf/option.xml +++ b/com.woltlab.wcf/option.xml @@ -147,6 +147,9 @@ security + + security.antispam + security.antispam module_system_recaptcha @@ -524,6 +527,41 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]> + + + + + + + + - - - - - - - - + + + + + + + diff --git a/com.woltlab.wcf/templates/__commentJavaScript.tpl b/com.woltlab.wcf/templates/__commentJavaScript.tpl index 2d2d936817..04337db0aa 100644 --- a/com.woltlab.wcf/templates/__commentJavaScript.tpl +++ b/com.woltlab.wcf/templates/__commentJavaScript.tpl @@ -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 index 0000000000..9ad50b29fb --- /dev/null +++ b/com.woltlab.wcf/templates/captcha.tpl @@ -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 index 0000000000..06e0a3fdd7 --- /dev/null +++ b/com.woltlab.wcf/templates/captchaQuestion.tpl @@ -0,0 +1,43 @@ + + +{if !$captchaQuestionAnswered} +
+ {lang}wcf.captcha.question.captcha{/lang} + {lang}wcf.captcha.question.captcha.description{/lang} + + +
+
+ + {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'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.captcha.question.answer.error.{$__errorType}{/lang} + {/if} + {/if} +
+ +
+ + {if !$ajaxCaptcha|empty} + + {/if} +{/if} diff --git a/com.woltlab.wcf/templates/commentAddGuestDialog.tpl b/com.woltlab.wcf/templates/commentAddGuestDialog.tpl index f0a4465cf9..1f758870f1 100644 --- a/com.woltlab.wcf/templates/commentAddGuestDialog.tpl +++ b/com.woltlab.wcf/templates/commentAddGuestDialog.tpl @@ -1,16 +1,23 @@
-
+
+ {if $errorType[username]|isset} + + {if $errorType[username] == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}wcf.user.username.error.$errorType[username]{/lang} + {/if} + + {/if}
- {if MODULE_SYSTEM_RECAPTCHA} - {include file='recaptcha'} - {/if} + {include file='captcha'}
diff --git a/com.woltlab.wcf/templates/lostPassword.tpl b/com.woltlab.wcf/templates/lostPassword.tpl index 2a37999878..d8d4929f76 100644 --- a/com.woltlab.wcf/templates/lostPassword.tpl +++ b/com.woltlab.wcf/templates/lostPassword.tpl @@ -80,9 +80,9 @@ {event name='fieldsets'} - {if $useCaptcha}{include file='recaptcha'}{/if} + {include file='captcha'}
- +
{@SECURITY_TOKEN_INPUT_TAG} diff --git a/com.woltlab.wcf/templates/mail.tpl b/com.woltlab.wcf/templates/mail.tpl index 5b994a8210..71b2beecc8 100644 --- a/com.woltlab.wcf/templates/mail.tpl +++ b/com.woltlab.wcf/templates/mail.tpl @@ -101,9 +101,7 @@ {event name='fieldsets'} - {if $useCaptcha} - {include file='recaptcha'} - {/if} + {include file='captcha'}
diff --git a/com.woltlab.wcf/templates/recaptcha.tpl b/com.woltlab.wcf/templates/recaptcha.tpl index eff2b03eac..c538a1157f 100644 --- a/com.woltlab.wcf/templates/recaptcha.tpl +++ b/com.woltlab.wcf/templates/recaptcha.tpl @@ -1,68 +1,95 @@ -
- - {lang}wcf.recaptcha.description{/lang} - -
- {if !$ajaxRecaptcha|isset || !$ajaxRecaptcha} - - {/if} -
- -
-
-
- - {if $errorField|isset && $errorField == 'recaptchaString'} - - {if $errorType == 'empty'}{lang}wcf.global.form.error.empty{/lang}{/if} - {if $errorType == 'false'}{lang}wcf.recaptcha.error.recaptchaString.false{/lang}{/if} - - {/if} -
- - {event name='fields'} - -
- -
+{if $recaptchaLegacyMode|empty} + {include file='captcha'} +{else} +
+ + {lang}wcf.recaptcha.description{/lang} - {if !$ajaxRecaptcha|isset || !$ajaxRecaptcha} - -
+ + + {event name='fields'} + +
+ +
+ + {if !$ajaxCaptcha|isset || !$ajaxCaptcha} + + + {else} + + {/if} +
+
+{/if} diff --git a/com.woltlab.wcf/templates/register.tpl b/com.woltlab.wcf/templates/register.tpl index f806ca097f..6ebc9c6c0a 100644 --- a/com.woltlab.wcf/templates/register.tpl +++ b/com.woltlab.wcf/templates/register.tpl @@ -234,13 +234,7 @@ {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'}
@@ -252,4 +246,4 @@ {include file='footer'} - \ No newline at end of file + diff --git a/com.woltlab.wcf/templates/search.tpl b/com.woltlab.wcf/templates/search.tpl index 11ecc2ba36..b91643c86f 100644 --- a/com.woltlab.wcf/templates/search.tpl +++ b/com.woltlab.wcf/templates/search.tpl @@ -98,7 +98,8 @@ {event name='fieldsets'} - {if $useCaptcha}{include file='recaptcha'}{/if} + + {include file='captcha'} {foreach from=$objectTypes key=objectTypeName item=objectType} {if $objectType->isAccessible() && $objectType->getFormTemplateName()} @@ -143,4 +144,4 @@ - \ No newline at end of file + diff --git a/com.woltlab.wcf/userGroupOption.xml b/com.woltlab.wcf/userGroupOption.xml index a9eea1a904..311e95d76d 100644 --- a/com.woltlab.wcf/userGroupOption.xml +++ b/com.woltlab.wcf/userGroupOption.xml @@ -78,6 +78,9 @@ admin.display + + admin.display + admin @@ -654,6 +657,13 @@ png]]> 0 1 + + diff --git a/wcfsetup/install/files/acp/templates/captchaQuestionAdd.tpl b/wcfsetup/install/files/acp/templates/captchaQuestionAdd.tpl new file mode 100644 index 0000000000..28746c4e7a --- /dev/null +++ b/wcfsetup/install/files/acp/templates/captchaQuestionAdd.tpl @@ -0,0 +1,86 @@ +{include file='header' pageTitle='wcf.acp.captcha.question.'|concat:$action} + +
+

{lang}wcf.acp.captcha.question.{$action}{/lang}

+
+ +{include file='formError'} + +{if $success|isset} +

{lang}wcf.global.success.{$action}{/lang}

+{/if} + +
+ +
+ +
+
+
+ {lang}wcf.global.form.data{/lang} + + +
+
+ + {if $errorField == 'question'} + + {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} + + {/if} +
+ + {include file='multipleLanguageInputJavascript' elementIdentifier='question' forceSelection=false} + + +
+
+ + {lang}wcf.acp.captcha.question.answers.description{/lang} + {if $errorField == 'answers'} + + {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} + + {/if} +
+ + {include file='multipleLanguageInputJavascript' elementIdentifier='answers' forceSelection=false} + +
+
+
+ +
+
+ + {event name='dataFields'} +
+ + {event name='fieldsets'} +
+ +
+ + {@SECURITY_TOKEN_INPUT_TAG} +
+
+ +{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 index 0000000000..1f41b88dc7 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/captchaQuestionList.tpl @@ -0,0 +1,94 @@ +{include file='header' pageTitle='wcf.acp.captcha.question.list'} + +
+

{lang}wcf.acp.captcha.question.list{/lang}

+
+ + + +
+ {pages print=true assign=pagesLinks controller="CaptchaQuestionList" link="pageNo=%d"} + + +
+ +{hascontent} +
+
+

{lang}wcf.acp.captcha.question.list{/lang} {#$items}

+
+ + + + + + + + {event name='columnHeads'} + + + + + {content} + {foreach from=$objects item='question'} + + + + + + {event name='columns'} + + {/foreach} + {/content} + +
{lang}wcf.global.objectID{/lang}{lang}wcf.acp.captcha.question.question{/lang}
+ + + + + {event name='rowButtons'} + {$question->questionID}{lang}{$question->question}{/lang}
+
+{hascontentelse} +

{lang}wcf.global.noItems{/lang}

+{/hascontent} + +
+ {@$pagesLinks} + + +
+ +{include file='footer'} + \ No newline at end of file diff --git a/wcfsetup/install/files/js/WCF.Comment.js b/wcfsetup/install/files/js/WCF.Comment.js index 9092c85ed3..1d1ab82147 100644 --- a/wcfsetup/install/files/js/WCF.Comment.js +++ b/wcfsetup/install/files/js/WCF.Comment.js @@ -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 = $('
').append(data.returnValues.template).hide().appendTo(document.body); + _createGuestDialog: function(template, useCaptcha) { + var $refreshGuestDialog = !!this._guestDialog; + if (!this._guestDialog) { + this._guestDialog = $('
').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 = $('').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 = $('').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 = $('').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 = $('').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(); }, /** diff --git a/wcfsetup/install/files/js/WCF.js b/wcfsetup/install/files/js/WCF.js index fe9cd49c67..66f6b82ab5 100755 --- a/wcfsetup/install/files/js/WCF.js +++ b/wcfsetup/install/files/js/WCF.js @@ -6951,6 +6951,53 @@ WCF.System.ObjectStore = { } }; +/** + * Stores captcha callbacks used for captchas in AJAX contexts. + */ +WCF.System.Captcha = { + /** + * adds call + * @var object + */ + _captchas: { }, + + /** + * Adds a callback for a certain captcha. + * + * @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 index 0000000000..afa092dfea --- /dev/null +++ b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php @@ -0,0 +1,178 @@ + + * @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 index 0000000000..bbdd8aab53 --- /dev/null +++ b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionEditForm.class.php @@ -0,0 +1,118 @@ + + * @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 index 0000000000..064df47d3b --- /dev/null +++ b/wcfsetup/install/files/lib/acp/page/CaptchaQuestionListPage.class.php @@ -0,0 +1,40 @@ + + * @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'; +} diff --git a/wcfsetup/install/files/lib/action/FacebookAuthAction.class.php b/wcfsetup/install/files/lib/action/FacebookAuthAction.class.php index d26e00ba69..a82912943c 100644 --- a/wcfsetup/install/files/lib/action/FacebookAuthAction.class.php +++ b/wcfsetup/install/files/lib/action/FacebookAuthAction.class.php @@ -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(); diff --git a/wcfsetup/install/files/lib/action/GithubAuthAction.class.php b/wcfsetup/install/files/lib/action/GithubAuthAction.class.php index fec9eca2c6..bcf0c93f2b 100644 --- a/wcfsetup/install/files/lib/action/GithubAuthAction.class.php +++ b/wcfsetup/install/files/lib/action/GithubAuthAction.class.php @@ -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(); diff --git a/wcfsetup/install/files/lib/action/GoogleAuthAction.class.php b/wcfsetup/install/files/lib/action/GoogleAuthAction.class.php index e5708e3543..62042513c5 100644 --- a/wcfsetup/install/files/lib/action/GoogleAuthAction.class.php +++ b/wcfsetup/install/files/lib/action/GoogleAuthAction.class.php @@ -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(); diff --git a/wcfsetup/install/files/lib/action/TwitterAuthAction.class.php b/wcfsetup/install/files/lib/action/TwitterAuthAction.class.php index 74ba55fd5b..fd40437a88 100644 --- a/wcfsetup/install/files/lib/action/TwitterAuthAction.class.php +++ b/wcfsetup/install/files/lib/action/TwitterAuthAction.class.php @@ -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 index 0000000000..98781ebcc9 --- /dev/null +++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php @@ -0,0 +1,52 @@ + + * @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 index 0000000000..6382a23a34 --- /dev/null +++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php @@ -0,0 +1,44 @@ + + * @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 index 0000000000..0cd804fafd --- /dev/null +++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionEditor.class.php @@ -0,0 +1,29 @@ + + * @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 index 0000000000..a97fd7d4a7 --- /dev/null +++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php @@ -0,0 +1,15 @@ + + * @package com.woltlab.wcf + * @subpackage data.captcha.question + * @category Community Framework + */ +class CaptchaQuestionList extends DatabaseObjectList { } diff --git a/wcfsetup/install/files/lib/data/comment/CommentAction.class.php b/wcfsetup/install/files/lib/data/comment/CommentAction.class.php index 70af4a95cf..37a592a68f 100644 --- a/wcfsetup/install/files/lib/data/comment/CommentAction.class.php +++ b/wcfsetup/install/files/lib/data/comment/CommentAction.class.php @@ -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 index 0000000000..ad3d7845c2 --- /dev/null +++ b/wcfsetup/install/files/lib/form/AbstractCaptchaForm.class.php @@ -0,0 +1,147 @@ + + * @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; + } + } +} diff --git a/wcfsetup/install/files/lib/form/LostPasswordForm.class.php b/wcfsetup/install/files/lib/form/LostPasswordForm.class.php index a5355dc2f3..f0eec4d85f 100644 --- a/wcfsetup/install/files/lib/form/LostPasswordForm.class.php +++ b/wcfsetup/install/files/lib/form/LostPasswordForm.class.php @@ -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() diff --git a/wcfsetup/install/files/lib/form/MailForm.class.php b/wcfsetup/install/files/lib/form/MailForm.class.php index dc801ae822..e1dc8ab83b 100644 --- a/wcfsetup/install/files/lib/form/MailForm.class.php +++ b/wcfsetup/install/files/lib/form/MailForm.class.php @@ -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 diff --git a/wcfsetup/install/files/lib/form/MessageForm.class.php b/wcfsetup/install/files/lib/form/MessageForm.class.php index 964dc8ddef..260966242f 100644 --- a/wcfsetup/install/files/lib/form/MessageForm.class.php +++ b/wcfsetup/install/files/lib/form/MessageForm.class.php @@ -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 diff --git a/wcfsetup/install/files/lib/form/RecaptchaForm.class.php b/wcfsetup/install/files/lib/form/RecaptchaForm.class.php index d12c32ca1c..12e49384a3 100644 --- a/wcfsetup/install/files/lib/form/RecaptchaForm.class.php +++ b/wcfsetup/install/files/lib/form/RecaptchaForm.class.php @@ -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 { /** diff --git a/wcfsetup/install/files/lib/form/RegisterForm.class.php b/wcfsetup/install/files/lib/form/RegisterForm.class.php index 81f9c98dd1..17f2dac5d0 100644 --- a/wcfsetup/install/files/lib/form/RegisterForm.class.php +++ b/wcfsetup/install/files/lib/form/RegisterForm.class.php @@ -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(); diff --git a/wcfsetup/install/files/lib/form/SearchForm.class.php b/wcfsetup/install/files/lib/form/SearchForm.class.php index 03104a0b26..e5dc4d836d 100644 --- a/wcfsetup/install/files/lib/form/SearchForm.class.php +++ b/wcfsetup/install/files/lib/form/SearchForm.class.php @@ -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 @@ -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 diff --git a/wcfsetup/install/files/lib/form/SignatureEditForm.class.php b/wcfsetup/install/files/lib/form/SignatureEditForm.class.php index 62a5a1612e..f1fb11f747 100644 --- a/wcfsetup/install/files/lib/form/SignatureEditForm.class.php +++ b/wcfsetup/install/files/lib/form/SignatureEditForm.class.php @@ -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 index 0000000000..d93208b818 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/builder/CaptchaQuestionCacheBuilder.class.php @@ -0,0 +1,26 @@ + + * @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 index 0000000000..12208735c9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/captcha/CaptchaHandler.class.php @@ -0,0 +1,69 @@ + + * @package com.woltlab.wcf + * @subpackage system.captcha + * @category Community Framework + */ +class CaptchaHandler extends SingletonFactory { + /** + * Returns the available captcha types for selection. + * + * @return array + */ + 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 index 0000000000..a22db5831c --- /dev/null +++ b/wcfsetup/install/files/lib/system/captcha/CaptchaQuestionHandler.class.php @@ -0,0 +1,121 @@ + + * @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 index 0000000000..8fa27e8a39 --- /dev/null +++ b/wcfsetup/install/files/lib/system/captcha/ICaptchaHandler.class.php @@ -0,0 +1,43 @@ + + * @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 index 0000000000..42eb32c5ad --- /dev/null +++ b/wcfsetup/install/files/lib/system/captcha/RecaptchaHandler.class.php @@ -0,0 +1,66 @@ + + * @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 index 0000000000..e49febdf55 --- /dev/null +++ b/wcfsetup/install/files/lib/system/option/CaptchaSelectOptionType.class.php @@ -0,0 +1,50 @@ + + * @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); + } + } +} diff --git a/wcfsetup/install/files/lib/system/recaptcha/RecaptchaHandler.class.php b/wcfsetup/install/files/lib/system/recaptcha/RecaptchaHandler.class.php index 4e69ff49af..837593cd0b 100644 --- a/wcfsetup/install/files/lib/system/recaptcha/RecaptchaHandler.class.php +++ b/wcfsetup/install/files/lib/system/recaptcha/RecaptchaHandler.class.php @@ -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 )); } } diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 5dc43d9913..3ba6412178 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -141,6 +141,18 @@ + + + + + + + + + + + + @@ -420,6 +432,8 @@ + + @@ -618,6 +632,9 @@ + + + @@ -881,9 +898,6 @@ - - - @@ -928,7 +942,6 @@ - @@ -967,6 +980,12 @@ GmbH=Gesellschaft mit beschränkter Haftung]]> + + + + + + @@ -1606,6 +1625,18 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getFormattedAllowedExt + + + + + + + + + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 16d57c15b3..bf43413747 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -140,6 +140,18 @@ Examples for medium ID detection: + + + + + + + + + + + + @@ -419,6 +431,8 @@ Examples for medium ID detection: + + @@ -617,6 +631,9 @@ Examples for medium ID detection: + + + @@ -880,9 +897,6 @@ Examples for medium ID detection: - - - @@ -928,7 +942,6 @@ Examples for medium ID detection: - @@ -967,6 +980,12 @@ GmbH=Gesellschaft mit beschränkter Haftung]]> + + + + + + @@ -1575,6 +1594,18 @@ Allowed extensions: {', '|implode:$attachmentHandler->getFormattedAllowedExtensi + + + + + + + + + + + + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 05569c1a72..681158ccc1 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -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,