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 144efaffb35..d34fe5609eb 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 @@ -10,9 +10,12 @@ use wcf\system\database\table\column\IntDatabaseTableColumn; use wcf\system\database\table\column\MediumtextDatabaseTableColumn; -use wcf\system\database\table\column\TextDatabaseTableColumn; -use wcf\system\database\table\column\TinyintDatabaseTableColumn; +use wcf\system\database\table\column\NotNullVarchar255DatabaseTableColumn; +use wcf\system\database\table\column\ObjectIdDatabaseTableColumn; +use wcf\system\database\table\DatabaseTable; use wcf\system\database\table\index\DatabaseTableForeignKey; +use wcf\system\database\table\index\DatabaseTableIndex; +use wcf\system\database\table\index\DatabaseTablePrimaryIndex; use wcf\system\database\table\PartialDatabaseTable; return [ @@ -58,4 +61,30 @@ ->columns([ IntDatabaseTableColumn::create('uploadTime'), ]), + DatabaseTable::create('wcf1_user_rank_content') + ->columns([ + ObjectIdDatabaseTableColumn::create('contentID'), + IntDatabaseTableColumn::create('rankID') + ->notNull(), + IntDatabaseTableColumn::create('languageID'), + NotNullVarchar255DatabaseTableColumn::create('title'), + ]) + ->indices([ + DatabaseTablePrimaryIndex::create() + ->columns(['contentID']), + DatabaseTableIndex::create('id') + ->columns(['rankID', 'languageID']), + ]) + ->foreignKeys([ + DatabaseTableForeignKey::create() + ->columns(['rankID']) + ->referencedTable('wcf1_user_rank') + ->referencedColumns(['rankID']) + ->onDelete('CASCADE'), + DatabaseTableForeignKey::create() + ->columns(['languageID']) + ->referencedTable('wcf1_language') + ->referencedColumns(['languageID']) + ->onDelete('CASCADE'), + ]), ]; diff --git a/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php b/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php index 7f0455495b0..237a2fad091 100644 --- a/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php +++ b/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php @@ -7,6 +7,7 @@ use wcf\data\user\UserEditor; use wcf\data\user\UserProfileAction; use wcf\system\image\adapter\ImagickImageAdapter; +use wcf\system\language\LanguageFactory; use wcf\system\WCF; // set default landing page @@ -24,22 +25,34 @@ ]); // install default user ranks +$sql = "INSERT INTO wcf1_user_rank_content + (rankID, languageID, title) + VALUES (?, ?, ?)"; +$statement = WCF::getDB()->prepare($sql); + foreach ([ - [4, 0, 'wcf.user.rank.administrator', 'blue'], - [5, 0, 'wcf.user.rank.moderator', 'blue'], - [3, 0, 'wcf.user.rank.user0', ''], - [3, 300, 'wcf.user.rank.user1', ''], - [3, 900, 'wcf.user.rank.user2', ''], - [3, 3000, 'wcf.user.rank.user3', ''], - [3, 9000, 'wcf.user.rank.user4', ''], - [3, 15000, 'wcf.user.rank.user5', ''], -] as [$groupID, $requiredPoints, $rankTitle, $cssClassName]) { - UserRankEditor::create([ + [4, 0, ['de' => 'Administrator', 'en' => 'Administrator'], 'blue'], + [5, 0, ['de' => 'Moderator', 'en' => 'Moderator'], 'blue'], + [3, 0, ['de' => 'Anfänger', 'en' => 'Beginner'], ''], + [3, 300, ['de' => 'Schüler', 'en' => 'Student'], ''], + [3, 900, ['de' => 'Fortgeschrittener', 'en' => 'Intermediate'], ''], + [3, 3000, ['de' => 'Profi', 'en' => 'Professional'], ''], + [3, 9000, ['de' => 'Meister', 'en' => 'Master'], ''], + [3, 15000, ['de' => 'Erleuchteter', 'en' => 'Enlightened'], ''], +] as [$groupID, $requiredPoints, $rankTitles, $cssClassName]) { + $userRank = UserRankEditor::create([ 'groupID' => $groupID, 'requiredPoints' => $requiredPoints, - 'rankTitle' => $rankTitle, 'cssClassName' => $cssClassName, ]); + + foreach (LanguageFactory::getInstance()->getLanguages() as $language) { + $statement->execute([ + $userRank->rankID, + $language->languageID, + $rankTitles[$language->languageCode], + ]); + } } // update administrator user rank and user online marking diff --git a/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_userRank.php b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_userRank.php new file mode 100644 index 00000000000..dc991e23ec1 --- /dev/null +++ b/wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_userRank.php @@ -0,0 +1,47 @@ +prepare($sql); +$statement->execute(); +$titles = $statement->fetchMap('rankID', 'rankTitle'); + +$sql = "INSERT INTO wcf1_user_rank_content + (rankID, languageID, title) + VALUES (?, ?, ?)"; +$statement = WCF::getDB()->prepare($sql); + +$languageItems = []; +foreach ($titles as $rankID => $title) { + if (\preg_match('~^wcf\.user\.rank\.\w+$~', $title, $matches)) { + $languageItems[] = $title; + + foreach (LanguageFactory::getInstance()->getLanguages() as $language) { + $statement->execute([ + $rankID, + $language->languageID, + $language->get($title), + ]); + } + } else { + $statement->execute([ + $rankID, + null, + $title, + ]); + } +} + +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/UserRankAddForm.class.php b/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php index e00b2f96bdc..c0c3367444c 100644 --- a/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php @@ -9,6 +9,7 @@ use wcf\form\AbstractFormBuilderForm; use wcf\system\form\builder\container\FormContainer; use wcf\system\form\builder\data\processor\CustomFormDataProcessor; +use wcf\system\form\builder\data\processor\I18nFormDataProcessor; use wcf\system\form\builder\field\BadgeColorFormField; use wcf\system\form\builder\field\BooleanFormField; use wcf\system\form\builder\field\IntegerFormField; @@ -66,7 +67,6 @@ public function createForm() TextFormField::create('rankTitle') ->label('wcf.acp.user.rank.title') ->i18n() - ->languageItemPattern('wcf.user.rank.\w+') ->required(), BadgeColorFormField::create('cssClassName') ->label('wcf.acp.user.rank.cssClassName') @@ -126,6 +126,12 @@ protected function finalizeForm() parent::finalizeForm(); $this->form->getDataHandler() + ->addProcessor( + new I18nFormDataProcessor( + 'wcf1_user_rank_content', + ['rankTitle' => 'title'] + ) + ) ->addProcessor( new CustomFormDataProcessor( 'requiredGenderProcessor', @@ -139,6 +145,25 @@ function (IFormDocument $document, array $data, IStorableObject $object) { $data['requiredGender'] = $data['requiredGender'] ?: null; + return $data; + } + ) + ) + ->addProcessor( + new CustomFormDataProcessor( + 'cssClassNameDataProcessor', + static function (IFormDocument $document, array $parameters) { + if (isset($parameters['data']['cssClassName']) && $parameters['data']['cssClassName'] === 'none') { + $parameters['data']['cssClassName'] = ''; + } + + return $parameters; + }, + static function (IFormDocument $document, array $data, IStorableObject $object) { + \assert($object instanceof UserRank); + + $data['cssClassName'] = $data['cssClassName'] ?: 'none'; + return $data; } ) diff --git a/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php b/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php index ecf27131ed2..4aeffb0c949 100644 --- a/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php @@ -2,11 +2,14 @@ namespace wcf\acp\form; -use wcf\acp\page\UserRankListPage; use CuyZ\Valinor\Mapper\MappingError; +use wcf\acp\page\UserRankListPage; +use wcf\data\IStorableObject; use wcf\data\user\rank\UserRank; use wcf\http\Helper; use wcf\system\exception\IllegalLinkException; +use wcf\system\form\builder\data\processor\CustomFormDataProcessor; +use wcf\system\form\builder\IFormDocument; use wcf\system\interaction\admin\UserRankInteractions; use wcf\system\interaction\StandaloneInteractionContextMenuView; use wcf\system\request\LinkHandler; @@ -58,6 +61,34 @@ public function readParameters() } } + #[\Override] + protected function finalizeForm() + { + parent::finalizeForm(); + + // The `DeleteInteraction` in `UserRankInteractions` outputs the title and would otherwise execute an additional SQL query. + $this->form->getDataHandler() + ->addProcessor( + new CustomFormDataProcessor( + 'setRankTitlesFormDataProcessor', + null, + static function (IFormDocument $document, array $data, IStorableObject $object) { + \assert($object instanceof UserRank); + + if (\is_array($data['rankTitle'])) { + foreach ($data['rankTitle'] as $languageID => $rankTitle) { + $object->setRankTitle($languageID, $rankTitle); + } + } else { + $object->setRankTitle(null, $data['rankTitle']); + } + + return $data; + } + ) + ); + } + /** * @inheritDoc */ diff --git a/wcfsetup/install/files/lib/data/user/UserProfile.class.php b/wcfsetup/install/files/lib/data/user/UserProfile.class.php index 3913e010668..be68d8b124c 100644 --- a/wcfsetup/install/files/lib/data/user/UserProfile.class.php +++ b/wcfsetup/install/files/lib/data/user/UserProfile.class.php @@ -19,7 +19,7 @@ use wcf\data\user\option\ViewableUserOption; use wcf\data\user\rank\UserRank; use wcf\system\cache\builder\UserGroupPermissionCacheBuilder; -use wcf\system\cache\builder\UserRankCacheBuilder; +use wcf\system\cache\eager\UserRankCache; use wcf\system\cache\runtime\FileRuntimeCache; use wcf\system\cache\runtime\UserProfileRuntimeCache; use wcf\system\database\util\PreparedStatementConditionBuilder; @@ -923,7 +923,7 @@ public function getUserTitle() return $this->userTitle; } if ($this->getRank() && $this->getRank()->showTitle()) { - return WCF::getLanguage()->get($this->getRank()->rankTitle); + return $this->getRank()->getTitle(); } return ''; @@ -939,7 +939,7 @@ public function getRank(): ?UserRank return null; } - return UserRankCacheBuilder::getInstance()->getRank($this->rankID); + return (new UserRankCache())->getRank($this->rankID); } /** diff --git a/wcfsetup/install/files/lib/data/user/rank/I18nUserRankList.class.php b/wcfsetup/install/files/lib/data/user/rank/I18nUserRankList.class.php index 0b8be07a888..d33aa93bc76 100644 --- a/wcfsetup/install/files/lib/data/user/rank/I18nUserRankList.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/I18nUserRankList.class.php @@ -13,6 +13,8 @@ * @since 6.0 * * @extends I18nDatabaseObjectList + * + * @deprecated 6.2 use `UserRankList` instead */ class I18nUserRankList extends I18nDatabaseObjectList { diff --git a/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php b/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php index 087285f3ad1..caa4fbc4bca 100644 --- a/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php @@ -5,6 +5,7 @@ use wcf\data\DatabaseObject; use wcf\data\ITitledObject; use wcf\system\form\builder\field\UploadFormField; +use wcf\system\language\LanguageFactory; use wcf\system\WCF; use wcf\util\StringUtil; @@ -18,7 +19,6 @@ * @property-read int $rankID unique id of the user rank * @property-read int $groupID id of the user group to which the user rank belongs * @property-read int $requiredPoints minimum number of user activity points required for a user to get the user rank - * @property-read string $rankTitle title of the user rank or name of the language item which contains the rank * @property-read string $cssClassName css class name used when displaying the user rank * @property-read string $rankImage (WCF relative) path to the image displayed next to the rank or empty if no rank image exists * @property-read int $repeatImage number of times the rank image is displayed @@ -29,6 +29,13 @@ class UserRank extends DatabaseObject implements ITitledObject { public const RANK_IMAGE_DIR = 'images/rank/'; + /** + * @var array + * + * @since 6.2 + */ + protected array $titles; + /** * Returns the image of this user rank. * @@ -54,7 +61,45 @@ public function getImage() */ public function getTitle(): string { - return WCF::getLanguage()->get($this->rankTitle); + $this->loadTitles(); + + return $this->titles[WCF::getLanguage()->languageID] + ?? $this->titles[LanguageFactory::getInstance()->getDefaultLanguageID()] + ?? \reset($this->titles); + } + + /** + * @since 6.2 + */ + protected function loadTitles(): void + { + if (isset($this->titles)) { + return; + } + + $sql = "SELECT languageID, title + FROM wcf1_user_rank_content + WHERE rankID = ?"; + + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$this->rankID]); + + $this->titles = []; + while ($row = $statement->fetchArray()) { + $this->titles[$row['languageID'] ?: 0] = $row['title']; + } + } + + /** + * @since 6.2 + */ + public function setRankTitle(?int $languageID, string $title): void + { + if (!isset($this->titles)) { + $this->titles = []; + } + + $this->titles[$languageID ?: 0] = $title; } /** diff --git a/wcfsetup/install/files/lib/data/user/rank/UserRankAction.class.php b/wcfsetup/install/files/lib/data/user/rank/UserRankAction.class.php index e50ba463c41..1677ab01091 100644 --- a/wcfsetup/install/files/lib/data/user/rank/UserRankAction.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/UserRankAction.class.php @@ -3,9 +3,9 @@ namespace wcf\data\user\rank; use wcf\data\AbstractDatabaseObjectAction; -use wcf\data\TI18nDatabaseObjectAction; use wcf\system\exception\InvalidObjectArgument; use wcf\system\file\upload\UploadFile; +use wcf\system\user\rank\command\SaveContent; /** * Executes user rank-related actions. @@ -18,8 +18,6 @@ */ class UserRankAction extends AbstractDatabaseObjectAction { - use TI18nDatabaseObjectAction; - /** * @inheritDoc */ @@ -38,7 +36,9 @@ public function create() /** @var UserRank $rank */ $rank = parent::create(); - $this->saveI18nValue($rank); + if (isset($this->parameters['rankTitle'])) { + (new SaveContent($rank->rankID, $this->parameters['rankTitle']))(); + } if (isset($this->parameters['rankImageFile']) && !empty($this->parameters['rankImageFile'])) { $rankImageFile = \reset($this->parameters['rankImageFile']); @@ -125,36 +125,10 @@ public function update() parent::update(); - foreach ($this->objects as $object) { - $this->saveI18nValue($object->getDecoratedObject()); + if (isset($this->parameters['rankTitle'])) { + foreach ($this->objects as $editor) { + (new SaveContent($editor->rankID, $this->parameters['rankTitle']))(); + } } } - - #[\Override] - public function delete() - { - $count = parent::delete(); - - $this->deleteI18nValues(); - - return $count; - } - - /** - * @return array - */ - public function getI18nSaveTypes(): array - { - return ['rankTitle' => 'wcf.user.rank.userRank\d+']; - } - - public function getLanguageCategory(): string - { - return 'wcf.user.rank'; - } - - public function getPackageID(): int - { - return PACKAGE_ID; - } } diff --git a/wcfsetup/install/files/lib/data/user/rank/UserRankEditor.class.php b/wcfsetup/install/files/lib/data/user/rank/UserRankEditor.class.php index 4fe9a9ca6ae..d122e9366ce 100644 --- a/wcfsetup/install/files/lib/data/user/rank/UserRankEditor.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/UserRankEditor.class.php @@ -4,7 +4,7 @@ use wcf\data\DatabaseObjectEditor; use wcf\data\IEditableCachedObject; -use wcf\system\cache\builder\UserRankCacheBuilder; +use wcf\system\cache\eager\UserRankCache; /** * Provides functions to edit user ranks. @@ -29,6 +29,6 @@ class UserRankEditor extends DatabaseObjectEditor implements IEditableCachedObje */ public static function resetCache() { - UserRankCacheBuilder::getInstance()->reset(); + (new UserRankCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/data/user/rank/UserRankList.class.php b/wcfsetup/install/files/lib/data/user/rank/UserRankList.class.php index 8b11dee7de7..9e4cc2f52ef 100644 --- a/wcfsetup/install/files/lib/data/user/rank/UserRankList.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/UserRankList.class.php @@ -3,6 +3,8 @@ namespace wcf\data\user\rank; use wcf\data\DatabaseObjectList; +use wcf\system\database\util\PreparedStatementConditionBuilder; +use wcf\system\WCF; /** * Represents a list of user ranks. @@ -13,4 +15,31 @@ * * @extends DatabaseObjectList */ -class UserRankList extends DatabaseObjectList {} +class UserRankList extends DatabaseObjectList +{ + #[\Override] + public function readObjects() + { + parent::readObjects(); + + if ($this->objectIDs !== []) { + $this->loadRankTitles(); + } + } + + private function loadRankTitles(): void + { + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add("rankID IN(?)", [$this->objectIDs]); + + $sql = "SELECT * + FROM wcf1_user_rank_content + {$conditionBuilder}"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute($conditionBuilder->getParameters()); + + while ($row = $statement->fetchArray()) { + $this->objects[$row['rankID']]->setRankTitle($row['languageID'], $row['title']); + } + } +} diff --git a/wcfsetup/install/files/lib/system/cache/builder/UserRankCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/UserRankCacheBuilder.class.php index 878a834d505..2aaf451b309 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/UserRankCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/UserRankCacheBuilder.class.php @@ -3,7 +3,7 @@ namespace wcf\system\cache\builder; use wcf\data\user\rank\UserRank; -use wcf\data\user\rank\UserRankList; +use wcf\system\cache\eager\UserRankCache; /** * Caches the list of user ranks. @@ -12,22 +12,24 @@ * @copyright 2001-2024 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.1 + * @deprecated 6.2 use `UserRankCache` instead */ -final class UserRankCacheBuilder extends AbstractCacheBuilder +final class UserRankCacheBuilder extends AbstractLegacyCacheBuilder { - /** - * @inheritDoc - */ - public function rebuild(array $parameters) + #[\Override] + protected function rebuild(array $parameters): array { - $list = new UserRankList(); - $list->readObjects(); - - return $list->getObjects(); + return (new UserRankCache())->getCache(); } public function getRank(int $rankID): ?UserRank { - return $this->getData()[$rankID] ?? null; + return (new UserRankCache())->getRank($rankID); + } + + #[\Override] + public function reset(array $parameters = []) + { + (new UserRankCache())->rebuild(); } } diff --git a/wcfsetup/install/files/lib/system/cache/eager/UserRankCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/UserRankCache.class.php new file mode 100644 index 00000000000..58225bececd --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/UserRankCache.class.php @@ -0,0 +1,33 @@ + + * @since 6.2 + * + * @extends AbstractEagerCache> + */ +final class UserRankCache extends AbstractEagerCache +{ + #[\Override] + protected function getCacheData(): array + { + $userRankList = new UserRankList(); + $userRankList->readObjects(); + + return $userRankList->getObjects(); + } + + public function getRank(int $rankID): ?UserRank + { + return $this->getCache()[$rankID] ?? null; + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/data/processor/I18nFormDataProcessor.class.php b/wcfsetup/install/files/lib/system/form/builder/data/processor/I18nFormDataProcessor.class.php new file mode 100644 index 00000000000..6b46d2cce61 --- /dev/null +++ b/wcfsetup/install/files/lib/system/form/builder/data/processor/I18nFormDataProcessor.class.php @@ -0,0 +1,64 @@ + + * @since 6.2 + */ +final class I18nFormDataProcessor extends AbstractFormDataProcessor +{ + 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; + } + + $select = \implode(', ', \array_values($this->fieldIds)); + + $sql = "SELECT languageID, {$select} + FROM {$this->contentTableName} + WHERE {$object::getDatabaseTableIndexName()} = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$object->{$object::getDatabaseTableIndexName()}]); + + foreach (\array_keys($this->fieldIds) as $fieldId) { + $data[$fieldId] = []; + } + + while ($row = $statement->fetchArray()) { + foreach ($this->fieldIds as $fieldId => $columnName) { + $data[$fieldId][$row['languageID'] ?: 0] = $row[$columnName]; + } + } + + foreach (\array_keys($this->fieldIds) as $fieldId) { + if (\count($data[$fieldId]) === 1) { + // monolingual + $data[$fieldId] = \reset($data[$fieldId]); + } elseif ($data[$fieldId] === []) { + $data[$fieldId] = ''; + } + } + + return $data; + } +} diff --git a/wcfsetup/install/files/lib/system/form/builder/field/II18nFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/II18nFormField.class.php index ccadc2a774b..6e542e8a40e 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/II18nFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/II18nFormField.class.php @@ -18,6 +18,8 @@ interface II18nFormField extends IFormField * @return string language item pattern * * @throws \BadMethodCallException if i18n is disabled for this field or no language item has been set + * + * @deprecated 6.2 */ public function getLanguageItemPattern(); @@ -81,6 +83,8 @@ public function isI18nRequired(); * * @throws \BadMethodCallException if i18n is disabled for this field * @throws \InvalidArgumentException if the given pattern is invalid + * + * @deprecated 6.2 */ public function languageItemPattern($pattern); } diff --git a/wcfsetup/install/files/lib/system/form/builder/field/TI18nFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/TI18nFormField.class.php index c66a4d5c52b..0243ec9110d 100644 --- a/wcfsetup/install/files/lib/system/form/builder/field/TI18nFormField.class.php +++ b/wcfsetup/install/files/lib/system/form/builder/field/TI18nFormField.class.php @@ -4,6 +4,7 @@ use wcf\data\IStorableObject; use wcf\data\language\item\LanguageItemList; +use wcf\data\language\Language; use wcf\system\form\builder\data\processor\CustomFormDataProcessor; use wcf\system\form\builder\exception\InvalidFormFieldValue; use wcf\system\form\builder\field\validation\FormFieldValidationError; @@ -52,17 +53,31 @@ trait TI18nFormField * Returns additional template variables used to generate the html representation * of this node. * - * @return array{}|array{elementIdentifier: string, forceSelection: bool} + * @return array{}|array{elementIdentifier: string, forceSelection: bool}|array{availableLanguages: Language[], i18nValues: array>, elementIdentifier: string, forceSelection: bool} */ public function getHtmlVariables() { if ($this->isI18n()) { - I18nHandler::getInstance()->assignVariables(); - - return [ - 'elementIdentifier' => $this->getPrefixedId(), - 'forceSelection' => $this->isI18nRequired(), - ]; + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + I18nHandler::getInstance()->assignVariables(); + + return [ + 'elementIdentifier' => $this->getPrefixedId(), + 'forceSelection' => $this->isI18nRequired(), + ]; + } else { + $i18nValues = \is_array($this->value) ? $this->value : []; + + return [ + 'availableLanguages' => LanguageFactory::getInstance()->getLanguages(), + 'i18nValues' => [ + $this->getPrefixedId() => $i18nValues, + ], + 'elementIdentifier' => $this->getPrefixedId(), + 'forceSelection' => $this->isI18nRequired(), + ]; + } } return []; @@ -74,6 +89,8 @@ public function getHtmlVariables() * @return string language item pattern * * @throws \BadMethodCallException if i18n is disabled for this field or no language item has been set + * + * @deprecated 6.2 */ public function getLanguageItemPattern() { @@ -121,9 +138,19 @@ public function getValue() { if ($this->isI18n()) { if ($this->hasPlainValue()) { - return I18nHandler::getInstance()->getValue($this->getPrefixedId()); + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + return I18nHandler::getInstance()->getValue($this->getPrefixedId()); + } else { + return $this->value; + } } elseif ($this->hasI18nValues()) { - $values = I18nHandler::getInstance()->getValues($this->getPrefixedId()); + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + $values = I18nHandler::getInstance()->getValues($this->getPrefixedId()); + } else { + $values = $this->value; + } // handle legacy values from the past when multilingual values // were available @@ -152,7 +179,12 @@ public function getValue() */ public function hasI18nValues() { - return I18nHandler::getInstance()->hasI18nValues($this->getPrefixedId()); + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + return I18nHandler::getInstance()->hasI18nValues($this->getPrefixedId()); + } else { + return \is_array($this->value); + } } /** @@ -163,7 +195,12 @@ public function hasI18nValues() */ public function hasPlainValue() { - return I18nHandler::getInstance()->isPlainValue($this->getPrefixedId()); + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + return I18nHandler::getInstance()->isPlainValue($this->getPrefixedId()); + } else { + return \is_string($this->value) || \is_numeric($this->value); + } } /** @@ -179,7 +216,16 @@ public function hasPlainValue() */ public function hasSaveValue() { - return !$this->isI18n() || $this->hasPlainValue(); + if ($this->isI18n()) { + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + return $this->hasPlainValue(); + } + + return false; + } + + return true; } /** @@ -218,7 +264,7 @@ public function getJavaScriptDataHandlerModule(): string */ public function i18nRequired($i18nRequired = true) { - $this->i18nRequired = $i18nRequired; + $this->i18nRequired = $i18nRequired && \count(LanguageFactory::getInstance()->getLanguages()) > 1; $this->i18n(); return $this; @@ -255,6 +301,8 @@ public function isI18nRequired() * * @throws \BadMethodCallException if i18n is disabled for this field * @throws \InvalidArgumentException if the given pattern is invalid + * + * @deprecated 6.2 */ public function languageItemPattern($pattern) { @@ -286,10 +334,15 @@ public function updatedObject(array $data, IStorableObject $object, $loadValues $value = $data[$this->getObjectProperty()]; if ($this->isI18n()) { - // do not use `I18nHandler::setOptions()` because then `I18nHandler` only - // reads the values when assigning the template variables and the values - // are not available in this class via `getValue()` - $this->setStringValue($value); + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + // do not use `I18nHandler::setOptions()` because then `I18nHandler` only + // reads the values when assigning the template variables and the values + // are not available in this class via `getValue()` + $this->setStringValue($value); + } else { + $this->value = $value; + } } else { $this->value = $value; } @@ -313,16 +366,35 @@ public function populate() parent::populate(); if ($this->isI18n()) { - I18nHandler::getInstance()->unregister($this->getPrefixedId()); - I18nHandler::getInstance()->register($this->getPrefixedId()); + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + I18nHandler::getInstance()->unregister($this->getPrefixedId()); + I18nHandler::getInstance()->register($this->getPrefixedId()); + } /** @var IFormDocument $document */ $document = $this->getDocument(); $document->getDataHandler()->addProcessor(new CustomFormDataProcessor( 'i18n', function (IFormDocument $document, array $parameters) { - if ($this->checkDependencies() && $this->hasI18nValues()) { - $parameters[$this->getObjectProperty() . '_i18n'] = $this->getValue(); + if (!$this->checkDependencies()) { + return $parameters; + } + + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + if ($this->hasI18nValues()) { + $parameters[$this->getObjectProperty() . '_i18n'] = $this->getValue(); + } + } else { + $values = []; + if ($this->hasI18nValues()) { + $values = $this->getValue(); + } else { + $values[0] = $this->getValue(); + } + + $parameters[$this->getObjectProperty()] = $values; } return $parameters; @@ -341,7 +413,24 @@ function (IFormDocument $document, array $parameters) { public function readValue() { if ($this->isI18n()) { - I18nHandler::getInstance()->readValues($this->getDocument()->getRequestData()); + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + I18nHandler::getInstance()->readValues($this->getDocument()->getRequestData()); + } else { + if ($this->getDocument()->hasRequestData("{$this->getPrefixedId()}_i18n")) { + $value = $this->getDocument()->getRequestData("{$this->getPrefixedId()}_i18n"); + + if (\is_array($value)) { + $this->value = $value; + } + } else { + $value = $this->getDocument()->getRequestData($this->getPrefixedId()); + + if (\is_string($value)) { + $this->value = StringUtil::trim($value); + } + } + } } elseif ($this->getDocument()->hasRequestData($this->getPrefixedId())) { $value = $this->getDocument()->getRequestData($this->getPrefixedId()); @@ -361,6 +450,8 @@ public function readValue() * * @param string $value set value * @return void + * + * @deprecated 6.2 */ protected function setStringValue($value) { @@ -399,10 +490,20 @@ public function value($value) { if ($this->isI18n()) { if (\is_string($value) || \is_numeric($value)) { - $this->setStringValue($value); + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + $this->setStringValue($value); + } else { + return parent::value($value); + } } elseif (\is_array($value)) { - if (!empty($value)) { - I18nHandler::getInstance()->setValues($this->getPrefixedId(), $value); + if ($value !== []) { + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + I18nHandler::getInstance()->setValues($this->getPrefixedId(), $value); + } else { + return parent::value($value); + } } } else { throw new InvalidFormFieldValue($this, 'string/number/array', \gettype($value)); @@ -433,18 +534,64 @@ public function validate() // as invalid even though it is a valid state for this form field, // thus the additional condition. if ($this->isI18n() && (!empty(ArrayUtil::trim($this->getValue())) || $this->isRequired())) { - if ( - !I18nHandler::getInstance()->validateValue( - $this->getPrefixedId(), - $this->isI18nRequired(), - !$this->isRequired() - ) - ) { - if ($this->hasPlainValue()) { - $this->addValidationError(new FormFieldValidationError('empty')); - } else { - $this->addValidationError(new FormFieldValidationError('multilingual')); + if ($this->languageItemPattern !== null) { + // backwards compatibility pre 6.2 + if ( + !I18nHandler::getInstance()->validateValue( + $this->getPrefixedId(), + $this->isI18nRequired(), + !$this->isRequired() + ) + ) { + if ($this->hasPlainValue()) { + $this->addValidationError(new FormFieldValidationError('empty')); + } else { + $this->addValidationError(new FormFieldValidationError('multilingual')); + } } + } else { + $this->validate18nValues(); + } + } + } + + private function validate18nValues(): void + { + if ($this->hasPlainValue()) { + if (!$this->isRequired()) { + return; + } + + if ($this->isI18nRequired()) { + $this->addValidationError(new FormFieldValidationError('empty')); + + return; + } + + if ($this->value === '') { + $this->addValidationError(new FormFieldValidationError('empty')); + + return; + } + } + + if ($this->isI18nRequired() && \count(ArrayUtil::trim($this->getValue())) === 0) { + $this->addValidationError(new FormFieldValidationError('multilingual')); + + return; + } + + foreach (LanguageFactory::getInstance()->getLanguages() as $language) { + if (!isset($this->value[$language->languageID])) { + $this->addValidationError(new FormFieldValidationError('multilingual')); + + return; + } + + if ($this->isRequired() && $this->value[$language->languageID] === '') { + $this->addValidationError(new FormFieldValidationError('multilingual')); + + return; } } } diff --git a/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php index 8bc801036f4..b52a2b95de8 100644 --- a/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php @@ -5,12 +5,12 @@ use wcf\acp\form\UserRankEditForm; use wcf\data\DatabaseObject; use wcf\data\user\group\UserGroup; -use wcf\data\user\rank\I18nUserRankList; use wcf\data\user\rank\UserRank; +use wcf\data\user\rank\UserRankList; use wcf\event\gridView\admin\UserRankGridViewInitialized; use wcf\system\gridView\AbstractGridView; -use wcf\system\gridView\filter\I18nTextFilter; use wcf\system\gridView\filter\SelectFilter; +use wcf\system\gridView\filter\TextFilter; use wcf\system\gridView\GridViewColumn; use wcf\system\gridView\GridViewRowLink; use wcf\system\gridView\renderer\DefaultColumnRenderer; @@ -20,6 +20,7 @@ use wcf\system\interaction\bulk\admin\UserRankBulkInteractions; use wcf\system\interaction\Divider; use wcf\system\interaction\EditInteraction; +use wcf\system\language\MultilingualHelper; use wcf\system\WCF; use wcf\util\StringUtil; @@ -31,7 +32,7 @@ * @license GNU Lesser General Public License * @since 6.2 * - * @extends AbstractGridView + * @extends AbstractGridView */ final class UserRankGridView extends AbstractGridView { @@ -44,9 +45,9 @@ public function __construct() ->sortable(), GridViewColumn::for('rankTitle') ->label('wcf.acp.user.rank.title') - ->sortable(true, 'rankTitleI18n') + ->sortable(sortByDatabaseColumn: $this->subqueryRankTitle()) ->titleColumn() - ->filter(new I18nTextFilter()) + ->filter(new TextFilter($this->subqueryRankTitle())) ->renderer([ new class extends DefaultColumnRenderer { public function render(mixed $value, DatabaseObject $row): string @@ -127,9 +128,9 @@ public function isAccessible(): bool } #[\Override] - protected function createObjectList(): I18nUserRankList + protected function createObjectList(): UserRankList { - return new I18nUserRankList(); + return new UserRankList(); } #[\Override] @@ -150,4 +151,14 @@ private function getAvailableUserGroups(): array return $groups; } + + private function subqueryRankTitle(): string + { + return MultilingualHelper::subqueryForContentTable( + "title", + "wcf1_user_rank_content", + "rankID", + "user_rank", + ); + } } diff --git a/wcfsetup/install/files/lib/system/language/MultilingualHelper.class.php b/wcfsetup/install/files/lib/system/language/MultilingualHelper.class.php new file mode 100644 index 00000000000..86927e3dbe0 --- /dev/null +++ b/wcfsetup/install/files/lib/system/language/MultilingualHelper.class.php @@ -0,0 +1,48 @@ + + * @since 6.2 + */ +final class MultilingualHelper +{ + /** + * Returns a subquery which returns the column in the content table depending on the preferred language, + * default language and if not available the content of the lowest languageID. + */ + public static function subqueryForContentTable( + string $selectColumn, + string $contentTableName, + string $objectIDColumn, + string $baseTable, + ?int $preferredLanguageID = null + ): string { + if ($preferredLanguageID === null) { + $preferredLanguageID = WCF::getLanguage()->languageID; + } + $defaultLanguageID = LanguageFactory::getInstance()->getDefaultLanguageID(); + + return << + * @since 6.2 + */ +final class SaveContent +{ + public function __construct( + public readonly int $rankID, + /** @var array */ + public readonly array $titles + ) { + } + + public function __invoke(): void + { + if ($this->titles === []) { + return; + } + + $this->deleteOldContent($this->rankID); + $this->saveContent($this->rankID, $this->titles); + } + + private function deleteOldContent(int $rankID): void + { + $sql = "DELETE FROM wcf1_user_rank_content + WHERE rankID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$rankID]); + } + + /** + * @param array $titles + */ + private function saveContent(int $rankID, array $titles): void + { + $sql = "INSERT INTO wcf1_user_rank_content + (rankID, languageID, title) + VALUES (?, ?, ?)"; + $statement = WCF::getDB()->prepare($sql); + + foreach ($titles as $languageID => $title) { + $statement->execute([$rankID, $languageID ?: null, $title]); + } + } +} diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 72edadeb1cc..6af8180a16e 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -5578,16 +5578,6 @@ Benachrichtigungen auf {PAGE_TITLE|phra

{@$message|newlineToBreak}

]]> - - - - - - - - - - @@ -7645,5 +7635,13 @@ Erlaubte Dateiendungen: {', '|implode:$allowedFileExtensions}]]> + + + + + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 931c376811e..5ee9959ff08 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -5580,16 +5580,6 @@ your notifications on
{PAGE_TITLE|phras

{@$message|newlineToBreak}

]]> - - - - - - - - - - @@ -7536,5 +7526,13 @@ Allowed extensions: {', '|implode:$allowedFileExtensions}]]> + + + + + + + + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 185ebbf105a..f79b2a3b987 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1958,6 +1958,17 @@ CREATE TABLE wcf1_user_rank ( hideTitle TINYINT(1) NOT NULL DEFAULT 0 ); +DROP TABLE IF EXISTS wcf1_user_rank_content; +CREATE TABLE wcf1_user_rank_content ( + contentID INT(10) NOT NULL AUTO_INCREMENT, + rankID INT NOT NULL, + languageID INT, + title VARCHAR(255) NOT NULL, + + PRIMARY KEY(contentID), + KEY id (rankID, languageID) +); + DROP TABLE IF EXISTS wcf1_user_session; CREATE TABLE wcf1_user_session ( sessionID CHAR(40) NOT NULL PRIMARY KEY, @@ -2294,6 +2305,9 @@ ALTER TABLE wcf1_user_profile_menu_item ADD FOREIGN KEY (packageID) REFERENCES w ALTER TABLE wcf1_user_rank ADD FOREIGN KEY (groupID) REFERENCES wcf1_user_group (groupID) ON DELETE CASCADE; +ALTER TABLE wcf1_user_rank_content ADD FOREIGN KEY (rankID) REFERENCES wcf1_user_rank (rankID) ON DELETE CASCADE; +ALTER TABLE wcf1_user_rank_content ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE CASCADE; + ALTER TABLE wcf1_user_activity_event ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; ALTER TABLE wcf1_user_activity_event ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE; ALTER TABLE wcf1_user_activity_event ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE SET NULL;