3 final class PhabricatorPeopleQuery
4 extends PhabricatorCursorPagedPolicyAwareQuery
{
11 private $dateCreatedAfter;
12 private $dateCreatedBefore;
14 private $isSystemAgent;
15 private $isMailingList;
20 private $namePrefixes;
21 private $isEnrolledInMultiFactor;
23 private $needPrimaryEmail;
25 private $needProfileImage;
26 private $needAvailability;
27 private $needBadgeAwards;
28 private $cacheKeys = array();
30 public function withIDs(array $ids) {
35 public function withPHIDs(array $phids) {
36 $this->phids
= $phids;
40 public function withEmails(array $emails) {
41 $this->emails
= $emails;
45 public function withRealnames(array $realnames) {
46 $this->realnames
= $realnames;
50 public function withUsernames(array $usernames) {
51 $this->usernames
= $usernames;
55 public function withDateCreatedBefore($date_created_before) {
56 $this->dateCreatedBefore
= $date_created_before;
60 public function withDateCreatedAfter($date_created_after) {
61 $this->dateCreatedAfter
= $date_created_after;
65 public function withIsAdmin($admin) {
66 $this->isAdmin
= $admin;
70 public function withIsSystemAgent($system_agent) {
71 $this->isSystemAgent
= $system_agent;
75 public function withIsMailingList($mailing_list) {
76 $this->isMailingList
= $mailing_list;
80 public function withIsDisabled($disabled) {
81 $this->isDisabled
= $disabled;
85 public function withIsApproved($approved) {
86 $this->isApproved
= $approved;
90 public function withNameLike($like) {
91 $this->nameLike
= $like;
95 public function withNameTokens(array $tokens) {
96 $this->nameTokens
= array_values($tokens);
100 public function withNamePrefixes(array $prefixes) {
101 $this->namePrefixes
= $prefixes;
105 public function withIsEnrolledInMultiFactor($enrolled) {
106 $this->isEnrolledInMultiFactor
= $enrolled;
110 public function needPrimaryEmail($need) {
111 $this->needPrimaryEmail
= $need;
115 public function needProfile($need) {
116 $this->needProfile
= $need;
120 public function needProfileImage($need) {
121 $cache_key = PhabricatorUserProfileImageCacheType
::KEY_URI
;
124 $this->cacheKeys
[$cache_key] = true;
126 unset($this->cacheKeys
[$cache_key]);
132 public function needAvailability($need) {
133 $this->needAvailability
= $need;
137 public function needUserSettings($need) {
138 $cache_key = PhabricatorUserPreferencesCacheType
::KEY_PREFERENCES
;
141 $this->cacheKeys
[$cache_key] = true;
143 unset($this->cacheKeys
[$cache_key]);
149 public function needBadgeAwards($need) {
150 $cache_key = PhabricatorUserBadgesCacheType
::KEY_BADGES
;
153 $this->cacheKeys
[$cache_key] = true;
155 unset($this->cacheKeys
[$cache_key]);
161 public function newResultObject() {
162 return new PhabricatorUser();
165 protected function didFilterPage(array $users) {
166 if ($this->needProfile
) {
167 $user_list = mpull($users, null, 'getPHID');
168 $profiles = new PhabricatorUserProfile();
169 $profiles = $profiles->loadAllWhere(
171 array_keys($user_list));
173 $profiles = mpull($profiles, null, 'getUserPHID');
174 foreach ($user_list as $user_phid => $user) {
175 $profile = idx($profiles, $user_phid);
178 $profile = PhabricatorUserProfile
::initializeNewProfile($user);
181 $user->attachUserProfile($profile);
185 if ($this->needAvailability
) {
187 foreach ($users as $user) {
188 $cache = $user->getAvailabilityCache();
189 if ($cache !== null) {
190 $user->attachAvailability($cache);
197 $this->rebuildAvailabilityCache($rebuild);
201 $this->fillUserCaches($users);
206 protected function shouldGroupQueryResultRows() {
207 if ($this->nameTokens
) {
211 return parent
::shouldGroupQueryResultRows();
214 protected function buildJoinClauseParts(AphrontDatabaseConnection
$conn) {
215 $joins = parent
::buildJoinClauseParts($conn);
218 $email_table = new PhabricatorUserEmail();
221 'JOIN %T email ON email.userPHID = user.PHID',
222 $email_table->getTableName());
225 if ($this->nameTokens
) {
226 foreach ($this->nameTokens
as $key => $token) {
227 $token_table = 'token_'.$key;
230 'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>',
231 PhabricatorUser
::NAMETOKEN_TABLE
,
242 protected function buildWhereClauseParts(AphrontDatabaseConnection
$conn) {
243 $where = parent
::buildWhereClauseParts($conn);
245 if ($this->usernames
!== null) {
248 'user.userName IN (%Ls)',
252 if ($this->namePrefixes
) {
254 foreach ($this->namePrefixes
as $name_prefix) {
257 'user.username LIKE %>',
260 $where[] = qsprintf($conn, '%LO', $parts);
263 if ($this->emails
!== null) {
266 'email.address IN (%Ls)',
270 if ($this->realnames
!== null) {
273 'user.realName IN (%Ls)',
277 if ($this->phids
!== null) {
280 'user.phid IN (%Ls)',
284 if ($this->ids
!== null) {
291 if ($this->dateCreatedAfter
) {
294 'user.dateCreated >= %d',
295 $this->dateCreatedAfter
);
298 if ($this->dateCreatedBefore
) {
301 'user.dateCreated <= %d',
302 $this->dateCreatedBefore
);
305 if ($this->isAdmin
!== null) {
309 (int)$this->isAdmin
);
312 if ($this->isDisabled
!== null) {
315 'user.isDisabled = %d',
316 (int)$this->isDisabled
);
319 if ($this->isApproved
!== null) {
322 'user.isApproved = %d',
323 (int)$this->isApproved
);
326 if ($this->isSystemAgent
!== null) {
329 'user.isSystemAgent = %d',
330 (int)$this->isSystemAgent
);
333 if ($this->isMailingList
!== null) {
336 'user.isMailingList = %d',
337 (int)$this->isMailingList
);
340 if ($this->nameLike
!== null) {
343 'user.username LIKE %~ OR user.realname LIKE %~',
348 if ($this->isEnrolledInMultiFactor
!== null) {
351 'user.isEnrolledInMultiFactor = %d',
352 (int)$this->isEnrolledInMultiFactor
);
358 protected function getPrimaryTableAlias() {
362 public function getQueryApplicationClass() {
363 return 'PhabricatorPeopleApplication';
366 public function getOrderableColumns() {
367 return parent
::getOrderableColumns() +
array(
370 'column' => 'username',
378 protected function newPagingMapFromPartialObject($object) {
380 'id' => (int)$object->getID(),
381 'username' => $object->getUsername(),
385 private function rebuildAvailabilityCache(array $rebuild) {
386 $rebuild = mpull($rebuild, null, 'getPHID');
388 // Limit the window we look at because far-future events are largely
389 // irrelevant and this makes the cache cheaper to build and allows it to
390 // self-heal over time.
391 $min_range = PhabricatorTime
::getNow();
392 $max_range = $min_range +
phutil_units('72 hours in seconds');
394 // NOTE: We don't need to generate ghosts here, because we only care if
395 // the user is attending, and you can't attend a ghost event: RSVP'ing
396 // to it creates a real event.
398 $events = id(new PhabricatorCalendarEventQuery())
399 ->setViewer(PhabricatorUser
::getOmnipotentUser())
400 ->withInvitedPHIDs(array_keys($rebuild))
401 ->withIsCancelled(false)
402 ->withDateRange($min_range, $max_range)
405 // Group all the events by invited user. Only examine events that users
406 // are actually attending.
408 $invitee_map = array();
409 foreach ($events as $event) {
410 foreach ($event->getInvitees() as $invitee) {
411 if (!$invitee->isAttending()) {
415 // If the user is set to "Available" for this event, don't consider it
416 // when computing their away status.
417 if (!$invitee->getDisplayAvailability($event)) {
421 $invitee_phid = $invitee->getInviteePHID();
422 if (!isset($rebuild[$invitee_phid])) {
426 $map[$invitee_phid][] = $event;
428 $event_phid = $event->getPHID();
429 $invitee_map[$invitee_phid][$event_phid] = $invitee;
433 // We need to load these users' timezone settings to figure out their
434 // availability if they're attending all-day events.
435 $this->needUserSettings(true);
436 $this->fillUserCaches($rebuild);
438 foreach ($rebuild as $phid => $user) {
439 $events = idx($map, $phid, array());
441 // We loaded events with the omnipotent user, but want to shift them
442 // into the user's timezone before building the cache because they will
443 // be unavailable during their own local day.
444 foreach ($events as $event) {
445 $event->applyViewerTimezone($user);
448 $cursor = $min_range;
451 // Find the next time when the user has no meetings. If we move forward
452 // because of an event, we check again for events after that one ends.
454 foreach ($events as $event) {
455 $from = $event->getStartDateTimeEpochForCache();
456 $to = $event->getEndDateTimeEpochForCache();
457 if (($from <= $cursor) && ($to > $cursor)) {
460 $next_event = $event;
469 if ($cursor > $min_range) {
470 $invitee = $invitee_map[$phid][$next_event->getPHID()];
471 $availability_type = $invitee->getDisplayAvailability($next_event);
472 $availability = array(
474 'eventPHID' => $next_event->getPHID(),
475 'availability' => $availability_type,
478 // We only cache this availability until the end of the current event,
479 // since the event PHID (and possibly the availability type) are only
480 // valid for that long.
482 // NOTE: This doesn't handle overlapping events with the greatest
483 // possible care. In theory, if you're attending multiple events
484 // simultaneously we should accommodate that. However, it's complex
485 // to compute, rare, and probably not confusing most of the time.
487 $availability_ttl = $next_event->getEndDateTimeEpochForCache();
489 $availability = array(
492 'availability' => null,
495 // Cache that the user is available until the next event they are
496 // invited to starts.
497 $availability_ttl = $max_range;
498 foreach ($events as $event) {
499 $from = $event->getStartDateTimeEpochForCache();
500 if ($from > $cursor) {
501 $availability_ttl = min($from, $availability_ttl);
506 // Never TTL the cache to longer than the maximum range we examined.
507 $availability_ttl = min($availability_ttl, $max_range);
509 $user->writeAvailabilityCache($availability, $availability_ttl);
510 $user->attachAvailability($availability);
514 private function fillUserCaches(array $users) {
515 if (!$this->cacheKeys
) {
519 $user_map = mpull($users, null, 'getPHID');
520 $keys = array_keys($this->cacheKeys
);
523 foreach ($keys as $key) {
524 $hashes[] = PhabricatorHash
::digestForIndex($key);
527 $types = PhabricatorUserCacheType
::getAllCacheTypes();
529 // First, pull any available caches. If we wanted to be particularly clever
530 // we could do this with JOINs in the main query.
532 $cache_table = new PhabricatorUserCache();
533 $cache_conn = $cache_table->establishConnection('r');
535 $cache_data = queryfx_all(
537 'SELECT cacheKey, userPHID, cacheData, cacheType FROM %T
538 WHERE cacheIndex IN (%Ls) AND userPHID IN (%Ls)',
539 $cache_table->getTableName(),
541 array_keys($user_map));
543 $skip_validation = array();
545 // After we read caches from the database, discard any which have data that
546 // invalid or out of date. This allows cache types to implement TTLs or
547 // versions instead of or in addition to explicit cache clears.
548 foreach ($cache_data as $row_key => $row) {
549 $cache_type = $row['cacheType'];
551 if (isset($skip_validation[$cache_type])) {
555 if (empty($types[$cache_type])) {
556 unset($cache_data[$row_key]);
560 $type = $types[$cache_type];
561 if (!$type->shouldValidateRawCacheData()) {
562 $skip_validation[$cache_type] = true;
566 $user = $user_map[$row['userPHID']];
567 $raw_data = $row['cacheData'];
568 if (!$type->isRawCacheDataValid($user, $row['cacheKey'], $raw_data)) {
569 unset($cache_data[$row_key]);
576 $cache_data = igroup($cache_data, 'userPHID');
577 foreach ($user_map as $user_phid => $user) {
578 $raw_rows = idx($cache_data, $user_phid, array());
579 $raw_data = ipull($raw_rows, 'cacheData', 'cacheKey');
581 foreach ($keys as $key) {
582 if (isset($raw_data[$key]) ||
array_key_exists($key, $raw_data)) {
585 $need[$key][$user_phid] = $user;
588 $user->attachRawCacheData($raw_data);
591 // If we missed any cache values, bulk-construct them now. This is
592 // usually much cheaper than generating them on-demand for each user
600 foreach ($need as $cache_key => $need_users) {
601 $type = PhabricatorUserCacheType
::getCacheTypeForKey($cache_key);
606 $data = $type->newValueForUsers($cache_key, $need_users);
608 foreach ($data as $user_phid => $raw_value) {
609 $data[$user_phid] = $raw_value;
611 'userPHID' => $user_phid,
614 'value' => $raw_value,
618 foreach ($need_users as $user_phid => $user) {
619 if (isset($data[$user_phid]) ||
array_key_exists($user_phid, $data)) {
620 $user->attachRawCacheData(
622 $cache_key => $data[$user_phid],
628 PhabricatorUserCache
::writeCaches($writes);