Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
12256 views
1
<?php
2
3
final class PhabricatorCalendarEventQuery
4
extends PhabricatorCursorPagedPolicyAwareQuery {
5
6
private $ids;
7
private $phids;
8
private $rangeBegin;
9
private $rangeEnd;
10
private $inviteePHIDs;
11
private $hostPHIDs;
12
private $isCancelled;
13
private $eventsWithNoParent;
14
private $instanceSequencePairs;
15
private $isStub;
16
private $parentEventPHIDs;
17
private $importSourcePHIDs;
18
private $importAuthorPHIDs;
19
private $importUIDs;
20
private $utcInitialEpochMin;
21
private $utcInitialEpochMax;
22
private $isImported;
23
private $needRSVPs;
24
25
private $generateGhosts = false;
26
27
public function newResultObject() {
28
return new PhabricatorCalendarEvent();
29
}
30
31
public function setGenerateGhosts($generate_ghosts) {
32
$this->generateGhosts = $generate_ghosts;
33
return $this;
34
}
35
36
public function withIDs(array $ids) {
37
$this->ids = $ids;
38
return $this;
39
}
40
41
public function withPHIDs(array $phids) {
42
$this->phids = $phids;
43
return $this;
44
}
45
46
public function withDateRange($begin, $end) {
47
$this->rangeBegin = $begin;
48
$this->rangeEnd = $end;
49
return $this;
50
}
51
52
public function withUTCInitialEpochBetween($min, $max) {
53
$this->utcInitialEpochMin = $min;
54
$this->utcInitialEpochMax = $max;
55
return $this;
56
}
57
58
public function withInvitedPHIDs(array $phids) {
59
$this->inviteePHIDs = $phids;
60
return $this;
61
}
62
63
public function withHostPHIDs(array $phids) {
64
$this->hostPHIDs = $phids;
65
return $this;
66
}
67
68
public function withIsCancelled($is_cancelled) {
69
$this->isCancelled = $is_cancelled;
70
return $this;
71
}
72
73
public function withIsStub($is_stub) {
74
$this->isStub = $is_stub;
75
return $this;
76
}
77
78
public function withEventsWithNoParent($events_with_no_parent) {
79
$this->eventsWithNoParent = $events_with_no_parent;
80
return $this;
81
}
82
83
public function withInstanceSequencePairs(array $pairs) {
84
$this->instanceSequencePairs = $pairs;
85
return $this;
86
}
87
88
public function withParentEventPHIDs(array $parent_phids) {
89
$this->parentEventPHIDs = $parent_phids;
90
return $this;
91
}
92
93
public function withImportSourcePHIDs(array $import_phids) {
94
$this->importSourcePHIDs = $import_phids;
95
return $this;
96
}
97
98
public function withImportAuthorPHIDs(array $author_phids) {
99
$this->importAuthorPHIDs = $author_phids;
100
return $this;
101
}
102
103
public function withImportUIDs(array $uids) {
104
$this->importUIDs = $uids;
105
return $this;
106
}
107
108
public function withIsImported($is_imported) {
109
$this->isImported = $is_imported;
110
return $this;
111
}
112
113
public function needRSVPs(array $phids) {
114
$this->needRSVPs = $phids;
115
return $this;
116
}
117
118
protected function getDefaultOrderVector() {
119
return array('start', 'id');
120
}
121
122
public function getBuiltinOrders() {
123
return array(
124
'start' => array(
125
'vector' => array('start', 'id'),
126
'name' => pht('Event Start'),
127
),
128
) + parent::getBuiltinOrders();
129
}
130
131
public function getOrderableColumns() {
132
return array(
133
'start' => array(
134
'table' => $this->getPrimaryTableAlias(),
135
'column' => 'utcInitialEpoch',
136
'reverse' => true,
137
'type' => 'int',
138
'unique' => false,
139
),
140
) + parent::getOrderableColumns();
141
}
142
143
protected function newPagingMapFromPartialObject($object) {
144
return array(
145
'id' => (int)$object->getID(),
146
'start' => (int)$object->getStartDateTimeEpoch(),
147
);
148
}
149
150
protected function shouldLimitResults() {
151
// When generating ghosts, we can't rely on database ordering because
152
// MySQL can't predict the ghost start times. We'll just load all matching
153
// events, then generate results from there.
154
if ($this->generateGhosts) {
155
return false;
156
}
157
158
return true;
159
}
160
161
protected function loadPage() {
162
$events = $this->loadStandardPage($this->newResultObject());
163
164
$viewer = $this->getViewer();
165
foreach ($events as $event) {
166
$event->applyViewerTimezone($viewer);
167
}
168
169
if (!$this->generateGhosts) {
170
return $events;
171
}
172
173
$raw_limit = $this->getRawResultLimit();
174
if (!$raw_limit && !$this->rangeEnd) {
175
throw new Exception(
176
pht(
177
'Event queries which generate ghost events must include either a '.
178
'result limit or an end date, because they may otherwise generate '.
179
'an infinite number of results. This query has neither.'));
180
}
181
182
foreach ($events as $key => $event) {
183
$sequence_start = 0;
184
$sequence_end = null;
185
$end = null;
186
187
$instance_of = $event->getInstanceOfEventPHID();
188
189
if ($instance_of == null && $this->isCancelled !== null) {
190
if ($event->getIsCancelled() != $this->isCancelled) {
191
unset($events[$key]);
192
continue;
193
}
194
}
195
}
196
197
// Pull out all of the parents first. We may discard them as we begin
198
// generating ghost events, but we still want to process all of them.
199
$parents = array();
200
foreach ($events as $key => $event) {
201
if ($event->isParentEvent()) {
202
$parents[$key] = $event;
203
}
204
}
205
206
// Now that we've picked out all the parent events, we can immediately
207
// discard anything outside of the time window.
208
$events = $this->getEventsInRange($events);
209
210
$generate_from = $this->rangeBegin;
211
$generate_until = $this->rangeEnd;
212
foreach ($parents as $key => $event) {
213
$duration = $event->getDuration();
214
215
$start_date = $this->getRecurrenceWindowStart(
216
$event,
217
$generate_from - $duration);
218
219
$end_date = $this->getRecurrenceWindowEnd(
220
$event,
221
$generate_until);
222
223
$limit = $this->getRecurrenceLimit($event, $raw_limit);
224
225
$set = $event->newRecurrenceSet();
226
227
$recurrences = $set->getEventsBetween(
228
$start_date,
229
$end_date,
230
$limit + 1);
231
232
// We're generating events from the beginning and then filtering them
233
// here (instead of only generating events starting at the start date)
234
// because we need to know the proper sequence indexes to generate ghost
235
// events. This may change after RDATE support.
236
if ($start_date) {
237
$start_epoch = $start_date->getEpoch();
238
} else {
239
$start_epoch = null;
240
}
241
242
foreach ($recurrences as $sequence_index => $sequence_datetime) {
243
if (!$sequence_index) {
244
// This is the parent event, which we already have.
245
continue;
246
}
247
248
if ($start_epoch) {
249
if ($sequence_datetime->getEpoch() < $start_epoch) {
250
continue;
251
}
252
}
253
254
$events[] = $event->newGhost(
255
$viewer,
256
$sequence_index,
257
$sequence_datetime);
258
}
259
260
// NOTE: We're slicing results every time because this makes it cheaper
261
// to generate future ghosts. If we already have 100 events that occur
262
// before July 1, we know we never need to generate ghosts after that
263
// because they couldn't possibly ever appear in the result set.
264
265
if ($raw_limit) {
266
if (count($events) > $raw_limit) {
267
$events = msort($events, 'getStartDateTimeEpoch');
268
$events = array_slice($events, 0, $raw_limit, true);
269
$generate_until = last($events)->getEndDateTimeEpoch();
270
}
271
}
272
}
273
274
// Now that we're done generating ghost events, we're going to remove any
275
// ghosts that we have concrete events for (or which we can load the
276
// concrete events for). These concrete events are generated when users
277
// edit a ghost, and replace the ghost events.
278
279
// First, generate a map of all concrete <parentPHID, sequence> events we
280
// already loaded. We don't need to load these again.
281
$have_pairs = array();
282
foreach ($events as $event) {
283
if ($event->getIsGhostEvent()) {
284
continue;
285
}
286
287
$parent_phid = $event->getInstanceOfEventPHID();
288
$sequence = $event->getSequenceIndex();
289
290
$have_pairs[$parent_phid][$sequence] = true;
291
}
292
293
// Now, generate a map of all <parentPHID, sequence> events we generated
294
// ghosts for. We need to try to load these if we don't already have them.
295
$map = array();
296
$parent_pairs = array();
297
foreach ($events as $key => $event) {
298
if (!$event->getIsGhostEvent()) {
299
continue;
300
}
301
302
$parent_phid = $event->getInstanceOfEventPHID();
303
$sequence = $event->getSequenceIndex();
304
305
// We already loaded the concrete version of this event, so we can just
306
// throw out the ghost and move on.
307
if (isset($have_pairs[$parent_phid][$sequence])) {
308
unset($events[$key]);
309
continue;
310
}
311
312
// We didn't load the concrete version of this event, so we need to
313
// try to load it if it exists.
314
$parent_pairs[] = array($parent_phid, $sequence);
315
$map[$parent_phid][$sequence] = $key;
316
}
317
318
if ($parent_pairs) {
319
$instances = id(new self())
320
->setViewer($viewer)
321
->setParentQuery($this)
322
->withInstanceSequencePairs($parent_pairs)
323
->execute();
324
325
foreach ($instances as $instance) {
326
$parent_phid = $instance->getInstanceOfEventPHID();
327
$sequence = $instance->getSequenceIndex();
328
329
$indexes = idx($map, $parent_phid);
330
$key = idx($indexes, $sequence);
331
332
// Replace the ghost with the corresponding concrete event.
333
$events[$key] = $instance;
334
}
335
}
336
337
$events = msort($events, 'getStartDateTimeEpoch');
338
339
return $events;
340
}
341
342
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
343
$parts = parent::buildJoinClauseParts($conn_r);
344
345
if ($this->inviteePHIDs !== null) {
346
$parts[] = qsprintf(
347
$conn_r,
348
'JOIN %T invitee ON invitee.eventPHID = event.phid
349
AND invitee.status != %s',
350
id(new PhabricatorCalendarEventInvitee())->getTableName(),
351
PhabricatorCalendarEventInvitee::STATUS_UNINVITED);
352
}
353
354
return $parts;
355
}
356
357
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
358
$where = parent::buildWhereClauseParts($conn);
359
360
if ($this->ids !== null) {
361
$where[] = qsprintf(
362
$conn,
363
'event.id IN (%Ld)',
364
$this->ids);
365
}
366
367
if ($this->phids !== null) {
368
$where[] = qsprintf(
369
$conn,
370
'event.phid IN (%Ls)',
371
$this->phids);
372
}
373
374
// NOTE: The date ranges we query for are larger than the requested ranges
375
// because we need to catch all-day events. We'll refine this range later
376
// after adjusting the visible range of events we load.
377
378
if ($this->rangeBegin) {
379
$where[] = qsprintf(
380
$conn,
381
'(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)',
382
$this->rangeBegin - phutil_units('16 hours in seconds'));
383
}
384
385
if ($this->rangeEnd) {
386
$where[] = qsprintf(
387
$conn,
388
'event.utcInitialEpoch <= %d',
389
$this->rangeEnd + phutil_units('16 hours in seconds'));
390
}
391
392
if ($this->utcInitialEpochMin !== null) {
393
$where[] = qsprintf(
394
$conn,
395
'event.utcInitialEpoch >= %d',
396
$this->utcInitialEpochMin);
397
}
398
399
if ($this->utcInitialEpochMax !== null) {
400
$where[] = qsprintf(
401
$conn,
402
'event.utcInitialEpoch <= %d',
403
$this->utcInitialEpochMax);
404
}
405
406
if ($this->inviteePHIDs !== null) {
407
$where[] = qsprintf(
408
$conn,
409
'invitee.inviteePHID IN (%Ls)',
410
$this->inviteePHIDs);
411
}
412
413
if ($this->hostPHIDs !== null) {
414
$where[] = qsprintf(
415
$conn,
416
'event.hostPHID IN (%Ls)',
417
$this->hostPHIDs);
418
}
419
420
if ($this->isCancelled !== null) {
421
$where[] = qsprintf(
422
$conn,
423
'event.isCancelled = %d',
424
(int)$this->isCancelled);
425
}
426
427
if ($this->eventsWithNoParent == true) {
428
$where[] = qsprintf(
429
$conn,
430
'event.instanceOfEventPHID IS NULL');
431
}
432
433
if ($this->instanceSequencePairs !== null) {
434
$sql = array();
435
436
foreach ($this->instanceSequencePairs as $pair) {
437
$sql[] = qsprintf(
438
$conn,
439
'(event.instanceOfEventPHID = %s AND event.sequenceIndex = %d)',
440
$pair[0],
441
$pair[1]);
442
}
443
444
$where[] = qsprintf(
445
$conn,
446
'%LO',
447
$sql);
448
}
449
450
if ($this->isStub !== null) {
451
$where[] = qsprintf(
452
$conn,
453
'event.isStub = %d',
454
(int)$this->isStub);
455
}
456
457
if ($this->parentEventPHIDs !== null) {
458
$where[] = qsprintf(
459
$conn,
460
'event.instanceOfEventPHID IN (%Ls)',
461
$this->parentEventPHIDs);
462
}
463
464
if ($this->importSourcePHIDs !== null) {
465
$where[] = qsprintf(
466
$conn,
467
'event.importSourcePHID IN (%Ls)',
468
$this->importSourcePHIDs);
469
}
470
471
if ($this->importAuthorPHIDs !== null) {
472
$where[] = qsprintf(
473
$conn,
474
'event.importAuthorPHID IN (%Ls)',
475
$this->importAuthorPHIDs);
476
}
477
478
if ($this->importUIDs !== null) {
479
$where[] = qsprintf(
480
$conn,
481
'event.importUID IN (%Ls)',
482
$this->importUIDs);
483
}
484
485
if ($this->isImported !== null) {
486
if ($this->isImported) {
487
$where[] = qsprintf(
488
$conn,
489
'event.importSourcePHID IS NOT NULL');
490
} else {
491
$where[] = qsprintf(
492
$conn,
493
'event.importSourcePHID IS NULL');
494
}
495
}
496
497
return $where;
498
}
499
500
protected function getPrimaryTableAlias() {
501
return 'event';
502
}
503
504
protected function shouldGroupQueryResultRows() {
505
if ($this->inviteePHIDs !== null) {
506
return true;
507
}
508
return parent::shouldGroupQueryResultRows();
509
}
510
511
public function getQueryApplicationClass() {
512
return 'PhabricatorCalendarApplication';
513
}
514
515
protected function willFilterPage(array $events) {
516
$instance_of_event_phids = array();
517
$recurring_events = array();
518
$viewer = $this->getViewer();
519
520
$events = $this->getEventsInRange($events);
521
522
$import_phids = array();
523
foreach ($events as $event) {
524
$import_phid = $event->getImportSourcePHID();
525
if ($import_phid !== null) {
526
$import_phids[$import_phid] = $import_phid;
527
}
528
}
529
530
if ($import_phids) {
531
$imports = id(new PhabricatorCalendarImportQuery())
532
->setParentQuery($this)
533
->setViewer($viewer)
534
->withPHIDs($import_phids)
535
->execute();
536
$imports = mpull($imports, null, 'getPHID');
537
} else {
538
$imports = array();
539
}
540
541
foreach ($events as $key => $event) {
542
$import_phid = $event->getImportSourcePHID();
543
if ($import_phid === null) {
544
$event->attachImportSource(null);
545
continue;
546
}
547
548
$import = idx($imports, $import_phid);
549
if (!$import) {
550
unset($events[$key]);
551
$this->didRejectResult($event);
552
continue;
553
}
554
555
$event->attachImportSource($import);
556
}
557
558
$phids = array();
559
560
foreach ($events as $event) {
561
$phids[] = $event->getPHID();
562
$instance_of = $event->getInstanceOfEventPHID();
563
564
if ($instance_of) {
565
$instance_of_event_phids[] = $instance_of;
566
}
567
}
568
569
if (count($instance_of_event_phids) > 0) {
570
$recurring_events = id(new PhabricatorCalendarEventQuery())
571
->setViewer($viewer)
572
->withPHIDs($instance_of_event_phids)
573
->withEventsWithNoParent(true)
574
->execute();
575
576
$recurring_events = mpull($recurring_events, null, 'getPHID');
577
}
578
579
if ($events) {
580
$invitees = id(new PhabricatorCalendarEventInviteeQuery())
581
->setViewer($viewer)
582
->withEventPHIDs($phids)
583
->execute();
584
$invitees = mgroup($invitees, 'getEventPHID');
585
} else {
586
$invitees = array();
587
}
588
589
foreach ($events as $key => $event) {
590
$event_invitees = idx($invitees, $event->getPHID(), array());
591
$event->attachInvitees($event_invitees);
592
593
$instance_of = $event->getInstanceOfEventPHID();
594
if (!$instance_of) {
595
continue;
596
}
597
$parent = idx($recurring_events, $instance_of);
598
599
// should never get here
600
if (!$parent) {
601
unset($events[$key]);
602
continue;
603
}
604
$event->attachParentEvent($parent);
605
606
if ($this->isCancelled !== null) {
607
if ($event->getIsCancelled() != $this->isCancelled) {
608
unset($events[$key]);
609
continue;
610
}
611
}
612
}
613
614
$events = msort($events, 'getStartDateTimeEpoch');
615
616
if ($this->needRSVPs) {
617
$rsvp_phids = $this->needRSVPs;
618
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
619
620
$project_phids = array();
621
foreach ($events as $event) {
622
foreach ($event->getInvitees() as $invitee) {
623
$invitee_phid = $invitee->getInviteePHID();
624
if (phid_get_type($invitee_phid) == $project_type) {
625
$project_phids[] = $invitee_phid;
626
}
627
}
628
}
629
630
if ($project_phids) {
631
$member_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
632
633
$query = id(new PhabricatorEdgeQuery())
634
->withSourcePHIDs($project_phids)
635
->withEdgeTypes(array($member_type))
636
->withDestinationPHIDs($rsvp_phids);
637
638
$edges = $query->execute();
639
640
$project_map = array();
641
foreach ($edges as $src => $types) {
642
foreach ($types as $type => $dsts) {
643
foreach ($dsts as $dst => $edge) {
644
$project_map[$dst][] = $src;
645
}
646
}
647
}
648
} else {
649
$project_map = array();
650
}
651
652
$membership_map = array();
653
foreach ($rsvp_phids as $rsvp_phid) {
654
$membership_map[$rsvp_phid] = array();
655
$membership_map[$rsvp_phid][] = $rsvp_phid;
656
657
$project_phids = idx($project_map, $rsvp_phid);
658
if ($project_phids) {
659
foreach ($project_phids as $project_phid) {
660
$membership_map[$rsvp_phid][] = $project_phid;
661
}
662
}
663
}
664
665
foreach ($events as $event) {
666
$invitees = $event->getInvitees();
667
$invitees = mpull($invitees, null, 'getInviteePHID');
668
669
$rsvp_map = array();
670
foreach ($rsvp_phids as $rsvp_phid) {
671
$membership_phids = $membership_map[$rsvp_phid];
672
$rsvps = array_select_keys($invitees, $membership_phids);
673
$rsvp_map[$rsvp_phid] = $rsvps;
674
}
675
676
$event->attachRSVPs($rsvp_map);
677
}
678
}
679
680
return $events;
681
}
682
683
private function getEventsInRange(array $events) {
684
$range_start = $this->rangeBegin;
685
$range_end = $this->rangeEnd;
686
687
foreach ($events as $key => $event) {
688
$event_start = $event->getStartDateTimeEpoch();
689
$event_end = $event->getEndDateTimeEpoch();
690
691
if ($range_start && $event_end < $range_start) {
692
unset($events[$key]);
693
}
694
695
if ($range_end && $event_start > $range_end) {
696
unset($events[$key]);
697
}
698
}
699
700
return $events;
701
}
702
703
private function getRecurrenceWindowStart(
704
PhabricatorCalendarEvent $event,
705
$generate_from) {
706
707
if (!$generate_from) {
708
return null;
709
}
710
711
return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from);
712
}
713
714
private function getRecurrenceWindowEnd(
715
PhabricatorCalendarEvent $event,
716
$generate_until) {
717
718
$end_epochs = array();
719
if ($generate_until) {
720
$end_epochs[] = $generate_until;
721
}
722
723
$until_epoch = $event->getUntilDateTimeEpoch();
724
if ($until_epoch) {
725
$end_epochs[] = $until_epoch;
726
}
727
728
if (!$end_epochs) {
729
return null;
730
}
731
732
return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs));
733
}
734
735
private function getRecurrenceLimit(
736
PhabricatorCalendarEvent $event,
737
$raw_limit) {
738
739
$count = $event->getRecurrenceCount();
740
if ($count && ($count <= $raw_limit)) {
741
return ($count - 1);
742
}
743
744
return $raw_limit;
745
}
746
747
}
748
749