Path: blob/master/src/applications/calendar/query/PhabricatorCalendarEventQuery.php
12256 views
<?php12final class PhabricatorCalendarEventQuery3extends PhabricatorCursorPagedPolicyAwareQuery {45private $ids;6private $phids;7private $rangeBegin;8private $rangeEnd;9private $inviteePHIDs;10private $hostPHIDs;11private $isCancelled;12private $eventsWithNoParent;13private $instanceSequencePairs;14private $isStub;15private $parentEventPHIDs;16private $importSourcePHIDs;17private $importAuthorPHIDs;18private $importUIDs;19private $utcInitialEpochMin;20private $utcInitialEpochMax;21private $isImported;22private $needRSVPs;2324private $generateGhosts = false;2526public function newResultObject() {27return new PhabricatorCalendarEvent();28}2930public function setGenerateGhosts($generate_ghosts) {31$this->generateGhosts = $generate_ghosts;32return $this;33}3435public function withIDs(array $ids) {36$this->ids = $ids;37return $this;38}3940public function withPHIDs(array $phids) {41$this->phids = $phids;42return $this;43}4445public function withDateRange($begin, $end) {46$this->rangeBegin = $begin;47$this->rangeEnd = $end;48return $this;49}5051public function withUTCInitialEpochBetween($min, $max) {52$this->utcInitialEpochMin = $min;53$this->utcInitialEpochMax = $max;54return $this;55}5657public function withInvitedPHIDs(array $phids) {58$this->inviteePHIDs = $phids;59return $this;60}6162public function withHostPHIDs(array $phids) {63$this->hostPHIDs = $phids;64return $this;65}6667public function withIsCancelled($is_cancelled) {68$this->isCancelled = $is_cancelled;69return $this;70}7172public function withIsStub($is_stub) {73$this->isStub = $is_stub;74return $this;75}7677public function withEventsWithNoParent($events_with_no_parent) {78$this->eventsWithNoParent = $events_with_no_parent;79return $this;80}8182public function withInstanceSequencePairs(array $pairs) {83$this->instanceSequencePairs = $pairs;84return $this;85}8687public function withParentEventPHIDs(array $parent_phids) {88$this->parentEventPHIDs = $parent_phids;89return $this;90}9192public function withImportSourcePHIDs(array $import_phids) {93$this->importSourcePHIDs = $import_phids;94return $this;95}9697public function withImportAuthorPHIDs(array $author_phids) {98$this->importAuthorPHIDs = $author_phids;99return $this;100}101102public function withImportUIDs(array $uids) {103$this->importUIDs = $uids;104return $this;105}106107public function withIsImported($is_imported) {108$this->isImported = $is_imported;109return $this;110}111112public function needRSVPs(array $phids) {113$this->needRSVPs = $phids;114return $this;115}116117protected function getDefaultOrderVector() {118return array('start', 'id');119}120121public function getBuiltinOrders() {122return array(123'start' => array(124'vector' => array('start', 'id'),125'name' => pht('Event Start'),126),127) + parent::getBuiltinOrders();128}129130public function getOrderableColumns() {131return array(132'start' => array(133'table' => $this->getPrimaryTableAlias(),134'column' => 'utcInitialEpoch',135'reverse' => true,136'type' => 'int',137'unique' => false,138),139) + parent::getOrderableColumns();140}141142protected function newPagingMapFromPartialObject($object) {143return array(144'id' => (int)$object->getID(),145'start' => (int)$object->getStartDateTimeEpoch(),146);147}148149protected function shouldLimitResults() {150// When generating ghosts, we can't rely on database ordering because151// MySQL can't predict the ghost start times. We'll just load all matching152// events, then generate results from there.153if ($this->generateGhosts) {154return false;155}156157return true;158}159160protected function loadPage() {161$events = $this->loadStandardPage($this->newResultObject());162163$viewer = $this->getViewer();164foreach ($events as $event) {165$event->applyViewerTimezone($viewer);166}167168if (!$this->generateGhosts) {169return $events;170}171172$raw_limit = $this->getRawResultLimit();173if (!$raw_limit && !$this->rangeEnd) {174throw new Exception(175pht(176'Event queries which generate ghost events must include either a '.177'result limit or an end date, because they may otherwise generate '.178'an infinite number of results. This query has neither.'));179}180181foreach ($events as $key => $event) {182$sequence_start = 0;183$sequence_end = null;184$end = null;185186$instance_of = $event->getInstanceOfEventPHID();187188if ($instance_of == null && $this->isCancelled !== null) {189if ($event->getIsCancelled() != $this->isCancelled) {190unset($events[$key]);191continue;192}193}194}195196// Pull out all of the parents first. We may discard them as we begin197// generating ghost events, but we still want to process all of them.198$parents = array();199foreach ($events as $key => $event) {200if ($event->isParentEvent()) {201$parents[$key] = $event;202}203}204205// Now that we've picked out all the parent events, we can immediately206// discard anything outside of the time window.207$events = $this->getEventsInRange($events);208209$generate_from = $this->rangeBegin;210$generate_until = $this->rangeEnd;211foreach ($parents as $key => $event) {212$duration = $event->getDuration();213214$start_date = $this->getRecurrenceWindowStart(215$event,216$generate_from - $duration);217218$end_date = $this->getRecurrenceWindowEnd(219$event,220$generate_until);221222$limit = $this->getRecurrenceLimit($event, $raw_limit);223224$set = $event->newRecurrenceSet();225226$recurrences = $set->getEventsBetween(227$start_date,228$end_date,229$limit + 1);230231// We're generating events from the beginning and then filtering them232// here (instead of only generating events starting at the start date)233// because we need to know the proper sequence indexes to generate ghost234// events. This may change after RDATE support.235if ($start_date) {236$start_epoch = $start_date->getEpoch();237} else {238$start_epoch = null;239}240241foreach ($recurrences as $sequence_index => $sequence_datetime) {242if (!$sequence_index) {243// This is the parent event, which we already have.244continue;245}246247if ($start_epoch) {248if ($sequence_datetime->getEpoch() < $start_epoch) {249continue;250}251}252253$events[] = $event->newGhost(254$viewer,255$sequence_index,256$sequence_datetime);257}258259// NOTE: We're slicing results every time because this makes it cheaper260// to generate future ghosts. If we already have 100 events that occur261// before July 1, we know we never need to generate ghosts after that262// because they couldn't possibly ever appear in the result set.263264if ($raw_limit) {265if (count($events) > $raw_limit) {266$events = msort($events, 'getStartDateTimeEpoch');267$events = array_slice($events, 0, $raw_limit, true);268$generate_until = last($events)->getEndDateTimeEpoch();269}270}271}272273// Now that we're done generating ghost events, we're going to remove any274// ghosts that we have concrete events for (or which we can load the275// concrete events for). These concrete events are generated when users276// edit a ghost, and replace the ghost events.277278// First, generate a map of all concrete <parentPHID, sequence> events we279// already loaded. We don't need to load these again.280$have_pairs = array();281foreach ($events as $event) {282if ($event->getIsGhostEvent()) {283continue;284}285286$parent_phid = $event->getInstanceOfEventPHID();287$sequence = $event->getSequenceIndex();288289$have_pairs[$parent_phid][$sequence] = true;290}291292// Now, generate a map of all <parentPHID, sequence> events we generated293// ghosts for. We need to try to load these if we don't already have them.294$map = array();295$parent_pairs = array();296foreach ($events as $key => $event) {297if (!$event->getIsGhostEvent()) {298continue;299}300301$parent_phid = $event->getInstanceOfEventPHID();302$sequence = $event->getSequenceIndex();303304// We already loaded the concrete version of this event, so we can just305// throw out the ghost and move on.306if (isset($have_pairs[$parent_phid][$sequence])) {307unset($events[$key]);308continue;309}310311// We didn't load the concrete version of this event, so we need to312// try to load it if it exists.313$parent_pairs[] = array($parent_phid, $sequence);314$map[$parent_phid][$sequence] = $key;315}316317if ($parent_pairs) {318$instances = id(new self())319->setViewer($viewer)320->setParentQuery($this)321->withInstanceSequencePairs($parent_pairs)322->execute();323324foreach ($instances as $instance) {325$parent_phid = $instance->getInstanceOfEventPHID();326$sequence = $instance->getSequenceIndex();327328$indexes = idx($map, $parent_phid);329$key = idx($indexes, $sequence);330331// Replace the ghost with the corresponding concrete event.332$events[$key] = $instance;333}334}335336$events = msort($events, 'getStartDateTimeEpoch');337338return $events;339}340341protected function buildJoinClauseParts(AphrontDatabaseConnection $conn_r) {342$parts = parent::buildJoinClauseParts($conn_r);343344if ($this->inviteePHIDs !== null) {345$parts[] = qsprintf(346$conn_r,347'JOIN %T invitee ON invitee.eventPHID = event.phid348AND invitee.status != %s',349id(new PhabricatorCalendarEventInvitee())->getTableName(),350PhabricatorCalendarEventInvitee::STATUS_UNINVITED);351}352353return $parts;354}355356protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {357$where = parent::buildWhereClauseParts($conn);358359if ($this->ids !== null) {360$where[] = qsprintf(361$conn,362'event.id IN (%Ld)',363$this->ids);364}365366if ($this->phids !== null) {367$where[] = qsprintf(368$conn,369'event.phid IN (%Ls)',370$this->phids);371}372373// NOTE: The date ranges we query for are larger than the requested ranges374// because we need to catch all-day events. We'll refine this range later375// after adjusting the visible range of events we load.376377if ($this->rangeBegin) {378$where[] = qsprintf(379$conn,380'(event.utcUntilEpoch >= %d) OR (event.utcUntilEpoch IS NULL)',381$this->rangeBegin - phutil_units('16 hours in seconds'));382}383384if ($this->rangeEnd) {385$where[] = qsprintf(386$conn,387'event.utcInitialEpoch <= %d',388$this->rangeEnd + phutil_units('16 hours in seconds'));389}390391if ($this->utcInitialEpochMin !== null) {392$where[] = qsprintf(393$conn,394'event.utcInitialEpoch >= %d',395$this->utcInitialEpochMin);396}397398if ($this->utcInitialEpochMax !== null) {399$where[] = qsprintf(400$conn,401'event.utcInitialEpoch <= %d',402$this->utcInitialEpochMax);403}404405if ($this->inviteePHIDs !== null) {406$where[] = qsprintf(407$conn,408'invitee.inviteePHID IN (%Ls)',409$this->inviteePHIDs);410}411412if ($this->hostPHIDs !== null) {413$where[] = qsprintf(414$conn,415'event.hostPHID IN (%Ls)',416$this->hostPHIDs);417}418419if ($this->isCancelled !== null) {420$where[] = qsprintf(421$conn,422'event.isCancelled = %d',423(int)$this->isCancelled);424}425426if ($this->eventsWithNoParent == true) {427$where[] = qsprintf(428$conn,429'event.instanceOfEventPHID IS NULL');430}431432if ($this->instanceSequencePairs !== null) {433$sql = array();434435foreach ($this->instanceSequencePairs as $pair) {436$sql[] = qsprintf(437$conn,438'(event.instanceOfEventPHID = %s AND event.sequenceIndex = %d)',439$pair[0],440$pair[1]);441}442443$where[] = qsprintf(444$conn,445'%LO',446$sql);447}448449if ($this->isStub !== null) {450$where[] = qsprintf(451$conn,452'event.isStub = %d',453(int)$this->isStub);454}455456if ($this->parentEventPHIDs !== null) {457$where[] = qsprintf(458$conn,459'event.instanceOfEventPHID IN (%Ls)',460$this->parentEventPHIDs);461}462463if ($this->importSourcePHIDs !== null) {464$where[] = qsprintf(465$conn,466'event.importSourcePHID IN (%Ls)',467$this->importSourcePHIDs);468}469470if ($this->importAuthorPHIDs !== null) {471$where[] = qsprintf(472$conn,473'event.importAuthorPHID IN (%Ls)',474$this->importAuthorPHIDs);475}476477if ($this->importUIDs !== null) {478$where[] = qsprintf(479$conn,480'event.importUID IN (%Ls)',481$this->importUIDs);482}483484if ($this->isImported !== null) {485if ($this->isImported) {486$where[] = qsprintf(487$conn,488'event.importSourcePHID IS NOT NULL');489} else {490$where[] = qsprintf(491$conn,492'event.importSourcePHID IS NULL');493}494}495496return $where;497}498499protected function getPrimaryTableAlias() {500return 'event';501}502503protected function shouldGroupQueryResultRows() {504if ($this->inviteePHIDs !== null) {505return true;506}507return parent::shouldGroupQueryResultRows();508}509510public function getQueryApplicationClass() {511return 'PhabricatorCalendarApplication';512}513514protected function willFilterPage(array $events) {515$instance_of_event_phids = array();516$recurring_events = array();517$viewer = $this->getViewer();518519$events = $this->getEventsInRange($events);520521$import_phids = array();522foreach ($events as $event) {523$import_phid = $event->getImportSourcePHID();524if ($import_phid !== null) {525$import_phids[$import_phid] = $import_phid;526}527}528529if ($import_phids) {530$imports = id(new PhabricatorCalendarImportQuery())531->setParentQuery($this)532->setViewer($viewer)533->withPHIDs($import_phids)534->execute();535$imports = mpull($imports, null, 'getPHID');536} else {537$imports = array();538}539540foreach ($events as $key => $event) {541$import_phid = $event->getImportSourcePHID();542if ($import_phid === null) {543$event->attachImportSource(null);544continue;545}546547$import = idx($imports, $import_phid);548if (!$import) {549unset($events[$key]);550$this->didRejectResult($event);551continue;552}553554$event->attachImportSource($import);555}556557$phids = array();558559foreach ($events as $event) {560$phids[] = $event->getPHID();561$instance_of = $event->getInstanceOfEventPHID();562563if ($instance_of) {564$instance_of_event_phids[] = $instance_of;565}566}567568if (count($instance_of_event_phids) > 0) {569$recurring_events = id(new PhabricatorCalendarEventQuery())570->setViewer($viewer)571->withPHIDs($instance_of_event_phids)572->withEventsWithNoParent(true)573->execute();574575$recurring_events = mpull($recurring_events, null, 'getPHID');576}577578if ($events) {579$invitees = id(new PhabricatorCalendarEventInviteeQuery())580->setViewer($viewer)581->withEventPHIDs($phids)582->execute();583$invitees = mgroup($invitees, 'getEventPHID');584} else {585$invitees = array();586}587588foreach ($events as $key => $event) {589$event_invitees = idx($invitees, $event->getPHID(), array());590$event->attachInvitees($event_invitees);591592$instance_of = $event->getInstanceOfEventPHID();593if (!$instance_of) {594continue;595}596$parent = idx($recurring_events, $instance_of);597598// should never get here599if (!$parent) {600unset($events[$key]);601continue;602}603$event->attachParentEvent($parent);604605if ($this->isCancelled !== null) {606if ($event->getIsCancelled() != $this->isCancelled) {607unset($events[$key]);608continue;609}610}611}612613$events = msort($events, 'getStartDateTimeEpoch');614615if ($this->needRSVPs) {616$rsvp_phids = $this->needRSVPs;617$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;618619$project_phids = array();620foreach ($events as $event) {621foreach ($event->getInvitees() as $invitee) {622$invitee_phid = $invitee->getInviteePHID();623if (phid_get_type($invitee_phid) == $project_type) {624$project_phids[] = $invitee_phid;625}626}627}628629if ($project_phids) {630$member_type = PhabricatorProjectMaterializedMemberEdgeType::EDGECONST;631632$query = id(new PhabricatorEdgeQuery())633->withSourcePHIDs($project_phids)634->withEdgeTypes(array($member_type))635->withDestinationPHIDs($rsvp_phids);636637$edges = $query->execute();638639$project_map = array();640foreach ($edges as $src => $types) {641foreach ($types as $type => $dsts) {642foreach ($dsts as $dst => $edge) {643$project_map[$dst][] = $src;644}645}646}647} else {648$project_map = array();649}650651$membership_map = array();652foreach ($rsvp_phids as $rsvp_phid) {653$membership_map[$rsvp_phid] = array();654$membership_map[$rsvp_phid][] = $rsvp_phid;655656$project_phids = idx($project_map, $rsvp_phid);657if ($project_phids) {658foreach ($project_phids as $project_phid) {659$membership_map[$rsvp_phid][] = $project_phid;660}661}662}663664foreach ($events as $event) {665$invitees = $event->getInvitees();666$invitees = mpull($invitees, null, 'getInviteePHID');667668$rsvp_map = array();669foreach ($rsvp_phids as $rsvp_phid) {670$membership_phids = $membership_map[$rsvp_phid];671$rsvps = array_select_keys($invitees, $membership_phids);672$rsvp_map[$rsvp_phid] = $rsvps;673}674675$event->attachRSVPs($rsvp_map);676}677}678679return $events;680}681682private function getEventsInRange(array $events) {683$range_start = $this->rangeBegin;684$range_end = $this->rangeEnd;685686foreach ($events as $key => $event) {687$event_start = $event->getStartDateTimeEpoch();688$event_end = $event->getEndDateTimeEpoch();689690if ($range_start && $event_end < $range_start) {691unset($events[$key]);692}693694if ($range_end && $event_start > $range_end) {695unset($events[$key]);696}697}698699return $events;700}701702private function getRecurrenceWindowStart(703PhabricatorCalendarEvent $event,704$generate_from) {705706if (!$generate_from) {707return null;708}709710return PhutilCalendarAbsoluteDateTime::newFromEpoch($generate_from);711}712713private function getRecurrenceWindowEnd(714PhabricatorCalendarEvent $event,715$generate_until) {716717$end_epochs = array();718if ($generate_until) {719$end_epochs[] = $generate_until;720}721722$until_epoch = $event->getUntilDateTimeEpoch();723if ($until_epoch) {724$end_epochs[] = $until_epoch;725}726727if (!$end_epochs) {728return null;729}730731return PhutilCalendarAbsoluteDateTime::newFromEpoch(min($end_epochs));732}733734private function getRecurrenceLimit(735PhabricatorCalendarEvent $event,736$raw_limit) {737738$count = $event->getRecurrenceCount();739if ($count && ($count <= $raw_limit)) {740return ($count - 1);741}742743return $raw_limit;744}745746}747748749