Correct a parameter order swap in "diffusion.historyquery" for Mercurial
[phabricator.git] / src / applications / people / storage / PhabricatorUser.php
blobfa6dc08e9fec86ffe90c7f22f53be02475f7a3f1
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 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());
292 return $result;
295 public function attachSession(PhabricatorAuthSession $session) {
296 $this->session = $session;
297 return $this;
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()) {
310 return false;
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;
329 return $this;
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',
339 $this->getPHID());
341 if (!$this->profile) {
342 $this->profile = PhabricatorUserProfile::initializeNewProfile($this);
345 return $this->profile;
348 public function loadPrimaryEmailAddress() {
349 $email = $this->loadPrimaryEmail();
350 if (!$email) {
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',
359 $this->getPHID());
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);
376 } else {
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));
389 if ($cache_map) {
390 $value = $cache_map[$cache_key];
391 } else {
392 $defaults = PhabricatorSetting::getAllSettings();
393 if (isset($defaults[$key])) {
394 $value = id(clone $defaults[$key])
395 ->setViewer($this)
396 ->getSettingDefaultValue();
397 } else {
398 $value = null;
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.
414 * @task settings
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;
424 return $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);
444 if (!$settings) {
445 $preferences = PhabricatorUserPreferences::loadGlobalPreferences($this);
446 $settings = $preferences->getPreferences();
447 $cache->setKey($cache_key, $settings);
450 return $settings;
455 * Override the user's timezone identifier.
457 * This is primarily useful for unit tests.
459 * @param string New timezone identifier.
460 * @return this
461 * @task settings
463 public function overrideTimezoneIdentifier($identifier) {
464 $timezone_key = PhabricatorTimezoneSetting::SETTINGKEY;
465 $this->settingCacheKeys[$timezone_key] = true;
466 $this->settingCache[$timezone_key] = $identifier;
467 return $this;
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());
486 $sql = array();
487 foreach ($tokens as $token) {
488 $sql[] = qsprintf(
489 $conn_w,
490 '(%d, %s)',
491 $this->getID(),
492 $token);
495 queryfx(
496 $conn_w,
497 'DELETE FROM %T WHERE userID = %d',
498 $table,
499 $this->getID());
500 if ($sql) {
501 queryfx(
502 $conn_w,
503 'INSERT INTO %T (userID, token) VALUES %LQ',
504 $table,
505 $sql);
509 public static function describeValidUsername() {
510 return pht(
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 '.
513 'characters.',
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) {
526 return false;
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().')';
559 } else {
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);
576 return $offset;
579 public function getTimeZoneOffsetInHours() {
580 $offset = $this->getTimeZoneOffset();
581 $offset = (int)round($offset / 60);
582 $offset = -$offset;
584 return $offset;
587 public function formatShortDateTime($when, $now = null) {
588 if ($now === null) {
589 $now = PhabricatorTime::getNow();
592 try {
593 $when = new DateTime('@'.$when);
594 $now = new DateTime('@'.$now);
595 } catch (Exception $ex) {
596 return null;
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".
606 $format = 'M j Y';
607 } else if ($when->format('Ymd') !== $now->format('Ymd')) {
608 // Same year but different month and day, so show "Feb 31".
609 $format = 'M j';
610 } else {
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(
625 'address = %s',
626 $address);
627 if (!$email) {
628 return null;
630 return id(new PhabricatorUser())->loadOneWhere(
631 'phid = %s',
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.
650 if ($spaces) {
651 $spaces = msort($spaces, 'getID');
652 return head($spaces)->getPHID();
655 return null;
659 public function hasConduitClusterToken() {
660 return ($this->conduitClusterToken !== self::ATTACHABLE);
663 public function attachConduitClusterToken(PhabricatorConduitToken $token) {
664 $this->conduitClusterToken = $token;
665 return $this;
668 public function getConduitClusterToken() {
669 return $this->assertAttached($this->conduitClusterToken);
673 /* -( Availability )------------------------------------------------------- */
677 * @task availability
679 public function attachAvailability(array $availability) {
680 $this->availability = $availability;
681 return $this;
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.
689 * @task availability
691 public function getAwayUntil() {
692 $availability = $this->availability;
694 $this->assertAttached($availability);
695 if (!$availability) {
696 return null;
699 return idx($availability, 'until');
703 public function getDisplayAvailability() {
704 $availability = $this->availability;
706 $this->assertAttached($availability);
707 if (!$availability) {
708 return null;
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) {
722 return null;
725 return idx($availability, 'eventPHID');
730 * Get cached availability, if present.
732 * @return wild|null Cache data, or null if no cache is available.
733 * @task availability
735 public function getAvailabilityCache() {
736 $now = PhabricatorTime::getNow();
737 if ($this->availabilityCacheTTL <= $now) {
738 return null;
741 try {
742 return phutil_json_decode($this->availabilityCache);
743 } catch (Exception $ex) {
744 return null;
750 * Write to the availability cache.
752 * @param wild Availability cache data.
753 * @param int|null Cache TTL.
754 * @return this
755 * @task availability
757 public function writeAvailabilityCache(array $availability, $ttl) {
758 if (PhabricatorEnv::isReadOnly()) {
759 return $this;
762 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
763 queryfx(
764 $this->establishConnection('w'),
765 'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd
766 WHERE id = %d',
767 $this->getTableName(),
768 phutil_json_encode($availability),
769 $ttl,
770 $this->getID());
771 unset($unguarded);
773 return $this;
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.
792 * @return void
793 * @task factors
795 public function updateMultiFactorEnrollment() {
796 $factors = id(new PhabricatorAuthFactorConfigQuery())
797 ->setViewer($this)
798 ->withUserPHIDs(array($this->getPHID()))
799 ->withFactorProviderStatuses(
800 array(
801 PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
802 PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
804 ->execute();
806 $enrolled = count($factors) ? 1 : 0;
807 if ($enrolled !== $this->isEnrolledInMultiFactor) {
808 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
809 queryfx(
810 $this->establishConnection('w'),
811 'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',
812 $this->getTableName(),
813 $enrolled,
814 $this->getID());
815 unset($unguarded);
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.
830 * @task factors
832 public function getIsEnrolledInMultiFactor() {
833 return $this->isEnrolledInMultiFactor;
837 /* -( Omnipotence )-------------------------------------------------------- */
841 * Returns true if this user is omnipotent. Omnipotent users bypass all policy
842 * checks.
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() {
858 static $user = null;
859 if (!$user) {
860 $user = new PhabricatorUser();
861 $user->omnipotent = true;
862 $user->makeEphemeral();
864 return $user;
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();
883 if ($phid) {
884 return 'u.'.$phid;
887 return 'u.public';
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.
900 * @task handle
902 public function loadHandles(array $phids) {
903 if ($this->handlePool === null) {
904 $this->handlePool = id(new PhabricatorHandlePool())
905 ->setViewer($this);
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.
919 * @task 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.
933 * @task handle
935 public function renderHandleList(array $phids) {
936 return $this->loadHandles($phids)->renderList();
939 public function attachBadgePHIDs(array $phids) {
940 $this->badgePHIDs = $phids;
941 return $this;
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`.
955 return null;
958 return $this->newCSRFEngine()
959 ->newToken();
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;
973 return $this;
976 private function newCSRFEngine() {
977 if ($this->getPHID()) {
978 $vec = $this->getPHID().$this->getAccountSecret();
979 } else {
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();
993 $engine
994 ->setSalt($this->csrfSalt)
995 ->setSecret(new PhutilOpaqueEnvelope($vec));
997 return $engine;
1001 /* -( PhabricatorPolicyInterface )----------------------------------------- */
1004 public function getCapabilities() {
1005 return array(
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;
1018 } else {
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.');
1032 default:
1033 return null;
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;
1055 return $this;
1059 /* -( PhabricatorDestructibleInterface )----------------------------------- */
1062 public function destroyObjectPermanently(
1063 PhabricatorDestructionEngine $engine) {
1065 $viewer = $engine->getViewer();
1067 $this->openTransaction();
1068 $this->delete();
1070 $externals = id(new PhabricatorExternalAccountQuery())
1071 ->setViewer($viewer)
1072 ->withUserPHIDs(array($this->getPHID()))
1073 ->newIterator();
1074 foreach ($externals as $external) {
1075 $engine->destroyObject($external);
1078 $prefs = id(new PhabricatorUserPreferencesQuery())
1079 ->setViewer($viewer)
1080 ->withUsers(array($this))
1081 ->execute();
1082 foreach ($prefs as $pref) {
1083 $engine->destroyObject($pref);
1086 $profiles = id(new PhabricatorUserProfile())->loadAllWhere(
1087 'userPHID = %s',
1088 $this->getPHID());
1089 foreach ($profiles as $profile) {
1090 $profile->delete();
1093 $keys = id(new PhabricatorAuthSSHKeyQuery())
1094 ->setViewer($viewer)
1095 ->withObjectPHIDs(array($this->getPHID()))
1096 ->execute();
1097 foreach ($keys as $key) {
1098 $engine->destroyObject($key);
1101 $emails = id(new PhabricatorUserEmail())->loadAllWhere(
1102 'userPHID = %s',
1103 $this->getPHID());
1104 foreach ($emails as $email) {
1105 $engine->destroyObject($email);
1108 $sessions = id(new PhabricatorAuthSession())->loadAllWhere(
1109 'userPHID = %s',
1110 $this->getPHID());
1111 foreach ($sessions as $session) {
1112 $session->delete();
1115 $factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(
1116 'userPHID = %s',
1117 $this->getPHID());
1118 foreach ($factors as $factor) {
1119 $factor->delete();
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
1132 // panel.
1133 return '/settings/panel/ssh/';
1134 } else {
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() {
1145 return array(
1146 $this->getPHID(),
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() {
1183 return array(
1184 id(new PhabricatorConduitSearchFieldSpecification())
1185 ->setKey('username')
1186 ->setType('string')
1187 ->setDescription(pht("The user's username.")),
1188 id(new PhabricatorConduitSearchFieldSpecification())
1189 ->setKey('realName')
1190 ->setType('string')
1191 ->setDescription(pht("The user's real name.")),
1192 id(new PhabricatorConduitSearchFieldSpecification())
1193 ->setKey('roles')
1194 ->setType('list<string>')
1195 ->setDescription(pht('List of account roles.')),
1199 public function getFieldValuesForConduit() {
1200 $roles = array();
1202 if ($this->getIsDisabled()) {
1203 $roles[] = 'disabled';
1206 if ($this->getIsSystemAgent()) {
1207 $roles[] = 'bot';
1210 if ($this->getIsMailingList()) {
1211 $roles[] = 'list';
1214 if ($this->getIsAdmin()) {
1215 $roles[] = 'admin';
1218 if ($this->getIsEmailVerified()) {
1219 $roles[] = 'verified';
1222 if ($this->getIsApproved()) {
1223 $roles[] = 'approved';
1226 if ($this->isUserActivated()) {
1227 $roles[] = 'activated';
1230 return array(
1231 'username' => $this->getUsername(),
1232 'realName' => $this->getRealName(),
1233 'roles' => $roles,
1237 public function getConduitSearchAttachments() {
1238 return array(
1239 id(new PhabricatorPeopleAvailabilitySearchEngineAttachment())
1240 ->setAttachmentKey('availability'),
1245 /* -( User Cache )--------------------------------------------------------- */
1249 * @task cache
1251 public function attachRawCacheData(array $data) {
1252 $this->rawCacheData = $data + $this->rawCacheData;
1253 return $this;
1256 public function setAllowInlineCacheGeneration($allow_cache_generation) {
1257 $this->allowInlineCacheGeneration = $allow_cache_generation;
1258 return $this;
1262 * @task cache
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.
1291 if ($user_phid) {
1292 $raw_data = PhabricatorUserCache::readCaches(
1293 $type,
1294 $key,
1295 array($user_phid));
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();
1307 if ($user_phid) {
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(
1315 $type,
1316 $key,
1317 $user_phid,
1318 $raw_value);
1322 $this->usableCacheData[$key] = $usable_value;
1324 return $usable_value;
1329 * @task cache
1331 public function clearCacheData($key) {
1332 unset($this->rawCacheData[$key]);
1333 unset($this->usableCacheData[$key]);
1334 return $this;
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(
1347 pht(
1348 'Unknown CSS variable "%s"!',
1349 $variable_key));
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
1390 // baggage on top.
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
1419 // it originally.
1421 $parts = array(
1422 $this->getUsername(),
1423 $envelope->openEnvelope(),
1424 $this->getPHID(),
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) {
1447 $list = array();
1448 $list[] = $this->getUsername();
1449 $list[] = $this->getRealName();
1451 $emails = id(new PhabricatorUserEmail())->loadAllWhere(
1452 'userPHID = %s',
1453 $this->getPHID());
1454 foreach ($emails as $email) {
1455 $list[] = $email->getAddress();
1458 return $list;