Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / people / query / PhabricatorPeopleQuery.php
blobb74b936ba8ac79ca01dbaf90e53d146ff8f118a8
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 didFilterPage(array $users) {
166 if ($this->needProfile) {
167 $user_list = mpull($users, null, 'getPHID');
168 $profiles = new PhabricatorUserProfile();
169 $profiles = $profiles->loadAllWhere(
170 'userPHID IN (%Ls)',
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);
177 if (!$profile) {
178 $profile = PhabricatorUserProfile::initializeNewProfile($user);
181 $user->attachUserProfile($profile);
185 if ($this->needAvailability) {
186 $rebuild = array();
187 foreach ($users as $user) {
188 $cache = $user->getAvailabilityCache();
189 if ($cache !== null) {
190 $user->attachAvailability($cache);
191 } else {
192 $rebuild[] = $user;
196 if ($rebuild) {
197 $this->rebuildAvailabilityCache($rebuild);
201 $this->fillUserCaches($users);
203 return $users;
206 protected function shouldGroupQueryResultRows() {
207 if ($this->nameTokens) {
208 return true;
211 return parent::shouldGroupQueryResultRows();
214 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
215 $joins = parent::buildJoinClauseParts($conn);
217 if ($this->emails) {
218 $email_table = new PhabricatorUserEmail();
219 $joins[] = qsprintf(
220 $conn,
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;
228 $joins[] = qsprintf(
229 $conn,
230 'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>',
231 PhabricatorUser::NAMETOKEN_TABLE,
232 $token_table,
233 $token_table,
234 $token_table,
235 $token);
239 return $joins;
242 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
243 $where = parent::buildWhereClauseParts($conn);
245 if ($this->usernames !== null) {
246 $where[] = qsprintf(
247 $conn,
248 'user.userName IN (%Ls)',
249 $this->usernames);
252 if ($this->namePrefixes) {
253 $parts = array();
254 foreach ($this->namePrefixes as $name_prefix) {
255 $parts[] = qsprintf(
256 $conn,
257 'user.username LIKE %>',
258 $name_prefix);
260 $where[] = qsprintf($conn, '%LO', $parts);
263 if ($this->emails !== null) {
264 $where[] = qsprintf(
265 $conn,
266 'email.address IN (%Ls)',
267 $this->emails);
270 if ($this->realnames !== null) {
271 $where[] = qsprintf(
272 $conn,
273 'user.realName IN (%Ls)',
274 $this->realnames);
277 if ($this->phids !== null) {
278 $where[] = qsprintf(
279 $conn,
280 'user.phid IN (%Ls)',
281 $this->phids);
284 if ($this->ids !== null) {
285 $where[] = qsprintf(
286 $conn,
287 'user.id IN (%Ld)',
288 $this->ids);
291 if ($this->dateCreatedAfter) {
292 $where[] = qsprintf(
293 $conn,
294 'user.dateCreated >= %d',
295 $this->dateCreatedAfter);
298 if ($this->dateCreatedBefore) {
299 $where[] = qsprintf(
300 $conn,
301 'user.dateCreated <= %d',
302 $this->dateCreatedBefore);
305 if ($this->isAdmin !== null) {
306 $where[] = qsprintf(
307 $conn,
308 'user.isAdmin = %d',
309 (int)$this->isAdmin);
312 if ($this->isDisabled !== null) {
313 $where[] = qsprintf(
314 $conn,
315 'user.isDisabled = %d',
316 (int)$this->isDisabled);
319 if ($this->isApproved !== null) {
320 $where[] = qsprintf(
321 $conn,
322 'user.isApproved = %d',
323 (int)$this->isApproved);
326 if ($this->isSystemAgent !== null) {
327 $where[] = qsprintf(
328 $conn,
329 'user.isSystemAgent = %d',
330 (int)$this->isSystemAgent);
333 if ($this->isMailingList !== null) {
334 $where[] = qsprintf(
335 $conn,
336 'user.isMailingList = %d',
337 (int)$this->isMailingList);
340 if ($this->nameLike !== null) {
341 $where[] = qsprintf(
342 $conn,
343 'user.username LIKE %~ OR user.realname LIKE %~',
344 $this->nameLike,
345 $this->nameLike);
348 if ($this->isEnrolledInMultiFactor !== null) {
349 $where[] = qsprintf(
350 $conn,
351 'user.isEnrolledInMultiFactor = %d',
352 (int)$this->isEnrolledInMultiFactor);
355 return $where;
358 protected function getPrimaryTableAlias() {
359 return 'user';
362 public function getQueryApplicationClass() {
363 return 'PhabricatorPeopleApplication';
366 public function getOrderableColumns() {
367 return parent::getOrderableColumns() + array(
368 'username' => array(
369 'table' => 'user',
370 'column' => 'username',
371 'type' => 'string',
372 'reverse' => true,
373 'unique' => true,
378 protected function newPagingMapFromPartialObject($object) {
379 return array(
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)
403 ->execute();
405 // Group all the events by invited user. Only examine events that users
406 // are actually attending.
407 $map = array();
408 $invitee_map = array();
409 foreach ($events as $event) {
410 foreach ($event->getInvitees() as $invitee) {
411 if (!$invitee->isAttending()) {
412 continue;
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)) {
418 continue;
421 $invitee_phid = $invitee->getInviteePHID();
422 if (!isset($rebuild[$invitee_phid])) {
423 continue;
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;
449 $next_event = null;
450 if ($events) {
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.
453 while (true) {
454 foreach ($events as $event) {
455 $from = $event->getStartDateTimeEpochForCache();
456 $to = $event->getEndDateTimeEpochForCache();
457 if (($from <= $cursor) && ($to > $cursor)) {
458 $cursor = $to;
459 if (!$next_event) {
460 $next_event = $event;
462 continue 2;
465 break;
469 if ($cursor > $min_range) {
470 $invitee = $invitee_map[$phid][$next_event->getPHID()];
471 $availability_type = $invitee->getDisplayAvailability($next_event);
472 $availability = array(
473 'until' => $cursor,
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();
488 } else {
489 $availability = array(
490 'until' => null,
491 'eventPHID' => null,
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) {
516 return;
519 $user_map = mpull($users, null, 'getPHID');
520 $keys = array_keys($this->cacheKeys);
522 $hashes = array();
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(
536 $cache_conn,
537 'SELECT cacheKey, userPHID, cacheData, cacheType FROM %T
538 WHERE cacheIndex IN (%Ls) AND userPHID IN (%Ls)',
539 $cache_table->getTableName(),
540 $hashes,
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])) {
552 continue;
555 if (empty($types[$cache_type])) {
556 unset($cache_data[$row_key]);
557 continue;
560 $type = $types[$cache_type];
561 if (!$type->shouldValidateRawCacheData()) {
562 $skip_validation[$cache_type] = true;
563 continue;
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]);
570 continue;
574 $need = array();
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)) {
583 continue;
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
593 // record.
595 if (!$need) {
596 return;
599 $writes = array();
600 foreach ($need as $cache_key => $need_users) {
601 $type = PhabricatorUserCacheType::getCacheTypeForKey($cache_key);
602 if (!$type) {
603 continue;
606 $data = $type->newValueForUsers($cache_key, $need_users);
608 foreach ($data as $user_phid => $raw_value) {
609 $data[$user_phid] = $raw_value;
610 $writes[] = array(
611 'userPHID' => $user_phid,
612 'key' => $cache_key,
613 'type' => $type,
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(
621 array(
622 $cache_key => $data[$user_phid],
628 PhabricatorUserCache::writeCaches($writes);