3 namespace wcf\data\user
;
5 use wcf\data\DatabaseObject
;
6 use wcf\data\IPopoverObject
;
7 use wcf\data\IUserContent
;
8 use wcf\data\language\Language
;
9 use wcf\data\user\group\UserGroup
;
10 use wcf\data\user\option\UserOption
;
11 use wcf\system\cache\builder\UserOptionCacheBuilder
;
12 use wcf\system\language\LanguageFactory
;
13 use wcf\system\request\IRouteController
;
14 use wcf\system\request\LinkHandler
;
15 use wcf\system\user\authentication\password\algorithm\DoubleBcrypt
;
16 use wcf\system\user\authentication\password\PasswordAlgorithmManager
;
17 use wcf\system\user\storage\UserStorageHandler
;
20 use wcf\util\UserUtil
;
25 * @author Alexander Ebert
26 * @copyright 2001-2020 WoltLab GmbH
27 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
29 * @property-read int $userID unique id of the user
30 * @property-read string $username name of the user
31 * @property-read string $email email address of the user
32 * @property-read string $password double salted hash of the user's password
33 * @property-read string $accessToken token used for access authentication, for example used by feed pages
34 * @property-read int $languageID id of the interface language used by the user
35 * @property-read int $registrationDate timestamp at which the user has registered/has been created
36 * @property-read int $styleID id of the style used by the user
37 * @property-read int $banned is `1` if the user is banned, otherwise `0`
38 * @property-read string $banReason reason why the user is banned
39 * @property-read int $banExpires timestamp at which the banned user is automatically unbanned
40 * @property-read int $activationCode flag which determines, whether the user is activated (for legacy reasons an random integer, if the user is *not* activated)
41 * @property-read string $emailConfirmed code sent to the user's email address used for account activation or null if the email is confirmed
42 * @property-read int $lastLostPasswordRequestTime timestamp at which the user has reported that they lost their password or 0 if password has not been reported as lost
43 * @property-read string $lostPasswordKey code used for authenticating setting new password after password loss or empty if password has not been reported as lost
44 * @property-read int $lastUsernameChange timestamp at which the user changed their name the last time or 0 if username has not been changed
45 * @property-read string $newEmail new email address of the user that has to be manually confirmed or empty if no new email address has been set
46 * @property-read string $oldUsername previous name of the user or empty if they have had no previous name
47 * @property-read int $quitStarted timestamp at which the user terminated their account
48 * @property-read int $reactivationCode code used for authenticating setting new email address or empty if no new email address has been set
49 * @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
50 * @property-read int|null $avatarID id of the user's avatar or null if they have no avatar
51 * @property-read int $disableAvatar is `1` if the user's avatar has been disabled, otherwise `0`
52 * @property-read string $disableAvatarReason reason why the user's avatar is disabled
53 * @property-read int $disableAvatarExpires timestamp at which the user's avatar will automatically be enabled again
54 * @property-read string $signature text of the user's signature
55 * @property-read int $signatureEnableHtml is `1` if HTML will rendered in the user's signature, otherwise `0`
56 * @property-read int $disableSignature is `1` if the user's signature has been disabled, otherwise `0`
57 * @property-read string $disableSignatureReason reason why the user's signature is disabled
58 * @property-read int $disableSignatureExpires timestamp at which the user's signature will automatically be enabled again
59 * @property-read int $lastActivityTime timestamp of the user's last activity
60 * @property-read int $profileHits number of times the user's profile has been visited
61 * @property-read int|null $rankID id of the user's rank or null if they have no rank
62 * @property-read string $userTitle custom user title used instead of rank title or empty if user has no custom title
63 * @property-read int|null $userOnlineGroupID id of the user group whose online marking is used when printing the user's formatted name or null if no special marking is used
64 * @property-read int $activityPoints total number of the user's activity points
65 * @property-read string $notificationMailToken token used for authenticating requests by the user to disable notification emails
66 * @property-read string $authData data of the third party used for authentication
67 * @property-read int $likesReceived cumulative result of likes (counting +1) the user's contents have received
68 * @property-read string $coverPhotoHash hash of the user's cover photo
69 * @property-read string $coverPhotoExtension extension of the user's cover photo file
70 * @property-read int $coverPhotoHasWebP is `1` if a webp variant of the cover photo and its thumbnail exists, otherwise `0`
71 * @property-read int $disableCoverPhoto is `1` if the user's cover photo has been disabled, otherwise `0`
72 * @property-read string $disableCoverPhotoReason reason why the user's cover photo is disabled
73 * @property-read int $disableCoverPhotoExpires timestamp at which the user's cover photo will automatically be enabled again
74 * @property-read int $articles number of articles written by the user
75 * @property-read string $blacklistMatches JSON string of an array with all matches in the blacklist, otherwise an empty string
76 * @property-read int $multifactorActive is `1` if the use has enabled a second factor, otherwise `0`
77 * @property-read int $trophyPoints total number of user's trophies in active categories
79 final class User
extends DatabaseObject
implements IPopoverObject
, IRouteController
, IUserContent
88 * true, if user has access to the ACP
91 protected $hasAdministrativePermissions;
94 * list of language ids
97 protected $languageIDs;
100 * date time zone object
103 protected $timezoneObj;
106 * list of user options
109 protected static $userOptions;
111 const REGISTER_ACTIVATION_NONE
= 0;
113 const REGISTER_ACTIVATION_USER
= 1;
115 const REGISTER_ACTIVATION_ADMIN
= 2;
117 const REGISTER_ACTIVATION_USER_AND_ADMIN
= self
::REGISTER_ACTIVATION_USER | self
::REGISTER_ACTIVATION_ADMIN
;
119 /** @noinspection PhpMissingParentConstructorInspection */
124 public function __construct($id, $row = null, ?DatabaseObject
$object = null)
127 $sql = "SELECT user_option_value.*, user_table.*
128 FROM wcf" . WCF_N
. "_user user_table
129 LEFT JOIN wcf" . WCF_N
. "_user_option_value user_option_value
130 ON user_option_value.userID = user_table.userID
131 WHERE user_table.userID = ?";
132 $statement = WCF
::getDB()->prepareStatement($sql);
133 $statement->execute([$id]);
134 $row = $statement->fetchArray();
136 // enforce data type 'array'
137 if ($row === false) {
140 } elseif ($object !== null) {
141 $row = $object->data
;
144 $this->handleData($row);
148 * Returns true if the given password is the correct password for this user.
150 * @param string $password
151 * @return bool password correct
153 public function checkPassword(
154 #[\SensitiveParameter]
159 $manager = PasswordAlgorithmManager
::getInstance();
161 // Compatibility for WoltLab Suite < 5.4.
162 if (DoubleBcrypt
::isLegacyDoubleBcrypt($this->password
)) {
163 $algorithmName = 'DoubleBcrypt';
164 $hash = $this->password
;
166 [$algorithmName, $hash] = \
explode(':', $this->password
, 2);
169 $algorithm = $manager->getAlgorithmFromName($algorithmName);
171 $isValid = $algorithm->verify($password, $hash);
177 $defaultAlgorithm = $manager->getDefaultAlgorithm();
178 if (\
get_class($algorithm) !== \
get_class($defaultAlgorithm) ||
$algorithm->needsRehash($hash)) {
179 $userEditor = new UserEditor($this);
180 $userEditor->update([
181 'password' => $password,
185 // $isValid is always true at this point. However we intentionally use a variable
186 // that defaults to false to prevent accidents during refactoring.
193 * @deprecated 5.4 - This method always returns false, as user sessions are long-lived now.
195 public function checkCookiePassword($passwordHash)
201 * Returns an array with all the groups in which the actual user is a member.
203 * @param bool $skipCache
206 public function getGroupIDs($skipCache = false)
208 if ($this->groupIDs
=== null ||
$skipCache) {
209 if (!$this->userID
) {
210 // user is a guest, use default guest group
211 $this->groupIDs
= UserGroup
::getGroupIDsByType([UserGroup
::GUESTS
, UserGroup
::EVERYONE
]);
214 $data = UserStorageHandler
::getInstance()->getField('groupIDs', $this->userID
);
216 // cache does not exist or is outdated
217 if ($data === null ||
$skipCache) {
218 $sql = "SELECT groupID
219 FROM wcf" . WCF_N
. "_user_to_group
221 $statement = WCF
::getDB()->prepareStatement($sql);
222 $statement->execute([$this->userID
]);
223 $this->groupIDs
= $statement->fetchAll(\PDO
::FETCH_COLUMN
);
225 // update storage data
227 UserStorageHandler
::getInstance()->update(
230 \
serialize($this->groupIDs
)
234 $this->groupIDs
= \
unserialize($data);
238 \
sort($this->groupIDs
, \SORT_NUMERIC
);
241 return $this->groupIDs
;
245 * Returns a list of language ids for this user.
249 public function getLanguageIDs()
251 if ($this->languageIDs
=== null) {
252 $this->languageIDs
= [];
256 $data = UserStorageHandler
::getInstance()->getField('languageIDs', $this->userID
);
258 // cache does not exist or is outdated
259 if ($data === null) {
260 $sql = "SELECT languageID
261 FROM wcf" . WCF_N
. "_user_to_language
263 $statement = WCF
::getDB()->prepareStatement($sql);
264 $statement->execute([$this->userID
]);
265 $this->languageIDs
= $statement->fetchAll(\PDO
::FETCH_COLUMN
);
267 // update storage data
268 UserStorageHandler
::getInstance()->update(
271 \
serialize($this->languageIDs
)
274 $this->languageIDs
= \
unserialize($data);
277 $this->languageIDs
= LanguageFactory
::getInstance()->getContentLanguageIDs();
281 return $this->languageIDs
;
285 * Returns the value of the user option with the given name.
287 * @param string $name user option name
288 * @param bool $filterDisabled suppress values for disabled options
289 * @return mixed user option value
291 public function getUserOption($name, $filterDisabled = false)
293 $optionID = self
::getUserOptionID($name);
294 if ($optionID === null) {
296 } elseif ($filterDisabled && self
::$userOptions[$name]->isDisabled
) {
300 return $this->data
['userOption' . $optionID] ??
null;
304 * Fetches all user options from cache.
306 protected static function getUserOptionCache()
308 self
::$userOptions = UserOptionCacheBuilder
::getInstance()->getData([], 'options');
312 * Returns the id of a user option.
314 * @param string $name
317 public static function getUserOptionID($name)
319 // get user option cache if necessary
320 if (self
::$userOptions === null) {
321 self
::getUserOptionCache();
324 if (!isset(self
::$userOptions[$name])) {
328 return self
::$userOptions[$name]->optionID
;
334 public function __get($name)
336 return $this->data
[$name] ??
$this->getUserOption($name);
340 * Returns the user with the given username.
342 * @param string $username
345 public static function getUserByUsername($username)
347 $sql = "SELECT user_option_value.*, user_table.*
348 FROM wcf" . WCF_N
. "_user user_table
349 LEFT JOIN wcf" . WCF_N
. "_user_option_value user_option_value
350 ON user_option_value.userID = user_table.userID
351 WHERE user_table.username = ?";
352 $statement = WCF
::getDB()->prepareStatement($sql);
353 $statement->execute([$username]);
354 $row = $statement->fetchArray();
359 return new self(null, $row);
363 * Returns the user with the given email.
365 * @param string $email
368 public static function getUserByEmail($email)
370 $sql = "SELECT user_option_value.*, user_table.*
371 FROM wcf" . WCF_N
. "_user user_table
372 LEFT JOIN wcf" . WCF_N
. "_user_option_value user_option_value
373 ON user_option_value.userID = user_table.userID
374 WHERE user_table.email = ?";
375 $statement = WCF
::getDB()->prepareStatement($sql);
376 $statement->execute([$email]);
377 $row = $statement->fetchArray();
382 return new self(null, $row);
386 * Returns the user with the given authData.
388 * @param string $authData
391 public static function getUserByAuthData($authData)
393 $sql = "SELECT user_option_value.*, user_table.*
394 FROM wcf" . WCF_N
. "_user user_table
395 LEFT JOIN wcf" . WCF_N
. "_user_option_value user_option_value
396 ON user_option_value.userID = user_table.userID
397 WHERE user_table.authData = ?";
398 $statement = WCF
::getDB()->prepareStatement($sql);
399 $statement->execute([$authData]);
400 $row = $statement->fetchArray();
405 return new self(null, $row);
409 * Returns 3rd party auth provider name.
414 public function getAuthProvider()
416 if (!$this->authData
) {
420 return \
mb_substr($this->authData
, 0, \
mb_strpos($this->authData
, ':'));
424 * Returns true if this user is marked.
428 public function isMarked()
430 $markedUsers = WCF
::getSession()->getVar('markedUsers');
431 if ($markedUsers !== null) {
432 if (\
in_array($this->userID
, $markedUsers)) {
441 * Returns true if the email is confirmed.
446 public function isEmailConfirmed()
448 return $this->emailConfirmed
=== null;
452 * Returns the time zone of this user.
454 * @return \DateTimeZone
456 public function getTimeZone()
458 if ($this->timezoneObj
=== null) {
459 if ($this->timezone
) {
460 $this->timezoneObj
= new \
DateTimeZone($this->timezone
);
462 $this->timezoneObj
= new \
DateTimeZone(TIMEZONE
);
466 return $this->timezoneObj
;
470 * Applies the user's timezone to the given timestamp.
472 public function getLocalDate(int $timestamp): \DateTimeImmutable
474 $dateTime = (new \
DateTimeImmutable('@' . $timestamp));
475 $dateTime = $dateTime->setTimezone($this->getTimeZone());
481 * Returns a list of users.
483 * @param array $userIDs
486 public static function getUsers(array $userIDs)
488 $userList = new UserList();
489 $userList->setObjectIDs($userIDs);
490 $userList->readObjects();
492 return $userList->getObjects();
498 public function __toString(): string
500 return $this->getTitle();
506 public static function getDatabaseTableAlias()
514 public function getTitle(): string
516 return $this->username ?
: '';
520 * Returns the language of this user.
524 public function getLanguage()
526 $language = LanguageFactory
::getInstance()->getLanguage($this->languageID
);
527 if ($language === null) {
528 $language = LanguageFactory
::getInstance()->getLanguage(LanguageFactory
::getInstance()->getDefaultLanguageID());
535 * Returns true if the active user can edit this user.
539 public function canEdit()
541 return WCF
::getSession()->getPermission('admin.user.canEditUser') && UserGroup
::isAccessibleGroup($this->getGroupIDs());
545 * Returns true, if this user has access to the ACP.
549 public function hasAdministrativeAccess()
551 if ($this->hasAdministrativePermissions
=== null) {
552 $this->hasAdministrativePermissions
= false;
555 foreach (UserGroup
::getGroupsByIDs($this->getGroupIDs()) as $group) {
556 if ($group->isAdminGroup()) {
557 $this->hasAdministrativePermissions
= true;
564 return $this->hasAdministrativePermissions
;
568 * Returns true, if this user is a member of the owner group.
573 public function hasOwnerAccess()
575 foreach (UserGroup
::getGroupsByIDs($this->getGroupIDs()) as $group) {
576 if ($group->isOwner()) {
587 public function getUserID()
589 return $this->userID
;
595 public function getUsername()
597 return $this->username
;
603 public function getTime()
605 return $this->registrationDate
;
611 public function getLink(): string
613 return LinkHandler
::getInstance()->getLink('User', [
614 'application' => 'wcf',
616 'forceFrontend' => true,
621 * Returns the registration ip address, attempts to convert to IPv4.
625 public function getRegistrationIpAddress()
627 if ($this->registrationIpAddress
) {
628 return UserUtil
::convertIPv6To4($this->registrationIpAddress
);
635 * Returns true, if this user can purchase paid subscriptions.
639 public function canPurchasePaidSubscriptions()
641 return WCF
::getUser()->userID
&& !$this->pendingActivation();
645 * Returns the list of fields that had matches in the blacklist. An empty list is
646 * returned if the user has been approved, regardless of any known matches.
651 public function getBlacklistMatches()
653 if ($this->pendingActivation() && $this->blacklistMatches
) {
654 $matches = JSON
::decode($this->blacklistMatches
);
655 if (\
is_array($matches)) {
664 * Returns a human readable list of fields that have positive matches against the
665 * blacklist. If you require the raw field names, please use `getBlacklistMatches()`
671 public function getBlacklistMatchesTitle()
673 return \array_map
(static function ($field) {
674 if ($field === 'ip') {
675 $field = 'ipAddress';
678 return WCF
::getLanguage()->get('wcf.user.' . $field);
679 }, $this->getBlacklistMatches());
683 * Returns true if this user is not activated.
688 public function pendingActivation()
690 return $this->activationCode
!= 0;
694 * Returns true if this user requires activation by the user.
699 public function requiresEmailActivation()
701 return REGISTER_ACTIVATION_METHOD
& self
::REGISTER_ACTIVATION_USER
&& $this->pendingActivation() && !$this->isEmailConfirmed();
705 * Returns true if this user requires the activation by an admin.
710 public function requiresAdminActivation()
712 return REGISTER_ACTIVATION_METHOD
& self
::REGISTER_ACTIVATION_ADMIN
&& $this->pendingActivation();
716 * Returns true if this user can confirm the email themself.
721 public function canEmailConfirm()
723 return REGISTER_ACTIVATION_METHOD
& self
::REGISTER_ACTIVATION_USER
&& !$this->isEmailConfirmed();
727 * Returns true, if the user must confirm his email by themself.
732 public function mustSelfEmailConfirm()
734 return REGISTER_ACTIVATION_METHOD
& self
::REGISTER_ACTIVATION_USER
;
738 * Returns true if the user is a member of a user group that requires
739 * multi-factor authentication to be enabled.
743 public function requiresMultifactor(): bool
745 foreach (UserGroup
::getGroupsByIDs($this->getGroupIDs()) as $group) {
746 if ($group->requireMultifactor
) {
757 public function getPopoverLinkClass()