123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- <?php
- namespace Sabre\CalDAV;
- use DateTimeZone;
- use Sabre\DAV;
- use Sabre\VObject;
- use Sabre\HTTP\RequestInterface;
- use Sabre\HTTP\ResponseInterface;
- use Sabre\DAV\Exception\BadRequest;
- use DateTime;
- /**
- * 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';
- $blobs = [];
- if ($start || $end || $componentType) {
- // If there was a start or end filter, we need to enlist
- // calendarQuery for speed.
- $calendarNode = $this->server->tree->getNodeForPath($path);
- $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);
- }
- $response->setHeader('Content-Type', $format);
- switch ($format) {
- case 'text/calendar' :
- $mergedCalendar = $mergedCalendar->serialize();
- break;
- case 'application/calendar+json' :
- $mergedCalendar = json_encode($mergedCalendar->jsonSerialize());
- break;
- }
- $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/',
- ];
- }
- }
|