From 34c0e2dabed83bef58776e328b4aae8b644a3181 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 6 May 2025 22:09:11 +0200 Subject: [PATCH 01/20] Add database structure --- .../update_com.woltlab.wcf_6.2_step1.php | 29 +++++++++++++++++-- wcfsetup/setup/db/install.sql | 11 +++++++ 2 files changed, 38 insertions(+), 2 deletions(-) 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 144efaffb3..d16517b161 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,10 @@ 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\DatabaseTable; use wcf\system\database\table\index\DatabaseTableForeignKey; +use wcf\system\database\table\index\DatabaseTablePrimaryIndex; use wcf\system\database\table\PartialDatabaseTable; return [ @@ -58,4 +59,28 @@ ->columns([ IntDatabaseTableColumn::create('uploadTime'), ]), + DatabaseTable::create('wcf1_user_rank_content') + ->columns([ + IntDatabaseTableColumn::create('rankID') + ->notNull(), + IntDatabaseTableColumn::create('languageID') + ->notNull(), + NotNullVarchar255DatabaseTableColumn::create('title'), + ]) + ->indices([ + DatabaseTablePrimaryIndex::create() + ->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/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 185ebbf105..edc8df975c 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1958,6 +1958,14 @@ 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 ( + rankID INT NOT NULL, + languageID INT NOT NULL, + title VARCHAR(255) NOT NULL, + PRIMARY KEY (rankID, languageID) +); + DROP TABLE IF EXISTS wcf1_user_session; CREATE TABLE wcf1_user_session ( sessionID CHAR(40) NOT NULL PRIMARY KEY, @@ -2294,6 +2302,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; From 8e065747a406013f250d99645a8369f651c6e9ec Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 6 May 2025 22:09:24 +0200 Subject: [PATCH 02/20] Add migration script --- .../update_com.woltlab.wcf_6.2_userRank.php | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 wcfsetup/install/files/acp/update_com.woltlab.wcf_6.2_userRank.php 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 0000000000..501a17d201 --- /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, + LanguageFactory::getInstance()->getDefaultLanguageID(), + $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()); +} From 57648b1da768a9a85f356bfb47afd15399ed3d20 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 6 May 2025 22:13:39 +0200 Subject: [PATCH 03/20] Moving language values for content in the user ranks to the installation script --- .../acp/install_com.woltlab.wcf_step2.php | 39 +++++++++++++------ wcfsetup/install/lang/de.xml | 18 ++++----- wcfsetup/install/lang/en.xml | 18 ++++----- 3 files changed, 44 insertions(+), 31 deletions(-) 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 7f0455495b..0bac7fb877 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,38 @@ ]); // 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) { + if (!isset($rankTitles[$language->languageCode])) { + continue; + } + + $statement->execute([ + $userRank->rankID, + $language->languageID, + $rankTitles[$language->languageCode], + ]); + } } // update administrator user rank and user online marking diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 72edadeb1c..6af8180a16 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 931c376811..5ee9959ff0 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}]]> + + + + + + + + From 0c1511aabf3e720c5139a20126109e585f6436f6 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 6 May 2025 22:27:19 +0200 Subject: [PATCH 04/20] Load rank titles in `UserRankList` --- .../data/user/rank/I18nUserRankList.class.php | 2 + .../lib/data/user/rank/UserRank.class.php | 50 ++++++++++++++++++- .../lib/data/user/rank/UserRankList.class.php | 31 +++++++++++- .../gridView/admin/UserRankGridView.class.php | 35 ++++++++++--- 4 files changed, 109 insertions(+), 9 deletions(-) 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 0b8be07a88..d33aa93bc7 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 087285f3ad..5bce50f188 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; @@ -29,6 +30,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 +62,47 @@ public function getImage() */ public function getTitle(): string { - return WCF::getLanguage()->get($this->rankTitle); + $this->loadTitles(); + + if ($this->titles === []) { + // Backwards compatibility + return WCF::getLanguage()->get($this->rankTitle); + } + + 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 = $statement->fetchMap('languageID', 'title'); + } + + /** + * @since 6.2 + */ + public function setRankTitle(int $languageID, string $title): void + { + if (!isset($this->titles)) { + $this->titles = []; + } + + $this->titles[$languageID] = $title; } /** 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 8b11dee7de..9e4cc2f52e 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/gridView/admin/UserRankGridView.class.php b/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php index 8bc801036f..b55cf4b335 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\LanguageFactory; 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,24 @@ private function getAvailableUserGroups(): array return $groups; } + + private function subqueryRankTitle(): string + { + $preferredLanguageID = WCF::getLanguage()->languageID; + $defaultLanguageID = LanguageFactory::getInstance()->getDefaultLanguageID(); + + return << Date: Tue, 6 May 2025 22:27:56 +0200 Subject: [PATCH 05/20] Implement eager cache for user ranks --- .../files/lib/data/user/UserProfile.class.php | 6 ++-- .../data/user/rank/UserRankEditor.class.php | 3 ++ .../builder/UserRankCacheBuilder.class.php | 22 ++++++++------- .../cache/eager/UserRankCache.class.php | 28 +++++++++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/cache/eager/UserRankCache.class.php diff --git a/wcfsetup/install/files/lib/data/user/UserProfile.class.php b/wcfsetup/install/files/lib/data/user/UserProfile.class.php index 3913e01066..4afee6416a 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())->getCache()[$this->rankID] ?? null; } /** 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 4fe9a9ca6a..cc956c64b9 100644 --- a/wcfsetup/install/files/lib/data/user/rank/UserRankEditor.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/UserRankEditor.class.php @@ -5,6 +5,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. @@ -30,5 +31,7 @@ class UserRankEditor extends DatabaseObjectEditor implements IEditableCachedObje public static function resetCache() { UserRankCacheBuilder::getInstance()->reset(); + + (new UserRankCache())->rebuild(); } } 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 878a834d50..0f64bdd88a 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; } + + #[\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 0000000000..8515470d2d --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/eager/UserRankCache.class.php @@ -0,0 +1,28 @@ + + * @since 6.2 + * + * @extends AbstractEagerCache> + */ +final class UserRankCache extends AbstractEagerCache +{ + #[\Override] + protected function getCacheData(): array + { + $userRankList = new UserRankList(); + $userRankList->readObjects(); + + return $userRankList->getObjects(); + } +} From aa1fe9da5280a114e4637bb4a50e9dc8ba64addc Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 6 May 2025 22:43:41 +0200 Subject: [PATCH 06/20] Save user rank content through the form builder --- .../lib/acp/form/UserRankAddForm.class.php | 26 ++- .../data/user/rank/UserRankAction.class.php | 42 +--- .../builder/field/II18nFormField.class.php | 4 + .../builder/field/TI18nFormField.class.php | 219 +++++++++++++++--- .../user/rank/command/SaveContent.class.php | 58 +++++ 5 files changed, 278 insertions(+), 71 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php diff --git a/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php b/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php index e00b2f96bd..25a392a603 100644 --- a/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php @@ -66,7 +66,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 +125,31 @@ protected function finalizeForm() parent::finalizeForm(); $this->form->getDataHandler() + ->addProcessor( + new CustomFormDataProcessor( + 'rankTitleDataProcessor', + null, + static function (IFormDocument $document, array $data, IStorableObject $object) { + \assert($object instanceof UserRank); + + $sql = "SELECT title, languageID + FROM wcf1_user_rank_content + WHERE rankID = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([$object->rankID]); + + $data["rankTitle"] = $statement->fetchMap('languageID', 'title'); + + if (\count($data["rankTitle"]) === 1) { + $data["rankTitle"] = \reset($data["rankTitle"]); + } elseif ($data["rankTitle"] === []) { + $data["rankTitle"] = ''; + } + + return $data; + } + ) + ) ->addProcessor( new CustomFormDataProcessor( 'requiredGenderProcessor', 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 e50ba463c4..1677ab0109 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/system/form/builder/field/II18nFormField.class.php b/wcfsetup/install/files/lib/system/form/builder/field/II18nFormField.class.php index ccadc2a774..6e542e8a40 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 c66a4d5c52..0d5bf81289 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[LanguageFactory::getInstance()->getDefaultLanguageID()] = $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/user/rank/command/SaveContent.class.php b/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php new file mode 100644 index 0000000000..7feed617a0 --- /dev/null +++ b/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php @@ -0,0 +1,58 @@ + + * @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; + } + + WCF::getDB()->beginTransaction(); + + $this->deleteOldContent($this->rankID); + $this->saveContent($this->rankID, $this->titles); + + WCF::getDB()->commitTransaction(); + } + + 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, $title]); + } + } +} From cb218472fed10c4142a88675bb13bc074d4bd4db Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Tue, 6 May 2025 23:04:31 +0200 Subject: [PATCH 07/20] Implement `I18nFormDataProcessor` to handle values from a content table --- .../lib/acp/form/UserRankAddForm.class.php | 26 ++------ .../processor/I18nFormDataProcessor.class.php | 64 +++++++++++++++++++ 2 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/form/builder/data/processor/I18nFormDataProcessor.class.php diff --git a/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php b/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php index 25a392a603..57ee462e21 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; @@ -126,28 +127,9 @@ protected function finalizeForm() $this->form->getDataHandler() ->addProcessor( - new CustomFormDataProcessor( - 'rankTitleDataProcessor', - null, - static function (IFormDocument $document, array $data, IStorableObject $object) { - \assert($object instanceof UserRank); - - $sql = "SELECT title, languageID - FROM wcf1_user_rank_content - WHERE rankID = ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([$object->rankID]); - - $data["rankTitle"] = $statement->fetchMap('languageID', 'title'); - - if (\count($data["rankTitle"]) === 1) { - $data["rankTitle"] = \reset($data["rankTitle"]); - } elseif ($data["rankTitle"] === []) { - $data["rankTitle"] = ''; - } - - return $data; - } + new I18nFormDataProcessor( + 'wcf1_user_rank_content', + ['rankTitle' => 'title'] ) ) ->addProcessor( 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 0000000000..b3654261dd --- /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']] = $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; + } +} From 8ad0570392649846d0e8ab5c8c363376e2f396d0 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 7 May 2025 10:29:27 +0200 Subject: [PATCH 08/20] Remove unnecessary backwards compatibility code --- wcfsetup/install/files/lib/data/user/rank/UserRank.class.php | 5 ----- .../files/lib/data/user/rank/UserRankEditor.class.php | 3 --- 2 files changed, 8 deletions(-) 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 5bce50f188..651f550796 100644 --- a/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php @@ -64,11 +64,6 @@ public function getTitle(): string { $this->loadTitles(); - if ($this->titles === []) { - // Backwards compatibility - return WCF::getLanguage()->get($this->rankTitle); - } - return $this->titles[WCF::getLanguage()->languageID] ?? $this->titles[LanguageFactory::getInstance()->getDefaultLanguageID()] ?? \reset($this->titles); 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 cc956c64b9..d122e9366c 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,6 @@ use wcf\data\DatabaseObjectEditor; use wcf\data\IEditableCachedObject; -use wcf\system\cache\builder\UserRankCacheBuilder; use wcf\system\cache\eager\UserRankCache; /** @@ -30,8 +29,6 @@ class UserRankEditor extends DatabaseObjectEditor implements IEditableCachedObje */ public static function resetCache() { - UserRankCacheBuilder::getInstance()->reset(); - (new UserRankCache())->rebuild(); } } From a649abdad1318fc4c8d1c1a8a3a6a29764f88033 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 7 May 2025 10:30:50 +0200 Subject: [PATCH 09/20] Remove `rankTitle` property from `UserRank` --- wcfsetup/install/files/lib/data/user/rank/UserRank.class.php | 1 - 1 file changed, 1 deletion(-) 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 651f550796..c27f83e5e4 100644 --- a/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php @@ -19,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 From 448aa14be67756a696ac985b45dcbf2f7242c49e Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Wed, 7 May 2025 10:50:55 +0200 Subject: [PATCH 10/20] Add a data processor to handle the value `none` for the CSS class name --- .../lib/acp/form/UserRankAddForm.class.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php b/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php index 57ee462e21..c0c3367444 100644 --- a/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php +++ b/wcfsetup/install/files/lib/acp/form/UserRankAddForm.class.php @@ -145,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; } ) From 8897a3a61cd3fd8ca847aa729c91d7ab5110ac82 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 8 May 2025 10:30:28 +0200 Subject: [PATCH 11/20] Set `null` as monolingual `langaugeID` --- .../acp/database/update_com.woltlab.wcf_6.2_step1.php | 9 +++++++-- .../install/files/lib/data/user/rank/UserRank.class.php | 4 ++-- .../system/form/builder/field/TI18nFormField.class.php | 2 +- .../lib/system/user/rank/command/SaveContent.class.php | 2 +- wcfsetup/setup/db/install.sql | 7 +++++-- 5 files changed, 16 insertions(+), 8 deletions(-) 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 d16517b161..03fecbab89 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 @@ -11,8 +11,10 @@ use wcf\system\database\table\column\IntDatabaseTableColumn; use wcf\system\database\table\column\MediumtextDatabaseTableColumn; 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; @@ -61,14 +63,17 @@ ]), DatabaseTable::create('wcf1_user_rank_content') ->columns([ + ObjectIdDatabaseTableColumn::create('contentID'), IntDatabaseTableColumn::create('rankID') ->notNull(), - IntDatabaseTableColumn::create('languageID') - ->notNull(), + IntDatabaseTableColumn::create('languageID'), NotNullVarchar255DatabaseTableColumn::create('title'), ]) ->indices([ DatabaseTablePrimaryIndex::create() + ->columns(['contentID']), + DatabaseTableIndex::create('id') + ->type(DatabaseTableIndex::UNIQUE_TYPE) ->columns(['rankID', 'languageID']), ]) ->foreignKeys([ 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 c27f83e5e4..3d89061e43 100644 --- a/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php @@ -90,13 +90,13 @@ protected function loadTitles(): void /** * @since 6.2 */ - public function setRankTitle(int $languageID, string $title): void + public function setRankTitle(?int $languageID, string $title): void { if (!isset($this->titles)) { $this->titles = []; } - $this->titles[$languageID] = $title; + $this->titles[$languageID ?: 0] = $title; } /** 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 0d5bf81289..0243ec9110 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 @@ -391,7 +391,7 @@ function (IFormDocument $document, array $parameters) { if ($this->hasI18nValues()) { $values = $this->getValue(); } else { - $values[LanguageFactory::getInstance()->getDefaultLanguageID()] = $this->getValue(); + $values[0] = $this->getValue(); } $parameters[$this->getObjectProperty()] = $values; diff --git a/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php b/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php index 7feed617a0..707d9d2982 100644 --- a/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php +++ b/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php @@ -52,7 +52,7 @@ private function saveContent(int $rankID, array $titles): void $statement = WCF::getDB()->prepare($sql); foreach ($titles as $languageID => $title) { - $statement->execute([$rankID, $languageID, $title]); + $statement->execute([$rankID, $languageID ?: null, $title]); } } } diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index edc8df975c..a5eba55e81 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1960,10 +1960,13 @@ CREATE TABLE wcf1_user_rank ( 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 NOT NULL, + languageID INT, title VARCHAR(255) NOT NULL, - PRIMARY KEY (rankID, languageID) + + PRIMARY KEY(contentID), + UNIQUE KEY id (rankID, languageID) ); DROP TABLE IF EXISTS wcf1_user_session; From 0b5686895754acab739a4ad196dc5dead48fe839 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 8 May 2025 10:34:25 +0200 Subject: [PATCH 12/20] Prevent the execution of an additional (unnecessary) SQL query --- .../lib/acp/form/UserRankEditForm.class.php | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php b/wcfsetup/install/files/lib/acp/form/UserRankEditForm.class.php index ecf27131ed..4aeffb0c94 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 */ From 0e84953d19bd09c0b201ff8a44ad2b2710d2bde0 Mon Sep 17 00:00:00 2001 From: Olaf Braun Date: Thu, 8 May 2025 17:54:17 +0200 Subject: [PATCH 13/20] Update wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php Co-authored-by: Alexander Ebert --- wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php | 4 ---- 1 file changed, 4 deletions(-) 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 0bac7fb877..237a2fad09 100644 --- a/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php +++ b/wcfsetup/install/files/acp/install_com.woltlab.wcf_step2.php @@ -47,10 +47,6 @@ ]); foreach (LanguageFactory::getInstance()->getLanguages() as $language) { - if (!isset($rankTitles[$language->languageCode])) { - continue; - } - $statement->execute([ $userRank->rankID, $language->languageID, From 5444bafd6e665994f961a0a3b8895a1f847fcc2e Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 8 May 2025 17:51:30 +0200 Subject: [PATCH 14/20] Don't use transaction here --- .../files/lib/system/user/rank/command/SaveContent.class.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php b/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php index 707d9d2982..5cd87e869d 100644 --- a/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php +++ b/wcfsetup/install/files/lib/system/user/rank/command/SaveContent.class.php @@ -25,12 +25,8 @@ public function __invoke(): void return; } - WCF::getDB()->beginTransaction(); - $this->deleteOldContent($this->rankID); $this->saveContent($this->rankID, $this->titles); - - WCF::getDB()->commitTransaction(); } private function deleteOldContent(int $rankID): void From ac89843d2d07fb333cb7e01e5ac570483ea76eb1 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 8 May 2025 17:53:48 +0200 Subject: [PATCH 15/20] Implement `getRank()` method in `UserRankCache` to simply get the user rank --- wcfsetup/install/files/lib/data/user/UserProfile.class.php | 2 +- .../lib/system/cache/builder/UserRankCacheBuilder.class.php | 2 +- .../files/lib/system/cache/eager/UserRankCache.class.php | 5 +++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/wcfsetup/install/files/lib/data/user/UserProfile.class.php b/wcfsetup/install/files/lib/data/user/UserProfile.class.php index 4afee6416a..be68d8b124 100644 --- a/wcfsetup/install/files/lib/data/user/UserProfile.class.php +++ b/wcfsetup/install/files/lib/data/user/UserProfile.class.php @@ -939,7 +939,7 @@ public function getRank(): ?UserRank return null; } - return (new UserRankCache())->getCache()[$this->rankID] ?? null; + return (new UserRankCache())->getRank($this->rankID); } /** 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 0f64bdd88a..2aaf451b30 100644 --- a/wcfsetup/install/files/lib/system/cache/builder/UserRankCacheBuilder.class.php +++ b/wcfsetup/install/files/lib/system/cache/builder/UserRankCacheBuilder.class.php @@ -24,7 +24,7 @@ protected function rebuild(array $parameters): array public function getRank(int $rankID): ?UserRank { - return $this->getData()[$rankID] ?? null; + return (new UserRankCache())->getRank($rankID); } #[\Override] diff --git a/wcfsetup/install/files/lib/system/cache/eager/UserRankCache.class.php b/wcfsetup/install/files/lib/system/cache/eager/UserRankCache.class.php index 8515470d2d..58225becec 100644 --- a/wcfsetup/install/files/lib/system/cache/eager/UserRankCache.class.php +++ b/wcfsetup/install/files/lib/system/cache/eager/UserRankCache.class.php @@ -25,4 +25,9 @@ protected function getCacheData(): array return $userRankList->getObjects(); } + + public function getRank(int $rankID): ?UserRank + { + return $this->getCache()[$rankID] ?? null; + } } From edfb753b9b376a8a7e2fefc10c55dc43f7da9821 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Thu, 8 May 2025 17:59:09 +0200 Subject: [PATCH 16/20] Do not use `UNIQUE KEY`, as the value `null` can be included --- .../files/acp/database/update_com.woltlab.wcf_6.2_step1.php | 1 - wcfsetup/setup/db/install.sql | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) 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 03fecbab89..d34fe5609e 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 @@ -73,7 +73,6 @@ DatabaseTablePrimaryIndex::create() ->columns(['contentID']), DatabaseTableIndex::create('id') - ->type(DatabaseTableIndex::UNIQUE_TYPE) ->columns(['rankID', 'languageID']), ]) ->foreignKeys([ diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index a5eba55e81..f79b2a3b98 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -1966,7 +1966,7 @@ CREATE TABLE wcf1_user_rank_content ( title VARCHAR(255) NOT NULL, PRIMARY KEY(contentID), - UNIQUE KEY id (rankID, languageID) + KEY id (rankID, languageID) ); DROP TABLE IF EXISTS wcf1_user_session; From 0761068546fce75985795f29786e3d4896b0719a Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Fri, 9 May 2025 09:18:17 +0200 Subject: [PATCH 17/20] Implement helper function to get the subquery for the column of the content table --- .../gridView/admin/UserRankGridView.class.php | 24 +++------- .../language/MultilingualHelper.class.php | 48 +++++++++++++++++++ 2 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 wcfsetup/install/files/lib/system/language/MultilingualHelper.class.php 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 b55cf4b335..b52a2b95de 100644 --- a/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php +++ b/wcfsetup/install/files/lib/system/gridView/admin/UserRankGridView.class.php @@ -20,7 +20,7 @@ use wcf\system\interaction\bulk\admin\UserRankBulkInteractions; use wcf\system\interaction\Divider; use wcf\system\interaction\EditInteraction; -use wcf\system\language\LanguageFactory; +use wcf\system\language\MultilingualHelper; use wcf\system\WCF; use wcf\util\StringUtil; @@ -154,21 +154,11 @@ private function getAvailableUserGroups(): array private function subqueryRankTitle(): string { - $preferredLanguageID = WCF::getLanguage()->languageID; - $defaultLanguageID = LanguageFactory::getInstance()->getDefaultLanguageID(); - - return << + * @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 $objectIDColum, + string $baseTable, + ?int $preferredLanguageID = null + ): string { + if ($preferredLanguageID === null) { + $preferredLanguageID = WCF::getLanguage()->languageID; + } + $defaultLanguageID = LanguageFactory::getInstance()->getDefaultLanguageID(); + + return << Date: Fri, 9 May 2025 09:21:41 +0200 Subject: [PATCH 18/20] Fix spelling for `$objectIDColumn` --- .../files/lib/system/language/MultilingualHelper.class.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wcfsetup/install/files/lib/system/language/MultilingualHelper.class.php b/wcfsetup/install/files/lib/system/language/MultilingualHelper.class.php index 2ae348aae7..86927e3dbe 100644 --- a/wcfsetup/install/files/lib/system/language/MultilingualHelper.class.php +++ b/wcfsetup/install/files/lib/system/language/MultilingualHelper.class.php @@ -22,7 +22,7 @@ final class MultilingualHelper public static function subqueryForContentTable( string $selectColumn, string $contentTableName, - string $objectIDColum, + string $objectIDColumn, string $baseTable, ?int $preferredLanguageID = null ): string { @@ -35,7 +35,7 @@ public static function subqueryForContentTable( ( SELECT {$selectColumn} FROM {$contentTableName} - WHERE {$objectIDColum} = {$baseTable}.{$objectIDColum} + WHERE {$objectIDColumn} = {$baseTable}.{$objectIDColumn} ORDER BY CASE WHEN languageID = {$preferredLanguageID} THEN -2 WHEN languageID = {$defaultLanguageID} THEN -1 From 4a8dad81c1187873e849442c3d6c6a2f5f995a18 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 12 May 2025 10:39:54 +0200 Subject: [PATCH 19/20] Use the langaugeID `null` as `0` array-key instead of an empty string --- wcfsetup/install/files/lib/data/user/rank/UserRank.class.php | 5 ++++- .../builder/data/processor/I18nFormDataProcessor.class.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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 3d89061e43..caa4fbc4bc 100644 --- a/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php +++ b/wcfsetup/install/files/lib/data/user/rank/UserRank.class.php @@ -84,7 +84,10 @@ protected function loadTitles(): void $statement = WCF::getDB()->prepare($sql); $statement->execute([$this->rankID]); - $this->titles = $statement->fetchMap('languageID', 'title'); + $this->titles = []; + while ($row = $statement->fetchArray()) { + $this->titles[$row['languageID'] ?: 0] = $row['title']; + } } /** 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 index b3654261dd..6b46d2cce6 100644 --- 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 @@ -46,7 +46,7 @@ public function processObjectData(IFormDocument $document, array $data, IStorabl while ($row = $statement->fetchArray()) { foreach ($this->fieldIds as $fieldId => $columnName) { - $data[$fieldId][$row['languageID']] = $row[$columnName]; + $data[$fieldId][$row['languageID'] ?: 0] = $row[$columnName]; } } From 31156de6536d7eaf3cc6d8655b3eb2d648731a65 Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 12 May 2025 11:08:23 +0200 Subject: [PATCH 20/20] Use `null` instead of the default language id for monolingual content --- .../install/files/acp/update_com.woltlab.wcf_6.2_userRank.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 501a17d201..dc991e23ec 100644 --- 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 @@ -30,7 +30,7 @@ } else { $statement->execute([ $rankID, - LanguageFactory::getInstance()->getDefaultLanguageID(), + null, $title, ]); }