Path: blob/master/src/applications/people/storage/PhabricatorUser.php
12256 views
<?php12/**3* @task availability Availability4* @task image-cache Profile Image Cache5* @task factors Multi-Factor Authentication6* @task handles Managing Handles7* @task settings Settings8* @task cache User Cache9*/10final class PhabricatorUser11extends PhabricatorUserDAO12implements13PhutilPerson,14PhabricatorPolicyInterface,15PhabricatorCustomFieldInterface,16PhabricatorDestructibleInterface,17PhabricatorSSHPublicKeyInterface,18PhabricatorFlaggableInterface,19PhabricatorApplicationTransactionInterface,20PhabricatorFulltextInterface,21PhabricatorFerretInterface,22PhabricatorConduitResultInterface,23PhabricatorAuthPasswordHashInterface {2425const SESSION_TABLE = 'phabricator_session';26const NAMETOKEN_TABLE = 'user_nametoken';27const MAXIMUM_USERNAME_LENGTH = 64;2829protected $userName;30protected $realName;31protected $profileImagePHID;32protected $defaultProfileImagePHID;33protected $defaultProfileImageVersion;34protected $availabilityCache;35protected $availabilityCacheTTL;3637protected $conduitCertificate;3839protected $isSystemAgent = 0;40protected $isMailingList = 0;41protected $isAdmin = 0;42protected $isDisabled = 0;43protected $isEmailVerified = 0;44protected $isApproved = 0;45protected $isEnrolledInMultiFactor = 0;4647protected $accountSecret;4849private $profile = null;50private $availability = self::ATTACHABLE;51private $preferences = null;52private $omnipotent = false;53private $customFields = self::ATTACHABLE;54private $badgePHIDs = self::ATTACHABLE;5556private $alternateCSRFString = self::ATTACHABLE;57private $session = self::ATTACHABLE;58private $rawCacheData = array();59private $usableCacheData = array();6061private $handlePool;62private $csrfSalt;6364private $settingCacheKeys = array();65private $settingCache = array();66private $allowInlineCacheGeneration;67private $conduitClusterToken = self::ATTACHABLE;6869protected function readField($field) {70switch ($field) {71// Make sure these return booleans.72case 'isAdmin':73return (bool)$this->isAdmin;74case 'isDisabled':75return (bool)$this->isDisabled;76case 'isSystemAgent':77return (bool)$this->isSystemAgent;78case 'isMailingList':79return (bool)$this->isMailingList;80case 'isEmailVerified':81return (bool)$this->isEmailVerified;82case 'isApproved':83return (bool)$this->isApproved;84default:85return parent::readField($field);86}87}888990/**91* Is this a live account which has passed required approvals? Returns true92* if this is an enabled, verified (if required), approved (if required)93* account, and false otherwise.94*95* @return bool True if this is a standard, usable account.96*/97public function isUserActivated() {98if (!$this->isLoggedIn()) {99return false;100}101102if ($this->isOmnipotent()) {103return true;104}105106if ($this->getIsDisabled()) {107return false;108}109110if (!$this->getIsApproved()) {111return false;112}113114if (PhabricatorUserEmail::isEmailVerificationRequired()) {115if (!$this->getIsEmailVerified()) {116return false;117}118}119120return true;121}122123124/**125* Is this a user who we can reasonably expect to respond to requests?126*127* This is used to provide a grey "disabled/unresponsive" dot cue when128* rendering handles and tags, so it isn't a surprise if you get ignored129* when you ask things of users who will not receive notifications or could130* not respond to them (because they are disabled, unapproved, do not have131* verified email addresses, etc).132*133* @return bool True if this user can receive and respond to requests from134* other humans.135*/136public function isResponsive() {137if (!$this->isUserActivated()) {138return false;139}140141if (!$this->getIsEmailVerified()) {142return false;143}144145return true;146}147148149public function canEstablishWebSessions() {150if ($this->getIsMailingList()) {151return false;152}153154if ($this->getIsSystemAgent()) {155return false;156}157158return true;159}160161public function canEstablishAPISessions() {162if ($this->getIsDisabled()) {163return false;164}165166// Intracluster requests are permitted even if the user is logged out:167// in particular, public users are allowed to issue intracluster requests168// when browsing Diffusion.169if (PhabricatorEnv::isClusterRemoteAddress()) {170if (!$this->isLoggedIn()) {171return true;172}173}174175if (!$this->isUserActivated()) {176return false;177}178179if ($this->getIsMailingList()) {180return false;181}182183return true;184}185186public function canEstablishSSHSessions() {187if (!$this->isUserActivated()) {188return false;189}190191if ($this->getIsMailingList()) {192return false;193}194195return true;196}197198/**199* Returns `true` if this is a standard user who is logged in. Returns `false`200* for logged out, anonymous, or external users.201*202* @return bool `true` if the user is a standard user who is logged in with203* a normal session.204*/205public function getIsStandardUser() {206$type_user = PhabricatorPeopleUserPHIDType::TYPECONST;207return $this->getPHID() && (phid_get_type($this->getPHID()) == $type_user);208}209210protected function getConfiguration() {211return array(212self::CONFIG_AUX_PHID => true,213self::CONFIG_COLUMN_SCHEMA => array(214'userName' => 'sort64',215'realName' => 'text128',216'profileImagePHID' => 'phid?',217'conduitCertificate' => 'text255',218'isSystemAgent' => 'bool',219'isMailingList' => 'bool',220'isDisabled' => 'bool',221'isAdmin' => 'bool',222'isEmailVerified' => 'uint32',223'isApproved' => 'uint32',224'accountSecret' => 'bytes64',225'isEnrolledInMultiFactor' => 'bool',226'availabilityCache' => 'text255?',227'availabilityCacheTTL' => 'uint32?',228'defaultProfileImagePHID' => 'phid?',229'defaultProfileImageVersion' => 'text64?',230),231self::CONFIG_KEY_SCHEMA => array(232'key_phid' => null,233'phid' => array(234'columns' => array('phid'),235'unique' => true,236),237'userName' => array(238'columns' => array('userName'),239'unique' => true,240),241'realName' => array(242'columns' => array('realName'),243),244'key_approved' => array(245'columns' => array('isApproved'),246),247),248self::CONFIG_NO_MUTATE => array(249'availabilityCache' => true,250'availabilityCacheTTL' => true,251),252) + parent::getConfiguration();253}254255public function generatePHID() {256return PhabricatorPHID::generateNewPHID(257PhabricatorPeopleUserPHIDType::TYPECONST);258}259260public function getMonogram() {261return '@'.$this->getUsername();262}263264public function isLoggedIn() {265return !($this->getPHID() === null);266}267268public function saveWithoutIndex() {269return parent::save();270}271272public function save() {273if (!$this->getConduitCertificate()) {274$this->setConduitCertificate($this->generateConduitCertificate());275}276277$secret = $this->getAccountSecret();278if (($secret === null) || !strlen($secret)) {279$this->setAccountSecret(Filesystem::readRandomCharacters(64));280}281282$result = $this->saveWithoutIndex();283284if ($this->profile) {285$this->profile->save();286}287288$this->updateNameTokens();289290PhabricatorSearchWorker::queueDocumentForIndexing($this->getPHID());291292return $result;293}294295public function attachSession(PhabricatorAuthSession $session) {296$this->session = $session;297return $this;298}299300public function getSession() {301return $this->assertAttached($this->session);302}303304public function hasSession() {305return ($this->session !== self::ATTACHABLE);306}307308public function hasHighSecuritySession() {309if (!$this->hasSession()) {310return false;311}312313return $this->getSession()->isHighSecuritySession();314}315316private function generateConduitCertificate() {317return Filesystem::readRandomCharacters(255);318}319320const EMAIL_CYCLE_FREQUENCY = 86400;321const EMAIL_TOKEN_LENGTH = 24;322323public function getUserProfile() {324return $this->assertAttached($this->profile);325}326327public function attachUserProfile(PhabricatorUserProfile $profile) {328$this->profile = $profile;329return $this;330}331332public function loadUserProfile() {333if ($this->profile) {334return $this->profile;335}336337$profile_dao = new PhabricatorUserProfile();338$this->profile = $profile_dao->loadOneWhere('userPHID = %s',339$this->getPHID());340341if (!$this->profile) {342$this->profile = PhabricatorUserProfile::initializeNewProfile($this);343}344345return $this->profile;346}347348public function loadPrimaryEmailAddress() {349$email = $this->loadPrimaryEmail();350if (!$email) {351throw new Exception(pht('User has no primary email address!'));352}353return $email->getAddress();354}355356public function loadPrimaryEmail() {357return id(new PhabricatorUserEmail())->loadOneWhere(358'userPHID = %s AND isPrimary = 1',359$this->getPHID());360}361362363/* -( Settings )----------------------------------------------------------- */364365366public function getUserSetting($key) {367// NOTE: We store available keys and cached values separately to make it368// faster to check for `null` in the cache, which is common.369if (isset($this->settingCacheKeys[$key])) {370return $this->settingCache[$key];371}372373$settings_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;374if ($this->getPHID()) {375$settings = $this->requireCacheData($settings_key);376} else {377$settings = $this->loadGlobalSettings();378}379380if (array_key_exists($key, $settings)) {381$value = $settings[$key];382return $this->writeUserSettingCache($key, $value);383}384385$cache = PhabricatorCaches::getRuntimeCache();386$cache_key = "settings.defaults({$key})";387$cache_map = $cache->getKeys(array($cache_key));388389if ($cache_map) {390$value = $cache_map[$cache_key];391} else {392$defaults = PhabricatorSetting::getAllSettings();393if (isset($defaults[$key])) {394$value = id(clone $defaults[$key])395->setViewer($this)396->getSettingDefaultValue();397} else {398$value = null;399}400401$cache->setKey($cache_key, $value);402}403404return $this->writeUserSettingCache($key, $value);405}406407408/**409* Test if a given setting is set to a particular value.410*411* @param const Setting key.412* @param wild Value to compare.413* @return bool True if the setting has the specified value.414* @task settings415*/416public function compareUserSetting($key, $value) {417$actual = $this->getUserSetting($key);418return ($actual == $value);419}420421private function writeUserSettingCache($key, $value) {422$this->settingCacheKeys[$key] = true;423$this->settingCache[$key] = $value;424return $value;425}426427public function getTranslation() {428return $this->getUserSetting(PhabricatorTranslationSetting::SETTINGKEY);429}430431public function getTimezoneIdentifier() {432return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY);433}434435public static function getGlobalSettingsCacheKey() {436return 'user.settings.globals.v1';437}438439private function loadGlobalSettings() {440$cache_key = self::getGlobalSettingsCacheKey();441$cache = PhabricatorCaches::getMutableStructureCache();442443$settings = $cache->getKey($cache_key);444if (!$settings) {445$preferences = PhabricatorUserPreferences::loadGlobalPreferences($this);446$settings = $preferences->getPreferences();447$cache->setKey($cache_key, $settings);448}449450return $settings;451}452453454/**455* Override the user's timezone identifier.456*457* This is primarily useful for unit tests.458*459* @param string New timezone identifier.460* @return this461* @task settings462*/463public function overrideTimezoneIdentifier($identifier) {464$timezone_key = PhabricatorTimezoneSetting::SETTINGKEY;465$this->settingCacheKeys[$timezone_key] = true;466$this->settingCache[$timezone_key] = $identifier;467return $this;468}469470public function getGender() {471return $this->getUserSetting(PhabricatorPronounSetting::SETTINGKEY);472}473474/**475* Populate the nametoken table, which used to fetch typeahead results. When476* a user types "linc", we want to match "Abraham Lincoln" from on-demand477* typeahead sources. To do this, we need a separate table of name fragments.478*/479public function updateNameTokens() {480$table = self::NAMETOKEN_TABLE;481$conn_w = $this->establishConnection('w');482483$tokens = PhabricatorTypeaheadDatasource::tokenizeString(484$this->getUserName().' '.$this->getRealName());485486$sql = array();487foreach ($tokens as $token) {488$sql[] = qsprintf(489$conn_w,490'(%d, %s)',491$this->getID(),492$token);493}494495queryfx(496$conn_w,497'DELETE FROM %T WHERE userID = %d',498$table,499$this->getID());500if ($sql) {501queryfx(502$conn_w,503'INSERT INTO %T (userID, token) VALUES %LQ',504$table,505$sql);506}507}508509public static function describeValidUsername() {510return 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.',514new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH));515}516517public static function validateUsername($username) {518// NOTE: If you update this, make sure to update:519//520// - Remarkup rule for @mentions.521// - Routing rule for "/p/username/".522// - Unit tests, obviously.523// - describeValidUsername() method, above.524525if (strlen($username) > self::MAXIMUM_USERNAME_LENGTH) {526return false;527}528529return (bool)preg_match('/^[a-zA-Z0-9._-]*[a-zA-Z0-9_-]\z/', $username);530}531532public static function getDefaultProfileImageURI() {533return celerity_get_resource_uri('/rsrc/image/avatar.png');534}535536public function getProfileImageURI() {537$uri_key = PhabricatorUserProfileImageCacheType::KEY_URI;538return $this->requireCacheData($uri_key);539}540541public function getUnreadNotificationCount() {542$notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT;543return $this->requireCacheData($notification_key);544}545546public function getUnreadMessageCount() {547$message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT;548return $this->requireCacheData($message_key);549}550551public function getRecentBadgeAwards() {552$badges_key = PhabricatorUserBadgesCacheType::KEY_BADGES;553return $this->requireCacheData($badges_key);554}555556public function getFullName() {557if (strlen($this->getRealName())) {558return $this->getUsername().' ('.$this->getRealName().')';559} else {560return $this->getUsername();561}562}563564public function getTimeZone() {565return new DateTimeZone($this->getTimezoneIdentifier());566}567568public function getTimeZoneOffset() {569$timezone = $this->getTimeZone();570$now = new DateTime('@'.PhabricatorTime::getNow());571$offset = $timezone->getOffset($now);572573// Javascript offsets are in minutes and have the opposite sign.574$offset = -(int)($offset / 60);575576return $offset;577}578579public function getTimeZoneOffsetInHours() {580$offset = $this->getTimeZoneOffset();581$offset = (int)round($offset / 60);582$offset = -$offset;583584return $offset;585}586587public function formatShortDateTime($when, $now = null) {588if ($now === null) {589$now = PhabricatorTime::getNow();590}591592try {593$when = new DateTime('@'.$when);594$now = new DateTime('@'.$now);595} catch (Exception $ex) {596return null;597}598599$zone = $this->getTimeZone();600601$when->setTimeZone($zone);602$now->setTimeZone($zone);603604if ($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);614}615616return $when->format($format);617}618619public function __toString() {620return $this->getUsername();621}622623public static function loadOneWithEmailAddress($address) {624$email = id(new PhabricatorUserEmail())->loadOneWhere(625'address = %s',626$address);627if (!$email) {628return null;629}630return id(new PhabricatorUser())->loadOneWhere(631'phid = %s',632$email->getUserPHID());633}634635public 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.638639// If the viewer has access to the default space, use that.640$spaces = PhabricatorSpacesNamespaceQuery::getViewerActiveSpaces($this);641foreach ($spaces as $space) {642if ($space->getIsDefaultNamespace()) {643return $space->getPHID();644}645}646647// 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.650if ($spaces) {651$spaces = msort($spaces, 'getID');652return head($spaces)->getPHID();653}654655return null;656}657658659public function hasConduitClusterToken() {660return ($this->conduitClusterToken !== self::ATTACHABLE);661}662663public function attachConduitClusterToken(PhabricatorConduitToken $token) {664$this->conduitClusterToken = $token;665return $this;666}667668public function getConduitClusterToken() {669return $this->assertAttached($this->conduitClusterToken);670}671672673/* -( Availability )------------------------------------------------------- */674675676/**677* @task availability678*/679public function attachAvailability(array $availability) {680$this->availability = $availability;681return $this;682}683684685/**686* Get the timestamp the user is away until, if they are currently away.687*688* @return int|null Epoch timestamp, or `null` if the user is not away.689* @task availability690*/691public function getAwayUntil() {692$availability = $this->availability;693694$this->assertAttached($availability);695if (!$availability) {696return null;697}698699return idx($availability, 'until');700}701702703public function getDisplayAvailability() {704$availability = $this->availability;705706$this->assertAttached($availability);707if (!$availability) {708return null;709}710711$busy = PhabricatorCalendarEventInvitee::AVAILABILITY_BUSY;712713return idx($availability, 'availability', $busy);714}715716717public function getAvailabilityEventPHID() {718$availability = $this->availability;719720$this->assertAttached($availability);721if (!$availability) {722return null;723}724725return idx($availability, 'eventPHID');726}727728729/**730* Get cached availability, if present.731*732* @return wild|null Cache data, or null if no cache is available.733* @task availability734*/735public function getAvailabilityCache() {736$now = PhabricatorTime::getNow();737if ($this->availabilityCacheTTL <= $now) {738return null;739}740741try {742return phutil_json_decode($this->availabilityCache);743} catch (Exception $ex) {744return null;745}746}747748749/**750* Write to the availability cache.751*752* @param wild Availability cache data.753* @param int|null Cache TTL.754* @return this755* @task availability756*/757public function writeAvailabilityCache(array $availability, $ttl) {758if (PhabricatorEnv::isReadOnly()) {759return $this;760}761762$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();763queryfx(764$this->establishConnection('w'),765'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd766WHERE id = %d',767$this->getTableName(),768phutil_json_encode($availability),769$ttl,770$this->getID());771unset($unguarded);772773return $this;774}775776777/* -( Multi-Factor Authentication )---------------------------------------- */778779780/**781* Update the flag storing this user's enrollment in multi-factor auth.782*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 this785* method synchronizes the cache by examining enrollment records. After786* updating the cache, use @{method:getIsEnrolledInMultiFactor} to check if787* the user is enrolled.788*789* This method should be called after any changes are made to a given user's790* multi-factor configuration.791*792* @return void793* @task factors794*/795public function updateMultiFactorEnrollment() {796$factors = id(new PhabricatorAuthFactorConfigQuery())797->setViewer($this)798->withUserPHIDs(array($this->getPHID()))799->withFactorProviderStatuses(800array(801PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,802PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,803))804->execute();805806$enrolled = count($factors) ? 1 : 0;807if ($enrolled !== $this->isEnrolledInMultiFactor) {808$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();809queryfx(810$this->establishConnection('w'),811'UPDATE %T SET isEnrolledInMultiFactor = %d WHERE id = %d',812$this->getTableName(),813$enrolled,814$this->getID());815unset($unguarded);816817$this->isEnrolledInMultiFactor = $enrolled;818}819}820821822/**823* Check if the user is enrolled in multi-factor authentication.824*825* Enrolled users have one or more multi-factor authentication sources826* attached to their account. For performance, this value is cached. You827* can use @{method:updateMultiFactorEnrollment} to update the cache.828*829* @return bool True if the user is enrolled.830* @task factors831*/832public function getIsEnrolledInMultiFactor() {833return $this->isEnrolledInMultiFactor;834}835836837/* -( Omnipotence )-------------------------------------------------------- */838839840/**841* Returns true if this user is omnipotent. Omnipotent users bypass all policy842* checks.843*844* @return bool True if the user bypasses policy checks.845*/846public function isOmnipotent() {847return $this->omnipotent;848}849850851/**852* Get an omnipotent user object for use in contexts where there is no acting853* user, notably daemons.854*855* @return PhabricatorUser An omnipotent user.856*/857public static function getOmnipotentUser() {858static $user = null;859if (!$user) {860$user = new PhabricatorUser();861$user->omnipotent = true;862$user->makeEphemeral();863}864return $user;865}866867868/**869* Get a scalar string identifying this user.870*871* This is similar to using the PHID, but distinguishes between omnipotent872* and public users explicitly. This allows safe construction of cache keys873* or cache buckets which do not conflate public and omnipotent users.874*875* @return string Scalar identifier.876*/877public function getCacheFragment() {878if ($this->isOmnipotent()) {879return 'u.omnipotent';880}881882$phid = $this->getPHID();883if ($phid) {884return 'u.'.$phid;885}886887return 'u.public';888}889890891/* -( Managing Handles )--------------------------------------------------- */892893894/**895* Get a @{class:PhabricatorHandleList} which benefits from this viewer's896* internal handle pool.897*898* @param list<phid> List of PHIDs to load.899* @return PhabricatorHandleList Handle list object.900* @task handle901*/902public function loadHandles(array $phids) {903if ($this->handlePool === null) {904$this->handlePool = id(new PhabricatorHandlePool())905->setViewer($this);906}907908return $this->handlePool->newHandleList($phids);909}910911912/**913* Get a @{class:PHUIHandleView} for a single handle.914*915* This benefits from the viewer's internal handle pool.916*917* @param phid PHID to render a handle for.918* @return PHUIHandleView View of the handle.919* @task handle920*/921public function renderHandle($phid) {922return $this->loadHandles(array($phid))->renderHandle($phid);923}924925926/**927* Get a @{class:PHUIHandleListView} for a list of handles.928*929* This benefits from the viewer's internal handle pool.930*931* @param list<phid> List of PHIDs to render.932* @return PHUIHandleListView View of the handles.933* @task handle934*/935public function renderHandleList(array $phids) {936return $this->loadHandles($phids)->renderList();937}938939public function attachBadgePHIDs(array $phids) {940$this->badgePHIDs = $phids;941return $this;942}943944public function getBadgePHIDs() {945return $this->assertAttached($this->badgePHIDs);946}947948/* -( CSRF )--------------------------------------------------------------- */949950951public function getCSRFToken() {952if ($this->isOmnipotent()) {953// We may end up here when called from the daemons. The omnipotent user954// has no meaningful CSRF token, so just return `null`.955return null;956}957958return $this->newCSRFEngine()959->newToken();960}961962public function validateCSRFToken($token) {963return $this->newCSRFengine()964->isValidToken($token);965}966967public function getAlternateCSRFString() {968return $this->assertAttached($this->alternateCSRFString);969}970971public function attachAlternateCSRFString($string) {972$this->alternateCSRFString = $string;973return $this;974}975976private function newCSRFEngine() {977if ($this->getPHID()) {978$vec = $this->getPHID().$this->getAccountSecret();979} else {980$vec = $this->getAlternateCSRFString();981}982983if ($this->hasSession()) {984$vec = $vec.$this->getSession()->getSessionKey();985}986987$engine = new PhabricatorAuthCSRFEngine();988989if ($this->csrfSalt === null) {990$this->csrfSalt = $engine->newSalt();991}992993$engine994->setSalt($this->csrfSalt)995->setSecret(new PhutilOpaqueEnvelope($vec));996997return $engine;998}99910001001/* -( PhabricatorPolicyInterface )----------------------------------------- */100210031004public function getCapabilities() {1005return array(1006PhabricatorPolicyCapability::CAN_VIEW,1007PhabricatorPolicyCapability::CAN_EDIT,1008);1009}10101011public function getPolicy($capability) {1012switch ($capability) {1013case PhabricatorPolicyCapability::CAN_VIEW:1014return PhabricatorPolicies::POLICY_PUBLIC;1015case PhabricatorPolicyCapability::CAN_EDIT:1016if ($this->getIsSystemAgent() || $this->getIsMailingList()) {1017return PhabricatorPolicies::POLICY_ADMIN;1018} else {1019return PhabricatorPolicies::POLICY_NOONE;1020}1021}1022}10231024public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {1025return $this->getPHID() && ($viewer->getPHID() === $this->getPHID());1026}10271028public function describeAutomaticCapability($capability) {1029switch ($capability) {1030case PhabricatorPolicyCapability::CAN_EDIT:1031return pht('Only you can edit your information.');1032default:1033return null;1034}1035}103610371038/* -( PhabricatorCustomFieldInterface )------------------------------------ */103910401041public function getCustomFieldSpecificationForRole($role) {1042return PhabricatorEnv::getEnvConfig('user.fields');1043}10441045public function getCustomFieldBaseClass() {1046return 'PhabricatorUserCustomField';1047}10481049public function getCustomFields() {1050return $this->assertAttached($this->customFields);1051}10521053public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) {1054$this->customFields = $fields;1055return $this;1056}105710581059/* -( PhabricatorDestructibleInterface )----------------------------------- */106010611062public function destroyObjectPermanently(1063PhabricatorDestructionEngine $engine) {10641065$viewer = $engine->getViewer();10661067$this->openTransaction();1068$this->delete();10691070$externals = id(new PhabricatorExternalAccountQuery())1071->setViewer($viewer)1072->withUserPHIDs(array($this->getPHID()))1073->newIterator();1074foreach ($externals as $external) {1075$engine->destroyObject($external);1076}10771078$prefs = id(new PhabricatorUserPreferencesQuery())1079->setViewer($viewer)1080->withUsers(array($this))1081->execute();1082foreach ($prefs as $pref) {1083$engine->destroyObject($pref);1084}10851086$profiles = id(new PhabricatorUserProfile())->loadAllWhere(1087'userPHID = %s',1088$this->getPHID());1089foreach ($profiles as $profile) {1090$profile->delete();1091}10921093$keys = id(new PhabricatorAuthSSHKeyQuery())1094->setViewer($viewer)1095->withObjectPHIDs(array($this->getPHID()))1096->execute();1097foreach ($keys as $key) {1098$engine->destroyObject($key);1099}11001101$emails = id(new PhabricatorUserEmail())->loadAllWhere(1102'userPHID = %s',1103$this->getPHID());1104foreach ($emails as $email) {1105$engine->destroyObject($email);1106}11071108$sessions = id(new PhabricatorAuthSession())->loadAllWhere(1109'userPHID = %s',1110$this->getPHID());1111foreach ($sessions as $session) {1112$session->delete();1113}11141115$factors = id(new PhabricatorAuthFactorConfig())->loadAllWhere(1116'userPHID = %s',1117$this->getPHID());1118foreach ($factors as $factor) {1119$factor->delete();1120}11211122$this->saveTransaction();1123}112411251126/* -( PhabricatorSSHPublicKeyInterface )----------------------------------- */112711281129public function getSSHPublicKeyManagementURI(PhabricatorUser $viewer) {1130if ($viewer->getPHID() == $this->getPHID()) {1131// If the viewer is managing their own keys, take them to the normal1132// panel.1133return '/settings/panel/ssh/';1134} else {1135// Otherwise, take them to the administrative panel for this user.1136return '/settings/user/'.$this->getUsername().'/page/ssh/';1137}1138}11391140public function getSSHKeyDefaultName() {1141return 'id_rsa_phabricator';1142}11431144public function getSSHKeyNotifyPHIDs() {1145return array(1146$this->getPHID(),1147);1148}114911501151/* -( PhabricatorApplicationTransactionInterface )------------------------- */115211531154public function getApplicationTransactionEditor() {1155return new PhabricatorUserTransactionEditor();1156}11571158public function getApplicationTransactionTemplate() {1159return new PhabricatorUserTransaction();1160}116111621163/* -( PhabricatorFulltextInterface )--------------------------------------- */116411651166public function newFulltextEngine() {1167return new PhabricatorUserFulltextEngine();1168}116911701171/* -( PhabricatorFerretInterface )----------------------------------------- */117211731174public function newFerretEngine() {1175return new PhabricatorUserFerretEngine();1176}117711781179/* -( PhabricatorConduitResultInterface )---------------------------------- */118011811182public function getFieldSpecificationsForConduit() {1183return array(1184id(new PhabricatorConduitSearchFieldSpecification())1185->setKey('username')1186->setType('string')1187->setDescription(pht("The user's username.")),1188id(new PhabricatorConduitSearchFieldSpecification())1189->setKey('realName')1190->setType('string')1191->setDescription(pht("The user's real name.")),1192id(new PhabricatorConduitSearchFieldSpecification())1193->setKey('roles')1194->setType('list<string>')1195->setDescription(pht('List of account roles.')),1196);1197}11981199public function getFieldValuesForConduit() {1200$roles = array();12011202if ($this->getIsDisabled()) {1203$roles[] = 'disabled';1204}12051206if ($this->getIsSystemAgent()) {1207$roles[] = 'bot';1208}12091210if ($this->getIsMailingList()) {1211$roles[] = 'list';1212}12131214if ($this->getIsAdmin()) {1215$roles[] = 'admin';1216}12171218if ($this->getIsEmailVerified()) {1219$roles[] = 'verified';1220}12211222if ($this->getIsApproved()) {1223$roles[] = 'approved';1224}12251226if ($this->isUserActivated()) {1227$roles[] = 'activated';1228}12291230return array(1231'username' => $this->getUsername(),1232'realName' => $this->getRealName(),1233'roles' => $roles,1234);1235}12361237public function getConduitSearchAttachments() {1238return array(1239id(new PhabricatorPeopleAvailabilitySearchEngineAttachment())1240->setAttachmentKey('availability'),1241);1242}124312441245/* -( User Cache )--------------------------------------------------------- */124612471248/**1249* @task cache1250*/1251public function attachRawCacheData(array $data) {1252$this->rawCacheData = $data + $this->rawCacheData;1253return $this;1254}12551256public function setAllowInlineCacheGeneration($allow_cache_generation) {1257$this->allowInlineCacheGeneration = $allow_cache_generation;1258return $this;1259}12601261/**1262* @task cache1263*/1264protected function requireCacheData($key) {1265if (isset($this->usableCacheData[$key])) {1266return $this->usableCacheData[$key];1267}12681269$type = PhabricatorUserCacheType::requireCacheTypeForKey($key);12701271if (isset($this->rawCacheData[$key])) {1272$raw_value = $this->rawCacheData[$key];12731274$usable_value = $type->getValueFromStorage($raw_value);1275$this->usableCacheData[$key] = $usable_value;12761277return $usable_value;1278}12791280// By default, we throw if a cache isn't available. This is consistent1281// with the standard `needX()` + `attachX()` + `getX()` interaction.1282if (!$this->allowInlineCacheGeneration) {1283throw new PhabricatorDataNotAttachedException($this);1284}12851286$user_phid = $this->getPHID();12871288// Try to read the actual cache before we generate a new value. We can1289// end up here via Conduit, which does not use normal sessions and can1290// not pick up a free cache load during session identification.1291if ($user_phid) {1292$raw_data = PhabricatorUserCache::readCaches(1293$type,1294$key,1295array($user_phid));1296if (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;1301return $usable_value;1302}1303}13041305$usable_value = $type->getDefaultValue();13061307if ($user_phid) {1308$map = $type->newValueForUsers($key, array($this));1309if (array_key_exists($user_phid, $map)) {1310$raw_value = $map[$user_phid];1311$usable_value = $type->getValueFromStorage($raw_value);13121313$this->rawCacheData[$key] = $raw_value;1314PhabricatorUserCache::writeCache(1315$type,1316$key,1317$user_phid,1318$raw_value);1319}1320}13211322$this->usableCacheData[$key] = $usable_value;13231324return $usable_value;1325}132613271328/**1329* @task cache1330*/1331public function clearCacheData($key) {1332unset($this->rawCacheData[$key]);1333unset($this->usableCacheData[$key]);1334return $this;1335}133613371338public function getCSSValue($variable_key) {1339$preference = PhabricatorAccessibilitySetting::SETTINGKEY;1340$key = $this->getUserSetting($preference);13411342$postprocessor = CelerityPostprocessor::getPostprocessor($key);1343$variables = $postprocessor->getVariables();13441345if (!isset($variables[$variable_key])) {1346throw new Exception(1347pht(1348'Unknown CSS variable "%s"!',1349$variable_key));1350}13511352return $variables[$variable_key];1353}13541355/* -( PhabricatorAuthPasswordHashInterface )------------------------------- */135613571358public function newPasswordDigest(1359PhutilOpaqueEnvelope $envelope,1360PhabricatorAuthPassword $password) {13611362// Before passwords are hashed, they are digested. The goal of digestion1363// is twofold: to reduce the length of very long passwords to something1364// reasonable; and to salt the password in case the best available hasher1365// does not include salt automatically.13661367// Users may choose arbitrarily long passwords, and attackers may try to1368// attack the system by probing it with very long passwords. When large1369// inputs are passed to hashers -- which are intentionally slow -- it1370// can result in unacceptably long runtimes. The classic attack here is1371// to try to log in with a 64MB password and see if that locks up the1372// machine for the next century. By digesting passwords to a standard1373// length first, the length of the raw input does not impact the runtime1374// of the hashing algorithm.13751376// Some hashers like bcrypt are self-salting, while other hashers are not.1377// Applying salt while digesting passwords ensures that hashes are salted1378// whether we ultimately select a self-salting hasher or not.13791380// For legacy compatibility reasons, old VCS and Account password digest1381// algorithms are significantly more complicated than necessary to achieve1382// these goals. This is because they once used a different hashing and1383// salting process. When we upgraded to the modern modular hasher1384// infrastructure, we just bolted it onto the end of the existing pipelines1385// so that upgrading didn't break all users' credentials.13861387// New implementations can (and, generally, should) safely select the1388// simple HMAC SHA256 digest at the bottom of the function, which does1389// everything that a digest callback should without any needless legacy1390// baggage on top.13911392if ($password->getLegacyDigestFormat() == 'v1') {1393switch ($password->getPasswordType()) {1394case 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 digest1397// algorithm once hashing was upgraded to include bcrypt.1398$digest = $envelope->openEnvelope();1399$salt = $this->getPHID();1400for ($ii = 0; $ii < 1000; $ii++) {1401$digest = PhabricatorHash::weakDigest($digest, $salt);1402}1403return new PhutilOpaqueEnvelope($digest);1404case PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT:1405// Account passwords previously used this weird mess of salt and did1406// not digest the input to a standard length.14071408// Beyond this being a weird special case, there are two actual1409// problems with this, although neither are particularly severe:14101411// First, because we do not normalize the length of passwords, this1412// algorithm may make us vulnerable to DOS attacks where an attacker1413// attempts to use a very long input to slow down hashers.14141415// Second, because the username is part of the hash algorithm,1416// renaming a user breaks their password. This isn't a huge deal but1417// it's pretty silly. There's no security justification for this1418// behavior, I just didn't think about the implication when I wrote1419// it originally.14201421$parts = array(1422$this->getUsername(),1423$envelope->openEnvelope(),1424$this->getPHID(),1425$password->getPasswordSalt(),1426);14271428return new PhutilOpaqueEnvelope(implode('', $parts));1429}1430}14311432// For passwords which do not have some crazy legacy reason to use some1433// other digest algorithm, HMAC SHA256 is an excellent choice. It satisfies1434// the digest requirements and is simple.14351436$digest = PhabricatorHash::digestHMACSHA256(1437$envelope->openEnvelope(),1438$password->getPasswordSalt());14391440return new PhutilOpaqueEnvelope($digest);1441}14421443public function newPasswordBlocklist(1444PhabricatorUser $viewer,1445PhabricatorAuthPasswordEngine $engine) {14461447$list = array();1448$list[] = $this->getUsername();1449$list[] = $this->getRealName();14501451$emails = id(new PhabricatorUserEmail())->loadAllWhere(1452'userPHID = %s',1453$this->getPHID());1454foreach ($emails as $email) {1455$list[] = $email->getAddress();1456}14571458return $list;1459}146014611462}146314641465