Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / calendar / query / PhabricatorCalendarEventQuery.php
blobdb50bb4d77355f95fd7253f4dd417141935b66d5
1 <?php
3 final class PhabricatorCalendarEventQuery
4 extends PhabricatorCursorPagedPolicyAwareQuery {
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;
25 private $generateGhosts = false;
27 public function newResultObject() {
28 return new PhabricatorCalendarEvent();
31 public function setGenerateGhosts($generate_ghosts) {
32 $this->generateGhosts = $generate_ghosts;
33 return $this;
36 public function withIDs(array $ids) {
37 $this->ids = $ids;
38 return $this;
41 public function withPHIDs(array $phids) {
42 $this->phids = $phids;
43 return $this;
46 public function withDateRange($begin, $end) {
47 $this->rangeBegin = $begin;
48 $this->rangeEnd = $end;
49 return $this;
52 public function withUTCInitialEpochBetween($min, $max) {
53 $this->utcInitialEpochMin = $min;
54 $this->utcInitialEpochMax = $max;
55 return $this;
58 public function withInvitedPHIDs(array $phids) {
59 $this->inviteePHIDs = $phids;
60 return $this;
63 public function withHostPHIDs(array $phids) {
64 $this->hostPHIDs = $phids;
65 return $this;
68 public function withIsCancelled($is_cancelled) {
69 $this->isCancelled = $is_cancelled;
70 return $this;
73 public function withIsStub($is_stub) {
74 $this->isStub = $is_stub;
75 return $this;
78 public function withEventsWithNoParent($events_with_no_parent) {
79 $this->eventsWithNoParent = $events_with_no_parent;
80 return $this;
83 public function withInstanceSequencePairs(array $pairs) {
84 $this->instanceSequencePairs = $pairs;
85 return $this;
88 public function withParentEventPHIDs(array $parent_phids) {
89 $this->parentEventPHIDs = $parent_phids;
90 return $this;
93 public function withImportSourcePHIDs(array $import_phids) {
94 $this->importSourcePHIDs = $import_phids;
95 return $this;
98 public function withImportAuthorPHIDs(array $author_phids) {
99 $this->importAuthorPHIDs = $author_phids;
100 return $this;
103 public function withImportUIDs(array $uids) {
104 $this->importUIDs = $uids;
105 return $this;
108 public function withIsImported($is_imported) {
109 $this->isImported = $is_imported;
110 return $this;
113 public function needRSVPs(array $phids) {
114 $this->needRSVPs = $phids;
115 return $this;
118 protected function getDefaultOrderVector() {
119 return array('start', 'id');
122 public function getBuiltinOrders() {
123 return array(
124 'start' => array(
125 'vector' => array('start', 'id'),
126 'name' => pht('Event Start'),
128 ) + parent::getBuiltinOrders();
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,
140 ) + parent::getOrderableColumns();
143 protected function newPagingMapFromPartialObject($object) {
144 return array(
145 'id' => (int)$object->getID(),
146 'start' => (int)$object->getStartDateTimeEpoch(),
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;
158 return true;
161 protected function loadPage() {
162 $events = $this->loadStandardPage($this->newResultObject());
164 $viewer = $this->getViewer();
165 foreach ($events as $event) {
166 $event->applyViewerTimezone($viewer);
169 if (!$this->generateGhosts) {
170 return $events;
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.'));
182 foreach ($events as $key => $event) {
183 $sequence_start = 0;
184 $sequence_end = null;
185 $end = null;
187 $instance_of = $event->getInstanceOfEventPHID();
189 if ($instance_of == null && $this->isCancelled !== null) {
190 if ($event->getIsCancelled() != $this->isCancelled) {
191 unset($events[$key]);
192 continue;
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;
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);
210 $generate_from = $this->rangeBegin;
211 $generate_until = $this->rangeEnd;
212 foreach ($parents as $key => $event) {
213 $duration = $event->getDuration();
215 $start_date = $this->getRecurrenceWindowStart(
216 $event,
217 $generate_from - $duration);
219 $end_date = $this->getRecurrenceWindowEnd(
220 $event,
221 $generate_until);
223 $limit = $this->getRecurrenceLimit($event, $raw_limit);
225 $set = $event->newRecurrenceSet();
227 $recurrences = $set->getEventsBetween(
228 $start_date,
229 $end_date,
230 $limit + 1);
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;
242 foreach ($recurrences as $sequence_index => $sequence_datetime) {
243 if (!$sequence_index) {
244 // This is the parent event, which we already have.
245 continue;
248 if ($start_epoch) {
249 if ($sequence_datetime->getEpoch() < $start_epoch) {
250 continue;
254 $events[] = $event->newGhost(
255 $viewer,
256 $sequence_index,
257 $sequence_datetime);
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.
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();
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.
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;
287 $parent_phid = $event->getInstanceOfEventPHID();
288 $sequence = $event->getSequenceIndex();
290 $have_pairs[$parent_phid][$sequence] = true;
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;
302 $parent_phid = $event->getInstanceOfEventPHID();
303 $sequence = $event->getSequenceIndex();
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;
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;
318 if ($parent_pairs) {
319 $instances = id(new self())
320 ->setViewer($viewer)
321 ->setParentQuery($this)
322 ->withInstanceSequencePairs($parent_pairs)
323 ->execute();
325 foreach ($instances as $instance) {
326 $parent_phid = $instance->getInstanceOfEventPHID();
327 $sequence = $instance->getSequenceIndex();
329 $indexes = idx($map, $parent_phid);
330 $key = idx($indexes, $sequence);
332 // Replace the ghost with the corresponding concrete event.
333 $events[$key] = $instance;
337 $events = msort($events, 'getStartDateTimeEpoch');
339 return $events;
342 protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {
343 $parts = parent::buildJoinClauseParts($conn_r);
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);
354 return $parts;
357 protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
358 $where = parent::buildWhereClauseParts($conn);
360 if ($this->ids !== null) {
361 $where[] = qsprintf(
362 $conn,
363 'event.id IN (%Ld)',
364 $this->ids);
367 if ($this->phids !== null) {
368 $where[] = qsprintf(
369 $conn,
370 'event.phid IN (%Ls)',
371 $this->phids);
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.
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'));
385 if ($this->rangeEnd) {
386 $where[] = qsprintf(
387 $conn,
388 'event.utcInitialEpoch <= %d',
389 $this->rangeEnd + phutil_units('16 hours in seconds'));
392 if ($this->utcInitialEpochMin !== null) {
393 $where[] = qsprintf(
394 $conn,
395 'event.utcInitialEpoch >= %d',
396 $this->utcInitialEpochMin);
399 if ($this->utcInitialEpochMax !== null) {
400 $where[] = qsprintf(
401 $conn,
402 'event.utcInitialEpoch <= %d',
403 $this->utcInitialEpochMax);
406 if ($this->inviteePHIDs !== null) {
407 $where[] = qsprintf(
408 $conn,
409 'invitee.inviteePHID IN (%Ls)',
410 $this->inviteePHIDs);
413 if ($this->hostPHIDs !== null) {
414 $where[] = qsprintf(
415 $conn,
416 'event.hostPHID IN (%Ls)',
417 $this->hostPHIDs);
420 if ($this->isCancelled !== null) {
421 $where[] = qsprintf(
422 $conn,
423 'event.isCancelled = %d',
424 (int)$this->isCancelled);
427 if ($this->eventsWithNoParent == true) {
428 $where[] = qsprintf(
429 $conn,
430 'event.instanceOfEventPHID IS NULL');
433 if ($this->instanceSequencePairs !== null) {
434 $sql = array();
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]);
444 $where[] = qsprintf(
445 $conn,
446 '%LO',
447 $sql);
450 if ($this->isStub !== null) {
451 $where[] = qsprintf(
452 $conn,
453 'event.isStub = %d',
454 (int)$this->isStub);
457 if ($this->parentEventPHIDs !== null) {
458 $where[] = qsprintf(
459 $conn,
460 'event.instanceOfEventPHID IN (%Ls)',
461 $this->parentEventPHIDs);
464 if ($this->importSourcePHIDs !== null) {
465 $where[] = qsprintf(
466 $conn,
467 'event.importSourcePHID IN (%Ls)',
468 $this->importSourcePHIDs);
471 if ($this->importAuthorPHIDs !== null) {
472 $where[] = qsprintf(
473 $conn,
474 'event.importAuthorPHID IN (%Ls)',
475 $this->importAuthorPHIDs);
478 if ($this->importUIDs !== null) {
479 $where[] = qsprintf(
480 $conn,
481 'event.importUID IN (%Ls)',
482 $this->importUIDs);
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');
497 return $where;
500 protected function getPrimaryTableAlias() {
501 return 'event';
504 protected function shouldGroupQueryResultRows() {
505 if ($this->inviteePHIDs !== null) {
506 return true;
508 return parent::shouldGroupQueryResultRows();
511 public function getQueryApplicationClass() {
512 return 'PhabricatorCalendarApplication';
515 protected function willFilterPage(array $events) {
516 $instance_of_event_phids = array();
517 $recurring_events = array();
518 $viewer = $this->getViewer();
520 $events = $this->getEventsInRange($events);
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;
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();
541 foreach ($events as $key => $event) {
542 $import_phid = $event->getImportSourcePHID();
543 if ($import_phid === null) {
544 $event->attachImportSource(null);
545 continue;
548 $import = idx($imports, $import_phid);
549 if (!$import) {
550 unset($events[$key]);
551 $this->didRejectResult($event);
552 continue;
555 $event->attachImportSource($import);
558 $phids = array();
560 foreach ($events as $event) {
561 $phids[] = $event->getPHID();
562 $instance_of = $event->getInstanceOfEventPHID();
564 if ($instance_of) {
565 $instance_of_event_phids[] = $instance_of;
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();
576 $recurring_events = mpull($recurring_events, null, 'getPHID');
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();
589 foreach ($events as $key => $event) {
590 $event_invitees = idx($invitees, $event->getPHID(), array());
591 $event->attachInvitees($event_invitees);
593 $instance_of = $event->getInstanceOfEventPHID();
594 if (!$instance_of) {
595 continue;
597 $parent = idx($recurring_events, $instance_of);
599 // should never get here
600 if (!$parent) {
601 unset($events[$key]);
602 continue;
604 $event->attachParentEvent($parent);
606 if ($this->isCancelled !== null) {
607 if ($event->getIsCancelled() != $this->isCancelled) {
608 unset($events[$key]);
609 continue;
614 $events = msort($events, 'getStartDateTimeEpoch');
616 if ($this->needRSVPs) {
617 $rsvp_phids = $this->needRSVPs;
618 $project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
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;
630 if ($project_phids) {
631 $member_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;
633 $query = id(new PhabricatorEdgeQuery())
634 ->withSourcePHIDs($project_phids)
635 ->withEdgeTypes(array($member_type))
636 ->withDestinationPHIDs($rsvp_phids);
638 $edges = $query->execute();
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;
648 } else {
649 $project_map = array();
652 $membership_map = array();
653 foreach ($rsvp_phids as $rsvp_phid) {
654 $membership_map[$rsvp_phid] = array();
655 $membership_map[$rsvp_phid][] = $rsvp_phid;
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;
665 foreach ($events as $event) {
666 $invitees = $event->getInvitees();
667 $invitees = mpull($invitees, null, 'getInviteePHID');
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;
676 $event->attachRSVPs($rsvp_map);
680 return $events;
683 private function getEventsInRange(array $events) {
684 $range_start = $this->rangeBegin;
685 $range_end = $this->rangeEnd;
687 foreach ($events as $key => $event) {
688 $event_start = $event->getStartDateTimeEpoch();
689 $event_end = $event->getEndDateTimeEpoch();
691 if ($range_start && $event_end < $range_start) {
692 unset($events[$key]);
695 if ($range_end && $event_start > $range_end) {
696 unset($events[$key]);
700 return $events;
703 private function getRecurrenceWindowStart(
704 PhabricatorCalendarEvent $event,
705 $generate_from) {
707 if (!$generate_from) {
708 return null;
711 return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from);
714 private function getRecurrenceWindowEnd(
715 PhabricatorCalendarEvent $event,
716 $generate_until) {
718 $end_epochs = array();
719 if ($generate_until) {
720 $end_epochs[] = $generate_until;
723 $until_epoch = $event->getUntilDateTimeEpoch();
724 if ($until_epoch) {
725 $end_epochs[] = $until_epoch;
728 if (!$end_epochs) {
729 return null;
732 return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs));
735 private function getRecurrenceLimit(
736 PhabricatorCalendarEvent $event,
737 $raw_limit) {
739 $count = $event->getRecurrenceCount();
740 if ($count && ($count <= $raw_limit)) {
741 return ($count - 1);
744 return $raw_limit;