1<?php 2 3namespace Sabre\CalDAV; 4 5use Sabre\DAV; 6use Sabre\DAV\Xml\Property\Href; 7use Sabre\HTTP\RequestInterface; 8use Sabre\HTTP\ResponseInterface; 9 10/** 11 * This plugin implements support for caldav sharing. 12 * 13 * This spec is defined at: 14 * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt 15 * 16 * See: 17 * Sabre\CalDAV\Backend\SharingSupport for all the documentation. 18 * 19 * Note: This feature is experimental, and may change in between different 20 * SabreDAV versions. 21 * 22 * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). 23 * @author Evert Pot (http://evertpot.com/) 24 * @license http://sabre.io/license/ Modified BSD License 25 */ 26class SharingPlugin extends DAV\ServerPlugin { 27 28 /** 29 * These are the various status constants used by sharing-messages. 30 */ 31 const STATUS_ACCEPTED = 1; 32 const STATUS_DECLINED = 2; 33 const STATUS_DELETED = 3; 34 const STATUS_NORESPONSE = 4; 35 const STATUS_INVALID = 5; 36 37 /** 38 * Reference to SabreDAV server object. 39 * 40 * @var Sabre\DAV\Server 41 */ 42 protected $server; 43 44 /** 45 * This method should return a list of server-features. 46 * 47 * This is for example 'versioning' and is added to the DAV: header 48 * in an OPTIONS response. 49 * 50 * @return array 51 */ 52 function getFeatures() { 53 54 return ['calendarserver-sharing']; 55 56 } 57 58 /** 59 * Returns a plugin name. 60 * 61 * Using this name other plugins will be able to access other plugins 62 * using Sabre\DAV\Server::getPlugin 63 * 64 * @return string 65 */ 66 function getPluginName() { 67 68 return 'caldav-sharing'; 69 70 } 71 72 /** 73 * This initializes the plugin. 74 * 75 * This function is called by Sabre\DAV\Server, after 76 * addPlugin is called. 77 * 78 * This method should set up the required event subscriptions. 79 * 80 * @param DAV\Server $server 81 * @return void 82 */ 83 function initialize(DAV\Server $server) { 84 85 $this->server = $server; 86 $server->resourceTypeMapping['Sabre\\CalDAV\\ISharedCalendar'] = '{' . Plugin::NS_CALENDARSERVER . '}shared'; 87 88 array_push( 89 $this->server->protectedProperties, 90 '{' . Plugin::NS_CALENDARSERVER . '}invite', 91 '{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes', 92 '{' . Plugin::NS_CALENDARSERVER . '}shared-url' 93 ); 94 95 $this->server->xml->elementMap['{' . Plugin::NS_CALENDARSERVER . '}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share'; 96 $this->server->xml->elementMap['{' . Plugin::NS_CALENDARSERVER . '}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply'; 97 98 $this->server->on('propFind', [$this, 'propFindEarly']); 99 $this->server->on('propFind', [$this, 'propFindLate'], 150); 100 $this->server->on('propPatch', [$this, 'propPatch'], 40); 101 $this->server->on('method:POST', [$this, 'httpPost']); 102 103 } 104 105 /** 106 * This event is triggered when properties are requested for a certain 107 * node. 108 * 109 * This allows us to inject any properties early. 110 * 111 * @param DAV\PropFind $propFind 112 * @param DAV\INode $node 113 * @return void 114 */ 115 function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) { 116 117 if ($node instanceof IShareableCalendar) { 118 119 $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) { 120 return new Xml\Property\Invite( 121 $node->getShares() 122 ); 123 }); 124 125 } 126 127 if ($node instanceof ISharedCalendar) { 128 129 $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}shared-url', function() use ($node) { 130 return new Href( 131 $node->getSharedUrl() 132 ); 133 }); 134 135 $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}invite', function() use ($node) { 136 137 // Fetching owner information 138 $props = $this->server->getPropertiesForPath($node->getOwner(), [ 139 '{http://sabredav.org/ns}email-address', 140 '{DAV:}displayname', 141 ], 0); 142 143 $ownerInfo = [ 144 'href' => $node->getOwner(), 145 ]; 146 147 if (isset($props[0][200])) { 148 149 // We're mapping the internal webdav properties to the 150 // elements caldav-sharing expects. 151 if (isset($props[0][200]['{http://sabredav.org/ns}email-address'])) { 152 $ownerInfo['href'] = 'mailto:' . $props[0][200]['{http://sabredav.org/ns}email-address']; 153 } 154 if (isset($props[0][200]['{DAV:}displayname'])) { 155 $ownerInfo['commonName'] = $props[0][200]['{DAV:}displayname']; 156 } 157 158 } 159 160 return new Xml\Property\Invite( 161 $node->getShares(), 162 $ownerInfo 163 ); 164 165 }); 166 167 } 168 169 } 170 171 /** 172 * This method is triggered *after* all properties have been retrieved. 173 * This allows us to inject the correct resourcetype for calendars that 174 * have been shared. 175 * 176 * @param DAV\PropFind $propFind 177 * @param DAV\INode $node 178 * @return void 179 */ 180 function propFindLate(DAV\PropFind $propFind, DAV\INode $node) { 181 182 if ($node instanceof IShareableCalendar) { 183 if ($rt = $propFind->get('{DAV:}resourcetype')) { 184 if (count($node->getShares()) > 0) { 185 $rt->add('{' . Plugin::NS_CALENDARSERVER . '}shared-owner'); 186 } 187 } 188 $propFind->handle('{' . Plugin::NS_CALENDARSERVER . '}allowed-sharing-modes', function() { 189 return new Xml\Property\AllowedSharingModes(true, false); 190 }); 191 192 } 193 194 } 195 196 /** 197 * This method is trigged when a user attempts to update a node's 198 * properties. 199 * 200 * A previous draft of the sharing spec stated that it was possible to use 201 * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing 202 * the calendar. 203 * 204 * Even though this is no longer in the current spec, we keep this around 205 * because OS X 10.7 may still make use of this feature. 206 * 207 * @param string $path 208 * @param DAV\PropPatch $propPatch 209 * @return void 210 */ 211 function propPatch($path, DAV\PropPatch $propPatch) { 212 213 $node = $this->server->tree->getNodeForPath($path); 214 if (!$node instanceof IShareableCalendar) 215 return; 216 217 $propPatch->handle('{DAV:}resourcetype', function($value) use ($node) { 218 if ($value->is('{' . Plugin::NS_CALENDARSERVER . '}shared-owner')) return false; 219 $shares = $node->getShares(); 220 $remove = []; 221 foreach ($shares as $share) { 222 $remove[] = $share['href']; 223 } 224 $node->updateShares([], $remove); 225 226 return true; 227 228 }); 229 230 } 231 232 /** 233 * We intercept this to handle POST requests on calendars. 234 * 235 * @param RequestInterface $request 236 * @param ResponseInterface $response 237 * @return null|bool 238 */ 239 function httpPost(RequestInterface $request, ResponseInterface $response) { 240 241 $path = $request->getPath(); 242 243 // Only handling xml 244 $contentType = $request->getHeader('Content-Type'); 245 if (strpos($contentType, 'application/xml') === false && strpos($contentType, 'text/xml') === false) 246 return; 247 248 // Making sure the node exists 249 try { 250 $node = $this->server->tree->getNodeForPath($path); 251 } catch (DAV\Exception\NotFound $e) { 252 return; 253 } 254 255 $requestBody = $request->getBodyAsString(); 256 257 // If this request handler could not deal with this POST request, it 258 // will return 'null' and other plugins get a chance to handle the 259 // request. 260 // 261 // However, we already requested the full body. This is a problem, 262 // because a body can only be read once. This is why we preemptively 263 // re-populated the request body with the existing data. 264 $request->setBody($requestBody); 265 266 $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); 267 268 switch ($documentType) { 269 270 // Dealing with the 'share' document, which modified invitees on a 271 // calendar. 272 case '{' . Plugin::NS_CALENDARSERVER . '}share' : 273 274 // We can only deal with IShareableCalendar objects 275 if (!$node instanceof IShareableCalendar) { 276 return; 277 } 278 279 $this->server->transactionType = 'post-calendar-share'; 280 281 // Getting ACL info 282 $acl = $this->server->getPlugin('acl'); 283 284 // If there's no ACL support, we allow everything 285 if ($acl) { 286 $acl->checkPrivileges($path, '{DAV:}write'); 287 } 288 289 $node->updateShares($message->set, $message->remove); 290 291 $response->setStatus(200); 292 // Adding this because sending a response body may cause issues, 293 // and I wanted some type of indicator the response was handled. 294 $response->setHeader('X-Sabre-Status', 'everything-went-well'); 295 296 // Breaking the event chain 297 return false; 298 299 // The invite-reply document is sent when the user replies to an 300 // invitation of a calendar share. 301 case '{' . Plugin::NS_CALENDARSERVER . '}invite-reply' : 302 303 // This only works on the calendar-home-root node. 304 if (!$node instanceof CalendarHome) { 305 return; 306 } 307 $this->server->transactionType = 'post-invite-reply'; 308 309 // Getting ACL info 310 $acl = $this->server->getPlugin('acl'); 311 312 // If there's no ACL support, we allow everything 313 if ($acl) { 314 $acl->checkPrivileges($path, '{DAV:}write'); 315 } 316 317 $url = $node->shareReply( 318 $message->href, 319 $message->status, 320 $message->calendarUri, 321 $message->inReplyTo, 322 $message->summary 323 ); 324 325 $response->setStatus(200); 326 // Adding this because sending a response body may cause issues, 327 // and I wanted some type of indicator the response was handled. 328 $response->setHeader('X-Sabre-Status', 'everything-went-well'); 329 330 if ($url) { 331 $writer = $this->server->xml->getWriter($this->server->getBaseUri()); 332 $writer->openMemory(); 333 $writer->startDocument(); 334 $writer->startElement('{' . Plugin::NS_CALENDARSERVER . '}shared-as'); 335 $writer->write(new Href($url)); 336 $writer->endElement(); 337 $response->setHeader('Content-Type', 'application/xml'); 338 $response->setBody($writer->outputMemory()); 339 340 } 341 342 // Breaking the event chain 343 return false; 344 345 case '{' . Plugin::NS_CALENDARSERVER . '}publish-calendar' : 346 347 // We can only deal with IShareableCalendar objects 348 if (!$node instanceof IShareableCalendar) { 349 return; 350 } 351 $this->server->transactionType = 'post-publish-calendar'; 352 353 // Getting ACL info 354 $acl = $this->server->getPlugin('acl'); 355 356 // If there's no ACL support, we allow everything 357 if ($acl) { 358 $acl->checkPrivileges($path, '{DAV:}write'); 359 } 360 361 $node->setPublishStatus(true); 362 363 // iCloud sends back the 202, so we will too. 364 $response->setStatus(202); 365 366 // Adding this because sending a response body may cause issues, 367 // and I wanted some type of indicator the response was handled. 368 $response->setHeader('X-Sabre-Status', 'everything-went-well'); 369 370 // Breaking the event chain 371 return false; 372 373 case '{' . Plugin::NS_CALENDARSERVER . '}unpublish-calendar' : 374 375 // We can only deal with IShareableCalendar objects 376 if (!$node instanceof IShareableCalendar) { 377 return; 378 } 379 $this->server->transactionType = 'post-unpublish-calendar'; 380 381 // Getting ACL info 382 $acl = $this->server->getPlugin('acl'); 383 384 // If there's no ACL support, we allow everything 385 if ($acl) { 386 $acl->checkPrivileges($path, '{DAV:}write'); 387 } 388 389 $node->setPublishStatus(false); 390 391 $response->setStatus(200); 392 393 // Adding this because sending a response body may cause issues, 394 // and I wanted some type of indicator the response was handled. 395 $response->setHeader('X-Sabre-Status', 'everything-went-well'); 396 397 // Breaking the event chain 398 return false; 399 400 } 401 402 403 404 } 405 406 /** 407 * Returns a bunch of meta-data about the plugin. 408 * 409 * Providing this information is optional, and is mainly displayed by the 410 * Browser plugin. 411 * 412 * The description key in the returned array may contain html and will not 413 * be sanitized. 414 * 415 * @return array 416 */ 417 function getPluginInfo() { 418 419 return [ 420 'name' => $this->getPluginName(), 421 'description' => 'Adds support for caldav-sharing.', 422 'link' => 'http://sabre.io/dav/caldav-sharing/', 423 ]; 424 425 } 426} 427