Path: blob/master/src/applications/people/query/PhabricatorPeopleQuery.php
12256 views
<?php12final class PhabricatorPeopleQuery3extends PhabricatorCursorPagedPolicyAwareQuery {45private $usernames;6private $realnames;7private $emails;8private $phids;9private $ids;10private $dateCreatedAfter;11private $dateCreatedBefore;12private $isAdmin;13private $isSystemAgent;14private $isMailingList;15private $isDisabled;16private $isApproved;17private $nameLike;18private $nameTokens;19private $namePrefixes;20private $isEnrolledInMultiFactor;2122private $needPrimaryEmail;23private $needProfile;24private $needProfileImage;25private $needAvailability;26private $needBadgeAwards;27private $cacheKeys = array();2829public function withIDs(array $ids) {30$this->ids = $ids;31return $this;32}3334public function withPHIDs(array $phids) {35$this->phids = $phids;36return $this;37}3839public function withEmails(array $emails) {40$this->emails = $emails;41return $this;42}4344public function withRealnames(array $realnames) {45$this->realnames = $realnames;46return $this;47}4849public function withUsernames(array $usernames) {50$this->usernames = $usernames;51return $this;52}5354public function withDateCreatedBefore($date_created_before) {55$this->dateCreatedBefore = $date_created_before;56return $this;57}5859public function withDateCreatedAfter($date_created_after) {60$this->dateCreatedAfter = $date_created_after;61return $this;62}6364public function withIsAdmin($admin) {65$this->isAdmin = $admin;66return $this;67}6869public function withIsSystemAgent($system_agent) {70$this->isSystemAgent = $system_agent;71return $this;72}7374public function withIsMailingList($mailing_list) {75$this->isMailingList = $mailing_list;76return $this;77}7879public function withIsDisabled($disabled) {80$this->isDisabled = $disabled;81return $this;82}8384public function withIsApproved($approved) {85$this->isApproved = $approved;86return $this;87}8889public function withNameLike($like) {90$this->nameLike = $like;91return $this;92}9394public function withNameTokens(array $tokens) {95$this->nameTokens = array_values($tokens);96return $this;97}9899public function withNamePrefixes(array $prefixes) {100$this->namePrefixes = $prefixes;101return $this;102}103104public function withIsEnrolledInMultiFactor($enrolled) {105$this->isEnrolledInMultiFactor = $enrolled;106return $this;107}108109public function needPrimaryEmail($need) {110$this->needPrimaryEmail = $need;111return $this;112}113114public function needProfile($need) {115$this->needProfile = $need;116return $this;117}118119public function needProfileImage($need) {120$cache_key = PhabricatorUserProfileImageCacheType::KEY_URI;121122if ($need) {123$this->cacheKeys[$cache_key] = true;124} else {125unset($this->cacheKeys[$cache_key]);126}127128return $this;129}130131public function needAvailability($need) {132$this->needAvailability = $need;133return $this;134}135136public function needUserSettings($need) {137$cache_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;138139if ($need) {140$this->cacheKeys[$cache_key] = true;141} else {142unset($this->cacheKeys[$cache_key]);143}144145return $this;146}147148public function needBadgeAwards($need) {149$cache_key = PhabricatorUserBadgesCacheType::KEY_BADGES;150151if ($need) {152$this->cacheKeys[$cache_key] = true;153} else {154unset($this->cacheKeys[$cache_key]);155}156157return $this;158}159160public function newResultObject() {161return new PhabricatorUser();162}163164protected function didFilterPage(array $users) {165if ($this->needProfile) {166$user_list = mpull($users, null, 'getPHID');167$profiles = new PhabricatorUserProfile();168$profiles = $profiles->loadAllWhere(169'userPHID IN (%Ls)',170array_keys($user_list));171172$profiles = mpull($profiles, null, 'getUserPHID');173foreach ($user_list as $user_phid => $user) {174$profile = idx($profiles, $user_phid);175176if (!$profile) {177$profile = PhabricatorUserProfile::initializeNewProfile($user);178}179180$user->attachUserProfile($profile);181}182}183184if ($this->needAvailability) {185$rebuild = array();186foreach ($users as $user) {187$cache = $user->getAvailabilityCache();188if ($cache !== null) {189$user->attachAvailability($cache);190} else {191$rebuild[] = $user;192}193}194195if ($rebuild) {196$this->rebuildAvailabilityCache($rebuild);197}198}199200$this->fillUserCaches($users);201202return $users;203}204205protected function shouldGroupQueryResultRows() {206if ($this->nameTokens) {207return true;208}209210return parent::shouldGroupQueryResultRows();211}212213protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {214$joins = parent::buildJoinClauseParts($conn);215216if ($this->emails) {217$email_table = new PhabricatorUserEmail();218$joins[] = qsprintf(219$conn,220'JOIN %T email ON email.userPHID = user.PHID',221$email_table->getTableName());222}223224if ($this->nameTokens) {225foreach ($this->nameTokens as $key => $token) {226$token_table = 'token_'.$key;227$joins[] = qsprintf(228$conn,229'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>',230PhabricatorUser::NAMETOKEN_TABLE,231$token_table,232$token_table,233$token_table,234$token);235}236}237238return $joins;239}240241protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {242$where = parent::buildWhereClauseParts($conn);243244if ($this->usernames !== null) {245$where[] = qsprintf(246$conn,247'user.userName IN (%Ls)',248$this->usernames);249}250251if ($this->namePrefixes) {252$parts = array();253foreach ($this->namePrefixes as $name_prefix) {254$parts[] = qsprintf(255$conn,256'user.username LIKE %>',257$name_prefix);258}259$where[] = qsprintf($conn, '%LO', $parts);260}261262if ($this->emails !== null) {263$where[] = qsprintf(264$conn,265'email.address IN (%Ls)',266$this->emails);267}268269if ($this->realnames !== null) {270$where[] = qsprintf(271$conn,272'user.realName IN (%Ls)',273$this->realnames);274}275276if ($this->phids !== null) {277$where[] = qsprintf(278$conn,279'user.phid IN (%Ls)',280$this->phids);281}282283if ($this->ids !== null) {284$where[] = qsprintf(285$conn,286'user.id IN (%Ld)',287$this->ids);288}289290if ($this->dateCreatedAfter) {291$where[] = qsprintf(292$conn,293'user.dateCreated >= %d',294$this->dateCreatedAfter);295}296297if ($this->dateCreatedBefore) {298$where[] = qsprintf(299$conn,300'user.dateCreated <= %d',301$this->dateCreatedBefore);302}303304if ($this->isAdmin !== null) {305$where[] = qsprintf(306$conn,307'user.isAdmin = %d',308(int)$this->isAdmin);309}310311if ($this->isDisabled !== null) {312$where[] = qsprintf(313$conn,314'user.isDisabled = %d',315(int)$this->isDisabled);316}317318if ($this->isApproved !== null) {319$where[] = qsprintf(320$conn,321'user.isApproved = %d',322(int)$this->isApproved);323}324325if ($this->isSystemAgent !== null) {326$where[] = qsprintf(327$conn,328'user.isSystemAgent = %d',329(int)$this->isSystemAgent);330}331332if ($this->isMailingList !== null) {333$where[] = qsprintf(334$conn,335'user.isMailingList = %d',336(int)$this->isMailingList);337}338339if ($this->nameLike !== null) {340$where[] = qsprintf(341$conn,342'user.username LIKE %~ OR user.realname LIKE %~',343$this->nameLike,344$this->nameLike);345}346347if ($this->isEnrolledInMultiFactor !== null) {348$where[] = qsprintf(349$conn,350'user.isEnrolledInMultiFactor = %d',351(int)$this->isEnrolledInMultiFactor);352}353354return $where;355}356357protected function getPrimaryTableAlias() {358return 'user';359}360361public function getQueryApplicationClass() {362return 'PhabricatorPeopleApplication';363}364365public function getOrderableColumns() {366return parent::getOrderableColumns() + array(367'username' => array(368'table' => 'user',369'column' => 'username',370'type' => 'string',371'reverse' => true,372'unique' => true,373),374);375}376377protected function newPagingMapFromPartialObject($object) {378return array(379'id' => (int)$object->getID(),380'username' => $object->getUsername(),381);382}383384private function rebuildAvailabilityCache(array $rebuild) {385$rebuild = mpull($rebuild, null, 'getPHID');386387// Limit the window we look at because far-future events are largely388// irrelevant and this makes the cache cheaper to build and allows it to389// self-heal over time.390$min_range = PhabricatorTime::getNow();391$max_range = $min_range + phutil_units('72 hours in seconds');392393// NOTE: We don't need to generate ghosts here, because we only care if394// the user is attending, and you can't attend a ghost event: RSVP'ing395// to it creates a real event.396397$events = id(new PhabricatorCalendarEventQuery())398->setViewer(PhabricatorUser::getOmnipotentUser())399->withInvitedPHIDs(array_keys($rebuild))400->withIsCancelled(false)401->withDateRange($min_range, $max_range)402->execute();403404// Group all the events by invited user. Only examine events that users405// are actually attending.406$map = array();407$invitee_map = array();408foreach ($events as $event) {409foreach ($event->getInvitees() as $invitee) {410if (!$invitee->isAttending()) {411continue;412}413414// If the user is set to "Available" for this event, don't consider it415// when computing their away status.416if (!$invitee->getDisplayAvailability($event)) {417continue;418}419420$invitee_phid = $invitee->getInviteePHID();421if (!isset($rebuild[$invitee_phid])) {422continue;423}424425$map[$invitee_phid][] = $event;426427$event_phid = $event->getPHID();428$invitee_map[$invitee_phid][$event_phid] = $invitee;429}430}431432// We need to load these users' timezone settings to figure out their433// availability if they're attending all-day events.434$this->needUserSettings(true);435$this->fillUserCaches($rebuild);436437foreach ($rebuild as $phid => $user) {438$events = idx($map, $phid, array());439440// We loaded events with the omnipotent user, but want to shift them441// into the user's timezone before building the cache because they will442// be unavailable during their own local day.443foreach ($events as $event) {444$event->applyViewerTimezone($user);445}446447$cursor = $min_range;448$next_event = null;449if ($events) {450// Find the next time when the user has no meetings. If we move forward451// because of an event, we check again for events after that one ends.452while (true) {453foreach ($events as $event) {454$from = $event->getStartDateTimeEpochForCache();455$to = $event->getEndDateTimeEpochForCache();456if (($from <= $cursor) && ($to > $cursor)) {457$cursor = $to;458if (!$next_event) {459$next_event = $event;460}461continue 2;462}463}464break;465}466}467468if ($cursor > $min_range) {469$invitee = $invitee_map[$phid][$next_event->getPHID()];470$availability_type = $invitee->getDisplayAvailability($next_event);471$availability = array(472'until' => $cursor,473'eventPHID' => $next_event->getPHID(),474'availability' => $availability_type,475);476477// We only cache this availability until the end of the current event,478// since the event PHID (and possibly the availability type) are only479// valid for that long.480481// NOTE: This doesn't handle overlapping events with the greatest482// possible care. In theory, if you're attending multiple events483// simultaneously we should accommodate that. However, it's complex484// to compute, rare, and probably not confusing most of the time.485486$availability_ttl = $next_event->getEndDateTimeEpochForCache();487} else {488$availability = array(489'until' => null,490'eventPHID' => null,491'availability' => null,492);493494// Cache that the user is available until the next event they are495// invited to starts.496$availability_ttl = $max_range;497foreach ($events as $event) {498$from = $event->getStartDateTimeEpochForCache();499if ($from > $cursor) {500$availability_ttl = min($from, $availability_ttl);501}502}503}504505// Never TTL the cache to longer than the maximum range we examined.506$availability_ttl = min($availability_ttl, $max_range);507508$user->writeAvailabilityCache($availability, $availability_ttl);509$user->attachAvailability($availability);510}511}512513private function fillUserCaches(array $users) {514if (!$this->cacheKeys) {515return;516}517518$user_map = mpull($users, null, 'getPHID');519$keys = array_keys($this->cacheKeys);520521$hashes = array();522foreach ($keys as $key) {523$hashes[] = PhabricatorHash::digestForIndex($key);524}525526$types = PhabricatorUserCacheType::getAllCacheTypes();527528// First, pull any available caches. If we wanted to be particularly clever529// we could do this with JOINs in the main query.530531$cache_table = new PhabricatorUserCache();532$cache_conn = $cache_table->establishConnection('r');533534$cache_data = queryfx_all(535$cache_conn,536'SELECT cacheKey, userPHID, cacheData, cacheType FROM %T537WHERE cacheIndex IN (%Ls) AND userPHID IN (%Ls)',538$cache_table->getTableName(),539$hashes,540array_keys($user_map));541542$skip_validation = array();543544// After we read caches from the database, discard any which have data that545// invalid or out of date. This allows cache types to implement TTLs or546// versions instead of or in addition to explicit cache clears.547foreach ($cache_data as $row_key => $row) {548$cache_type = $row['cacheType'];549550if (isset($skip_validation[$cache_type])) {551continue;552}553554if (empty($types[$cache_type])) {555unset($cache_data[$row_key]);556continue;557}558559$type = $types[$cache_type];560if (!$type->shouldValidateRawCacheData()) {561$skip_validation[$cache_type] = true;562continue;563}564565$user = $user_map[$row['userPHID']];566$raw_data = $row['cacheData'];567if (!$type->isRawCacheDataValid($user, $row['cacheKey'], $raw_data)) {568unset($cache_data[$row_key]);569continue;570}571}572573$need = array();574575$cache_data = igroup($cache_data, 'userPHID');576foreach ($user_map as $user_phid => $user) {577$raw_rows = idx($cache_data, $user_phid, array());578$raw_data = ipull($raw_rows, 'cacheData', 'cacheKey');579580foreach ($keys as $key) {581if (isset($raw_data[$key]) || array_key_exists($key, $raw_data)) {582continue;583}584$need[$key][$user_phid] = $user;585}586587$user->attachRawCacheData($raw_data);588}589590// If we missed any cache values, bulk-construct them now. This is591// usually much cheaper than generating them on-demand for each user592// record.593594if (!$need) {595return;596}597598$writes = array();599foreach ($need as $cache_key => $need_users) {600$type = PhabricatorUserCacheType::getCacheTypeForKey($cache_key);601if (!$type) {602continue;603}604605$data = $type->newValueForUsers($cache_key, $need_users);606607foreach ($data as $user_phid => $raw_value) {608$data[$user_phid] = $raw_value;609$writes[] = array(610'userPHID' => $user_phid,611'key' => $cache_key,612'type' => $type,613'value' => $raw_value,614);615}616617foreach ($need_users as $user_phid => $user) {618if (isset($data[$user_phid]) || array_key_exists($user_phid, $data)) {619$user->attachRawCacheData(620array(621$cache_key => $data[$user_phid],622));623}624}625}626627PhabricatorUserCache::writeCaches($writes);628}629}630631632