1<?php 2 3namespace Sabre\CalDAV; 4 5use DateTimeZone; 6use Sabre\DAV; 7use Sabre\DAV\Exception\BadRequest; 8use Sabre\DAV\MkCol; 9use Sabre\DAV\Xml\Property\Href; 10use Sabre\DAVACL; 11use Sabre\VObject; 12use Sabre\HTTP; 13use Sabre\Uri; 14use Sabre\HTTP\RequestInterface; 15use Sabre\HTTP\ResponseInterface; 16 17/** 18 * CalDAV plugin 19 * 20 * This plugin provides functionality added by CalDAV (RFC 4791) 21 * It implements new reports, and the MKCALENDAR method. 22 * 23 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). 24 * @author Evert Pot (http://evertpot.com/) 25 * @license http://sabre.io/license/ Modified BSD License 26 */ 27class Plugin extends DAV\ServerPlugin { 28 29 /** 30 * This is the official CalDAV namespace 31 */ 32 const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav'; 33 34 /** 35 * This is the namespace for the proprietary calendarserver extensions 36 */ 37 const NS_CALENDARSERVER = 'http://calendarserver.org/ns/'; 38 39 /** 40 * The hardcoded root for calendar objects. It is unfortunate 41 * that we're stuck with it, but it will have to do for now 42 */ 43 const CALENDAR_ROOT = 'calendars'; 44 45 /** 46 * Reference to server object 47 * 48 * @var DAV\Server 49 */ 50 protected $server; 51 52 /** 53 * The default PDO storage uses a MySQL MEDIUMBLOB for iCalendar data, 54 * which can hold up to 2^24 = 16777216 bytes. This is plenty. We're 55 * capping it to 10M here. 56 */ 57 protected $maxResourceSize = 10000000; 58 59 /** 60 * Use this method to tell the server this plugin defines additional 61 * HTTP methods. 62 * 63 * This method is passed a uri. It should only return HTTP methods that are 64 * available for the specified uri. 65 * 66 * @param string $uri 67 * @return array 68 */ 69 function getHTTPMethods($uri) { 70 71 // The MKCALENDAR is only available on unmapped uri's, whose 72 // parents extend IExtendedCollection 73 list($parent, $name) = Uri\split($uri); 74 75 $node = $this->server->tree->getNodeForPath($parent); 76 77 if ($node instanceof DAV\IExtendedCollection) { 78 try { 79 $node->getChild($name); 80 } catch (DAV\Exception\NotFound $e) { 81 return ['MKCALENDAR']; 82 } 83 } 84 return []; 85 86 } 87 88 /** 89 * Returns the path to a principal's calendar home. 90 * 91 * The return url must not end with a slash. 92 * 93 * @param string $principalUrl 94 * @return string 95 */ 96 function getCalendarHomeForPrincipal($principalUrl) { 97 98 // The default is a bit naive, but it can be overwritten. 99 list(, $nodeName) = Uri\split($principalUrl); 100 101 return self::CALENDAR_ROOT . '/' . $nodeName; 102 103 } 104 105 /** 106 * Returns a list of features for the DAV: HTTP header. 107 * 108 * @return array 109 */ 110 function getFeatures() { 111 112 return ['calendar-access', 'calendar-proxy']; 113 114 } 115 116 /** 117 * Returns a plugin name. 118 * 119 * Using this name other plugins will be able to access other plugins 120 * using DAV\Server::getPlugin 121 * 122 * @return string 123 */ 124 function getPluginName() { 125 126 return 'caldav'; 127 128 } 129 130 /** 131 * Returns a list of reports this plugin supports. 132 * 133 * This will be used in the {DAV:}supported-report-set property. 134 * Note that you still need to subscribe to the 'report' event to actually 135 * implement them 136 * 137 * @param string $uri 138 * @return array 139 */ 140 function getSupportedReportSet($uri) { 141 142 $node = $this->server->tree->getNodeForPath($uri); 143 144 $reports = []; 145 if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) { 146 $reports[] = '{' . self::NS_CALDAV . '}calendar-multiget'; 147 $reports[] = '{' . self::NS_CALDAV . '}calendar-query'; 148 } 149 if ($node instanceof ICalendar) { 150 $reports[] = '{' . self::NS_CALDAV . '}free-busy-query'; 151 } 152 // iCal has a bug where it assumes that sync support is enabled, only 153 // if we say we support it on the calendar-home, even though this is 154 // not actually the case. 155 if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) { 156 $reports[] = '{DAV:}sync-collection'; 157 } 158 return $reports; 159 160 } 161 162 /** 163 * Initializes the plugin 164 * 165 * @param DAV\Server $server 166 * @return void 167 */ 168 function initialize(DAV\Server $server) { 169 170 $this->server = $server; 171 172 $server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']); 173 $server->on('report', [$this, 'report']); 174 $server->on('propFind', [$this, 'propFind']); 175 $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); 176 $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); 177 $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); 178 $server->on('afterMethod:GET', [$this, 'httpAfterGET']); 179 180 $server->xml->namespaceMap[self::NS_CALDAV] = 'cal'; 181 $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; 182 183 $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport'; 184 $server->xml->elementMap['{' . self::NS_CALDAV . '}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport'; 185 $server->xml->elementMap['{' . self::NS_CALDAV . '}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport'; 186 $server->xml->elementMap['{' . self::NS_CALDAV . '}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar'; 187 $server->xml->elementMap['{' . self::NS_CALDAV . '}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp'; 188 $server->xml->elementMap['{' . self::NS_CALDAV . '}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; 189 190 $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar'; 191 192 $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read'; 193 $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write'; 194 195 array_push($server->protectedProperties, 196 197 '{' . self::NS_CALDAV . '}supported-calendar-component-set', 198 '{' . self::NS_CALDAV . '}supported-calendar-data', 199 '{' . self::NS_CALDAV . '}max-resource-size', 200 '{' . self::NS_CALDAV . '}min-date-time', 201 '{' . self::NS_CALDAV . '}max-date-time', 202 '{' . self::NS_CALDAV . '}max-instances', 203 '{' . self::NS_CALDAV . '}max-attendees-per-instance', 204 '{' . self::NS_CALDAV . '}calendar-home-set', 205 '{' . self::NS_CALDAV . '}supported-collation-set', 206 '{' . self::NS_CALDAV . '}calendar-data', 207 208 // CalendarServer extensions 209 '{' . self::NS_CALENDARSERVER . '}getctag', 210 '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for', 211 '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for' 212 213 ); 214 215 if ($aclPlugin = $server->getPlugin('acl')) { 216 $aclPlugin->principalSearchPropertySet['{' . self::NS_CALDAV . '}calendar-user-address-set'] = 'Calendar address'; 217 } 218 } 219 220 /** 221 * This functions handles REPORT requests specific to CalDAV 222 * 223 * @param string $reportName 224 * @param mixed $report 225 * @return bool 226 */ 227 function report($reportName, $report) { 228 229 switch ($reportName) { 230 case '{' . self::NS_CALDAV . '}calendar-multiget' : 231 $this->server->transactionType = 'report-calendar-multiget'; 232 $this->calendarMultiGetReport($report); 233 return false; 234 case '{' . self::NS_CALDAV . '}calendar-query' : 235 $this->server->transactionType = 'report-calendar-query'; 236 $this->calendarQueryReport($report); 237 return false; 238 case '{' . self::NS_CALDAV . '}free-busy-query' : 239 $this->server->transactionType = 'report-free-busy-query'; 240 $this->freeBusyQueryReport($report); 241 return false; 242 243 } 244 245 246 } 247 248 /** 249 * This function handles the MKCALENDAR HTTP method, which creates 250 * a new calendar. 251 * 252 * @param RequestInterface $request 253 * @param ResponseInterface $response 254 * @return bool 255 */ 256 function httpMkCalendar(RequestInterface $request, ResponseInterface $response) { 257 258 $body = $request->getBodyAsString(); 259 $path = $request->getPath(); 260 261 $properties = []; 262 263 if ($body) { 264 265 try { 266 $mkcalendar = $this->server->xml->expect( 267 '{urn:ietf:params:xml:ns:caldav}mkcalendar', 268 $body 269 ); 270 } catch (\Sabre\Xml\ParseException $e) { 271 throw new BadRequest($e->getMessage(), null, $e); 272 } 273 $properties = $mkcalendar->getProperties(); 274 275 } 276 277 // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored 278 // subscriptions. Before that it used MKCOL which was the correct way 279 // to do this. 280 // 281 // If the body had a {DAV:}resourcetype, it means we stumbled upon this 282 // request, and we simply use it instead of the pre-defined list. 283 if (isset($properties['{DAV:}resourcetype'])) { 284 $resourceType = $properties['{DAV:}resourcetype']->getValue(); 285 } else { 286 $resourceType = ['{DAV:}collection','{urn:ietf:params:xml:ns:caldav}calendar']; 287 } 288 289 $this->server->createCollection($path, new MkCol($resourceType, $properties)); 290 291 $this->server->httpResponse->setStatus(201); 292 $this->server->httpResponse->setHeader('Content-Length', 0); 293 294 // This breaks the method chain. 295 return false; 296 } 297 298 /** 299 * PropFind 300 * 301 * This method handler is invoked before any after properties for a 302 * resource are fetched. This allows us to add in any CalDAV specific 303 * properties. 304 * 305 * @param DAV\PropFind $propFind 306 * @param DAV\INode $node 307 * @return void 308 */ 309 function propFind(DAV\PropFind $propFind, DAV\INode $node) { 310 311 $ns = '{' . self::NS_CALDAV . '}'; 312 313 if ($node instanceof ICalendarObjectContainer) { 314 315 $propFind->handle($ns . 'max-resource-size', $this->maxResourceSize); 316 $propFind->handle($ns . 'supported-calendar-data', function() { 317 return new Xml\Property\SupportedCalendarData(); 318 }); 319 $propFind->handle($ns . 'supported-collation-set', function() { 320 return new Xml\Property\SupportedCollationSet(); 321 }); 322 323 } 324 325 if ($node instanceof DAVACL\IPrincipal) { 326 327 $principalUrl = $node->getPrincipalUrl(); 328 329 $propFind->handle('{' . self::NS_CALDAV . '}calendar-home-set', function() use ($principalUrl) { 330 331 $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl) . '/'; 332 return new Href($calendarHomePath); 333 334 }); 335 // The calendar-user-address-set property is basically mapped to 336 // the {DAV:}alternate-URI-set property. 337 $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-address-set', function() use ($node) { 338 $addresses = $node->getAlternateUriSet(); 339 $addresses[] = $this->server->getBaseUri() . $node->getPrincipalUrl() . '/'; 340 return new Href($addresses, false); 341 }); 342 // For some reason somebody thought it was a good idea to add 343 // another one of these properties. We're supporting it too. 344 $propFind->handle('{' . self::NS_CALENDARSERVER . '}email-address-set', function() use ($node) { 345 $addresses = $node->getAlternateUriSet(); 346 $emails = []; 347 foreach ($addresses as $address) { 348 if (substr($address, 0, 7) === 'mailto:') { 349 $emails[] = substr($address, 7); 350 } 351 } 352 return new Xml\Property\EmailAddressSet($emails); 353 }); 354 355 // These two properties are shortcuts for ical to easily find 356 // other principals this principal has access to. 357 $propRead = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-read-for'; 358 $propWrite = '{' . self::NS_CALENDARSERVER . '}calendar-proxy-write-for'; 359 360 if ($propFind->getStatus($propRead) === 404 || $propFind->getStatus($propWrite) === 404) { 361 362 $aclPlugin = $this->server->getPlugin('acl'); 363 $membership = $aclPlugin->getPrincipalMembership($propFind->getPath()); 364 $readList = []; 365 $writeList = []; 366 367 foreach ($membership as $group) { 368 369 $groupNode = $this->server->tree->getNodeForPath($group); 370 371 $listItem = Uri\split($group)[0] . '/'; 372 373 // If the node is either ap proxy-read or proxy-write 374 // group, we grab the parent principal and add it to the 375 // list. 376 if ($groupNode instanceof Principal\IProxyRead) { 377 $readList[] = $listItem; 378 } 379 if ($groupNode instanceof Principal\IProxyWrite) { 380 $writeList[] = $listItem; 381 } 382 383 } 384 385 $propFind->set($propRead, new Href($readList)); 386 $propFind->set($propWrite, new Href($writeList)); 387 388 } 389 390 } // instanceof IPrincipal 391 392 if ($node instanceof ICalendarObject) { 393 394 // The calendar-data property is not supposed to be a 'real' 395 // property, but in large chunks of the spec it does act as such. 396 // Therefore we simply expose it as a property. 397 $propFind->handle('{' . self::NS_CALDAV . '}calendar-data', function() use ($node) { 398 $val = $node->get(); 399 if (is_resource($val)) 400 $val = stream_get_contents($val); 401 402 // Taking out \r to not screw up the xml output 403 return str_replace("\r", "", $val); 404 405 }); 406 407 } 408 409 } 410 411 /** 412 * This function handles the calendar-multiget REPORT. 413 * 414 * This report is used by the client to fetch the content of a series 415 * of urls. Effectively avoiding a lot of redundant requests. 416 * 417 * @param CalendarMultiGetReport $report 418 * @return void 419 */ 420 function calendarMultiGetReport($report) { 421 422 $needsJson = $report->contentType === 'application/calendar+json'; 423 424 $timeZones = []; 425 $propertyList = []; 426 427 $paths = array_map( 428 [$this->server, 'calculateUri'], 429 $report->hrefs 430 ); 431 432 foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) { 433 434 if (($needsJson || $report->expand) && isset($objProps[200]['{' . self::NS_CALDAV . '}calendar-data'])) { 435 $vObject = VObject\Reader::read($objProps[200]['{' . self::NS_CALDAV . '}calendar-data']); 436 437 if ($report->expand) { 438 // We're expanding, and for that we need to figure out the 439 // calendar's timezone. 440 list($calendarPath) = Uri\split($uri); 441 if (!isset($timeZones[$calendarPath])) { 442 // Checking the calendar-timezone property. 443 $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone'; 444 $tzResult = $this->server->getProperties($calendarPath, [$tzProp]); 445 if (isset($tzResult[$tzProp])) { 446 // This property contains a VCALENDAR with a single 447 // VTIMEZONE. 448 $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); 449 $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); 450 } else { 451 // Defaulting to UTC. 452 $timeZone = new DateTimeZone('UTC'); 453 } 454 $timeZones[$calendarPath] = $timeZone; 455 } 456 457 $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]); 458 } 459 if ($needsJson) { 460 $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize()); 461 } else { 462 $objProps[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); 463 } 464 } 465 466 $propertyList[] = $objProps; 467 468 } 469 470 $prefer = $this->server->getHTTPPrefer(); 471 472 $this->server->httpResponse->setStatus(207); 473 $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); 474 $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); 475 $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, $prefer['return'] === 'minimal')); 476 477 } 478 479 /** 480 * This function handles the calendar-query REPORT 481 * 482 * This report is used by clients to request calendar objects based on 483 * complex conditions. 484 * 485 * @param Xml\Request\CalendarQueryReport $report 486 * @return void 487 */ 488 function calendarQueryReport($report) { 489 490 $path = $this->server->getRequestUri(); 491 492 $needsJson = $report->contentType === 'application/calendar+json'; 493 494 $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); 495 $depth = $this->server->getHTTPDepth(0); 496 497 // The default result is an empty array 498 $result = []; 499 500 $calendarTimeZone = null; 501 if ($report->expand) { 502 // We're expanding, and for that we need to figure out the 503 // calendar's timezone. 504 $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone'; 505 $tzResult = $this->server->getProperties($path, [$tzProp]); 506 if (isset($tzResult[$tzProp])) { 507 // This property contains a VCALENDAR with a single 508 // VTIMEZONE. 509 $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); 510 $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); 511 unset($vtimezoneObj); 512 } else { 513 // Defaulting to UTC. 514 $calendarTimeZone = new DateTimeZone('UTC'); 515 } 516 } 517 518 // The calendarobject was requested directly. In this case we handle 519 // this locally. 520 if ($depth == 0 && $node instanceof ICalendarObject) { 521 522 $requestedCalendarData = true; 523 $requestedProperties = $report->properties; 524 525 if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) { 526 527 // We always retrieve calendar-data, as we need it for filtering. 528 $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data'; 529 530 // If calendar-data wasn't explicitly requested, we need to remove 531 // it after processing. 532 $requestedCalendarData = false; 533 } 534 535 $properties = $this->server->getPropertiesForPath( 536 $path, 537 $requestedProperties, 538 0 539 ); 540 541 // This array should have only 1 element, the first calendar 542 // object. 543 $properties = current($properties); 544 545 // If there wasn't any calendar-data returned somehow, we ignore 546 // this. 547 if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) { 548 549 $validator = new CalendarQueryValidator(); 550 551 $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); 552 if ($validator->validate($vObject, $report->filters)) { 553 554 // If the client didn't require the calendar-data property, 555 // we won't give it back. 556 if (!$requestedCalendarData) { 557 unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); 558 } else { 559 560 561 if ($report->expand) { 562 $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); 563 } 564 if ($needsJson) { 565 $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize()); 566 } elseif ($report->expand) { 567 $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); 568 } 569 } 570 571 $result = [$properties]; 572 573 } 574 575 } 576 577 } 578 579 if ($node instanceof ICalendarObjectContainer && $depth === 0) { 580 581 if (strpos($this->server->httpRequest->getHeader('User-Agent'), 'MSFT-WP/') === 0) { 582 // Windows phone incorrectly supplied depth as 0, when it actually 583 // should have set depth to 1. We're implementing a workaround here 584 // to deal with this. 585 $depth = 1; 586 } else { 587 throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1'); 588 } 589 590 } 591 592 // If we're dealing with a calendar, the calendar itself is responsible 593 // for the calendar-query. 594 if ($node instanceof ICalendarObjectContainer && $depth == 1) { 595 596 $nodePaths = $node->calendarQuery($report->filters); 597 598 foreach ($nodePaths as $path) { 599 600 list($properties) = 601 $this->server->getPropertiesForPath($this->server->getRequestUri() . '/' . $path, $report->properties); 602 603 if (($needsJson || $report->expand)) { 604 $vObject = VObject\Reader::read($properties[200]['{' . self::NS_CALDAV . '}calendar-data']); 605 606 if ($report->expand) { 607 $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); 608 } 609 610 if ($needsJson) { 611 $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = json_encode($vObject->jsonSerialize()); 612 } else { 613 $properties[200]['{' . self::NS_CALDAV . '}calendar-data'] = $vObject->serialize(); 614 } 615 } 616 $result[] = $properties; 617 618 } 619 620 } 621 622 $prefer = $this->server->getHTTPPrefer(); 623 624 $this->server->httpResponse->setStatus(207); 625 $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); 626 $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); 627 $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, $prefer['return'] === 'minimal')); 628 629 } 630 631 /** 632 * This method is responsible for parsing the request and generating the 633 * response for the CALDAV:free-busy-query REPORT. 634 * 635 * @param Xml\Request\FreeBusyQueryReport $report 636 * @return void 637 */ 638 protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) { 639 640 $uri = $this->server->getRequestUri(); 641 642 $acl = $this->server->getPlugin('acl'); 643 if ($acl) { 644 $acl->checkPrivileges($uri, '{' . self::NS_CALDAV . '}read-free-busy'); 645 } 646 647 $calendar = $this->server->tree->getNodeForPath($uri); 648 if (!$calendar instanceof ICalendar) { 649 throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars'); 650 } 651 652 $tzProp = '{' . self::NS_CALDAV . '}calendar-timezone'; 653 654 // Figuring out the default timezone for the calendar, for floating 655 // times. 656 $calendarProps = $this->server->getProperties($uri, [$tzProp]); 657 658 if (isset($calendarProps[$tzProp])) { 659 $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]); 660 $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); 661 } else { 662 $calendarTimeZone = new DateTimeZone('UTC'); 663 } 664 665 // Doing a calendar-query first, to make sure we get the most 666 // performance. 667 $urls = $calendar->calendarQuery([ 668 'name' => 'VCALENDAR', 669 'comp-filters' => [ 670 [ 671 'name' => 'VEVENT', 672 'comp-filters' => [], 673 'prop-filters' => [], 674 'is-not-defined' => false, 675 'time-range' => [ 676 'start' => $report->start, 677 'end' => $report->end, 678 ], 679 ], 680 ], 681 'prop-filters' => [], 682 'is-not-defined' => false, 683 'time-range' => null, 684 ]); 685 686 $objects = array_map(function($url) use ($calendar) { 687 $obj = $calendar->getChild($url)->get(); 688 return $obj; 689 }, $urls); 690 691 $generator = new VObject\FreeBusyGenerator(); 692 $generator->setObjects($objects); 693 $generator->setTimeRange($report->start, $report->end); 694 $generator->setTimeZone($calendarTimeZone); 695 $result = $generator->getResult(); 696 $result = $result->serialize(); 697 698 $this->server->httpResponse->setStatus(200); 699 $this->server->httpResponse->setHeader('Content-Type', 'text/calendar'); 700 $this->server->httpResponse->setHeader('Content-Length', strlen($result)); 701 $this->server->httpResponse->setBody($result); 702 703 } 704 705 /** 706 * This method is triggered before a file gets updated with new content. 707 * 708 * This plugin uses this method to ensure that CalDAV objects receive 709 * valid calendar data. 710 * 711 * @param string $path 712 * @param DAV\IFile $node 713 * @param resource $data 714 * @param bool $modified Should be set to true, if this event handler 715 * changed &$data. 716 * @return void 717 */ 718 function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) { 719 720 if (!$node instanceof ICalendarObject) 721 return; 722 723 // We're onyl interested in ICalendarObject nodes that are inside of a 724 // real calendar. This is to avoid triggering validation and scheduling 725 // for non-calendars (such as an inbox). 726 list($parent) = Uri\split($path); 727 $parentNode = $this->server->tree->getNodeForPath($parent); 728 729 if (!$parentNode instanceof ICalendar) 730 return; 731 732 $this->validateICalendar( 733 $data, 734 $path, 735 $modified, 736 $this->server->httpRequest, 737 $this->server->httpResponse, 738 false 739 ); 740 741 } 742 743 /** 744 * This method is triggered before a new file is created. 745 * 746 * This plugin uses this method to ensure that newly created calendar 747 * objects contain valid calendar data. 748 * 749 * @param string $path 750 * @param resource $data 751 * @param DAV\ICollection $parentNode 752 * @param bool $modified Should be set to true, if this event handler 753 * changed &$data. 754 * @return void 755 */ 756 function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) { 757 758 if (!$parentNode instanceof ICalendar) 759 return; 760 761 $this->validateICalendar( 762 $data, 763 $path, 764 $modified, 765 $this->server->httpRequest, 766 $this->server->httpResponse, 767 true 768 ); 769 770 } 771 772 /** 773 * Checks if the submitted iCalendar data is in fact, valid. 774 * 775 * An exception is thrown if it's not. 776 * 777 * @param resource|string $data 778 * @param string $path 779 * @param bool $modified Should be set to true, if this event handler 780 * changed &$data. 781 * @param RequestInterface $request The http request. 782 * @param ResponseInterface $response The http response. 783 * @param bool $isNew Is the item a new one, or an update. 784 * @return void 785 */ 786 protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) { 787 788 // If it's a stream, we convert it to a string first. 789 if (is_resource($data)) { 790 $data = stream_get_contents($data); 791 } 792 793 $before = md5($data); 794 // Converting the data to unicode, if needed. 795 $data = DAV\StringUtil::ensureUTF8($data); 796 797 if ($before !== md5($data)) $modified = true; 798 799 try { 800 801 // If the data starts with a [, we can reasonably assume we're dealing 802 // with a jCal object. 803 if (substr($data, 0, 1) === '[') { 804 $vobj = VObject\Reader::readJson($data); 805 806 // Converting $data back to iCalendar, as that's what we 807 // technically support everywhere. 808 $data = $vobj->serialize(); 809 $modified = true; 810 } else { 811 $vobj = VObject\Reader::read($data); 812 } 813 814 } catch (VObject\ParseException $e) { 815 816 throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: ' . $e->getMessage()); 817 818 } 819 820 if ($vobj->name !== 'VCALENDAR') { 821 throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.'); 822 } 823 824 $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; 825 826 // Get the Supported Components for the target calendar 827 list($parentPath) = Uri\split($path); 828 $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]); 829 830 if (isset($calendarProperties[$sCCS])) { 831 $supportedComponents = $calendarProperties[$sCCS]->getValue(); 832 } else { 833 $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT']; 834 } 835 836 $foundType = null; 837 $foundUID = null; 838 foreach ($vobj->getComponents() as $component) { 839 switch ($component->name) { 840 case 'VTIMEZONE' : 841 continue 2; 842 case 'VEVENT' : 843 case 'VTODO' : 844 case 'VJOURNAL' : 845 if (is_null($foundType)) { 846 $foundType = $component->name; 847 if (!in_array($foundType, $supportedComponents)) { 848 throw new Exception\InvalidComponentType('This calendar only supports ' . implode(', ', $supportedComponents) . '. We found a ' . $foundType); 849 } 850 if (!isset($component->UID)) { 851 throw new DAV\Exception\BadRequest('Every ' . $component->name . ' component must have an UID'); 852 } 853 $foundUID = (string)$component->UID; 854 } else { 855 if ($foundType !== $component->name) { 856 throw new DAV\Exception\BadRequest('A calendar object must only contain 1 component. We found a ' . $component->name . ' as well as a ' . $foundType); 857 } 858 if ($foundUID !== (string)$component->UID) { 859 throw new DAV\Exception\BadRequest('Every ' . $component->name . ' in this object must have identical UIDs'); 860 } 861 } 862 break; 863 default : 864 throw new DAV\Exception\BadRequest('You are not allowed to create components of type: ' . $component->name . ' here'); 865 866 } 867 } 868 if (!$foundType) 869 throw new DAV\Exception\BadRequest('iCalendar object must contain at least 1 of VEVENT, VTODO or VJOURNAL'); 870 871 // We use an extra variable to allow event handles to tell us wether 872 // the object was modified or not. 873 // 874 // This helps us determine if we need to re-serialize the object. 875 $subModified = false; 876 877 $this->server->emit( 878 'calendarObjectChange', 879 [ 880 $request, 881 $response, 882 $vobj, 883 $parentPath, 884 &$subModified, 885 $isNew 886 ] 887 ); 888 889 if ($subModified) { 890 // An event handler told us that it modified the object. 891 $data = $vobj->serialize(); 892 893 // Using md5 to figure out if there was an *actual* change. 894 if (!$modified && $before !== md5($data)) { 895 $modified = true; 896 } 897 898 } 899 900 } 901 902 903 /** 904 * This method is used to generate HTML output for the 905 * DAV\Browser\Plugin. This allows us to generate an interface users 906 * can use to create new calendars. 907 * 908 * @param DAV\INode $node 909 * @param string $output 910 * @return bool 911 */ 912 function htmlActionsPanel(DAV\INode $node, &$output) { 913 914 if (!$node instanceof CalendarHome) 915 return; 916 917 $output .= '<tr><td colspan="2"><form method="post" action=""> 918 <h3>Create new calendar</h3> 919 <input type="hidden" name="sabreAction" value="mkcol" /> 920 <input type="hidden" name="resourceType" value="{DAV:}collection,{' . self::NS_CALDAV . '}calendar" /> 921 <label>Name (uri):</label> <input type="text" name="name" /><br /> 922 <label>Display name:</label> <input type="text" name="{DAV:}displayname" /><br /> 923 <input type="submit" value="create" /> 924 </form> 925 </td></tr>'; 926 927 return false; 928 929 } 930 931 /** 932 * This event is triggered after GET requests. 933 * 934 * This is used to transform data into jCal, if this was requested. 935 * 936 * @param RequestInterface $request 937 * @param ResponseInterface $response 938 * @return void 939 */ 940 function httpAfterGet(RequestInterface $request, ResponseInterface $response) { 941 942 if (strpos($response->getHeader('Content-Type'), 'text/calendar') === false) { 943 return; 944 } 945 946 $result = HTTP\Util::negotiate( 947 $request->getHeader('Accept'), 948 ['text/calendar', 'application/calendar+json'] 949 ); 950 951 if ($result !== 'application/calendar+json') { 952 // Do nothing 953 return; 954 } 955 956 // Transforming. 957 $vobj = VObject\Reader::read($response->getBody()); 958 959 $jsonBody = json_encode($vobj->jsonSerialize()); 960 $response->setBody($jsonBody); 961 962 $response->setHeader('Content-Type', 'application/calendar+json'); 963 $response->setHeader('Content-Length', strlen($jsonBody)); 964 965 } 966 967 /** 968 * Returns a bunch of meta-data about the plugin. 969 * 970 * Providing this information is optional, and is mainly displayed by the 971 * Browser plugin. 972 * 973 * The description key in the returned array may contain html and will not 974 * be sanitized. 975 * 976 * @return array 977 */ 978 function getPluginInfo() { 979 980 return [ 981 'name' => $this->getPluginName(), 982 'description' => 'Adds support for CalDAV (rfc4791)', 983 'link' => 'http://sabre.io/dav/caldav/', 984 ]; 985 986 } 987 988} 989