ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
ICSExportPlugin.php
Go to the documentation of this file.
1<?php
2
3namespace Sabre\CalDAV;
4
5use DateTime;
6use DateTimeZone;
7use Sabre\DAV;
12
48
54 protected $server;
55
62 function initialize(DAV\Server $server) {
63
64 $this->server = $server;
65 $server->on('method:GET', [$this, 'httpGet'], 90);
66 $server->on('browserButtonActions', function($path, $node, &$actions) {
67 if ($node instanceof ICalendar) {
68 $actions .= '<a href="' . htmlspecialchars($path, ENT_QUOTES, 'UTF-8') . '?export"><span class="oi" data-glyph="calendar"></span></a>';
69 }
70 });
71
72 }
73
82
83 $queryParams = $request->getQueryParameters();
84 if (!array_key_exists('export', $queryParams)) return;
85
86 $path = $request->getPath();
87
88 $node = $this->server->getProperties($path, [
89 '{DAV:}resourcetype',
90 '{DAV:}displayname',
91 '{http://sabredav.org/ns}sync-token',
92 '{DAV:}sync-token',
93 '{http://apple.com/ns/ical/}calendar-color',
94 ]);
95
96 if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{' . Plugin::NS_CALDAV . '}calendar')) {
97 return;
98 }
99 // Marking the transactionType, for logging purposes.
100 $this->server->transactionType = 'get-calendar-export';
101
102 $properties = $node;
103
104 $start = null;
105 $end = null;
106 $expand = false;
107 $componentType = false;
108 if (isset($queryParams['start'])) {
109 if (!ctype_digit($queryParams['start'])) {
110 throw new BadRequest('The start= parameter must contain a unix timestamp');
111 }
112 $start = DateTime::createFromFormat('U', $queryParams['start']);
113 }
114 if (isset($queryParams['end'])) {
115 if (!ctype_digit($queryParams['end'])) {
116 throw new BadRequest('The end= parameter must contain a unix timestamp');
117 }
118 $end = DateTime::createFromFormat('U', $queryParams['end']);
119 }
120 if (isset($queryParams['expand']) && !!$queryParams['expand']) {
121 if (!$start || !$end) {
122 throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.');
123 }
124 $expand = true;
125 $componentType = 'VEVENT';
126 }
127 if (isset($queryParams['componentType'])) {
128 if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) {
129 throw new BadRequest('You are not allowed to search for components of type: ' . $queryParams['componentType'] . ' here');
130 }
131 $componentType = $queryParams['componentType'];
132 }
133
134 $format = \Sabre\HTTP\Util::Negotiate(
135 $request->getHeader('Accept'),
136 [
137 'text/calendar',
138 'application/calendar+json',
139 ]
140 );
141
142 if (isset($queryParams['accept'])) {
143 if ($queryParams['accept'] === 'application/calendar+json' || $queryParams['accept'] === 'jcal') {
144 $format = 'application/calendar+json';
145 }
146 }
147 if (!$format) {
148 $format = 'text/calendar';
149 }
150
151 $this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response);
152
153 // Returning false to break the event chain
154 return false;
155
156 }
157
170 protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) {
171
172 $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
173 $calendarNode = $this->server->tree->getNodeForPath($path);
174
175 $blobs = [];
176 if ($start || $end || $componentType) {
177
178 // If there was a start or end filter, we need to enlist
179 // calendarQuery for speed.
180 $queryResult = $calendarNode->calendarQuery([
181 'name' => 'VCALENDAR',
182 'comp-filters' => [
183 [
184 'name' => $componentType,
185 'comp-filters' => [],
186 'prop-filters' => [],
187 'is-not-defined' => false,
188 'time-range' => [
189 'start' => $start,
190 'end' => $end,
191 ],
192 ],
193 ],
194 'prop-filters' => [],
195 'is-not-defined' => false,
196 'time-range' => null,
197 ]);
198
199 // queryResult is just a list of base urls. We need to prefix the
200 // calendar path.
201 $queryResult = array_map(
202 function($item) use ($path) {
203 return $path . '/' . $item;
204 },
205 $queryResult
206 );
207 $nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]);
208 unset($queryResult);
209
210 } else {
211 $nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1);
212 }
213
214 // Flattening the arrays
215 foreach ($nodes as $node) {
216 if (isset($node[200][$calDataProp])) {
217 $blobs[$node['href']] = $node[200][$calDataProp];
218 }
219 }
220 unset($nodes);
221
222 $mergedCalendar = $this->mergeObjects(
223 $properties,
224 $blobs
225 );
226
227 if ($expand) {
228 $calendarTimeZone = null;
229 // We're expanding, and for that we need to figure out the
230 // calendar's timezone.
231 $tzProp = '{' . Plugin::NS_CALDAV . '}calendar-timezone';
232 $tzResult = $this->server->getProperties($path, [$tzProp]);
233 if (isset($tzResult[$tzProp])) {
234 // This property contains a VCALENDAR with a single
235 // VTIMEZONE.
236 $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]);
237 $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();
238 // Destroy circular references to PHP will GC the object.
239 $vtimezoneObj->destroy();
240 unset($vtimezoneObj);
241 } else {
242 // Defaulting to UTC.
243 $calendarTimeZone = new DateTimeZone('UTC');
244 }
245
246 $mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone);
247 }
248
249 $filenameExtension = '.ics';
250
251 switch ($format) {
252 case 'text/calendar' :
253 $mergedCalendar = $mergedCalendar->serialize();
254 $filenameExtension = '.ics';
255 break;
256 case 'application/calendar+json' :
257 $mergedCalendar = json_encode($mergedCalendar->jsonSerialize());
258 $filenameExtension = '.json';
259 break;
260 }
261
262 $filename = preg_replace(
263 '/[^a-zA-Z0-9-_ ]/um',
264 '',
265 $calendarNode->getName()
266 );
267 $filename .= '-' . date('Y-m-d') . $filenameExtension;
268
269 $response->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"');
270 $response->setHeader('Content-Type', $format);
271
272 $response->setStatus(200);
273 $response->setBody($mergedCalendar);
274
275 }
276
284 function mergeObjects(array $properties, array $inputObjects) {
285
287 $calendar->VERSION = '2.0';
288 if (DAV\Server::$exposeVersion) {
289 $calendar->PRODID = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
290 } else {
291 $calendar->PRODID = '-//SabreDAV//SabreDAV//EN';
292 }
293 if (isset($properties['{DAV:}displayname'])) {
294 $calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname'];
295 }
296 if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) {
297 $calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color'];
298 }
299
300 $collectedTimezones = [];
301
302 $timezones = [];
303 $objects = [];
304
305 foreach ($inputObjects as $href => $inputObject) {
306
307 $nodeComp = VObject\Reader::read($inputObject);
308
309 foreach ($nodeComp->children() as $child) {
310
311 switch ($child->name) {
312 case 'VEVENT' :
313 case 'VTODO' :
314 case 'VJOURNAL' :
315 $objects[] = clone $child;
316 break;
317
318 // VTIMEZONE is special, because we need to filter out the duplicates
319 case 'VTIMEZONE' :
320 // Naively just checking tzid.
321 if (in_array((string)$child->TZID, $collectedTimezones)) continue;
322
323 $timezones[] = clone $child;
324 $collectedTimezones[] = $child->TZID;
325 break;
326
327 }
328
329 }
330 // Destroy circular references to PHP will GC the object.
331 $nodeComp->destroy();
332 unset($nodeComp);
333
334 }
335
336 foreach ($timezones as $tz) $calendar->add($tz);
337 foreach ($objects as $obj) $calendar->add($obj);
338
339 return $calendar;
340
341 }
342
351 function getPluginName() {
352
353 return 'ics-export';
354
355 }
356
368 function getPluginInfo() {
369
370 return [
371 'name' => $this->getPluginName(),
372 'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.',
373 'link' => 'http://sabre.io/dav/ics-export-plugin/',
374 ];
375
376 }
377
378}
$path
Definition: aliased.php:25
foreach($paths as $path) $request
Definition: asyncclient.php:32
$filename
Definition: buildRTE.php:89
An exception for terminatinating execution or to throw for unit testing.
initialize(DAV\Server $server)
Initializes the plugin and registers event handlers.
httpGet(RequestInterface $request, ResponseInterface $response)
Intercepts GET requests on calendar urls ending with ?export.
mergeObjects(array $properties, array $inputObjects)
Merges all calendar objects, and builds one big iCalendar blob.
generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response)
This method is responsible for generating the actual, full response.
getPluginInfo()
Returns a bunch of meta-data about the plugin.
getPluginName()
Returns a plugin name.
const NS_CALDAV
This is the official CalDAV namespace.
Definition: Plugin.php:33
The baseclass for all server plugins.
Main DAV server class.
Definition: Server.php:23
static $exposeVersion
Definition: Server.php:184
const VERSION
Full version number.
Definition: Version.php:17
The VCalendar component.
Definition: VCalendar.php:23
static read($data, $options=0, $charset='UTF-8')
Parses a vCard or iCalendar object, and returns the top component.
Definition: Reader.php:42
Calendar interface.
Definition: ICalendar.php:16
The RequestInterface represents a HTTP request.
This interface represents a HTTP response.
$format
Definition: metadata.php:141
$response
$start
Definition: bench.php:8