ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
ICSExportPlugin.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Sabre\CalDAV;
4 
5 use DateTime;
6 use DateTimeZone;
7 use Sabre\DAV;
11 use Sabre\VObject;
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 }
This interface represents a HTTP response.
The RequestInterface represents a HTTP request.
$path
Definition: aliased.php:25
The baseclass for all server plugins.
$format
Definition: metadata.php:141
setBody($body)
Updates the body resource with a new stream.
foreach($paths as $path) $request
Definition: asyncclient.php:32
mergeObjects(array $properties, array $inputObjects)
Merges all calendar objects, and builds one big iCalendar blob.
getPluginInfo()
Returns a bunch of meta-data about the plugin.
The VCalendar component.
Definition: VCalendar.php:23
const VERSION
Full version number.
Definition: Version.php:17
Calendar interface.
Definition: ICalendar.php:16
$start
Definition: bench.php:8
getQueryParameters()
Returns the list of query parameters.
setStatus($status)
Sets the HTTP status code.
Main DAV server class.
Definition: Server.php:23
getHeader($name)
Returns a specific HTTP header, based on it&#39;s name.
initialize(DAV\Server $server)
Initializes the plugin and registers event handlers.
static $exposeVersion
Definition: Server.php:184
$filename
Definition: buildRTE.php:89
getPath()
Returns the relative path.
static read($data, $options=0, $charset='UTF-8')
Parses a vCard or iCalendar object, and returns the top component.
Definition: Reader.php:42
httpGet(RequestInterface $request, ResponseInterface $response)
Intercepts GET requests on calendar urls ending with ?export.
$response
const NS_CALDAV
This is the official CalDAV namespace.
Definition: Plugin.php:33
setHeader($name, $value)
Updates a HTTP header.
generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response)
This method is responsible for generating the actual, full response.
getPluginName()
Returns a plugin name.