1<?php 2 3declare(strict_types=1); 4 5namespace Sabre\CalDAV; 6 7use Sabre\DAV; 8use Sabre\DAV\Xml\Property\LocalHref; 9use Sabre\HTTP\RequestInterface; 10use Sabre\HTTP\ResponseInterface; 11 12/** 13 * This plugin implements support for caldav sharing. 14 * 15 * This spec is defined at: 16 * http://svn.calendarserver.org/repository/calendarserver/CalendarServer/trunk/doc/Extensions/caldav-sharing.txt 17 * 18 * See: 19 * Sabre\CalDAV\Backend\SharingSupport for all the documentation. 20 * 21 * Note: This feature is experimental, and may change in between different 22 * SabreDAV versions. 23 * 24 * @copyright Copyright (C) fruux GmbH (https://fruux.com/) 25 * @author Evert Pot (http://evertpot.com/) 26 * @license http://sabre.io/license/ Modified BSD License 27 */ 28class SharingPlugin extends DAV\ServerPlugin 29{ 30 /** 31 * Reference to SabreDAV server object. 32 * 33 * @var DAV\Server 34 */ 35 protected $server; 36 37 /** 38 * This method should return a list of server-features. 39 * 40 * This is for example 'versioning' and is added to the DAV: header 41 * in an OPTIONS response. 42 * 43 * @return array 44 */ 45 public function getFeatures() 46 { 47 return ['calendarserver-sharing']; 48 } 49 50 /** 51 * Returns a plugin name. 52 * 53 * Using this name other plugins will be able to access other plugins 54 * using Sabre\DAV\Server::getPlugin 55 * 56 * @return string 57 */ 58 public function getPluginName() 59 { 60 return 'caldav-sharing'; 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 public function initialize(DAV\Server $server) 72 { 73 $this->server = $server; 74 75 if (is_null($this->server->getPlugin('sharing'))) { 76 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.'); 77 } 78 79 array_push( 80 $this->server->protectedProperties, 81 '{'.Plugin::NS_CALENDARSERVER.'}invite', 82 '{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes', 83 '{'.Plugin::NS_CALENDARSERVER.'}shared-url' 84 ); 85 86 $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share'; 87 $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply'; 88 89 $this->server->on('propFind', [$this, 'propFindEarly']); 90 $this->server->on('propFind', [$this, 'propFindLate'], 150); 91 $this->server->on('propPatch', [$this, 'propPatch'], 40); 92 $this->server->on('method:POST', [$this, 'httpPost']); 93 } 94 95 /** 96 * This event is triggered when properties are requested for a certain 97 * node. 98 * 99 * This allows us to inject any properties early. 100 */ 101 public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) 102 { 103 if ($node instanceof ISharedCalendar) { 104 $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}invite', function () use ($node) { 105 return new Xml\Property\Invite( 106 $node->getInvites() 107 ); 108 }); 109 } 110 } 111 112 /** 113 * This method is triggered *after* all properties have been retrieved. 114 * This allows us to inject the correct resourcetype for calendars that 115 * have been shared. 116 */ 117 public function propFindLate(DAV\PropFind $propFind, DAV\INode $node) 118 { 119 if ($node instanceof ISharedCalendar) { 120 $shareAccess = $node->getShareAccess(); 121 if ($rt = $propFind->get('{DAV:}resourcetype')) { 122 switch ($shareAccess) { 123 case \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER: 124 $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared-owner'); 125 break; 126 case \Sabre\DAV\Sharing\Plugin::ACCESS_READ: 127 case \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE: 128 $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared'); 129 break; 130 } 131 } 132 $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes', function () { 133 return new Xml\Property\AllowedSharingModes(true, false); 134 }); 135 } 136 } 137 138 /** 139 * This method is trigged when a user attempts to update a node's 140 * properties. 141 * 142 * A previous draft of the sharing spec stated that it was possible to use 143 * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing 144 * the calendar. 145 * 146 * Even though this is no longer in the current spec, we keep this around 147 * because OS X 10.7 may still make use of this feature. 148 * 149 * @param string $path 150 */ 151 public function propPatch($path, DAV\PropPatch $propPatch) 152 { 153 $node = $this->server->tree->getNodeForPath($path); 154 if (!$node instanceof ISharedCalendar) { 155 return; 156 } 157 158 if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $node->getShareAccess() || \Sabre\DAV\Sharing\Plugin::ACCESS_NOTSHARED === $node->getShareAccess()) { 159 $propPatch->handle('{DAV:}resourcetype', function ($value) use ($node) { 160 if ($value->is('{'.Plugin::NS_CALENDARSERVER.'}shared-owner')) { 161 return false; 162 } 163 $shares = $node->getInvites(); 164 foreach ($shares as $share) { 165 $share->access = DAV\Sharing\Plugin::ACCESS_NOACCESS; 166 } 167 $node->updateInvites($shares); 168 169 return true; 170 }); 171 } 172 } 173 174 /** 175 * We intercept this to handle POST requests on calendars. 176 * 177 * @return bool|null 178 */ 179 public function httpPost(RequestInterface $request, ResponseInterface $response) 180 { 181 $path = $request->getPath(); 182 183 // Only handling xml 184 $contentType = $request->getHeader('Content-Type'); 185 if (null === $contentType) { 186 return; 187 } 188 if (false === strpos($contentType, 'application/xml') && false === strpos($contentType, 'text/xml')) { 189 return; 190 } 191 192 // Making sure the node exists 193 try { 194 $node = $this->server->tree->getNodeForPath($path); 195 } catch (DAV\Exception\NotFound $e) { 196 return; 197 } 198 199 $requestBody = $request->getBodyAsString(); 200 201 // If this request handler could not deal with this POST request, it 202 // will return 'null' and other plugins get a chance to handle the 203 // request. 204 // 205 // However, we already requested the full body. This is a problem, 206 // because a body can only be read once. This is why we preemptively 207 // re-populated the request body with the existing data. 208 $request->setBody($requestBody); 209 210 $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); 211 212 switch ($documentType) { 213 // Both the DAV:share-resource and CALENDARSERVER:share requests 214 // behave identically. 215 case '{'.Plugin::NS_CALENDARSERVER.'}share': 216 $sharingPlugin = $this->server->getPlugin('sharing'); 217 $sharingPlugin->shareResource($path, $message->sharees); 218 219 $response->setStatus(200); 220 // Adding this because sending a response body may cause issues, 221 // and I wanted some type of indicator the response was handled. 222 $response->setHeader('X-Sabre-Status', 'everything-went-well'); 223 224 // Breaking the event chain 225 return false; 226 227 // The invite-reply document is sent when the user replies to an 228 // invitation of a calendar share. 229 case '{'.Plugin::NS_CALENDARSERVER.'}invite-reply': 230 // This only works on the calendar-home-root node. 231 if (!$node instanceof CalendarHome) { 232 return; 233 } 234 $this->server->transactionType = 'post-invite-reply'; 235 236 // Getting ACL info 237 $acl = $this->server->getPlugin('acl'); 238 239 // If there's no ACL support, we allow everything 240 if ($acl) { 241 $acl->checkPrivileges($path, '{DAV:}write'); 242 } 243 244 $url = $node->shareReply( 245 $message->href, 246 $message->status, 247 $message->calendarUri, 248 $message->inReplyTo, 249 $message->summary 250 ); 251 252 $response->setStatus(200); 253 // Adding this because sending a response body may cause issues, 254 // and I wanted some type of indicator the response was handled. 255 $response->setHeader('X-Sabre-Status', 'everything-went-well'); 256 257 if ($url) { 258 $writer = $this->server->xml->getWriter(); 259 $writer->contextUri = $request->getUrl(); 260 $writer->openMemory(); 261 $writer->startDocument(); 262 $writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}shared-as'); 263 $writer->write(new LocalHref($url)); 264 $writer->endElement(); 265 $response->setHeader('Content-Type', 'application/xml'); 266 $response->setBody($writer->outputMemory()); 267 } 268 269 // Breaking the event chain 270 return false; 271 272 case '{'.Plugin::NS_CALENDARSERVER.'}publish-calendar': 273 // We can only deal with IShareableCalendar objects 274 if (!$node instanceof ISharedCalendar) { 275 return; 276 } 277 $this->server->transactionType = 'post-publish-calendar'; 278 279 // Getting ACL info 280 $acl = $this->server->getPlugin('acl'); 281 282 // If there's no ACL support, we allow everything 283 if ($acl) { 284 $acl->checkPrivileges($path, '{DAV:}share'); 285 } 286 287 $node->setPublishStatus(true); 288 289 // iCloud sends back the 202, so we will too. 290 $response->setStatus(202); 291 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 case '{'.Plugin::NS_CALENDARSERVER.'}unpublish-calendar': 300 // We can only deal with IShareableCalendar objects 301 if (!$node instanceof ISharedCalendar) { 302 return; 303 } 304 $this->server->transactionType = 'post-unpublish-calendar'; 305 306 // Getting ACL info 307 $acl = $this->server->getPlugin('acl'); 308 309 // If there's no ACL support, we allow everything 310 if ($acl) { 311 $acl->checkPrivileges($path, '{DAV:}share'); 312 } 313 314 $node->setPublishStatus(false); 315 316 $response->setStatus(200); 317 318 // Adding this because sending a response body may cause issues, 319 // and I wanted some type of indicator the response was handled. 320 $response->setHeader('X-Sabre-Status', 'everything-went-well'); 321 322 // Breaking the event chain 323 return false; 324 } 325 } 326 327 /** 328 * Returns a bunch of meta-data about the plugin. 329 * 330 * Providing this information is optional, and is mainly displayed by the 331 * Browser plugin. 332 * 333 * The description key in the returned array may contain html and will not 334 * be sanitized. 335 * 336 * @return array 337 */ 338 public function getPluginInfo() 339 { 340 return [ 341 'name' => $this->getPluginName(), 342 'description' => 'Adds support for caldav-sharing.', 343 'link' => 'http://sabre.io/dav/caldav-sharing/', 344 ]; 345 } 346} 347