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