Blame view

sources/3rdparty/sabre/dav/lib/Sabre/CalDAV/Plugin.php 47.6 KB
03e52840d   Kload   Init
1
  <?php
6d9380f96   Cédric Dupont   Update sources OC...
2
3
4
5
  namespace Sabre\CalDAV;
  
  use Sabre\DAV;
  use Sabre\DAVACL;
03e52840d   Kload   Init
6
7
8
9
10
11
12
13
  use Sabre\VObject;
  
  /**
   * CalDAV plugin
   *
   * This plugin provides functionality added by CalDAV (RFC 4791)
   * It implements new reports, and the MKCALENDAR method.
   *
6d9380f96   Cédric Dupont   Update sources OC...
14
15
16
   * @copyright Copyright (C) 2007-2014 fruux GmbH (https://fruux.com/).
   * @author Evert Pot (http://evertpot.com/)
   * @license http://sabre.io/license/ Modified BSD License
03e52840d   Kload   Init
17
   */
6d9380f96   Cédric Dupont   Update sources OC...
18
  class Plugin extends DAV\ServerPlugin {
03e52840d   Kload   Init
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  
      /**
       * This is the official CalDAV namespace
       */
      const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav';
  
      /**
       * This is the namespace for the proprietary calendarserver extensions
       */
      const NS_CALENDARSERVER = 'http://calendarserver.org/ns/';
  
      /**
       * The hardcoded root for calendar objects. It is unfortunate
       * that we're stuck with it, but it will have to do for now
       */
      const CALENDAR_ROOT = 'calendars';
  
      /**
       * Reference to server object
       *
6d9380f96   Cédric Dupont   Update sources OC...
39
       * @var DAV\Server
03e52840d   Kload   Init
40
41
42
43
44
45
       */
      protected $server;
  
      /**
       * The email handler for invites and other scheduling messages.
       *
6d9380f96   Cédric Dupont   Update sources OC...
46
       * @var Schedule\IMip
03e52840d   Kload   Init
47
48
49
50
51
52
53
54
55
56
57
58
59
60
       */
      protected $imipHandler;
  
      /**
       * Sets the iMIP handler.
       *
       * iMIP = The email transport of iCalendar scheduling messages. Setting
       * this is optional, but if you want the server to allow invites to be sent
       * out, you must set a handler.
       *
       * Specifically iCal will plain assume that the server supports this. If
       * the server doesn't, iCal will display errors when inviting people to
       * events.
       *
6d9380f96   Cédric Dupont   Update sources OC...
61
       * @param Schedule\IMip $imipHandler
03e52840d   Kload   Init
62
63
       * @return void
       */
6d9380f96   Cédric Dupont   Update sources OC...
64
      public function setIMipHandler(Schedule\IMip $imipHandler) {
03e52840d   Kload   Init
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
  
          $this->imipHandler = $imipHandler;
  
      }
  
      /**
       * Use this method to tell the server this plugin defines additional
       * HTTP methods.
       *
       * This method is passed a uri. It should only return HTTP methods that are
       * available for the specified uri.
       *
       * @param string $uri
       * @return array
       */
      public function getHTTPMethods($uri) {
  
          // The MKCALENDAR is only available on unmapped uri's, whose
          // parents extend IExtendedCollection
6d9380f96   Cédric Dupont   Update sources OC...
84
          list($parent, $name) = DAV\URLUtil::splitPath($uri);
03e52840d   Kload   Init
85
86
  
          $node = $this->server->tree->getNodeForPath($parent);
6d9380f96   Cédric Dupont   Update sources OC...
87
          if ($node instanceof DAV\IExtendedCollection) {
03e52840d   Kload   Init
88
89
              try {
                  $node->getChild($name);
6d9380f96   Cédric Dupont   Update sources OC...
90
              } catch (DAV\Exception\NotFound $e) {
03e52840d   Kload   Init
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
                  return array('MKCALENDAR');
              }
          }
          return array();
  
      }
  
      /**
       * Returns a list of features for the DAV: HTTP header.
       *
       * @return array
       */
      public function getFeatures() {
  
          return array('calendar-access', 'calendar-proxy');
  
      }
  
      /**
       * Returns a plugin name.
       *
       * Using this name other plugins will be able to access other plugins
6d9380f96   Cédric Dupont   Update sources OC...
113
       * using DAV\Server::getPlugin
03e52840d   Kload   Init
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
       *
       * @return string
       */
      public function getPluginName() {
  
          return 'caldav';
  
      }
  
      /**
       * Returns a list of reports this plugin supports.
       *
       * This will be used in the {DAV:}supported-report-set property.
       * Note that you still need to subscribe to the 'report' event to actually
       * implement them
       *
       * @param string $uri
       * @return array
       */
      public function getSupportedReportSet($uri) {
  
          $node = $this->server->tree->getNodeForPath($uri);
  
          $reports = array();
6d9380f96   Cédric Dupont   Update sources OC...
138
          if ($node instanceof ICalendar || $node instanceof ICalendarObject) {
03e52840d   Kload   Init
139
140
141
              $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget';
              $reports[] = '{' . self::NS_CALDAV . '}calendar-query';
          }
6d9380f96   Cédric Dupont   Update sources OC...
142
          if ($node instanceof ICalendar) {
03e52840d   Kload   Init
143
144
145
146
147
148
149
150
151
              $reports[] = '{' . self::NS_CALDAV . '}free-busy-query';
          }
          return $reports;
  
      }
  
      /**
       * Initializes the plugin
       *
6d9380f96   Cédric Dupont   Update sources OC...
152
       * @param DAV\Server $server
03e52840d   Kload   Init
153
154
       * @return void
       */
6d9380f96   Cédric Dupont   Update sources OC...
155
      public function initialize(DAV\Server $server) {
03e52840d   Kload   Init
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
  
          $this->server = $server;
  
          $server->subscribeEvent('unknownMethod',array($this,'unknownMethod'));
          //$server->subscribeEvent('unknownMethod',array($this,'unknownMethod2'),1000);
          $server->subscribeEvent('report',array($this,'report'));
          $server->subscribeEvent('beforeGetProperties',array($this,'beforeGetProperties'));
          $server->subscribeEvent('onHTMLActionsPanel', array($this,'htmlActionsPanel'));
          $server->subscribeEvent('onBrowserPostAction', array($this,'browserPostAction'));
          $server->subscribeEvent('beforeWriteContent', array($this, 'beforeWriteContent'));
          $server->subscribeEvent('beforeCreateFile', array($this, 'beforeCreateFile'));
          $server->subscribeEvent('beforeMethod', array($this,'beforeMethod'));
  
          $server->xmlNamespaces[self::NS_CALDAV] = 'cal';
          $server->xmlNamespaces[self::NS_CALENDARSERVER] = 'cs';
6d9380f96   Cédric Dupont   Update sources OC...
171
172
          $server->propertyMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Property\\SupportedCalendarComponentSet';
          $server->propertyMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Property\\ScheduleCalendarTransp';
03e52840d   Kload   Init
173

6d9380f96   Cédric Dupont   Update sources OC...
174
175
176
177
178
          $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar';
          $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = '{urn:ietf:params:xml:ns:caldav}schedule-outbox';
          $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read';
          $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write';
          $server->resourceTypeMapping['\\Sabre\\CalDAV\\Notifications\\ICollection'] = '{' . self::NS_CALENDARSERVER . '}notification';
03e52840d   Kload   Init
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
  
          array_push($server->protectedProperties,
  
              '{' . self::NS_CALDAV . '}supported-calendar-component-set',
              '{' . self::NS_CALDAV . '}supported-calendar-data',
              '{' . self::NS_CALDAV . '}max-resource-size',
              '{' . self::NS_CALDAV . '}min-date-time',
              '{' . self::NS_CALDAV . '}max-date-time',
              '{' . self::NS_CALDAV . '}max-instances',
              '{' . self::NS_CALDAV . '}max-attendees-per-instance',
              '{' . self::NS_CALDAV . '}calendar-home-set',
              '{' . self::NS_CALDAV . '}supported-collation-set',
              '{' . self::NS_CALDAV . '}calendar-data',
  
              // scheduling extension
              '{' . self::NS_CALDAV . '}schedule-inbox-URL',
              '{' . self::NS_CALDAV . '}schedule-outbox-URL',
              '{' . self::NS_CALDAV . '}calendar-user-address-set',
              '{' . self::NS_CALDAV . '}calendar-user-type',
  
              // CalendarServer extensions
              '{' . self::NS_CALENDARSERVER . '}getctag',
              '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for',
              '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for',
              '{' . self::NS_CALENDARSERVER . '}notification-URL',
              '{' . self::NS_CALENDARSERVER . '}notificationtype'
  
          );
      }
  
      /**
       * This function handles support for the MKCALENDAR method
       *
       * @param string $method
       * @param string $uri
       * @return bool
       */
      public function unknownMethod($method, $uri) {
  
          switch ($method) {
              case 'MKCALENDAR' :
                  $this->httpMkCalendar($uri);
                  // false is returned to stop the propagation of the
                  // unknownMethod event.
                  return false;
              case 'POST' :
  
                  // Checking if this is a text/calendar content type
                  $contentType = $this->server->httpRequest->getHeader('Content-Type');
                  if (strpos($contentType, 'text/calendar')!==0) {
                      return;
                  }
  
                  // Checking if we're talking to an outbox
                  try {
                      $node = $this->server->tree->getNodeForPath($uri);
6d9380f96   Cédric Dupont   Update sources OC...
235
                  } catch (DAV\Exception\NotFound $e) {
03e52840d   Kload   Init
236
237
                      return;
                  }
6d9380f96   Cédric Dupont   Update sources OC...
238
                  if (!$node instanceof Schedule\IOutbox)
03e52840d   Kload   Init
239
240
241
242
243
244
245
246
247
248
249
250
251
                      return;
  
                  $this->outboxRequest($node, $uri);
                  return false;
  
          }
  
      }
  
      /**
       * This functions handles REPORT requests specific to CalDAV
       *
       * @param string $reportName
6d9380f96   Cédric Dupont   Update sources OC...
252
       * @param \DOMNode $dom
03e52840d   Kload   Init
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
       * @return bool
       */
      public function report($reportName,$dom) {
  
          switch($reportName) {
              case '{'.self::NS_CALDAV.'}calendar-multiget' :
                  $this->calendarMultiGetReport($dom);
                  return false;
              case '{'.self::NS_CALDAV.'}calendar-query' :
                  $this->calendarQueryReport($dom);
                  return false;
              case '{'.self::NS_CALDAV.'}free-busy-query' :
                  $this->freeBusyQueryReport($dom);
                  return false;
  
          }
  
  
      }
  
      /**
       * This function handles the MKCALENDAR HTTP method, which creates
       * a new calendar.
       *
       * @param string $uri
       * @return void
       */
      public function httpMkCalendar($uri) {
  
          // Due to unforgivable bugs in iCal, we're completely disabling MKCALENDAR support
          // for clients matching iCal in the user agent
          //$ua = $this->server->httpRequest->getHeader('User-Agent');
          //if (strpos($ua,'iCal/')!==false) {
6d9380f96   Cédric Dupont   Update sources OC...
286
          //    throw new \Sabre\DAV\Exception\Forbidden('iCal has major bugs in it\'s RFC3744 support. Therefore we are left with no other choice but disabling this feature.');
03e52840d   Kload   Init
287
288
289
290
291
292
          //}
  
          $body = $this->server->httpRequest->getBody(true);
          $properties = array();
  
          if ($body) {
6d9380f96   Cédric Dupont   Update sources OC...
293
              $dom = DAV\XMLUtil::loadDOMDocument($body);
03e52840d   Kload   Init
294
295
  
              foreach($dom->firstChild->childNodes as $child) {
6d9380f96   Cédric Dupont   Update sources OC...
296
297
                  if (DAV\XMLUtil::toClarkNotation($child)!=='{DAV:}set') continue;
                  foreach(DAV\XMLUtil::parseProperties($child,$this->server->propertyMap) as $k=>$prop) {
03e52840d   Kload   Init
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
                      $properties[$k] = $prop;
                  }
  
              }
          }
  
          $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
  
          $this->server->createCollection($uri,$resourceType,$properties);
  
          $this->server->httpResponse->sendStatus(201);
          $this->server->httpResponse->setHeader('Content-Length',0);
      }
  
      /**
       * beforeGetProperties
       *
       * This method handler is invoked before any after properties for a
       * resource are fetched. This allows us to add in any CalDAV specific
       * properties.
       *
       * @param string $path
6d9380f96   Cédric Dupont   Update sources OC...
320
       * @param DAV\INode $node
03e52840d   Kload   Init
321
322
323
324
       * @param array $requestedProperties
       * @param array $returnedProperties
       * @return void
       */
6d9380f96   Cédric Dupont   Update sources OC...
325
      public function beforeGetProperties($path, DAV\INode $node, &$requestedProperties, &$returnedProperties) {
03e52840d   Kload   Init
326

6d9380f96   Cédric Dupont   Update sources OC...
327
          if ($node instanceof DAVACL\IPrincipal) {
03e52840d   Kload   Init
328
329
330
331
332
333
  
              // calendar-home-set property
              $calHome = '{' . self::NS_CALDAV . '}calendar-home-set';
              if (in_array($calHome,$requestedProperties)) {
                  $principalId = $node->getName();
                  $calendarHomePath = self::CALENDAR_ROOT . '/' . $principalId . '/';
6d9380f96   Cédric Dupont   Update sources OC...
334

03e52840d   Kload   Init
335
                  unset($requestedProperties[array_search($calHome, $requestedProperties)]);
6d9380f96   Cédric Dupont   Update sources OC...
336
                  $returnedProperties[200][$calHome] = new DAV\Property\Href($calendarHomePath);
03e52840d   Kload   Init
337
338
339
340
341
342
343
              }
  
              // schedule-outbox-URL property
              $scheduleProp = '{' . self::NS_CALDAV . '}schedule-outbox-URL';
              if (in_array($scheduleProp,$requestedProperties)) {
                  $principalId = $node->getName();
                  $outboxPath = self::CALENDAR_ROOT . '/' . $principalId . '/outbox';
6d9380f96   Cédric Dupont   Update sources OC...
344

03e52840d   Kload   Init
345
                  unset($requestedProperties[array_search($scheduleProp, $requestedProperties)]);
6d9380f96   Cédric Dupont   Update sources OC...
346
                  $returnedProperties[200][$scheduleProp] = new DAV\Property\Href($outboxPath);
03e52840d   Kload   Init
347
348
349
350
351
352
353
              }
  
              // calendar-user-address-set property
              $calProp = '{' . self::NS_CALDAV . '}calendar-user-address-set';
              if (in_array($calProp,$requestedProperties)) {
  
                  $addresses = $node->getAlternateUriSet();
6d9380f96   Cédric Dupont   Update sources OC...
354
                  $addresses[] = $this->server->getBaseUri() . DAV\URLUtil::encodePath($node->getPrincipalUrl() . '/');
03e52840d   Kload   Init
355
                  unset($requestedProperties[array_search($calProp, $requestedProperties)]);
6d9380f96   Cédric Dupont   Update sources OC...
356
                  $returnedProperties[200][$calProp] = new DAV\Property\HrefList($addresses, false);
03e52840d   Kload   Init
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
  
              }
  
              // These two properties are shortcuts for ical to easily find
              // other principals this principal has access to.
              $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for';
              $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for';
              if (in_array($propRead,$requestedProperties) || in_array($propWrite,$requestedProperties)) {
  
                  $aclPlugin = $this->server->getPlugin('acl');
                  $membership = $aclPlugin->getPrincipalMembership($path);
                  $readList = array();
                  $writeList = array();
  
                  foreach($membership as $group) {
  
                      $groupNode = $this->server->tree->getNodeForPath($group);
  
                      // If the node is either ap proxy-read or proxy-write
                      // group, we grab the parent principal and add it to the
                      // list.
6d9380f96   Cédric Dupont   Update sources OC...
378
379
                      if ($groupNode instanceof Principal\IProxyRead) {
                          list($readList[]) = DAV\URLUtil::splitPath($group);
03e52840d   Kload   Init
380
                      }
6d9380f96   Cédric Dupont   Update sources OC...
381
382
                      if ($groupNode instanceof Principal\IProxyWrite) {
                          list($writeList[]) = DAV\URLUtil::splitPath($group);
03e52840d   Kload   Init
383
384
385
386
387
                      }
  
                  }
                  if (in_array($propRead,$requestedProperties)) {
                      unset($requestedProperties[$propRead]);
6d9380f96   Cédric Dupont   Update sources OC...
388
                      $returnedProperties[200][$propRead] = new DAV\Property\HrefList($readList);
03e52840d   Kload   Init
389
390
391
                  }
                  if (in_array($propWrite,$requestedProperties)) {
                      unset($requestedProperties[$propWrite]);
6d9380f96   Cédric Dupont   Update sources OC...
392
                      $returnedProperties[200][$propWrite] = new DAV\Property\HrefList($writeList);
03e52840d   Kload   Init
393
394
395
396
397
398
399
400
401
402
                  }
  
              }
  
              // notification-URL property
              $notificationUrl = '{' . self::NS_CALENDARSERVER . '}notification-URL';
              if (($index = array_search($notificationUrl, $requestedProperties)) !== false) {
                  $principalId = $node->getName();
                  $calendarHomePath = 'calendars/' . $principalId . '/notifications/';
                  unset($requestedProperties[$index]);
6d9380f96   Cédric Dupont   Update sources OC...
403
                  $returnedProperties[200][$notificationUrl] = new DAV\Property\Href($calendarHomePath);
03e52840d   Kload   Init
404
405
406
              }
  
          } // instanceof IPrincipal
6d9380f96   Cédric Dupont   Update sources OC...
407
          if ($node instanceof Notifications\INode) {
03e52840d   Kload   Init
408
409
410
411
412
413
414
415
416
417
418
419
  
              $propertyName = '{' . self::NS_CALENDARSERVER . '}notificationtype';
              if (($index = array_search($propertyName, $requestedProperties)) !== false) {
  
                  $returnedProperties[200][$propertyName] =
                      $node->getNotificationType();
  
                  unset($requestedProperties[$index]);
  
              }
  
          } // instanceof Notifications_INode
6d9380f96   Cédric Dupont   Update sources OC...
420
          if ($node instanceof ICalendarObject) {
03e52840d   Kload   Init
421
422
423
              // The calendar-data property is not supposed to be a 'real'
              // property, but in large chunks of the spec it does act as such.
              // Therefore we simply expose it as a property.
6d9380f96   Cédric Dupont   Update sources OC...
424
              $calDataProp = '{' . Plugin::NS_CALDAV . '}calendar-data';
03e52840d   Kload   Init
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
              if (in_array($calDataProp, $requestedProperties)) {
                  unset($requestedProperties[$calDataProp]);
                  $val = $node->get();
                  if (is_resource($val))
                      $val = stream_get_contents($val);
  
                  // Taking out \r to not screw up the xml output
                  $returnedProperties[200][$calDataProp] = str_replace("\r","", $val);
  
              }
          }
  
      }
  
      /**
       * This function handles the calendar-multiget REPORT.
       *
       * This report is used by the client to fetch the content of a series
       * of urls. Effectively avoiding a lot of redundant requests.
       *
6d9380f96   Cédric Dupont   Update sources OC...
445
       * @param \DOMNode $dom
03e52840d   Kload   Init
446
447
448
       * @return void
       */
      public function calendarMultiGetReport($dom) {
6d9380f96   Cédric Dupont   Update sources OC...
449
          $properties = array_keys(DAV\XMLUtil::parseProperties($dom->firstChild));
03e52840d   Kload   Init
450
          $hrefElems = $dom->getElementsByTagNameNS('urn:DAV','href');
6d9380f96   Cédric Dupont   Update sources OC...
451
452
          $xpath = new \DOMXPath($dom);
          $xpath->registerNameSpace('cal',Plugin::NS_CALDAV);
03e52840d   Kload   Init
453
454
455
456
457
458
459
460
          $xpath->registerNameSpace('dav','urn:DAV');
  
          $expand = $xpath->query('/cal:calendar-multiget/dav:prop/cal:calendar-data/cal:expand');
          if ($expand->length>0) {
              $expandElem = $expand->item(0);
              $start = $expandElem->getAttribute('start');
              $end = $expandElem->getAttribute('end');
              if(!$start || !$end) {
6d9380f96   Cédric Dupont   Update sources OC...
461
                  throw new DAV\Exception\BadRequest('The "start" and "end" attributes are required for the CALDAV:expand element');
03e52840d   Kload   Init
462
463
464
465
466
              }
              $start = VObject\DateTimeParser::parseDateTime($start);
              $end = VObject\DateTimeParser::parseDateTime($end);
  
              if ($end <= $start) {
6d9380f96   Cédric Dupont   Update sources OC...
467
                  throw new DAV\Exception\BadRequest('The end-date must be larger than the start-date in the expand element.');
03e52840d   Kload   Init
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
              }
  
              $expand = true;
  
          } else {
  
              $expand = false;
  
          }
  
          foreach($hrefElems as $elem) {
              $uri = $this->server->calculateUri($elem->nodeValue);
              list($objProps) = $this->server->getPropertiesForPath($uri,$properties);
  
              if ($expand && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) {
                  $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']);
                  $vObject->expand($start, $end);
                  $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
              }
  
              $propertyList[]=$objProps;
  
          }
  
          $prefer = $this->server->getHTTPPRefer();
  
          $this->server->httpResponse->sendStatus(207);
          $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
          $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
          $this->server->httpResponse->sendBody($this->server->generateMultiStatus($propertyList, $prefer['return-minimal']));
  
      }
  
      /**
       * This function handles the calendar-query REPORT
       *
       * This report is used by clients to request calendar objects based on
       * complex conditions.
       *
6d9380f96   Cédric Dupont   Update sources OC...
507
       * @param \DOMNode $dom
03e52840d   Kload   Init
508
509
510
       * @return void
       */
      public function calendarQueryReport($dom) {
6d9380f96   Cédric Dupont   Update sources OC...
511
          $parser = new CalendarQueryParser($dom);
03e52840d   Kload   Init
512
513
514
515
516
517
518
519
520
521
          $parser->parse();
  
          $node = $this->server->tree->getNodeForPath($this->server->getRequestUri());
          $depth = $this->server->getHTTPDepth(0);
  
          // The default result is an empty array
          $result = array();
  
          // The calendarobject was requested directly. In this case we handle
          // this locally.
6d9380f96   Cédric Dupont   Update sources OC...
522
          if ($depth == 0 && $node instanceof ICalendarObject) {
03e52840d   Kload   Init
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
  
              $requestedCalendarData = true;
              $requestedProperties = $parser->requestedProperties;
  
              if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) {
  
                  // We always retrieve calendar-data, as we need it for filtering.
                  $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data';
  
                  // If calendar-data wasn't explicitly requested, we need to remove
                  // it after processing.
                  $requestedCalendarData = false;
              }
  
              $properties = $this->server->getPropertiesForPath(
                  $this->server->getRequestUri(),
                  $requestedProperties,
                  0
              );
  
              // This array should have only 1 element, the first calendar
              // object.
              $properties = current($properties);
  
              // If there wasn't any calendar-data returned somehow, we ignore
              // this.
              if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) {
6d9380f96   Cédric Dupont   Update sources OC...
550
                  $validator = new CalendarQueryValidator();
03e52840d   Kload   Init
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
                  $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
                  if ($validator->validate($vObject,$parser->filters)) {
  
                      // If the client didn't require the calendar-data property,
                      // we won't give it back.
                      if (!$requestedCalendarData) {
                          unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
                      } else {
                          if ($parser->expand) {
                              $vObject->expand($parser->expand['start'], $parser->expand['end']);
                              $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
                          }
                      }
  
                      $result = array($properties);
  
                  }
  
              }
  
          }
          // If we're dealing with a calendar, the calendar itself is responsible
          // for the calendar-query.
6d9380f96   Cédric Dupont   Update sources OC...
574
          if ($node instanceof ICalendar && $depth = 1) {
03e52840d   Kload   Init
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
  
              $nodePaths = $node->calendarQuery($parser->filters);
  
              foreach($nodePaths as $path) {
  
                  list($properties) =
                      $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $parser->requestedProperties);
  
                  if ($parser->expand) {
                      // We need to do some post-processing
                      $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']);
                      $vObject->expand($parser->expand['start'], $parser->expand['end']);
                      $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize();
                  }
  
                  $result[] = $properties;
  
              }
  
          }
  
          $prefer = $this->server->getHTTPPRefer();
  
          $this->server->httpResponse->sendStatus(207);
          $this->server->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
          $this->server->httpResponse->setHeader('Vary','Brief,Prefer');
          $this->server->httpResponse->sendBody($this->server->generateMultiStatus($result, $prefer['return-minimal']));
  
      }
  
      /**
       * This method is responsible for parsing the request and generating the
       * response for the CALDAV:free-busy-query REPORT.
       *
6d9380f96   Cédric Dupont   Update sources OC...
609
       * @param \DOMNode $dom
03e52840d   Kload   Init
610
611
       * @return void
       */
6d9380f96   Cédric Dupont   Update sources OC...
612
      protected function freeBusyQueryReport(\DOMNode $dom) {
03e52840d   Kload   Init
613
614
615
616
617
  
          $start = null;
          $end = null;
  
          foreach($dom->firstChild->childNodes as $childNode) {
6d9380f96   Cédric Dupont   Update sources OC...
618
              $clark = DAV\XMLUtil::toClarkNotation($childNode);
03e52840d   Kload   Init
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
              if ($clark == '{' . self::NS_CALDAV . '}time-range') {
                  $start = $childNode->getAttribute('start');
                  $end = $childNode->getAttribute('end');
                  break;
              }
  
          }
          if ($start) {
              $start = VObject\DateTimeParser::parseDateTime($start);
          }
          if ($end) {
              $end = VObject\DateTimeParser::parseDateTime($end);
          }
  
          if (!$start && !$end) {
6d9380f96   Cédric Dupont   Update sources OC...
634
              throw new DAV\Exception\BadRequest('The freebusy report must have a time-range filter');
03e52840d   Kload   Init
635
636
637
638
          }
          $acl = $this->server->getPlugin('acl');
  
          if (!$acl) {
6d9380f96   Cédric Dupont   Update sources OC...
639
              throw new DAV\Exception('The ACL plugin must be loaded for free-busy queries to work');
03e52840d   Kload   Init
640
641
642
643
644
          }
          $uri = $this->server->getRequestUri();
          $acl->checkPrivileges($uri,'{' . self::NS_CALDAV . '}read-free-busy');
  
          $calendar = $this->server->tree->getNodeForPath($uri);
6d9380f96   Cédric Dupont   Update sources OC...
645
646
          if (!$calendar instanceof ICalendar) {
              throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars');
03e52840d   Kload   Init
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
          }
  
          // Doing a calendar-query first, to make sure we get the most
          // performance.
          $urls = $calendar->calendarQuery(array(
              'name' => 'VCALENDAR',
              'comp-filters' => array(
                  array(
                      'name' => 'VEVENT',
                      'comp-filters' => array(),
                      'prop-filters' => array(),
                      'is-not-defined' => false,
                      'time-range' => array(
                          'start' => $start,
                          'end' => $end,
                      ),
                  ),
              ),
              'prop-filters' => array(),
              'is-not-defined' => false,
              'time-range' => null,
          ));
  
          $objects = array_map(function($url) use ($calendar) {
              $obj = $calendar->getChild($url)->get();
              return $obj;
          }, $urls);
  
          $generator = new VObject\FreeBusyGenerator();
          $generator->setObjects($objects);
          $generator->setTimeRange($start, $end);
          $result = $generator->getResult();
          $result = $result->serialize();
  
          $this->server->httpResponse->sendStatus(200);
          $this->server->httpResponse->setHeader('Content-Type', 'text/calendar');
          $this->server->httpResponse->setHeader('Content-Length', strlen($result));
          $this->server->httpResponse->sendBody($result);
  
      }
  
      /**
       * This method is triggered before a file gets updated with new content.
       *
       * This plugin uses this method to ensure that CalDAV objects receive
       * valid calendar data.
       *
       * @param string $path
6d9380f96   Cédric Dupont   Update sources OC...
695
       * @param DAV\IFile $node
03e52840d   Kload   Init
696
697
698
       * @param resource $data
       * @return void
       */
6d9380f96   Cédric Dupont   Update sources OC...
699
      public function beforeWriteContent($path, DAV\IFile $node, &$data) {
03e52840d   Kload   Init
700

6d9380f96   Cédric Dupont   Update sources OC...
701
          if (!$node instanceof ICalendarObject)
03e52840d   Kload   Init
702
703
704
705
706
707
708
709
710
711
712
713
714
715
              return;
  
          $this->validateICalendar($data, $path);
  
      }
  
      /**
       * This method is triggered before a new file is created.
       *
       * This plugin uses this method to ensure that newly created calendar
       * objects contain valid calendar data.
       *
       * @param string $path
       * @param resource $data
6d9380f96   Cédric Dupont   Update sources OC...
716
       * @param DAV\ICollection $parentNode
03e52840d   Kload   Init
717
718
       * @return void
       */
6d9380f96   Cédric Dupont   Update sources OC...
719
      public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode) {
03e52840d   Kload   Init
720

6d9380f96   Cédric Dupont   Update sources OC...
721
          if (!$parentNode instanceof Calendar)
03e52840d   Kload   Init
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
              return;
  
          $this->validateICalendar($data, $path);
  
      }
  
      /**
       * This event is triggered before any HTTP request is handled.
       *
       * We use this to intercept GET calls to notification nodes, and return the
       * proper response.
       *
       * @param string $method
       * @param string $path
       * @return void
       */
      public function beforeMethod($method, $path) {
  
          if ($method!=='GET') return;
  
          try {
              $node = $this->server->tree->getNodeForPath($path);
6d9380f96   Cédric Dupont   Update sources OC...
744
          } catch (DAV\Exception\NotFound $e) {
03e52840d   Kload   Init
745
746
              return;
          }
6d9380f96   Cédric Dupont   Update sources OC...
747
          if (!$node instanceof Notifications\INode)
03e52840d   Kload   Init
748
749
750
              return;
  
          if (!$this->server->checkPreconditions(true)) return false;
6d9380f96   Cédric Dupont   Update sources OC...
751
          $dom = new \DOMDocument('1.0', 'UTF-8');
03e52840d   Kload   Init
752

03e52840d   Kload   Init
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
          $dom->formatOutput = true;
  
          $root = $dom->createElement('cs:notification');
          foreach($this->server->xmlNamespaces as $namespace => $prefix) {
              $root->setAttribute('xmlns:' . $prefix, $namespace);
          }
  
          $dom->appendChild($root);
          $node->getNotificationType()->serializeBody($this->server, $root);
  
          $this->server->httpResponse->setHeader('Content-Type','application/xml');
          $this->server->httpResponse->setHeader('ETag',$node->getETag());
          $this->server->httpResponse->sendStatus(200);
          $this->server->httpResponse->sendBody($dom->saveXML());
  
          return false;
  
      }
  
      /**
       * Checks if the submitted iCalendar data is in fact, valid.
       *
       * An exception is thrown if it's not.
       *
       * @param resource|string $data
       * @param string $path
       * @return void
       */
      protected function validateICalendar(&$data, $path) {
  
          // If it's a stream, we convert it to a string first.
          if (is_resource($data)) {
              $data = stream_get_contents($data);
          }
  
          // Converting the data to unicode, if needed.
6d9380f96   Cédric Dupont   Update sources OC...
789
          $data = DAV\StringUtil::ensureUTF8($data);
03e52840d   Kload   Init
790
791
792
793
794
795
  
          try {
  
              $vobj = VObject\Reader::read($data);
  
          } catch (VObject\ParseException $e) {
6d9380f96   Cédric Dupont   Update sources OC...
796
              throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage());
03e52840d   Kload   Init
797
798
799
800
  
          }
  
          if ($vobj->name !== 'VCALENDAR') {
6d9380f96   Cédric Dupont   Update sources OC...
801
              throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.');
03e52840d   Kload   Init
802
803
804
          }
  
          // Get the Supported Components for the target calendar
6d9380f96   Cédric Dupont   Update sources OC...
805
          list($parentPath,$object) = DAV\URLUtil::splitPath($path);
03e52840d   Kload   Init
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
          $calendarProperties = $this->server->getProperties($parentPath,array('{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'));
          $supportedComponents = $calendarProperties['{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set']->getValue();
  
          $foundType = null;
          $foundUID = null;
          foreach($vobj->getComponents() as $component) {
              switch($component->name) {
                  case 'VTIMEZONE' :
                      continue 2;
                  case 'VEVENT' :
                  case 'VTODO' :
                  case 'VJOURNAL' :
                      if (is_null($foundType)) {
                          $foundType = $component->name;
                          if (!in_array($foundType, $supportedComponents)) {
6d9380f96   Cédric Dupont   Update sources OC...
821
                              throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType);
03e52840d   Kload   Init
822
823
                          }
                          if (!isset($component->UID)) {
6d9380f96   Cédric Dupont   Update sources OC...
824
                              throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID');
03e52840d   Kload   Init
825
826
827
828
                          }
                          $foundUID = (string)$component->UID;
                      } else {
                          if ($foundType !== $component->name) {
6d9380f96   Cédric Dupont   Update sources OC...
829
                              throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType);
03e52840d   Kload   Init
830
831
                          }
                          if ($foundUID !== (string)$component->UID) {
6d9380f96   Cédric Dupont   Update sources OC...
832
                              throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs');
03e52840d   Kload   Init
833
834
835
836
                          }
                      }
                      break;
                  default :
6d9380f96   Cédric Dupont   Update sources OC...
837
                      throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here');
03e52840d   Kload   Init
838
839
840
841
  
              }
          }
          if (!$foundType)
6d9380f96   Cédric Dupont   Update sources OC...
842
              throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL');
03e52840d   Kload   Init
843
844
845
846
847
848
849
850
851
852
853
854
855
856
  
      }
  
      /**
       * This method handles POST requests to the schedule-outbox.
       *
       * Currently, two types of requests are support:
       *   * FREEBUSY requests from RFC 6638
       *   * Simple iTIP messages from draft-desruisseaux-caldav-sched-04
       *
       * The latter is from an expired early draft of the CalDAV scheduling
       * extensions, but iCal depends on a feature from that spec, so we
       * implement it.
       *
6d9380f96   Cédric Dupont   Update sources OC...
857
       * @param Schedule\IOutbox $outboxNode
03e52840d   Kload   Init
858
859
860
       * @param string $outboxUri
       * @return void
       */
6d9380f96   Cédric Dupont   Update sources OC...
861
      public function outboxRequest(Schedule\IOutbox $outboxNode, $outboxUri) {
03e52840d   Kload   Init
862
863
864
865
866
  
          // Parsing the request body
          try {
              $vObject = VObject\Reader::read($this->server->httpRequest->getBody(true));
          } catch (VObject\ParseException $e) {
6d9380f96   Cédric Dupont   Update sources OC...
867
              throw new DAV\Exception\BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage());
03e52840d   Kload   Init
868
869
870
871
872
873
874
875
876
877
878
879
880
          }
  
          // The incoming iCalendar object must have a METHOD property, and a
          // component. The combination of both determines what type of request
          // this is.
          $componentType = null;
          foreach($vObject->getComponents() as $component) {
              if ($component->name !== 'VTIMEZONE') {
                  $componentType = $component->name;
                  break;
              }
          }
          if (is_null($componentType)) {
6d9380f96   Cédric Dupont   Update sources OC...
881
              throw new DAV\Exception\BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component');
03e52840d   Kload   Init
882
883
884
885
886
          }
  
          // Validating the METHOD
          $method = strtoupper((string)$vObject->METHOD);
          if (!$method) {
6d9380f96   Cédric Dupont   Update sources OC...
887
              throw new DAV\Exception\BadRequest('A METHOD property must be specified in iTIP messages');
03e52840d   Kload   Init
888
889
890
891
892
893
894
895
896
897
          }
  
          // So we support two types of requests:
          //
          // REQUEST with a VFREEBUSY component
          // REQUEST, REPLY, ADD, CANCEL on VEVENT components
  
          $acl = $this->server->getPlugin('acl');
  
          if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') {
6d9380f96   Cédric Dupont   Update sources OC...
898
              $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-query-freebusy');
03e52840d   Kload   Init
899
900
901
              $this->handleFreeBusyRequest($outboxNode, $vObject);
  
          } elseif ($componentType === 'VEVENT' && in_array($method, array('REQUEST','REPLY','ADD','CANCEL'))) {
6d9380f96   Cédric Dupont   Update sources OC...
902
              $acl && $acl->checkPrivileges($outboxUri,'{' . Plugin::NS_CALDAV . '}schedule-post-vevent');
03e52840d   Kload   Init
903
904
905
              $this->handleEventNotification($outboxNode, $vObject);
  
          } else {
6d9380f96   Cédric Dupont   Update sources OC...
906
              throw new DAV\Exception\NotImplemented('SabreDAV supports only VFREEBUSY (REQUEST) and VEVENT (REQUEST, REPLY, ADD, CANCEL)');
03e52840d   Kload   Init
907
908
909
910
911
912
913
914
915
916
917
  
          }
  
      }
  
      /**
       * This method handles the REQUEST, REPLY, ADD and CANCEL methods for
       * VEVENT iTip messages.
       *
       * @return void
       */
6d9380f96   Cédric Dupont   Update sources OC...
918
      protected function handleEventNotification(Schedule\IOutbox $outboxNode, VObject\Component $vObject) {
03e52840d   Kload   Init
919
920
921
922
923
  
          $originator = $this->server->httpRequest->getHeader('Originator');
          $recipients = $this->server->httpRequest->getHeader('Recipient');
  
          if (!$originator) {
6d9380f96   Cédric Dupont   Update sources OC...
924
              throw new DAV\Exception\BadRequest('The Originator: header must be specified when making POST requests');
03e52840d   Kload   Init
925
926
          }
          if (!$recipients) {
6d9380f96   Cédric Dupont   Update sources OC...
927
              throw new DAV\Exception\BadRequest('The Recipient: header must be specified when making POST requests');
03e52840d   Kload   Init
928
929
930
931
932
933
934
          }
  
          $recipients = explode(',',$recipients);
          foreach($recipients as $k=>$recipient) {
  
              $recipient = trim($recipient);
              if (!preg_match('/^mailto:(.*)@(.*)$/i', $recipient)) {
6d9380f96   Cédric Dupont   Update sources OC...
935
                  throw new DAV\Exception\BadRequest('Recipients must start with mailto: and must be valid email address');
03e52840d   Kload   Init
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
              }
              $recipient = substr($recipient, 7);
              $recipients[$k] = $recipient;
          }
  
          // We need to make sure that 'originator' matches one of the email
          // addresses of the selected principal.
          $principal = $outboxNode->getOwner();
          $props = $this->server->getProperties($principal,array(
              '{' . self::NS_CALDAV . '}calendar-user-address-set',
          ));
  
          $addresses = array();
          if (isset($props['{' . self::NS_CALDAV . '}calendar-user-address-set'])) {
              $addresses = $props['{' . self::NS_CALDAV . '}calendar-user-address-set']->getHrefs();
          }
  
          $found = false;
          foreach($addresses as $address) {
  
              // Trimming the / on both sides, just in case..
              if (rtrim(strtolower($originator),'/') === rtrim(strtolower($address),'/')) {
                  $found = true;
                  break;
              }
  
          }
  
          if (!$found) {
6d9380f96   Cédric Dupont   Update sources OC...
965
              throw new DAV\Exception\Forbidden('The addresses specified in the Originator header did not match any addresses in the owners calendar-user-address-set header');
03e52840d   Kload   Init
966
967
968
969
970
971
972
973
974
          }
  
          // If the Originator header was a url, and not a mailto: address..
          // we're going to try to pull the mailto: from the vobject body.
          if (strtolower(substr($originator,0,7)) !== 'mailto:') {
              $originator = (string)$vObject->VEVENT->ORGANIZER;
  
          }
          if (strtolower(substr($originator,0,7)) !== 'mailto:') {
6d9380f96   Cédric Dupont   Update sources OC...
975
              throw new DAV\Exception\Forbidden('Could not find mailto: address in both the Orignator header, and the ORGANIZER property in the VEVENT');
03e52840d   Kload   Init
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
          }
          $originator = substr($originator,7);
  
          $result = $this->iMIPMessage($originator, $recipients, $vObject, $principal);
          $this->server->httpResponse->sendStatus(200);
          $this->server->httpResponse->setHeader('Content-Type','application/xml');
          $this->server->httpResponse->sendBody($this->generateScheduleResponse($result));
  
      }
  
      /**
       * Sends an iMIP message by email.
       *
       * This method must return an array with status codes per recipient.
       * This should look something like:
       *
       * array(
       *    'user1@example.org' => '2.0;Success'
       * )
       *
       * Formatting for this status code can be found at:
       * https://tools.ietf.org/html/rfc5545#section-3.8.8.3
       *
       * A list of valid status codes can be found at:
       * https://tools.ietf.org/html/rfc5546#section-3.6
       *
       * @param string $originator
       * @param array $recipients
6d9380f96   Cédric Dupont   Update sources OC...
1004
1005
1006
       * @param VObject\Component $vObject
       * @param string $principal Principal url
       * @return array 
03e52840d   Kload   Init
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
       */
      protected function iMIPMessage($originator, array $recipients, VObject\Component $vObject, $principal) {
  
          if (!$this->imipHandler) {
              $resultStatus = '5.2;This server does not support this operation';
          } else {
              $this->imipHandler->sendMessage($originator, $recipients, $vObject, $principal);
              $resultStatus = '2.0;Success';
          }
  
          $result = array();
          foreach($recipients as $recipient) {
              $result[$recipient] = $resultStatus;
          }
  
          return $result;
  
      }
  
      /**
       * Generates a schedule-response XML body
       *
       * The recipients array is a key->value list, containing email addresses
       * and iTip status codes. See the iMIPMessage method for a description of
       * the value.
       *
       * @param array $recipients
       * @return string
       */
      public function generateScheduleResponse(array $recipients) {
6d9380f96   Cédric Dupont   Update sources OC...
1037
          $dom = new \DOMDocument('1.0','utf-8');
03e52840d   Kload   Init
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
          $dom->formatOutput = true;
          $xscheduleResponse = $dom->createElement('cal:schedule-response');
          $dom->appendChild($xscheduleResponse);
  
          foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
  
              $xscheduleResponse->setAttribute('xmlns:' . $prefix, $namespace);
  
          }
  
          foreach($recipients as $recipient=>$status) {
              $xresponse = $dom->createElement('cal:response');
  
              $xrecipient = $dom->createElement('cal:recipient');
              $xrecipient->appendChild($dom->createTextNode($recipient));
              $xresponse->appendChild($xrecipient);
  
              $xrequestStatus = $dom->createElement('cal:request-status');
              $xrequestStatus->appendChild($dom->createTextNode($status));
              $xresponse->appendChild($xrequestStatus);
  
              $xscheduleResponse->appendChild($xresponse);
  
          }
  
          return $dom->saveXML();
  
      }
  
      /**
       * This method is responsible for parsing a free-busy query request and
       * returning it's result.
       *
6d9380f96   Cédric Dupont   Update sources OC...
1071
       * @param Schedule\IOutbox $outbox
03e52840d   Kload   Init
1072
1073
1074
       * @param string $request
       * @return string
       */
6d9380f96   Cédric Dupont   Update sources OC...
1075
      protected function handleFreeBusyRequest(Schedule\IOutbox $outbox, VObject\Component $vObject) {
03e52840d   Kload   Init
1076
1077
1078
1079
1080
1081
1082
1083
  
          $vFreeBusy = $vObject->VFREEBUSY;
          $organizer = $vFreeBusy->organizer;
  
          $organizer = (string)$organizer;
  
          // Validating if the organizer matches the owner of the inbox.
          $owner = $outbox->getOwner();
6d9380f96   Cédric Dupont   Update sources OC...
1084
          $caldavNS = '{' . Plugin::NS_CALDAV . '}';
03e52840d   Kload   Init
1085
1086
1087
1088
1089
  
          $uas = $caldavNS . 'calendar-user-address-set';
          $props = $this->server->getProperties($owner,array($uas));
  
          if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) {
6d9380f96   Cédric Dupont   Update sources OC...
1090
              throw new DAV\Exception\Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox');
03e52840d   Kload   Init
1091
1092
1093
          }
  
          if (!isset($vFreeBusy->ATTENDEE)) {
6d9380f96   Cédric Dupont   Update sources OC...
1094
              throw new DAV\Exception\BadRequest('You must at least specify 1 attendee');
03e52840d   Kload   Init
1095
1096
1097
1098
1099
1100
1101
1102
1103
          }
  
          $attendees = array();
          foreach($vFreeBusy->ATTENDEE as $attendee) {
              $attendees[]= (string)$attendee;
          }
  
  
          if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) {
6d9380f96   Cédric Dupont   Update sources OC...
1104
              throw new DAV\Exception\BadRequest('DTSTART and DTEND must both be specified');
03e52840d   Kload   Init
1105
1106
1107
1108
1109
1110
1111
1112
1113
          }
  
          $startRange = $vFreeBusy->DTSTART->getDateTime();
          $endRange = $vFreeBusy->DTEND->getDateTime();
  
          $results = array();
          foreach($attendees as $attendee) {
              $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject);
          }
6d9380f96   Cédric Dupont   Update sources OC...
1114
          $dom = new \DOMDocument('1.0','utf-8');
03e52840d   Kload   Init
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
          $dom->formatOutput = true;
          $scheduleResponse = $dom->createElement('cal:schedule-response');
          foreach($this->server->xmlNamespaces as $namespace=>$prefix) {
  
              $scheduleResponse->setAttribute('xmlns:' . $prefix,$namespace);
  
          }
          $dom->appendChild($scheduleResponse);
  
          foreach($results as $result) {
              $response = $dom->createElement('cal:response');
  
              $recipient = $dom->createElement('cal:recipient');
              $recipientHref = $dom->createElement('d:href');
  
              $recipientHref->appendChild($dom->createTextNode($result['href']));
              $recipient->appendChild($recipientHref);
              $response->appendChild($recipient);
  
              $reqStatus = $dom->createElement('cal:request-status');
              $reqStatus->appendChild($dom->createTextNode($result['request-status']));
              $response->appendChild($reqStatus);
  
              if (isset($result['calendar-data'])) {
  
                  $calendardata = $dom->createElement('cal:calendar-data');
                  $calendardata->appendChild($dom->createTextNode(str_replace("\r
  ","
  ",$result['calendar-data']->serialize())));
                  $response->appendChild($calendardata);
  
              }
              $scheduleResponse->appendChild($response);
          }
  
          $this->server->httpResponse->sendStatus(200);
          $this->server->httpResponse->setHeader('Content-Type','application/xml');
          $this->server->httpResponse->sendBody($dom->saveXML());
  
      }
  
      /**
       * Returns free-busy information for a specific address. The returned
       * data is an array containing the following properties:
       *
       * calendar-data : A VFREEBUSY VObject
       * request-status : an iTip status code.
       * href: The principal's email address, as requested
       *
       * The following request status codes may be returned:
       *   * 2.0;description
       *   * 3.7;description
       *
       * @param string $email address
6d9380f96   Cédric Dupont   Update sources OC...
1169
1170
1171
1172
       * @param \DateTime $start
       * @param \DateTime $end
       * @param VObject\Component $request
       * @return array
03e52840d   Kload   Init
1173
       */
6d9380f96   Cédric Dupont   Update sources OC...
1174
      protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) {
03e52840d   Kload   Init
1175

6d9380f96   Cédric Dupont   Update sources OC...
1176
          $caldavNS = '{' . Plugin::NS_CALDAV . '}';
03e52840d   Kload   Init
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
  
          $aclPlugin = $this->server->getPlugin('acl');
          if (substr($email,0,7)==='mailto:') $email = substr($email,7);
  
          $result = $aclPlugin->principalSearch(
              array('{http://sabredav.org/ns}email-address' => $email),
              array(
                  '{DAV:}principal-URL', $caldavNS . 'calendar-home-set',
                  '{http://sabredav.org/ns}email-address',
              )
          );
  
          if (!count($result)) {
              return array(
                  'request-status' => '3.7;Could not find principal',
                  'href' => 'mailto:' . $email,
              );
          }
  
          if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) {
              return array(
                  'request-status' => '3.7;No calendar-home-set property found',
                  'href' => 'mailto:' . $email,
              );
          }
          $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref();
  
          // Grabbing the calendar list
          $objects = array();
          foreach($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) {
6d9380f96   Cédric Dupont   Update sources OC...
1207
              if (!$node instanceof ICalendar) {
03e52840d   Kload   Init
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
                  continue;
              }
              $aclPlugin->checkPrivileges($homeSet . $node->getName() ,$caldavNS . 'read-free-busy');
  
              // Getting the list of object uris within the time-range
              $urls = $node->calendarQuery(array(
                  'name' => 'VCALENDAR',
                  'comp-filters' => array(
                      array(
                          'name' => 'VEVENT',
                          'comp-filters' => array(),
                          'prop-filters' => array(),
                          'is-not-defined' => false,
                          'time-range' => array(
                              'start' => $start,
                              'end' => $end,
                          ),
                      ),
                  ),
                  'prop-filters' => array(),
                  'is-not-defined' => false,
                  'time-range' => null,
              ));
  
              $calObjects = array_map(function($url) use ($node) {
                  $obj = $node->getChild($url)->get();
                  return $obj;
              }, $urls);
  
              $objects = array_merge($objects,$calObjects);
  
          }
6d9380f96   Cédric Dupont   Update sources OC...
1240
          $vcalendar = new VObject\Component\VCalendar();
03e52840d   Kload   Init
1241
1242
1243
          $vcalendar->VERSION = '2.0';
          $vcalendar->METHOD = 'REPLY';
          $vcalendar->CALSCALE = 'GREGORIAN';
6d9380f96   Cédric Dupont   Update sources OC...
1244
          $vcalendar->PRODID = '-//SabreDAV//SabreDAV ' . DAV\Version::VERSION . '//EN';
03e52840d   Kload   Init
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
  
          $generator = new VObject\FreeBusyGenerator();
          $generator->setObjects($objects);
          $generator->setTimeRange($start, $end);
          $generator->setBaseObject($vcalendar);
  
          $result = $generator->getResult();
  
          $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email;
          $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID;
          $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER;
  
          return array(
              'calendar-data' => $result,
              'request-status' => '2.0;Success',
              'href' => 'mailto:' . $email,
          );
      }
  
      /**
       * This method is used to generate HTML output for the
6d9380f96   Cédric Dupont   Update sources OC...
1266
       * DAV\Browser\Plugin. This allows us to generate an interface users
03e52840d   Kload   Init
1267
1268
       * can use to create new calendars.
       *
6d9380f96   Cédric Dupont   Update sources OC...
1269
       * @param DAV\INode $node
03e52840d   Kload   Init
1270
1271
1272
       * @param string $output
       * @return bool
       */
6d9380f96   Cédric Dupont   Update sources OC...
1273
      public function htmlActionsPanel(DAV\INode $node, &$output) {
03e52840d   Kload   Init
1274

6d9380f96   Cédric Dupont   Update sources OC...
1275
          if (!$node instanceof UserCalendars)
03e52840d   Kload   Init
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
              return;
  
          $output.= '<tr><td colspan="2"><form method="post" action="">
              <h3>Create new calendar</h3>
              <input type="hidden" name="sabreAction" value="mkcalendar" />
              <label>Name (uri):</label> <input type="text" name="name" /><br />
              <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br />
              <input type="submit" value="create" />
              </form>
              </td></tr>';
  
          return false;
  
      }
  
      /**
       * This method allows us to intercept the 'mkcalendar' sabreAction. This
       * action enables the user to create new calendars from the browser plugin.
       *
       * @param string $uri
       * @param string $action
       * @param array $postVars
       * @return bool
       */
      public function browserPostAction($uri, $action, array $postVars) {
  
          if ($action!=='mkcalendar')
              return;
  
          $resourceType = array('{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar');
          $properties = array();
          if (isset($postVars['{DAV:}displayname'])) {
              $properties['{DAV:}displayname'] = $postVars['{DAV:}displayname'];
          }
          $this->server->createCollection($uri . '/' . $postVars['name'],$resourceType,$properties);
          return false;
  
      }
  
  }