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