Merge branch '3.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / user / User.class.php
1 <?php
2 namespace wcf\data\user;
3 use wcf\data\language\Language;
4 use wcf\data\user\group\UserGroup;
5 use wcf\data\DatabaseObject;
6 use wcf\data\IUserContent;
7 use wcf\data\user\option\UserOption;
8 use wcf\system\cache\builder\UserOptionCacheBuilder;
9 use wcf\system\language\LanguageFactory;
10 use wcf\system\request\IRouteController;
11 use wcf\system\request\LinkHandler;
12 use wcf\system\user\storage\UserStorageHandler;
13 use wcf\system\WCF;
14 use wcf\util\CryptoUtil;
15 use wcf\util\PasswordUtil;
16 use wcf\util\UserUtil;
17
18 /**
19 * Represents a user.
20 *
21 * @author Alexander Ebert
22 * @copyright 2001-2018 WoltLab GmbH
23 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
24 * @package WoltLabSuite\Core\Data\User
25 *
26 * @property-read integer $userID unique id of the user
27 * @property-read string $username name of the user
28 * @property-read string $email email address of the user
29 * @property-read string $password double salted hash of the user's password
30 * @property-read string $accessToken token used for access authentication, for example used by feed pages
31 * @property-read integer $languageID id of the interface language used by the user
32 * @property-read integer $registrationDate timestamp at which the user has registered/has been created
33 * @property-read integer $styleID id of the style used by the user
34 * @property-read integer $banned is `1` if the user is banned, otherwise `0`
35 * @property-read string $banReason reason why the user is banned
36 * @property-read integer $banExpires timestamp at which the banned user is automatically unbanned
37 * @property-read integer $activationCode code sent to the user's email address used for account activation
38 * @property-read integer $lastLostPasswordRequestTime timestamp at which the user has reported that they lost their password or 0 if password has not been reported as lost
39 * @property-read string $lostPasswordKey code used for authenticating setting new password after password loss or empty if password has not been reported as lost
40 * @property-read integer $lastUsernameChange timestamp at which the user changed their name the last time or 0 if username has not been changed
41 * @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
42 * @property-read string $oldUsername previous name of the user or empty if they have had no previous name
43 * @property-read integer $quitStarted timestamp at which the user terminated their account
44 * @property-read integer $reactivationCode code used for authenticating setting new email address or empty if no new email address has been set
45 * @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
46 * @property-read integer|null $avatarID id of the user's avatar or null if they have no avatar
47 * @property-read integer $disableAvatar is `1` if the user's avatar has been disabled, otherwise `0`
48 * @property-read string $disableAvatarReason reason why the user's avatar is disabled
49 * @property-read integer $disableAvatarExpires timestamp at which the user's avatar will automatically be enabled again
50 * @property-read integer $enableGravatar is `1` if the user uses a gravatar as avatar, otherwise `0`
51 * @property-read string $gravatarFileExtension extension of the user's gravatar file
52 * @property-read string $signature text of the user's signature
53 * @property-read integer $signatureEnableBBCodes is `1` if BBCodes will rendered in the user's signature, otherwise `0`
54 * @property-read integer $signatureEnableHtml is `1` if HTML will rendered in the user's signature, otherwise `0`
55 * @property-read integer $signatureEnableSmilies is `1` if smilies will rendered in the user's signature, otherwise `0`
56 * @property-read integer $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 integer $disableSignatureExpires timestamp at which the user's signature will automatically be enabled again
59 * @property-read integer $lastActivityTime timestamp of the user's last activity
60 * @property-read integer $profileHits number of times the user's profile has been visited
61 * @property-read integer|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 integer|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 integer $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 integer $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 integer $disableCoverPhoto is `1` if the user's cover photo has been disabled, otherwise `0`
71 * @property-read string $disableCoverPhotoReason reason why the user's cover photo is disabled
72 * @property-read integer $disableCoverPhotoExpires timestamp at which the user's cover photo will automatically be enabled again
73 */
74 final class User extends DatabaseObject implements IRouteController, IUserContent {
75 /**
76 * list of group ids
77 * @var integer[]
78 */
79 protected $groupIDs;
80
81 /**
82 * true, if user has access to the ACP
83 * @var boolean
84 */
85 protected $hasAdministrativePermissions;
86
87 /**
88 * list of language ids
89 * @var integer[]
90 */
91 protected $languageIDs;
92
93 /**
94 * date time zone object
95 * @var \DateTimeZone
96 */
97 protected $timezoneObj;
98
99 /**
100 * list of user options
101 * @var UserOption[]
102 */
103 protected static $userOptions;
104
105 /** @noinspection PhpMissingParentConstructorInspection */
106 /**
107 * @inheritDoc
108 */
109 public function __construct($id, $row = null, DatabaseObject $object = null) {
110 if ($id !== null) {
111 $sql = "SELECT user_option_value.*, user_table.*
112 FROM wcf".WCF_N."_user user_table
113 LEFT JOIN wcf".WCF_N."_user_option_value user_option_value
114 ON (user_option_value.userID = user_table.userID)
115 WHERE user_table.userID = ?";
116 $statement = WCF::getDB()->prepareStatement($sql);
117 $statement->execute([$id]);
118 $row = $statement->fetchArray();
119
120 // enforce data type 'array'
121 if ($row === false) $row = [];
122 }
123 else if ($object !== null) {
124 $row = $object->data;
125 }
126
127 $this->handleData($row);
128 }
129
130 /**
131 * Returns true if the given password is the correct password for this user.
132 *
133 * @param string $password
134 * @return boolean password correct
135 */
136 public function checkPassword($password) {
137 $isValid = false;
138 $rebuild = false;
139
140 // check if password is a valid bcrypt hash
141 if (PasswordUtil::isBlowfish($this->password)) {
142 if (PasswordUtil::isDifferentBlowfish($this->password)) {
143 $rebuild = true;
144 }
145
146 // password is correct
147 if (CryptoUtil::secureCompare($this->password, PasswordUtil::getDoubleSaltedHash($password, $this->password))) {
148 $isValid = true;
149 }
150 }
151 else {
152 // different encryption type
153 if (PasswordUtil::checkPassword($this->username, $password, $this->password)) {
154 $isValid = true;
155 $rebuild = true;
156 }
157 }
158
159 // create new password hash, either different encryption or different blowfish cost factor
160 if ($rebuild && $isValid) {
161 $userEditor = new UserEditor($this);
162 $userEditor->update([
163 'password' => $password
164 ]);
165 }
166
167 return $isValid;
168 }
169
170 /**
171 * Returns true if the given password hash from a cookie is the correct password for this user.
172 *
173 * @param string $passwordHash
174 * @return boolean password correct
175 */
176 public function checkCookiePassword($passwordHash) {
177 if (PasswordUtil::isBlowfish($this->password) && CryptoUtil::secureCompare($this->password, PasswordUtil::getSaltedHash($passwordHash, $this->password))) {
178 return true;
179 }
180
181 return false;
182 }
183
184 /**
185 * Returns an array with all the groups in which the actual user is a member.
186 *
187 * @param boolean $skipCache
188 * @return integer[]
189 */
190 public function getGroupIDs($skipCache = false) {
191 if ($this->groupIDs === null || $skipCache) {
192 if (!$this->userID) {
193 // user is a guest, use default guest group
194 $this->groupIDs = UserGroup::getGroupIDsByType([UserGroup::GUESTS, UserGroup::EVERYONE]);
195 }
196 else {
197 // get group ids
198 $data = UserStorageHandler::getInstance()->getField('groupIDs', $this->userID);
199
200 // cache does not exist or is outdated
201 if ($data === null || $skipCache) {
202 $sql = "SELECT groupID
203 FROM wcf".WCF_N."_user_to_group
204 WHERE userID = ?";
205 $statement = WCF::getDB()->prepareStatement($sql);
206 $statement->execute([$this->userID]);
207 $this->groupIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
208
209 // update storage data
210 if (!$skipCache) {
211 UserStorageHandler::getInstance()->update($this->userID, 'groupIDs', serialize($this->groupIDs));
212 }
213 }
214 else {
215 $this->groupIDs = unserialize($data);
216 }
217 }
218
219 sort($this->groupIDs, SORT_NUMERIC);
220 }
221
222 return $this->groupIDs;
223 }
224
225 /**
226 * Returns a list of language ids for this user.
227 *
228 * @return integer[]
229 */
230 public function getLanguageIDs() {
231 if ($this->languageIDs === null) {
232 $this->languageIDs = [];
233
234 if ($this->userID) {
235 // get language ids
236 $data = UserStorageHandler::getInstance()->getField('languageIDs', $this->userID);
237
238 // cache does not exist or is outdated
239 if ($data === null) {
240 $sql = "SELECT languageID
241 FROM wcf".WCF_N."_user_to_language
242 WHERE userID = ?";
243 $statement = WCF::getDB()->prepareStatement($sql);
244 $statement->execute([$this->userID]);
245 $this->languageIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
246
247 // update storage data
248 UserStorageHandler::getInstance()->update($this->userID, 'languageIDs', serialize($this->languageIDs));
249 }
250 else {
251 $this->languageIDs = unserialize($data);
252 }
253 }
254 else if (!WCF::getSession()->spiderID) {
255 $this->languageIDs[] = WCF::getLanguage()->languageID;
256 }
257 }
258
259 return $this->languageIDs;
260 }
261
262 /**
263 * Returns the value of the user option with the given name.
264 *
265 * @param string $name user option name
266 * @param boolean $filterDisabled suppress values for disabled options
267 * @return mixed user option value
268 */
269 public function getUserOption($name, $filterDisabled = false) {
270 $optionID = self::getUserOptionID($name);
271 if ($optionID === null) {
272 return null;
273 }
274 else if ($filterDisabled && self::$userOptions[$name]->isDisabled) {
275 return null;
276 }
277
278 if (!isset($this->data['userOption'.$optionID])) return null;
279 return $this->data['userOption'.$optionID];
280 }
281
282 /**
283 * Fetches all user options from cache.
284 */
285 protected static function getUserOptionCache() {
286 self::$userOptions = UserOptionCacheBuilder::getInstance()->getData([], 'options');
287 }
288
289 /**
290 * Returns the id of a user option.
291 *
292 * @param string $name
293 * @return integer id
294 */
295 public static function getUserOptionID($name) {
296 // get user option cache if necessary
297 if (self::$userOptions === null) {
298 self::getUserOptionCache();
299 }
300
301 if (!isset(self::$userOptions[$name])) {
302 return null;
303 }
304
305 return self::$userOptions[$name]->optionID;
306 }
307
308 /**
309 * @inheritDoc
310 */
311 public function __get($name) {
312 $value = parent::__get($name);
313 if ($value === null) $value = $this->getUserOption($name);
314 return $value;
315 }
316
317 /**
318 * Returns the user with the given username.
319 *
320 * @param string $username
321 * @return User
322 */
323 public static function getUserByUsername($username) {
324 $sql = "SELECT user_option_value.*, user_table.*
325 FROM wcf".WCF_N."_user user_table
326 LEFT JOIN wcf".WCF_N."_user_option_value user_option_value
327 ON (user_option_value.userID = user_table.userID)
328 WHERE user_table.username = ?";
329 $statement = WCF::getDB()->prepareStatement($sql);
330 $statement->execute([$username]);
331 $row = $statement->fetchArray();
332 if (!$row) $row = [];
333
334 return new User(null, $row);
335 }
336
337 /**
338 * Returns the user with the given email.
339 *
340 * @param string $email
341 * @return User
342 */
343 public static function getUserByEmail($email) {
344 $sql = "SELECT user_option_value.*, user_table.*
345 FROM wcf".WCF_N."_user user_table
346 LEFT JOIN wcf".WCF_N."_user_option_value user_option_value
347 ON (user_option_value.userID = user_table.userID)
348 WHERE user_table.email = ?";
349 $statement = WCF::getDB()->prepareStatement($sql);
350 $statement->execute([$email]);
351 $row = $statement->fetchArray();
352 if (!$row) $row = [];
353
354 return new User(null, $row);
355 }
356
357 /**
358 * Returns the user with the given authData.
359 *
360 * @param string $authData
361 * @return User
362 */
363 public static function getUserByAuthData($authData) {
364 $sql = "SELECT user_option_value.*, user_table.*
365 FROM wcf".WCF_N."_user user_table
366 LEFT JOIN wcf".WCF_N."_user_option_value user_option_value
367 ON (user_option_value.userID = user_table.userID)
368 WHERE user_table.authData = ?";
369 $statement = WCF::getDB()->prepareStatement($sql);
370 $statement->execute([$authData]);
371 $row = $statement->fetchArray();
372 if (!$row) $row = [];
373
374 return new User(null, $row);
375 }
376
377 /**
378 * Returns true if this user is marked.
379 *
380 * @return boolean
381 */
382 public function isMarked() {
383 $markedUsers = WCF::getSession()->getVar('markedUsers');
384 if ($markedUsers !== null) {
385 if (in_array($this->userID, $markedUsers)) return 1;
386 }
387
388 return 0;
389 }
390
391 /**
392 * Returns the time zone of this user.
393 *
394 * @return \DateTimeZone
395 */
396 public function getTimeZone() {
397 if ($this->timezoneObj === null) {
398 if ($this->timezone) {
399 $this->timezoneObj = new \DateTimeZone($this->timezone);
400 }
401 else {
402 $this->timezoneObj = new \DateTimeZone(TIMEZONE);
403 }
404 }
405
406 return $this->timezoneObj;
407 }
408
409 /**
410 * Returns a list of users.
411 *
412 * @param array $userIDs
413 * @return User[]
414 */
415 public static function getUsers(array $userIDs) {
416 $userList = new UserList();
417 $userList->setObjectIDs($userIDs);
418 $userList->readObjects();
419
420 return $userList->getObjects();
421 }
422
423 /**
424 * Returns username.
425 *
426 * @return string
427 */
428 public function __toString() {
429 return ($this->username ?: '');
430 }
431
432 /**
433 * @inheritDoc
434 */
435 public static function getDatabaseTableAlias() {
436 return 'user_table';
437 }
438
439 /**
440 * @inheritDoc
441 */
442 public function getTitle() {
443 return $this->username;
444 }
445
446 /**
447 * Returns the language of this user.
448 *
449 * @return Language
450 */
451 public function getLanguage() {
452 $language = LanguageFactory::getInstance()->getLanguage($this->languageID);
453 if ($language === null) {
454 $language = LanguageFactory::getInstance()->getLanguage(LanguageFactory::getInstance()->getDefaultLanguageID());
455 }
456
457 return $language;
458 }
459
460 /**
461 * Returns true if the active user can edit this user.
462 *
463 * @return boolean
464 */
465 public function canEdit() {
466 return (WCF::getSession()->getPermission('admin.user.canEditUser') && UserGroup::isAccessibleGroup($this->getGroupIDs()));
467 }
468
469 /**
470 * Returns true, if this user has access to the ACP.
471 *
472 * @return boolean
473 */
474 public function hasAdministrativeAccess() {
475 if ($this->hasAdministrativePermissions === null) {
476 $this->hasAdministrativePermissions = false;
477
478 if ($this->userID) {
479 foreach ($this->getGroupIDs() as $groupID) {
480 $group = UserGroup::getGroupByID($groupID);
481 if ($group->isAdminGroup()) {
482 $this->hasAdministrativePermissions = true;
483 break;
484 }
485 }
486 }
487 }
488
489 return $this->hasAdministrativePermissions;
490 }
491
492 /**
493 * @inheritDoc
494 */
495 public function getUserID() {
496 return $this->userID;
497 }
498
499 /**
500 * @inheritDoc
501 */
502 public function getUsername() {
503 return $this->username;
504 }
505
506 /**
507 * @inheritDoc
508 */
509 public function getTime() {
510 return $this->registrationDate;
511 }
512
513 /**
514 * @inheritDoc
515 */
516 public function getLink() {
517 return LinkHandler::getInstance()->getLink('User', [
518 'application' => 'wcf',
519 'object' => $this,
520 'forceFrontend' => true
521 ]);
522 }
523
524 /**
525 * Returns the social network privacy settings of the user.
526 * @deprecated 3.0
527 *
528 * @return boolean[]
529 */
530 public function getSocialNetworkPrivacySettings() {
531 return [
532 'facebook' => false,
533 'google' => false,
534 'reddit' => false,
535 'twitter' => false
536 ];
537 }
538
539 /**
540 * Returns the registration ip address, attempts to convert to IPv4.
541 *
542 * @return string
543 */
544 public function getRegistrationIpAddress() {
545 if ($this->registrationIpAddress) {
546 return UserUtil::convertIPv6To4($this->registrationIpAddress);
547 }
548
549 return '';
550 }
551 }