Merge branch '2.0'
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / data / user / UserProfile.class.php
CommitLineData
320f4a6d
MW
1<?php
2namespace wcf\data\user;
3use wcf\data\user\avatar\DefaultAvatar;
4use wcf\data\user\avatar\Gravatar;
5use wcf\data\user\avatar\UserAvatar;
2fd304c6 6use wcf\data\user\group\UserGroup;
320f4a6d 7use wcf\data\user\online\UserOnline;
22bbc96b 8use wcf\data\user\option\ViewableUserOption;
320f4a6d
MW
9use wcf\data\user\rank\UserRank;
10use wcf\data\DatabaseObjectDecorator;
11use wcf\system\breadcrumb\Breadcrumb;
12use wcf\system\breadcrumb\IBreadcrumbProvider;
13use wcf\system\cache\builder\UserGroupPermissionCacheBuilder;
14use wcf\system\request\LinkHandler;
15use wcf\system\user\online\location\UserOnlineLocationHandler;
16use wcf\system\user\signature\SignatureCache;
17use wcf\system\user\storage\UserStorageHandler;
18use wcf\system\WCF;
19use wcf\util\DateUtil;
20use wcf\util\StringUtil;
21
22/**
23 * Decorates the user object and provides functions to retrieve data for user profiles.
24 *
25 * @author Marcel Werk
ca4ba303 26 * @copyright 2001-2014 WoltLab GmbH
320f4a6d 27 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
f4f05aa5 28 * @package com.woltlab.wcf
320f4a6d
MW
29 * @subpackage data.user
30 * @category Community Framework
31 */
32class UserProfile extends DatabaseObjectDecorator implements IBreadcrumbProvider {
33 /**
0ad90fc3 34 * @see \wcf\data\DatabaseObjectDecorator::$baseClass
320f4a6d
MW
35 */
36 protected static $baseClass = 'wcf\data\user\User';
37
38 /**
39 * cached list of user profiles
0ad90fc3 40 * @var array<\wcf\data\user\UserProfile>
320f4a6d
MW
41 */
42 protected static $userProfiles = array();
43
44 /**
45 * list of ignored user ids
46 * @var array<integer>
47 */
48 protected $ignoredUserIDs = null;
49
50 /**
51 * list of follower user ids
52 * @var array<integer>
53 */
54 protected $followerUserIDs = null;
55
56 /**
57 * list of following user ids
58 * @var array<integer>
59 */
60 protected $followingUserIDs = null;
61
62 /**
63 * user avatar
0ad90fc3 64 * @var \wcf\data\user\avatar\IUserAvatar
320f4a6d
MW
65 */
66 protected $avatar = null;
67
68 /**
69 * user rank object
0ad90fc3 70 * @var \wcf\data\user\rank\UserRank
320f4a6d
MW
71 */
72 protected $rank = null;
73
74 /**
75 * age of this user
76 * @var integer
77 */
78 protected $__age = null;
79
80 /**
81 * group data and permissions
82 * @var array<array>
83 */
84 protected $groupData = null;
85
86 /**
87 * current location of this user.
88 * @var string
89 */
90 protected $currentLocation = null;
91
92 const GENDER_MALE = 1;
93 const GENDER_FEMALE = 2;
94
95 const ACCESS_EVERYONE = 0;
96 const ACCESS_REGISTERED = 1;
97 const ACCESS_FOLLOWING = 2;
98 const ACCESS_NOBODY = 3;
99
100 /**
0ad90fc3 101 * @see \wcf\data\user\User::__toString()
320f4a6d
MW
102 */
103 public function __toString() {
104 return $this->getDecoratedObject()->__toString();
105 }
106
107 /**
108 * Returns a list of all user ids being followed by current user.
109 *
110 * @return array<integer>
111 */
112 public function getFollowingUsers() {
113 if ($this->followingUserIDs === null) {
114 $this->followingUserIDs = array();
115
116 if ($this->userID) {
117 // load storage data
118 UserStorageHandler::getInstance()->loadStorage(array($this->userID));
119
120 // get ids
121 $data = UserStorageHandler::getInstance()->getStorage(array($this->userID), 'followingUserIDs');
122
123 // cache does not exist or is outdated
124 if ($data[$this->userID] === null) {
125 $sql = "SELECT followUserID
126 FROM wcf".WCF_N."_user_follow
127 WHERE userID = ?";
128 $statement = WCF::getDB()->prepareStatement($sql);
129 $statement->execute(array($this->userID));
130 while ($row = $statement->fetchArray()) {
131 $this->followingUserIDs[] = $row['followUserID'];
132 }
133
134 // update storage data
135 UserStorageHandler::getInstance()->update($this->userID, 'followingUserIDs', serialize($this->followingUserIDs));
136 }
137 else {
138 $this->followingUserIDs = unserialize($data[$this->userID]);
139 }
140 }
141 }
142
143 return $this->followingUserIDs;
144 }
145
146 /**
147 * Returns a list of user ids following current user.
148 *
149 * @return array<integer>
150 */
151 public function getFollowers() {
152 if ($this->followerUserIDs === null) {
153 $this->followerUserIDs = array();
154
155 if ($this->userID) {
156 // load storage data
157 UserStorageHandler::getInstance()->loadStorage(array($this->userID));
158
159 // get ids
160 $data = UserStorageHandler::getInstance()->getStorage(array($this->userID), 'followerUserIDs');
161
162 // cache does not exist or is outdated
163 if ($data[$this->userID] === null) {
164 $sql = "SELECT userID
165 FROM wcf".WCF_N."_user_follow
166 WHERE followUserID = ?";
167 $statement = WCF::getDB()->prepareStatement($sql);
168 $statement->execute(array($this->userID));
169 while ($row = $statement->fetchArray()) {
170 $this->followerUserIDs[] = $row['userID'];
171 }
172
173 // update storage data
174 UserStorageHandler::getInstance()->update($this->userID, 'followerUserIDs', serialize($this->followerUserIDs));
175 }
176 else {
177 $this->followerUserIDs = unserialize($data[$this->userID]);
178 }
179 }
180 }
181
182 return $this->followerUserIDs;
183 }
184
185 /**
186 * Returns a list of ignored user ids.
187 *
188 * @return array<integer>
189 */
190 public function getIgnoredUsers() {
191 if ($this->ignoredUserIDs === null) {
192 $this->ignoredUserIDs = array();
193
194 if ($this->userID) {
195 // load storage data
196 UserStorageHandler::getInstance()->loadStorage(array($this->userID));
197
198 // get ids
199 $data = UserStorageHandler::getInstance()->getStorage(array($this->userID), 'ignoredUserIDs');
200
201 // cache does not exist or is outdated
202 if ($data[$this->userID] === null) {
203 $sql = "SELECT ignoreUserID
204 FROM wcf".WCF_N."_user_ignore
205 WHERE userID = ?";
206 $statement = WCF::getDB()->prepareStatement($sql);
207 $statement->execute(array($this->userID));
208 while ($row = $statement->fetchArray()) {
209 $this->ignoredUserIDs[] = $row['ignoreUserID'];
210 }
211
212 // update storage data
213 UserStorageHandler::getInstance()->update($this->userID, 'ignoredUserIDs', serialize($this->ignoredUserIDs));
214 }
215 else {
216 $this->ignoredUserIDs = unserialize($data[$this->userID]);
217 }
218 }
219 }
220
221 return $this->ignoredUserIDs;
222 }
223
224 /**
225 * Returns true if current user is following given user id.
226 *
227 * @param integer $userID
228 * @return boolean
229 */
230 public function isFollowing($userID) {
231 return in_array($userID, $this->getFollowingUsers());
232 }
233
234 /**
235 * Returns true if given user ids follows current user.
236 *
237 * @param integer $userID
238 * @return boolean
239 */
240 public function isFollower($userID) {
241 return in_array($userID, $this->getFollowers());
242 }
243
244 /**
245 * Returns true if given user is ignored.
246 *
247 * @param integer $userID
248 * @return boolean
249 */
250 public function isIgnoredUser($userID) {
251 return in_array($userID, $this->getIgnoredUsers());
252 }
253
254 /**
255 * Gets the user's avatar.
256 *
0ad90fc3 257 * @return \wcf\data\user\avatar\IUserAvatar
320f4a6d
MW
258 */
259 public function getAvatar() {
260 if ($this->avatar === null) {
261 if (!$this->disableAvatar) {
2f6ace13
SG
262 if ($this->canSeeAvatar()) {
263 if ($this->avatarID) {
264 if (!$this->fileHash) {
265 // load storage data
266 UserStorageHandler::getInstance()->loadStorage(array($this->userID));
267 $data = UserStorageHandler::getInstance()->getStorage(array($this->userID), 'avatar');
268
269 if ($data[$this->userID] === null) {
270 $this->avatar = new UserAvatar($this->avatarID);
271 UserStorageHandler::getInstance()->update($this->userID, 'avatar', serialize($this->avatar));
272 }
273 else {
274 $this->avatar = unserialize($data[$this->userID]);
275 }
320f4a6d
MW
276 }
277 else {
2f6ace13 278 $this->avatar = new UserAvatar(null, $this->getDecoratedObject()->data);
320f4a6d
MW
279 }
280 }
2f6ace13
SG
281 else if (MODULE_GRAVATAR && $this->enableGravatar) {
282 $this->avatar = new Gravatar($this->userID, $this->email);
320f4a6d
MW
283 }
284 }
320f4a6d
MW
285 }
286
287 // use default avatar
288 if ($this->avatar === null) {
289 $this->avatar = new DefaultAvatar();
290 }
291 }
292
293 return $this->avatar;
294 }
295
296 /**
2f6ace13
SG
297 * Returns true if the active user can view the avatar of this user.
298 *
299 * @return boolean
300 */
301 public function canSeeAvatar() {
302 return (WCF::getUser()->userID == $this->userID || WCF::getSession()->getPermission('user.profile.avatar.canSeeAvatars'));
303 }
304
305 /**
320f4a6d
MW
306 * Returns true if this user is currently online.
307 *
308 * @return boolean
309 */
310 public function isOnline() {
019702d2 311 if ($this->getLastActivityTime() > (TIME_NOW - USER_ONLINE_TIMEOUT) && $this->canViewOnlineStatus()) {
320f4a6d
MW
312 return true;
313 }
314 return false;
315 }
316
317 /**
318 * Returns true if the active user can view the online status of this user.
319 *
320 * @return boolean
321 */
322 public function canViewOnlineStatus() {
323 return (WCF::getUser()->userID == $this->userID || WCF::getSession()->getPermission('admin.user.canViewInvisible') || $this->isAccessible('canViewOnlineStatus'));
324 }
325
326 /**
327 * Returns the current location of this user.
328 *
329 * @return string
330 */
331 public function getCurrentLocation() {
332 if ($this->currentLocation === null) {
333 $this->currentLocation = '';
334 $this->currentLocation = UserOnlineLocationHandler::getInstance()->getLocation(new UserOnline(new User(null, array(
335 'controller' => $this->controller,
336 'objectID' => $this->locationObjectID
337 ))));
338 }
339
340 return $this->currentLocation;
341 }
342
343 /**
344 * Returns the last activity time.
345 *
346 * @return integer
347 */
348 public function getLastActivityTime() {
349 return max($this->lastActivityTime, $this->sessionLastActivityTime);
350 }
351
352 /**
353 * Returns a new user profile object.
354 *
355 * @param integer $userID
0ad90fc3 356 * @return \wcf\data\user\UserProfile
320f4a6d
MW
357 */
358 public static function getUserProfile($userID) {
359 $users = self::getUserProfiles(array($userID));
360
361 return (isset($users[$userID]) ? $users[$userID] : null);
362 }
363
364 /**
365 * Returns a list of user profiles.
366 *
367 * @param array $userIDs
0ad90fc3 368 * @return array<\wcf\data\user\UserProfile>
320f4a6d
MW
369 */
370 public static function getUserProfiles(array $userIDs) {
371 $users = array();
372
373 // check cache
374 foreach ($userIDs as $index => $userID) {
375 if (isset(self::$userProfiles[$userID])) {
376 $users[$userID] = self::$userProfiles[$userID];
377 unset($userIDs[$index]);
378 }
379 }
380
381 if (!empty($userIDs)) {
382 $userList = new UserProfileList();
383 $userList->getConditionBuilder()->add("user_table.userID IN (?)", array($userIDs));
384 $userList->readObjects();
385
386 foreach ($userList as $user) {
387 $users[$user->userID] = $user;
388 self::$userProfiles[$user->userID] = $user;
389 }
390 }
391
392 return $users;
393 }
394
395 /**
396 * Returns the user profile of the user with the given name.
397 *
398 * @param string $username
0ad90fc3 399 * @return \wcf\data\user\UserProfile
320f4a6d
MW
400 */
401 public static function getUserProfileByUsername($username) {
402 $users = self::getUserProfilesByUsername(array($username));
403
404 return $users[$username];
405 }
406
407 /**
408 * Returns the user profiles of the users with the given names.
409 *
410 * @param array<string> $usernames
0ad90fc3 411 * @return array<\wcf\data\user\UserProfile>
320f4a6d
MW
412 */
413 public static function getUserProfilesByUsername(array $usernames) {
414 $users = array();
415
416 // save case sensitive usernames
417 $caseSensitiveUsernames = array();
418 foreach ($usernames as &$username) {
838e315b 419 $tmp = mb_strtolower($username);
320f4a6d
MW
420 $caseSensitiveUsernames[$tmp] = $username;
421 $username = $tmp;
422 }
423 unset($username);
424
425 // check cache
426 foreach ($usernames as $index => $username) {
427 foreach (self::$userProfiles as $user) {
838e315b 428 if (mb_strtolower($user->username) === $username) {
320f4a6d
MW
429 $users[$username] = $user;
430 unset($usernames[$index]);
431 }
432 }
433 }
434
435 if (!empty($usernames)) {
436 $userList = new UserProfileList();
437 $userList->getConditionBuilder()->add("user_table.username IN (?)", array($usernames));
438 $userList->readObjects();
439
440 foreach ($userList as $user) {
838e315b 441 $users[mb_strtolower($user->username)] = $user;
320f4a6d
MW
442 self::$userProfiles[$user->userID] = $user;
443 }
444
445 foreach ($usernames as $username) {
446 if (!isset($users[$username])) {
447 $users[$username] = null;
448 }
449 }
450 }
451
452 // revert usernames to original case
453 foreach ($users as $username => $user) {
454 unset($users[$username]);
864bb47f
MW
455 if (isset($caseSensitiveUsernames[$username])) {
456 $users[$caseSensitiveUsernames[$username]] = $user;
457 }
320f4a6d
MW
458 }
459
460 return $users;
461 }
462
463 /**
464 * Returns true if current user fulfills the required permissions.
465 *
466 * @param string $name
467 * @return boolean
468 */
469 public function isAccessible($name) {
470 switch ($this->$name) {
471 case self::ACCESS_EVERYONE:
472 return true;
473 break;
474
475 case self::ACCESS_REGISTERED:
476 return (WCF::getUser()->userID ? true : false);
477 break;
478
479 case self::ACCESS_FOLLOWING:
480 return ($this->isFollowing(WCF::getUser()->userID) ? true : false);
481 break;
482
483 case self::ACCESS_NOBODY:
484 return false;
485 break;
486 }
487 }
488
489 /**
490 * Returns true if current user profile is protected.
491 *
492 * @return boolean
493 */
494 public function isProtected() {
495 return (!WCF::getSession()->getPermission('admin.general.canViewPrivateUserOptions') && !$this->isAccessible('canViewProfile') && $this->userID != WCF::getUser()->userID);
496 }
497
498 /**
499 * Returns the age of this user.
500 *
8444b115 501 * @param integer $year
320f4a6d
MW
502 * @return integer
503 */
8444b115
MW
504 public function getAge($year = null) {
505 if ($year !== null) {
506 if ($this->birthdayShowYear) {
507 $birthdayYear = 0;
508 $value = explode('-', $this->birthday);
509 if (isset($value[0])) $birthdayYear = intval($value[0]);
510 if ($birthdayYear) {
511 return $year - $birthdayYear;
512 }
513
320f4a6d 514 }
8444b115
MW
515
516 return 0;
517 }
518 else {
519 if ($this->__age === null) {
520 if ($this->birthday && $this->birthdayShowYear) {
521 $this->__age = DateUtil::getAge($this->birthday);
522 }
523 else {
524 $this->__age = 0;
525 }
526 }
527
528 return $this->__age;
529 }
530 }
531
532 /**
533 * Returns the formatted birthday of this user.
534 *
535 * @param integer $year
536 * @return string
537 */
538 public function getBirthday($year = null) {
539 // split date
540 $birthdayYear = $month = $day = 0;
541 $value = explode('-', $this->birthday);
542 if (isset($value[0])) $birthdayYear = intval($value[0]);
543 if (isset($value[1])) $month = intval($value[1]);
544 if (isset($value[2])) $day = intval($value[2]);
545
a6c7fd52
MW
546 if (!$month || !$day) return '';
547
8444b115
MW
548 $d = new \DateTime();
549 $d->setTimezone(WCF::getUser()->getTimeZone());
550 $d->setDate($birthdayYear, $month, $day);
551 $dateFormat = (($this->birthdayShowYear && $birthdayYear) ? WCF::getLanguage()->get(DateUtil::DATE_FORMAT) : str_replace('Y', '', WCF::getLanguage()->get(DateUtil::DATE_FORMAT)));
552 $birthday = DateUtil::localizeDate($d->format($dateFormat), $dateFormat, WCF::getLanguage());
553
554 if ($this->birthdayShowYear) {
555 $age = $this->getAge($year);
556 if ($age > 0) {
557 $birthday .= ' ('.$age.')';
320f4a6d
MW
558 }
559 }
560
8444b115 561 return $birthday;
320f4a6d
MW
562 }
563
564 /**
565 * Returns the age of user account in days.
566 *
567 * @return integer
568 */
569 public function getProfileAge() {
570 return (TIME_NOW - $this->registrationDate) / 86400;
571 }
572
573 /**
574 * Returns the value of the permission with the given name.
575 *
576 * @param string $permission
577 * @return mixed permission value
578 */
579 public function getPermission($permission) {
580 if ($this->groupData === null) $this->loadGroupData();
581
582 if (!isset($this->groupData[$permission])) return false;
583 return $this->groupData[$permission];
584 }
585
586 /**
587 * Returns the user title of this user.
588 *
589 * @return string
590 */
591 public function getUserTitle() {
592 if ($this->userTitle) return $this->userTitle;
593 if ($this->getRank()) return WCF::getLanguage()->get($this->getRank()->rankTitle);
594
595 return '';
596 }
597
598 /**
599 * Returns the user rank.
600 *
0ad90fc3 601 * @return \wcf\data\user\rank\UserRank
320f4a6d
MW
602 */
603 public function getRank() {
604 if ($this->rank === null) {
605 if (MODULE_USER_RANK && $this->rankID) {
606 if ($this->rankTitle) {
607 $this->rank = new UserRank(null, array(
608 'rankID' => $this->rankID,
609 'groupID' => $this->groupID,
610 'requiredPoints' => $this->requiredPoints,
611 'rankTitle' => $this->rankTitle,
612 'cssClassName' => $this->cssClassName,
613 'rankImage' => $this->rankImage,
614 'repeatImage' => $this->repeatImage,
615 'requiredGender' => $this->requiredGender
616 ));
617 }
618 else {
619 // load storage data
620 UserStorageHandler::getInstance()->loadStorage(array($this->userID));
621 $data = UserStorageHandler::getInstance()->getStorage(array($this->userID), 'userRank');
622
623 if ($data[$this->userID] === null) {
624 $this->rank = new UserRank($this->rankID);
625 UserStorageHandler::getInstance()->update($this->userID, 'userRank', serialize($this->rank));
626 }
627 else {
628 $this->rank = unserialize($data[$this->userID]);
629 }
630 }
631 }
632 }
633
634 return $this->rank;
635 }
636
637 /**
638 * Loads group data from cache.
639 */
640 protected function loadGroupData() {
641 // get group data from cache
642 $this->groupData = UserGroupPermissionCacheBuilder::getInstance()->getData($this->getGroupIDs());
643 if (isset($this->groupData['groupIDs']) && $this->groupData['groupIDs'] != $this->getGroupIDs()) {
644 $this->groupData = array();
645 }
646 }
647
648 /**
649 * Returns the old username of this user.
650 *
651 * @return string
652 */
653 public function getOldUsername() {
654 if ($this->oldUsername) {
655 if ($this->lastUsernameChange + PROFILE_SHOW_OLD_USERNAME * 86400 > TIME_NOW) {
656 return $this->oldUsername;
657 }
658 }
659
660 return '';
661 }
662
663 /**
664 * Returns true if this user can edit his profile.
665 *
666 * @return boolean
667 */
668 public function canEditOwnProfile() {
669 return ($this->activationCode ? false : true);
670 }
671
672 /**
0ad90fc3 673 * @see \wcf\system\breadcrumb\IBreadcrumbProvider::getBreadcrumb()
320f4a6d
MW
674 */
675 public function getBreadcrumb() {
676 return new Breadcrumb($this->username, LinkHandler::getInstance()->getLink('User', array(
677 'object' => $this
678 )));
679 }
680
681 /**
682 * Returns the encoded email address.
683 *
684 * @return string
685 */
686 public function getEncodedEmail() {
687 return StringUtil::encodeAllChars($this->email);
688 }
689
690 /**
691 * Returns true if the current user is connected with Facebook.
692 *
693 * @return boolean
694 */
695 public function isConnectedWithFacebook() {
696 return StringUtil::startsWith($this->authData, 'facebook:');
697 }
698
699 /**
700 * Returns true if the current user is connected with GitHub.
701 *
702 * @return boolean
703 */
704 public function isConnectedWithGithub() {
705 return StringUtil::startsWith($this->authData, 'github:');
706 }
707
708 /**
709 * Returns true if the current user is connected with Google Plus.
710 *
711 * @return boolean
712 */
713 public function isConnectedWithGoogle() {
714 return StringUtil::startsWith($this->authData, 'google:');
715 }
716
717 /**
718 * Returns true if the current user is connected with Twitter.
719 *
720 * @return boolean
721 */
722 public function isConnectedWithTwitter() {
723 return StringUtil::startsWith($this->authData, 'twitter:');
724 }
725
726 /**
727 * Returns 3rd party auth provider name.
728 *
729 * @return string
730 */
731 public function getAuthProvider() {
732 if (!$this->authData) {
733 return '';
734 }
735
838e315b 736 return mb_substr($this->authData, 0, mb_strpos($this->authData, ':'));
320f4a6d
MW
737 }
738
739 /**
740 * Return true if the user's signature is visible.
741 *
742 * @return boolean
743 */
744 public function showSignature() {
745 if (!$this->signature) return false;
acf94e2c 746 if ($this->disableSignature) return false;
320f4a6d
MW
747 if (WCF::getUser()->userID && !WCF::getUser()->showSignature) return false;
748
749 return true;
750 }
751
752 /**
753 * Returns the parsed signature.
754 *
755 * @return string
756 */
757 public function getSignature() {
758 return SignatureCache::getInstance()->getSignature($this->getDecoratedObject());
759 }
0f8df254
MW
760
761 /**
762 * Returns the formatted value of the user option with the given name.
59dc0db6 763 *
0f8df254
MW
764 * @param string $name
765 * @return mixed
766 */
767 public function getFormattedUserOption($name) {
768 // get value
769 $value = $this->getUserOption($name);
770 if (!$value) return '';
771
772 $option = ViewableUserOption::getUserOption($name);
45d248f8 773 if (!$option->isVisible()) return '';
0f8df254
MW
774 $option->setOptionValue($this->getDecoratedObject());
775 return $option->optionValue;
776 }
2fd304c6
MW
777
778 /**
779 * Returns the formatted username.
780 *
781 * @return string
782 */
783 public function getFormattedUsername() {
784 $username = StringUtil::encodeHTML($this->username);
785
786 if ($this->userOnlineGroupID) {
787 $group = UserGroup::getGroupByID($this->userOnlineGroupID);
788 if ($group !== null && $group->userOnlineMarking && $group->userOnlineMarking != '%s') {
789 return sprintf($group->userOnlineMarking, $username);
790 }
791 }
792
793 return $username;
794 }
320f4a6d 795}