1*a1a3b679SAndreas Boehler<?php 2*a1a3b679SAndreas Boehler 3*a1a3b679SAndreas Boehlernamespace Sabre\CalDAV\Schedule; 4*a1a3b679SAndreas Boehler 5*a1a3b679SAndreas Boehleruse DateTimeZone; 6*a1a3b679SAndreas Boehleruse Sabre\DAV\Server; 7*a1a3b679SAndreas Boehleruse Sabre\DAV\ServerPlugin; 8*a1a3b679SAndreas Boehleruse Sabre\DAV\PropFind; 9*a1a3b679SAndreas Boehleruse Sabre\DAV\INode; 10*a1a3b679SAndreas Boehleruse Sabre\DAV\Xml\Property\Href; 11*a1a3b679SAndreas Boehleruse Sabre\HTTP\RequestInterface; 12*a1a3b679SAndreas Boehleruse Sabre\HTTP\ResponseInterface; 13*a1a3b679SAndreas Boehleruse Sabre\VObject; 14*a1a3b679SAndreas Boehleruse Sabre\VObject\Reader; 15*a1a3b679SAndreas Boehleruse Sabre\VObject\Component\VCalendar; 16*a1a3b679SAndreas Boehleruse Sabre\VObject\ITip; 17*a1a3b679SAndreas Boehleruse Sabre\VObject\ITip\Message; 18*a1a3b679SAndreas Boehleruse Sabre\DAVACL; 19*a1a3b679SAndreas Boehleruse Sabre\CalDAV\ICalendar; 20*a1a3b679SAndreas Boehleruse Sabre\CalDAV\ICalendarObject; 21*a1a3b679SAndreas Boehleruse Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; 22*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\NotFound; 23*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\Forbidden; 24*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\BadRequest; 25*a1a3b679SAndreas Boehleruse Sabre\DAV\Exception\NotImplemented; 26*a1a3b679SAndreas Boehler 27*a1a3b679SAndreas Boehler/** 28*a1a3b679SAndreas Boehler * CalDAV scheduling plugin. 29*a1a3b679SAndreas Boehler * ========================= 30*a1a3b679SAndreas Boehler * 31*a1a3b679SAndreas Boehler * This plugin provides the functionality added by the "Scheduling Extensions 32*a1a3b679SAndreas Boehler * to CalDAV" standard, as defined in RFC6638. 33*a1a3b679SAndreas Boehler * 34*a1a3b679SAndreas Boehler * calendar-auto-schedule largely works by intercepting a users request to 35*a1a3b679SAndreas Boehler * update their local calendar. If a user creates a new event with attendees, 36*a1a3b679SAndreas Boehler * this plugin is supposed to grab the information from that event, and notify 37*a1a3b679SAndreas Boehler * the attendees of this. 38*a1a3b679SAndreas Boehler * 39*a1a3b679SAndreas Boehler * There's 3 possible transports for this: 40*a1a3b679SAndreas Boehler * * local delivery 41*a1a3b679SAndreas Boehler * * delivery through email (iMip) 42*a1a3b679SAndreas Boehler * * server-to-server delivery (iSchedule) 43*a1a3b679SAndreas Boehler * 44*a1a3b679SAndreas Boehler * iMip is simply, because we just need to add the iTip message as an email 45*a1a3b679SAndreas Boehler * attachment. Local delivery is harder, because we both need to add this same 46*a1a3b679SAndreas Boehler * message to a local DAV inbox, as well as live-update the relevant events. 47*a1a3b679SAndreas Boehler * 48*a1a3b679SAndreas Boehler * iSchedule is something for later. 49*a1a3b679SAndreas Boehler * 50*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). 51*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/) 52*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License 53*a1a3b679SAndreas Boehler */ 54*a1a3b679SAndreas Boehlerclass Plugin extends ServerPlugin { 55*a1a3b679SAndreas Boehler 56*a1a3b679SAndreas Boehler /** 57*a1a3b679SAndreas Boehler * This is the official CalDAV namespace 58*a1a3b679SAndreas Boehler */ 59*a1a3b679SAndreas Boehler const NS_CALDAV = 'urn:ietf:params:xml:ns:caldav'; 60*a1a3b679SAndreas Boehler 61*a1a3b679SAndreas Boehler /** 62*a1a3b679SAndreas Boehler * Reference to main Server object. 63*a1a3b679SAndreas Boehler * 64*a1a3b679SAndreas Boehler * @var Server 65*a1a3b679SAndreas Boehler */ 66*a1a3b679SAndreas Boehler protected $server; 67*a1a3b679SAndreas Boehler 68*a1a3b679SAndreas Boehler /** 69*a1a3b679SAndreas Boehler * Returns a list of features for the DAV: HTTP header. 70*a1a3b679SAndreas Boehler * 71*a1a3b679SAndreas Boehler * @return array 72*a1a3b679SAndreas Boehler */ 73*a1a3b679SAndreas Boehler function getFeatures() { 74*a1a3b679SAndreas Boehler 75*a1a3b679SAndreas Boehler return ['calendar-auto-schedule']; 76*a1a3b679SAndreas Boehler 77*a1a3b679SAndreas Boehler } 78*a1a3b679SAndreas Boehler 79*a1a3b679SAndreas Boehler /** 80*a1a3b679SAndreas Boehler * Returns the name of the plugin. 81*a1a3b679SAndreas Boehler * 82*a1a3b679SAndreas Boehler * Using this name other plugins will be able to access other plugins 83*a1a3b679SAndreas Boehler * using Server::getPlugin 84*a1a3b679SAndreas Boehler * 85*a1a3b679SAndreas Boehler * @return string 86*a1a3b679SAndreas Boehler */ 87*a1a3b679SAndreas Boehler function getPluginName() { 88*a1a3b679SAndreas Boehler 89*a1a3b679SAndreas Boehler return 'caldav-schedule'; 90*a1a3b679SAndreas Boehler 91*a1a3b679SAndreas Boehler } 92*a1a3b679SAndreas Boehler 93*a1a3b679SAndreas Boehler /** 94*a1a3b679SAndreas Boehler * Initializes the plugin 95*a1a3b679SAndreas Boehler * 96*a1a3b679SAndreas Boehler * @param Server $server 97*a1a3b679SAndreas Boehler * @return void 98*a1a3b679SAndreas Boehler */ 99*a1a3b679SAndreas Boehler function initialize(Server $server) { 100*a1a3b679SAndreas Boehler 101*a1a3b679SAndreas Boehler $this->server = $server; 102*a1a3b679SAndreas Boehler $server->on('method:POST', [$this, 'httpPost']); 103*a1a3b679SAndreas Boehler $server->on('propFind', [$this, 'propFind']); 104*a1a3b679SAndreas Boehler $server->on('calendarObjectChange', [$this, 'calendarObjectChange']); 105*a1a3b679SAndreas Boehler $server->on('beforeUnbind', [$this, 'beforeUnbind']); 106*a1a3b679SAndreas Boehler $server->on('schedule', [$this, 'scheduleLocalDelivery']); 107*a1a3b679SAndreas Boehler 108*a1a3b679SAndreas Boehler $ns = '{' . self::NS_CALDAV . '}'; 109*a1a3b679SAndreas Boehler 110*a1a3b679SAndreas Boehler /** 111*a1a3b679SAndreas Boehler * This information ensures that the {DAV:}resourcetype property has 112*a1a3b679SAndreas Boehler * the correct values. 113*a1a3b679SAndreas Boehler */ 114*a1a3b679SAndreas Boehler $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns . 'schedule-outbox'; 115*a1a3b679SAndreas Boehler $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns . 'schedule-inbox'; 116*a1a3b679SAndreas Boehler 117*a1a3b679SAndreas Boehler /** 118*a1a3b679SAndreas Boehler * Properties we protect are made read-only by the server. 119*a1a3b679SAndreas Boehler */ 120*a1a3b679SAndreas Boehler array_push($server->protectedProperties, 121*a1a3b679SAndreas Boehler $ns . 'schedule-inbox-URL', 122*a1a3b679SAndreas Boehler $ns . 'schedule-outbox-URL', 123*a1a3b679SAndreas Boehler $ns . 'calendar-user-address-set', 124*a1a3b679SAndreas Boehler $ns . 'calendar-user-type', 125*a1a3b679SAndreas Boehler $ns . 'schedule-default-calendar-URL' 126*a1a3b679SAndreas Boehler ); 127*a1a3b679SAndreas Boehler 128*a1a3b679SAndreas Boehler } 129*a1a3b679SAndreas Boehler 130*a1a3b679SAndreas Boehler /** 131*a1a3b679SAndreas Boehler * Use this method to tell the server this plugin defines additional 132*a1a3b679SAndreas Boehler * HTTP methods. 133*a1a3b679SAndreas Boehler * 134*a1a3b679SAndreas Boehler * This method is passed a uri. It should only return HTTP methods that are 135*a1a3b679SAndreas Boehler * available for the specified uri. 136*a1a3b679SAndreas Boehler * 137*a1a3b679SAndreas Boehler * @param string $uri 138*a1a3b679SAndreas Boehler * @return array 139*a1a3b679SAndreas Boehler */ 140*a1a3b679SAndreas Boehler function getHTTPMethods($uri) { 141*a1a3b679SAndreas Boehler 142*a1a3b679SAndreas Boehler try { 143*a1a3b679SAndreas Boehler $node = $this->server->tree->getNodeForPath($uri); 144*a1a3b679SAndreas Boehler } catch (NotFound $e) { 145*a1a3b679SAndreas Boehler return []; 146*a1a3b679SAndreas Boehler } 147*a1a3b679SAndreas Boehler 148*a1a3b679SAndreas Boehler if ($node instanceof IOutbox) { 149*a1a3b679SAndreas Boehler return ['POST']; 150*a1a3b679SAndreas Boehler } 151*a1a3b679SAndreas Boehler 152*a1a3b679SAndreas Boehler return []; 153*a1a3b679SAndreas Boehler 154*a1a3b679SAndreas Boehler } 155*a1a3b679SAndreas Boehler 156*a1a3b679SAndreas Boehler /** 157*a1a3b679SAndreas Boehler * This method handles POST request for the outbox. 158*a1a3b679SAndreas Boehler * 159*a1a3b679SAndreas Boehler * @param RequestInterface $request 160*a1a3b679SAndreas Boehler * @param ResponseInterface $response 161*a1a3b679SAndreas Boehler * @return bool 162*a1a3b679SAndreas Boehler */ 163*a1a3b679SAndreas Boehler function httpPost(RequestInterface $request, ResponseInterface $response) { 164*a1a3b679SAndreas Boehler 165*a1a3b679SAndreas Boehler // Checking if this is a text/calendar content type 166*a1a3b679SAndreas Boehler $contentType = $request->getHeader('Content-Type'); 167*a1a3b679SAndreas Boehler if (strpos($contentType, 'text/calendar') !== 0) { 168*a1a3b679SAndreas Boehler return; 169*a1a3b679SAndreas Boehler } 170*a1a3b679SAndreas Boehler 171*a1a3b679SAndreas Boehler $path = $request->getPath(); 172*a1a3b679SAndreas Boehler 173*a1a3b679SAndreas Boehler // Checking if we're talking to an outbox 174*a1a3b679SAndreas Boehler try { 175*a1a3b679SAndreas Boehler $node = $this->server->tree->getNodeForPath($path); 176*a1a3b679SAndreas Boehler } catch (NotFound $e) { 177*a1a3b679SAndreas Boehler return; 178*a1a3b679SAndreas Boehler } 179*a1a3b679SAndreas Boehler if (!$node instanceof IOutbox) 180*a1a3b679SAndreas Boehler return; 181*a1a3b679SAndreas Boehler 182*a1a3b679SAndreas Boehler $this->server->transactionType = 'post-caldav-outbox'; 183*a1a3b679SAndreas Boehler $this->outboxRequest($node, $request, $response); 184*a1a3b679SAndreas Boehler 185*a1a3b679SAndreas Boehler // Returning false breaks the event chain and tells the server we've 186*a1a3b679SAndreas Boehler // handled the request. 187*a1a3b679SAndreas Boehler return false; 188*a1a3b679SAndreas Boehler 189*a1a3b679SAndreas Boehler } 190*a1a3b679SAndreas Boehler 191*a1a3b679SAndreas Boehler /** 192*a1a3b679SAndreas Boehler * This method handler is invoked during fetching of properties. 193*a1a3b679SAndreas Boehler * 194*a1a3b679SAndreas Boehler * We use this event to add calendar-auto-schedule-specific properties. 195*a1a3b679SAndreas Boehler * 196*a1a3b679SAndreas Boehler * @param PropFind $propFind 197*a1a3b679SAndreas Boehler * @param INode $node 198*a1a3b679SAndreas Boehler * @return void 199*a1a3b679SAndreas Boehler */ 200*a1a3b679SAndreas Boehler function propFind(PropFind $propFind, INode $node) { 201*a1a3b679SAndreas Boehler 202*a1a3b679SAndreas Boehler if (!$node instanceof DAVACL\IPrincipal) return; 203*a1a3b679SAndreas Boehler 204*a1a3b679SAndreas Boehler $caldavPlugin = $this->server->getPlugin('caldav'); 205*a1a3b679SAndreas Boehler $principalUrl = $node->getPrincipalUrl(); 206*a1a3b679SAndreas Boehler 207*a1a3b679SAndreas Boehler // schedule-outbox-URL property 208*a1a3b679SAndreas Boehler $propFind->handle('{' . self::NS_CALDAV . '}schedule-outbox-URL', function() use ($principalUrl, $caldavPlugin) { 209*a1a3b679SAndreas Boehler 210*a1a3b679SAndreas Boehler $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); 211*a1a3b679SAndreas Boehler $outboxPath = $calendarHomePath . '/outbox/'; 212*a1a3b679SAndreas Boehler 213*a1a3b679SAndreas Boehler return new Href($outboxPath); 214*a1a3b679SAndreas Boehler 215*a1a3b679SAndreas Boehler }); 216*a1a3b679SAndreas Boehler // schedule-inbox-URL property 217*a1a3b679SAndreas Boehler $propFind->handle('{' . self::NS_CALDAV . '}schedule-inbox-URL', function() use ($principalUrl, $caldavPlugin) { 218*a1a3b679SAndreas Boehler 219*a1a3b679SAndreas Boehler $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); 220*a1a3b679SAndreas Boehler $inboxPath = $calendarHomePath . '/inbox/'; 221*a1a3b679SAndreas Boehler 222*a1a3b679SAndreas Boehler return new Href($inboxPath); 223*a1a3b679SAndreas Boehler 224*a1a3b679SAndreas Boehler }); 225*a1a3b679SAndreas Boehler 226*a1a3b679SAndreas Boehler $propFind->handle('{' . self::NS_CALDAV . '}schedule-default-calendar-URL', function() use ($principalUrl, $caldavPlugin) { 227*a1a3b679SAndreas Boehler 228*a1a3b679SAndreas Boehler // We don't support customizing this property yet, so in the 229*a1a3b679SAndreas Boehler // meantime we just grab the first calendar in the home-set. 230*a1a3b679SAndreas Boehler $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); 231*a1a3b679SAndreas Boehler 232*a1a3b679SAndreas Boehler $sccs = '{' . self::NS_CALDAV . '}supported-calendar-component-set'; 233*a1a3b679SAndreas Boehler 234*a1a3b679SAndreas Boehler $result = $this->server->getPropertiesForPath($calendarHomePath, [ 235*a1a3b679SAndreas Boehler '{DAV:}resourcetype', 236*a1a3b679SAndreas Boehler $sccs, 237*a1a3b679SAndreas Boehler ], 1); 238*a1a3b679SAndreas Boehler 239*a1a3b679SAndreas Boehler foreach ($result as $child) { 240*a1a3b679SAndreas Boehler if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{' . self::NS_CALDAV . '}calendar') || $child[200]['{DAV:}resourcetype']->is('{http://calendarserver.org/ns/}shared')) { 241*a1a3b679SAndreas Boehler // Node is either not a calendar or a shared instance. 242*a1a3b679SAndreas Boehler continue; 243*a1a3b679SAndreas Boehler } 244*a1a3b679SAndreas Boehler if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) { 245*a1a3b679SAndreas Boehler // Either there is no supported-calendar-component-set 246*a1a3b679SAndreas Boehler // (which is fine) or we found one that supports VEVENT. 247*a1a3b679SAndreas Boehler return new Href($child['href']); 248*a1a3b679SAndreas Boehler } 249*a1a3b679SAndreas Boehler } 250*a1a3b679SAndreas Boehler 251*a1a3b679SAndreas Boehler }); 252*a1a3b679SAndreas Boehler 253*a1a3b679SAndreas Boehler // The server currently reports every principal to be of type 254*a1a3b679SAndreas Boehler // 'INDIVIDUAL' 255*a1a3b679SAndreas Boehler $propFind->handle('{' . self::NS_CALDAV . '}calendar-user-type', function() { 256*a1a3b679SAndreas Boehler 257*a1a3b679SAndreas Boehler return 'INDIVIDUAL'; 258*a1a3b679SAndreas Boehler 259*a1a3b679SAndreas Boehler }); 260*a1a3b679SAndreas Boehler 261*a1a3b679SAndreas Boehler } 262*a1a3b679SAndreas Boehler 263*a1a3b679SAndreas Boehler /** 264*a1a3b679SAndreas Boehler * This method is triggered whenever there was a calendar object gets 265*a1a3b679SAndreas Boehler * created or updated. 266*a1a3b679SAndreas Boehler * 267*a1a3b679SAndreas Boehler * @param RequestInterface $request HTTP request 268*a1a3b679SAndreas Boehler * @param ResponseInterface $response HTTP Response 269*a1a3b679SAndreas Boehler * @param VCalendar $vCal Parsed iCalendar object 270*a1a3b679SAndreas Boehler * @param mixed $calendarPath Path to calendar collection 271*a1a3b679SAndreas Boehler * @param mixed $modified The iCalendar object has been touched. 272*a1a3b679SAndreas Boehler * @param mixed $isNew Whether this was a new item or we're updating one 273*a1a3b679SAndreas Boehler * @return void 274*a1a3b679SAndreas Boehler */ 275*a1a3b679SAndreas Boehler function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) { 276*a1a3b679SAndreas Boehler 277*a1a3b679SAndreas Boehler if (!$this->scheduleReply($this->server->httpRequest)) { 278*a1a3b679SAndreas Boehler return; 279*a1a3b679SAndreas Boehler } 280*a1a3b679SAndreas Boehler 281*a1a3b679SAndreas Boehler $calendarNode = $this->server->tree->getNodeForPath($calendarPath); 282*a1a3b679SAndreas Boehler 283*a1a3b679SAndreas Boehler $addresses = $this->getAddressesForPrincipal( 284*a1a3b679SAndreas Boehler $calendarNode->getOwner() 285*a1a3b679SAndreas Boehler ); 286*a1a3b679SAndreas Boehler 287*a1a3b679SAndreas Boehler if (!$isNew) { 288*a1a3b679SAndreas Boehler $node = $this->server->tree->getNodeForPath($request->getPath()); 289*a1a3b679SAndreas Boehler $oldObj = Reader::read($node->get()); 290*a1a3b679SAndreas Boehler } else { 291*a1a3b679SAndreas Boehler $oldObj = null; 292*a1a3b679SAndreas Boehler } 293*a1a3b679SAndreas Boehler 294*a1a3b679SAndreas Boehler $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified); 295*a1a3b679SAndreas Boehler 296*a1a3b679SAndreas Boehler } 297*a1a3b679SAndreas Boehler 298*a1a3b679SAndreas Boehler /** 299*a1a3b679SAndreas Boehler * This method is responsible for delivering the ITip message. 300*a1a3b679SAndreas Boehler * 301*a1a3b679SAndreas Boehler * @param ITip\Message $itipMessage 302*a1a3b679SAndreas Boehler * @return void 303*a1a3b679SAndreas Boehler */ 304*a1a3b679SAndreas Boehler function deliver(ITip\Message $iTipMessage) { 305*a1a3b679SAndreas Boehler 306*a1a3b679SAndreas Boehler $this->server->emit('schedule', [$iTipMessage]); 307*a1a3b679SAndreas Boehler if (!$iTipMessage->scheduleStatus) { 308*a1a3b679SAndreas Boehler $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message'; 309*a1a3b679SAndreas Boehler } 310*a1a3b679SAndreas Boehler // In case the change was considered 'insignificant', we are going to 311*a1a3b679SAndreas Boehler // remove any error statuses, if any. See ticket #525. 312*a1a3b679SAndreas Boehler list($baseCode) = explode('.', $iTipMessage->scheduleStatus); 313*a1a3b679SAndreas Boehler if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) { 314*a1a3b679SAndreas Boehler $iTipMessage->scheduleStatus = null; 315*a1a3b679SAndreas Boehler } 316*a1a3b679SAndreas Boehler 317*a1a3b679SAndreas Boehler } 318*a1a3b679SAndreas Boehler 319*a1a3b679SAndreas Boehler /** 320*a1a3b679SAndreas Boehler * This method is triggered before a file gets deleted. 321*a1a3b679SAndreas Boehler * 322*a1a3b679SAndreas Boehler * We use this event to make sure that when this happens, attendees get 323*a1a3b679SAndreas Boehler * cancellations, and organizers get 'DECLINED' statuses. 324*a1a3b679SAndreas Boehler * 325*a1a3b679SAndreas Boehler * @param string $path 326*a1a3b679SAndreas Boehler * @return void 327*a1a3b679SAndreas Boehler */ 328*a1a3b679SAndreas Boehler function beforeUnbind($path) { 329*a1a3b679SAndreas Boehler 330*a1a3b679SAndreas Boehler // FIXME: We shouldn't trigger this functionality when we're issuing a 331*a1a3b679SAndreas Boehler // MOVE. This is a hack. 332*a1a3b679SAndreas Boehler if ($this->server->httpRequest->getMethod() === 'MOVE') return; 333*a1a3b679SAndreas Boehler 334*a1a3b679SAndreas Boehler $node = $this->server->tree->getNodeForPath($path); 335*a1a3b679SAndreas Boehler 336*a1a3b679SAndreas Boehler if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) { 337*a1a3b679SAndreas Boehler return; 338*a1a3b679SAndreas Boehler } 339*a1a3b679SAndreas Boehler 340*a1a3b679SAndreas Boehler if (!$this->scheduleReply($this->server->httpRequest)) { 341*a1a3b679SAndreas Boehler return; 342*a1a3b679SAndreas Boehler } 343*a1a3b679SAndreas Boehler 344*a1a3b679SAndreas Boehler $addresses = $this->getAddressesForPrincipal( 345*a1a3b679SAndreas Boehler $node->getOwner() 346*a1a3b679SAndreas Boehler ); 347*a1a3b679SAndreas Boehler 348*a1a3b679SAndreas Boehler $broker = new ITip\Broker(); 349*a1a3b679SAndreas Boehler $messages = $broker->parseEvent(null, $addresses, $node->get()); 350*a1a3b679SAndreas Boehler 351*a1a3b679SAndreas Boehler foreach ($messages as $message) { 352*a1a3b679SAndreas Boehler $this->deliver($message); 353*a1a3b679SAndreas Boehler } 354*a1a3b679SAndreas Boehler 355*a1a3b679SAndreas Boehler } 356*a1a3b679SAndreas Boehler 357*a1a3b679SAndreas Boehler /** 358*a1a3b679SAndreas Boehler * Event handler for the 'schedule' event. 359*a1a3b679SAndreas Boehler * 360*a1a3b679SAndreas Boehler * This handler attempts to look at local accounts to deliver the 361*a1a3b679SAndreas Boehler * scheduling object. 362*a1a3b679SAndreas Boehler * 363*a1a3b679SAndreas Boehler * @param ITip\Message $iTipMessage 364*a1a3b679SAndreas Boehler * @return void 365*a1a3b679SAndreas Boehler */ 366*a1a3b679SAndreas Boehler function scheduleLocalDelivery(ITip\Message $iTipMessage) { 367*a1a3b679SAndreas Boehler 368*a1a3b679SAndreas Boehler $aclPlugin = $this->server->getPlugin('acl'); 369*a1a3b679SAndreas Boehler 370*a1a3b679SAndreas Boehler // Local delivery is not available if the ACL plugin is not loaded. 371*a1a3b679SAndreas Boehler if (!$aclPlugin) { 372*a1a3b679SAndreas Boehler return; 373*a1a3b679SAndreas Boehler } 374*a1a3b679SAndreas Boehler 375*a1a3b679SAndreas Boehler $caldavNS = '{' . self::NS_CALDAV . '}'; 376*a1a3b679SAndreas Boehler 377*a1a3b679SAndreas Boehler $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient); 378*a1a3b679SAndreas Boehler if (!$principalUri) { 379*a1a3b679SAndreas Boehler $iTipMessage->scheduleStatus = '3.7;Could not find principal.'; 380*a1a3b679SAndreas Boehler return; 381*a1a3b679SAndreas Boehler } 382*a1a3b679SAndreas Boehler 383*a1a3b679SAndreas Boehler // We found a principal URL, now we need to find its inbox. 384*a1a3b679SAndreas Boehler // Unfortunately we may not have sufficient privileges to find this, so 385*a1a3b679SAndreas Boehler // we are temporarily turning off ACL to let this come through. 386*a1a3b679SAndreas Boehler // 387*a1a3b679SAndreas Boehler // Once we support PHP 5.5, this should be wrapped in a try..finally 388*a1a3b679SAndreas Boehler // block so we can ensure that this privilege gets added again after. 389*a1a3b679SAndreas Boehler $this->server->removeListener('propFind', [$aclPlugin, 'propFind']); 390*a1a3b679SAndreas Boehler 391*a1a3b679SAndreas Boehler $result = $this->server->getProperties( 392*a1a3b679SAndreas Boehler $principalUri, 393*a1a3b679SAndreas Boehler [ 394*a1a3b679SAndreas Boehler '{DAV:}principal-URL', 395*a1a3b679SAndreas Boehler $caldavNS . 'calendar-home-set', 396*a1a3b679SAndreas Boehler $caldavNS . 'schedule-inbox-URL', 397*a1a3b679SAndreas Boehler $caldavNS . 'schedule-default-calendar-URL', 398*a1a3b679SAndreas Boehler '{http://sabredav.org/ns}email-address', 399*a1a3b679SAndreas Boehler ] 400*a1a3b679SAndreas Boehler ); 401*a1a3b679SAndreas Boehler 402*a1a3b679SAndreas Boehler // Re-registering the ACL event 403*a1a3b679SAndreas Boehler $this->server->on('propFind', [$aclPlugin, 'propFind'], 20); 404*a1a3b679SAndreas Boehler 405*a1a3b679SAndreas Boehler if (!isset($result[$caldavNS . 'schedule-inbox-URL'])) { 406*a1a3b679SAndreas Boehler $iTipMessage->scheduleStatus = '5.2;Could not find local inbox'; 407*a1a3b679SAndreas Boehler return; 408*a1a3b679SAndreas Boehler } 409*a1a3b679SAndreas Boehler if (!isset($result[$caldavNS . 'calendar-home-set'])) { 410*a1a3b679SAndreas Boehler $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set'; 411*a1a3b679SAndreas Boehler return; 412*a1a3b679SAndreas Boehler } 413*a1a3b679SAndreas Boehler if (!isset($result[$caldavNS . 'schedule-default-calendar-URL'])) { 414*a1a3b679SAndreas Boehler $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property'; 415*a1a3b679SAndreas Boehler return; 416*a1a3b679SAndreas Boehler } 417*a1a3b679SAndreas Boehler 418*a1a3b679SAndreas Boehler $calendarPath = $result[$caldavNS . 'schedule-default-calendar-URL']->getHref(); 419*a1a3b679SAndreas Boehler $homePath = $result[$caldavNS . 'calendar-home-set']->getHref(); 420*a1a3b679SAndreas Boehler $inboxPath = $result[$caldavNS . 'schedule-inbox-URL']->getHref(); 421*a1a3b679SAndreas Boehler 422*a1a3b679SAndreas Boehler if ($iTipMessage->method === 'REPLY') { 423*a1a3b679SAndreas Boehler $privilege = 'schedule-deliver-reply'; 424*a1a3b679SAndreas Boehler } else { 425*a1a3b679SAndreas Boehler $privilege = 'schedule-deliver-invite'; 426*a1a3b679SAndreas Boehler } 427*a1a3b679SAndreas Boehler 428*a1a3b679SAndreas Boehler if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS . $privilege, DAVACL\Plugin::R_PARENT, false)) { 429*a1a3b679SAndreas Boehler $iTipMessage->scheduleStatus = '3.8;organizer did not have the ' . $privilege . ' privilege on the attendees inbox'; 430*a1a3b679SAndreas Boehler return; 431*a1a3b679SAndreas Boehler } 432*a1a3b679SAndreas Boehler 433*a1a3b679SAndreas Boehler // Next, we're going to find out if the item already exits in one of 434*a1a3b679SAndreas Boehler // the users' calendars. 435*a1a3b679SAndreas Boehler $uid = $iTipMessage->uid; 436*a1a3b679SAndreas Boehler 437*a1a3b679SAndreas Boehler $newFileName = 'sabredav-' . \Sabre\DAV\UUIDUtil::getUUID() . '.ics'; 438*a1a3b679SAndreas Boehler 439*a1a3b679SAndreas Boehler $home = $this->server->tree->getNodeForPath($homePath); 440*a1a3b679SAndreas Boehler $inbox = $this->server->tree->getNodeForPath($inboxPath); 441*a1a3b679SAndreas Boehler 442*a1a3b679SAndreas Boehler $currentObject = null; 443*a1a3b679SAndreas Boehler $objectNode = null; 444*a1a3b679SAndreas Boehler $isNewNode = false; 445*a1a3b679SAndreas Boehler 446*a1a3b679SAndreas Boehler $result = $home->getCalendarObjectByUID($uid); 447*a1a3b679SAndreas Boehler if ($result) { 448*a1a3b679SAndreas Boehler // There was an existing object, we need to update probably. 449*a1a3b679SAndreas Boehler $objectPath = $homePath . '/' . $result; 450*a1a3b679SAndreas Boehler $objectNode = $this->server->tree->getNodeForPath($objectPath); 451*a1a3b679SAndreas Boehler $oldICalendarData = $objectNode->get(); 452*a1a3b679SAndreas Boehler $currentObject = Reader::read($oldICalendarData); 453*a1a3b679SAndreas Boehler } else { 454*a1a3b679SAndreas Boehler $isNewNode = true; 455*a1a3b679SAndreas Boehler } 456*a1a3b679SAndreas Boehler 457*a1a3b679SAndreas Boehler $broker = new ITip\Broker(); 458*a1a3b679SAndreas Boehler $newObject = $broker->processMessage($iTipMessage, $currentObject); 459*a1a3b679SAndreas Boehler 460*a1a3b679SAndreas Boehler $inbox->createFile($newFileName, $iTipMessage->message->serialize()); 461*a1a3b679SAndreas Boehler 462*a1a3b679SAndreas Boehler if (!$newObject) { 463*a1a3b679SAndreas Boehler // We received an iTip message referring to a UID that we don't 464*a1a3b679SAndreas Boehler // have in any calendars yet, and processMessage did not give us a 465*a1a3b679SAndreas Boehler // calendarobject back. 466*a1a3b679SAndreas Boehler // 467*a1a3b679SAndreas Boehler // The implication is that processMessage did not understand the 468*a1a3b679SAndreas Boehler // iTip message. 469*a1a3b679SAndreas Boehler $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.'; 470*a1a3b679SAndreas Boehler return; 471*a1a3b679SAndreas Boehler } 472*a1a3b679SAndreas Boehler 473*a1a3b679SAndreas Boehler // Note that we are bypassing ACL on purpose by calling this directly. 474*a1a3b679SAndreas Boehler // We may need to look a bit deeper into this later. Supporting ACL 475*a1a3b679SAndreas Boehler // here would be nice. 476*a1a3b679SAndreas Boehler if ($isNewNode) { 477*a1a3b679SAndreas Boehler $calendar = $this->server->tree->getNodeForPath($calendarPath); 478*a1a3b679SAndreas Boehler $calendar->createFile($newFileName, $newObject->serialize()); 479*a1a3b679SAndreas Boehler } else { 480*a1a3b679SAndreas Boehler // If the message was a reply, we may have to inform other 481*a1a3b679SAndreas Boehler // attendees of this attendees status. Therefore we're shooting off 482*a1a3b679SAndreas Boehler // another itipMessage. 483*a1a3b679SAndreas Boehler if ($iTipMessage->method === 'REPLY') { 484*a1a3b679SAndreas Boehler $this->processICalendarChange( 485*a1a3b679SAndreas Boehler $oldICalendarData, 486*a1a3b679SAndreas Boehler $newObject, 487*a1a3b679SAndreas Boehler [$iTipMessage->recipient], 488*a1a3b679SAndreas Boehler [$iTipMessage->sender] 489*a1a3b679SAndreas Boehler ); 490*a1a3b679SAndreas Boehler } 491*a1a3b679SAndreas Boehler $objectNode->put($newObject->serialize()); 492*a1a3b679SAndreas Boehler } 493*a1a3b679SAndreas Boehler $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; 494*a1a3b679SAndreas Boehler 495*a1a3b679SAndreas Boehler } 496*a1a3b679SAndreas Boehler 497*a1a3b679SAndreas Boehler /** 498*a1a3b679SAndreas Boehler * This method looks at an old iCalendar object, a new iCalendar object and 499*a1a3b679SAndreas Boehler * starts sending scheduling messages based on the changes. 500*a1a3b679SAndreas Boehler * 501*a1a3b679SAndreas Boehler * A list of addresses needs to be specified, so the system knows who made 502*a1a3b679SAndreas Boehler * the update, because the behavior may be different based on if it's an 503*a1a3b679SAndreas Boehler * attendee or an organizer. 504*a1a3b679SAndreas Boehler * 505*a1a3b679SAndreas Boehler * This method may update $newObject to add any status changes. 506*a1a3b679SAndreas Boehler * 507*a1a3b679SAndreas Boehler * @param VCalendar|string $oldObject 508*a1a3b679SAndreas Boehler * @param VCalendar $newObject 509*a1a3b679SAndreas Boehler * @param array $addresses 510*a1a3b679SAndreas Boehler * @param array $ignore Any addresses to not send messages to. 511*a1a3b679SAndreas Boehler * @param bool $modified A marker to indicate that the original object 512*a1a3b679SAndreas Boehler * modified by this process. 513*a1a3b679SAndreas Boehler * @return void 514*a1a3b679SAndreas Boehler */ 515*a1a3b679SAndreas Boehler protected function processICalendarChange($oldObject = null, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) { 516*a1a3b679SAndreas Boehler 517*a1a3b679SAndreas Boehler $broker = new ITip\Broker(); 518*a1a3b679SAndreas Boehler $messages = $broker->parseEvent($newObject, $addresses, $oldObject); 519*a1a3b679SAndreas Boehler 520*a1a3b679SAndreas Boehler if ($messages) $modified = true; 521*a1a3b679SAndreas Boehler 522*a1a3b679SAndreas Boehler foreach ($messages as $message) { 523*a1a3b679SAndreas Boehler 524*a1a3b679SAndreas Boehler if (in_array($message->recipient, $ignore)) { 525*a1a3b679SAndreas Boehler continue; 526*a1a3b679SAndreas Boehler } 527*a1a3b679SAndreas Boehler 528*a1a3b679SAndreas Boehler $this->deliver($message); 529*a1a3b679SAndreas Boehler 530*a1a3b679SAndreas Boehler if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) { 531*a1a3b679SAndreas Boehler if ($message->scheduleStatus) { 532*a1a3b679SAndreas Boehler $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus(); 533*a1a3b679SAndreas Boehler } 534*a1a3b679SAndreas Boehler unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']); 535*a1a3b679SAndreas Boehler 536*a1a3b679SAndreas Boehler } else { 537*a1a3b679SAndreas Boehler 538*a1a3b679SAndreas Boehler if (isset($newObject->VEVENT->ATTENDEE)) foreach ($newObject->VEVENT->ATTENDEE as $attendee) { 539*a1a3b679SAndreas Boehler 540*a1a3b679SAndreas Boehler if ($attendee->getNormalizedValue() === $message->recipient) { 541*a1a3b679SAndreas Boehler if ($message->scheduleStatus) { 542*a1a3b679SAndreas Boehler $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus(); 543*a1a3b679SAndreas Boehler } 544*a1a3b679SAndreas Boehler unset($attendee['SCHEDULE-FORCE-SEND']); 545*a1a3b679SAndreas Boehler break; 546*a1a3b679SAndreas Boehler } 547*a1a3b679SAndreas Boehler 548*a1a3b679SAndreas Boehler } 549*a1a3b679SAndreas Boehler 550*a1a3b679SAndreas Boehler } 551*a1a3b679SAndreas Boehler 552*a1a3b679SAndreas Boehler } 553*a1a3b679SAndreas Boehler 554*a1a3b679SAndreas Boehler } 555*a1a3b679SAndreas Boehler 556*a1a3b679SAndreas Boehler /** 557*a1a3b679SAndreas Boehler * Returns a list of addresses that are associated with a principal. 558*a1a3b679SAndreas Boehler * 559*a1a3b679SAndreas Boehler * @param string $principal 560*a1a3b679SAndreas Boehler * @return array 561*a1a3b679SAndreas Boehler */ 562*a1a3b679SAndreas Boehler protected function getAddressesForPrincipal($principal) { 563*a1a3b679SAndreas Boehler 564*a1a3b679SAndreas Boehler $CUAS = '{' . self::NS_CALDAV . '}calendar-user-address-set'; 565*a1a3b679SAndreas Boehler 566*a1a3b679SAndreas Boehler $properties = $this->server->getProperties( 567*a1a3b679SAndreas Boehler $principal, 568*a1a3b679SAndreas Boehler [$CUAS] 569*a1a3b679SAndreas Boehler ); 570*a1a3b679SAndreas Boehler 571*a1a3b679SAndreas Boehler // If we can't find this information, we'll stop processing 572*a1a3b679SAndreas Boehler if (!isset($properties[$CUAS])) { 573*a1a3b679SAndreas Boehler return; 574*a1a3b679SAndreas Boehler } 575*a1a3b679SAndreas Boehler 576*a1a3b679SAndreas Boehler $addresses = $properties[$CUAS]->getHrefs(); 577*a1a3b679SAndreas Boehler return $addresses; 578*a1a3b679SAndreas Boehler 579*a1a3b679SAndreas Boehler } 580*a1a3b679SAndreas Boehler 581*a1a3b679SAndreas Boehler /** 582*a1a3b679SAndreas Boehler * This method handles POST requests to the schedule-outbox. 583*a1a3b679SAndreas Boehler * 584*a1a3b679SAndreas Boehler * Currently, two types of requests are support: 585*a1a3b679SAndreas Boehler * * FREEBUSY requests from RFC 6638 586*a1a3b679SAndreas Boehler * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04 587*a1a3b679SAndreas Boehler * 588*a1a3b679SAndreas Boehler * The latter is from an expired early draft of the CalDAV scheduling 589*a1a3b679SAndreas Boehler * extensions, but iCal depends on a feature from that spec, so we 590*a1a3b679SAndreas Boehler * implement it. 591*a1a3b679SAndreas Boehler * 592*a1a3b679SAndreas Boehler * @param IOutbox $outboxNode 593*a1a3b679SAndreas Boehler * @param RequestInterface $request 594*a1a3b679SAndreas Boehler * @param ResponseInterface $response 595*a1a3b679SAndreas Boehler * @return void 596*a1a3b679SAndreas Boehler */ 597*a1a3b679SAndreas Boehler function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response) { 598*a1a3b679SAndreas Boehler 599*a1a3b679SAndreas Boehler $outboxPath = $request->getPath(); 600*a1a3b679SAndreas Boehler 601*a1a3b679SAndreas Boehler // Parsing the request body 602*a1a3b679SAndreas Boehler try { 603*a1a3b679SAndreas Boehler $vObject = VObject\Reader::read($request->getBody()); 604*a1a3b679SAndreas Boehler } catch (VObject\ParseException $e) { 605*a1a3b679SAndreas Boehler throw new BadRequest('The request body must be a valid iCalendar object. Parse error: ' . $e->getMessage()); 606*a1a3b679SAndreas Boehler } 607*a1a3b679SAndreas Boehler 608*a1a3b679SAndreas Boehler // The incoming iCalendar object must have a METHOD property, and a 609*a1a3b679SAndreas Boehler // component. The combination of both determines what type of request 610*a1a3b679SAndreas Boehler // this is. 611*a1a3b679SAndreas Boehler $componentType = null; 612*a1a3b679SAndreas Boehler foreach ($vObject->getComponents() as $component) { 613*a1a3b679SAndreas Boehler if ($component->name !== 'VTIMEZONE') { 614*a1a3b679SAndreas Boehler $componentType = $component->name; 615*a1a3b679SAndreas Boehler break; 616*a1a3b679SAndreas Boehler } 617*a1a3b679SAndreas Boehler } 618*a1a3b679SAndreas Boehler if (is_null($componentType)) { 619*a1a3b679SAndreas Boehler throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component'); 620*a1a3b679SAndreas Boehler } 621*a1a3b679SAndreas Boehler 622*a1a3b679SAndreas Boehler // Validating the METHOD 623*a1a3b679SAndreas Boehler $method = strtoupper((string)$vObject->METHOD); 624*a1a3b679SAndreas Boehler if (!$method) { 625*a1a3b679SAndreas Boehler throw new BadRequest('A METHOD property must be specified in iTIP messages'); 626*a1a3b679SAndreas Boehler } 627*a1a3b679SAndreas Boehler 628*a1a3b679SAndreas Boehler // So we support one type of request: 629*a1a3b679SAndreas Boehler // 630*a1a3b679SAndreas Boehler // REQUEST with a VFREEBUSY component 631*a1a3b679SAndreas Boehler 632*a1a3b679SAndreas Boehler $acl = $this->server->getPlugin('acl'); 633*a1a3b679SAndreas Boehler 634*a1a3b679SAndreas Boehler if ($componentType === 'VFREEBUSY' && $method === 'REQUEST') { 635*a1a3b679SAndreas Boehler 636*a1a3b679SAndreas Boehler $acl && $acl->checkPrivileges($outboxPath, '{' . self::NS_CALDAV . '}schedule-query-freebusy'); 637*a1a3b679SAndreas Boehler $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response); 638*a1a3b679SAndreas Boehler 639*a1a3b679SAndreas Boehler } else { 640*a1a3b679SAndreas Boehler 641*a1a3b679SAndreas Boehler throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint'); 642*a1a3b679SAndreas Boehler 643*a1a3b679SAndreas Boehler } 644*a1a3b679SAndreas Boehler 645*a1a3b679SAndreas Boehler } 646*a1a3b679SAndreas Boehler 647*a1a3b679SAndreas Boehler /** 648*a1a3b679SAndreas Boehler * This method is responsible for parsing a free-busy query request and 649*a1a3b679SAndreas Boehler * returning it's result. 650*a1a3b679SAndreas Boehler * 651*a1a3b679SAndreas Boehler * @param IOutbox $outbox 652*a1a3b679SAndreas Boehler * @param VObject\Component $vObject 653*a1a3b679SAndreas Boehler * @param RequestInterface $request 654*a1a3b679SAndreas Boehler * @param ResponseInterface $response 655*a1a3b679SAndreas Boehler * @return string 656*a1a3b679SAndreas Boehler */ 657*a1a3b679SAndreas Boehler protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) { 658*a1a3b679SAndreas Boehler 659*a1a3b679SAndreas Boehler $vFreeBusy = $vObject->VFREEBUSY; 660*a1a3b679SAndreas Boehler $organizer = $vFreeBusy->organizer; 661*a1a3b679SAndreas Boehler 662*a1a3b679SAndreas Boehler $organizer = (string)$organizer; 663*a1a3b679SAndreas Boehler 664*a1a3b679SAndreas Boehler // Validating if the organizer matches the owner of the inbox. 665*a1a3b679SAndreas Boehler $owner = $outbox->getOwner(); 666*a1a3b679SAndreas Boehler 667*a1a3b679SAndreas Boehler $caldavNS = '{' . self::NS_CALDAV . '}'; 668*a1a3b679SAndreas Boehler 669*a1a3b679SAndreas Boehler $uas = $caldavNS . 'calendar-user-address-set'; 670*a1a3b679SAndreas Boehler $props = $this->server->getProperties($owner, [$uas]); 671*a1a3b679SAndreas Boehler 672*a1a3b679SAndreas Boehler if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) { 673*a1a3b679SAndreas Boehler throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox'); 674*a1a3b679SAndreas Boehler } 675*a1a3b679SAndreas Boehler 676*a1a3b679SAndreas Boehler if (!isset($vFreeBusy->ATTENDEE)) { 677*a1a3b679SAndreas Boehler throw new BadRequest('You must at least specify 1 attendee'); 678*a1a3b679SAndreas Boehler } 679*a1a3b679SAndreas Boehler 680*a1a3b679SAndreas Boehler $attendees = []; 681*a1a3b679SAndreas Boehler foreach ($vFreeBusy->ATTENDEE as $attendee) { 682*a1a3b679SAndreas Boehler $attendees[] = (string)$attendee; 683*a1a3b679SAndreas Boehler } 684*a1a3b679SAndreas Boehler 685*a1a3b679SAndreas Boehler 686*a1a3b679SAndreas Boehler if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) { 687*a1a3b679SAndreas Boehler throw new BadRequest('DTSTART and DTEND must both be specified'); 688*a1a3b679SAndreas Boehler } 689*a1a3b679SAndreas Boehler 690*a1a3b679SAndreas Boehler $startRange = $vFreeBusy->DTSTART->getDateTime(); 691*a1a3b679SAndreas Boehler $endRange = $vFreeBusy->DTEND->getDateTime(); 692*a1a3b679SAndreas Boehler 693*a1a3b679SAndreas Boehler $results = []; 694*a1a3b679SAndreas Boehler foreach ($attendees as $attendee) { 695*a1a3b679SAndreas Boehler $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject); 696*a1a3b679SAndreas Boehler } 697*a1a3b679SAndreas Boehler 698*a1a3b679SAndreas Boehler $dom = new \DOMDocument('1.0', 'utf-8'); 699*a1a3b679SAndreas Boehler $dom->formatOutput = true; 700*a1a3b679SAndreas Boehler $scheduleResponse = $dom->createElement('cal:schedule-response'); 701*a1a3b679SAndreas Boehler foreach ($this->server->xml->namespaceMap as $namespace => $prefix) { 702*a1a3b679SAndreas Boehler 703*a1a3b679SAndreas Boehler $scheduleResponse->setAttribute('xmlns:' . $prefix, $namespace); 704*a1a3b679SAndreas Boehler 705*a1a3b679SAndreas Boehler } 706*a1a3b679SAndreas Boehler $dom->appendChild($scheduleResponse); 707*a1a3b679SAndreas Boehler 708*a1a3b679SAndreas Boehler foreach ($results as $result) { 709*a1a3b679SAndreas Boehler $xresponse = $dom->createElement('cal:response'); 710*a1a3b679SAndreas Boehler 711*a1a3b679SAndreas Boehler $recipient = $dom->createElement('cal:recipient'); 712*a1a3b679SAndreas Boehler $recipientHref = $dom->createElement('d:href'); 713*a1a3b679SAndreas Boehler 714*a1a3b679SAndreas Boehler $recipientHref->appendChild($dom->createTextNode($result['href'])); 715*a1a3b679SAndreas Boehler $recipient->appendChild($recipientHref); 716*a1a3b679SAndreas Boehler $xresponse->appendChild($recipient); 717*a1a3b679SAndreas Boehler 718*a1a3b679SAndreas Boehler $reqStatus = $dom->createElement('cal:request-status'); 719*a1a3b679SAndreas Boehler $reqStatus->appendChild($dom->createTextNode($result['request-status'])); 720*a1a3b679SAndreas Boehler $xresponse->appendChild($reqStatus); 721*a1a3b679SAndreas Boehler 722*a1a3b679SAndreas Boehler if (isset($result['calendar-data'])) { 723*a1a3b679SAndreas Boehler 724*a1a3b679SAndreas Boehler $calendardata = $dom->createElement('cal:calendar-data'); 725*a1a3b679SAndreas Boehler $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize()))); 726*a1a3b679SAndreas Boehler $xresponse->appendChild($calendardata); 727*a1a3b679SAndreas Boehler 728*a1a3b679SAndreas Boehler } 729*a1a3b679SAndreas Boehler $scheduleResponse->appendChild($xresponse); 730*a1a3b679SAndreas Boehler } 731*a1a3b679SAndreas Boehler 732*a1a3b679SAndreas Boehler $response->setStatus(200); 733*a1a3b679SAndreas Boehler $response->setHeader('Content-Type', 'application/xml'); 734*a1a3b679SAndreas Boehler $response->setBody($dom->saveXML()); 735*a1a3b679SAndreas Boehler 736*a1a3b679SAndreas Boehler } 737*a1a3b679SAndreas Boehler 738*a1a3b679SAndreas Boehler /** 739*a1a3b679SAndreas Boehler * Returns free-busy information for a specific address. The returned 740*a1a3b679SAndreas Boehler * data is an array containing the following properties: 741*a1a3b679SAndreas Boehler * 742*a1a3b679SAndreas Boehler * calendar-data : A VFREEBUSY VObject 743*a1a3b679SAndreas Boehler * request-status : an iTip status code. 744*a1a3b679SAndreas Boehler * href: The principal's email address, as requested 745*a1a3b679SAndreas Boehler * 746*a1a3b679SAndreas Boehler * The following request status codes may be returned: 747*a1a3b679SAndreas Boehler * * 2.0;description 748*a1a3b679SAndreas Boehler * * 3.7;description 749*a1a3b679SAndreas Boehler * 750*a1a3b679SAndreas Boehler * @param string $email address 751*a1a3b679SAndreas Boehler * @param \DateTime $start 752*a1a3b679SAndreas Boehler * @param \DateTime $end 753*a1a3b679SAndreas Boehler * @param VObject\Component $request 754*a1a3b679SAndreas Boehler * @return array 755*a1a3b679SAndreas Boehler */ 756*a1a3b679SAndreas Boehler protected function getFreeBusyForEmail($email, \DateTime $start, \DateTime $end, VObject\Component $request) { 757*a1a3b679SAndreas Boehler 758*a1a3b679SAndreas Boehler $caldavNS = '{' . self::NS_CALDAV . '}'; 759*a1a3b679SAndreas Boehler 760*a1a3b679SAndreas Boehler $aclPlugin = $this->server->getPlugin('acl'); 761*a1a3b679SAndreas Boehler if (substr($email, 0, 7) === 'mailto:') $email = substr($email, 7); 762*a1a3b679SAndreas Boehler 763*a1a3b679SAndreas Boehler $result = $aclPlugin->principalSearch( 764*a1a3b679SAndreas Boehler ['{http://sabredav.org/ns}email-address' => $email], 765*a1a3b679SAndreas Boehler [ 766*a1a3b679SAndreas Boehler '{DAV:}principal-URL', $caldavNS . 'calendar-home-set', 767*a1a3b679SAndreas Boehler '{http://sabredav.org/ns}email-address', 768*a1a3b679SAndreas Boehler ] 769*a1a3b679SAndreas Boehler ); 770*a1a3b679SAndreas Boehler 771*a1a3b679SAndreas Boehler if (!count($result)) { 772*a1a3b679SAndreas Boehler return [ 773*a1a3b679SAndreas Boehler 'request-status' => '3.7;Could not find principal', 774*a1a3b679SAndreas Boehler 'href' => 'mailto:' . $email, 775*a1a3b679SAndreas Boehler ]; 776*a1a3b679SAndreas Boehler } 777*a1a3b679SAndreas Boehler 778*a1a3b679SAndreas Boehler if (!isset($result[0][200][$caldavNS . 'calendar-home-set'])) { 779*a1a3b679SAndreas Boehler return [ 780*a1a3b679SAndreas Boehler 'request-status' => '3.7;No calendar-home-set property found', 781*a1a3b679SAndreas Boehler 'href' => 'mailto:' . $email, 782*a1a3b679SAndreas Boehler ]; 783*a1a3b679SAndreas Boehler } 784*a1a3b679SAndreas Boehler $homeSet = $result[0][200][$caldavNS . 'calendar-home-set']->getHref(); 785*a1a3b679SAndreas Boehler 786*a1a3b679SAndreas Boehler // Grabbing the calendar list 787*a1a3b679SAndreas Boehler $objects = []; 788*a1a3b679SAndreas Boehler $calendarTimeZone = new DateTimeZone('UTC'); 789*a1a3b679SAndreas Boehler 790*a1a3b679SAndreas Boehler foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) { 791*a1a3b679SAndreas Boehler if (!$node instanceof ICalendar) { 792*a1a3b679SAndreas Boehler continue; 793*a1a3b679SAndreas Boehler } 794*a1a3b679SAndreas Boehler 795*a1a3b679SAndreas Boehler $sct = $caldavNS . 'schedule-calendar-transp'; 796*a1a3b679SAndreas Boehler $ctz = $caldavNS . 'calendar-timezone'; 797*a1a3b679SAndreas Boehler $props = $node->getProperties([$sct, $ctz]); 798*a1a3b679SAndreas Boehler 799*a1a3b679SAndreas Boehler if (isset($props[$sct]) && $props[$sct]->getValue() == ScheduleCalendarTransp::TRANSPARENT) { 800*a1a3b679SAndreas Boehler // If a calendar is marked as 'transparent', it means we must 801*a1a3b679SAndreas Boehler // ignore it for free-busy purposes. 802*a1a3b679SAndreas Boehler continue; 803*a1a3b679SAndreas Boehler } 804*a1a3b679SAndreas Boehler 805*a1a3b679SAndreas Boehler $aclPlugin->checkPrivileges($homeSet . $node->getName(), $caldavNS . 'read-free-busy'); 806*a1a3b679SAndreas Boehler 807*a1a3b679SAndreas Boehler if (isset($props[$ctz])) { 808*a1a3b679SAndreas Boehler $vtimezoneObj = VObject\Reader::read($props[$ctz]); 809*a1a3b679SAndreas Boehler $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); 810*a1a3b679SAndreas Boehler } 811*a1a3b679SAndreas Boehler 812*a1a3b679SAndreas Boehler // Getting the list of object uris within the time-range 813*a1a3b679SAndreas Boehler $urls = $node->calendarQuery([ 814*a1a3b679SAndreas Boehler 'name' => 'VCALENDAR', 815*a1a3b679SAndreas Boehler 'comp-filters' => [ 816*a1a3b679SAndreas Boehler [ 817*a1a3b679SAndreas Boehler 'name' => 'VEVENT', 818*a1a3b679SAndreas Boehler 'comp-filters' => [], 819*a1a3b679SAndreas Boehler 'prop-filters' => [], 820*a1a3b679SAndreas Boehler 'is-not-defined' => false, 821*a1a3b679SAndreas Boehler 'time-range' => [ 822*a1a3b679SAndreas Boehler 'start' => $start, 823*a1a3b679SAndreas Boehler 'end' => $end, 824*a1a3b679SAndreas Boehler ], 825*a1a3b679SAndreas Boehler ], 826*a1a3b679SAndreas Boehler ], 827*a1a3b679SAndreas Boehler 'prop-filters' => [], 828*a1a3b679SAndreas Boehler 'is-not-defined' => false, 829*a1a3b679SAndreas Boehler 'time-range' => null, 830*a1a3b679SAndreas Boehler ]); 831*a1a3b679SAndreas Boehler 832*a1a3b679SAndreas Boehler $calObjects = array_map(function($url) use ($node) { 833*a1a3b679SAndreas Boehler $obj = $node->getChild($url)->get(); 834*a1a3b679SAndreas Boehler return $obj; 835*a1a3b679SAndreas Boehler }, $urls); 836*a1a3b679SAndreas Boehler 837*a1a3b679SAndreas Boehler $objects = array_merge($objects, $calObjects); 838*a1a3b679SAndreas Boehler 839*a1a3b679SAndreas Boehler } 840*a1a3b679SAndreas Boehler 841*a1a3b679SAndreas Boehler $vcalendar = new VObject\Component\VCalendar(); 842*a1a3b679SAndreas Boehler $vcalendar->METHOD = 'REPLY'; 843*a1a3b679SAndreas Boehler 844*a1a3b679SAndreas Boehler $generator = new VObject\FreeBusyGenerator(); 845*a1a3b679SAndreas Boehler $generator->setObjects($objects); 846*a1a3b679SAndreas Boehler $generator->setTimeRange($start, $end); 847*a1a3b679SAndreas Boehler $generator->setBaseObject($vcalendar); 848*a1a3b679SAndreas Boehler $generator->setTimeZone($calendarTimeZone); 849*a1a3b679SAndreas Boehler 850*a1a3b679SAndreas Boehler $result = $generator->getResult(); 851*a1a3b679SAndreas Boehler 852*a1a3b679SAndreas Boehler $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:' . $email; 853*a1a3b679SAndreas Boehler $vcalendar->VFREEBUSY->UID = (string)$request->VFREEBUSY->UID; 854*a1a3b679SAndreas Boehler $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER; 855*a1a3b679SAndreas Boehler 856*a1a3b679SAndreas Boehler return [ 857*a1a3b679SAndreas Boehler 'calendar-data' => $result, 858*a1a3b679SAndreas Boehler 'request-status' => '2.0;Success', 859*a1a3b679SAndreas Boehler 'href' => 'mailto:' . $email, 860*a1a3b679SAndreas Boehler ]; 861*a1a3b679SAndreas Boehler } 862*a1a3b679SAndreas Boehler 863*a1a3b679SAndreas Boehler /** 864*a1a3b679SAndreas Boehler * This method checks the 'Schedule-Reply' header 865*a1a3b679SAndreas Boehler * and returns false if it's 'F', otherwise true. 866*a1a3b679SAndreas Boehler * 867*a1a3b679SAndreas Boehler * @param RequestInterface $request 868*a1a3b679SAndreas Boehler * @return bool 869*a1a3b679SAndreas Boehler */ 870*a1a3b679SAndreas Boehler private function scheduleReply(RequestInterface $request) { 871*a1a3b679SAndreas Boehler 872*a1a3b679SAndreas Boehler $scheduleReply = $request->getHeader('Schedule-Reply'); 873*a1a3b679SAndreas Boehler return $scheduleReply !== 'F'; 874*a1a3b679SAndreas Boehler 875*a1a3b679SAndreas Boehler } 876*a1a3b679SAndreas Boehler 877*a1a3b679SAndreas Boehler /** 878*a1a3b679SAndreas Boehler * Returns a bunch of meta-data about the plugin. 879*a1a3b679SAndreas Boehler * 880*a1a3b679SAndreas Boehler * Providing this information is optional, and is mainly displayed by the 881*a1a3b679SAndreas Boehler * Browser plugin. 882*a1a3b679SAndreas Boehler * 883*a1a3b679SAndreas Boehler * The description key in the returned array may contain html and will not 884*a1a3b679SAndreas Boehler * be sanitized. 885*a1a3b679SAndreas Boehler * 886*a1a3b679SAndreas Boehler * @return array 887*a1a3b679SAndreas Boehler */ 888*a1a3b679SAndreas Boehler function getPluginInfo() { 889*a1a3b679SAndreas Boehler 890*a1a3b679SAndreas Boehler return [ 891*a1a3b679SAndreas Boehler 'name' => $this->getPluginName(), 892*a1a3b679SAndreas Boehler 'description' => 'Adds calendar-auto-schedule, as defined in rf6868', 893*a1a3b679SAndreas Boehler 'link' => 'http://sabre.io/dav/scheduling/', 894*a1a3b679SAndreas Boehler ]; 895*a1a3b679SAndreas Boehler 896*a1a3b679SAndreas Boehler } 897*a1a3b679SAndreas Boehler} 898