1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-27 09:12:41 +01:00

Export recurring events and build ICS files for configured exports

Summary:
Ref T10747. This:

  - Exports recurring events properly, with RRULE + RECURRENCE-ID.
  - When exporting a part of an event series, export the whole series to ICS so it is represented faithfully.
  - Make the subscribable URL for "Export" objects work.

Test Plan:
  - Downloaded the ".ics" for a normal event, imported it into Calendar.app and Google Calendar.
  - Downloaded the ".ics" for a recurring event, imported it into Calendar.app and Google Calendar.
  - Defined an ".ics" Export of my events, subscribed to them in Calendar.app.
    - Edited an event in Phabricator.
    - Hit {key Command R} in Calendar.app, saw changes. (MAGIC!)
    - This export included recurring events, which appeared the same way in Calendar.app and Phabricator.
  - Can't import into Google Calendar from my local install easily since Google's servers can't hit my laptop, but I'll test once we deploy.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10747

Differential Revision: https://secure.phabricator.com/D16679
This commit is contained in:
epriestley 2016-10-06 12:31:57 -07:00
parent fa6a5a46ba
commit 4819446fe5
9 changed files with 232 additions and 26 deletions

View file

@ -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',

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,93 @@
<?php
final class PhabricatorCalendarExportICSController
extends PhabricatorCalendarController {
public function shouldRequireLogin() {
// Export URIs are available if you know the secret key. We can't do any
// other kind of authentication because third-party applications like
// Google Calendar and Calendar.app need to be able to fetch these URIs.
return false;
}
public function handleRequest(AphrontRequest $request) {
$omnipotent = PhabricatorUser::getOmnipotentUser();
// NOTE: We're using the omnipotent viewer to fetch the export, but the
// URI must contain the secret key. Once we load the export we'll figure
// out who the effective viewer is.
$export = id(new PhabricatorCalendarExportQuery())
->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);
}
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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;
}

View file

@ -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;
}