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 loadPage() {
166 return $this->loadStandardPage($this->newResultObject());
169 protected function didFilterPage(array $users) {
170 if ($this->needProfile
) {
171 $user_list = mpull($users, null, 'getPHID');
172 $profiles = new PhabricatorUserProfile();
173 $profiles = $profiles->loadAllWhere(
175 array_keys($user_list));
177 $profiles = mpull($profiles, null, 'getUserPHID');
178 foreach ($user_list as $user_phid => $user) {
179 $profile = idx($profiles, $user_phid);
182 $profile = PhabricatorUserProfile
::initializeNewProfile($user);
185 $user->attachUserProfile($profile);
189 if ($this->needAvailability
) {
191 foreach ($users as $user) {
192 $cache = $user->getAvailabilityCache();
193 if ($cache !== null) {
194 $user->attachAvailability($cache);
201 $this->rebuildAvailabilityCache($rebuild);
205 $this->fillUserCaches($users);
210 protected function shouldGroupQueryResultRows() {
211 if ($this->nameTokens
) {
215 return parent
::shouldGroupQueryResultRows();
218 protected function buildJoinClauseParts(AphrontDatabaseConnection
$conn) {
219 $joins = parent
::buildJoinClauseParts($conn);
222 $email_table = new PhabricatorUserEmail();
225 'JOIN %T email ON email.userPHID = user.PHID',
226 $email_table->getTableName());
229 if ($this->nameTokens
) {
230 foreach ($this->nameTokens
as $key => $token) {
231 $token_table = 'token_'.$key;
234 'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>',
235 PhabricatorUser
::NAMETOKEN_TABLE
,
246 protected function buildWhereClauseParts(AphrontDatabaseConnection
$conn) {
247 $where = parent
::buildWhereClauseParts($conn);
249 if ($this->usernames
!== null) {
252 'user.userName IN (%Ls)',
256 if ($this->namePrefixes
) {
258 foreach ($this->namePrefixes
as $name_prefix) {
261 'user.username LIKE %>',
264 $where[] = qsprintf($conn, '%LO', $parts);
267 if ($this->emails
!== null) {
270 'email.address IN (%Ls)',
274 if ($this->realnames
!== null) {
277 'user.realName IN (%Ls)',
281 if ($this->phids
!== null) {
284 'user.phid IN (%Ls)',
288 if ($this->ids
!== null) {
295 if ($this->dateCreatedAfter
) {
298 'user.dateCreated >= %d',
299 $this->dateCreatedAfter
);
302 if ($this->dateCreatedBefore
) {
305 'user.dateCreated <= %d',
306 $this->dateCreatedBefore
);
309 if ($this->isAdmin
!== null) {
313 (int)$this->isAdmin
);
316 if ($this->isDisabled
!== null) {
319 'user.isDisabled = %d',
320 (int)$this->isDisabled
);
323 if ($this->isApproved
!== null) {
326 'user.isApproved = %d',
327 (int)$this->isApproved
);
330 if ($this->isSystemAgent
!== null) {
333 'user.isSystemAgent = %d',
334 (int)$this->isSystemAgent
);
337 if ($this->isMailingList
!== null) {
340 'user.isMailingList = %d',
341 (int)$this->isMailingList
);
344 if ($this->nameLike
!== null) {
347 'user.username LIKE %~ OR user.realname LIKE %~',
352 if ($this->isEnrolledInMultiFactor
!== null) {
355 'user.isEnrolledInMultiFactor = %d',
356 (int)$this->isEnrolledInMultiFactor
);
362 protected function getPrimaryTableAlias() {
366 public function getQueryApplicationClass() {
367 return 'PhabricatorPeopleApplication';
370 public function getOrderableColumns() {
371 return parent
::getOrderableColumns() +
array(
374 'column' => 'username',
382 protected function newPagingMapFromPartialObject($object) {
384 'id' => (int)$object->getID(),
385 'username' => $object->getUsername(),
389 private function rebuildAvailabilityCache(array $rebuild) {
390 $rebuild = mpull($rebuild, null, 'getPHID');
392 // Limit the window we look at because far-future events are largely
393 // irrelevant and this makes the cache cheaper to build and allows it to
394 // self-heal over time.
395 $min_range = PhabricatorTime
::getNow();
396 $max_range = $min_range +
phutil_units('72 hours in seconds');
398 // NOTE: We don't need to generate ghosts here, because we only care if
399 // the user is attending, and you can't attend a ghost event: RSVP'ing
400 // to it creates a real event.
402 $events = id(new PhabricatorCalendarEventQuery())
403 ->setViewer(PhabricatorUser
::getOmnipotentUser())
404 ->withInvitedPHIDs(array_keys($rebuild))
405 ->withIsCancelled(false)
406 ->withDateRange($min_range, $max_range)
409 // Group all the events by invited user. Only examine events that users
410 // are actually attending.
412 $invitee_map = array();
413 foreach ($events as $event) {
414 foreach ($event->getInvitees() as $invitee) {
415 if (!$invitee->isAttending()) {
419 // If the user is set to "Available" for this event, don't consider it
420 // when computing their away status.
421 if (!$invitee->getDisplayAvailability($event)) {
425 $invitee_phid = $invitee->getInviteePHID();
426 if (!isset($rebuild[$invitee_phid])) {
430 $map[$invitee_phid][] = $event;
432 $event_phid = $event->getPHID();
433 $invitee_map[$invitee_phid][$event_phid] = $invitee;
437 // We need to load these users' timezone settings to figure out their
438 // availability if they're attending all-day events.
439 $this->needUserSettings(true);
440 $this->fillUserCaches($rebuild);
442 foreach ($rebuild as $phid => $user) {
443 $events = idx($map, $phid, array());
445 // We loaded events with the omnipotent user, but want to shift them
446 // into the user's timezone before building the cache because they will
447 // be unavailable during their own local day.
448 foreach ($events as $event) {
449 $event->applyViewerTimezone($user);
452 $cursor = $min_range;
455 // Find the next time when the user has no meetings. If we move forward
456 // because of an event, we check again for events after that one ends.
458 foreach ($events as $event) {
459 $from = $event->getStartDateTimeEpochForCache();
460 $to = $event->getEndDateTimeEpochForCache();
461 if (($from <= $cursor) && ($to > $cursor)) {
464 $next_event = $event;
473 if ($cursor > $min_range) {
474 $invitee = $invitee_map[$phid][$next_event->getPHID()];
475 $availability_type = $invitee->getDisplayAvailability($next_event);
476 $availability = array(
478 'eventPHID' => $next_event->getPHID(),
479 'availability' => $availability_type,
482 // We only cache this availability until the end of the current event,
483 // since the event PHID (and possibly the availability type) are only
484 // valid for that long.
486 // NOTE: This doesn't handle overlapping events with the greatest
487 // possible care. In theory, if you're attending multiple events
488 // simultaneously we should accommodate that. However, it's complex
489 // to compute, rare, and probably not confusing most of the time.
491 $availability_ttl = $next_event->getEndDateTimeEpochForCache();
493 $availability = array(
496 'availability' => null,
499 // Cache that the user is available until the next event they are
500 // invited to starts.
501 $availability_ttl = $max_range;
502 foreach ($events as $event) {
503 $from = $event->getStartDateTimeEpochForCache();
504 if ($from > $cursor) {
505 $availability_ttl = min($from, $availability_ttl);
510 // Never TTL the cache to longer than the maximum range we examined.
511 $availability_ttl = min($availability_ttl, $max_range);
513 $user->writeAvailabilityCache($availability, $availability_ttl);
514 $user->attachAvailability($availability);
518 private function fillUserCaches(array $users) {
519 if (!$this->cacheKeys
) {
523 $user_map = mpull($users, null, 'getPHID');
524 $keys = array_keys($this->cacheKeys
);
527 foreach ($keys as $key) {
528 $hashes[] = PhabricatorHash
::digestForIndex($key);
531 $types = PhabricatorUserCacheType
::getAllCacheTypes();
533 // First, pull any available caches. If we wanted to be particularly clever
534 // we could do this with JOINs in the main query.
536 $cache_table = new PhabricatorUserCache();
537 $cache_conn = $cache_table->establishConnection('r');
539 $cache_data = queryfx_all(
541 'SELECT cacheKey, userPHID, cacheData, cacheType FROM %T
542 WHERE cacheIndex IN (%Ls) AND userPHID IN (%Ls)',
543 $cache_table->getTableName(),
545 array_keys($user_map));
547 $skip_validation = array();
549 // After we read caches from the database, discard any which have data that
550 // invalid or out of date. This allows cache types to implement TTLs or
551 // versions instead of or in addition to explicit cache clears.
552 foreach ($cache_data as $row_key => $row) {
553 $cache_type = $row['cacheType'];
555 if (isset($skip_validation[$cache_type])) {
559 if (empty($types[$cache_type])) {
560 unset($cache_data[$row_key]);
564 $type = $types[$cache_type];
565 if (!$type->shouldValidateRawCacheData()) {
566 $skip_validation[$cache_type] = true;
570 $user = $user_map[$row['userPHID']];
571 $raw_data = $row['cacheData'];
572 if (!$type->isRawCacheDataValid($user, $row['cacheKey'], $raw_data)) {
573 unset($cache_data[$row_key]);
580 $cache_data = igroup($cache_data, 'userPHID');
581 foreach ($user_map as $user_phid => $user) {
582 $raw_rows = idx($cache_data, $user_phid, array());
583 $raw_data = ipull($raw_rows, 'cacheData', 'cacheKey');
585 foreach ($keys as $key) {
586 if (isset($raw_data[$key]) ||
array_key_exists($key, $raw_data)) {
589 $need[$key][$user_phid] = $user;
592 $user->attachRawCacheData($raw_data);
595 // If we missed any cache values, bulk-construct them now. This is
596 // usually much cheaper than generating them on-demand for each user
604 foreach ($need as $cache_key => $need_users) {
605 $type = PhabricatorUserCacheType
::getCacheTypeForKey($cache_key);
610 $data = $type->newValueForUsers($cache_key, $need_users);
612 foreach ($data as $user_phid => $raw_value) {
613 $data[$user_phid] = $raw_value;
615 'userPHID' => $user_phid,
618 'value' => $raw_value,
622 foreach ($need_users as $user_phid => $user) {
623 if (isset($data[$user_phid]) ||
array_key_exists($user_phid, $data)) {
624 $user->attachRawCacheData(
626 $cache_key => $data[$user_phid],
632 PhabricatorUserCache
::writeCaches($writes);