Skip to content
Draft
1 change: 1 addition & 0 deletions com.woltlab.wcf/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
Required order of the following steps for the update to 6.2:
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_62_step1.php</instruction>
<instruction type="script">acp/update_com.woltlab.wcf_6.2_contactOptions.php</instruction>
<instruction type="script">acp/update_com.woltlab.wcf_6.2_captchaQuestion.php</instruction>
<instruction type="database" run="standalone">acp/database/update_com.woltlab.wcf_62_step2.php</instruction>
-->
</package>
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,31 @@
->referencedColumns(['languageID'])
->onDelete('CASCADE'),
]),
DatabaseTable::create('wcf1_captcha_question_content')
->columns([
ObjectIdDatabaseTableColumn::create('contentID'),
IntDatabaseTableColumn::create('questionID')
->notNull(),
IntDatabaseTableColumn::create('languageID'),
NotNullVarchar255DatabaseTableColumn::create('question'),
MediumtextDatabaseTableColumn::create('answers'),
])
->indices([
DatabaseTablePrimaryIndex::create()
->columns(['contentID']),
DatabaseTableIndex::create('id')
->columns(['questionID', 'languageID']),
])
->foreignKeys([
DatabaseTableForeignKey::create()
->columns(['questionID'])
->referencedTable('wcf1_captcha_question')
->referencedColumns(['questionID'])
->onDelete('CASCADE'),
DatabaseTableForeignKey::create()
->columns(['languageID'])
->referencedTable('wcf1_language')
->referencedColumns(['languageID'])
->onDelete('CASCADE'),
]),
];
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/

use wcf\system\database\table\column\MediumtextDatabaseTableColumn;
use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn;
use wcf\system\database\table\column\TextDatabaseTableColumn;
use wcf\system\database\table\column\TinyintDatabaseTableColumn;
use wcf\system\database\table\PartialDatabaseTable;
Expand All @@ -25,4 +26,11 @@
TinyintDatabaseTableColumn::create('required')
->drop(),
]),
PartialDatabaseTable::create('wcf1_captcha_question')
->columns([
NotNullVarchar255DatabaseTableColumn::create('question')
->drop(),
MediumtextDatabaseTableColumn::create('answers')
->drop(),
]),
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

use wcf\system\database\util\PreparedStatementConditionBuilder;
use wcf\system\language\LanguageFactory;
use wcf\system\WCF;

$sql = "SELECT questionID, question, answers
FROM wcf1_captcha_question";
$statement = WCF::getDB()->prepare($sql);
$statement->execute();

$questionIDs = $questions = $answers = [];
while ($row = $statement->fetchArray()) {
$questionIDs[] = $row['questionID'];
$questions[$row['questionID']] = $row['question'];
$answers[$row['questionID']] = $row['answers'];
}

$sql = "INSERT INTO wcf1_captcha_question_content
(questionID, languageID, question, answers)
VALUES (?, ?, ?, ?)";
$statement = WCF::getDB()->prepare($sql);

$languageItems = [];
foreach ($questionIDs as $questionID) {
$answer = $answers[$questionID];
$question = $questions[$questionID];
$multilingual = false;

if (\preg_match('~^wcf\.captcha\.question\.question\.question\d+$~', $question, $matches)) {
$languageItems[] = $question;
$multilingual = true;
}
if (\preg_match('~^wcf\.captcha\.question\.answers\.question\d+$~', $answer, $matches)) {
$languageItems[] = $answer;
$multilingual = true;
}

if ($multilingual) {
foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
$statement->execute([
$questionID,
$language->languageID,
$language->get($question),
$language->get($answer),
]);
}
} else {
$statement->execute([
$questionID,
null,
$question,
$answer,
]);
}
}

if ($languageItems !== []) {
$conditionBuilder = new PreparedStatementConditionBuilder();
$conditionBuilder->add('languageItem IN (?)', [$languageItems]);

$sql = "DELETE FROM wcf1_language_item
{$conditionBuilder}";
$statement = WCF::getDB()->prepare($sql);
$statement->execute($conditionBuilder->getParameters());
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
use wcf\data\language\Language;
use wcf\form\AbstractFormBuilderForm;
use wcf\system\form\builder\container\FormContainer;
use wcf\system\form\builder\container\MultilingualFormContainer;
use wcf\system\form\builder\data\processor\MultilingualFormDataProcessor;
use wcf\system\form\builder\data\processor\VoidFormDataProcessor;
use wcf\system\form\builder\field\BooleanFormField;
use wcf\system\form\builder\field\dependency\EmptyFormFieldDependency;
use wcf\system\form\builder\field\IFormField;
use wcf\system\form\builder\field\MultilineTextFormField;
use wcf\system\form\builder\field\TextFormField;
use wcf\system\form\builder\field\validation\FormFieldValidationError;
Expand Down Expand Up @@ -51,43 +56,76 @@ protected function createForm()
{
parent::createForm();

$multilingualContainer = MultilingualFormContainer::create('content')
->label('wcf.acp.captcha.question.content')
->appendChildren($this->getContentFields());

foreach ($multilingualContainer->getLangaugeContainers() as $langaugeCode => $container) {
$container->appendChildren(
$this->getContentFields(LanguageFactory::getInstance()->getLanguageByCode($langaugeCode))
);
}

$this->form->appendChildren([
FormContainer::create('general')
->appendChildren([
TextFormField::create('question')
->label('wcf.acp.captcha.question.question')
->i18n()
->languageItemPattern('wcf.captcha.question.question.question\d+')
->required(),
MultilineTextFormField::create('answers')
->label('wcf.acp.captcha.question.answers')
->i18n()
->languageItemPattern('wcf.captcha.question.answers.question\d+')
->required()
->addValidator(
new FormFieldValidator('regexValidator', function (MultilineTextFormField $formField) {
$value = $formField->getValue();

if ($formField->hasPlainValue()) {
$this->validateAnswer($value, $formField);
} else {
foreach ($value as $languageID => $languageValue) {
$this->validateAnswer(
$languageValue,
$formField,
LanguageFactory::getInstance()->getLanguage($languageID)
);
}
}
})
),
BooleanFormField::create('isDisabled')
->label('wcf.acp.captcha.question.isDisabled')
->value(false)
])
->value(false),
]),
$multilingualContainer,
]);
}

#[\Override]
protected function finalizeForm()
{
parent::finalizeForm();

$this->form->getDataHandler()
->addProcessor(
new MultilingualFormDataProcessor(
'wcf1_captcha_question_content',
['question' => 'question', 'answers' => 'answers']
)
)
->addProcessor(new VoidFormDataProcessor('isMultilingual'));
}

/**
* @return IFormField[]
*/
protected function getContentFields(?Language $language = null): array
{
$questionFormField = TextFormField::create('question' . ($language ? '_' . $language->languageCode : ''))
->label('wcf.acp.captcha.question.question')
->required();

$answerFormField = MultilineTextFormField::create('answers' . ($language ? '_' . $language->languageCode : ''))
->label('wcf.acp.captcha.question.answers')
->required()
->addValidator(
new FormFieldValidator('regexValidator', function (MultilineTextFormField $formField) use ($language) {
$value = $formField->getValue();

$this->validateAnswer($value, $formField, $language);
})
);

if ($language === null) {
$questionFormField->addDependency(
EmptyFormFieldDependency::create('isMultilingual')
->fieldId('isMultilingual')
);
$answerFormField->addDependency(
EmptyFormFieldDependency::create('isMultilingual')
->fieldId('isMultilingual')
);
}

return [$questionFormField, $answerFormField];
}

protected function validateAnswer(
string $answer,
MultilineTextFormField $formField,
Expand All @@ -105,7 +143,7 @@ protected function validateAnswer(
'wcf.acp.captcha.question.answers.error.invalidRegex',
[
'invalidRegex' => $answer,
'language' => $language
'language' => $language,
]
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace wcf\data\captcha\question;

use wcf\data\DatabaseObject;
use wcf\system\language\LanguageFactory;
use wcf\system\Regex;
use wcf\system\WCF;
use wcf\util\StringUtil;
Expand All @@ -15,12 +16,15 @@
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
*
* @property-read int $questionID unique id of the captcha question
* @property-read string $question question of the captcha or name of language item which contains the question
* @property-read string $answers newline-separated list of answers or name of language item which contains the answers
* @property-read int $isDisabled is `1` if the captcha question is disabled and thus not offered to answer, otherwise `0`
*/
class CaptchaQuestion extends DatabaseObject
{
/**
* @var array<int, array{question: string, answers: string}>
*/
protected array $content;

/**
* Returns the question in the active user's language.
*
Expand All @@ -29,7 +33,7 @@ class CaptchaQuestion extends DatabaseObject
*/
public function getQuestion()
{
return WCF::getLanguage()->get($this->question);
return $this->getContent()['question'];
}

/**
Expand All @@ -40,7 +44,9 @@ public function getQuestion()
*/
public function isAnswer($answer)
{
$answers = \explode("\n", StringUtil::unifyNewlines(WCF::getLanguage()->get($this->answers)));
$this->loadContent();

$answers = \explode("\n", StringUtil::unifyNewlines($this->getContent()['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), Regex::CASE_INSENSITIVE)->match($answer)) {
Expand All @@ -55,4 +61,57 @@ public function isAnswer($answer)

return false;
}

/**
* @since 6.2
*/
protected function loadContent(): void
{
if (isset($this->content)) {
return;
}

$sql = "SELECT languageID, question, answers
FROM wcf1_captcha_question_content
WHERE questionID = ?";

$statement = WCF::getDB()->prepare($sql);
$statement->execute([$this->questionID]);

$this->content = [];
while ($row = $statement->fetchArray()) {
$this->content[$row['languageID'] ?: 0] = [
'question' => $row['question'],
'answers' => $row['answers'],
];
}
}

/**
* @return array{question: string, answers: string}
* @since 6.2
*/
protected function getContent(): array
{
$this->loadContent();

return $this->content[WCF::getLanguage()->languageID]
?? $this->content[LanguageFactory::getInstance()->getDefaultLanguageID()]
?? \reset($this->content);
}

/**
* @since 6.2
*/
public function setContent(?int $languageID, string $question, string $answers): void
{
if (!isset($this->content)) {
$this->content = [];
}

$this->content[$languageID ?: 0] = [
'question' => $question,
'answers' => $answers,
];
}
}
Loading