diff --git a/com.woltlab.wcf/package.xml b/com.woltlab.wcf/package.xml
index bbf5922e535..44bfec4759b 100644
--- a/com.woltlab.wcf/package.xml
+++ b/com.woltlab.wcf/package.xml
@@ -54,6 +54,7 @@
Required order of the following steps for the update to 6.2:
acp/database/update_com.woltlab.wcf_62_step1.php
acp/update_com.woltlab.wcf_6.2_contactOptions.php
+ acp/update_com.woltlab.wcf_6.2_captchaQuestion.php
acp/database/update_com.woltlab.wcf_62_step2.php
-->
diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php
index d34fe5609eb..82b74cc927c 100644
--- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php
+++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step1.php
@@ -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'),
+ ]),
];
diff --git a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step2.php b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step2.php
index f10a241c93d..34c8d0d4a03 100644
--- a/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step2.php
+++ b/wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2_step2.php
@@ -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;
@@ -25,4 +26,11 @@
TinyintDatabaseTableColumn::create('required')
->drop(),
]),
+ PartialDatabaseTable::create('wcf1_captcha_question')
+ ->columns([
+ NotNullVarchar255DatabaseTableColumn::create('question')
+ ->drop(),
+ MediumtextDatabaseTableColumn::create('answers')
+ ->drop(),
+ ]),
];
diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_captchaQuestion.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_captchaQuestion.php
new file mode 100644
index 00000000000..db2a600429c
--- /dev/null
+++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_captchaQuestion.php
@@ -0,0 +1,66 @@
+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());
+}
diff --git a/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php
index 0b8209c25bd..7120ae7449d 100644
--- a/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php
+++ b/wcfsetup/install/files/lib/acp/form/CaptchaQuestionAddForm.class.php
@@ -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;
@@ -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,
@@ -105,7 +143,7 @@ protected function validateAnswer(
'wcf.acp.captcha.question.answers.error.invalidRegex',
[
'invalidRegex' => $answer,
- 'language' => $language
+ 'language' => $language,
]
)
);
diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php
index 0c3fe3b8109..e8b592435b1 100644
--- a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php
+++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestion.class.php
@@ -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;
@@ -15,12 +16,15 @@
* @license GNU Lesser General Public License
*
* @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
+ */
+ protected array $content;
+
/**
* Returns the question in the active user's language.
*
@@ -29,7 +33,7 @@ class CaptchaQuestion extends DatabaseObject
*/
public function getQuestion()
{
- return WCF::getLanguage()->get($this->question);
+ return $this->getContent()['question'];
}
/**
@@ -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)) {
@@ -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,
+ ];
+ }
}
diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php
index 52651d3201c..a5c3c08bccc 100644
--- a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php
+++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionAction.class.php
@@ -5,7 +5,8 @@
use wcf\data\AbstractDatabaseObjectAction;
use wcf\data\IToggleAction;
use wcf\data\TDatabaseObjectToggle;
-use wcf\data\TI18nDatabaseObjectAction;
+use wcf\system\captcha\question\command\SaveContent;
+use wcf\system\form\builder\data\processor\MultilingualFormDataProcessor;
/**
* Executes captcha question-related actions.
@@ -19,7 +20,6 @@
class CaptchaQuestionAction extends AbstractDatabaseObjectAction implements IToggleAction
{
use TDatabaseObjectToggle;
- use TI18nDatabaseObjectAction;
/**
* @inheritDoc
@@ -31,60 +31,27 @@ class CaptchaQuestionAction extends AbstractDatabaseObjectAction implements ITog
*/
protected $permissionsUpdate = ['admin.captcha.canManageCaptchaQuestion'];
- /**
- * @return array
- */
- #[\Override]
- public function getI18nSaveTypes(): array
- {
- return [
- 'question' => 'wcf.captcha.question.question.question\d+',
- 'answers' => 'wcf.captcha.question.answers.question\d+',
- ];
- }
-
- #[\Override]
- public function getLanguageCategory(): string
- {
- return 'wcf.captcha.question';
- }
-
- #[\Override]
- public function getPackageID(): int
- {
- return PACKAGE_ID;
- }
-
#[\Override]
public function update()
{
parent::update();
- foreach ($this->objects as $object) {
- $this->saveI18nValue($object->getDecoratedObject());
+ if (isset($this->parameters[MultilingualFormDataProcessor::ARRAY_INDEX])) {
+ foreach ($this->objects as $object) {
+ (new SaveContent($object->questionID, $this->parameters[MultilingualFormDataProcessor::ARRAY_INDEX]))();
+ }
}
}
#[\Override]
public function create()
{
- // Question column doesn't have a default value
- $this->parameters['data']['question'] = $this->parameters['data']['question'] ?? '';
-
$captchaQuestion = parent::create();
- $this->saveI18nValue($captchaQuestion);
+ if (isset($this->parameters[MultilingualFormDataProcessor::ARRAY_INDEX])) {
+ (new SaveContent($captchaQuestion->questionID, $this->parameters[MultilingualFormDataProcessor::ARRAY_INDEX]))();
+ }
return $captchaQuestion;
}
-
- #[\Override]
- public function delete()
- {
- $returnValue = parent::delete();
-
- $this->deleteI18nValues();
-
- return $returnValue;
- }
}
diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionEditor.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionEditor.class.php
index 839dea59661..fa4d23cb916 100644
--- a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionEditor.class.php
+++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionEditor.class.php
@@ -4,7 +4,7 @@
use wcf\data\DatabaseObjectEditor;
use wcf\data\IEditableCachedObject;
-use wcf\system\cache\builder\CaptchaQuestionCacheBuilder;
+use wcf\system\cache\eager\CaptchaQuestionCache;
/**
* Provides functions to edit captcha questions.
@@ -29,6 +29,6 @@ class CaptchaQuestionEditor extends DatabaseObjectEditor implements IEditableCac
*/
public static function resetCache()
{
- CaptchaQuestionCacheBuilder::getInstance()->reset();
+ (new CaptchaQuestionCache())->rebuild();
}
}
diff --git a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php
index 5cfaa578011..d72965d9105 100644
--- a/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php
+++ b/wcfsetup/install/files/lib/data/captcha/question/CaptchaQuestionList.class.php
@@ -3,6 +3,8 @@
namespace wcf\data\captcha\question;
use wcf\data\DatabaseObjectList;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\WCF;
/**
* Represents a list of captcha questions.
@@ -13,4 +15,31 @@
*
* @extends DatabaseObjectList
*/
-class CaptchaQuestionList extends DatabaseObjectList {}
+class CaptchaQuestionList extends DatabaseObjectList
+{
+ #[\Override]
+ public function readObjects()
+ {
+ parent::readObjects();
+
+ if ($this->objectIDs !== []) {
+ $this->loadContent();
+ }
+ }
+
+ private function loadContent(): void
+ {
+ $conditionBuilder = new PreparedStatementConditionBuilder();
+ $conditionBuilder->add("questionID IN(?)", [$this->objectIDs]);
+
+ $sql = "SELECT *
+ FROM wcf1_captcha_question_content
+ {$conditionBuilder}";
+ $statement = WCF::getDB()->prepare($sql);
+ $statement->execute($conditionBuilder->getParameters());
+
+ while ($row = $statement->fetchArray()) {
+ $this->objects[$row['questionID']]->setContent($row['languageID'], $row['question'], $row['answers']);
+ }
+ }
+}
diff --git a/wcfsetup/install/files/lib/data/captcha/question/I18nCaptchaQuestionList.class.php b/wcfsetup/install/files/lib/data/captcha/question/I18nCaptchaQuestionList.class.php
deleted file mode 100644
index 4784e731df5..00000000000
--- a/wcfsetup/install/files/lib/data/captcha/question/I18nCaptchaQuestionList.class.php
+++ /dev/null
@@ -1,28 +0,0 @@
-
- * @since 6.2
- *
- * @extends I18nDatabaseObjectList
- */
-class I18nCaptchaQuestionList extends I18nDatabaseObjectList
-{
- /**
- * @inheritDoc
- */
- public $i18nFields = ['question' => 'questionI18n'];
-
- /**
- * @inheritDoc
- */
- public $className = CaptchaQuestion::class;
-}
diff --git a/wcfsetup/install/files/lib/system/cache/builder/CaptchaQuestionCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/CaptchaQuestionCacheBuilder.class.php
index 8a91a6f8bb3..32f237801c2 100644
--- a/wcfsetup/install/files/lib/system/cache/builder/CaptchaQuestionCacheBuilder.class.php
+++ b/wcfsetup/install/files/lib/system/cache/builder/CaptchaQuestionCacheBuilder.class.php
@@ -2,7 +2,7 @@
namespace wcf\system\cache\builder;
-use wcf\data\captcha\question\CaptchaQuestionList;
+use wcf\system\cache\eager\CaptchaQuestionCache;
/**
* Caches the enabled captcha questions.
@@ -10,18 +10,20 @@
* @author Matthias Schmidt
* @copyright 2001-2019 WoltLab GmbH
* @license GNU Lesser General Public License
+ *
+ * @deprecated 6.2 use `CaptchaQuestionCache` instead
*/
-class CaptchaQuestionCacheBuilder extends AbstractCacheBuilder
+class CaptchaQuestionCacheBuilder extends AbstractLegacyCacheBuilder
{
- /**
- * @inheritDoc
- */
- public function rebuild(array $parameters)
+ #[\Override]
+ protected function rebuild(array $parameters): array
{
- $questionList = new CaptchaQuestionList();
- $questionList->getConditionBuilder()->add('isDisabled = ?', [0]);
- $questionList->readObjects();
+ return (new CaptchaQuestionCache())->getCache();
+ }
- return $questionList->getObjects();
+ #[\Override]
+ public function reset(array $parameters = [])
+ {
+ (new CaptchaQuestionCache())->rebuild();
}
}
diff --git a/wcfsetup/install/files/lib/system/cache/eager/CaptchaQuestionCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/CaptchaQuestionCache.class.php
new file mode 100644
index 00000000000..a7f22404595
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/cache/eager/CaptchaQuestionCache.class.php
@@ -0,0 +1,29 @@
+
+ * @since 6.2
+ *
+ * @extends AbstractEagerCache>
+ */
+final class CaptchaQuestionCache extends AbstractEagerCache
+{
+ #[\Override]
+ protected function getCacheData(): array
+ {
+ $questionList = new CaptchaQuestionList();
+ $questionList->getConditionBuilder()->add('isDisabled = ?', [0]);
+ $questionList->readObjects();
+
+ return $questionList->getObjects();
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/captcha/CaptchaQuestionHandler.class.php b/wcfsetup/install/files/lib/system/captcha/CaptchaQuestionHandler.class.php
index e5321901d14..17791c405c2 100644
--- a/wcfsetup/install/files/lib/system/captcha/CaptchaQuestionHandler.class.php
+++ b/wcfsetup/install/files/lib/system/captcha/CaptchaQuestionHandler.class.php
@@ -5,7 +5,7 @@
use ParagonIE\ConstantTime\Hex;
use wcf\data\captcha\question\CaptchaQuestion;
use wcf\data\captcha\question\CaptchaQuestionEditor;
-use wcf\system\cache\builder\CaptchaQuestionCacheBuilder;
+use wcf\system\cache\eager\CaptchaQuestionCache;
use wcf\system\exception\UserInputException;
use wcf\system\WCF;
use wcf\util\StringUtil;
@@ -42,7 +42,7 @@ final class CaptchaQuestionHandler implements ICaptchaHandler
public function __construct()
{
- $this->questions = CaptchaQuestionCacheBuilder::getInstance()->getData();
+ $this->questions = (new CaptchaQuestionCache())->getCache();
}
/**
diff --git a/wcfsetup/install/files/lib/system/captcha/question/command/SaveContent.class.php b/wcfsetup/install/files/lib/system/captcha/question/command/SaveContent.class.php
new file mode 100644
index 00000000000..5f586e2a7fe
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/captcha/question/command/SaveContent.class.php
@@ -0,0 +1,59 @@
+
+ * @since 6.2
+ */
+final class SaveContent
+{
+ public function __construct(
+ public readonly int $questionID,
+ /** @var array */
+ public readonly array $contents,
+ ) {
+ }
+
+ public function __invoke(): void
+ {
+ if ($this->contents === []) {
+ return;
+ }
+
+ $this->deleteOldContent($this->questionID);
+ $this->saveContent($this->questionID, $this->contents);
+ }
+
+ private function deleteOldContent(int $questionID): void
+ {
+ $sql = "DELETE FROM wcf1_captcha_question_content
+ WHERE questionID = ?";
+ $statement = WCF::getDB()->prepare($sql);
+ $statement->execute([$questionID]);
+ }
+
+ /**
+ * @param array $contents
+ */
+ private function saveContent(int $questionID, array $contents): void
+ {
+ $sql = "INSERT INTO wcf1_captcha_question_content
+ (questionID, languageID, question, answers)
+ VALUES (?, ?, ?, ?)";
+ $statement = WCF::getDB()->prepare($sql);
+
+ foreach ($contents as $languageID => $content) {
+ $statement->execute([
+ $questionID,
+ $languageID ?: null,
+ $content['question'],
+ $content['answers'],
+ ]);
+ }
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/form/builder/container/MultilingualFormContainer.class.php b/wcfsetup/install/files/lib/system/form/builder/container/MultilingualFormContainer.class.php
new file mode 100644
index 00000000000..6c1c401c25f
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/form/builder/container/MultilingualFormContainer.class.php
@@ -0,0 +1,91 @@
+
+ * @since 6.2
+ */
+final class MultilingualFormContainer extends FormContainer
+{
+ public readonly TabMenuFormContainer $tabContainer;
+
+ public function __construct()
+ {
+ parent::__construct();
+
+ $this->appendChild(
+ BooleanFormField::create('isMultilingual')
+ ->label('wcf.global.isMultilingual')
+ ->available(\count(LanguageFactory::getInstance()->getLanguages()) > 1)
+ );
+
+ $this->tabContainer = TabMenuFormContainer::create('languages')
+ ->addDependency(
+ NonEmptyFormFieldDependency::create('isMultilingual')
+ ->fieldId('isMultilingual')
+ )
+ ->available(\count(LanguageFactory::getInstance()->getLanguages()) > 1);
+ $this->appendChild($this->tabContainer);
+ }
+
+ #[\Override]
+ public static function create($id): static
+ {
+ $formField = (new static())->id($id);
+
+ foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
+ $tab = TabFormContainer::create("{$formField->getId()}_language_{$language->languageCode}")
+ ->label($language->languageName)
+ ->appendChildren([
+ FormContainer::create("{$formField->getId()}_{$language->languageCode}"),
+ ]);
+
+ $formField->tabContainer->appendChild($tab);
+ }
+
+ return $formField;
+ }
+
+ /**
+ * Returns a map of language codes to their respective tab containers.
+ *
+ * @return array
+ */
+ public function getLangaugeTabs(): array
+ {
+ $result = [];
+ foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
+ $tab = $this->tabContainer->getNodeById("{$this->getId()}_language_{$language->languageCode}");
+ \assert($tab instanceof TabFormContainer);
+
+ $result[$language->languageCode] = $tab;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns a map of language codes to their respective form containers in the tab.
+ *
+ * @return array
+ */
+ public function getLangaugeContainers(): array
+ {
+ $result = [];
+ foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
+ $container = $this->tabContainer->getNodeById("{$this->getId()}_{$language->languageCode}");
+ \assert($container instanceof FormContainer);
+
+ $result[$language->languageCode] = $container;
+ }
+
+ return $result;
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/form/builder/data/processor/MultilingualFormDataProcessor.class.php b/wcfsetup/install/files/lib/system/form/builder/data/processor/MultilingualFormDataProcessor.class.php
new file mode 100644
index 00000000000..e23c675ee07
--- /dev/null
+++ b/wcfsetup/install/files/lib/system/form/builder/data/processor/MultilingualFormDataProcessor.class.php
@@ -0,0 +1,219 @@
+
+ * @since 6.2
+ */
+final class MultilingualFormDataProcessor extends AbstractFormDataProcessor
+{
+ public const ARRAY_INDEX = 'content';
+
+ public function __construct(
+ public readonly string $contentTableName,
+ /**
+ * Mapping of field id to database column name
+ *
+ * @var array
+ */
+ public readonly array $fieldIds
+ ) {
+ }
+
+ #[\Override]
+ public function processObjectData(IFormDocument $document, array $data, IStorableObject $object)
+ {
+ if ($this->fieldIds === []) {
+ return $data;
+ }
+
+ $indexName = $object::getDatabaseTableIndexName();
+ $select = \implode(', ', \array_values($this->fieldIds));
+
+ $sql = "SELECT languageID, {$select}
+ FROM {$this->contentTableName}
+ WHERE {$indexName} = ?";
+ $statement = WCF::getDB()->prepare($sql);
+ $statement->execute([$object->{$indexName}]);
+
+ $contents = [];
+ while ($row = $statement->fetchArray()) {
+ $languageCode = $row['languageID'] ? LanguageFactory::getInstance()->getLanguage($row['languageID'])->languageCode : "";
+
+ $content = [];
+ foreach ($this->fieldIds as $fieldId => $columnName) {
+ $content[$fieldId] = $row[$columnName];
+ }
+
+ $contents[$languageCode] = $content;
+ }
+
+ if (\count($contents) > 1) {
+ $data['isMultilingual'] = 1;
+
+ foreach ($contents as $languageCode => $content) {
+ foreach (\array_keys($this->fieldIds) as $fieldId) {
+ $data["{$fieldId}_{$languageCode}"] = $content[$fieldId];
+ }
+ }
+ } else {
+ $data['isMultilingual'] = 0;
+
+ if ($contents !== []) {
+ $content = \reset($contents);
+ foreach (\array_keys($this->fieldIds) as $fieldId) {
+ $data[$fieldId] = $content[$fieldId];
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ #[\Override]
+ public function processFormData(IFormDocument $document, array $parameters)
+ {
+ $languages = LanguageFactory::getInstance()->getLanguages();
+ $parameters[self::ARRAY_INDEX] = [];
+
+ $isMultilingual = $parameters['data']['isMultilingual'] ?? false;
+ $isMultilingual = $isMultilingual && \count($languages) > 1;
+
+ $parameters['data']['isMultilingual'] = $isMultilingual ? 1 : 0;
+
+ if ($isMultilingual) {
+ $parameters = $this->removeMonolingualValues($parameters, $languages);
+ $parameters = $this->processMultilingualValues($parameters, $languages);
+ } else {
+ $parameters = $this->removeMultilingualValues($parameters, $languages);
+ $parameters = $this->processMonolingualValues($parameters);
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * @param array $parameters
+ * @param Language[] $languages
+ *
+ * @return array
+ */
+ private function removeMonolingualValues(array $parameters, array $languages): array
+ {
+ return \array_filter($parameters, function ($key) use ($languages) {
+ foreach ($this->fieldIds as $fieldId) {
+ if (!\str_starts_with($key, "{$fieldId}_")) {
+ continue;
+ }
+
+ foreach ($languages as $language) {
+ if (\str_starts_with($key, "{$fieldId}_{$language->languageCode}")) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ return true;
+ }, \ARRAY_FILTER_USE_KEY);
+ }
+
+ /**
+ * @param array $parameters
+ * @param Language[] $languages
+ *
+ * @return array
+ */
+ private function processMultilingualValues(array $parameters, array $languages): array
+ {
+ foreach ($languages as $language) {
+ foreach ($this->fieldIds as $fieldId) {
+ $languageFieldId = "{$fieldId}_{$language->languageCode}";
+
+ if (isset($parameters["data"][$languageFieldId])) {
+ $parameters[self::ARRAY_INDEX][$language->languageID][$fieldId] = $parameters["data"][$languageFieldId];
+ unset($parameters["data"][$languageFieldId]);
+ }
+
+ foreach ($parameters as $key => $value) {
+ if (\str_starts_with($key, "{$languageFieldId}_")) {
+ $index = \substr($key, \strlen($languageFieldId) + 1);
+
+ $parameters[self::ARRAY_INDEX][$language->languageID][$index] = $value;
+ unset($parameters[$key]);
+ }
+ }
+ }
+ }
+
+ return $parameters;
+ }
+
+ /**
+ * @param array $parameters
+ * @param Language[] $languages
+ *
+ * @return array
+ */
+ private function removeMultilingualValues(array $parameters, array $languages): array
+ {
+ return \array_filter($parameters, function ($key) use ($languages) {
+ foreach ($this->fieldIds as $fieldId) {
+ if (!\str_starts_with($key, "{$fieldId}_")) {
+ continue;
+ }
+
+ foreach ($languages as $language) {
+ if (\str_starts_with($key, "{$fieldId}_{$language->languageCode}")) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }, \ARRAY_FILTER_USE_KEY);
+ }
+
+ /**
+ * @param array $parameters
+ *
+ * @return array
+ */
+ private function processMonolingualValues(array $parameters): array
+ {
+ $defaultLanguageID = LanguageFactory::getInstance()->getDefaultLanguageID();
+
+ $parameters[self::ARRAY_INDEX][$defaultLanguageID] = [];
+ foreach ($this->fieldIds as $fieldId) {
+ if (isset($parameters["data"][$fieldId])) {
+ $parameters[self::ARRAY_INDEX][$defaultLanguageID][$fieldId] = $parameters["data"][$fieldId];
+ unset($parameters["data"][$fieldId]);
+ }
+
+ foreach ($parameters as $key => $value) {
+ if (\str_starts_with($key, "{$fieldId}_")) {
+ $index = \substr($key, \strlen($fieldId) + 1);
+
+ $parameters[self::ARRAY_INDEX][$defaultLanguageID][$index] = $value;
+ unset($parameters[$key]);
+ }
+ }
+ }
+
+ return $parameters;
+ }
+}
diff --git a/wcfsetup/install/files/lib/system/gridView/admin/CaptchaQuestionGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/CaptchaQuestionGridView.class.php
index bcdd8254f25..0acfbd077a4 100644
--- a/wcfsetup/install/files/lib/system/gridView/admin/CaptchaQuestionGridView.class.php
+++ b/wcfsetup/install/files/lib/system/gridView/admin/CaptchaQuestionGridView.class.php
@@ -4,20 +4,24 @@
use wcf\acp\form\CaptchaQuestionEditForm;
use wcf\data\captcha\question\CaptchaQuestion;
-use wcf\data\captcha\question\I18nCaptchaQuestionList;
+use wcf\data\captcha\question\CaptchaQuestionList;
+use wcf\data\DatabaseObject;
use wcf\event\gridView\admin\CaptchaQuestionGridViewInitialized;
use wcf\system\gridView\AbstractGridView;
-use wcf\system\gridView\filter\I18nTextFilter;
use wcf\system\gridView\filter\NumericFilter;
+use wcf\system\gridView\filter\TextFilter;
use wcf\system\gridView\GridViewColumn;
use wcf\system\gridView\GridViewRowLink;
+use wcf\system\gridView\renderer\DefaultColumnRenderer;
use wcf\system\gridView\renderer\ObjectIdColumnRenderer;
use wcf\system\interaction\admin\CaptchaQuestionInteractions;
use wcf\system\interaction\bulk\admin\CaptchaQuestionBulkInteractions;
use wcf\system\interaction\Divider;
use wcf\system\interaction\EditInteraction;
use wcf\system\interaction\ToggleInteraction;
+use wcf\system\language\MultilingualHelper;
use wcf\system\WCF;
+use wcf\util\StringUtil;
/**
* Grid view for the list of user ranks.
@@ -27,7 +31,7 @@
* @license GNU Lesser General Public License
* @since 6.2
*
- * @extends AbstractGridView
+ * @extends AbstractGridView
*/
final class CaptchaQuestionGridView extends AbstractGridView
{
@@ -41,8 +45,19 @@ public function __construct()
GridViewColumn::for('question')
->label('wcf.acp.captcha.question.question')
->titleColumn()
- ->filter(new I18nTextFilter())
- ->sortable(sortByDatabaseColumn: 'questionI18n'),
+ ->renderer(
+ new class extends DefaultColumnRenderer {
+ #[\Override]
+ public function render(mixed $value, DatabaseObject $row): string
+ {
+ \assert($row instanceof CaptchaQuestion);
+
+ return StringUtil::encodeHTML($row->getQuestion());
+ }
+ }
+ )
+ ->filter(new TextFilter($this->subqueryQuestion()))
+ ->sortable(sortByDatabaseColumn: $this->subqueryQuestion()),
GridViewColumn::for('views')
->label('wcf.acp.captcha.question.views')
->sortable()
@@ -60,7 +75,7 @@ public function __construct()
$provider = new CaptchaQuestionInteractions();
$provider->addInteractions([
new Divider(),
- new EditInteraction(CaptchaQuestionEditForm::class)
+ new EditInteraction(CaptchaQuestionEditForm::class),
]);
$this->setInteractionProvider($provider);
$this->setBulkInteractionProvider(new CaptchaQuestionBulkInteractions());
@@ -80,9 +95,9 @@ public function isAccessible(): bool
}
#[\Override]
- protected function createObjectList(): I18nCaptchaQuestionList
+ protected function createObjectList(): CaptchaQuestionList
{
- return new I18nCaptchaQuestionList();
+ return new CaptchaQuestionList();
}
#[\Override]
@@ -90,4 +105,14 @@ protected function getInitializedEvent(): CaptchaQuestionGridViewInitialized
{
return new CaptchaQuestionGridViewInitialized($this);
}
+
+ private function subqueryQuestion(): string
+ {
+ return MultilingualHelper::subqueryForContentTable(
+ "question",
+ "wcf1_captcha_question_content",
+ "questionID",
+ "captcha_question",
+ );
+ }
}
diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml
index 6af8180a16e..a3a71333ffb 100644
--- a/wcfsetup/install/lang/de.xml
+++ b/wcfsetup/install/lang/de.xml
@@ -277,6 +277,7 @@
+
diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml
index 5ee9959ff08..51e5a783ade 100644
--- a/wcfsetup/install/lang/en.xml
+++ b/wcfsetup/install/lang/en.xml
@@ -276,6 +276,7 @@
+
diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql
index f79b2a3b987..c4079b22d13 100644
--- a/wcfsetup/setup/db/install.sql
+++ b/wcfsetup/setup/db/install.sql
@@ -361,14 +361,24 @@ CREATE TABLE wcf1_box_to_page (
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,
views INT(10) NOT NULL DEFAULT 0,
correctSubmissions INT(10) NOT NULL DEFAULT 0,
incorrectSubmissions INT(10) NOT NULL DEFAULT 0
);
+DROP TABLE IF EXISTS wcf1_captcha_question_content;
+CREATE TABLE wcf1_captcha_question_content (
+ contentID INT(10) NOT NULL AUTO_INCREMENT,
+ questionID INT NOT NULL,
+ languageID INT,
+ question VARCHAR(255) NOT NULL,
+ answers MEDIUMTEXT,
+
+ PRIMARY KEY(contentID),
+ KEY id (questionID, languageID)
+);
+
DROP TABLE IF EXISTS wcf1_category;
CREATE TABLE wcf1_category (
categoryID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
@@ -2080,6 +2090,9 @@ ALTER TABLE wcf1_box_content ADD FOREIGN KEY (imageID) REFERENCES wcf1_media (me
ALTER TABLE wcf1_box_to_page ADD FOREIGN KEY (boxID) REFERENCES wcf1_box (boxID) ON DELETE CASCADE;
ALTER TABLE wcf1_box_to_page ADD FOREIGN KEY (pageID) REFERENCES wcf1_page (pageID) ON DELETE CASCADE;
+ALTER TABLE wcf1_captcha_question_content ADD FOREIGN KEY (questionID) REFERENCES wcf1_captcha_question (questionID) ON DELETE CASCADE;
+ALTER TABLE wcf1_captcha_question_content ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE CASCADE;
+
ALTER TABLE wcf1_category ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE;
ALTER TABLE wcf1_clipboard_action ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE;