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