diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 2dab43aa5d..fb94a74cbf 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2080,6 +2080,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportEditController' => 'applications/calendar/controller/PhabricatorCalendarExportEditController.php', 'PhabricatorCalendarExportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarExportEditEngine.php', 'PhabricatorCalendarExportEditor' => 'applications/calendar/editor/PhabricatorCalendarExportEditor.php', + 'PhabricatorCalendarExportICSController' => 'applications/calendar/controller/PhabricatorCalendarExportICSController.php', 'PhabricatorCalendarExportListController' => 'applications/calendar/controller/PhabricatorCalendarExportListController.php', 'PhabricatorCalendarExportModeTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportModeTransaction.php', 'PhabricatorCalendarExportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarExportNameTransaction.php', @@ -6851,6 +6852,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarExportEditEngine' => 'PhabricatorEditEngine', 'PhabricatorCalendarExportEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorCalendarExportICSController' => 'PhabricatorCalendarController', 'PhabricatorCalendarExportListController' => 'PhabricatorCalendarController', 'PhabricatorCalendarExportModeTransaction' => 'PhabricatorCalendarExportTransactionType', 'PhabricatorCalendarExportNameTransaction' => 'PhabricatorCalendarExportTransactionType', diff --git a/src/applications/calendar/controller/PhabricatorCalendarController.php b/src/applications/calendar/controller/PhabricatorCalendarController.php index 01ea367aeb..9a811b6a8d 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarController.php @@ -2,4 +2,43 @@ abstract class PhabricatorCalendarController extends PhabricatorController { + protected function newICSResponse( + PhabricatorUser $viewer, + $file_name, + array $events) { + $events = mpull($events, null, 'getPHID'); + + if ($events) { + $child_map = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withParentEventPHIDs(array_keys($events)) + ->execute(); + $child_map = mpull($child_map, null, 'getPHID'); + } else { + $child_map = array(); + } + + $all_events = $events + $child_map; + $child_groups = mgroup($child_map, 'getInstanceOfEventPHID'); + + $document_node = new PhutilCalendarDocumentNode(); + + foreach ($all_events as $event) { + $child_events = idx($child_groups, $event->getPHID(), array()); + $event_node = $event->newIntermediateEventNode($viewer, $child_events); + $document_node->appendChild($event_node); + } + + $root_node = id(new PhutilCalendarRootNode()) + ->appendChild($document_node); + + $ics_data = id(new PhutilICSWriter()) + ->writeICSDocument($root_node); + + return id(new AphrontFileResponse()) + ->setDownload($file_name) + ->setMimeType('text/calendar') + ->setContent($ics_data); + } + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventExportController.php b/src/applications/calendar/controller/PhabricatorCalendarEventExportController.php index 8163e4abeb..5e29479169 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventExportController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventExportController.php @@ -19,22 +19,16 @@ final class PhabricatorCalendarEventExportController return new Aphront404Response(); } - $file_name = $event->getICSFilename(); - $event_node = $event->newIntermediateEventNode($viewer); + if ($event->isChildEvent()) { + $target = $event->getParentEvent(); + } else { + $target = $event; + } - $document_node = id(new PhutilCalendarDocumentNode()) - ->appendChild($event_node); - - $root_node = id(new PhutilCalendarRootNode()) - ->appendChild($document_node); - - $ics_data = id(new PhutilICSWriter()) - ->writeICSDocument($root_node); - - return id(new AphrontFileResponse()) - ->setDownload($file_name) - ->setMimeType('text/calendar') - ->setContent($ics_data); + return $this->newICSResponse( + $viewer, + $target->getICSFileName(), + array($target)); } } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php index a2915b0be9..ac9015fbcf 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php @@ -11,17 +11,19 @@ final class PhabricatorCalendarEventListController $year = $request->getURIData('year'); $month = $request->getURIData('month'); $day = $request->getURIData('day'); + $engine = new PhabricatorCalendarEventSearchEngine(); if ($month && $year) { $engine->setCalendarYearAndMonthAndDay($year, $month, $day); } - $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($request->getURIData('queryKey')) - ->setSearchEngine($engine); + $nav_items = $this->buildNavigationItems(); - return $this->delegateToController($controller); + return $engine + ->setNavigationItems($nav_items) + ->setController($this) + ->buildResponse(); } protected function buildApplicationCrumbs() { @@ -34,4 +36,18 @@ final class PhabricatorCalendarEventListController return $crumbs; } + protected function buildNavigationItems() { + $items = array(); + + $items[] = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LABEL) + ->setName(pht('Import/Export')); + + $items[] = id(new PHUIListItemView()) + ->setName('Exports') + ->setHref('/calendar/export/'); + + return $items; + } + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php b/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php new file mode 100644 index 0000000000..04a116a740 --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarExportICSController.php @@ -0,0 +1,93 @@ +setViewer($omnipotent) + ->withSecretKeys(array($request->getURIData('secretKey'))) + ->executeOne(); + if (!$export) { + return new Aphront404Response(); + } + + $author = id(new PhabricatorPeopleQuery()) + ->setViewer($omnipotent) + ->withPHIDs(array($export->getAuthorPHID())) + ->needUserSettings(true) + ->executeOne(); + if (!$author) { + return new Aphront404Response(); + } + + $mode = $export->getPolicyMode(); + switch ($mode) { + case PhabricatorCalendarExport::MODE_PUBLIC: + $viewer = new PhabricatorUser(); + break; + case PhabricatorCalendarExport::MODE_PRIVILEGED: + $viewer = $author; + break; + default: + throw new Exception( + pht( + 'This export has an invalid mode ("%s").', + $mode)); + } + + $engine = id(new PhabricatorCalendarEventSearchEngine()) + ->setViewer($viewer); + + $query_key = $export->getQueryKey(); + $saved = id(new PhabricatorSavedQueryQuery()) + ->setViewer($omnipotent) + ->withEngineClassNames(array(get_class($engine))) + ->withQueryKeys(array($query_key)) + ->executeOne(); + if (!$saved) { + $saved = $engine->buildSavedQueryFromBuiltin($query_key); + } + + if (!$saved) { + return new Aphront404Response(); + } + + $saved = clone $saved; + + // Mark this as a query for export, so we get the correct ghost/recurring + // behaviors. We also want to load all matching events. + $saved->setParameter('export', true); + $saved->setParameter('limit', 0xFFFF); + + // Remove any range constraints. We always export all matching events into + // ICS files. + $saved->setParameter('rangeStart', null); + $saved->setParameter('rangeEnd', null); + $saved->setParameter('upcoming', null); + + $query = $engine->buildQueryFromSavedQuery($saved); + + $events = $query + ->setViewer($viewer) + ->execute(); + + return $this->newICSResponse( + $viewer, + $export->getICSFilename(), + $events); + } + +} diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index 06eb50cc54..9b928c9d1a 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -13,6 +13,7 @@ final class PhabricatorCalendarEventQuery private $eventsWithNoParent; private $instanceSequencePairs; private $isStub; + private $parentEventPHIDs; private $generateGhosts = false; @@ -71,6 +72,11 @@ final class PhabricatorCalendarEventQuery return $this; } + public function withParentEventPHIDs(array $parent_phids) { + $this->parentEventPHIDs = $parent_phids; + return $this; + } + protected function getDefaultOrderVector() { return array('start', 'id'); } @@ -315,14 +321,14 @@ final class PhabricatorCalendarEventQuery protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( $conn, 'event.id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( $conn, 'event.phid IN (%Ls)', @@ -354,7 +360,7 @@ final class PhabricatorCalendarEventQuery $this->inviteePHIDs); } - if ($this->hostPHIDs) { + if ($this->hostPHIDs !== null) { $where[] = qsprintf( $conn, 'event.hostPHID IN (%Ls)', @@ -398,6 +404,13 @@ final class PhabricatorCalendarEventQuery (int)$this->isStub); } + if ($this->parentEventPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'event.instanceOfEventPHID IN (%Ls)', + $this->parentEventPHIDs); + } + return $where; } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 64c0c39aec..a29b7ea37a 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -115,8 +115,12 @@ final class PhabricatorCalendarEventSearchEngine } // Generate ghosts (and ignore stub events) if we aren't querying for - // specific events. - if (!$map['ids'] && !$map['phids']) { + // specific events or exporting. + if (!empty($map['export'])) { + // This is a specific mode enabled by event exports. + $query + ->withIsStub(false); + } else if (!$map['ids'] && !$map['phids']) { $query ->withIsStub(false) ->setGenerateGhosts(true); diff --git a/src/applications/calendar/query/PhabricatorCalendarExportQuery.php b/src/applications/calendar/query/PhabricatorCalendarExportQuery.php index ecd3dc8e45..c51671c806 100644 --- a/src/applications/calendar/query/PhabricatorCalendarExportQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarExportQuery.php @@ -6,6 +6,7 @@ final class PhabricatorCalendarExportQuery private $ids; private $phids; private $authorPHIDs; + private $secretKeys; private $isDisabled; public function withIDs(array $ids) { @@ -28,6 +29,11 @@ final class PhabricatorCalendarExportQuery return $this; } + public function withSecretKeys(array $keys) { + $this->secretKeys = $keys; + return $this; + } + public function newResultObject() { return new PhabricatorCalendarExport(); } @@ -67,6 +73,13 @@ final class PhabricatorCalendarExportQuery (int)$this->isDisabled); } + if ($this->secretKeys !== null) { + $where[] = qsprintf( + $conn, + 'export.secretKey IN (%Ls)', + $this->secretKeys); + } + return $where; } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 676d85b608..67a04d226e 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -601,11 +601,28 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return $this->getMonogram().'.ics'; } - public function newIntermediateEventNode(PhabricatorUser $viewer) { + public function newIntermediateEventNode( + PhabricatorUser $viewer, + array $children) { + $base_uri = new PhutilURI(PhabricatorEnv::getProductionURI('/')); $domain = $base_uri->getDomain(); - $uid = $this->getPHID().'@'.$domain; + // NOTE: For recurring events, all of the events in the series have the + // same UID (the UID of the parent). The child event instances are + // differentiated by the "RECURRENCE-ID" field. + if ($this->isChildEvent()) { + $parent = $this->getParentEvent(); + $instance_datetime = PhutilCalendarAbsoluteDateTime::newFromEpoch( + $this->getUTCInstanceEpoch()); + $recurrence_id = $instance_datetime->getISO8601(); + $rrule = null; + } else { + $parent = $this; + $recurrence_id = null; + $rrule = $this->newRecurrenceRule(); + } + $uid = $parent->getPHID().'@'.$domain; $created = $this->getDateCreated(); $created = PhutilCalendarAbsoluteDateTime::newFromEpoch($created); @@ -674,6 +691,8 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setStatus($status); } + // TODO: Use $children to generate EXDATE/RDATE information. + $node = id(new PhutilCalendarEventNode()) ->setUID($uid) ->setName($this->getName()) @@ -685,6 +704,14 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setOrganizer($organizer) ->setAttendees($attendees); + if ($rrule) { + $node->setRecurrenceRule($rrule); + } + + if ($recurrence_id) { + $node->setRecurrenceID($recurrence_id); + } + return $node; } @@ -833,6 +860,11 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO $start = $this->newStartDateTime(); $rrule->setStartDateTime($start); + $until = $this->newUntilDateTime(); + if ($until) { + $rrule->setUntil($until); + } + return $rrule; }