streams/vendor/sabre/dav/lib/CalDAV/ICSExportPlugin.php
2017-03-16 11:42:06 +11:00

378 lines
12 KiB
PHP

<?php
namespace Sabre\CalDAV;
use DateTime;
use DateTimeZone;
use Sabre\DAV;
use Sabre\DAV\Exception\BadRequest;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use Sabre\VObject;
/**
* ICS Exporter
*
* This plugin adds the ability to export entire calendars as .ics files.
* This is useful for clients that don't support CalDAV yet. They often do
* support ics files.
*
* To use this, point a http client to a caldav calendar, and add ?expand to
* the url.
*
* Further options that can be added to the url:
* start=123456789 - Only return events after the given unix timestamp
* end=123245679 - Only return events from before the given unix timestamp
* expand=1 - Strip timezone information and expand recurring events.
* If you'd like to expand, you _must_ also specify start
* and end.
*
* By default this plugin returns data in the text/calendar format (iCalendar
* 2.0). If you'd like to receive jCal data instead, you can use an Accept
* header:
*
* Accept: application/calendar+json
*
* Alternatively, you can also specify this in the url using
* accept=application/calendar+json, or accept=jcal for short. If the url
* parameter and Accept header is specified, the url parameter wins.
*
* Note that specifying a start or end data implies that only events will be
* returned. VTODO and VJOURNAL will be stripped.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class ICSExportPlugin extends DAV\ServerPlugin {
/**
* Reference to Server class
*
* @var \Sabre\DAV\Server
*/
protected $server;
/**
* Initializes the plugin and registers event handlers
*
* @param \Sabre\DAV\Server $server
* @return void
*/
function initialize(DAV\Server $server) {
$this->server = $server;
$server->on('method:GET', [$this, 'httpGet'], 90);
$server->on('browserButtonActions', function($path, $node, &$actions) {
if ($node instanceof ICalendar) {
$actions .= '<a href="' . htmlspecialchars($path, ENT_QUOTES, 'UTF-8') . '?export"><span class="oi" data-glyph="calendar"></span></a>';
}
});
}
/**
* Intercepts GET requests on calendar urls ending with ?export.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return bool
*/
function httpGet(RequestInterface $request, ResponseInterface $response) {
$queryParams = $request->getQueryParameters();
if (!array_key_exists('export', $queryParams)) return;
$path = $request->getPath();
$node = $this->server->getProperties($path, [
'{DAV:}resourcetype',
'{DAV:}displayname',
'{http://sabredav.org/ns}sync-token',
'{DAV:}sync-token',
'{http://apple.com/ns/ical/}calendar-color',
]);
if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{' . Plugin::NS_CALDAV . '}calendar')) {
return;
}
// Marking the transactionType, for logging purposes.
$this->server->transactionType = 'get-calendar-export';
$properties = $node;
$start = null;
$end = null;
$expand = false;
$componentType = false;
if (isset($queryParams['start'])) {
if (!ctype_digit($queryParams['start'])) {
throw new BadRequest('The start= parameter must contain a unix timestamp');
}
$start = DateTime::createFromFormat('U', $queryParams['start']);
}
if (isset($queryParams['end'])) {
if (!ctype_digit($queryParams['end'])) {
throw new BadRequest('The end= parameter must contain a unix timestamp');
}
$end = DateTime::createFromFormat('U', $queryParams['end']);
}
if (isset($queryParams['expand']) && !!$queryParams['expand']) {
if (!$start || !$end) {
throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.');
}
$expand = true;
$componentType = 'VEVENT';
}
if (isset($queryParams['componentType'])) {
if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) {
throw new BadRequest('You are not allowed to search for components of type: ' . $queryParams['componentType'] . ' here');
}
$componentType = $queryParams['componentType'];
}
$format = \Sabre\HTTP\Util::Negotiate(
$request->getHeader('Accept'),
[
'text/calendar',
'application/calendar+json',
]
);
if (isset($queryParams['accept'])) {
if ($queryParams['accept'] === 'application/calendar+json' || $queryParams['accept'] === 'jcal') {
$format = 'application/calendar+json';
}
}
if (!$format) {
$format = 'text/calendar';
}
$this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response);
// Returning false to break the event chain
return false;
}
/**
* This method is responsible for generating the actual, full response.
*
* @param string $path
* @param DateTime|null $start
* @param DateTime|null $end
* @param bool $expand
* @param string $componentType
* @param string $format
* @param array $properties
* @param ResponseInterface $response
*/
protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) {
$calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
$calendarNode = $this->server->tree->getNodeForPath($path);
$blobs = [];
if ($start || $end || $componentType) {
// If there was a start or end filter, we need to enlist
// calendarQuery for speed.
$queryResult = $calendarNode->calendarQuery([
'name' => 'VCALENDAR',
'comp-filters' => [
[
'name' => $componentType,
'comp-filters' => [],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => [
'start' => $start,
'end' => $end,
],
],
],
'prop-filters' => [],
'is-not-defined' => false,
'time-range' => null,
]);
// queryResult is just a list of base urls. We need to prefix the
// calendar path.
$queryResult = array_map(
function($item) use ($path) {
return $path . '/' . $item;
},
$queryResult
);
$nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]);
unset($queryResult);
} else {
$nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1);
}
// Flattening the arrays
foreach ($nodes as $node) {
if (isset($node[200][$calDataProp])) {
$blobs[$node['href']] = $node[200][$calDataProp];
}
}
unset($nodes);
$mergedCalendar = $this->mergeObjects(
$properties,
$blobs
);
if ($expand) {
$calendarTimeZone = null;
// We're expanding, and for that we need to figure out the
// calendar's timezone.
$tzProp = '{' . Plugin::NS_CALDAV . '}calendar-timezone';
$tzResult = $this->server->getProperties($path, [$tzProp]);
if (isset($tzResult[$tzProp])) {
// This property contains a VCALENDAR with a single
// VTIMEZONE.
$vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
$calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
// Destroy circular references to PHP will GC the object.
$vtimezoneObj->destroy();
unset($vtimezoneObj);
} else {
// Defaulting to UTC.
$calendarTimeZone = new DateTimeZone('UTC');
}
$mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone);
}
$filenameExtension = '.ics';
switch ($format) {
case 'text/calendar' :
$mergedCalendar = $mergedCalendar->serialize();
$filenameExtension = '.ics';
break;
case 'application/calendar+json' :
$mergedCalendar = json_encode($mergedCalendar->jsonSerialize());
$filenameExtension = '.json';
break;
}
$filename = preg_replace(
'/[^a-zA-Z0-9-_ ]/um',
'',
$calendarNode->getName()
);
$filename .= '-' . date('Y-m-d') . $filenameExtension;
$response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
$response->setHeader('Content-Type', $format);
$response->setStatus(200);
$response->setBody($mergedCalendar);
}
/**
* Merges all calendar objects, and builds one big iCalendar blob.
*
* @param array $properties Some CalDAV properties
* @param array $inputObjects
* @return VObject\Component\VCalendar
*/
function mergeObjects(array $properties, array $inputObjects) {
$calendar = new VObject\Component\VCalendar();
$calendar->VERSION = '2.0';
if (DAV\Server::$exposeVersion) {
$calendar->PRODID = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
} else {
$calendar->PRODID = '-//SabreDAV//SabreDAV//EN';
}
if (isset($properties['{DAV:}displayname'])) {
$calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname'];
}
if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) {
$calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color'];
}
$collectedTimezones = [];
$timezones = [];
$objects = [];
foreach ($inputObjects as $href => $inputObject) {
$nodeComp = VObject\Reader::read($inputObject);
foreach ($nodeComp->children() as $child) {
switch ($child->name) {
case 'VEVENT' :
case 'VTODO' :
case 'VJOURNAL' :
$objects[] = clone $child;
break;
// VTIMEZONE is special, because we need to filter out the duplicates
case 'VTIMEZONE' :
// Naively just checking tzid.
if (in_array((string)$child->TZID, $collectedTimezones)) continue;
$timezones[] = clone $child;
$collectedTimezones[] = $child->TZID;
break;
}
}
// Destroy circular references to PHP will GC the object.
$nodeComp->destroy();
unset($nodeComp);
}
foreach ($timezones as $tz) $calendar->add($tz);
foreach ($objects as $obj) $calendar->add($obj);
return $calendar;
}
/**
* Returns a plugin name.
*
* Using this name other plugins will be able to access other plugins
* using \Sabre\DAV\Server::getPlugin
*
* @return string
*/
function getPluginName() {
return 'ics-export';
}
/**
* Returns a bunch of meta-data about the plugin.
*
* Providing this information is optional, and is mainly displayed by the
* Browser plugin.
*
* The description key in the returned array may contain html and will not
* be sanitized.
*
* @return array
*/
function getPluginInfo() {
return [
'name' => $this->getPluginName(),
'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.',
'link' => 'http://sabre.io/dav/ics-export-plugin/',
];
}
}