Merge branch '6.2' into 6.2-user-coverphoto
authorCyperghost <olaf_schmitz_1@t-online.de>
Mon, 23 Dec 2024 08:02:45 +0000 (09:02 +0100)
committerCyperghost <olaf_schmitz_1@t-online.de>
Mon, 23 Dec 2024 08:02:45 +0000 (09:02 +0100)
# Conflicts:
# com.woltlab.wcf/fileDelete.xml
# com.woltlab.wcf/objectType.xml
# wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2.php
# wcfsetup/install/files/lib/data/user/UserProfile.class.php
# wcfsetup/install/files/lib/data/user/UserProfileAction.class.php
# wcfsetup/install/files/lib/data/user/UserProfileList.class.php
# wcfsetup/setup/db/install.sql

18 files changed:
1  2 
com.woltlab.wcf/fileDelete.xml
com.woltlab.wcf/objectType.xml
com.woltlab.wcf/templates/userProfileHeader.tpl
ts/WoltLabSuite/Core/Bootstrap.ts
ts/WoltLabSuite/Core/Component/Image/Cropper.ts
wcfsetup/install/files/acp/database/update_com.woltlab.wcf_6.2.php
wcfsetup/install/files/acp/templates/userAdd.tpl
wcfsetup/install/files/js/WoltLabSuite/Core/Bootstrap.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Image/Cropper.js
wcfsetup/install/files/lib/acp/form/UserEditForm.class.php
wcfsetup/install/files/lib/data/user/User.class.php
wcfsetup/install/files/lib/data/user/UserProfile.class.php
wcfsetup/install/files/lib/data/user/UserProfileAction.class.php
wcfsetup/install/files/lib/data/user/UserProfileList.class.php
wcfsetup/install/files/lib/system/worker/UserRebuildDataWorker.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 302f95860dcc777e85918df0b690f761fb430df5,0bf2d4c5bb3c549d8bf275844ec91b66c1586b6e..437e8ddf7d09466f7417dab097147ab23ba28423
                <file>lib/system/template/plugin/TemplatePluginPrefilterIcon.class.php</file>
                <file>lib/system/template/plugin/TemplatePluginPrefilterLang.class.php</file>
                <file>lib/system/template/plugin/WordwrapModifierTemplatePlugin.class.php</file>
+               <file>lib/system/upload/AvatarUploadFileSaveStrategy.class.php</file>
+               <file>lib/system/upload/AvatarUploadFileValidationStrategy.class.php</file>
 +              <file>lib/system/upload/UserCoverPhotoUploadFileSaveStrategy.class.php</file>
 +              <file>lib/system/upload/UserCoverPhotoUploadFileValidationStrategy.class.php</file>
                <file>lib/system/user/UserCollapsibleContentHandler.class.php</file>
                <file>lib/system/user/activity/point/AbstractUserActivityPointObjectProcessor.class.php</file>
                <file>lib/system/user/activity/point/DefaultUserActivityPointObjectProcessor.class.php</file>
index c522744e96465bd9dfa42191f1b68c4851a2e3e5,567b539407f1a1406b4d3684955afeebb94b133c..654ccf917d40d1e73f71d6e3de23916ab2963cf5
                        <definitionname>com.woltlab.wcf.file</definitionname>
                        <classname>wcf\system\file\processor\AttachmentFileProcessor</classname>
                </type>
+               <type>
+                       <name>com.woltlab.wcf.user.avatar</name>
+                       <definitionname>com.woltlab.wcf.file</definitionname>
+                       <classname>wcf\system\file\processor\UserAvatarFileProcessor</classname>
+               </type>
 +              <type>
 +                      <name>com.woltlab.wcf.user.coverPhoto</name>
 +                      <definitionname>com.woltlab.wcf.file</definitionname>
 +                      <classname>wcf\system\file\processor\UserCoverPhotoFileProcessor</classname>
 +              </type>
                <!-- deprecated -->
                <type>
                        <name>com.woltlab.wcf.page.controller</name>
index cdc58d0d8008118d126ec9d85f1ad300676b6c6a,3a9fe00d0b609434e056eeea36abdf757bb065d1..399042ad8c8b961c5d4191278d95ba6f2217b8c1
                        {event name='beforeManageButtons'}
                        
                        {if $view->canEditCoverPhoto()}
 -                              <div class="dropdown">
 -                                      <button type="button" class="button small dropdownToggle">{icon name='camera'} {lang}wcf.user.coverPhoto.edit{/lang}</button>
 -                                      <ul class="dropdownMenu">
 -                                              {if $view->canAddCoverPhoto()}
 -                                                      <li><button type="button" class="jsButtonUploadCoverPhoto jsStaticDialog" data-dialog-id="userProfileCoverPhotoUpload">{lang}wcf.user.coverPhoto.upload{/lang}</button></li>
 -                                              {/if}
 -                                              <li{if !$view->user->coverPhotoHash} style="display:none;"{/if}><button type="button" class="jsButtonDeleteCoverPhoto">{lang}wcf.user.coverPhoto.delete{/lang}</button></li>
 -                                      </ul>
 -                              </div>
 +                              <ul class="userProfileManageCoverPhoto buttonGroup buttonList smallButtons">
 +                                      <li>
 +                                              <button type="button" data-edit-cover-photo="{link controller="UserCoverPhoto" id=$user->userID}{/link}" data-default-cover-photo="{$__wcf->styleHandler->getStyle()->getCoverPhotoUrl()}" class="button small">
 +                                                      {icon name='camera'} {lang}wcf.user.coverPhoto.management{/lang}
 +                                              </button>
 +                                      </li>
 +                              </ul>
                        {/if}
  
+                       {if $view->user->canEditAvatar()}
+                               <button type="button" data-edit-avatar="{link controller="UserAvatar" id=$view->user->userID}{/link}" class="button small">
+                                       {icon name='circle-user' type='solid'} {lang}wcf.user.avatar.edit{/lang}
+                               </button>
+                       {/if}
                        {if $view->canEditUser()}
                                <button type="button" class="jsButtonEditProfile button small">{icon name='pencil'} <span>{lang}wcf.user.editProfile{/lang}</span></button>
                        {/if}
Simple merge
index edc887ef15bda8c8fa21d4939e162a6255d973fb,c275d354e18ff285503067e040988079cc00fd30..991913e5976df6c330b57480139d47ae5092db51
@@@ -15,15 -15,15 +15,23 @@@ use wcf\system\database\table\PartialDa
  return [
      PartialDatabaseTable::create('wcf1_user')
          ->columns([
+             IntDatabaseTableColumn::create('avatarFileID')
+                 ->length(10)
+                 ->defaultValue(null),
 +            IntDatabaseTableColumn::create('coverPhotoFileID')
 +                ->length(10)
 +                ->defaultValue(null),
          ])
          ->foreignKeys([
+             DatabaseTableForeignKey::create()
+                 ->columns(['avatarFileID'])
+                 ->referencedTable('wcf1_file')
+                 ->referencedColumns(['fileID'])
+                 ->onDelete('SET NULL'),
 +            DatabaseTableForeignKey::create()
 +                ->columns(['coverPhotoFileID'])
 +                ->referencedTable('wcf1_file')
 +                ->referencedColumns(['fileID'])
 +                ->onDelete('SET NULL'),
          ]),
  ];
index f123518cb4d4b3ec63a073f2c27b65713dec93ef,bdcd0c0fef6dd847e3975a43c795bebf83b69605..6f11659bf1b2632fa907fb69a0506ca4113b40c2
@@@ -151,14 -154,11 +154,14 @@@ define(["require", "exports", "tslib", 
          });
          (0, LazyLoader_1.whenFirstSeen)(".jsImageViewer", () => {
              console.warn("The class `jsImageViewer` is deprecated. Use the attribute `data-fancybox` instead.");
-             void new Promise((resolve_12, reject_12) => { require(["./Component/Image/Viewer"], resolve_12, reject_12); }).then(tslib_1.__importStar).then(({ setupLegacy }) => setupLegacy());
+             void new Promise((resolve_13, reject_13) => { require(["./Component/Image/Viewer"], resolve_13, reject_13); }).then(tslib_1.__importStar).then(({ setupLegacy }) => setupLegacy());
          });
          (0, LazyLoader_1.whenFirstSeen)(".jsEnablesOptions", () => {
-             void new Promise((resolve_13, reject_13) => { require(["./Component/Option/Enable"], resolve_13, reject_13); }).then(tslib_1.__importStar).then(({ setup }) => setup());
+             void new Promise((resolve_14, reject_14) => { require(["./Component/Option/Enable"], resolve_14, reject_14); }).then(tslib_1.__importStar).then(({ setup }) => setup());
          });
 +        (0, LazyLoader_1.whenFirstSeen)("[data-edit-cover-photo]", () => {
 +            void new Promise((resolve_14, reject_14) => { require(["./Component/User/CoverPhoto"], resolve_14, reject_14); }).then(tslib_1.__importStar).then(({ setup }) => setup());
 +        });
          // Move the reCAPTCHA widget overlay to the `pageOverlayContainer`
          // when widget form elements are placed in a dialog.
          const observer = new MutationObserver((mutations) => {
index 8093a07d02e68fe62aad632409ca3f04925d80ef,0b2083a4d350692f6aa78b85765db379e78f2855..a8aef1554e72e081f091858e9fa04ca9302382f6
@@@ -1190,23 -1199,26 +1197,46 @@@ class UserProfile extends DatabaseObjec
              && ($this->isAccessible('canViewTrophies') || $this->userID == WCF::getSession()->userID);
      }
  
+     /**
+      * @since 6.2
+      */
+     public function canEditAvatar(): bool
+     {
+         if (
+             WCF::getSession()->getPermission('admin.user.canEditUser')
+             && UserGroup::isAccessibleGroup($this->getGroupIDs())
+         ) {
+             return true;
+         }
+         if ($this->userID !== WCF::getUser()->userID) {
+             return false;
+         }
+         if ($this->disableAvatar) {
+             return false;
+         }
+         return WCF::getSession()->getPermission('user.profile.avatar.canUploadAvatar');
+     }
++
 +    /**
 +     * @since 6.2
 +     */
 +    public function canEditCoverPhoto(): bool
 +    {
 +        if ($this->canEdit() && WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto')) {
 +            return true;
 +        }
 +
 +        if ($this->userID !== WCF::getUser()->userID) {
 +            return false;
 +        }
 +
 +        if ($this->disableCoverPhoto) {
 +            return false;
 +        }
 +
 +        return WCF::getSession()->getPermission('user.profile.coverPhoto.canUploadCoverPhoto');
 +    }
  }
index 250af1f74f56689bc7d810f676de52fe933d1d64,b4a56b6b2d3a3be195200338119e26b0df741ac0..b6d5940d85c903296dc16ed1263ea7ae64d3e502
@@@ -508,94 -506,128 +503,6 @@@ class UserProfileAction extends UserAct
          }
      }
  
--    /**
-      * Sets an avatar for a given user. The given file will be renamed and is gone after this method call.
 -     * Validates the 'uploadCoverPhoto' method.
--     *
-      * @throws UserInputException If none or more than one user is given.
-      * @throws \InvalidArgumentException If the given file is not an image or is incorrectly sized.
-      * @since 5.5
 -     * @throws  PermissionDeniedException
 -     * @throws  UserInputException
 -     * @since   3.1
--     */
-     public function setAvatar(): array
 -    public function validateUploadCoverPhoto()
--    {
-         $user = $this->getSingleObject();
-         $imageData = \getimagesize($this->parameters['fileLocation']);
 -        WCF::getSession()->checkPermissions(['user.profile.coverPhoto.canUploadCoverPhoto']);
--
-         if (!$imageData) {
-             throw new \InvalidArgumentException("The given file is not an image.");
 -        $this->readInteger('userID', true);
 -        // The `userID` parameter did not exist in 3.1, defaulting to the own user for backwards compatibility.
 -        if (!$this->parameters['userID']) {
 -            $this->parameters['userID'] = WCF::getUser()->userID;
--        }
--
-         if (
-             ($imageData[0] != UserAvatar::AVATAR_SIZE || $imageData[1] != UserAvatar::AVATAR_SIZE)
-             && ($imageData[0] != UserAvatar::AVATAR_SIZE_2X || $imageData[1] != UserAvatar::AVATAR_SIZE_2X)
 -        $this->user = new User($this->parameters['userID']);
 -        if (!$this->user->userID) {
 -            throw new UserInputException('userID');
 -        } elseif ($this->user->userID == WCF::getUser()->userID && WCF::getUser()->disableCoverPhoto) {
 -            throw new PermissionDeniedException();
 -        } elseif (
 -            $this->user->userID != WCF::getUser()->userID
 -            && (!$this->user->canEdit() || !WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto'))
--        ) {
-             throw new \InvalidArgumentException(
-                 \sprintf(
-                     "The given file does not have the size of %dx%d",
-                     UserAvatar::AVATAR_SIZE,
-                     UserAvatar::AVATAR_SIZE
-                 )
-             );
 -            throw new PermissionDeniedException();
--        }
--
-         $data = [
-             'avatarName' => $this->parameters['filename'] ?? \basename($this->parameters['fileLocation']),
-             'avatarExtension' => ImageUtil::getExtensionByMimeType($imageData['mime']),
-             'width' => $imageData[0],
-             'height' => $imageData[1],
-             'userID' => $user->userID,
-             'fileHash' => \sha1_file($this->parameters['fileLocation']),
-         ];
 -        // validate uploaded file
 -        if (!isset($this->parameters['__files']) || \count($this->parameters['__files']->getFiles()) != 1) {
 -            throw new UserInputException('files');
 -        }
--
-         // create avatar
-         $avatar = UserAvatarEditor::create($data);
 -        /** @var UploadHandler $uploadHandler */
 -        $uploadHandler = $this->parameters['__files'];
--
-         try {
-             // check avatar directory
-             // and create subdirectory if necessary
-             $dir = \dirname($avatar->getLocation(null, false));
-             if (!\file_exists($dir)) {
-                 FileUtil::makePath($dir);
-             }
 -        $this->uploadFile = $uploadHandler->getFiles()[0];
--
-             \rename($this->parameters['fileLocation'], $avatar->getLocation(null, false));
 -        $uploadHandler->validateFiles(new UserCoverPhotoUploadFileValidationStrategy());
 -    }
--
-             // Fix the permissions of the file in case the source file was created with restricted
-             // permissions (e.g. 0600 instead of 0644). Without this the file might not be readable
-             // for the web server if it runs with a different system user.
-             FileUtil::makeWritable($avatar->getLocation(null, false));
 -    /**
 -     * Uploads a cover photo.
 -     *
 -     * @since   3.1
 -     */
 -    public function uploadCoverPhoto()
 -    {
 -        $saveStrategy = new UserCoverPhotoUploadFileSaveStrategy(
 -            (!empty($this->parameters['userID']) ? \intval($this->parameters['userID']) : WCF::getUser()->userID)
 -        );
 -        /** @noinspection PhpUndefinedMethodInspection */
 -        $this->parameters['__files']->saveFiles($saveStrategy);
--
-             // Create the WebP variant or the JPEG fallback of the avatar.
-             $avatarEditor = new UserAvatarEditor($avatar);
-             if ($avatarEditor->createAvatarVariant()) {
-                 $avatar = new UserAvatar($avatar->avatarID);
-             }
 -        if ($this->uploadFile->getValidationErrorType()) {
 -            return [
 -                'filesize' => $this->uploadFile->getFilesize(),
 -                'errorMessage' => WCF::getLanguage()->getDynamicVariable(
 -                    'wcf.user.coverPhoto.upload.error.' . $this->uploadFile->getValidationErrorType(),
 -                    [
 -                        'file' => $this->uploadFile,
 -                    ]
 -                ),
 -                'errorType' => $this->uploadFile->getValidationErrorType(),
 -            ];
 -        } else {
 -            return [
 -                'url' => $saveStrategy->getCoverPhoto()->getURL(),
 -            ];
 -        }
 -    }
--
-             // update user
-             $userEditor = new UserEditor($user->getDecoratedObject());
-             $userEditor->update([
-                 'avatarID' => $avatar->avatarID,
-             ]);
-         } catch (\Exception $e) {
-             $editor = new UserAvatarEditor($avatar);
-             $editor->delete();
 -    /**
 -     * Validates the `deleteCoverPhoto` action.
 -     *
 -     * @throws  PermissionDeniedException
 -     * @throws  UserInputException
 -     */
 -    public function validateDeleteCoverPhoto()
 -    {
 -        $this->readInteger('userID', true);
 -        // The `userID` parameter did not exist in 3.1, defaulting to the own user for backwards compatibility.
 -        if (!$this->parameters['userID']) {
 -            $this->parameters['userID'] = WCF::getUser()->userID;
 -        }
--
-             throw $e;
 -        $this->user = new User($this->parameters['userID']);
 -        if (!$this->user->userID) {
 -            throw new UserInputException('userID');
 -        } elseif (
 -            $this->user->userID != WCF::getUser()->userID
 -            && (!$this->user->canEdit() || !WCF::getSession()->getPermission('admin.user.canDisableCoverPhoto'))
 -        ) {
 -            throw new PermissionDeniedException();
--        }
 -    }
--
-         // delete old avatar
-         if ($user->avatarID) {
-             (new UserAvatarAction([$user->avatarID], 'delete'))->executeAction();
 -    /**
 -     * Deletes the cover photo of the active user.
 -     *
 -     * @return  string[]    link to the new cover photo
 -     */
 -    public function deleteCoverPhoto()
 -    {
 -        if ($this->user->coverPhotoHash) {
 -            UserProfileRuntimeCache::getInstance()->getObject($this->user->userID)->getCoverPhoto()->delete();
 -
 -            (new UserEditor($this->user))->update([
 -                'coverPhotoHash' => null,
 -                'coverPhotoExtension' => '',
 -            ]);
--        }
--
-         // reset user storage
-         UserStorageHandler::getInstance()->reset([$user->userID], 'avatar');
 -        // force-reload the user profile to use a predictable code-path to fetch the cover photo
 -        UserProfileRuntimeCache::getInstance()->removeObject($this->user->userID);
--
--        return [
-             'avatar' => $avatar,
 -            'url' => UserProfileRuntimeCache::getInstance()->getObject($this->user->userID)->getCoverPhoto()->getURL(),
--        ];
--    }
--
      /**
       * Returns the user option handler object.
       *
index ad997ba68662f14c1003ccb8490e7f97fd5ec042,bb362da65ef4c3382048b8aa22edee419ac795b4..f54c72556a5d0244aeaddc39875cca567ca9ebbd
@@@ -62,12 -58,6 +60,14 @@@ class UserProfileList extends UserLis
  
          parent::readObjects();
  
+         $this->cacheAvatarFiles();
++
 +        $coverPhotoFileIDs = [];
 +        foreach ($this->objects as $object) {
 +            if ($object->coverPhotoFileID) {
 +                $coverPhotoFileIDs[] = $object->coverPhotoFileID;
 +            }
 +        }
 +        FileRuntimeCache::getInstance()->cacheObjectIDs($coverPhotoFileIDs);
      }
  }
index 521e59496db01ed6966b7f269c31349bb948ce44,9409784bfebd808bef4dfc3fa277a84d56c2c145..d766b7f6201cc21670f3c5406ff0bfa605a6b073
@@@ -4,10 -4,10 +4,9 @@@ namespace wcf\system\worker
  
  use wcf\data\file\FileEditor;
  use wcf\data\reaction\type\ReactionTypeCache;
- use wcf\data\user\avatar\UserAvatar;
  use wcf\data\user\avatar\UserAvatarEditor;
  use wcf\data\user\avatar\UserAvatarList;
 -use wcf\data\user\cover\photo\DefaultUserCoverPhoto;
 -use wcf\data\user\cover\photo\IWebpUserCoverPhoto;
 +use wcf\data\user\cover\photo\UserCoverPhoto;
  use wcf\data\user\User;
  use wcf\data\user\UserEditor;
  use wcf\data\user\UserList;
@@@ -15,9 -15,10 +14,10 @@@ use wcf\data\user\UserProfileAction
  use wcf\system\bbcode\BBCodeHandler;
  use wcf\system\database\util\PreparedStatementConditionBuilder;
  use wcf\system\exception\SystemException;
+ use wcf\system\file\processor\UserAvatarFileProcessor;
  use wcf\system\html\input\HtmlInputProcessor;
  use wcf\system\image\ImageHandler;
 +use wcf\system\user\command\SetCoverPhoto;
  use wcf\system\user\storage\UserStorageHandler;
  use wcf\system\WCF;
  
Simple merge
Simple merge
index 956151aaadd73b12d1d057cc2cbd67f7ae4d6c6f,b2b8d4ff2b4d364c1ee1b673f1c8630afc1d09e4..e3a9aba8bb04bcc2dd76df6810d3c073f4a1c678
@@@ -2257,7 -2257,7 +2258,8 @@@ ALTER TABLE wcf1_tracked_visit_type AD
  ALTER TABLE wcf1_unfurl_url ADD FOREIGN KEY (imageID) REFERENCES wcf1_unfurl_url_image (imageID) ON DELETE SET NULL;
  
  ALTER TABLE wcf1_user ADD FOREIGN KEY (avatarID) REFERENCES wcf1_user_avatar (avatarID) ON DELETE SET NULL;
+ ALTER TABLE wcf1_user ADD FOREIGN KEY (avatarFileID) REFERENCES wcf1_file (fileID) ON DELETE SET NULL;
 +ALTER TABLE wcf1_user ADD FOREIGN KEY (coverPhotoFileID) REFERENCES wcf1_file (fileID) ON DELETE SET NULL;
  ALTER TABLE wcf1_user ADD FOREIGN KEY (rankID) REFERENCES wcf1_user_rank (rankID) ON DELETE SET NULL;
  ALTER TABLE wcf1_user ADD FOREIGN KEY (userOnlineGroupID) REFERENCES wcf1_user_group (groupID) ON DELETE SET NULL;