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 a7bfc88c4ec..14e7290488c 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,6 +10,7 @@
use wcf\system\database\table\column\IntDatabaseTableColumn;
use wcf\system\database\table\column\MediumtextDatabaseTableColumn;
+use wcf\system\database\table\column\VarcharDatabaseTableColumn;
use wcf\system\database\table\index\DatabaseTableForeignKey;
use wcf\system\database\table\index\DatabaseTableIndex;
use wcf\system\database\table\PartialDatabaseTable;
@@ -26,6 +27,8 @@
IntDatabaseTableColumn::create('avatarFileID')
->length(10)
->defaultValue(null),
+ VarcharDatabaseTableColumn::create('avatarPathname')
+ ->defaultValue(null),
IntDatabaseTableColumn::create('coverPhotoFileID')
->length(10)
->defaultValue(null),
diff --git a/wcfsetup/install/files/lib/data/user/TUserAvatarObjectList.class.php b/wcfsetup/install/files/lib/data/user/TUserAvatarObjectList.class.php
deleted file mode 100644
index 645465f5dd0..00000000000
--- a/wcfsetup/install/files/lib/data/user/TUserAvatarObjectList.class.php
+++ /dev/null
@@ -1,36 +0,0 @@
-
- *
- * @property UserProfile[] $objects
- * @mixin UserProfileList
- *
- * @since 6.2
- */
-trait TUserAvatarObjectList
-{
- protected function cacheAvatarFiles(): void
- {
- $avatarFileIDs = [];
- foreach ($this->objects as $user) {
- if ($user->avatarFileID !== null) {
- $avatarFileIDs[] = $user->avatarFileID;
- }
- }
- if ($avatarFileIDs === []) {
- return;
- }
-
- FileRuntimeCache::getInstance()->cacheObjectIDs($avatarFileIDs);
- }
-}
diff --git a/wcfsetup/install/files/lib/data/user/User.class.php b/wcfsetup/install/files/lib/data/user/User.class.php
index c0e4f42303d..5c0e0527487 100644
--- a/wcfsetup/install/files/lib/data/user/User.class.php
+++ b/wcfsetup/install/files/lib/data/user/User.class.php
@@ -49,6 +49,7 @@
* @property-read string $registrationIpAddress ip address of the user at the time of registration or empty if user has been created manually or if no ip address are logged
* @property-read int|null $avatarID id of the user's avatar or null if they have no avatar
* @property-read int|null $avatarFileID id of the user's avatar core file or null if they have no avatar
+ * @property-read string|null $avatarPathname pathname of the user's avatar relative to the core itself
* @property-read int $disableAvatar is `1` if the user's avatar has been disabled, otherwise `0`
* @property-read string $disableAvatarReason reason why the user's avatar is disabled
* @property-read int $disableAvatarExpires timestamp at which the user's avatar will automatically be enabled again
diff --git a/wcfsetup/install/files/lib/data/user/UserProfile.class.php b/wcfsetup/install/files/lib/data/user/UserProfile.class.php
index 3913e010668..8b91c5c036a 100644
--- a/wcfsetup/install/files/lib/data/user/UserProfile.class.php
+++ b/wcfsetup/install/files/lib/data/user/UserProfile.class.php
@@ -10,6 +10,7 @@
use wcf\data\user\avatar\AvatarDecorator;
use wcf\data\user\avatar\DefaultAvatar;
use wcf\data\user\avatar\IUserAvatar;
+use wcf\data\user\avatar\StaticAvatar;
use wcf\data\user\cover\photo\DefaultUserCoverPhoto;
use wcf\data\user\cover\photo\IUserCoverPhoto;
use wcf\data\user\cover\photo\UserCoverPhoto;
@@ -353,19 +354,8 @@ public function getAvatar()
$avatar = null;
if (!$this->disableAvatar) {
if ($this->canSeeAvatar()) {
- if ($this->avatarFileID !== null) {
- $data = UserStorageHandler::getInstance()->getField('avatar', $this->userID);
- if ($data === null) {
- $avatar = FileRuntimeCache::getInstance()->getObject($this->avatarFileID);
-
- UserStorageHandler::getInstance()->update(
- $this->userID,
- 'avatar',
- \serialize($avatar)
- );
- } else {
- $avatar = \unserialize($data);
- }
+ if ($this->avatarPathname !== null) {
+ $avatar = new StaticAvatar($this->avatarPathname);
} else {
$parameters = ['avatar' => null];
EventHandler::getInstance()->fireAction($this, 'getAvatar', $parameters);
diff --git a/wcfsetup/install/files/lib/data/user/UserProfileList.class.php b/wcfsetup/install/files/lib/data/user/UserProfileList.class.php
index 8820bf30e64..20d481140b7 100644
--- a/wcfsetup/install/files/lib/data/user/UserProfileList.class.php
+++ b/wcfsetup/install/files/lib/data/user/UserProfileList.class.php
@@ -15,8 +15,6 @@
*/
class UserProfileList extends UserList
{
- use TUserAvatarObjectList;
-
/**
* @inheritDoc
*/
@@ -56,8 +54,6 @@ public function readObjects()
parent::readObjects();
- $this->cacheAvatarFiles();
-
$coverPhotoFileIDs = [];
foreach ($this->objects as $object) {
if ($object->coverPhotoFileID) {
diff --git a/wcfsetup/install/files/lib/data/user/avatar/StaticAvatar.class.php b/wcfsetup/install/files/lib/data/user/avatar/StaticAvatar.class.php
new file mode 100644
index 00000000000..570bc0e305e
--- /dev/null
+++ b/wcfsetup/install/files/lib/data/user/avatar/StaticAvatar.class.php
@@ -0,0 +1,65 @@
+
+ * @since 6.2
+ */
+final class StaticAvatar implements IUserAvatar, ISafeFormatAvatar
+{
+ private readonly string $src;
+
+ public function __construct(string $pathname)
+ {
+ $this->src = WCF::getPath() . $pathname;
+ }
+
+ #[\Override]
+ public function getImageTag($size = null)
+ {
+ if ($size === null) {
+ $size = UserAvatarFileProcessor::AVATAR_SIZE;
+ }
+
+ return '
';
+ }
+
+ #[\Override]
+ public function getSafeURL(?int $size = null): string
+ {
+ return $this->getURL($size);
+ }
+
+ #[\Override]
+ public function getSafeImageTag(?int $size = null): string
+ {
+ return '
';
+ }
+
+ #[\Override]
+ public function getURL($size = null)
+ {
+ return $this->src;
+ }
+
+ #[\Override]
+ public function getHeight()
+ {
+ return UserAvatarFileProcessor::AVATAR_SIZE;
+ }
+
+ #[\Override]
+ public function getWidth()
+ {
+ return UserAvatarFileProcessor::AVATAR_SIZE;
+ }
+}
diff --git a/wcfsetup/install/files/lib/data/user/follow/UserFollowerList.class.php b/wcfsetup/install/files/lib/data/user/follow/UserFollowerList.class.php
index 232ed2c3202..670587b7c81 100644
--- a/wcfsetup/install/files/lib/data/user/follow/UserFollowerList.class.php
+++ b/wcfsetup/install/files/lib/data/user/follow/UserFollowerList.class.php
@@ -2,7 +2,6 @@
namespace wcf\data\user\follow;
-use wcf\data\user\TUserAvatarObjectList;
use wcf\data\user\User;
use wcf\data\user\UserProfile;
@@ -21,8 +20,6 @@
*/
class UserFollowerList extends UserFollowList
{
- use TUserAvatarObjectList;
-
/**
* @inheritDoc
*/
@@ -50,18 +47,10 @@ public function __construct()
{
parent::__construct();
- $this->sqlSelects .= "user_table.username, user_table.email, user_table.disableAvatar";
+ $this->sqlSelects .= "user_table.username, user_table.email, user_table.disableAvatar, user_table.avatarPathname";
$this->sqlJoins .= "
LEFT JOIN wcf1_user user_table
ON user_table.userID = user_follow.userID";
}
-
- #[\Override]
- public function readObjects()
- {
- parent::readObjects();
-
- $this->cacheAvatarFiles();
- }
}
diff --git a/wcfsetup/install/files/lib/data/user/ignore/ViewableUserIgnoreList.class.php b/wcfsetup/install/files/lib/data/user/ignore/ViewableUserIgnoreList.class.php
index 6a419c2c171..c5848178fe2 100644
--- a/wcfsetup/install/files/lib/data/user/ignore/ViewableUserIgnoreList.class.php
+++ b/wcfsetup/install/files/lib/data/user/ignore/ViewableUserIgnoreList.class.php
@@ -2,7 +2,6 @@
namespace wcf\data\user\ignore;
-use wcf\data\user\TUserAvatarObjectList;
use wcf\data\user\User;
use wcf\data\user\UserProfile;
@@ -15,8 +14,6 @@
*/
class ViewableUserIgnoreList extends UserIgnoreList
{
- use TUserAvatarObjectList;
-
/**
* @inheritDoc
*/
@@ -58,12 +55,4 @@ public function __construct()
$this->sqlSelects .= ", user_table.*";
}
-
- #[\Override]
- public function readObjects()
- {
- parent::readObjects();
-
- $this->cacheAvatarFiles();
- }
}
diff --git a/wcfsetup/install/files/lib/data/user/online/UsersOnlineList.class.php b/wcfsetup/install/files/lib/data/user/online/UsersOnlineList.class.php
index 45d652816ab..bd573cc120c 100644
--- a/wcfsetup/install/files/lib/data/user/online/UsersOnlineList.class.php
+++ b/wcfsetup/install/files/lib/data/user/online/UsersOnlineList.class.php
@@ -5,7 +5,6 @@
use wcf\data\option\OptionAction;
use wcf\data\session\SessionList;
use wcf\data\user\group\UserGroup;
-use wcf\data\user\TUserAvatarObjectList;
use wcf\data\user\User;
use wcf\data\user\UserProfile;
use wcf\system\event\EventHandler;
@@ -24,8 +23,6 @@
*/
class UsersOnlineList extends SessionList
{
- use TUserAvatarObjectList;
-
/**
* @inheritDoc
*/
@@ -91,8 +88,6 @@ public function readObjects()
}
$this->objectIDs = $this->indexToObject;
$this->rewind();
-
- $this->cacheAvatarFiles();
}
/**
diff --git a/wcfsetup/install/files/lib/data/user/profile/visitor/UserProfileVisitorList.class.php b/wcfsetup/install/files/lib/data/user/profile/visitor/UserProfileVisitorList.class.php
index 871ee4b8fe0..fa693b8152b 100644
--- a/wcfsetup/install/files/lib/data/user/profile/visitor/UserProfileVisitorList.class.php
+++ b/wcfsetup/install/files/lib/data/user/profile/visitor/UserProfileVisitorList.class.php
@@ -3,7 +3,6 @@
namespace wcf\data\user\profile\visitor;
use wcf\data\DatabaseObjectList;
-use wcf\data\user\TUserAvatarObjectList;
use wcf\data\user\User;
use wcf\data\user\UserProfile;
@@ -18,8 +17,6 @@
*/
class UserProfileVisitorList extends DatabaseObjectList
{
- use TUserAvatarObjectList;
-
/**
* @inheritDoc
*/
@@ -42,18 +39,10 @@ public function __construct()
{
parent::__construct();
- $this->sqlSelects .= "user_table.username, user_table.email, user_table.disableAvatar";
+ $this->sqlSelects .= "user_table.username, user_table.email, user_table.disableAvatar, user_table.avatarPathname";
$this->sqlJoins .= "
LEFT JOIN wcf1_user user_table
ON user_table.userID = user_profile_visitor.userID";
}
-
- #[\Override]
- public function readObjects()
- {
- parent::readObjects();
-
- $this->cacheAvatarFiles();
- }
}
diff --git a/wcfsetup/install/files/lib/system/file/processor/AbstractFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/AbstractFileProcessor.class.php
index 21634bd5a02..df62dfa0e84 100644
--- a/wcfsetup/install/files/lib/system/file/processor/AbstractFileProcessor.class.php
+++ b/wcfsetup/install/files/lib/system/file/processor/AbstractFileProcessor.class.php
@@ -97,4 +97,10 @@ public function getImageCropperConfiguration(): ?ImageCropperConfiguration
// Do not crop images.
return null;
}
+
+ #[\Override]
+ public function replacedWithWebpVariant(File $file): void
+ {
+ // There is usually no need to react to this change.
+ }
}
diff --git a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php
index d8a847ace0c..e3e90a93ac9 100644
--- a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php
+++ b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php
@@ -438,7 +438,15 @@ public function convertImageFormat(File $file): File
case 'webp':
$command = new ReplaceWithWebpVariant($file);
- return $command();
+ $newFile = $command();
+
+ // The files identity differs if the file has been replaced.
+ if ($file !== $newFile) {
+ $processor = $newFile->getProcessor();
+ $processor?->replacedWithWebpVariant($newFile);
+ }
+
+ return $newFile;
default:
throw new \LogicException("Unreachable");
diff --git a/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php
index dab5e98cd8c..911f2e2ee65 100644
--- a/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php
+++ b/wcfsetup/install/files/lib/system/file/processor/IFileProcessor.class.php
@@ -174,4 +174,12 @@ public function trackDownload(File $file): void;
* @since 6.2
*/
public function getImageCropperConfiguration(): ?ImageCropperConfiguration;
+
+ /**
+ * Notifies the processor that one of its files was replaced with its WebP
+ * variant.
+ *
+ * @since 6.2
+ */
+ public function replacedWithWebpVariant(File $file): void;
}
diff --git a/wcfsetup/install/files/lib/system/file/processor/UserAvatarFileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/UserAvatarFileProcessor.class.php
index a21fc0882f6..aee18cd9e7e 100644
--- a/wcfsetup/install/files/lib/system/file/processor/UserAvatarFileProcessor.class.php
+++ b/wcfsetup/install/files/lib/system/file/processor/UserAvatarFileProcessor.class.php
@@ -3,6 +3,7 @@
namespace wcf\system\file\processor;
use wcf\data\file\File;
+use wcf\data\user\UserEditor;
use wcf\data\user\UserProfile;
use wcf\system\cache\runtime\UserProfileRuntimeCache;
use wcf\system\database\util\PreparedStatementConditionBuilder;
@@ -145,20 +146,7 @@ public function canDownload(File $file): bool
#[\Override]
public function getThumbnailFormats(): array
{
- return [
- new ThumbnailFormat(
- '128',
- UserAvatarFileProcessor::AVATAR_SIZE,
- UserAvatarFileProcessor::AVATAR_SIZE,
- false
- ),
- new ThumbnailFormat(
- '256',
- UserAvatarFileProcessor::AVATAR_SIZE_2X,
- UserAvatarFileProcessor::AVATAR_SIZE_2X,
- false
- ),
- ];
+ return [];
}
#[\Override]
@@ -216,6 +204,29 @@ public function getImageCropperConfiguration(): ImageCropperConfiguration
);
}
+ #[\Override]
+ public function replacedWithWebpVariant(File $file): void
+ {
+ $user = $this->getUserByFile($file);
+ if ($user === null) {
+ return;
+ }
+
+ $filename = $file->getSourceFilenameWebp() ?? $file->getSourceFilename();
+ $pathname = $file->getRelativePath() . $filename;
+
+ if ($user->avatarPathname === $pathname) {
+ return;
+ }
+
+ // The relative path to the avatar is stored in a denormalized form in
+ // the user table. This path may be outdated if either the avatar is
+ // later converted to WebP or during the upload.
+ (new UserEditor($user->getDecoratedObject()))->update([
+ 'avatarPathname' => $pathname,
+ ]);
+ }
+
/**
* @param array $context
*/
diff --git a/wcfsetup/install/files/lib/system/user/command/SetAvatar.class.php b/wcfsetup/install/files/lib/system/user/command/SetAvatar.class.php
index e4bbd648c4b..89b8db7fa4a 100644
--- a/wcfsetup/install/files/lib/system/user/command/SetAvatar.class.php
+++ b/wcfsetup/install/files/lib/system/user/command/SetAvatar.class.php
@@ -33,8 +33,15 @@ public function __invoke(): void
(new FileAction([$this->user->avatarFileID], 'delete'))->executeAction();
}
+ $pathname = '';
+ if ($this->file !== null) {
+ $filename = $this->file->getSourceFilenameWebp() ?? $this->file->getSourceFilename();
+ $pathname = $this->file->getRelativePath() . $filename;
+ }
+
(new UserEditor($this->user))->update([
'avatarFileID' => $this->file?->fileID,
+ 'avatarPathname' => $pathname,
'avatarID' => null,
]);
diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql
index 99388982a3d..b315d88a360 100644
--- a/wcfsetup/setup/db/install.sql
+++ b/wcfsetup/setup/db/install.sql
@@ -1560,6 +1560,7 @@ CREATE TABLE wcf1_user (
registrationIpAddress VARCHAR(39) NOT NULL DEFAULT '',
avatarID INT(10),
avatarFileID INT(10) DEFAULT NULL,
+ avatarPathname VARCHAR(255) DEFAULT NULL,
disableAvatar TINYINT(1) NOT NULL DEFAULT 0,
disableAvatarReason TEXT,
disableAvatarExpires INT(10) NOT NULL DEFAULT 0,