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 if (!strlen($this->getAccountSecret())) {
279 $this->setAccountSecret(Filesystem
::readRandomCharacters(64));
282 $result = $this->saveWithoutIndex();
284 if ($this->profile
) {
285 $this->profile
->save();
288 $this->updateNameTokens();
290 PhabricatorSearchWorker
::queueDocumentForIndexing($this->getPHID());
295 public function attachSession(PhabricatorAuthSession
$session) {
296 $this->session
= $session;
300 public function getSession() {
301 return $this->assertAttached($this->session
);
304 public function hasSession() {
305 return ($this->session
!== self
::ATTACHABLE
);
308 public function hasHighSecuritySession() {
309 if (!$this->hasSession()) {
313 return $this->getSession()->isHighSecuritySession();
316 private function generateConduitCertificate() {
317 return Filesystem
::readRandomCharacters(255);
320 const EMAIL_CYCLE_FREQUENCY
= 86400;
321 const EMAIL_TOKEN_LENGTH
= 24;
323 public function getUserProfile() {
324 return $this->assertAttached($this->profile
);
327 public function attachUserProfile(PhabricatorUserProfile
$profile) {
328 $this->profile
= $profile;
332 public function loadUserProfile() {
333 if ($this->profile
) {
334 return $this->profile
;
337 $profile_dao = new PhabricatorUserProfile();
338 $this->profile
= $profile_dao->loadOneWhere('userPHID = %s',
341 if (!$this->profile
) {
342 $this->profile
= PhabricatorUserProfile
::initializeNewProfile($this);
345 return $this->profile
;
348 public function loadPrimaryEmailAddress() {
349 $email = $this->loadPrimaryEmail();
351 throw new Exception(pht('User has no primary email address!'));
353 return $email->getAddress();
356 public function loadPrimaryEmail() {
357 return id(new PhabricatorUserEmail())->loadOneWhere(
358 'userPHID = %s AND isPrimary = 1',
363 /* -( Settings )----------------------------------------------------------- */
366 public function getUserSetting($key) {
367 // NOTE: We store available keys and cached values separately to make it
368 // faster to check for `null` in the cache, which is common.
369 if (isset($this->settingCacheKeys
[$key])) {
370 return $this->settingCache
[$key];
373 $settings_key = PhabricatorUserPreferencesCacheType
::KEY_PREFERENCES
;
374 if ($this->getPHID()) {
375 $settings = $this->requireCacheData($settings_key);
377 $settings = $this->loadGlobalSettings();
380 if (array_key_exists($key, $settings)) {
381 $value = $settings[$key];
382 return $this->writeUserSettingCache($key, $value);
385 $cache = PhabricatorCaches
::getRuntimeCache();
386 $cache_key = "settings.defaults({$key})";
387 $cache_map = $cache->getKeys(array($cache_key));
390 $value = $cache_map[$cache_key];
392 $defaults = PhabricatorSetting
::getAllSettings();
393 if (isset($defaults[$key])) {
394 $value = id(clone $defaults[$key])
396 ->getSettingDefaultValue();
401 $cache->setKey($cache_key, $value);
404 return $this->writeUserSettingCache($key, $value);
409 * Test if a given setting is set to a particular value.
411 * @param const Setting key.
412 * @param wild Value to compare.
413 * @return bool True if the setting has the specified value.
416 public function compareUserSetting($key, $value) {
417 $actual = $this->getUserSetting($key);
418 return ($actual == $value);
421 private function writeUserSettingCache($key, $value) {
422 $this->settingCacheKeys
[$key] = true;
423 $this->settingCache
[$key] = $value;
427 public function getTranslation() {
428 return $this->getUserSetting(PhabricatorTranslationSetting
::SETTINGKEY
);
431 public function getTimezoneIdentifier() {
432 return $this->getUserSetting(PhabricatorTimezoneSetting
::SETTINGKEY
);
435 public static function getGlobalSettingsCacheKey() {
436 return 'user.settings.globals.v1';
439 private function loadGlobalSettings() {
440 $cache_key = self
::getGlobalSettingsCacheKey();
441 $cache = PhabricatorCaches
::getMutableStructureCache();
443 $settings = $cache->getKey($cache_key);
445 $preferences = PhabricatorUserPreferences
::loadGlobalPreferences($this);
446 $settings = $preferences->getPreferences();
447 $cache->setKey($cache_key, $settings);
455 * Override the user's timezone identifier.
457 * This is primarily useful for unit tests.
459 * @param string New timezone identifier.
463 public function overrideTimezoneIdentifier($identifier) {
464 $timezone_key = PhabricatorTimezoneSetting
::SETTINGKEY
;
465 $this->settingCacheKeys
[$timezone_key] = true;
466 $this->settingCache
[$timezone_key] = $identifier;
470 public function getGender() {
471 return $this->getUserSetting(PhabricatorPronounSetting
::SETTINGKEY
);
475 * Populate the nametoken table, which used to fetch typeahead results. When
476 * a user types "linc", we want to match "Abraham Lincoln" from on-demand
477 * typeahead sources. To do this, we need a separate table of name fragments.
479 public function updateNameTokens() {
480 $table = self
::NAMETOKEN_TABLE
;
481 $conn_w = $this->establishConnection('w');
483 $tokens = PhabricatorTypeaheadDatasource
::tokenizeString(
484 $this->getUserName().' '.$this->getRealName());
487 foreach ($tokens as $token) {
497 'DELETE FROM %T WHERE userID = %d',
503 'INSERT INTO %T (userID, token) VALUES %LQ',
509 public static function describeValidUsername() {
511 'Usernames must contain only numbers, letters, period, underscore, and '.
512 'hyphen, and can not end with a period. They must have no more than %d '.
514 new PhutilNumber(self
::MAXIMUM_USERNAME_LENGTH
));
517 public static function validateUsername($username) {
518 // NOTE: If you update this, make sure to update:
520 // - Remarkup rule for @mentions.
521 // - Routing rule for "/p/username/".
522 // - Unit tests, obviously.
523 // - describeValidUsername() method, above.
525 if (strlen($username) > self
::MAXIMUM_USERNAME_LENGTH
) {
529 return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);
532 public static function getDefaultProfileImageURI() {
533 return celerity_get_resource_uri('/rsrc/image/avatar.png');
536 public function getProfileImageURI() {
537 $uri_key = PhabricatorUserProfileImageCacheType
::KEY_URI
;
538 return $this->requireCacheData($uri_key);
541 public function getUnreadNotificationCount() {
542 $notification_key = PhabricatorUserNotificationCountCacheType
::KEY_COUNT
;
543 return $this->requireCacheData($notification_key);
546 public function getUnreadMessageCount() {
547 $message_key = PhabricatorUserMessageCountCacheType
::KEY_COUNT
;
548 return $this->requireCacheData($message_key);
551 public function getRecentBadgeAwards() {
552 $badges_key = PhabricatorUserBadgesCacheType
::KEY_BADGES
;
553 return $this->requireCacheData($badges_key);
556 public function getFullName() {
557 if (strlen($this->getRealName())) {
558 return $this->getUsername().' ('.$this->getRealName().')';
560 return $this->getUsername();
564 public function getTimeZone() {
565 return new DateTimeZone($this->getTimezoneIdentifier());
568 public function getTimeZoneOffset() {
569 $timezone = $this->getTimeZone();
570 $now = new DateTime('@'.PhabricatorTime
::getNow());
571 $offset = $timezone->getOffset($now);
573 // Javascript offsets are in minutes and have the opposite sign.
574 $offset = -(int)($offset / 60);
579 public function getTimeZoneOffsetInHours() {
580 $offset = $this->getTimeZoneOffset();
581 $offset = (int)round($offset / 60);
587 public function formatShortDateTime($when, $now = null) {
589 $now = PhabricatorTime
::getNow();
593 $when = new DateTime('@'.$when);
594 $now = new DateTime('@'.$now);
595 } catch (Exception
$ex) {
599 $zone = $this->getTimeZone();
601 $when->setTimeZone($zone);
602 $now->setTimeZone($zone);
604 if ($when->format('Y') !== $now->format('Y')) {
605 // Different year, so show "Feb 31 2075".
607 } else if ($when->format('Ymd') !== $now->format('Ymd')) {
608 // Same year but different month and day, so show "Feb 31".
611 // Same year, month and day so show a time of day.
612 $pref_time = PhabricatorTimeFormatSetting
::SETTINGKEY
;
613 $format = $this->getUserSetting($pref_time);
616 return $when->format($format);
619 public function __toString() {
620 return $this->getUsername();
623 public static function loadOneWithEmailAddress($address) {
624 $email = id(new PhabricatorUserEmail())->loadOneWhere(
630 return id(new PhabricatorUser())->loadOneWhere(
632 $email->getUserPHID());
635 public function getDefaultSpacePHID() {
636 // TODO: We might let the user switch which space they're "in" later on;
637 // for now just use the global space if one exists.
639 // If the viewer has access to the default space, use that.
640 $spaces = PhabricatorSpacesNamespaceQuery
::getViewerActiveSpaces($this);
641 foreach ($spaces as $space) {
642 if ($space->getIsDefaultNamespace()) {
643 return $space->getPHID();
647 // Otherwise, use the space with the lowest ID that they have access to.
648 // This just tends to keep the default stable and predictable over time,
649 // so adding a new space won't change behavior for users.
651 $spaces = msort($spaces, 'getID');
652 return head($spaces)->getPHID();
659 public function hasConduitClusterToken() {
660 return ($this->conduitClusterToken
!== self
::ATTACHABLE
);
663 public function attachConduitClusterToken(PhabricatorConduitToken
$token) {
664 $this->conduitClusterToken
= $token;
668 public function getConduitClusterToken() {
669 return $this->assertAttached($this->conduitClusterToken
);
673 /* -( Availability )------------------------------------------------------- */
679 public function attachAvailability(array $availability) {
680 $this->availability
= $availability;
686 * Get the timestamp the user is away until, if they are currently away.
688 * @return int|null Epoch timestamp, or `null` if the user is not away.
691 public function getAwayUntil() {
692 $availability = $this->availability
;
694 $this->assertAttached($availability);
695 if (!$availability) {
699 return idx($availability, 'until');
703 public function getDisplayAvailability() {
704 $availability = $this->availability
;
706 $this->assertAttached($availability);
707 if (!$availability) {
711 $busy = PhabricatorCalendarEventInvitee
::AVAILABILITY_BUSY
;
713 return idx($availability, 'availability', $busy);
717 public function getAvailabilityEventPHID() {
718 $availability = $this->availability
;
720 $this->assertAttached($availability);
721 if (!$availability) {
725 return idx($availability, 'eventPHID');
730 * Get cached availability, if present.
732 * @return wild|null Cache data, or null if no cache is available.
735 public function getAvailabilityCache() {
736 $now = PhabricatorTime
::getNow();
737 if ($this->availabilityCacheTTL
<= $now) {
742 return phutil_json_decode($this->availabilityCache
);
743 } catch (Exception
$ex) {
750 * Write to the availability cache.
752 * @param wild Availability cache data.
753 * @param int|null Cache TTL.
757 public function writeAvailabilityCache(array $availability, $ttl) {
758 if (PhabricatorEnv
::isReadOnly()) {
762 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
764 $this->establishConnection('w'),
765 'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
767 $this->getTableName(),
768 phutil_json_encode($availability),
777 /* -( Multi-Factor Authentication )---------------------------------------- */
781 * Update the flag storing this user's enrollment in multi-factor auth.
783 * With certain settings, we need to check if a user has MFA on every page,
784 * so we cache MFA enrollment on the user object for performance. Calling this
785 * method synchronizes the cache by examining enrollment records. After
786 * updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if
787 * the user is enrolled.
789 * This method should be called after any changes are made to a given user's
790 * multi-factor configuration.
795 public function updateMultiFactorEnrollment() {
796 $factors = id(new PhabricatorAuthFactorConfigQuery())
798 ->withUserPHIDs(array($this->getPHID()))
799 ->withFactorProviderStatuses(
801 PhabricatorAuthFactorProviderStatus
::STATUS_ACTIVE
,
802 PhabricatorAuthFactorProviderStatus
::STATUS_DEPRECATED
,
806 $enrolled = count($factors) ?
1 : 0;
807 if ($enrolled !== $this->isEnrolledInMultiFactor
) {
808 $unguarded = AphrontWriteGuard
::beginScopedUnguardedWrites();
810 $this->establishConnection('w'),
811 'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
812 $this->getTableName(),
817 $this->isEnrolledInMultiFactor
= $enrolled;
823 * Check if the user is enrolled in multi-factor authentication.
825 * Enrolled users have one or more multi-factor authentication sources
826 * attached to their account. For performance, this value is cached. You
827 * can use @{method:updateMultiFactorEnrollment} to update the cache.
829 * @return bool True if the user is enrolled.
832 public function getIsEnrolledInMultiFactor() {
833 return $this->isEnrolledInMultiFactor
;
837 /* -( Omnipotence )-------------------------------------------------------- */
841 * Returns true if this user is omnipotent. Omnipotent users bypass all policy
844 * @return bool True if the user bypasses policy checks.
846 public function isOmnipotent() {
847 return $this->omnipotent
;
852 * Get an omnipotent user object for use in contexts where there is no acting
853 * user, notably daemons.
855 * @return PhabricatorUser An omnipotent user.
857 public static function getOmnipotentUser() {
860 $user = new PhabricatorUser();
861 $user->omnipotent
= true;
862 $user->makeEphemeral();
869 * Get a scalar string identifying this user.
871 * This is similar to using the PHID, but distinguishes between omnipotent
872 * and public users explicitly. This allows safe construction of cache keys
873 * or cache buckets which do not conflate public and omnipotent users.
875 * @return string Scalar identifier.
877 public function getCacheFragment() {
878 if ($this->isOmnipotent()) {
879 return 'u.omnipotent';
882 $phid = $this->getPHID();
891 /* -( Managing Handles )--------------------------------------------------- */
895 * Get a @{class:PhabricatorHandleList} which benefits from this viewer's
896 * internal handle pool.
898 * @param list<phid> List of PHIDs to load.
899 * @return PhabricatorHandleList Handle list object.
902 public function loadHandles(array $phids) {
903 if ($this->handlePool
=== null) {
904 $this->handlePool
= id(new PhabricatorHandlePool())
908 return $this->handlePool
->newHandleList($phids);
913 * Get a @{class:PHUIHandleView} for a single handle.
915 * This benefits from the viewer's internal handle pool.
917 * @param phid PHID to render a handle for.
918 * @return PHUIHandleView View of the handle.
921 public function renderHandle($phid) {
922 return $this->loadHandles(array($phid))->renderHandle($phid);
927 * Get a @{class:PHUIHandleListView} for a list of handles.
929 * This benefits from the viewer's internal handle pool.
931 * @param list<phid> List of PHIDs to render.
932 * @return PHUIHandleListView View of the handles.
935 public function renderHandleList(array $phids) {
936 return $this->loadHandles($phids)->renderList();
939 public function attachBadgePHIDs(array $phids) {
940 $this->badgePHIDs
= $phids;
944 public function getBadgePHIDs() {
945 return $this->assertAttached($this->badgePHIDs
);
948 /* -( CSRF )--------------------------------------------------------------- */
951 public function getCSRFToken() {
952 if ($this->isOmnipotent()) {
953 // We may end up here when called from the daemons. The omnipotent user
954 // has no meaningful CSRF token, so just return `null`.
958 return $this->newCSRFEngine()
962 public function validateCSRFToken($token) {
963 return $this->newCSRFengine()
964 ->isValidToken($token);
967 public function getAlternateCSRFString() {
968 return $this->assertAttached($this->alternateCSRFString
);
971 public function attachAlternateCSRFString($string) {
972 $this->alternateCSRFString
= $string;
976 private function newCSRFEngine() {
977 if ($this->getPHID()) {
978 $vec = $this->getPHID().$this->getAccountSecret();
980 $vec = $this->getAlternateCSRFString();
983 if ($this->hasSession()) {
984 $vec = $vec.$this->getSession()->getSessionKey();
987 $engine = new PhabricatorAuthCSRFEngine();
989 if ($this->csrfSalt
=== null) {
990 $this->csrfSalt
= $engine->newSalt();
994 ->setSalt($this->csrfSalt
)
995 ->setSecret(new PhutilOpaqueEnvelope($vec));
1001 /* -( PhabricatorPolicyInterface )----------------------------------------- */
1004 public function getCapabilities() {
1006 PhabricatorPolicyCapability
::CAN_VIEW
,
1007 PhabricatorPolicyCapability
::CAN_EDIT
,
1011 public function getPolicy($capability) {
1012 switch ($capability) {
1013 case PhabricatorPolicyCapability
::CAN_VIEW
:
1014 return PhabricatorPolicies
::POLICY_PUBLIC
;
1015 case PhabricatorPolicyCapability
::CAN_EDIT
:
1016 if ($this->getIsSystemAgent() ||
$this->getIsMailingList()) {
1017 return PhabricatorPolicies
::POLICY_ADMIN
;
1019 return PhabricatorPolicies
::POLICY_NOONE
;
1024 public function hasAutomaticCapability($capability, PhabricatorUser
$viewer) {
1025 return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());
1028 public function describeAutomaticCapability($capability) {
1029 switch ($capability) {
1030 case PhabricatorPolicyCapability
::CAN_EDIT
:
1031 return pht('Only you can edit your information.');
1038 /* -( PhabricatorCustomFieldInterface )------------------------------------ */
1041 public function getCustomFieldSpecificationForRole($role) {
1042 return PhabricatorEnv
::getEnvConfig('user.fields');
1045 public function getCustomFieldBaseClass() {
1046 return 'PhabricatorUserCustomField';
1049 public function getCustomFields() {
1050 return $this->assertAttached($this->customFields
);
1053 public function attachCustomFields(PhabricatorCustomFieldAttachment
$fields) {
1054 $this->customFields
= $fields;
1059 /* -( PhabricatorDestructibleInterface )----------------------------------- */
1062 public function destroyObjectPermanently(
1063 PhabricatorDestructionEngine
$engine) {
1065 $viewer = $engine->getViewer();
1067 $this->openTransaction();
1070 $externals = id(new PhabricatorExternalAccountQuery())
1071 ->setViewer($viewer)
1072 ->withUserPHIDs(array($this->getPHID()))
1074 foreach ($externals as $external) {
1075 $engine->destroyObject($external);
1078 $prefs = id(new PhabricatorUserPreferencesQuery())
1079 ->setViewer($viewer)
1080 ->withUsers(array($this))
1082 foreach ($prefs as $pref) {
1083 $engine->destroyObject($pref);
1086 $profiles = id(new PhabricatorUserProfile())->loadAllWhere(
1089 foreach ($profiles as $profile) {
1093 $keys = id(new PhabricatorAuthSSHKeyQuery())
1094 ->setViewer($viewer)
1095 ->withObjectPHIDs(array($this->getPHID()))
1097 foreach ($keys as $key) {
1098 $engine->destroyObject($key);
1101 $emails = id(new PhabricatorUserEmail())->loadAllWhere(
1104 foreach ($emails as $email) {
1105 $engine->destroyObject($email);
1108 $sessions = id(new PhabricatorAuthSession())->loadAllWhere(
1111 foreach ($sessions as $session) {
1115 $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
1118 foreach ($factors as $factor) {
1122 $this->saveTransaction();
1126 /* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */
1129 public function getSSHPublicKeyManagementURI(PhabricatorUser
$viewer) {
1130 if ($viewer->getPHID() == $this->getPHID()) {
1131 // If the viewer is managing their own keys, take them to the normal
1133 return '/settings/panel/ssh/';
1135 // Otherwise, take them to the administrative panel for this user.
1136 return '/settings/user/'.$this->getUsername().'/page/ssh/';
1140 public function getSSHKeyDefaultName() {
1141 return 'id_rsa_phabricator';
1144 public function getSSHKeyNotifyPHIDs() {
1151 /* -( PhabricatorApplicationTransactionInterface )------------------------- */
1154 public function getApplicationTransactionEditor() {
1155 return new PhabricatorUserTransactionEditor();
1158 public function getApplicationTransactionTemplate() {
1159 return new PhabricatorUserTransaction();
1163 /* -( PhabricatorFulltextInterface )--------------------------------------- */
1166 public function newFulltextEngine() {
1167 return new PhabricatorUserFulltextEngine();
1171 /* -( PhabricatorFerretInterface )----------------------------------------- */
1174 public function newFerretEngine() {
1175 return new PhabricatorUserFerretEngine();
1179 /* -( PhabricatorConduitResultInterface )---------------------------------- */
1182 public function getFieldSpecificationsForConduit() {
1184 id(new PhabricatorConduitSearchFieldSpecification())
1185 ->setKey('username')
1187 ->setDescription(pht("The user's username.")),
1188 id(new PhabricatorConduitSearchFieldSpecification())
1189 ->setKey('realName')
1191 ->setDescription(pht("The user's real name.")),
1192 id(new PhabricatorConduitSearchFieldSpecification())
1194 ->setType('list<string>')
1195 ->setDescription(pht('List of account roles.')),
1199 public function getFieldValuesForConduit() {
1202 if ($this->getIsDisabled()) {
1203 $roles[] = 'disabled';
1206 if ($this->getIsSystemAgent()) {
1210 if ($this->getIsMailingList()) {
1214 if ($this->getIsAdmin()) {
1218 if ($this->getIsEmailVerified()) {
1219 $roles[] = 'verified';
1222 if ($this->getIsApproved()) {
1223 $roles[] = 'approved';
1226 if ($this->isUserActivated()) {
1227 $roles[] = 'activated';
1231 'username' => $this->getUsername(),
1232 'realName' => $this->getRealName(),
1237 public function getConduitSearchAttachments() {
1239 id(new PhabricatorPeopleAvailabilitySearchEngineAttachment())
1240 ->setAttachmentKey('availability'),
1245 /* -( User Cache )--------------------------------------------------------- */
1251 public function attachRawCacheData(array $data) {
1252 $this->rawCacheData
= $data +
$this->rawCacheData
;
1256 public function setAllowInlineCacheGeneration($allow_cache_generation) {
1257 $this->allowInlineCacheGeneration
= $allow_cache_generation;
1264 protected function requireCacheData($key) {
1265 if (isset($this->usableCacheData
[$key])) {
1266 return $this->usableCacheData
[$key];
1269 $type = PhabricatorUserCacheType
::requireCacheTypeForKey($key);
1271 if (isset($this->rawCacheData
[$key])) {
1272 $raw_value = $this->rawCacheData
[$key];
1274 $usable_value = $type->getValueFromStorage($raw_value);
1275 $this->usableCacheData
[$key] = $usable_value;
1277 return $usable_value;
1280 // By default, we throw if a cache isn't available. This is consistent
1281 // with the standard `needX()` + `attachX()` + `getX()` interaction.
1282 if (!$this->allowInlineCacheGeneration
) {
1283 throw new PhabricatorDataNotAttachedException($this);
1286 $user_phid = $this->getPHID();
1288 // Try to read the actual cache before we generate a new value. We can
1289 // end up here via Conduit, which does not use normal sessions and can
1290 // not pick up a free cache load during session identification.
1292 $raw_data = PhabricatorUserCache
::readCaches(
1296 if (array_key_exists($user_phid, $raw_data)) {
1297 $raw_value = $raw_data[$user_phid];
1298 $usable_value = $type->getValueFromStorage($raw_value);
1299 $this->rawCacheData
[$key] = $raw_value;
1300 $this->usableCacheData
[$key] = $usable_value;
1301 return $usable_value;
1305 $usable_value = $type->getDefaultValue();
1308 $map = $type->newValueForUsers($key, array($this));
1309 if (array_key_exists($user_phid, $map)) {
1310 $raw_value = $map[$user_phid];
1311 $usable_value = $type->getValueFromStorage($raw_value);
1313 $this->rawCacheData
[$key] = $raw_value;
1314 PhabricatorUserCache
::writeCache(
1322 $this->usableCacheData
[$key] = $usable_value;
1324 return $usable_value;
1331 public function clearCacheData($key) {
1332 unset($this->rawCacheData
[$key]);
1333 unset($this->usableCacheData
[$key]);
1338 public function getCSSValue($variable_key) {
1339 $preference = PhabricatorAccessibilitySetting
::SETTINGKEY
;
1340 $key = $this->getUserSetting($preference);
1342 $postprocessor = CelerityPostprocessor
::getPostprocessor($key);
1343 $variables = $postprocessor->getVariables();
1345 if (!isset($variables[$variable_key])) {
1346 throw new Exception(
1348 'Unknown CSS variable "%s"!',
1352 return $variables[$variable_key];
1355 /* -( PhabricatorAuthPasswordHashInterface )------------------------------- */
1358 public function newPasswordDigest(
1359 PhutilOpaqueEnvelope
$envelope,
1360 PhabricatorAuthPassword
$password) {
1362 // Before passwords are hashed, they are digested. The goal of digestion
1363 // is twofold: to reduce the length of very long passwords to something
1364 // reasonable; and to salt the password in case the best available hasher
1365 // does not include salt automatically.
1367 // Users may choose arbitrarily long passwords, and attackers may try to
1368 // attack the system by probing it with very long passwords. When large
1369 // inputs are passed to hashers -- which are intentionally slow -- it
1370 // can result in unacceptably long runtimes. The classic attack here is
1371 // to try to log in with a 64MB password and see if that locks up the
1372 // machine for the next century. By digesting passwords to a standard
1373 // length first, the length of the raw input does not impact the runtime
1374 // of the hashing algorithm.
1376 // Some hashers like bcrypt are self-salting, while other hashers are not.
1377 // Applying salt while digesting passwords ensures that hashes are salted
1378 // whether we ultimately select a self-salting hasher or not.
1380 // For legacy compatibility reasons, old VCS and Account password digest
1381 // algorithms are significantly more complicated than necessary to achieve
1382 // these goals. This is because they once used a different hashing and
1383 // salting process. When we upgraded to the modern modular hasher
1384 // infrastructure, we just bolted it onto the end of the existing pipelines
1385 // so that upgrading didn't break all users' credentials.
1387 // New implementations can (and, generally, should) safely select the
1388 // simple HMAC SHA256 digest at the bottom of the function, which does
1389 // everything that a digest callback should without any needless legacy
1392 if ($password->getLegacyDigestFormat() == 'v1') {
1393 switch ($password->getPasswordType()) {
1394 case PhabricatorAuthPassword
::PASSWORD_TYPE_VCS
:
1395 // Old VCS passwords use an iterated HMAC SHA1 as a digest algorithm.
1396 // They originally used this as a hasher, but it became a digest
1397 // algorithm once hashing was upgraded to include bcrypt.
1398 $digest = $envelope->openEnvelope();
1399 $salt = $this->getPHID();
1400 for ($ii = 0; $ii < 1000; $ii++
) {
1401 $digest = PhabricatorHash
::weakDigest($digest, $salt);
1403 return new PhutilOpaqueEnvelope($digest);
1404 case PhabricatorAuthPassword
::PASSWORD_TYPE_ACCOUNT
:
1405 // Account passwords previously used this weird mess of salt and did
1406 // not digest the input to a standard length.
1408 // Beyond this being a weird special case, there are two actual
1409 // problems with this, although neither are particularly severe:
1411 // First, because we do not normalize the length of passwords, this
1412 // algorithm may make us vulnerable to DOS attacks where an attacker
1413 // attempts to use a very long input to slow down hashers.
1415 // Second, because the username is part of the hash algorithm,
1416 // renaming a user breaks their password. This isn't a huge deal but
1417 // it's pretty silly. There's no security justification for this
1418 // behavior, I just didn't think about the implication when I wrote
1422 $this->getUsername(),
1423 $envelope->openEnvelope(),
1425 $password->getPasswordSalt(),
1428 return new PhutilOpaqueEnvelope(implode('', $parts));
1432 // For passwords which do not have some crazy legacy reason to use some
1433 // other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies
1434 // the digest requirements and is simple.
1436 $digest = PhabricatorHash
::digestHMACSHA256(
1437 $envelope->openEnvelope(),
1438 $password->getPasswordSalt());
1440 return new PhutilOpaqueEnvelope($digest);
1443 public function newPasswordBlocklist(
1444 PhabricatorUser
$viewer,
1445 PhabricatorAuthPasswordEngine
$engine) {
1448 $list[] = $this->getUsername();
1449 $list[] = $this->getRealName();
1451 $emails = id(new PhabricatorUserEmail())->loadAllWhere(
1454 foreach ($emails as $email) {
1455 $list[] = $email->getAddress();