Merge branch '5.3' into 5.4
authorTim Düsterhus <duesterhus@woltlab.com>
Wed, 9 Mar 2022 09:46:52 +0000 (10:46 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Wed, 9 Mar 2022 09:46:52 +0000 (10:46 +0100)
1  2 
wcfsetup/install/files/lib/data/user/profile/visitor/UserProfileVisitorAction.class.php
wcfsetup/install/files/lib/data/user/trophy/UserTrophyAction.class.php
wcfsetup/install/files/lib/system/cache/runtime/CommentResponseRuntimeCache.class.php
wcfsetup/install/files/lib/system/cache/runtime/CommentRuntimeCache.class.php
wcfsetup/install/files/lib/system/cache/runtime/MediaRuntimeCache.class.php
wcfsetup/install/files/lib/system/cache/runtime/UserProfileRuntimeCache.class.php
wcfsetup/install/files/lib/system/cache/runtime/UserRuntimeCache.class.php
wcfsetup/install/files/lib/system/cache/runtime/ViewableArticleContentRuntimeCache.class.php
wcfsetup/install/files/lib/system/cache/runtime/ViewableArticleRuntimeCache.class.php
wcfsetup/install/files/lib/system/cache/runtime/ViewableMediaRuntimeCache.class.php

index 820cd8cc6522cf2b18109ec44493864c48ffc4f9,cb66bb76c014b1b730a4271993247ae5056d09bb..2c41204f6a9c12e40177a97c0bc05c619b19e259
  <?php
 +
  namespace wcf\data\user\profile\visitor;
 -use wcf\data\user\UserProfile;
 +
  use wcf\data\AbstractDatabaseObjectAction;
  use wcf\data\IGroupedUserListAction;
 +use wcf\data\user\UserProfile;
  use wcf\system\cache\runtime\UserProfileRuntimeCache;
  use wcf\system\exception\PermissionDeniedException;
+ use wcf\system\exception\UserInputException;
  use wcf\system\user\GroupedUserList;
  use wcf\system\WCF;
  
  /**
   * Executes profile visitor-related actions.
 - * 
 - * @author    Alexander Ebert
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\Data\User\Profile\Visitor
 - * 
 - * @method    UserProfileVisitor              create()
 - * @method    UserProfileVisitorEditor[]      getObjects()
 - * @method    UserProfileVisitorEditor        getSingleObject()
 + *
 + * @author  Alexander Ebert
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\Data\User\Profile\Visitor
 + *
 + * @method  UserProfileVisitor      create()
 + * @method  UserProfileVisitorEditor[]  getObjects()
 + * @method  UserProfileVisitorEditor    getSingleObject()
   */
 -class UserProfileVisitorAction extends AbstractDatabaseObjectAction implements IGroupedUserListAction {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $allowGuestAccess = ['getGroupedUserList'];
 -      
 -      /**
 -       * user profile object
 -       * @var UserProfile;
 -       */
 -      public $userProfile = null;
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function validateGetGroupedUserList() {
 -              $this->readInteger('pageNo');
 -              $this->readInteger('userID');
 -              
 -              $this->userProfile = UserProfileRuntimeCache::getInstance()->getObject($this->parameters['userID']);
 -              if (!$this->userProfile) {
 -                      throw new UserInputException('userID');
 -              }
 -              if ($this->userProfile->isProtected()) {
 -                      throw new PermissionDeniedException();
 -              }
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function getGroupedUserList() {
 -              // resolve page count
 -              $sql = "SELECT  COUNT(*)
 -                      FROM    wcf".WCF_N."_user_profile_visitor
 -                      WHERE   ownerID = ?";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute([$this->parameters['userID']]);
 -              $pageCount = ceil($statement->fetchSingleColumn() / 20);
 -              
 -              // get user ids
 -              $sql = "SELECT          userID
 -                      FROM            wcf".WCF_N."_user_profile_visitor
 -                      WHERE           ownerID = ?
 -                      ORDER BY        time DESC";
 -              $statement = WCF::getDB()->prepareStatement($sql, 20, ($this->parameters['pageNo'] - 1) * 20);
 -              $statement->execute([$this->parameters['userID']]);
 -              $userIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
 -              
 -              // create group
 -              $group = new GroupedUserList();
 -              $group->addUserIDs($userIDs);
 -              
 -              // load user profiles
 -              GroupedUserList::loadUsers();
 -              
 -              WCF::getTPL()->assign([
 -                      'groupedUsers' => [$group]
 -              ]);
 -              
 -              return [
 -                      'pageCount' => $pageCount,
 -                      'template' => WCF::getTPL()->fetch('groupedUserList')
 -              ];
 -      }
 -      
 -      /**
 -       * Inserts a new visitor if it does not already exist, or updates it if it does.
 -       * @since       5.2
 -       */
 -      public function registerVisitor() {
 -              $sql = "INSERT INTO             wcf".WCF_N."_user_profile_visitor
 -                                              (ownerID, userID, time)
 -                      VALUES                  (?, ?, ?)
 -                      ON DUPLICATE KEY UPDATE time = VALUES(time)";
 -              $statement = WCF::getDB()->prepareStatement($sql);
 -              $statement->execute([
 -                      $this->parameters['data']['ownerID'],
 -                      $this->parameters['data']['userID'],
 -                      $this->parameters['data']['time'] ?? TIME_NOW,
 -              ]);
 -      }
 +class UserProfileVisitorAction extends AbstractDatabaseObjectAction implements IGroupedUserListAction
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $allowGuestAccess = ['getGroupedUserList'];
 +
 +    /**
 +     * user profile object
 +     * @var UserProfile;
 +     */
 +    public $userProfile;
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function validateGetGroupedUserList()
 +    {
 +        $this->readInteger('pageNo');
 +        $this->readInteger('userID');
 +
 +        $this->userProfile = UserProfileRuntimeCache::getInstance()->getObject($this->parameters['userID']);
++        if (!$this->userProfile) {
++            throw new UserInputException('userID');
++        }
 +        if ($this->userProfile->isProtected()) {
 +            throw new PermissionDeniedException();
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function getGroupedUserList()
 +    {
 +        // resolve page count
 +        $sql = "SELECT  COUNT(*)
 +                FROM    wcf" . WCF_N . "_user_profile_visitor
 +                WHERE   ownerID = ?";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute([$this->parameters['userID']]);
 +        $pageCount = \ceil($statement->fetchSingleColumn() / 20);
 +
 +        // get user ids
 +        $sql = "SELECT      userID
 +                FROM        wcf" . WCF_N . "_user_profile_visitor
 +                WHERE       ownerID = ?
 +                ORDER BY    time DESC";
 +        $statement = WCF::getDB()->prepareStatement($sql, 20, ($this->parameters['pageNo'] - 1) * 20);
 +        $statement->execute([$this->parameters['userID']]);
 +        $userIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
 +
 +        // create group
 +        $group = new GroupedUserList();
 +        $group->addUserIDs($userIDs);
 +
 +        // load user profiles
 +        GroupedUserList::loadUsers();
 +
 +        WCF::getTPL()->assign([
 +            'groupedUsers' => [$group],
 +        ]);
 +
 +        return [
 +            'pageCount' => $pageCount,
 +            'template' => WCF::getTPL()->fetch('groupedUserList'),
 +        ];
 +    }
 +
 +    /**
 +     * Inserts a new visitor if it does not already exist, or updates it if it does.
 +     * @since       5.2
 +     */
 +    public function registerVisitor()
 +    {
 +        $sql = "INSERT INTO             wcf" . WCF_N . "_user_profile_visitor
 +                                        (ownerID, userID, time)
 +                VALUES                  (?, ?, ?)
 +                ON DUPLICATE KEY UPDATE time = VALUES(time)";
 +        $statement = WCF::getDB()->prepareStatement($sql);
 +        $statement->execute([
 +            $this->parameters['data']['ownerID'],
 +            $this->parameters['data']['userID'],
 +            $this->parameters['data']['time'] ?? TIME_NOW,
 +        ]);
 +    }
  }
index 6f03ef176153ca8f68680d6b839ff1ed2012f79f,3cff83342a56bad0dd99bde7eeb8ca44d1d2a88e..c2177ec6d17b2be2cecfcefaec5f5b9fdf16b11f
@@@ -17,238 -16,201 +18,241 @@@ use wcf\system\user\storage\UserStorage
  use wcf\system\WCF;
  
  /**
 - * Provides user trophy actions. 
 + * Provides user trophy actions.
   *
 - * @author    Joshua Ruesweg
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\Data\User\Trophy
 - * @since     3.1
 + * @author  Joshua Ruesweg
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\Data\User\Trophy
 + * @since   3.1
   *
 - * @method    UserTrophyEditor[]              getObjects()
 - * @method    UserTrophyEditor                getSingleObject()
 + * @method  UserTrophyEditor[]      getObjects()
 + * @method  UserTrophyEditor        getSingleObject()
   */
 -class UserTrophyAction extends AbstractDatabaseObjectAction {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $permissionsDelete = ['admin.trophy.canAwardTrophy'];
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $allowGuestAccess = ['getGroupedUserTrophyList'];
 -      
 -      /**
 -       * @var UserProfile
 -       */
 -      public $userProfile;
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function create() {
 -              /** @var UserTrophy $userTrophy */
 -              $userTrophy = parent::create();
 -              
 -              if (!$userTrophy->getTrophy()->isDisabled()) {
 -                      $userAction = new UserAction([$userTrophy->userID], 'update', [
 -                              'counters' => [
 -                                      'trophyPoints' => 1
 -                              ]
 -                      ]); 
 -                      $userAction->executeAction(); 
 -                      
 -                      // checks if the user still has space to add special trophies
 -                      if (count($userTrophy->getUserProfile()->getSpecialTrophies()) < $userTrophy->getUserProfile()->getPermission('user.profile.trophy.maxUserSpecialTrophies')) {
 -                              $hasTrophy = false;
 -                              foreach (UserTrophyList::getUserTrophies([$userTrophy->getUserProfile()->userID])[$userTrophy->getUserProfile()->userID] as $trophy) {
 -                                      if ($trophy->trophyID == $userTrophy->trophyID && $trophy->userTrophyID !== $userTrophy->userTrophyID) {
 -                                              $hasTrophy = true; 
 -                                              break; 
 -                                      }
 -                              }
 -                              
 -                              if (!$hasTrophy) {
 -                                      $userProfileAction = new UserProfileAction([$userTrophy->getUserProfile()->getDecoratedObject()], 'updateSpecialTrophies', [
 -                                              'trophyIDs' => array_unique(array_merge(array_map(function($trophy) {
 -                                                      return $trophy->trophyID;
 -                                              }, $userTrophy->getUserProfile()->getSpecialTrophies()), [$userTrophy->trophyID]))
 -                                      ]);
 -                                      $userProfileAction->executeAction();
 -                              }
 -                      }
 -              }
 -              
 -              UserActivityEventHandler::getInstance()->fireEvent('com.woltlab.wcf.userTrophy.recentActivityEvent.trophyReceived', $userTrophy->getObjectID(), null, $userTrophy->userID);
 -              
 -              UserNotificationHandler::getInstance()->fireEvent('received', 'com.woltlab.wcf.userTrophy.notification', new UserTrophyNotificationObject($userTrophy), [
 -                      $userTrophy->userID
 -              ]);
 -              
 -              return $userTrophy; 
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function validateDelete() {
 -              parent::validateDelete();
 -              
 -              /** @var UserTrophy $object */
 -              foreach ($this->objects as $object) {
 -                      if ($object->getTrophy()->awardAutomatically) {
 -                              throw new PermissionDeniedException(); 
 -                      }
 -              }
 -      }
 -      
 -      /**
 -       * @inheritDoc
 -       */
 -      public function delete() {
 -              if (empty($this->objects)) {
 -                      $this->readObjects();
 -              }
 -              
 -              $trophyIDs = $userIDs = []; 
 -              foreach ($this->getObjects() as $object) {
 -                      $trophyIDs[] = $object->trophyID; 
 -                      $userIDs[] = $object->userID; 
 -              }
 -              
 -              $returnValues = parent::delete();
 -              
 -              if (!empty($this->objects)) {
 -                      // update user special trophies trophies
 -                      $userTrophies = UserTrophyList::getUserTrophies($userIDs);
 -                      
 -                      foreach ($userTrophies as $userID => $trophies) {
 -                              $userTrophyIDs = [];
 -                              foreach ($trophies as $trophy) {
 -                                      $userTrophyIDs[] = $trophy->trophyID;
 -                              }
 -                              
 -                              $conditionBuilder = new PreparedStatementConditionBuilder();
 -                              if (!empty($userTrophyIDs)) $conditionBuilder->add('trophyID NOT IN (?)', [array_unique($userTrophyIDs)]);
 -                              $conditionBuilder->add('userID = ?', [$userID]);
 -                              
 -                              $sql = "DELETE FROM wcf". WCF_N ."_user_special_trophy ". $conditionBuilder;
 -                              $statement = WCF::getDB()->prepareStatement($sql);
 -                              $statement->execute($conditionBuilder->getParameters());
 -                              
 -                              UserStorageHandler::getInstance()->reset([$userID], 'specialTrophies');
 -                      }
 -                      
 -                      $updateUserTrophies = [];
 -                      foreach ($this->getObjects() as $object) {
 -                              if (!$object->getTrophy()->isDisabled()) {
 -                                      if (!isset($updateUserTrophies[$object->userID])) $updateUserTrophies[$object->userID] = 0;
 -                                      $updateUserTrophies[$object->userID]--;
 -                              }
 -                      }
 -                      
 -                      foreach ($updateUserTrophies as $userID => $count) {
 -                              $userAction = new UserAction([$userID], 'update', [
 -                                      'counters' => [
 -                                              'trophyPoints' => $count
 -                                      ]
 -                              ]);
 -                              $userAction->executeAction();
 -                      }
 -              }
 -              
 -              return $returnValues;
 -      }
 -      
 -      /**
 -       * Validates the getGroupedUserTrophyList method. 
 -       */
 -      public function validateGetGroupedUserTrophyList() {
 -              if (!MODULE_TROPHY) {
 -                      throw new IllegalLinkException();
 -              }
 -              
 -              WCF::getSession()->checkPermissions(['user.profile.trophy.canSeeTrophies']);
 -              
 -              $this->readInteger('pageNo');
 -              $this->readInteger('userID');
 -              
 -              $this->userProfile = UserProfileRuntimeCache::getInstance()->getObject($this->parameters['userID']);
 -              if (!$this->userProfile) {
 -                      throw new UserInputException('userID');
 -              }
 -              if (!$this->userProfile->isAccessible('canViewTrophies') && $this->userProfile->userID != WCF::getSession()->userID) {
 -                      throw new PermissionDeniedException();
 -              }
 -      }
 -      
 -      /**
 -       * Returns a viewable user trophy list for a specific user. 
 -       */
 -      public function getGroupedUserTrophyList() {
 -              $userTrophyList = new UserTrophyList();
 -              $userTrophyList->getConditionBuilder()->add('userID = ?', [$this->parameters['userID']]);
 -              if (!empty($userTrophyList->sqlJoins)) $userTrophyList->sqlJoins .= ' ';
 -              if (!empty($userTrophyList->sqlConditionJoins)) $userTrophyList->sqlConditionJoins .= ' ';
 -              $userTrophyList->sqlJoins .= 'LEFT JOIN wcf'. WCF_N . '_trophy trophy ON user_trophy.trophyID = trophy.trophyID';
 -              $userTrophyList->sqlConditionJoins .= 'LEFT JOIN wcf'. WCF_N . '_trophy trophy ON user_trophy.trophyID = trophy.trophyID';
 -              
 -              // trophy category join
 -              $userTrophyList->sqlJoins .= ' LEFT JOIN wcf'. WCF_N . '_category category ON trophy.categoryID = category.categoryID';
 -              $userTrophyList->sqlConditionJoins .= ' LEFT JOIN wcf'. WCF_N . '_category category ON trophy.categoryID = category.categoryID';
 -              
 -              $userTrophyList->getConditionBuilder()->add('trophy.isDisabled = ?', [0]);
 -              $userTrophyList->getConditionBuilder()->add('category.isDisabled = ?', [0]);
 -              $userTrophyList->sqlLimit = 10; 
 -              $userTrophyList->sqlOffset = ($this->parameters['pageNo'] - 1) * 10;
 -              $userTrophyList->sqlOrderBy = 'time DESC';
 -              $pageCount = ceil($userTrophyList->countObjects() / 10);
 -              $userTrophyList->readObjects();
 -              
 -              return [
 -                      'pageCount' => $pageCount,
 -                      'title' => WCF::getLanguage()->getDynamicVariable('wcf.user.trophy.dialogTitle', ['username' => $this->userProfile->username]),
 -                      'template' => WCF::getTPL()->fetch('groupedUserTrophyList', 'wcf', [
 -                              'userTrophyList' => $userTrophyList
 -                      ])
 -              ];
 -      }
 +class UserTrophyAction extends AbstractDatabaseObjectAction
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $permissionsDelete = ['admin.trophy.canAwardTrophy'];
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $allowGuestAccess = ['getGroupedUserTrophyList'];
 +
 +    /**
 +     * @var UserProfile
 +     */
 +    public $userProfile;
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function create()
 +    {
 +        /** @var UserTrophy $userTrophy */
 +        $userTrophy = parent::create();
 +
 +        if (!$userTrophy->getTrophy()->isDisabled()) {
 +            $userAction = new UserAction([$userTrophy->userID], 'update', [
 +                'counters' => [
 +                    'trophyPoints' => 1,
 +                ],
 +            ]);
 +            $userAction->executeAction();
 +
 +            // checks if the user still has space to add special trophies
 +            if (\count($userTrophy->getUserProfile()->getSpecialTrophies()) < $userTrophy->getUserProfile()->getPermission('user.profile.trophy.maxUserSpecialTrophies')) {
 +                $hasTrophy = false;
 +                foreach (UserTrophyList::getUserTrophies([$userTrophy->getUserProfile()->userID])[$userTrophy->getUserProfile()->userID] as $trophy) {
 +                    if ($trophy->trophyID == $userTrophy->trophyID && $trophy->userTrophyID !== $userTrophy->userTrophyID) {
 +                        $hasTrophy = true;
 +                        break;
 +                    }
 +                }
 +
 +                if (!$hasTrophy) {
 +                    $userProfileAction = new UserProfileAction(
 +                        [$userTrophy->getUserProfile()->getDecoratedObject()],
 +                        'updateSpecialTrophies',
 +                        [
 +                            'trophyIDs' => \array_unique(\array_merge(\array_map(static function ($trophy) {
 +                                return $trophy->trophyID;
 +                            }, $userTrophy->getUserProfile()->getSpecialTrophies()), [$userTrophy->trophyID])),
 +                        ]
 +                    );
 +                    $userProfileAction->executeAction();
 +                }
 +            }
 +        }
 +
 +        UserActivityEventHandler::getInstance()->fireEvent(
 +            'com.woltlab.wcf.userTrophy.recentActivityEvent.trophyReceived',
 +            $userTrophy->getObjectID(),
 +            null,
 +            $userTrophy->userID
 +        );
 +
 +        UserNotificationHandler::getInstance()->fireEvent(
 +            'received',
 +            'com.woltlab.wcf.userTrophy.notification',
 +            new UserTrophyNotificationObject($userTrophy),
 +            [
 +                $userTrophy->userID,
 +            ]
 +        );
 +
 +        return $userTrophy;
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function validateDelete()
 +    {
 +        parent::validateDelete();
 +
 +        /** @var UserTrophy $object */
 +        foreach ($this->objects as $object) {
 +            if ($object->getTrophy()->awardAutomatically) {
 +                throw new PermissionDeniedException();
 +            }
 +        }
 +    }
 +
 +    /**
 +     * @inheritDoc
 +     */
 +    public function delete()
 +    {
 +        if (empty($this->objects)) {
 +            $this->readObjects();
 +        }
 +
 +        $trophyIDs = $userIDs = [];
 +        foreach ($this->getObjects() as $object) {
 +            $trophyIDs[] = $object->trophyID;
 +            $userIDs[] = $object->userID;
 +        }
 +
 +        $returnValues = parent::delete();
 +
 +        if (!empty($this->objects)) {
 +            // update user special trophies trophies
 +            $userTrophies = UserTrophyList::getUserTrophies($userIDs);
 +
 +            foreach ($userTrophies as $userID => $trophies) {
 +                $userTrophyIDs = [];
 +                foreach ($trophies as $trophy) {
 +                    $userTrophyIDs[] = $trophy->trophyID;
 +                }
 +
 +                $conditionBuilder = new PreparedStatementConditionBuilder();
 +                if (!empty($userTrophyIDs)) {
 +                    $conditionBuilder->add('trophyID NOT IN (?)', [\array_unique($userTrophyIDs)]);
 +                }
 +                $conditionBuilder->add('userID = ?', [$userID]);
 +
 +                $sql = "DELETE FROM wcf" . WCF_N . "_user_special_trophy
 +                        " . $conditionBuilder;
 +                $statement = WCF::getDB()->prepareStatement($sql);
 +                $statement->execute($conditionBuilder->getParameters());
 +
 +                UserStorageHandler::getInstance()->reset([$userID], 'specialTrophies');
 +            }
 +
 +            $updateUserTrophies = [];
 +            foreach ($this->getObjects() as $object) {
 +                if (!$object->getTrophy()->isDisabled()) {
 +                    if (!isset($updateUserTrophies[$object->userID])) {
 +                        $updateUserTrophies[$object->userID] = 0;
 +                    }
 +                    $updateUserTrophies[$object->userID]--;
 +                }
 +            }
 +
 +            foreach ($updateUserTrophies as $userID => $count) {
 +                $userAction = new UserAction([$userID], 'update', [
 +                    'counters' => [
 +                        'trophyPoints' => $count,
 +                    ],
 +                ]);
 +                $userAction->executeAction();
 +            }
 +        }
 +
 +        return $returnValues;
 +    }
 +
 +    /**
 +     * Validates the getGroupedUserTrophyList method.
 +     */
 +    public function validateGetGroupedUserTrophyList()
 +    {
 +        if (!MODULE_TROPHY) {
 +            throw new IllegalLinkException();
 +        }
 +
 +        WCF::getSession()->checkPermissions(['user.profile.trophy.canSeeTrophies']);
 +
 +        $this->readInteger('pageNo');
 +        $this->readInteger('userID');
 +
 +        $this->userProfile = UserProfileRuntimeCache::getInstance()->getObject($this->parameters['userID']);
-         if (!$this->userProfile->isAccessible('canViewTrophies') && !($this->userProfile->userID == WCF::getSession()->userID)) {
++        if (!$this->userProfile) {
++            throw new UserInputException('userID');
++        }
++        if (!$this->userProfile->isAccessible('canViewTrophies') && $this->userProfile->userID != WCF::getSession()->userID) {
 +            throw new PermissionDeniedException();
 +        }
 +    }
 +
 +    /**
 +     * Returns a viewable user trophy list for a specific user.
 +     */
 +    public function getGroupedUserTrophyList()
 +    {
 +        $userTrophyList = new UserTrophyList();
 +        $userTrophyList->getConditionBuilder()->add('userID = ?', [$this->parameters['userID']]);
 +        if (!empty($userTrophyList->sqlJoins)) {
 +            $userTrophyList->sqlJoins .= ' ';
 +        }
 +        if (!empty($userTrophyList->sqlConditionJoins)) {
 +            $userTrophyList->sqlConditionJoins .= ' ';
 +        }
 +        $userTrophyList->sqlJoins .= '
 +            LEFT JOIN   wcf' . WCF_N . '_trophy trophy
 +            ON          user_trophy.trophyID = trophy.trophyID';
 +        $userTrophyList->sqlConditionJoins .= '
 +            LEFT JOIN   wcf' . WCF_N . '_trophy trophy
 +            ON          user_trophy.trophyID = trophy.trophyID';
 +
 +        // trophy category join
 +        $userTrophyList->sqlJoins .= '
 +            LEFT JOIN   wcf' . WCF_N . '_category category
 +            ON          trophy.categoryID = category.categoryID';
 +        $userTrophyList->sqlConditionJoins .= '
 +            LEFT JOIN   wcf' . WCF_N . '_category category
 +            ON          trophy.categoryID = category.categoryID';
 +
 +        $userTrophyList->getConditionBuilder()->add('trophy.isDisabled = ?', [0]);
 +        $userTrophyList->getConditionBuilder()->add('category.isDisabled = ?', [0]);
 +        $userTrophyList->sqlLimit = 10;
 +        $userTrophyList->sqlOffset = ($this->parameters['pageNo'] - 1) * 10;
 +        $userTrophyList->sqlOrderBy = 'time DESC';
 +        $pageCount = \ceil($userTrophyList->countObjects() / 10);
 +        $userTrophyList->readObjects();
 +
 +        return [
 +            'pageCount' => $pageCount,
 +            'title' => WCF::getLanguage()->getDynamicVariable(
 +                'wcf.user.trophy.dialogTitle',
 +                ['username' => $this->userProfile->username]
 +            ),
 +            'template' => WCF::getTPL()->fetch('groupedUserTrophyList', 'wcf', [
 +                'userTrophyList' => $userTrophyList,
 +            ]),
 +        ];
 +    }
  }
index abf5c964bf188d22600a47fa6184ab41e343ba0a,83052819813ec295645fb40e9b3ed11284c4b406..7c4ff8755f6efbf915dbb73b9c157a7172708afa
@@@ -7,21 -5,20 +7,21 @@@ use wcf\data\comment\response\CommentRe
  
  /**
   * Runtime cache implementation for comment responses.
 - * 
 - * @author    Matthias Schmidt
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Cache\Runtime
 - * @since     3.0
 - * 
 - * @method    CommentResponse[]       getCachedObjects()
 - * @method    CommentResponse|null    getObject($objectID)
 - * @method    CommentResponse[]       getObjects(array $objectIDs)
 + *
 + * @author  Matthias Schmidt
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Cache\Runtime
 + * @since   3.0
 + *
-  * @method  CommentResponse[]   getCachedObjects()
-  * @method  CommentResponse     getObject($objectID)
-  * @method  CommentResponse[]   getObjects(array $objectIDs)
++ * @method  CommentResponse[]    getCachedObjects()
++ * @method  CommentResponse|null getObject($objectID)
++ * @method  CommentResponse[]    getObjects(array $objectIDs)
   */
 -class CommentResponseRuntimeCache extends AbstractRuntimeCache {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $listClassName = CommentResponseList::class;
 +class CommentResponseRuntimeCache extends AbstractRuntimeCache
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $listClassName = CommentResponseList::class;
  }
index 4a88a16474f41b98306698feb30d1aa15a9698b9,2af90dbe59d7be0a6efd50de83b2fd669e762e56..0255fa48442730e130e4d32054d300f48ff87d54
@@@ -7,21 -5,20 +7,21 @@@ use wcf\data\comment\CommentList
  
  /**
   * Runtime cache implementation for comments.
 - * 
 - * @author    Matthias Schmidt
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Cache\Runtime
 - * @since     3.0
 - * 
 - * @method    Comment[]       getCachedObjects()
 - * @method    Comment|null    getObject($objectID)
 - * @method    Comment[]       getObjects(array $objectIDs)
 + *
 + * @author  Matthias Schmidt
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Cache\Runtime
 + * @since   3.0
 + *
-  * @method  Comment[]   getCachedObjects()
-  * @method  Comment     getObject($objectID)
-  * @method  Comment[]   getObjects(array $objectIDs)
++ * @method  Comment[]    getCachedObjects()
++ * @method  Comment|null getObject($objectID)
++ * @method  Comment[]    getObjects(array $objectIDs)
   */
 -class CommentRuntimeCache extends AbstractRuntimeCache {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $listClassName = CommentList::class;
 +class CommentRuntimeCache extends AbstractRuntimeCache
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $listClassName = CommentList::class;
  }
index 72bb2e955532890e19817a99b2c5e707b8baf785,87faca5a093deb9d66d31928dbe8abb657781362..4b0b4cbd4a54b26d35ee026134b77d44e9309c38
@@@ -7,21 -5,20 +7,21 @@@ use wcf\data\media\MediaList
  
  /**
   * Runtime cache implementation for shared media.
 - * 
 - * @author    Alexander Ebert
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Cache\Runtime
 - * @since     3.0
 - * 
 - * @method    Media[]         getCachedObjects()
 - * @method    Media|null      getObject($objectID)
 - * @method    Media[]         getObjects(array $objectIDs)
 + *
 + * @author  Alexander Ebert
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Cache\Runtime
 + * @since   3.0
 + *
 + * @method  Media[]         getCachedObjects()
-  * @method  Media           getObject($objectID)
++ * @method  Media|null      getObject($objectID)
 + * @method  Media[]         getObjects(array $objectIDs)
   */
 -class MediaRuntimeCache extends AbstractRuntimeCache {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $listClassName = MediaList::class;
 +class MediaRuntimeCache extends AbstractRuntimeCache
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $listClassName = MediaList::class;
  }
index 8b33edf93ccc6db0eaeb91915d79957d693dceb1,0a40d9fa47a95261bc24446f3c92dfe68b5c47ef..f7bbb55107631b1930503643fd92d191a6482d18
@@@ -8,36 -6,34 +8,36 @@@ use wcf\data\user\UserProfileList
  /**
   * Runtime cache implementation for user profiles.
   *
 - * @author    Alexander Ebert, Matthias Schmidt
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Cache\Runtime
 - * @since     3.0
 - * 
 - * @method    UserProfile[]     getCachedObjects()
 - * @method    UserProfile|null  getObject($objectID)
 - * @method    UserProfile[]     getObjects(array $objectIDs)
 + * @author  Alexander Ebert, Matthias Schmidt
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Cache\Runtime
 + * @since   3.0
 + *
-  * @method  UserProfile[]   getCachedObjects()
-  * @method  UserProfile getObject($objectID)
-  * @method  UserProfile[]   getObjects(array $objectIDs)
++ * @method  UserProfile[]    getCachedObjects()
++ * @method  UserProfile|null getObject($objectID)
++ * @method  UserProfile[]    getObjects(array $objectIDs)
   */
 -class UserProfileRuntimeCache extends AbstractRuntimeCache {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $listClassName = UserProfileList::class;
 -      
 -      /**
 -       * Adds a user profile to the cache. This is an internal method that should
 -       * not be used on a regular basis.
 -       * 
 -       * @param       UserProfile     $profile
 -       * @since       3.1
 -       */
 -      public function addUserProfile(UserProfile $profile) {
 -              $objectID = $profile->getObjectID();
 -              
 -              if (!isset($this->objects[$objectID])) {
 -                      $this->objects[$objectID] = $profile;
 -              }
 -      }
 +class UserProfileRuntimeCache extends AbstractRuntimeCache
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $listClassName = UserProfileList::class;
 +
 +    /**
 +     * Adds a user profile to the cache. This is an internal method that should
 +     * not be used on a regular basis.
 +     *
 +     * @param UserProfile $profile
 +     * @since       3.1
 +     */
 +    public function addUserProfile(UserProfile $profile)
 +    {
 +        $objectID = $profile->getObjectID();
 +
 +        if (!isset($this->objects[$objectID])) {
 +            $this->objects[$objectID] = $profile;
 +        }
 +    }
  }
index e93d3765ae5e411424e097e211cf7116bb86314f,4b86a5824b54feb3d31698ea6f3e36b7f008156c..959c62a11bdd8a54d6fd4f7bf549e5d51c79abc7
@@@ -8,20 -6,19 +8,20 @@@ use wcf\data\user\UserList
  /**
   * Runtime cache implementation for users.
   *
 - * @author    Matthias Schmidt
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Cache\Runtime
 - * @since     3.0
 - * 
 - * @method    User[]          getCachedObjects()
 - * @method    User|null       getObject($objectID)
 - * @method    User[]          getObjects(array $objectIDs)
 + * @author  Matthias Schmidt
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Cache\Runtime
 + * @since   3.0
 + *
 + * @method  User[]      getCachedObjects()
-  * @method  User        getObject($objectID)
++ * @method  User|null   getObject($objectID)
 + * @method  User[]      getObjects(array $objectIDs)
   */
 -class UserRuntimeCache extends AbstractRuntimeCache {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $listClassName = UserList::class;
 +class UserRuntimeCache extends AbstractRuntimeCache
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $listClassName = UserList::class;
  }
index 948de345af810e4818fe85a23497fa5aa78a9eca,58833cc8b50e17042bbe10a1809b281b47082d75..1924315a9c5fdae7c70ea420a6683339f7a31ee6
@@@ -8,20 -6,19 +8,20 @@@ use wcf\data\article\content\ViewableAr
  /**
   * Runtime cache implementation for viewable article contents.
   *
 - * @author    Joshua Ruesweg
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Cache\Runtime
 - * @since     5.2
 + * @author  Joshua Ruesweg
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Cache\Runtime
 + * @since   5.2
   *
 - * @method    ViewableArticleContent[]                getCachedObjects()
 - * @method    ViewableArticleContent|null             getObject($objectID)
 - * @method    ViewableArticleContent[]                getObjects(array $objectIDs)
 + * @method  ViewableArticleContent[]        getCachedObjects()
-  * @method  ViewableArticleContent              getObject($objectID)
++ * @method  ViewableArticleContent|null     getObject($objectID)
 + * @method  ViewableArticleContent[]        getObjects(array $objectIDs)
   */
 -class ViewableArticleContentRuntimeCache extends AbstractRuntimeCache {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $listClassName = ViewableArticleContentList::class;
 +class ViewableArticleContentRuntimeCache extends AbstractRuntimeCache
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $listClassName = ViewableArticleContentList::class;
  }
index 8f3217ccf61c571801154a1964421110e4e458c6,9851a0f784b468bbfbb7d39c3ba11f781d58427a..e26b047f2357f77f107e5d273dcfd6c0b931b41f
@@@ -8,20 -6,19 +8,20 @@@ use wcf\data\article\ViewableArticleLis
  /**
   * Runtime cache implementation for viewable articles.
   *
 - * @author    Marcel Werk
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Cache\Runtime
 - * @since     3.0
 - * 
 - * @method    ViewableArticle[]               getCachedObjects()
 - * @method    ViewableArticle|null            getObject($objectID)
 - * @method    ViewableArticle[]               getObjects(array $objectIDs)
 + * @author  Marcel Werk
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Cache\Runtime
 + * @since   3.0
 + *
 + * @method  ViewableArticle[]       getCachedObjects()
-  * @method  ViewableArticle             getObject($objectID)
++ * @method  ViewableArticle|null    getObject($objectID)
 + * @method  ViewableArticle[]       getObjects(array $objectIDs)
   */
 -class ViewableArticleRuntimeCache extends AbstractRuntimeCache {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $listClassName = ViewableArticleList::class;
 +class ViewableArticleRuntimeCache extends AbstractRuntimeCache
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $listClassName = ViewableArticleList::class;
  }
index 74459053f3e86d8b117ae50c0c9496f545d650f4,874fe683258155b79fa7caa9ca9c21293cc5c1d2..d46f35115850ad30ff3508658f1790d710dc1cce
@@@ -7,21 -5,20 +7,21 @@@ use wcf\data\media\ViewableMediaList
  
  /**
   * Runtime cache implementation for viewable media.
 - * 
 - * @author    Alexander Ebert
 - * @copyright 2001-2019 WoltLab GmbH
 - * @license   GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 - * @package   WoltLabSuite\Core\System\Cache\Runtime
 - * @since     3.0
 - * 
 - * @method    ViewableMedia[]         getCachedObjects()
 - * @method    ViewableMedia|null      getObject($objectID)
 - * @method    ViewableMedia[]         getObjects(array $objectIDs)
 + *
 + * @author  Alexander Ebert
 + * @copyright   2001-2019 WoltLab GmbH
 + * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
 + * @package WoltLabSuite\Core\System\Cache\Runtime
 + * @since   3.0
 + *
 + * @method  ViewableMedia[]         getCachedObjects()
-  * @method  ViewableMedia           getObject($objectID)
++ * @method  ViewableMedia|null      getObject($objectID)
 + * @method  ViewableMedia[]         getObjects(array $objectIDs)
   */
 -class ViewableMediaRuntimeCache extends AbstractRuntimeCache {
 -      /**
 -       * @inheritDoc
 -       */
 -      protected $listClassName = ViewableMediaList::class;
 +class ViewableMediaRuntimeCache extends AbstractRuntimeCache
 +{
 +    /**
 +     * @inheritDoc
 +     */
 +    protected $listClassName = ViewableMediaList::class;
  }