Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/people/query/PhabricatorPeopleQuery.php
12256 views
1
<?php
2
3
final class PhabricatorPeopleQuery
4
extends PhabricatorCursorPagedPolicyAwareQuery {
5
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;
22
23
private $needPrimaryEmail;
24
private $needProfile;
25
private $needProfileImage;
26
private $needAvailability;
27
private $needBadgeAwards;
28
private $cacheKeys = array();
29
30
public function withIDs(array $ids) {
31
$this->ids = $ids;
32
return $this;
33
}
34
35
public function withPHIDs(array $phids) {
36
$this->phids = $phids;
37
return $this;
38
}
39
40
public function withEmails(array $emails) {
41
$this->emails = $emails;
42
return $this;
43
}
44
45
public function withRealnames(array $realnames) {
46
$this->realnames = $realnames;
47
return $this;
48
}
49
50
public function withUsernames(array $usernames) {
51
$this->usernames = $usernames;
52
return $this;
53
}
54
55
public function withDateCreatedBefore($date_created_before) {
56
$this->dateCreatedBefore = $date_created_before;
57
return $this;
58
}
59
60
public function withDateCreatedAfter($date_created_after) {
61
$this->dateCreatedAfter = $date_created_after;
62
return $this;
63
}
64
65
public function withIsAdmin($admin) {
66
$this->isAdmin = $admin;
67
return $this;
68
}
69
70
public function withIsSystemAgent($system_agent) {
71
$this->isSystemAgent = $system_agent;
72
return $this;
73
}
74
75
public function withIsMailingList($mailing_list) {
76
$this->isMailingList = $mailing_list;
77
return $this;
78
}
79
80
public function withIsDisabled($disabled) {
81
$this->isDisabled = $disabled;
82
return $this;
83
}
84
85
public function withIsApproved($approved) {
86
$this->isApproved = $approved;
87
return $this;
88
}
89
90
public function withNameLike($like) {
91
$this->nameLike = $like;
92
return $this;
93
}
94
95
public function withNameTokens(array $tokens) {
96
$this->nameTokens = array_values($tokens);
97
return $this;
98
}
99
100
public function withNamePrefixes(array $prefixes) {
101
$this->namePrefixes = $prefixes;
102
return $this;
103
}
104
105
public function withIsEnrolledInMultiFactor($enrolled) {
106
$this->isEnrolledInMultiFactor = $enrolled;
107
return $this;
108
}
109
110
public function needPrimaryEmail($need) {
111
$this->needPrimaryEmail = $need;
112
return $this;
113
}
114
115
public function needProfile($need) {
116
$this->needProfile = $need;
117
return $this;
118
}
119
120
public function needProfileImage($need) {
121
$cache_key = PhabricatorUserProfileImageCacheType::KEY_URI;
122
123
if ($need) {
124
$this->cacheKeys[$cache_key] = true;
125
} else {
126
unset($this->cacheKeys[$cache_key]);
127
}
128
129
return $this;
130
}
131
132
public function needAvailability($need) {
133
$this->needAvailability = $need;
134
return $this;
135
}
136
137
public function needUserSettings($need) {
138
$cache_key = PhabricatorUserPreferencesCacheType::KEY_PREFERENCES;
139
140
if ($need) {
141
$this->cacheKeys[$cache_key] = true;
142
} else {
143
unset($this->cacheKeys[$cache_key]);
144
}
145
146
return $this;
147
}
148
149
public function needBadgeAwards($need) {
150
$cache_key = PhabricatorUserBadgesCacheType::KEY_BADGES;
151
152
if ($need) {
153
$this->cacheKeys[$cache_key] = true;
154
} else {
155
unset($this->cacheKeys[$cache_key]);
156
}
157
158
return $this;
159
}
160
161
public function newResultObject() {
162
return new PhabricatorUser();
163
}
164
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));
172
173
$profiles = mpull($profiles, null, 'getUserPHID');
174
foreach ($user_list as $user_phid => $user) {
175
$profile = idx($profiles, $user_phid);
176
177
if (!$profile) {
178
$profile = PhabricatorUserProfile::initializeNewProfile($user);
179
}
180
181
$user->attachUserProfile($profile);
182
}
183
}
184
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;
193
}
194
}
195
196
if ($rebuild) {
197
$this->rebuildAvailabilityCache($rebuild);
198
}
199
}
200
201
$this->fillUserCaches($users);
202
203
return $users;
204
}
205
206
protected function shouldGroupQueryResultRows() {
207
if ($this->nameTokens) {
208
return true;
209
}
210
211
return parent::shouldGroupQueryResultRows();
212
}
213
214
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
215
$joins = parent::buildJoinClauseParts($conn);
216
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());
223
}
224
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);
236
}
237
}
238
239
return $joins;
240
}
241
242
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
243
$where = parent::buildWhereClauseParts($conn);
244
245
if ($this->usernames !== null) {
246
$where[] = qsprintf(
247
$conn,
248
'user.userName IN (%Ls)',
249
$this->usernames);
250
}
251
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);
259
}
260
$where[] = qsprintf($conn, '%LO', $parts);
261
}
262
263
if ($this->emails !== null) {
264
$where[] = qsprintf(
265
$conn,
266
'email.address IN (%Ls)',
267
$this->emails);
268
}
269
270
if ($this->realnames !== null) {
271
$where[] = qsprintf(
272
$conn,
273
'user.realName IN (%Ls)',
274
$this->realnames);
275
}
276
277
if ($this->phids !== null) {
278
$where[] = qsprintf(
279
$conn,
280
'user.phid IN (%Ls)',
281
$this->phids);
282
}
283
284
if ($this->ids !== null) {
285
$where[] = qsprintf(
286
$conn,
287
'user.id IN (%Ld)',
288
$this->ids);
289
}
290
291
if ($this->dateCreatedAfter) {
292
$where[] = qsprintf(
293
$conn,
294
'user.dateCreated >= %d',
295
$this->dateCreatedAfter);
296
}
297
298
if ($this->dateCreatedBefore) {
299
$where[] = qsprintf(
300
$conn,
301
'user.dateCreated <= %d',
302
$this->dateCreatedBefore);
303
}
304
305
if ($this->isAdmin !== null) {
306
$where[] = qsprintf(
307
$conn,
308
'user.isAdmin = %d',
309
(int)$this->isAdmin);
310
}
311
312
if ($this->isDisabled !== null) {
313
$where[] = qsprintf(
314
$conn,
315
'user.isDisabled = %d',
316
(int)$this->isDisabled);
317
}
318
319
if ($this->isApproved !== null) {
320
$where[] = qsprintf(
321
$conn,
322
'user.isApproved = %d',
323
(int)$this->isApproved);
324
}
325
326
if ($this->isSystemAgent !== null) {
327
$where[] = qsprintf(
328
$conn,
329
'user.isSystemAgent = %d',
330
(int)$this->isSystemAgent);
331
}
332
333
if ($this->isMailingList !== null) {
334
$where[] = qsprintf(
335
$conn,
336
'user.isMailingList = %d',
337
(int)$this->isMailingList);
338
}
339
340
if ($this->nameLike !== null) {
341
$where[] = qsprintf(
342
$conn,
343
'user.username LIKE %~ OR user.realname LIKE %~',
344
$this->nameLike,
345
$this->nameLike);
346
}
347
348
if ($this->isEnrolledInMultiFactor !== null) {
349
$where[] = qsprintf(
350
$conn,
351
'user.isEnrolledInMultiFactor = %d',
352
(int)$this->isEnrolledInMultiFactor);
353
}
354
355
return $where;
356
}
357
358
protected function getPrimaryTableAlias() {
359
return 'user';
360
}
361
362
public function getQueryApplicationClass() {
363
return 'PhabricatorPeopleApplication';
364
}
365
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,
374
),
375
);
376
}
377
378
protected function newPagingMapFromPartialObject($object) {
379
return array(
380
'id' => (int)$object->getID(),
381
'username' => $object->getUsername(),
382
);
383
}
384
385
private function rebuildAvailabilityCache(array $rebuild) {
386
$rebuild = mpull($rebuild, null, 'getPHID');
387
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');
393
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.
397
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();
404
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;
413
}
414
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;
419
}
420
421
$invitee_phid = $invitee->getInviteePHID();
422
if (!isset($rebuild[$invitee_phid])) {
423
continue;
424
}
425
426
$map[$invitee_phid][] = $event;
427
428
$event_phid = $event->getPHID();
429
$invitee_map[$invitee_phid][$event_phid] = $invitee;
430
}
431
}
432
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);
437
438
foreach ($rebuild as $phid => $user) {
439
$events = idx($map, $phid, array());
440
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);
446
}
447
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;
461
}
462
continue 2;
463
}
464
}
465
break;
466
}
467
}
468
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,
476
);
477
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.
481
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.
486
487
$availability_ttl = $next_event->getEndDateTimeEpochForCache();
488
} else {
489
$availability = array(
490
'until' => null,
491
'eventPHID' => null,
492
'availability' => null,
493
);
494
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);
502
}
503
}
504
}
505
506
// Never TTL the cache to longer than the maximum range we examined.
507
$availability_ttl = min($availability_ttl, $max_range);
508
509
$user->writeAvailabilityCache($availability, $availability_ttl);
510
$user->attachAvailability($availability);
511
}
512
}
513
514
private function fillUserCaches(array $users) {
515
if (!$this->cacheKeys) {
516
return;
517
}
518
519
$user_map = mpull($users, null, 'getPHID');
520
$keys = array_keys($this->cacheKeys);
521
522
$hashes = array();
523
foreach ($keys as $key) {
524
$hashes[] = PhabricatorHash::digestForIndex($key);
525
}
526
527
$types = PhabricatorUserCacheType::getAllCacheTypes();
528
529
// First, pull any available caches. If we wanted to be particularly clever
530
// we could do this with JOINs in the main query.
531
532
$cache_table = new PhabricatorUserCache();
533
$cache_conn = $cache_table->establishConnection('r');
534
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));
542
543
$skip_validation = array();
544
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'];
550
551
if (isset($skip_validation[$cache_type])) {
552
continue;
553
}
554
555
if (empty($types[$cache_type])) {
556
unset($cache_data[$row_key]);
557
continue;
558
}
559
560
$type = $types[$cache_type];
561
if (!$type->shouldValidateRawCacheData()) {
562
$skip_validation[$cache_type] = true;
563
continue;
564
}
565
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;
571
}
572
}
573
574
$need = array();
575
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');
580
581
foreach ($keys as $key) {
582
if (isset($raw_data[$key]) || array_key_exists($key, $raw_data)) {
583
continue;
584
}
585
$need[$key][$user_phid] = $user;
586
}
587
588
$user->attachRawCacheData($raw_data);
589
}
590
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.
594
595
if (!$need) {
596
return;
597
}
598
599
$writes = array();
600
foreach ($need as $cache_key => $need_users) {
601
$type = PhabricatorUserCacheType::getCacheTypeForKey($cache_key);
602
if (!$type) {
603
continue;
604
}
605
606
$data = $type->newValueForUsers($cache_key, $need_users);
607
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,
615
);
616
}
617
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],
623
));
624
}
625
}
626
}
627
628
PhabricatorUserCache::writeCaches($writes);
629
}
630
}
631
632