1<?php 2 3namespace Sabre\DAV\Sharing; 4 5use Sabre\DAV\Exception\BadRequest; 6use Sabre\DAV\Exception\Forbidden; 7use Sabre\DAV\INode; 8use Sabre\DAV\PropFind; 9use Sabre\DAV\Server; 10use Sabre\DAV\ServerPlugin; 11use Sabre\DAV\Xml\Element\Sharee; 12use Sabre\DAV\Xml\Property; 13use Sabre\HTTP\RequestInterface; 14use Sabre\HTTP\ResponseInterface; 15 16/** 17 * This plugin implements HTTP requests and properties related to: 18 * 19 * draft-pot-webdav-resource-sharing 20 * 21 * This specification allows people to share webdav resources with others. 22 * 23 * @copyright Copyright (C) 2007-2015 fruux GmbH. (https://fruux.com/) 24 * @author Evert Pot (http://evertpot.com/) 25 * @license http://sabre.io/license/ Modified BSD License 26 */ 27class Plugin extends ServerPlugin { 28 29 const ACCESS_NOTSHARED = 0; 30 const ACCESS_SHAREDOWNER = 1; 31 const ACCESS_READ = 2; 32 const ACCESS_READWRITE = 3; 33 const ACCESS_NOACCESS = 4; 34 35 const INVITE_NORESPONSE = 1; 36 const INVITE_ACCEPTED = 2; 37 const INVITE_DECLINED = 3; 38 const INVITE_INVALID = 4; 39 40 /** 41 * Reference to SabreDAV server object. 42 * 43 * @var Server 44 */ 45 protected $server; 46 47 /** 48 * This method should return a list of server-features. 49 * 50 * This is for example 'versioning' and is added to the DAV: header 51 * in an OPTIONS response. 52 * 53 * @return array 54 */ 55 function getFeatures() { 56 57 return ['resource-sharing']; 58 59 } 60 61 /** 62 * Returns a plugin name. 63 * 64 * Using this name other plugins will be able to access other plugins 65 * using \Sabre\DAV\Server::getPlugin 66 * 67 * @return string 68 */ 69 function getPluginName() { 70 71 return 'sharing'; 72 73 } 74 75 /** 76 * This initializes the plugin. 77 * 78 * This function is called by Sabre\DAV\Server, after 79 * addPlugin is called. 80 * 81 * This method should set up the required event subscriptions. 82 * 83 * @param Server $server 84 * @return void 85 */ 86 function initialize(Server $server) { 87 88 $this->server = $server; 89 90 $server->xml->elementMap['{DAV:}share-resource'] = 'Sabre\\DAV\\Xml\\Request\\ShareResource'; 91 92 array_push( 93 $server->protectedProperties, 94 '{DAV:}share-mode' 95 ); 96 97 $server->on('method:POST', [$this, 'httpPost']); 98 $server->on('propFind', [$this, 'propFind']); 99 $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); 100 $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); 101 $server->on('onBrowserPostAction', [$this, 'browserPostAction']); 102 103 } 104 105 /** 106 * Updates the list of sharees on a shared resource. 107 * 108 * The sharees array is a list of people that are to be added modified 109 * or removed in the list of shares. 110 * 111 * @param string $path 112 * @param Sharee[] $sharees 113 * @return void 114 */ 115 function shareResource($path, array $sharees) { 116 117 $node = $this->server->tree->getNodeForPath($path); 118 119 if (!$node instanceof ISharedNode) { 120 121 throw new Forbidden('Sharing is not allowed on this node'); 122 123 } 124 125 // Getting ACL info 126 $acl = $this->server->getPlugin('acl'); 127 128 // If there's no ACL support, we allow everything 129 if ($acl) { 130 $acl->checkPrivileges($path, '{DAV:}share'); 131 } 132 133 foreach ($sharees as $sharee) { 134 // We're going to attempt to get a local principal uri for a share 135 // href by emitting the getPrincipalByUri event. 136 $principal = null; 137 $this->server->emit('getPrincipalByUri', [$sharee->href, &$principal]); 138 $sharee->principal = $principal; 139 } 140 $node->updateInvites($sharees); 141 142 } 143 144 /** 145 * This event is triggered when properties are requested for nodes. 146 * 147 * This allows us to inject any sharings-specific properties. 148 * 149 * @param PropFind $propFind 150 * @param INode $node 151 * @return void 152 */ 153 function propFind(PropFind $propFind, INode $node) { 154 155 if ($node instanceof ISharedNode) { 156 157 $propFind->handle('{DAV:}share-access', function() use ($node) { 158 159 return new Property\ShareAccess($node->getShareAccess()); 160 161 }); 162 $propFind->handle('{DAV:}invite', function() use ($node) { 163 164 return new Property\Invite($node->getInvites()); 165 166 }); 167 $propFind->handle('{DAV:}share-resource-uri', function() use ($node) { 168 169 return new Property\Href($node->getShareResourceUri()); 170 171 }); 172 173 } 174 175 } 176 177 /** 178 * We intercept this to handle POST requests on shared resources 179 * 180 * @param RequestInterface $request 181 * @param ResponseInterface $response 182 * @return null|bool 183 */ 184 function httpPost(RequestInterface $request, ResponseInterface $response) { 185 186 $path = $request->getPath(); 187 $contentType = $request->getHeader('Content-Type'); 188 189 // We're only interested in the davsharing content type. 190 if (strpos($contentType, 'application/davsharing+xml') === false) { 191 return; 192 } 193 194 $message = $this->server->xml->parse( 195 $request->getBody(), 196 $request->getUrl(), 197 $documentType 198 ); 199 200 switch ($documentType) { 201 202 case '{DAV:}share-resource': 203 204 $this->shareResource($path, $message->sharees); 205 $response->setStatus(200); 206 // Adding this because sending a response body may cause issues, 207 // and I wanted some type of indicator the response was handled. 208 $response->setHeader('X-Sabre-Status', 'everything-went-well'); 209 210 // Breaking the event chain 211 return false; 212 213 default : 214 throw new BadRequest('Unexpected document type: ' . $documentType . ' for this Content-Type'); 215 216 } 217 218 } 219 220 /** 221 * This method is triggered whenever a subsystem reqeuests the privileges 222 * hat are supported on a particular node. 223 * 224 * We need to add a number of privileges for scheduling purposes. 225 * 226 * @param INode $node 227 * @param array $supportedPrivilegeSet 228 */ 229 function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) { 230 231 if ($node instanceof ISharedNode) { 232 $supportedPrivilegeSet['{DAV:}share'] = [ 233 'abstract' => false, 234 'aggregates' => [], 235 ]; 236 } 237 } 238 239 /** 240 * Returns a bunch of meta-data about the plugin. 241 * 242 * Providing this information is optional, and is mainly displayed by the 243 * Browser plugin. 244 * 245 * The description key in the returned array may contain html and will not 246 * be sanitized. 247 * 248 * @return array 249 */ 250 function getPluginInfo() { 251 252 return [ 253 'name' => $this->getPluginName(), 254 'description' => 'This plugin implements WebDAV resource sharing', 255 'link' => 'https://github.com/evert/webdav-sharing' 256 ]; 257 258 } 259 260 /** 261 * This method is used to generate HTML output for the 262 * DAV\Browser\Plugin. 263 * 264 * @param INode $node 265 * @param string $output 266 * @param string $path 267 * @return bool|null 268 */ 269 function htmlActionsPanel(INode $node, &$output, $path) { 270 271 if (!$node instanceof ISharedNode) { 272 return; 273 } 274 275 $aclPlugin = $this->server->getPlugin('acl'); 276 if ($aclPlugin) { 277 if (!$aclPlugin->checkPrivileges($path, '{DAV:}share', \Sabre\DAVACL\Plugin::R_PARENT, false)) { 278 // Sharing is not permitted, we will not draw this interface. 279 return; 280 } 281 } 282 283 $output .= '<tr><td colspan="2"><form method="post" action=""> 284 <h3>Share this resource</h3> 285 <input type="hidden" name="sabreAction" value="share" /> 286 <label>Share with (uri):</label> <input type="text" name="href" placeholder="mailto:user@example.org"/><br /> 287 <label>Access</label> 288 <select name="access"> 289 <option value="readwrite">Read-write</option> 290 <option value="read">Read-only</option> 291 <option value="no-access">Revoke access</option> 292 </select><br /> 293 <input type="submit" value="share" /> 294 </form> 295 </td></tr>'; 296 297 } 298 299 /** 300 * This method is triggered for POST actions generated by the browser 301 * plugin. 302 * 303 * @param string $path 304 * @param string $action 305 * @param array $postVars 306 */ 307 function browserPostAction($path, $action, $postVars) { 308 309 if ($action !== 'share') { 310 return; 311 } 312 313 if (empty($postVars['href'])) { 314 throw new BadRequest('The "href" POST parameter is required'); 315 } 316 if (empty($postVars['access'])) { 317 throw new BadRequest('The "access" POST parameter is required'); 318 } 319 320 $accessMap = [ 321 'readwrite' => self::ACCESS_READWRITE, 322 'read' => self::ACCESS_READ, 323 'no-access' => self::ACCESS_NOACCESS, 324 ]; 325 326 if (!isset($accessMap[$postVars['access']])) { 327 throw new BadRequest('The "access" POST must be readwrite, read or no-access'); 328 } 329 $sharee = new Sharee([ 330 'href' => $postVars['href'], 331 'access' => $accessMap[$postVars['access']], 332 ]); 333 334 $this->shareResource( 335 $path, 336 [$sharee] 337 ); 338 return false; 339 340 } 341 342} 343