Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / people / storage / PhabricatorUser.php
blob7276253085ebe88cc6bd9f703623591fa7fa1910
1 <?php
3 /**
4 * @task availability Availability
5 * @task image-cache Profile Image Cache
6 * @task factors Multi-Factor Authentication
7 * @task handles Managing Handles
8 * @task settings Settings
9 * @task cache User Cache
11 final class PhabricatorUser
12 extends PhabricatorUserDAO
13 implements
14 PhutilPerson,
15 PhabricatorPolicyInterface,
16 PhabricatorCustomFieldInterface,
17 PhabricatorDestructibleInterface,
18 PhabricatorSSHPublicKeyInterface,
19 PhabricatorFlaggableInterface,
20 PhabricatorApplicationTransactionInterface,
21 PhabricatorFulltextInterface,
22 PhabricatorFerretInterface,
23 PhabricatorConduitResultInterface,
24 PhabricatorAuthPasswordHashInterface {
26 const SESSION_TABLE = 'phabricator_session';
27 const NAMETOKEN_TABLE = 'user_nametoken';
28 const MAXIMUM_USERNAME_LENGTH = 64;
30 protected $userName;
31 protected $realName;
32 protected $profileImagePHID;
33 protected $defaultProfileImagePHID;
34 protected $defaultProfileImageVersion;
35 protected $availabilityCache;
36 protected $availabilityCacheTTL;
38 protected $conduitCertificate;
40 protected $isSystemAgent = 0;
41 protected $isMailingList = 0;
42 protected $isAdmin = 0;
43 protected $isDisabled = 0;
44 protected $isEmailVerified = 0;
45 protected $isApproved = 0;
46 protected $isEnrolledInMultiFactor = 0;
48 protected $accountSecret;
50 private $profile = null;
51 private $availability = self::ATTACHABLE;
52 private $preferences = null;
53 private $omnipotent = false;
54 private $customFields = self::ATTACHABLE;
55 private $badgePHIDs = self::ATTACHABLE;
57 private $alternateCSRFString = self::ATTACHABLE;
58 private $session = self::ATTACHABLE;
59 private $rawCacheData = array();
60 private $usableCacheData = array();
62 private $handlePool;
63 private $csrfSalt;
65 private $settingCacheKeys = array();
66 private $settingCache = array();
67 private $allowInlineCacheGeneration;
68 private $conduitClusterToken = self::ATTACHABLE;
70 protected function readField($field) {
71 switch ($field) {
72 // Make sure these return booleans.
73 case 'isAdmin':
74 return (bool)$this->isAdmin;
75 case 'isDisabled':
76 return (bool)$this->isDisabled;
77 case 'isSystemAgent':
78 return (bool)$this->isSystemAgent;
79 case 'isMailingList':
80 return (bool)$this->isMailingList;
81 case 'isEmailVerified':
82 return (bool)$this->isEmailVerified;
83 case 'isApproved':
84 return (bool)$this->isApproved;
85 default:
86 return parent::readField($field);
91 /**
92 * Is this a live account which has passed required approvals? Returns true
93 * if this is an enabled, verified (if required), approved (if required)
94 * account, and false otherwise.
96 * @return bool True if this is a standard, usable account.
98 public function isUserActivated() {
99 if (!$this->isLoggedIn()) {
100 return false;
103 if ($this->isOmnipotent()) {
104 return true;
107 if ($this->getIsDisabled()) {
108 return false;
111 if (!$this->getIsApproved()) {
112 return false;
115 if (PhabricatorUserEmail::isEmailVerificationRequired()) {
116 if (!$this->getIsEmailVerified()) {
117 return false;
121 return true;
126 * Is this a user who we can reasonably expect to respond to requests?
128 * This is used to provide a grey "disabled/unresponsive" dot cue when
129 * rendering handles and tags, so it isn't a surprise if you get ignored
130 * when you ask things of users who will not receive notifications or could
131 * not respond to them (because they are disabled, unapproved, do not have
132 * verified email addresses, etc).
134 * @return bool True if this user can receive and respond to requests from
135 * other humans.
137 public function isResponsive() {
138 if (!$this->isUserActivated()) {
139 return false;
142 if (!$this->getIsEmailVerified()) {
143 return false;
146 return true;
150 public function canEstablishWebSessions() {
151 if ($this->getIsMailingList()) {
152 return false;
155 if ($this->getIsSystemAgent()) {
156 return false;
159 return true;
162 public function canEstablishAPISessions() {
163 if ($this->getIsDisabled()) {
164 return false;
167 // Intracluster requests are permitted even if the user is logged out:
168 // in particular, public users are allowed to issue intracluster requests
169 // when browsing Diffusion.
170 if (PhabricatorEnv::isClusterRemoteAddress()) {
171 if (!$this->isLoggedIn()) {
172 return true;
176 if (!$this->isUserActivated()) {
177 return false;
180 if ($this->getIsMailingList()) {
181 return false;
184 return true;
187 public function canEstablishSSHSessions() {
188 if (!$this->isUserActivated()) {
189 return false;
192 if ($this->getIsMailingList()) {
193 return false;
196 return true;
200 * Returns `true` if this is a standard user who is logged in. Returns `false`
201 * for logged out, anonymous, or external users.
203 * @return bool `true` if the user is a standard user who is logged in with
204 * a normal session.
206 public function getIsStandardUser() {
207 $type_user = PhabricatorPeopleUserPHIDType::TYPECONST;
208 return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
211 protected function getConfiguration() {
212 return array(
213 self::CONFIG_AUX_PHID => true,
214 self::CONFIG_COLUMN_SCHEMA => array(
215 'userName' => 'sort64',
216 'realName' => 'text128',
217 'profileImagePHID' => 'phid?',
218 'conduitCertificate' => 'text255',
219 'isSystemAgent' => 'bool',
220 'isMailingList' => 'bool',
221 'isDisabled' => 'bool',
222 'isAdmin' => 'bool',
223 'isEmailVerified' => 'uint32',
224 'isApproved' => 'uint32',
225 'accountSecret' => 'bytes64',
226 'isEnrolledInMultiFactor' => 'bool',
227 'availabilityCache' => 'text255?',
228 'availabilityCacheTTL' => 'uint32?',
229 'defaultProfileImagePHID' => 'phid?',
230 'defaultProfileImageVersion' => 'text64?',
232 self::CONFIG_KEY_SCHEMA => array(
233 'key_phid' => null,
234 'phid' => array(
235 'columns' => array('phid'),
236 'unique' => true,
238 'userName' => array(
239 'columns' => array('userName'),
240 'unique' => true,
242 'realName' => array(
243 'columns' => array('realName'),
245 'key_approved' => array(
246 'columns' => array('isApproved'),
249 self::CONFIG_NO_MUTATE => array(
250 'availabilityCache' => true,
251 'availabilityCacheTTL' => true,
253 ) + parent::getConfiguration();
256 public function generatePHID() {
257 return PhabricatorPHID::generateNewPHID(
258 PhabricatorPeopleUserPHIDType::TYPECONST);
261 public function getMonogram() {
262 return '@'.$this->getUsername();
265 public function isLoggedIn() {
266 return !($this->getPHID() === null);
269 public function saveWithoutIndex() {
270 return parent::save();
273 public function save() {
274 if (!$this->getConduitCertificate()) {
275 $this->setConduitCertificate($this->generateConduitCertificate());
278 $secret = $this->getAccountSecret();
279 if (($secret === null) || !strlen($secret)) {
280 $this->setAccountSecret(Filesystem::readRandomCharacters(64));
283 $result = $this->saveWithoutIndex();
285 if ($this->profile) {
286 $this->profile->save();
289 $this->updateNameTokens();
291 PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());
293 return $result;
296 public function attachSession(PhabricatorAuthSession $session) {
297 $this->session = $session;
298 return $this;
301 public function getSession() {
302 return $this->assertAttached($this->session);
305 public function hasSession() {
306 return ($this->session !== self::ATTACHABLE);
309 public function hasHighSecuritySession() {
310 if (!$this->hasSession()) {
311 return false;
314 return $this->getSession()->isHighSecuritySession();
317 private function generateConduitCertificate() {
318 return Filesystem::readRandomCharacters(255);
321 const EMAIL_CYCLE_FREQUENCY = 86400;
322 const EMAIL_TOKEN_LENGTH = 24;
324 public function getUserProfile() {
325 return $this->assertAttached($this->profile);
328 public function attachUserProfile(PhabricatorUserProfile $profile) {
329 $this->profile = $profile;
330 return $this;
333 public function loadUserProfile() {
334 if ($this->profile) {
335 return $this->profile;
338 $profile_dao = new PhabricatorUserProfile();
339 $this->profile = $profile_dao->loadOneWhere('userPHID = %s',
340 $this->getPHID());
342 if (!$this->profile) {
343 $this->profile = PhabricatorUserProfile::initializeNewProfile($this);
346 return $this->profile;
349 public function loadPrimaryEmailAddress() {
350 $email = $this->loadPrimaryEmail();
351 if (!$email) {
352 throw new Exception(pht('User has no primary email address!'));
354 return $email->getAddress();
357 public function loadPrimaryEmail() {
358 return id(new PhabricatorUserEmail())->loadOneWhere(
359 'userPHID = %s AND isPrimary = 1',
360 $this->getPHID());
364 /* -( Settings )----------------------------------------------------------- */
367 public function getUserSetting($key) {
368 // NOTE: We store available keys and cached values separately to make it
369 // faster to check for `null` in the cache, which is common.
370 if (isset($this->settingCacheKeys[$key])) {
371 return $this->settingCache[$key];
374 $settings_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
375 if ($this->getPHID()) {
376 $settings = $this->requireCacheData($settings_key);
377 } else {
378 $settings = $this->loadGlobalSettings();
381 if (array_key_exists($key, $settings)) {
382 $value = $settings[$key];
383 return $this->writeUserSettingCache($key, $value);
386 $cache = PhabricatorCaches::getRuntimeCache();
387 $cache_key = "settings.defaults({$key})";
388 $cache_map = $cache->getKeys(array($cache_key));
390 if ($cache_map) {
391 $value = $cache_map[$cache_key];
392 } else {
393 $defaults = PhabricatorSetting::getAllSettings();
394 if (isset($defaults[$key])) {
395 $value = id(clone $defaults[$key])
396 ->setViewer($this)
397 ->getSettingDefaultValue();
398 } else {
399 $value = null;
402 $cache->setKey($cache_key, $value);
405 return $this->writeUserSettingCache($key, $value);
410 * Test if a given setting is set to a particular value.
412 * @param const Setting key.
413 * @param wild Value to compare.
414 * @return bool True if the setting has the specified value.
415 * @task settings
417 public function compareUserSetting($key, $value) {
418 $actual = $this->getUserSetting($key);
419 return ($actual == $value);
422 private function writeUserSettingCache($key, $value) {
423 $this->settingCacheKeys[$key] = true;
424 $this->settingCache[$key] = $value;
425 return $value;
428 public function getTranslation() {
429 return $this->getUserSetting(PhabricatorTranslationSetting::SETTINGKEY);
432 public function getTimezoneIdentifier() {
433 return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY);
436 public static function getGlobalSettingsCacheKey() {
437 return 'user.settings.globals.v1';
440 private function loadGlobalSettings() {
441 $cache_key = self::getGlobalSettingsCacheKey();
442 $cache = PhabricatorCaches::getMutableStructureCache();
444 $settings = $cache->getKey($cache_key);
445 if (!$settings) {
446 $preferences = PhabricatorUserPreferences::loadGlobalPreferences($this);
447 $settings = $preferences->getPreferences();
448 $cache->setKey($cache_key, $settings);
451 return $settings;
456 * Override the user's timezone identifier.
458 * This is primarily useful for unit tests.
460 * @param string New timezone identifier.
461 * @return this
462 * @task settings
464 public function overrideTimezoneIdentifier($identifier) {
465 $timezone_key = PhabricatorTimezoneSetting::SETTINGKEY;
466 $this->settingCacheKeys[$timezone_key] = true;
467 $this->settingCache[$timezone_key] = $identifier;
468 return $this;
471 public function getGender() {
472 return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY);
476 * Populate the nametoken table, which used to fetch typeahead results. When
477 * a user types "linc", we want to match "Abraham Lincoln" from on-demand
478 * typeahead sources. To do this, we need a separate table of name fragments.
480 public function updateNameTokens() {
481 $table = self::NAMETOKEN_TABLE;
482 $conn_w = $this->establishConnection('w');
484 $tokens = PhabricatorTypeaheadDatasource::tokenizeString(
485 $this->getUserName().' '.$this->getRealName());
487 $sql = array();
488 foreach ($tokens as $token) {
489 $sql[] = qsprintf(
490 $conn_w,
491 '(%d, %s)',
492 $this->getID(),
493 $token);
496 queryfx(
497 $conn_w,
498 'DELETE FROM %T WHERE userID = %d',
499 $table,
500 $this->getID());
501 if ($sql) {
502 queryfx(
503 $conn_w,
504 'INSERT INTO %T (userID, token) VALUES %LQ',
505 $table,
506 $sql);
510 public static function describeValidUsername() {
511 return pht(
512 'Usernames must contain only numbers, letters, period, underscore, and '.
513 'hyphen, and can not end with a period. They must have no more than %d '.
514 'characters.',
515 new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));
518 public static function validateUsername($username) {
519 // NOTE: If you update this, make sure to update:
521 // - Remarkup rule for @mentions.
522 // - Routing rule for "/p/username/".
523 // - Unit tests, obviously.
524 // - describeValidUsername() method, above.
526 if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {
527 return false;
530 return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
533 public static function getDefaultProfileImageURI() {
534 return celerity_get_resource_uri('/rsrc/image/avatar.png');
537 public function getProfileImageURI() {
538 $uri_key = PhabricatorUserProfileImageCacheType::KEY_URI;
539 return $this->requireCacheData($uri_key);
542 public function getUnreadNotificationCount() {
543 $notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT;
544 return $this->requireCacheData($notification_key);
547 public function getUnreadMessageCount() {
548 $message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT;
549 return $this->requireCacheData($message_key);
552 public function getRecentBadgeAwards() {
553 $badges_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
554 return $this->requireCacheData($badges_key);
557 public function getFullName() {
558 if (strlen($this->getRealName())) {
559 return $this->getUsername().' ('.$this->getRealName().')';
560 } else {
561 return $this->getUsername();
565 public function getTimeZone() {
566 return new DateTimeZone($this->getTimezoneIdentifier());
569 public function getTimeZoneOffset() {
570 $timezone = $this->getTimeZone();
571 $now = new DateTime('@'.PhabricatorTime::getNow());
572 $offset = $timezone->getOffset($now);
574 // Javascript offsets are in minutes and have the opposite sign.
575 $offset = -(int)($offset / 60);
577 return $offset;
580 public function getTimeZoneOffsetInHours() {
581 $offset = $this->getTimeZoneOffset();
582 $offset = (int)round($offset / 60);
583 $offset = -$offset;
585 return $offset;
588 public function formatShortDateTime($when, $now = null) {
589 if ($now === null) {
590 $now = PhabricatorTime::getNow();
593 try {
594 $when = new DateTime('@'.$when);
595 $now = new DateTime('@'.$now);
596 } catch (Exception $ex) {
597 return null;
600 $zone = $this->getTimeZone();
602 $when->setTimeZone($zone);
603 $now->setTimeZone($zone);
605 if ($when->format('Y') !== $now->format('Y')) {
606 // Different year, so show "Feb 31 2075".
607 $format = 'M j Y';
608 } else if ($when->format('Ymd') !== $now->format('Ymd')) {
609 // Same year but different month and day, so show "Feb 31".
610 $format = 'M j';
611 } else {
612 // Same year, month and day so show a time of day.
613 $pref_time = PhabricatorTimeFormatSetting::SETTINGKEY;
614 $format = $this->getUserSetting($pref_time);
617 return $when->format($format);
620 public function __toString() {
621 return $this->getUsername();
624 public static function loadOneWithEmailAddress($address) {
625 $email = id(new PhabricatorUserEmail())->loadOneWhere(
626 'address = %s',
627 $address);
628 if (!$email) {
629 return null;
631 return id(new PhabricatorUser())->loadOneWhere(
632 'phid = %s',
633 $email->getUserPHID());
636 public function getDefaultSpacePHID() {
637 // TODO: We might let the user switch which space they're "in" later on;
638 // for now just use the global space if one exists.
640 // If the viewer has access to the default space, use that.
641 $spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this);
642 foreach ($spaces as $space) {
643 if ($space->getIsDefaultNamespace()) {
644 return $space->getPHID();
648 // Otherwise, use the space with the lowest ID that they have access to.
649 // This just tends to keep the default stable and predictable over time,
650 // so adding a new space won't change behavior for users.
651 if ($spaces) {
652 $spaces = msort($spaces, 'getID');
653 return head($spaces)->getPHID();
656 return null;
660 public function hasConduitClusterToken() {
661 return ($this->conduitClusterToken !== self::ATTACHABLE);
664 public function attachConduitClusterToken(PhabricatorConduitToken $token) {
665 $this->conduitClusterToken = $token;
666 return $this;
669 public function getConduitClusterToken() {
670 return $this->assertAttached($this->conduitClusterToken);
674 /* -( Availability )------------------------------------------------------- */
678 * @task availability
680 public function attachAvailability(array $availability) {
681 $this->availability = $availability;
682 return $this;
687 * Get the timestamp the user is away until, if they are currently away.
689 * @return int|null Epoch timestamp, or `null` if the user is not away.
690 * @task availability
692 public function getAwayUntil() {
693 $availability = $this->availability;
695 $this->assertAttached($availability);
696 if (!$availability) {
697 return null;
700 return idx($availability, 'until');
704 public function getDisplayAvailability() {
705 $availability = $this->availability;
707 $this->assertAttached($availability);
708 if (!$availability) {
709 return null;
712 $busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY;
714 return idx($availability, 'availability', $busy);
718 public function getAvailabilityEventPHID() {
719 $availability = $this->availability;
721 $this->assertAttached($availability);
722 if (!$availability) {
723 return null;
726 return idx($availability, 'eventPHID');
731 * Get cached availability, if present.
733 * @return wild|null Cache data, or null if no cache is available.
734 * @task availability
736 public function getAvailabilityCache() {
737 $now = PhabricatorTime::getNow();
738 if ($this->availabilityCacheTTL <= $now) {
739 return null;
742 try {
743 return phutil_json_decode($this->availabilityCache);
744 } catch (Exception $ex) {
745 return null;
751 * Write to the availability cache.
753 * @param wild Availability cache data.
754 * @param int|null Cache TTL.
755 * @return this
756 * @task availability
758 public function writeAvailabilityCache(array $availability, $ttl) {
759 if (PhabricatorEnv::isReadOnly()) {
760 return $this;
763 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
764 queryfx(
765 $this->establishConnection('w'),
766 'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
767 WHERE id = %d',
768 $this->getTableName(),
769 phutil_json_encode($availability),
770 $ttl,
771 $this->getID());
772 unset($unguarded);
774 return $this;
778 /* -( Multi-Factor Authentication )---------------------------------------- */
782 * Update the flag storing this user's enrollment in multi-factor auth.
784 * With certain settings, we need to check if a user has MFA on every page,
785 * so we cache MFA enrollment on the user object for performance. Calling this
786 * method synchronizes the cache by examining enrollment records. After
787 * updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
788 * the user is enrolled.
790 * This method should be called after any changes are made to a given user's
791 * multi-factor configuration.
793 * @return void
794 * @task factors
796 public function updateMultiFactorEnrollment() {
797 $factors = id(new PhabricatorAuthFactorConfigQuery())
798 ->setViewer($this)
799 ->withUserPHIDs(array($this->getPHID()))
800 ->withFactorProviderStatuses(
801 array(
802 PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
803 PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
805 ->execute();
807 $enrolled = count($factors) ? 1 : 0;
808 if ($enrolled !== $this->isEnrolledInMultiFactor) {
809 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
810 queryfx(
811 $this->establishConnection('w'),
812 'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
813 $this->getTableName(),
814 $enrolled,
815 $this->getID());
816 unset($unguarded);
818 $this->isEnrolledInMultiFactor = $enrolled;
824 * Check if the user is enrolled in multi-factor authentication.
826 * Enrolled users have one or more multi-factor authentication sources
827 * attached to their account. For performance, this value is cached. You
828 * can use @{method:updateMultiFactorEnrollment} to update the cache.
830 * @return bool True if the user is enrolled.
831 * @task factors
833 public function getIsEnrolledInMultiFactor() {
834 return $this->isEnrolledInMultiFactor;
838 /* -( Omnipotence )-------------------------------------------------------- */
842 * Returns true if this user is omnipotent. Omnipotent users bypass all policy
843 * checks.
845 * @return bool True if the user bypasses policy checks.
847 public function isOmnipotent() {
848 return $this->omnipotent;
853 * Get an omnipotent user object for use in contexts where there is no acting
854 * user, notably daemons.
856 * @return PhabricatorUser An omnipotent user.
858 public static function getOmnipotentUser() {
859 static $user = null;
860 if (!$user) {
861 $user = new PhabricatorUser();
862 $user->omnipotent = true;
863 $user->makeEphemeral();
865 return $user;
870 * Get a scalar string identifying this user.
872 * This is similar to using the PHID, but distinguishes between omnipotent
873 * and public users explicitly. This allows safe construction of cache keys
874 * or cache buckets which do not conflate public and omnipotent users.
876 * @return string Scalar identifier.
878 public function getCacheFragment() {
879 if ($this->isOmnipotent()) {
880 return 'u.omnipotent';
883 $phid = $this->getPHID();
884 if ($phid) {
885 return 'u.'.$phid;
888 return 'u.public';
892 /* -( Managing Handles )--------------------------------------------------- */
896 * Get a @{class:PhabricatorHandleList} which benefits from this viewer's
897 * internal handle pool.
899 * @param list<phid> List of PHIDs to load.
900 * @return PhabricatorHandleList Handle list object.
901 * @task handle
903 public function loadHandles(array $phids) {
904 if ($this->handlePool === null) {
905 $this->handlePool = id(new PhabricatorHandlePool())
906 ->setViewer($this);
909 return $this->handlePool->newHandleList($phids);
914 * Get a @{class:PHUIHandleView} for a single handle.
916 * This benefits from the viewer's internal handle pool.
918 * @param phid PHID to render a handle for.
919 * @return PHUIHandleView View of the handle.
920 * @task handle
922 public function renderHandle($phid) {
923 return $this->loadHandles(array($phid))->renderHandle($phid);
928 * Get a @{class:PHUIHandleListView} for a list of handles.
930 * This benefits from the viewer's internal handle pool.
932 * @param list<phid> List of PHIDs to render.
933 * @return PHUIHandleListView View of the handles.
934 * @task handle
936 public function renderHandleList(array $phids) {
937 return $this->loadHandles($phids)->renderList();
940 public function attachBadgePHIDs(array $phids) {
941 $this->badgePHIDs = $phids;
942 return $this;
945 public function getBadgePHIDs() {
946 return $this->assertAttached($this->badgePHIDs);
949 /* -( CSRF )--------------------------------------------------------------- */
952 public function getCSRFToken() {
953 if ($this->isOmnipotent()) {
954 // We may end up here when called from the daemons. The omnipotent user
955 // has no meaningful CSRF token, so just return `null`.
956 return null;
959 return $this->newCSRFEngine()
960 ->newToken();
963 public function validateCSRFToken($token) {
964 return $this->newCSRFengine()
965 ->isValidToken($token);
968 public function getAlternateCSRFString() {
969 return $this->assertAttached($this->alternateCSRFString);
972 public function attachAlternateCSRFString($string) {
973 $this->alternateCSRFString = $string;
974 return $this;
977 private function newCSRFEngine() {
978 if ($this->getPHID()) {
979 $vec = $this->getPHID().$this->getAccountSecret();
980 } else {
981 $vec = $this->getAlternateCSRFString();
984 if ($this->hasSession()) {
985 $vec = $vec.$this->getSession()->getSessionKey();
988 $engine = new PhabricatorAuthCSRFEngine();
990 if ($this->csrfSalt === null) {
991 $this->csrfSalt = $engine->newSalt();
994 $engine
995 ->setSalt($this->csrfSalt)
996 ->setSecret(new PhutilOpaqueEnvelope($vec));
998 return $engine;
1002 /* -( PhabricatorPolicyInterface )----------------------------------------- */
1005 public function getCapabilities() {
1006 return array(
1007 PhabricatorPolicyCapability::CAN_VIEW,
1008 PhabricatorPolicyCapability::CAN_EDIT,
1012 public function getPolicy($capability) {
1013 switch ($capability) {
1014 case PhabricatorPolicyCapability::CAN_VIEW:
1015 return PhabricatorPolicies::POLICY_PUBLIC;
1016 case PhabricatorPolicyCapability::CAN_EDIT:
1017 if ($this->getIsSystemAgent() || $this->getIsMailingList()) {
1018 return PhabricatorPolicies::POLICY_ADMIN;
1019 } else {
1020 return PhabricatorPolicies::POLICY_NOONE;
1025 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
1026 return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
1029 public function describeAutomaticCapability($capability) {
1030 switch ($capability) {
1031 case PhabricatorPolicyCapability::CAN_EDIT:
1032 return pht('Only you can edit your information.');
1033 default:
1034 return null;
1039 /* -( PhabricatorCustomFieldInterface )------------------------------------ */
1042 public function getCustomFieldSpecificationForRole($role) {
1043 return PhabricatorEnv::getEnvConfig('user.fields');
1046 public function getCustomFieldBaseClass() {
1047 return 'PhabricatorUserCustomField';
1050 public function getCustomFields() {
1051 return $this->assertAttached($this->customFields);
1054 public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {
1055 $this->customFields = $fields;
1056 return $this;
1060 /* -( PhabricatorDestructibleInterface )----------------------------------- */
1063 public function destroyObjectPermanently(
1064 PhabricatorDestructionEngine $engine) {
1066 $viewer = $engine->getViewer();
1068 $this->openTransaction();
1069 $this->delete();
1071 $externals = id(new PhabricatorExternalAccountQuery())
1072 ->setViewer($viewer)
1073 ->withUserPHIDs(array($this->getPHID()))
1074 ->newIterator();
1075 foreach ($externals as $external) {
1076 $engine->destroyObject($external);
1079 $prefs = id(new PhabricatorUserPreferencesQuery())
1080 ->setViewer($viewer)
1081 ->withUsers(array($this))
1082 ->execute();
1083 foreach ($prefs as $pref) {
1084 $engine->destroyObject($pref);
1087 $profiles = id(new PhabricatorUserProfile())->loadAllWhere(
1088 'userPHID = %s',
1089 $this->getPHID());
1090 foreach ($profiles as $profile) {
1091 $profile->delete();
1094 $keys = id(new PhabricatorAuthSSHKeyQuery())
1095 ->setViewer($viewer)
1096 ->withObjectPHIDs(array($this->getPHID()))
1097 ->execute();
1098 foreach ($keys as $key) {
1099 $engine->destroyObject($key);
1102 $emails = id(new PhabricatorUserEmail())->loadAllWhere(
1103 'userPHID = %s',
1104 $this->getPHID());
1105 foreach ($emails as $email) {
1106 $engine->destroyObject($email);
1109 $sessions = id(new PhabricatorAuthSession())->loadAllWhere(
1110 'userPHID = %s',
1111 $this->getPHID());
1112 foreach ($sessions as $session) {
1113 $session->delete();
1116 $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
1117 'userPHID = %s',
1118 $this->getPHID());
1119 foreach ($factors as $factor) {
1120 $factor->delete();
1123 $this->saveTransaction();
1127 /* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
1130 public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {
1131 if ($viewer->getPHID() == $this->getPHID()) {
1132 // If the viewer is managing their own keys, take them to the normal
1133 // panel.
1134 return '/settings/panel/ssh/';
1135 } else {
1136 // Otherwise, take them to the administrative panel for this user.
1137 return '/settings/user/'.$this->getUsername().'/page/ssh/';
1141 public function getSSHKeyDefaultName() {
1142 return 'id_rsa_phabricator';
1145 public function getSSHKeyNotifyPHIDs() {
1146 return array(
1147 $this->getPHID(),
1152 /* -( PhabricatorApplicationTransactionInterface )------------------------- */
1155 public function getApplicationTransactionEditor() {
1156 return new PhabricatorUserTransactionEditor();
1159 public function getApplicationTransactionTemplate() {
1160 return new PhabricatorUserTransaction();
1164 /* -( PhabricatorFulltextInterface )--------------------------------------- */
1167 public function newFulltextEngine() {
1168 return new PhabricatorUserFulltextEngine();
1172 /* -( PhabricatorFerretInterface )----------------------------------------- */
1175 public function newFerretEngine() {
1176 return new PhabricatorUserFerretEngine();
1180 /* -( PhabricatorConduitResultInterface )---------------------------------- */
1183 public function getFieldSpecificationsForConduit() {
1184 return array(
1185 id(new PhabricatorConduitSearchFieldSpecification())
1186 ->setKey('username')
1187 ->setType('string')
1188 ->setDescription(pht("The user's username.")),
1189 id(new PhabricatorConduitSearchFieldSpecification())
1190 ->setKey('realName')
1191 ->setType('string')
1192 ->setDescription(pht("The user's real name.")),
1193 id(new PhabricatorConduitSearchFieldSpecification())
1194 ->setKey('roles')
1195 ->setType('list<string>')
1196 ->setDescription(pht('List of account roles.')),
1200 public function getFieldValuesForConduit() {
1201 $roles = array();
1203 if ($this->getIsDisabled()) {
1204 $roles[] = 'disabled';
1207 if ($this->getIsSystemAgent()) {
1208 $roles[] = 'bot';
1211 if ($this->getIsMailingList()) {
1212 $roles[] = 'list';
1215 if ($this->getIsAdmin()) {
1216 $roles[] = 'admin';
1219 if ($this->getIsEmailVerified()) {
1220 $roles[] = 'verified';
1223 if ($this->getIsApproved()) {
1224 $roles[] = 'approved';
1227 if ($this->isUserActivated()) {
1228 $roles[] = 'activated';
1231 return array(
1232 'username' => $this->getUsername(),
1233 'realName' => $this->getRealName(),
1234 'roles' => $roles,
1238 public function getConduitSearchAttachments() {
1239 return array(
1240 id(new PhabricatorPeopleAvailabilitySearchEngineAttachment())
1241 ->setAttachmentKey('availability'),
1246 /* -( User Cache )--------------------------------------------------------- */
1250 * @task cache
1252 public function attachRawCacheData(array $data) {
1253 $this->rawCacheData = $data + $this->rawCacheData;
1254 return $this;
1257 public function setAllowInlineCacheGeneration($allow_cache_generation) {
1258 $this->allowInlineCacheGeneration = $allow_cache_generation;
1259 return $this;
1263 * @task cache
1265 protected function requireCacheData($key) {
1266 if (isset($this->usableCacheData[$key])) {
1267 return $this->usableCacheData[$key];
1270 $type = PhabricatorUserCacheType::requireCacheTypeForKey($key);
1272 if (isset($this->rawCacheData[$key])) {
1273 $raw_value = $this->rawCacheData[$key];
1275 $usable_value = $type->getValueFromStorage($raw_value);
1276 $this->usableCacheData[$key] = $usable_value;
1278 return $usable_value;
1281 // By default, we throw if a cache isn't available. This is consistent
1282 // with the standard `needX()` + `attachX()` + `getX()` interaction.
1283 if (!$this->allowInlineCacheGeneration) {
1284 throw new PhabricatorDataNotAttachedException($this);
1287 $user_phid = $this->getPHID();
1289 // Try to read the actual cache before we generate a new value. We can
1290 // end up here via Conduit, which does not use normal sessions and can
1291 // not pick up a free cache load during session identification.
1292 if ($user_phid) {
1293 $raw_data = PhabricatorUserCache::readCaches(
1294 $type,
1295 $key,
1296 array($user_phid));
1297 if (array_key_exists($user_phid, $raw_data)) {
1298 $raw_value = $raw_data[$user_phid];
1299 $usable_value = $type->getValueFromStorage($raw_value);
1300 $this->rawCacheData[$key] = $raw_value;
1301 $this->usableCacheData[$key] = $usable_value;
1302 return $usable_value;
1306 $usable_value = $type->getDefaultValue();
1308 if ($user_phid) {
1309 $map = $type->newValueForUsers($key, array($this));
1310 if (array_key_exists($user_phid, $map)) {
1311 $raw_value = $map[$user_phid];
1312 $usable_value = $type->getValueFromStorage($raw_value);
1314 $this->rawCacheData[$key] = $raw_value;
1315 PhabricatorUserCache::writeCache(
1316 $type,
1317 $key,
1318 $user_phid,
1319 $raw_value);
1323 $this->usableCacheData[$key] = $usable_value;
1325 return $usable_value;
1330 * @task cache
1332 public function clearCacheData($key) {
1333 unset($this->rawCacheData[$key]);
1334 unset($this->usableCacheData[$key]);
1335 return $this;
1339 public function getCSSValue($variable_key) {
1340 $preference = PhabricatorAccessibilitySetting::SETTINGKEY;
1341 $key = $this->getUserSetting($preference);
1343 $postprocessor = CelerityPostprocessor::getPostprocessor($key);
1344 $variables = $postprocessor->getVariables();
1346 if (!isset($variables[$variable_key])) {
1347 throw new Exception(
1348 pht(
1349 'Unknown CSS variable "%s"!',
1350 $variable_key));
1353 return $variables[$variable_key];
1356 /* -( PhabricatorAuthPasswordHashInterface )------------------------------- */
1359 public function newPasswordDigest(
1360 PhutilOpaqueEnvelope $envelope,
1361 PhabricatorAuthPassword $password) {
1363 // Before passwords are hashed, they are digested. The goal of digestion
1364 // is twofold: to reduce the length of very long passwords to something
1365 // reasonable; and to salt the password in case the best available hasher
1366 // does not include salt automatically.
1368 // Users may choose arbitrarily long passwords, and attackers may try to
1369 // attack the system by probing it with very long passwords. When large
1370 // inputs are passed to hashers -- which are intentionally slow -- it
1371 // can result in unacceptably long runtimes. The classic attack here is
1372 // to try to log in with a 64MB password and see if that locks up the
1373 // machine for the next century. By digesting passwords to a standard
1374 // length first, the length of the raw input does not impact the runtime
1375 // of the hashing algorithm.
1377 // Some hashers like bcrypt are self-salting, while other hashers are not.
1378 // Applying salt while digesting passwords ensures that hashes are salted
1379 // whether we ultimately select a self-salting hasher or not.
1381 // For legacy compatibility reasons, old VCS and Account password digest
1382 // algorithms are significantly more complicated than necessary to achieve
1383 // these goals. This is because they once used a different hashing and
1384 // salting process. When we upgraded to the modern modular hasher
1385 // infrastructure, we just bolted it onto the end of the existing pipelines
1386 // so that upgrading didn't break all users' credentials.
1388 // New implementations can (and, generally, should) safely select the
1389 // simple HMAC SHA256 digest at the bottom of the function, which does
1390 // everything that a digest callback should without any needless legacy
1391 // baggage on top.
1393 if ($password->getLegacyDigestFormat() == 'v1') {
1394 switch ($password->getPasswordType()) {
1395 case PhabricatorAuthPassword::PASSWORD_TYPE_VCS:
1396 // Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm.
1397 // They originally used this as a hasher, but it became a digest
1398 // algorithm once hashing was upgraded to include bcrypt.
1399 $digest = $envelope->openEnvelope();
1400 $salt = $this->getPHID();
1401 for ($ii = 0; $ii < 1000; $ii++) {
1402 $digest = PhabricatorHash::weakDigest($digest, $salt);
1404 return new PhutilOpaqueEnvelope($digest);
1405 case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT:
1406 // Account passwords previously used this weird mess of salt and did
1407 // not digest the input to a standard length.
1409 // Beyond this being a weird special case, there are two actual
1410 // problems with this, although neither are particularly severe:
1412 // First, because we do not normalize the length of passwords, this
1413 // algorithm may make us vulnerable to DOS attacks where an attacker
1414 // attempts to use a very long input to slow down hashers.
1416 // Second, because the username is part of the hash algorithm,
1417 // renaming a user breaks their password. This isn't a huge deal but
1418 // it's pretty silly. There's no security justification for this
1419 // behavior, I just didn't think about the implication when I wrote
1420 // it originally.
1422 $parts = array(
1423 $this->getUsername(),
1424 $envelope->openEnvelope(),
1425 $this->getPHID(),
1426 $password->getPasswordSalt(),
1429 return new PhutilOpaqueEnvelope(implode('', $parts));
1433 // For passwords which do not have some crazy legacy reason to use some
1434 // other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies
1435 // the digest requirements and is simple.
1437 $digest = PhabricatorHash::digestHMACSHA256(
1438 $envelope->openEnvelope(),
1439 $password->getPasswordSalt());
1441 return new PhutilOpaqueEnvelope($digest);
1444 public function newPasswordBlocklist(
1445 PhabricatorUser $viewer,
1446 PhabricatorAuthPasswordEngine $engine) {
1448 $list = array();
1449 $list[] = $this->getUsername();
1450 $list[] = $this->getRealName();
1452 $emails = id(new PhabricatorUserEmail())->loadAllWhere(
1453 'userPHID = %s',
1454 $this->getPHID());
1455 foreach ($emails as $email) {
1456 $list[] = $email->getAddress();
1459 return $list;