Remove product literal strings in "pht()", part 6
[phabricator.git] / src / applications / people / query / PhabricatorPeopleQuery.php
blobe122be0b2e9f66ce8800a513460e44bd020e163e
1 <?php
3 final class PhabricatorPeopleQuery
4 extends PhabricatorCursorPagedPolicyAwareQuery {
6 private $usernames;
7 private $realnames;
8 private $emails;
9 private $phids;
10 private $ids;
11 private $dateCreatedAfter;
12 private $dateCreatedBefore;
13 private $isAdmin;
14 private $isSystemAgent;
15 private $isMailingList;
16 private $isDisabled;
17 private $isApproved;
18 private $nameLike;
19 private $nameTokens;
20 private $namePrefixes;
21 private $isEnrolledInMultiFactor;
23 private $needPrimaryEmail;
24 private $needProfile;
25 private $needProfileImage;
26 private $needAvailability;
27 private $needBadgeAwards;
28 private $cacheKeys = array();
30 public function withIDs(array $ids) {
31 $this->ids = $ids;
32 return $this;
35 public function withPHIDs(array $phids) {
36 $this->phids = $phids;
37 return $this;
40 public function withEmails(array $emails) {
41 $this->emails = $emails;
42 return $this;
45 public function withRealnames(array $realnames) {
46 $this->realnames = $realnames;
47 return $this;
50 public function withUsernames(array $usernames) {
51 $this->usernames = $usernames;
52 return $this;
55 public function withDateCreatedBefore($date_created_before) {
56 $this->dateCreatedBefore = $date_created_before;
57 return $this;
60 public function withDateCreatedAfter($date_created_after) {
61 $this->dateCreatedAfter = $date_created_after;
62 return $this;
65 public function withIsAdmin($admin) {
66 $this->isAdmin = $admin;
67 return $this;
70 public function withIsSystemAgent($system_agent) {
71 $this->isSystemAgent = $system_agent;
72 return $this;
75 public function withIsMailingList($mailing_list) {
76 $this->isMailingList = $mailing_list;
77 return $this;
80 public function withIsDisabled($disabled) {
81 $this->isDisabled = $disabled;
82 return $this;
85 public function withIsApproved($approved) {
86 $this->isApproved = $approved;
87 return $this;
90 public function withNameLike($like) {
91 $this->nameLike = $like;
92 return $this;
95 public function withNameTokens(array $tokens) {
96 $this->nameTokens = array_values($tokens);
97 return $this;
100 public function withNamePrefixes(array $prefixes) {
101 $this->namePrefixes = $prefixes;
102 return $this;
105 public function withIsEnrolledInMultiFactor($enrolled) {
106 $this->isEnrolledInMultiFactor = $enrolled;
107 return $this;
110 public function needPrimaryEmail($need) {
111 $this->needPrimaryEmail = $need;
112 return $this;
115 public function needProfile($need) {
116 $this->needProfile = $need;
117 return $this;
120 public function needProfileImage($need) {
121 $cache_key = PhabricatorUserProfileImageCacheType::KEY_URI;
123 if ($need) {
124 $this->cacheKeys[$cache_key] = true;
125 } else {
126 unset($this->cacheKeys[$cache_key]);
129 return $this;
132 public function needAvailability($need) {
133 $this->needAvailability = $need;
134 return $this;
137 public function needUserSettings($need) {
138 $cache_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
140 if ($need) {
141 $this->cacheKeys[$cache_key] = true;
142 } else {
143 unset($this->cacheKeys[$cache_key]);
146 return $this;
149 public function needBadgeAwards($need) {
150 $cache_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
152 if ($need) {
153 $this->cacheKeys[$cache_key] = true;
154 } else {
155 unset($this->cacheKeys[$cache_key]);
158 return $this;
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(
174 'userPHID IN (%Ls)',
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);
181 if (!$profile) {
182 $profile = PhabricatorUserProfile::initializeNewProfile($user);
185 $user->attachUserProfile($profile);
189 if ($this->needAvailability) {
190 $rebuild = array();
191 foreach ($users as $user) {
192 $cache = $user->getAvailabilityCache();
193 if ($cache !== null) {
194 $user->attachAvailability($cache);
195 } else {
196 $rebuild[] = $user;
200 if ($rebuild) {
201 $this->rebuildAvailabilityCache($rebuild);
205 $this->fillUserCaches($users);
207 return $users;
210 protected function shouldGroupQueryResultRows() {
211 if ($this->nameTokens) {
212 return true;
215 return parent::shouldGroupQueryResultRows();
218 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
219 $joins = parent::buildJoinClauseParts($conn);
221 if ($this->emails) {
222 $email_table = new PhabricatorUserEmail();
223 $joins[] = qsprintf(
224 $conn,
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;
232 $joins[] = qsprintf(
233 $conn,
234 'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>',
235 PhabricatorUser::NAMETOKEN_TABLE,
236 $token_table,
237 $token_table,
238 $token_table,
239 $token);
243 return $joins;
246 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
247 $where = parent::buildWhereClauseParts($conn);
249 if ($this->usernames !== null) {
250 $where[] = qsprintf(
251 $conn,
252 'user.userName IN (%Ls)',
253 $this->usernames);
256 if ($this->namePrefixes) {
257 $parts = array();
258 foreach ($this->namePrefixes as $name_prefix) {
259 $parts[] = qsprintf(
260 $conn,
261 'user.username LIKE %>',
262 $name_prefix);
264 $where[] = qsprintf($conn, '%LO', $parts);
267 if ($this->emails !== null) {
268 $where[] = qsprintf(
269 $conn,
270 'email.address IN (%Ls)',
271 $this->emails);
274 if ($this->realnames !== null) {
275 $where[] = qsprintf(
276 $conn,
277 'user.realName IN (%Ls)',
278 $this->realnames);
281 if ($this->phids !== null) {
282 $where[] = qsprintf(
283 $conn,
284 'user.phid IN (%Ls)',
285 $this->phids);
288 if ($this->ids !== null) {
289 $where[] = qsprintf(
290 $conn,
291 'user.id IN (%Ld)',
292 $this->ids);
295 if ($this->dateCreatedAfter) {
296 $where[] = qsprintf(
297 $conn,
298 'user.dateCreated >= %d',
299 $this->dateCreatedAfter);
302 if ($this->dateCreatedBefore) {
303 $where[] = qsprintf(
304 $conn,
305 'user.dateCreated <= %d',
306 $this->dateCreatedBefore);
309 if ($this->isAdmin !== null) {
310 $where[] = qsprintf(
311 $conn,
312 'user.isAdmin = %d',
313 (int)$this->isAdmin);
316 if ($this->isDisabled !== null) {
317 $where[] = qsprintf(
318 $conn,
319 'user.isDisabled = %d',
320 (int)$this->isDisabled);
323 if ($this->isApproved !== null) {
324 $where[] = qsprintf(
325 $conn,
326 'user.isApproved = %d',
327 (int)$this->isApproved);
330 if ($this->isSystemAgent !== null) {
331 $where[] = qsprintf(
332 $conn,
333 'user.isSystemAgent = %d',
334 (int)$this->isSystemAgent);
337 if ($this->isMailingList !== null) {
338 $where[] = qsprintf(
339 $conn,
340 'user.isMailingList = %d',
341 (int)$this->isMailingList);
344 if ($this->nameLike !== null) {
345 $where[] = qsprintf(
346 $conn,
347 'user.username LIKE %~ OR user.realname LIKE %~',
348 $this->nameLike,
349 $this->nameLike);
352 if ($this->isEnrolledInMultiFactor !== null) {
353 $where[] = qsprintf(
354 $conn,
355 'user.isEnrolledInMultiFactor = %d',
356 (int)$this->isEnrolledInMultiFactor);
359 return $where;
362 protected function getPrimaryTableAlias() {
363 return 'user';
366 public function getQueryApplicationClass() {
367 return 'PhabricatorPeopleApplication';
370 public function getOrderableColumns() {
371 return parent::getOrderableColumns() + array(
372 'username' => array(
373 'table' => 'user',
374 'column' => 'username',
375 'type' => 'string',
376 'reverse' => true,
377 'unique' => true,
382 protected function newPagingMapFromPartialObject($object) {
383 return array(
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)
407 ->execute();
409 // Group all the events by invited user. Only examine events that users
410 // are actually attending.
411 $map = array();
412 $invitee_map = array();
413 foreach ($events as $event) {
414 foreach ($event->getInvitees() as $invitee) {
415 if (!$invitee->isAttending()) {
416 continue;
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)) {
422 continue;
425 $invitee_phid = $invitee->getInviteePHID();
426 if (!isset($rebuild[$invitee_phid])) {
427 continue;
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;
453 $next_event = null;
454 if ($events) {
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.
457 while (true) {
458 foreach ($events as $event) {
459 $from = $event->getStartDateTimeEpochForCache();
460 $to = $event->getEndDateTimeEpochForCache();
461 if (($from <= $cursor) && ($to > $cursor)) {
462 $cursor = $to;
463 if (!$next_event) {
464 $next_event = $event;
466 continue 2;
469 break;
473 if ($cursor > $min_range) {
474 $invitee = $invitee_map[$phid][$next_event->getPHID()];
475 $availability_type = $invitee->getDisplayAvailability($next_event);
476 $availability = array(
477 'until' => $cursor,
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();
492 } else {
493 $availability = array(
494 'until' => null,
495 'eventPHID' => null,
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) {
520 return;
523 $user_map = mpull($users, null, 'getPHID');
524 $keys = array_keys($this->cacheKeys);
526 $hashes = array();
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(
540 $cache_conn,
541 'SELECT cacheKey, userPHID, cacheData, cacheType FROM %T
542 WHERE cacheIndex IN (%Ls) AND userPHID IN (%Ls)',
543 $cache_table->getTableName(),
544 $hashes,
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])) {
556 continue;
559 if (empty($types[$cache_type])) {
560 unset($cache_data[$row_key]);
561 continue;
564 $type = $types[$cache_type];
565 if (!$type->shouldValidateRawCacheData()) {
566 $skip_validation[$cache_type] = true;
567 continue;
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]);
574 continue;
578 $need = array();
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)) {
587 continue;
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
597 // record.
599 if (!$need) {
600 return;
603 $writes = array();
604 foreach ($need as $cache_key => $need_users) {
605 $type = PhabricatorUserCacheType::getCacheTypeForKey($cache_key);
606 if (!$type) {
607 continue;
610 $data = $type->newValueForUsers($cache_key, $need_users);
612 foreach ($data as $user_phid => $raw_value) {
613 $data[$user_phid] = $raw_value;
614 $writes[] = array(
615 'userPHID' => $user_phid,
616 'key' => $cache_key,
617 'type' => $type,
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(
625 array(
626 $cache_key => $data[$user_phid],
632 PhabricatorUserCache::writeCaches($writes);