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
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;
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();
65 private $settingCacheKeys = array();
66 private $settingCache = array();
67 private $allowInlineCacheGeneration;
68 private $conduitClusterToken = self
::ATTACHABLE
;
70 protected function readField($field) {
72 // Make sure these return booleans.
74 return (bool)$this->isAdmin
;
76 return (bool)$this->isDisabled
;
78 return (bool)$this->isSystemAgent
;
80 return (bool)$this->isMailingList
;
81 case 'isEmailVerified':
82 return (bool)$this->isEmailVerified
;
84 return (bool)$this->isApproved
;
86 return parent
::readField($field);
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()) {
103 if ($this->isOmnipotent()) {
107 if ($this->getIsDisabled()) {
111 if (!$this->getIsApproved()) {
115 if (PhabricatorUserEmail
::isEmailVerificationRequired()) {
116 if (!$this->getIsEmailVerified()) {
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
137 public function isResponsive() {
138 if (!$this->isUserActivated()) {
142 if (!$this->getIsEmailVerified()) {
150 public function canEstablishWebSessions() {
151 if ($this->getIsMailingList()) {
155 if ($this->getIsSystemAgent()) {
162 public function canEstablishAPISessions() {
163 if ($this->getIsDisabled()) {
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()) {
176 if (!$this->isUserActivated()) {
180 if ($this->getIsMailingList()) {
187 public function canEstablishSSHSessions() {
188 if (!$this->isUserActivated()) {
192 if ($this->getIsMailingList()) {
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
206 public function getIsStandardUser() {
207 $type_user = PhabricatorPeopleUserPHIDType
::TYPECONST
;
208 return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);
211 protected function getConfiguration() {
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',
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(
235 'columns' => array('phid'),
239 'columns' => array('userName'),
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());
296 public function attachSession(PhabricatorAuthSession
$session) {
297 $this->session
= $session;
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()) {
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;
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',
342 if (!$this->profile
) {
343 $this->profile
= PhabricatorUserProfile
::initializeNewProfile($this);
346 return $this->profile
;
349 public function loadPrimaryEmailAddress() {
350 $email = $this->loadPrimaryEmail();
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',
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);
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));
391 $value = $cache_map[$cache_key];
393 $defaults = PhabricatorSetting
::getAllSettings();
394 if (isset($defaults[$key])) {
395 $value = id(clone $defaults[$key])
397 ->getSettingDefaultValue();
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.
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;
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);
446 $preferences = PhabricatorUserPreferences
::loadGlobalPreferences($this);
447 $settings = $preferences->getPreferences();
448 $cache->setKey($cache_key, $settings);
456 * Override the user's timezone identifier.
458 * This is primarily useful for unit tests.
460 * @param string New timezone identifier.
464 public function overrideTimezoneIdentifier($identifier) {
465 $timezone_key = PhabricatorTimezoneSetting
::SETTINGKEY
;
466 $this->settingCacheKeys
[$timezone_key] = true;
467 $this->settingCache
[$timezone_key] = $identifier;
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());
488 foreach ($tokens as $token) {
498 'DELETE FROM %T WHERE userID = %d',
504 'INSERT INTO %T (userID, token) VALUES %LQ',
510 public static function describeValidUsername() {
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 '.
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
) {
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().')';
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);
580 public function getTimeZoneOffsetInHours() {
581 $offset = $this->getTimeZoneOffset();
582 $offset = (int)round($offset / 60);
588 public function formatShortDateTime($when, $now = null) {
590 $now = PhabricatorTime
::getNow();
594 $when = new DateTime('@'.$when);
595 $now = new DateTime('@'.$now);
596 } catch (Exception
$ex) {
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".
608 } else if ($when->format('Ymd') !== $now->format('Ymd')) {
609 // Same year but different month and day, so show "Feb 31".
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(
631 return id(new PhabricatorUser())->loadOneWhere(
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.
652 $spaces = msort($spaces, 'getID');
653 return head($spaces)->getPHID();
660 public function hasConduitClusterToken() {
661 return ($this->conduitClusterToken
!== self
::ATTACHABLE
);
664 public function attachConduitClusterToken(PhabricatorConduitToken
$token) {
665 $this->conduitClusterToken
= $token;
669 public function getConduitClusterToken() {
670 return $this->assertAttached($this->conduitClusterToken
);
674 /* -( Availability )------------------------------------------------------- */
680 public function attachAvailability(array $availability) {
681 $this->availability
= $availability;
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.
692 public function getAwayUntil() {
693 $availability = $this->availability
;
695 $this->assertAttached($availability);
696 if (!$availability) {
700 return idx($availability, 'until');
704 public function getDisplayAvailability() {
705 $availability = $this->availability
;
707 $this->assertAttached($availability);
708 if (!$availability) {
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) {
726 return idx($availability, 'eventPHID');
731 * Get cached availability, if present.
733 * @return wild|null Cache data, or null if no cache is available.
736 public function getAvailabilityCache() {
737 $now = PhabricatorTime
::getNow();
738 if ($this->availabilityCacheTTL
<= $now) {
743 return phutil_json_decode($this->availabilityCache
);
744 } catch (Exception
$ex) {
751 * Write to the availability cache.
753 * @param wild Availability cache data.
754 * @param int|null Cache TTL.
758 public function writeAvailabilityCache(array $availability, $ttl) {
759 if (PhabricatorEnv
::isReadOnly()) {
763 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
765 $this->establishConnection('w'),
766 'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
768 $this->getTableName(),
769 phutil_json_encode($availability),
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.
796 public function updateMultiFactorEnrollment() {
797 $factors = id(new PhabricatorAuthFactorConfigQuery())
799 ->withUserPHIDs(array($this->getPHID()))
800 ->withFactorProviderStatuses(
802 PhabricatorAuthFactorProviderStatus
::STATUS_ACTIVE
,
803 PhabricatorAuthFactorProviderStatus
::STATUS_DEPRECATED
,
807 $enrolled = count($factors) ?
1 : 0;
808 if ($enrolled !== $this->isEnrolledInMultiFactor
) {
809 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
811 $this->establishConnection('w'),
812 'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
813 $this->getTableName(),
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.
833 public function getIsEnrolledInMultiFactor() {
834 return $this->isEnrolledInMultiFactor
;
838 /* -( Omnipotence )-------------------------------------------------------- */
842 * Returns true if this user is omnipotent. Omnipotent users bypass all policy
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() {
861 $user = new PhabricatorUser();
862 $user->omnipotent
= true;
863 $user->makeEphemeral();
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();
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.
903 public function loadHandles(array $phids) {
904 if ($this->handlePool
=== null) {
905 $this->handlePool
= id(new PhabricatorHandlePool())
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.
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.
936 public function renderHandleList(array $phids) {
937 return $this->loadHandles($phids)->renderList();
940 public function attachBadgePHIDs(array $phids) {
941 $this->badgePHIDs
= $phids;
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`.
959 return $this->newCSRFEngine()
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;
977 private function newCSRFEngine() {
978 if ($this->getPHID()) {
979 $vec = $this->getPHID().$this->getAccountSecret();
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();
995 ->setSalt($this->csrfSalt
)
996 ->setSecret(new PhutilOpaqueEnvelope($vec));
1002 /* -( PhabricatorPolicyInterface )----------------------------------------- */
1005 public function getCapabilities() {
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
;
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.');
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;
1060 /* -( PhabricatorDestructibleInterface )----------------------------------- */
1063 public function destroyObjectPermanently(
1064 PhabricatorDestructionEngine
$engine) {
1066 $viewer = $engine->getViewer();
1068 $this->openTransaction();
1071 $externals = id(new PhabricatorExternalAccountQuery())
1072 ->setViewer($viewer)
1073 ->withUserPHIDs(array($this->getPHID()))
1075 foreach ($externals as $external) {
1076 $engine->destroyObject($external);
1079 $prefs = id(new PhabricatorUserPreferencesQuery())
1080 ->setViewer($viewer)
1081 ->withUsers(array($this))
1083 foreach ($prefs as $pref) {
1084 $engine->destroyObject($pref);
1087 $profiles = id(new PhabricatorUserProfile())->loadAllWhere(
1090 foreach ($profiles as $profile) {
1094 $keys = id(new PhabricatorAuthSSHKeyQuery())
1095 ->setViewer($viewer)
1096 ->withObjectPHIDs(array($this->getPHID()))
1098 foreach ($keys as $key) {
1099 $engine->destroyObject($key);
1102 $emails = id(new PhabricatorUserEmail())->loadAllWhere(
1105 foreach ($emails as $email) {
1106 $engine->destroyObject($email);
1109 $sessions = id(new PhabricatorAuthSession())->loadAllWhere(
1112 foreach ($sessions as $session) {
1116 $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
1119 foreach ($factors as $factor) {
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
1134 return '/settings/panel/ssh/';
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() {
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() {
1185 id(new PhabricatorConduitSearchFieldSpecification())
1186 ->setKey('username')
1188 ->setDescription(pht("The user's username.")),
1189 id(new PhabricatorConduitSearchFieldSpecification())
1190 ->setKey('realName')
1192 ->setDescription(pht("The user's real name.")),
1193 id(new PhabricatorConduitSearchFieldSpecification())
1195 ->setType('list<string>')
1196 ->setDescription(pht('List of account roles.')),
1200 public function getFieldValuesForConduit() {
1203 if ($this->getIsDisabled()) {
1204 $roles[] = 'disabled';
1207 if ($this->getIsSystemAgent()) {
1211 if ($this->getIsMailingList()) {
1215 if ($this->getIsAdmin()) {
1219 if ($this->getIsEmailVerified()) {
1220 $roles[] = 'verified';
1223 if ($this->getIsApproved()) {
1224 $roles[] = 'approved';
1227 if ($this->isUserActivated()) {
1228 $roles[] = 'activated';
1232 'username' => $this->getUsername(),
1233 'realName' => $this->getRealName(),
1238 public function getConduitSearchAttachments() {
1240 id(new PhabricatorPeopleAvailabilitySearchEngineAttachment())
1241 ->setAttachmentKey('availability'),
1246 /* -( User Cache )--------------------------------------------------------- */
1252 public function attachRawCacheData(array $data) {
1253 $this->rawCacheData
= $data +
$this->rawCacheData
;
1257 public function setAllowInlineCacheGeneration($allow_cache_generation) {
1258 $this->allowInlineCacheGeneration
= $allow_cache_generation;
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.
1293 $raw_data = PhabricatorUserCache
::readCaches(
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();
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(
1323 $this->usableCacheData
[$key] = $usable_value;
1325 return $usable_value;
1332 public function clearCacheData($key) {
1333 unset($this->rawCacheData
[$key]);
1334 unset($this->usableCacheData
[$key]);
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(
1349 'Unknown CSS variable "%s"!',
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
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
1423 $this->getUsername(),
1424 $envelope->openEnvelope(),
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) {
1449 $list[] = $this->getUsername();
1450 $list[] = $this->getRealName();
1452 $emails = id(new PhabricatorUserEmail())->loadAllWhere(
1455 foreach ($emails as $email) {
1456 $list[] = $email->getAddress();