1*a1a3b679SAndreas Boehler<?php 2*a1a3b679SAndreas Boehler 3*a1a3b679SAndreas Boehlernamespace Sabre\DAV; 4*a1a3b679SAndreas Boehler 5*a1a3b679SAndreas Boehleruse Sabre\Event\EventEmitter; 6*a1a3b679SAndreas Boehleruse Sabre\HTTP; 7*a1a3b679SAndreas Boehleruse Sabre\HTTP\RequestInterface; 8*a1a3b679SAndreas Boehleruse Sabre\HTTP\ResponseInterface; 9*a1a3b679SAndreas Boehleruse Sabre\HTTP\URLUtil; 10*a1a3b679SAndreas Boehleruse Sabre\Uri; 11*a1a3b679SAndreas Boehler 12*a1a3b679SAndreas Boehler/** 13*a1a3b679SAndreas Boehler * Main DAV server class 14*a1a3b679SAndreas Boehler * 15*a1a3b679SAndreas Boehler * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/). 16*a1a3b679SAndreas Boehler * @author Evert Pot (http://evertpot.com/) 17*a1a3b679SAndreas Boehler * @license http://sabre.io/license/ Modified BSD License 18*a1a3b679SAndreas Boehler */ 19*a1a3b679SAndreas Boehlerclass Server extends EventEmitter { 20*a1a3b679SAndreas Boehler 21*a1a3b679SAndreas Boehler /** 22*a1a3b679SAndreas Boehler * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree 23*a1a3b679SAndreas Boehler */ 24*a1a3b679SAndreas Boehler const DEPTH_INFINITY = -1; 25*a1a3b679SAndreas Boehler 26*a1a3b679SAndreas Boehler /** 27*a1a3b679SAndreas Boehler * XML namespace for all SabreDAV related elements 28*a1a3b679SAndreas Boehler */ 29*a1a3b679SAndreas Boehler const NS_SABREDAV = 'http://sabredav.org/ns'; 30*a1a3b679SAndreas Boehler 31*a1a3b679SAndreas Boehler /** 32*a1a3b679SAndreas Boehler * The tree object 33*a1a3b679SAndreas Boehler * 34*a1a3b679SAndreas Boehler * @var Sabre\DAV\Tree 35*a1a3b679SAndreas Boehler */ 36*a1a3b679SAndreas Boehler public $tree; 37*a1a3b679SAndreas Boehler 38*a1a3b679SAndreas Boehler /** 39*a1a3b679SAndreas Boehler * The base uri 40*a1a3b679SAndreas Boehler * 41*a1a3b679SAndreas Boehler * @var string 42*a1a3b679SAndreas Boehler */ 43*a1a3b679SAndreas Boehler protected $baseUri = null; 44*a1a3b679SAndreas Boehler 45*a1a3b679SAndreas Boehler /** 46*a1a3b679SAndreas Boehler * httpResponse 47*a1a3b679SAndreas Boehler * 48*a1a3b679SAndreas Boehler * @var Sabre\HTTP\Response 49*a1a3b679SAndreas Boehler */ 50*a1a3b679SAndreas Boehler public $httpResponse; 51*a1a3b679SAndreas Boehler 52*a1a3b679SAndreas Boehler /** 53*a1a3b679SAndreas Boehler * httpRequest 54*a1a3b679SAndreas Boehler * 55*a1a3b679SAndreas Boehler * @var Sabre\HTTP\Request 56*a1a3b679SAndreas Boehler */ 57*a1a3b679SAndreas Boehler public $httpRequest; 58*a1a3b679SAndreas Boehler 59*a1a3b679SAndreas Boehler /** 60*a1a3b679SAndreas Boehler * PHP HTTP Sapi 61*a1a3b679SAndreas Boehler * 62*a1a3b679SAndreas Boehler * @var Sabre\HTTP\Sapi 63*a1a3b679SAndreas Boehler */ 64*a1a3b679SAndreas Boehler public $sapi; 65*a1a3b679SAndreas Boehler 66*a1a3b679SAndreas Boehler /** 67*a1a3b679SAndreas Boehler * The list of plugins 68*a1a3b679SAndreas Boehler * 69*a1a3b679SAndreas Boehler * @var array 70*a1a3b679SAndreas Boehler */ 71*a1a3b679SAndreas Boehler protected $plugins = []; 72*a1a3b679SAndreas Boehler 73*a1a3b679SAndreas Boehler /** 74*a1a3b679SAndreas Boehler * This property will be filled with a unique string that describes the 75*a1a3b679SAndreas Boehler * transaction. This is useful for performance measuring and logging 76*a1a3b679SAndreas Boehler * purposes. 77*a1a3b679SAndreas Boehler * 78*a1a3b679SAndreas Boehler * By default it will just fill it with a lowercased HTTP method name, but 79*a1a3b679SAndreas Boehler * plugins override this. For example, the WebDAV-Sync sync-collection 80*a1a3b679SAndreas Boehler * report will set this to 'report-sync-collection'. 81*a1a3b679SAndreas Boehler * 82*a1a3b679SAndreas Boehler * @var string 83*a1a3b679SAndreas Boehler */ 84*a1a3b679SAndreas Boehler public $transactionType; 85*a1a3b679SAndreas Boehler 86*a1a3b679SAndreas Boehler /** 87*a1a3b679SAndreas Boehler * This is a list of properties that are always server-controlled, and 88*a1a3b679SAndreas Boehler * must not get modified with PROPPATCH. 89*a1a3b679SAndreas Boehler * 90*a1a3b679SAndreas Boehler * Plugins may add to this list. 91*a1a3b679SAndreas Boehler * 92*a1a3b679SAndreas Boehler * @var string[] 93*a1a3b679SAndreas Boehler */ 94*a1a3b679SAndreas Boehler public $protectedProperties = [ 95*a1a3b679SAndreas Boehler 96*a1a3b679SAndreas Boehler // RFC4918 97*a1a3b679SAndreas Boehler '{DAV:}getcontentlength', 98*a1a3b679SAndreas Boehler '{DAV:}getetag', 99*a1a3b679SAndreas Boehler '{DAV:}getlastmodified', 100*a1a3b679SAndreas Boehler '{DAV:}lockdiscovery', 101*a1a3b679SAndreas Boehler '{DAV:}supportedlock', 102*a1a3b679SAndreas Boehler 103*a1a3b679SAndreas Boehler // RFC4331 104*a1a3b679SAndreas Boehler '{DAV:}quota-available-bytes', 105*a1a3b679SAndreas Boehler '{DAV:}quota-used-bytes', 106*a1a3b679SAndreas Boehler 107*a1a3b679SAndreas Boehler // RFC3744 108*a1a3b679SAndreas Boehler '{DAV:}supported-privilege-set', 109*a1a3b679SAndreas Boehler '{DAV:}current-user-privilege-set', 110*a1a3b679SAndreas Boehler '{DAV:}acl', 111*a1a3b679SAndreas Boehler '{DAV:}acl-restrictions', 112*a1a3b679SAndreas Boehler '{DAV:}inherited-acl-set', 113*a1a3b679SAndreas Boehler 114*a1a3b679SAndreas Boehler // RFC3253 115*a1a3b679SAndreas Boehler '{DAV:}supported-method-set', 116*a1a3b679SAndreas Boehler '{DAV:}supported-report-set', 117*a1a3b679SAndreas Boehler 118*a1a3b679SAndreas Boehler // RFC6578 119*a1a3b679SAndreas Boehler '{DAV:}sync-token', 120*a1a3b679SAndreas Boehler 121*a1a3b679SAndreas Boehler // calendarserver.org extensions 122*a1a3b679SAndreas Boehler '{http://calendarserver.org/ns/}ctag', 123*a1a3b679SAndreas Boehler 124*a1a3b679SAndreas Boehler // sabredav extensions 125*a1a3b679SAndreas Boehler '{http://sabredav.org/ns}sync-token', 126*a1a3b679SAndreas Boehler 127*a1a3b679SAndreas Boehler ]; 128*a1a3b679SAndreas Boehler 129*a1a3b679SAndreas Boehler /** 130*a1a3b679SAndreas Boehler * This is a flag that allow or not showing file, line and code 131*a1a3b679SAndreas Boehler * of the exception in the returned XML 132*a1a3b679SAndreas Boehler * 133*a1a3b679SAndreas Boehler * @var bool 134*a1a3b679SAndreas Boehler */ 135*a1a3b679SAndreas Boehler public $debugExceptions = false; 136*a1a3b679SAndreas Boehler 137*a1a3b679SAndreas Boehler /** 138*a1a3b679SAndreas Boehler * This property allows you to automatically add the 'resourcetype' value 139*a1a3b679SAndreas Boehler * based on a node's classname or interface. 140*a1a3b679SAndreas Boehler * 141*a1a3b679SAndreas Boehler * The preset ensures that {DAV:}collection is automatically added for nodes 142*a1a3b679SAndreas Boehler * implementing Sabre\DAV\ICollection. 143*a1a3b679SAndreas Boehler * 144*a1a3b679SAndreas Boehler * @var array 145*a1a3b679SAndreas Boehler */ 146*a1a3b679SAndreas Boehler public $resourceTypeMapping = [ 147*a1a3b679SAndreas Boehler 'Sabre\\DAV\\ICollection' => '{DAV:}collection', 148*a1a3b679SAndreas Boehler ]; 149*a1a3b679SAndreas Boehler 150*a1a3b679SAndreas Boehler /** 151*a1a3b679SAndreas Boehler * This property allows the usage of Depth: infinity on PROPFIND requests. 152*a1a3b679SAndreas Boehler * 153*a1a3b679SAndreas Boehler * By default Depth: infinity is treated as Depth: 1. Allowing Depth: 154*a1a3b679SAndreas Boehler * infinity is potentially risky, as it allows a single client to do a full 155*a1a3b679SAndreas Boehler * index of the webdav server, which is an easy DoS attack vector. 156*a1a3b679SAndreas Boehler * 157*a1a3b679SAndreas Boehler * Only turn this on if you know what you're doing. 158*a1a3b679SAndreas Boehler * 159*a1a3b679SAndreas Boehler * @var bool 160*a1a3b679SAndreas Boehler */ 161*a1a3b679SAndreas Boehler public $enablePropfindDepthInfinity = false; 162*a1a3b679SAndreas Boehler 163*a1a3b679SAndreas Boehler /** 164*a1a3b679SAndreas Boehler * Reference to the XML utility object. 165*a1a3b679SAndreas Boehler * 166*a1a3b679SAndreas Boehler * @var Xml\Service 167*a1a3b679SAndreas Boehler */ 168*a1a3b679SAndreas Boehler public $xml; 169*a1a3b679SAndreas Boehler 170*a1a3b679SAndreas Boehler /** 171*a1a3b679SAndreas Boehler * If this setting is turned off, SabreDAV's version number will be hidden 172*a1a3b679SAndreas Boehler * from various places. 173*a1a3b679SAndreas Boehler * 174*a1a3b679SAndreas Boehler * Some people feel this is a good security measure. 175*a1a3b679SAndreas Boehler * 176*a1a3b679SAndreas Boehler * @var bool 177*a1a3b679SAndreas Boehler */ 178*a1a3b679SAndreas Boehler static $exposeVersion = true; 179*a1a3b679SAndreas Boehler 180*a1a3b679SAndreas Boehler /** 181*a1a3b679SAndreas Boehler * Sets up the server 182*a1a3b679SAndreas Boehler * 183*a1a3b679SAndreas Boehler * If a Sabre\DAV\Tree object is passed as an argument, it will 184*a1a3b679SAndreas Boehler * use it as the directory tree. If a Sabre\DAV\INode is passed, it 185*a1a3b679SAndreas Boehler * will create a Sabre\DAV\Tree and use the node as the root. 186*a1a3b679SAndreas Boehler * 187*a1a3b679SAndreas Boehler * If nothing is passed, a Sabre\DAV\SimpleCollection is created in 188*a1a3b679SAndreas Boehler * a Sabre\DAV\Tree. 189*a1a3b679SAndreas Boehler * 190*a1a3b679SAndreas Boehler * If an array is passed, we automatically create a root node, and use 191*a1a3b679SAndreas Boehler * the nodes in the array as top-level children. 192*a1a3b679SAndreas Boehler * 193*a1a3b679SAndreas Boehler * @param Tree|INode|array|null $treeOrNode The tree object 194*a1a3b679SAndreas Boehler */ 195*a1a3b679SAndreas Boehler function __construct($treeOrNode = null) { 196*a1a3b679SAndreas Boehler 197*a1a3b679SAndreas Boehler if ($treeOrNode instanceof Tree) { 198*a1a3b679SAndreas Boehler $this->tree = $treeOrNode; 199*a1a3b679SAndreas Boehler } elseif ($treeOrNode instanceof INode) { 200*a1a3b679SAndreas Boehler $this->tree = new Tree($treeOrNode); 201*a1a3b679SAndreas Boehler } elseif (is_array($treeOrNode)) { 202*a1a3b679SAndreas Boehler 203*a1a3b679SAndreas Boehler // If it's an array, a list of nodes was passed, and we need to 204*a1a3b679SAndreas Boehler // create the root node. 205*a1a3b679SAndreas Boehler foreach ($treeOrNode as $node) { 206*a1a3b679SAndreas Boehler if (!($node instanceof INode)) { 207*a1a3b679SAndreas Boehler throw new Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre\\DAV\\INode'); 208*a1a3b679SAndreas Boehler } 209*a1a3b679SAndreas Boehler } 210*a1a3b679SAndreas Boehler 211*a1a3b679SAndreas Boehler $root = new SimpleCollection('root', $treeOrNode); 212*a1a3b679SAndreas Boehler $this->tree = new Tree($root); 213*a1a3b679SAndreas Boehler 214*a1a3b679SAndreas Boehler } elseif (is_null($treeOrNode)) { 215*a1a3b679SAndreas Boehler $root = new SimpleCollection('root'); 216*a1a3b679SAndreas Boehler $this->tree = new Tree($root); 217*a1a3b679SAndreas Boehler } else { 218*a1a3b679SAndreas Boehler throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null'); 219*a1a3b679SAndreas Boehler } 220*a1a3b679SAndreas Boehler 221*a1a3b679SAndreas Boehler $this->xml = new Xml\Service(); 222*a1a3b679SAndreas Boehler $this->sapi = new HTTP\Sapi(); 223*a1a3b679SAndreas Boehler $this->httpResponse = new HTTP\Response(); 224*a1a3b679SAndreas Boehler $this->httpRequest = $this->sapi->getRequest(); 225*a1a3b679SAndreas Boehler $this->addPlugin(new CorePlugin()); 226*a1a3b679SAndreas Boehler 227*a1a3b679SAndreas Boehler } 228*a1a3b679SAndreas Boehler 229*a1a3b679SAndreas Boehler /** 230*a1a3b679SAndreas Boehler * Starts the DAV Server 231*a1a3b679SAndreas Boehler * 232*a1a3b679SAndreas Boehler * @return void 233*a1a3b679SAndreas Boehler */ 234*a1a3b679SAndreas Boehler function exec() { 235*a1a3b679SAndreas Boehler 236*a1a3b679SAndreas Boehler try { 237*a1a3b679SAndreas Boehler 238*a1a3b679SAndreas Boehler // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an 239*a1a3b679SAndreas Boehler // origin, we must make sure we send back HTTP/1.0 if this was 240*a1a3b679SAndreas Boehler // requested. 241*a1a3b679SAndreas Boehler // This is mainly because nginx doesn't support Chunked Transfer 242*a1a3b679SAndreas Boehler // Encoding, and this forces the webserver SabreDAV is running on, 243*a1a3b679SAndreas Boehler // to buffer entire responses to calculate Content-Length. 244*a1a3b679SAndreas Boehler $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion()); 245*a1a3b679SAndreas Boehler 246*a1a3b679SAndreas Boehler // Setting the base url 247*a1a3b679SAndreas Boehler $this->httpRequest->setBaseUrl($this->getBaseUri()); 248*a1a3b679SAndreas Boehler $this->invokeMethod($this->httpRequest, $this->httpResponse); 249*a1a3b679SAndreas Boehler 250*a1a3b679SAndreas Boehler } catch (\Exception $e) { 251*a1a3b679SAndreas Boehler 252*a1a3b679SAndreas Boehler try { 253*a1a3b679SAndreas Boehler $this->emit('exception', [$e]); 254*a1a3b679SAndreas Boehler } catch (\Exception $ignore) { 255*a1a3b679SAndreas Boehler } 256*a1a3b679SAndreas Boehler $DOM = new \DOMDocument('1.0', 'utf-8'); 257*a1a3b679SAndreas Boehler $DOM->formatOutput = true; 258*a1a3b679SAndreas Boehler 259*a1a3b679SAndreas Boehler $error = $DOM->createElementNS('DAV:', 'd:error'); 260*a1a3b679SAndreas Boehler $error->setAttribute('xmlns:s', self::NS_SABREDAV); 261*a1a3b679SAndreas Boehler $DOM->appendChild($error); 262*a1a3b679SAndreas Boehler 263*a1a3b679SAndreas Boehler $h = function($v) { 264*a1a3b679SAndreas Boehler 265*a1a3b679SAndreas Boehler return htmlspecialchars($v, ENT_NOQUOTES, 'UTF-8'); 266*a1a3b679SAndreas Boehler 267*a1a3b679SAndreas Boehler }; 268*a1a3b679SAndreas Boehler 269*a1a3b679SAndreas Boehler if (self::$exposeVersion) { 270*a1a3b679SAndreas Boehler $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION))); 271*a1a3b679SAndreas Boehler } 272*a1a3b679SAndreas Boehler 273*a1a3b679SAndreas Boehler $error->appendChild($DOM->createElement('s:exception', $h(get_class($e)))); 274*a1a3b679SAndreas Boehler $error->appendChild($DOM->createElement('s:message', $h($e->getMessage()))); 275*a1a3b679SAndreas Boehler if ($this->debugExceptions) { 276*a1a3b679SAndreas Boehler $error->appendChild($DOM->createElement('s:file', $h($e->getFile()))); 277*a1a3b679SAndreas Boehler $error->appendChild($DOM->createElement('s:line', $h($e->getLine()))); 278*a1a3b679SAndreas Boehler $error->appendChild($DOM->createElement('s:code', $h($e->getCode()))); 279*a1a3b679SAndreas Boehler $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString()))); 280*a1a3b679SAndreas Boehler } 281*a1a3b679SAndreas Boehler 282*a1a3b679SAndreas Boehler if ($this->debugExceptions) { 283*a1a3b679SAndreas Boehler $previous = $e; 284*a1a3b679SAndreas Boehler while ($previous = $previous->getPrevious()) { 285*a1a3b679SAndreas Boehler $xPrevious = $DOM->createElement('s:previous-exception'); 286*a1a3b679SAndreas Boehler $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous)))); 287*a1a3b679SAndreas Boehler $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage()))); 288*a1a3b679SAndreas Boehler $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile()))); 289*a1a3b679SAndreas Boehler $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine()))); 290*a1a3b679SAndreas Boehler $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode()))); 291*a1a3b679SAndreas Boehler $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString()))); 292*a1a3b679SAndreas Boehler $error->appendChild($xPrevious); 293*a1a3b679SAndreas Boehler } 294*a1a3b679SAndreas Boehler } 295*a1a3b679SAndreas Boehler 296*a1a3b679SAndreas Boehler 297*a1a3b679SAndreas Boehler if ($e instanceof Exception) { 298*a1a3b679SAndreas Boehler 299*a1a3b679SAndreas Boehler $httpCode = $e->getHTTPCode(); 300*a1a3b679SAndreas Boehler $e->serialize($this, $error); 301*a1a3b679SAndreas Boehler $headers = $e->getHTTPHeaders($this); 302*a1a3b679SAndreas Boehler 303*a1a3b679SAndreas Boehler } else { 304*a1a3b679SAndreas Boehler 305*a1a3b679SAndreas Boehler $httpCode = 500; 306*a1a3b679SAndreas Boehler $headers = []; 307*a1a3b679SAndreas Boehler 308*a1a3b679SAndreas Boehler } 309*a1a3b679SAndreas Boehler $headers['Content-Type'] = 'application/xml; charset=utf-8'; 310*a1a3b679SAndreas Boehler 311*a1a3b679SAndreas Boehler $this->httpResponse->setStatus($httpCode); 312*a1a3b679SAndreas Boehler $this->httpResponse->setHeaders($headers); 313*a1a3b679SAndreas Boehler $this->httpResponse->setBody($DOM->saveXML()); 314*a1a3b679SAndreas Boehler $this->sapi->sendResponse($this->httpResponse); 315*a1a3b679SAndreas Boehler 316*a1a3b679SAndreas Boehler } 317*a1a3b679SAndreas Boehler 318*a1a3b679SAndreas Boehler } 319*a1a3b679SAndreas Boehler 320*a1a3b679SAndreas Boehler /** 321*a1a3b679SAndreas Boehler * Sets the base server uri 322*a1a3b679SAndreas Boehler * 323*a1a3b679SAndreas Boehler * @param string $uri 324*a1a3b679SAndreas Boehler * @return void 325*a1a3b679SAndreas Boehler */ 326*a1a3b679SAndreas Boehler function setBaseUri($uri) { 327*a1a3b679SAndreas Boehler 328*a1a3b679SAndreas Boehler // If the baseUri does not end with a slash, we must add it 329*a1a3b679SAndreas Boehler if ($uri[strlen($uri) - 1] !== '/') 330*a1a3b679SAndreas Boehler $uri .= '/'; 331*a1a3b679SAndreas Boehler 332*a1a3b679SAndreas Boehler $this->baseUri = $uri; 333*a1a3b679SAndreas Boehler 334*a1a3b679SAndreas Boehler } 335*a1a3b679SAndreas Boehler 336*a1a3b679SAndreas Boehler /** 337*a1a3b679SAndreas Boehler * Returns the base responding uri 338*a1a3b679SAndreas Boehler * 339*a1a3b679SAndreas Boehler * @return string 340*a1a3b679SAndreas Boehler */ 341*a1a3b679SAndreas Boehler function getBaseUri() { 342*a1a3b679SAndreas Boehler 343*a1a3b679SAndreas Boehler if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri(); 344*a1a3b679SAndreas Boehler return $this->baseUri; 345*a1a3b679SAndreas Boehler 346*a1a3b679SAndreas Boehler } 347*a1a3b679SAndreas Boehler 348*a1a3b679SAndreas Boehler /** 349*a1a3b679SAndreas Boehler * This method attempts to detect the base uri. 350*a1a3b679SAndreas Boehler * Only the PATH_INFO variable is considered. 351*a1a3b679SAndreas Boehler * 352*a1a3b679SAndreas Boehler * If this variable is not set, the root (/) is assumed. 353*a1a3b679SAndreas Boehler * 354*a1a3b679SAndreas Boehler * @return string 355*a1a3b679SAndreas Boehler */ 356*a1a3b679SAndreas Boehler function guessBaseUri() { 357*a1a3b679SAndreas Boehler 358*a1a3b679SAndreas Boehler $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO'); 359*a1a3b679SAndreas Boehler $uri = $this->httpRequest->getRawServerValue('REQUEST_URI'); 360*a1a3b679SAndreas Boehler 361*a1a3b679SAndreas Boehler // If PATH_INFO is found, we can assume it's accurate. 362*a1a3b679SAndreas Boehler if (!empty($pathInfo)) { 363*a1a3b679SAndreas Boehler 364*a1a3b679SAndreas Boehler // We need to make sure we ignore the QUERY_STRING part 365*a1a3b679SAndreas Boehler if ($pos = strpos($uri, '?')) 366*a1a3b679SAndreas Boehler $uri = substr($uri, 0, $pos); 367*a1a3b679SAndreas Boehler 368*a1a3b679SAndreas Boehler // PATH_INFO is only set for urls, such as: /example.php/path 369*a1a3b679SAndreas Boehler // in that case PATH_INFO contains '/path'. 370*a1a3b679SAndreas Boehler // Note that REQUEST_URI is percent encoded, while PATH_INFO is 371*a1a3b679SAndreas Boehler // not, Therefore they are only comparable if we first decode 372*a1a3b679SAndreas Boehler // REQUEST_INFO as well. 373*a1a3b679SAndreas Boehler $decodedUri = URLUtil::decodePath($uri); 374*a1a3b679SAndreas Boehler 375*a1a3b679SAndreas Boehler // A simple sanity check: 376*a1a3b679SAndreas Boehler if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) { 377*a1a3b679SAndreas Boehler $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo)); 378*a1a3b679SAndreas Boehler return rtrim($baseUri, '/') . '/'; 379*a1a3b679SAndreas Boehler } 380*a1a3b679SAndreas Boehler 381*a1a3b679SAndreas Boehler throw new Exception('The REQUEST_URI (' . $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.'); 382*a1a3b679SAndreas Boehler 383*a1a3b679SAndreas Boehler } 384*a1a3b679SAndreas Boehler 385*a1a3b679SAndreas Boehler // The last fallback is that we're just going to assume the server root. 386*a1a3b679SAndreas Boehler return '/'; 387*a1a3b679SAndreas Boehler 388*a1a3b679SAndreas Boehler } 389*a1a3b679SAndreas Boehler 390*a1a3b679SAndreas Boehler /** 391*a1a3b679SAndreas Boehler * Adds a plugin to the server 392*a1a3b679SAndreas Boehler * 393*a1a3b679SAndreas Boehler * For more information, console the documentation of Sabre\DAV\ServerPlugin 394*a1a3b679SAndreas Boehler * 395*a1a3b679SAndreas Boehler * @param ServerPlugin $plugin 396*a1a3b679SAndreas Boehler * @return void 397*a1a3b679SAndreas Boehler */ 398*a1a3b679SAndreas Boehler function addPlugin(ServerPlugin $plugin) { 399*a1a3b679SAndreas Boehler 400*a1a3b679SAndreas Boehler $this->plugins[$plugin->getPluginName()] = $plugin; 401*a1a3b679SAndreas Boehler $plugin->initialize($this); 402*a1a3b679SAndreas Boehler 403*a1a3b679SAndreas Boehler } 404*a1a3b679SAndreas Boehler 405*a1a3b679SAndreas Boehler /** 406*a1a3b679SAndreas Boehler * Returns an initialized plugin by it's name. 407*a1a3b679SAndreas Boehler * 408*a1a3b679SAndreas Boehler * This function returns null if the plugin was not found. 409*a1a3b679SAndreas Boehler * 410*a1a3b679SAndreas Boehler * @param string $name 411*a1a3b679SAndreas Boehler * @return ServerPlugin 412*a1a3b679SAndreas Boehler */ 413*a1a3b679SAndreas Boehler function getPlugin($name) { 414*a1a3b679SAndreas Boehler 415*a1a3b679SAndreas Boehler if (isset($this->plugins[$name])) 416*a1a3b679SAndreas Boehler return $this->plugins[$name]; 417*a1a3b679SAndreas Boehler 418*a1a3b679SAndreas Boehler return null; 419*a1a3b679SAndreas Boehler 420*a1a3b679SAndreas Boehler } 421*a1a3b679SAndreas Boehler 422*a1a3b679SAndreas Boehler /** 423*a1a3b679SAndreas Boehler * Returns all plugins 424*a1a3b679SAndreas Boehler * 425*a1a3b679SAndreas Boehler * @return array 426*a1a3b679SAndreas Boehler */ 427*a1a3b679SAndreas Boehler function getPlugins() { 428*a1a3b679SAndreas Boehler 429*a1a3b679SAndreas Boehler return $this->plugins; 430*a1a3b679SAndreas Boehler 431*a1a3b679SAndreas Boehler } 432*a1a3b679SAndreas Boehler 433*a1a3b679SAndreas Boehler /** 434*a1a3b679SAndreas Boehler * Handles a http request, and execute a method based on its name 435*a1a3b679SAndreas Boehler * 436*a1a3b679SAndreas Boehler * @param RequestInterface $request 437*a1a3b679SAndreas Boehler * @param ResponseInterface $response 438*a1a3b679SAndreas Boehler * @param $sendResponse Whether to send the HTTP response to the DAV client. 439*a1a3b679SAndreas Boehler * @return void 440*a1a3b679SAndreas Boehler */ 441*a1a3b679SAndreas Boehler function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true) { 442*a1a3b679SAndreas Boehler 443*a1a3b679SAndreas Boehler $method = $request->getMethod(); 444*a1a3b679SAndreas Boehler 445*a1a3b679SAndreas Boehler if (!$this->emit('beforeMethod:' . $method, [$request, $response])) return; 446*a1a3b679SAndreas Boehler if (!$this->emit('beforeMethod', [$request, $response])) return; 447*a1a3b679SAndreas Boehler 448*a1a3b679SAndreas Boehler if (self::$exposeVersion) { 449*a1a3b679SAndreas Boehler $response->setHeader('X-Sabre-Version', Version::VERSION); 450*a1a3b679SAndreas Boehler } 451*a1a3b679SAndreas Boehler 452*a1a3b679SAndreas Boehler $this->transactionType = strtolower($method); 453*a1a3b679SAndreas Boehler 454*a1a3b679SAndreas Boehler if (!$this->checkPreconditions($request, $response)) { 455*a1a3b679SAndreas Boehler $this->sapi->sendResponse($response); 456*a1a3b679SAndreas Boehler return; 457*a1a3b679SAndreas Boehler } 458*a1a3b679SAndreas Boehler 459*a1a3b679SAndreas Boehler if ($this->emit('method:' . $method, [$request, $response])) { 460*a1a3b679SAndreas Boehler if ($this->emit('method', [$request, $response])) { 461*a1a3b679SAndreas Boehler // Unsupported method 462*a1a3b679SAndreas Boehler throw new Exception\NotImplemented('There was no handler found for this "' . $method . '" method'); 463*a1a3b679SAndreas Boehler } 464*a1a3b679SAndreas Boehler } 465*a1a3b679SAndreas Boehler 466*a1a3b679SAndreas Boehler if (!$this->emit('afterMethod:' . $method, [$request, $response])) return; 467*a1a3b679SAndreas Boehler if (!$this->emit('afterMethod', [$request, $response])) return; 468*a1a3b679SAndreas Boehler 469*a1a3b679SAndreas Boehler if ($sendResponse) { 470*a1a3b679SAndreas Boehler $this->sapi->sendResponse($response); 471*a1a3b679SAndreas Boehler $this->emit('afterResponse', [$request, $response]); 472*a1a3b679SAndreas Boehler } 473*a1a3b679SAndreas Boehler 474*a1a3b679SAndreas Boehler } 475*a1a3b679SAndreas Boehler 476*a1a3b679SAndreas Boehler // {{{ HTTP/WebDAV protocol helpers 477*a1a3b679SAndreas Boehler 478*a1a3b679SAndreas Boehler /** 479*a1a3b679SAndreas Boehler * Returns an array with all the supported HTTP methods for a specific uri. 480*a1a3b679SAndreas Boehler * 481*a1a3b679SAndreas Boehler * @param string $path 482*a1a3b679SAndreas Boehler * @return array 483*a1a3b679SAndreas Boehler */ 484*a1a3b679SAndreas Boehler function getAllowedMethods($path) { 485*a1a3b679SAndreas Boehler 486*a1a3b679SAndreas Boehler $methods = [ 487*a1a3b679SAndreas Boehler 'OPTIONS', 488*a1a3b679SAndreas Boehler 'GET', 489*a1a3b679SAndreas Boehler 'HEAD', 490*a1a3b679SAndreas Boehler 'DELETE', 491*a1a3b679SAndreas Boehler 'PROPFIND', 492*a1a3b679SAndreas Boehler 'PUT', 493*a1a3b679SAndreas Boehler 'PROPPATCH', 494*a1a3b679SAndreas Boehler 'COPY', 495*a1a3b679SAndreas Boehler 'MOVE', 496*a1a3b679SAndreas Boehler 'REPORT' 497*a1a3b679SAndreas Boehler ]; 498*a1a3b679SAndreas Boehler 499*a1a3b679SAndreas Boehler // The MKCOL is only allowed on an unmapped uri 500*a1a3b679SAndreas Boehler try { 501*a1a3b679SAndreas Boehler $this->tree->getNodeForPath($path); 502*a1a3b679SAndreas Boehler } catch (Exception\NotFound $e) { 503*a1a3b679SAndreas Boehler $methods[] = 'MKCOL'; 504*a1a3b679SAndreas Boehler } 505*a1a3b679SAndreas Boehler 506*a1a3b679SAndreas Boehler // We're also checking if any of the plugins register any new methods 507*a1a3b679SAndreas Boehler foreach ($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($path)); 508*a1a3b679SAndreas Boehler array_unique($methods); 509*a1a3b679SAndreas Boehler 510*a1a3b679SAndreas Boehler return $methods; 511*a1a3b679SAndreas Boehler 512*a1a3b679SAndreas Boehler } 513*a1a3b679SAndreas Boehler 514*a1a3b679SAndreas Boehler /** 515*a1a3b679SAndreas Boehler * Gets the uri for the request, keeping the base uri into consideration 516*a1a3b679SAndreas Boehler * 517*a1a3b679SAndreas Boehler * @return string 518*a1a3b679SAndreas Boehler */ 519*a1a3b679SAndreas Boehler function getRequestUri() { 520*a1a3b679SAndreas Boehler 521*a1a3b679SAndreas Boehler return $this->calculateUri($this->httpRequest->getUrl()); 522*a1a3b679SAndreas Boehler 523*a1a3b679SAndreas Boehler } 524*a1a3b679SAndreas Boehler 525*a1a3b679SAndreas Boehler /** 526*a1a3b679SAndreas Boehler * Calculates the uri for a request, making sure that the base uri is stripped out 527*a1a3b679SAndreas Boehler * 528*a1a3b679SAndreas Boehler * @param string $uri 529*a1a3b679SAndreas Boehler * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri 530*a1a3b679SAndreas Boehler * @return string 531*a1a3b679SAndreas Boehler */ 532*a1a3b679SAndreas Boehler function calculateUri($uri) { 533*a1a3b679SAndreas Boehler 534*a1a3b679SAndreas Boehler if ($uri[0] != '/' && strpos($uri, '://')) { 535*a1a3b679SAndreas Boehler 536*a1a3b679SAndreas Boehler $uri = parse_url($uri, PHP_URL_PATH); 537*a1a3b679SAndreas Boehler 538*a1a3b679SAndreas Boehler } 539*a1a3b679SAndreas Boehler 540*a1a3b679SAndreas Boehler $uri = Uri\normalize(str_replace('//', '/', $uri)); 541*a1a3b679SAndreas Boehler $baseUri = Uri\normalize($this->getBaseUri()); 542*a1a3b679SAndreas Boehler 543*a1a3b679SAndreas Boehler if (strpos($uri, $baseUri) === 0) { 544*a1a3b679SAndreas Boehler 545*a1a3b679SAndreas Boehler return trim(URLUtil::decodePath(substr($uri, strlen($baseUri))), '/'); 546*a1a3b679SAndreas Boehler 547*a1a3b679SAndreas Boehler // A special case, if the baseUri was accessed without a trailing 548*a1a3b679SAndreas Boehler // slash, we'll accept it as well. 549*a1a3b679SAndreas Boehler } elseif ($uri . '/' === $baseUri) { 550*a1a3b679SAndreas Boehler 551*a1a3b679SAndreas Boehler return ''; 552*a1a3b679SAndreas Boehler 553*a1a3b679SAndreas Boehler } else { 554*a1a3b679SAndreas Boehler 555*a1a3b679SAndreas Boehler throw new Exception\Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')'); 556*a1a3b679SAndreas Boehler 557*a1a3b679SAndreas Boehler } 558*a1a3b679SAndreas Boehler 559*a1a3b679SAndreas Boehler } 560*a1a3b679SAndreas Boehler 561*a1a3b679SAndreas Boehler /** 562*a1a3b679SAndreas Boehler * Returns the HTTP depth header 563*a1a3b679SAndreas Boehler * 564*a1a3b679SAndreas Boehler * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object 565*a1a3b679SAndreas Boehler * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent 566*a1a3b679SAndreas Boehler * 567*a1a3b679SAndreas Boehler * @param mixed $default 568*a1a3b679SAndreas Boehler * @return int 569*a1a3b679SAndreas Boehler */ 570*a1a3b679SAndreas Boehler function getHTTPDepth($default = self::DEPTH_INFINITY) { 571*a1a3b679SAndreas Boehler 572*a1a3b679SAndreas Boehler // If its not set, we'll grab the default 573*a1a3b679SAndreas Boehler $depth = $this->httpRequest->getHeader('Depth'); 574*a1a3b679SAndreas Boehler 575*a1a3b679SAndreas Boehler if (is_null($depth)) return $default; 576*a1a3b679SAndreas Boehler 577*a1a3b679SAndreas Boehler if ($depth == 'infinity') return self::DEPTH_INFINITY; 578*a1a3b679SAndreas Boehler 579*a1a3b679SAndreas Boehler 580*a1a3b679SAndreas Boehler // If its an unknown value. we'll grab the default 581*a1a3b679SAndreas Boehler if (!ctype_digit($depth)) return $default; 582*a1a3b679SAndreas Boehler 583*a1a3b679SAndreas Boehler return (int)$depth; 584*a1a3b679SAndreas Boehler 585*a1a3b679SAndreas Boehler } 586*a1a3b679SAndreas Boehler 587*a1a3b679SAndreas Boehler /** 588*a1a3b679SAndreas Boehler * Returns the HTTP range header 589*a1a3b679SAndreas Boehler * 590*a1a3b679SAndreas Boehler * This method returns null if there is no well-formed HTTP range request 591*a1a3b679SAndreas Boehler * header or array($start, $end). 592*a1a3b679SAndreas Boehler * 593*a1a3b679SAndreas Boehler * The first number is the offset of the first byte in the range. 594*a1a3b679SAndreas Boehler * The second number is the offset of the last byte in the range. 595*a1a3b679SAndreas Boehler * 596*a1a3b679SAndreas Boehler * If the second offset is null, it should be treated as the offset of the last byte of the entity 597*a1a3b679SAndreas Boehler * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity 598*a1a3b679SAndreas Boehler * 599*a1a3b679SAndreas Boehler * @return array|null 600*a1a3b679SAndreas Boehler */ 601*a1a3b679SAndreas Boehler function getHTTPRange() { 602*a1a3b679SAndreas Boehler 603*a1a3b679SAndreas Boehler $range = $this->httpRequest->getHeader('range'); 604*a1a3b679SAndreas Boehler if (is_null($range)) return null; 605*a1a3b679SAndreas Boehler 606*a1a3b679SAndreas Boehler // Matching "Range: bytes=1234-5678: both numbers are optional 607*a1a3b679SAndreas Boehler 608*a1a3b679SAndreas Boehler if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) return null; 609*a1a3b679SAndreas Boehler 610*a1a3b679SAndreas Boehler if ($matches[1] === '' && $matches[2] === '') return null; 611*a1a3b679SAndreas Boehler 612*a1a3b679SAndreas Boehler return [ 613*a1a3b679SAndreas Boehler $matches[1] !== '' ? $matches[1] : null, 614*a1a3b679SAndreas Boehler $matches[2] !== '' ? $matches[2] : null, 615*a1a3b679SAndreas Boehler ]; 616*a1a3b679SAndreas Boehler 617*a1a3b679SAndreas Boehler } 618*a1a3b679SAndreas Boehler 619*a1a3b679SAndreas Boehler /** 620*a1a3b679SAndreas Boehler * Returns the HTTP Prefer header information. 621*a1a3b679SAndreas Boehler * 622*a1a3b679SAndreas Boehler * The prefer header is defined in: 623*a1a3b679SAndreas Boehler * http://tools.ietf.org/html/draft-snell-http-prefer-14 624*a1a3b679SAndreas Boehler * 625*a1a3b679SAndreas Boehler * This method will return an array with options. 626*a1a3b679SAndreas Boehler * 627*a1a3b679SAndreas Boehler * Currently, the following options may be returned: 628*a1a3b679SAndreas Boehler * [ 629*a1a3b679SAndreas Boehler * 'return-asynch' => true, 630*a1a3b679SAndreas Boehler * 'return-minimal' => true, 631*a1a3b679SAndreas Boehler * 'return-representation' => true, 632*a1a3b679SAndreas Boehler * 'wait' => 30, 633*a1a3b679SAndreas Boehler * 'strict' => true, 634*a1a3b679SAndreas Boehler * 'lenient' => true, 635*a1a3b679SAndreas Boehler * ] 636*a1a3b679SAndreas Boehler * 637*a1a3b679SAndreas Boehler * This method also supports the Brief header, and will also return 638*a1a3b679SAndreas Boehler * 'return-minimal' if the brief header was set to 't'. 639*a1a3b679SAndreas Boehler * 640*a1a3b679SAndreas Boehler * For the boolean options, false will be returned if the headers are not 641*a1a3b679SAndreas Boehler * specified. For the integer options it will be 'null'. 642*a1a3b679SAndreas Boehler * 643*a1a3b679SAndreas Boehler * @return array 644*a1a3b679SAndreas Boehler */ 645*a1a3b679SAndreas Boehler function getHTTPPrefer() { 646*a1a3b679SAndreas Boehler 647*a1a3b679SAndreas Boehler $result = [ 648*a1a3b679SAndreas Boehler // can be true or false 649*a1a3b679SAndreas Boehler 'respond-async' => false, 650*a1a3b679SAndreas Boehler // Could be set to 'representation' or 'minimal'. 651*a1a3b679SAndreas Boehler 'return' => null, 652*a1a3b679SAndreas Boehler // Used as a timeout, is usually a number. 653*a1a3b679SAndreas Boehler 'wait' => null, 654*a1a3b679SAndreas Boehler // can be 'strict' or 'lenient'. 655*a1a3b679SAndreas Boehler 'handling' => false, 656*a1a3b679SAndreas Boehler ]; 657*a1a3b679SAndreas Boehler 658*a1a3b679SAndreas Boehler if ($prefer = $this->httpRequest->getHeader('Prefer')) { 659*a1a3b679SAndreas Boehler 660*a1a3b679SAndreas Boehler $result = array_merge( 661*a1a3b679SAndreas Boehler $result, 662*a1a3b679SAndreas Boehler \Sabre\HTTP\parsePrefer($prefer) 663*a1a3b679SAndreas Boehler ); 664*a1a3b679SAndreas Boehler 665*a1a3b679SAndreas Boehler } elseif ($this->httpRequest->getHeader('Brief') == 't') { 666*a1a3b679SAndreas Boehler $result['return'] = 'minimal'; 667*a1a3b679SAndreas Boehler } 668*a1a3b679SAndreas Boehler 669*a1a3b679SAndreas Boehler return $result; 670*a1a3b679SAndreas Boehler 671*a1a3b679SAndreas Boehler } 672*a1a3b679SAndreas Boehler 673*a1a3b679SAndreas Boehler 674*a1a3b679SAndreas Boehler /** 675*a1a3b679SAndreas Boehler * Returns information about Copy and Move requests 676*a1a3b679SAndreas Boehler * 677*a1a3b679SAndreas Boehler * This function is created to help getting information about the source and the destination for the 678*a1a3b679SAndreas Boehler * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions 679*a1a3b679SAndreas Boehler * 680*a1a3b679SAndreas Boehler * The returned value is an array with the following keys: 681*a1a3b679SAndreas Boehler * * destination - Destination path 682*a1a3b679SAndreas Boehler * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten) 683*a1a3b679SAndreas Boehler * 684*a1a3b679SAndreas Boehler * @param RequestInterface $request 685*a1a3b679SAndreas Boehler * @throws Exception\BadRequest upon missing or broken request headers 686*a1a3b679SAndreas Boehler * @throws Exception\UnsupportedMediaType when trying to copy into a 687*a1a3b679SAndreas Boehler * non-collection. 688*a1a3b679SAndreas Boehler * @throws Exception\PreconditionFailed If overwrite is set to false, but 689*a1a3b679SAndreas Boehler * the destination exists. 690*a1a3b679SAndreas Boehler * @throws Exception\Forbidden when source and destination paths are 691*a1a3b679SAndreas Boehler * identical. 692*a1a3b679SAndreas Boehler * @throws Exception\Conflict When trying to copy a node into its own 693*a1a3b679SAndreas Boehler * subtree. 694*a1a3b679SAndreas Boehler * @return array 695*a1a3b679SAndreas Boehler */ 696*a1a3b679SAndreas Boehler function getCopyAndMoveInfo(RequestInterface $request) { 697*a1a3b679SAndreas Boehler 698*a1a3b679SAndreas Boehler // Collecting the relevant HTTP headers 699*a1a3b679SAndreas Boehler if (!$request->getHeader('Destination')) throw new Exception\BadRequest('The destination header was not supplied'); 700*a1a3b679SAndreas Boehler $destination = $this->calculateUri($request->getHeader('Destination')); 701*a1a3b679SAndreas Boehler $overwrite = $request->getHeader('Overwrite'); 702*a1a3b679SAndreas Boehler if (!$overwrite) $overwrite = 'T'; 703*a1a3b679SAndreas Boehler if (strtoupper($overwrite) == 'T') $overwrite = true; 704*a1a3b679SAndreas Boehler elseif (strtoupper($overwrite) == 'F') $overwrite = false; 705*a1a3b679SAndreas Boehler // We need to throw a bad request exception, if the header was invalid 706*a1a3b679SAndreas Boehler else throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); 707*a1a3b679SAndreas Boehler 708*a1a3b679SAndreas Boehler list($destinationDir) = URLUtil::splitPath($destination); 709*a1a3b679SAndreas Boehler 710*a1a3b679SAndreas Boehler try { 711*a1a3b679SAndreas Boehler $destinationParent = $this->tree->getNodeForPath($destinationDir); 712*a1a3b679SAndreas Boehler if (!($destinationParent instanceof ICollection)) throw new Exception\UnsupportedMediaType('The destination node is not a collection'); 713*a1a3b679SAndreas Boehler } catch (Exception\NotFound $e) { 714*a1a3b679SAndreas Boehler 715*a1a3b679SAndreas Boehler // If the destination parent node is not found, we throw a 409 716*a1a3b679SAndreas Boehler throw new Exception\Conflict('The destination node is not found'); 717*a1a3b679SAndreas Boehler } 718*a1a3b679SAndreas Boehler 719*a1a3b679SAndreas Boehler try { 720*a1a3b679SAndreas Boehler 721*a1a3b679SAndreas Boehler $destinationNode = $this->tree->getNodeForPath($destination); 722*a1a3b679SAndreas Boehler 723*a1a3b679SAndreas Boehler // If this succeeded, it means the destination already exists 724*a1a3b679SAndreas Boehler // we'll need to throw precondition failed in case overwrite is false 725*a1a3b679SAndreas Boehler if (!$overwrite) throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); 726*a1a3b679SAndreas Boehler 727*a1a3b679SAndreas Boehler } catch (Exception\NotFound $e) { 728*a1a3b679SAndreas Boehler 729*a1a3b679SAndreas Boehler // Destination didn't exist, we're all good 730*a1a3b679SAndreas Boehler $destinationNode = false; 731*a1a3b679SAndreas Boehler 732*a1a3b679SAndreas Boehler } 733*a1a3b679SAndreas Boehler 734*a1a3b679SAndreas Boehler $requestPath = $request->getPath(); 735*a1a3b679SAndreas Boehler if ($destination === $requestPath) { 736*a1a3b679SAndreas Boehler throw new Exception\Forbidden('Source and destination uri are identical.'); 737*a1a3b679SAndreas Boehler } 738*a1a3b679SAndreas Boehler if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath . '/') { 739*a1a3b679SAndreas Boehler throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.'); 740*a1a3b679SAndreas Boehler } 741*a1a3b679SAndreas Boehler 742*a1a3b679SAndreas Boehler // These are the three relevant properties we need to return 743*a1a3b679SAndreas Boehler return [ 744*a1a3b679SAndreas Boehler 'destination' => $destination, 745*a1a3b679SAndreas Boehler 'destinationExists' => !!$destinationNode, 746*a1a3b679SAndreas Boehler 'destinationNode' => $destinationNode, 747*a1a3b679SAndreas Boehler ]; 748*a1a3b679SAndreas Boehler 749*a1a3b679SAndreas Boehler } 750*a1a3b679SAndreas Boehler 751*a1a3b679SAndreas Boehler /** 752*a1a3b679SAndreas Boehler * Returns a list of properties for a path 753*a1a3b679SAndreas Boehler * 754*a1a3b679SAndreas Boehler * This is a simplified version getPropertiesForPath. 755*a1a3b679SAndreas Boehler * if you aren't interested in status codes, but you just 756*a1a3b679SAndreas Boehler * want to have a flat list of properties. Use this method. 757*a1a3b679SAndreas Boehler * 758*a1a3b679SAndreas Boehler * @param string $path 759*a1a3b679SAndreas Boehler * @param array $propertyNames 760*a1a3b679SAndreas Boehler */ 761*a1a3b679SAndreas Boehler function getProperties($path, $propertyNames) { 762*a1a3b679SAndreas Boehler 763*a1a3b679SAndreas Boehler $result = $this->getPropertiesForPath($path, $propertyNames, 0); 764*a1a3b679SAndreas Boehler return $result[0][200]; 765*a1a3b679SAndreas Boehler 766*a1a3b679SAndreas Boehler } 767*a1a3b679SAndreas Boehler 768*a1a3b679SAndreas Boehler /** 769*a1a3b679SAndreas Boehler * A kid-friendly way to fetch properties for a node's children. 770*a1a3b679SAndreas Boehler * 771*a1a3b679SAndreas Boehler * The returned array will be indexed by the path of the of child node. 772*a1a3b679SAndreas Boehler * Only properties that are actually found will be returned. 773*a1a3b679SAndreas Boehler * 774*a1a3b679SAndreas Boehler * The parent node will not be returned. 775*a1a3b679SAndreas Boehler * 776*a1a3b679SAndreas Boehler * @param string $path 777*a1a3b679SAndreas Boehler * @param array $propertyNames 778*a1a3b679SAndreas Boehler * @return array 779*a1a3b679SAndreas Boehler */ 780*a1a3b679SAndreas Boehler function getPropertiesForChildren($path, $propertyNames) { 781*a1a3b679SAndreas Boehler 782*a1a3b679SAndreas Boehler $result = []; 783*a1a3b679SAndreas Boehler foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { 784*a1a3b679SAndreas Boehler 785*a1a3b679SAndreas Boehler // Skipping the parent path 786*a1a3b679SAndreas Boehler if ($k === 0) continue; 787*a1a3b679SAndreas Boehler 788*a1a3b679SAndreas Boehler $result[$row['href']] = $row[200]; 789*a1a3b679SAndreas Boehler 790*a1a3b679SAndreas Boehler } 791*a1a3b679SAndreas Boehler return $result; 792*a1a3b679SAndreas Boehler 793*a1a3b679SAndreas Boehler } 794*a1a3b679SAndreas Boehler 795*a1a3b679SAndreas Boehler /** 796*a1a3b679SAndreas Boehler * Returns a list of HTTP headers for a particular resource 797*a1a3b679SAndreas Boehler * 798*a1a3b679SAndreas Boehler * The generated http headers are based on properties provided by the 799*a1a3b679SAndreas Boehler * resource. The method basically provides a simple mapping between 800*a1a3b679SAndreas Boehler * DAV property and HTTP header. 801*a1a3b679SAndreas Boehler * 802*a1a3b679SAndreas Boehler * The headers are intended to be used for HEAD and GET requests. 803*a1a3b679SAndreas Boehler * 804*a1a3b679SAndreas Boehler * @param string $path 805*a1a3b679SAndreas Boehler * @return array 806*a1a3b679SAndreas Boehler */ 807*a1a3b679SAndreas Boehler function getHTTPHeaders($path) { 808*a1a3b679SAndreas Boehler 809*a1a3b679SAndreas Boehler $propertyMap = [ 810*a1a3b679SAndreas Boehler '{DAV:}getcontenttype' => 'Content-Type', 811*a1a3b679SAndreas Boehler '{DAV:}getcontentlength' => 'Content-Length', 812*a1a3b679SAndreas Boehler '{DAV:}getlastmodified' => 'Last-Modified', 813*a1a3b679SAndreas Boehler '{DAV:}getetag' => 'ETag', 814*a1a3b679SAndreas Boehler ]; 815*a1a3b679SAndreas Boehler 816*a1a3b679SAndreas Boehler $properties = $this->getProperties($path, array_keys($propertyMap)); 817*a1a3b679SAndreas Boehler 818*a1a3b679SAndreas Boehler $headers = []; 819*a1a3b679SAndreas Boehler foreach ($propertyMap as $property => $header) { 820*a1a3b679SAndreas Boehler if (!isset($properties[$property])) continue; 821*a1a3b679SAndreas Boehler 822*a1a3b679SAndreas Boehler if (is_scalar($properties[$property])) { 823*a1a3b679SAndreas Boehler $headers[$header] = $properties[$property]; 824*a1a3b679SAndreas Boehler 825*a1a3b679SAndreas Boehler // GetLastModified gets special cased 826*a1a3b679SAndreas Boehler } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) { 827*a1a3b679SAndreas Boehler $headers[$header] = HTTP\Util::toHTTPDate($properties[$property]->getTime()); 828*a1a3b679SAndreas Boehler } 829*a1a3b679SAndreas Boehler 830*a1a3b679SAndreas Boehler } 831*a1a3b679SAndreas Boehler 832*a1a3b679SAndreas Boehler return $headers; 833*a1a3b679SAndreas Boehler 834*a1a3b679SAndreas Boehler } 835*a1a3b679SAndreas Boehler 836*a1a3b679SAndreas Boehler /** 837*a1a3b679SAndreas Boehler * Small helper to support PROPFIND with DEPTH_INFINITY. 838*a1a3b679SAndreas Boehler * 839*a1a3b679SAndreas Boehler * @param array[] $propFindRequests 840*a1a3b679SAndreas Boehler * @param PropFind $propFind 841*a1a3b679SAndreas Boehler * @return void 842*a1a3b679SAndreas Boehler */ 843*a1a3b679SAndreas Boehler private function addPathNodesRecursively(&$propFindRequests, PropFind $propFind) { 844*a1a3b679SAndreas Boehler 845*a1a3b679SAndreas Boehler $newDepth = $propFind->getDepth(); 846*a1a3b679SAndreas Boehler $path = $propFind->getPath(); 847*a1a3b679SAndreas Boehler 848*a1a3b679SAndreas Boehler if ($newDepth !== self::DEPTH_INFINITY) { 849*a1a3b679SAndreas Boehler $newDepth--; 850*a1a3b679SAndreas Boehler } 851*a1a3b679SAndreas Boehler 852*a1a3b679SAndreas Boehler foreach ($this->tree->getChildren($path) as $childNode) { 853*a1a3b679SAndreas Boehler $subPropFind = clone $propFind; 854*a1a3b679SAndreas Boehler $subPropFind->setDepth($newDepth); 855*a1a3b679SAndreas Boehler if ($path !== '') { 856*a1a3b679SAndreas Boehler $subPath = $path . '/' . $childNode->getName(); 857*a1a3b679SAndreas Boehler } else { 858*a1a3b679SAndreas Boehler $subPath = $childNode->getName(); 859*a1a3b679SAndreas Boehler } 860*a1a3b679SAndreas Boehler $subPropFind->setPath($subPath); 861*a1a3b679SAndreas Boehler 862*a1a3b679SAndreas Boehler $propFindRequests[] = [ 863*a1a3b679SAndreas Boehler $subPropFind, 864*a1a3b679SAndreas Boehler $childNode 865*a1a3b679SAndreas Boehler ]; 866*a1a3b679SAndreas Boehler 867*a1a3b679SAndreas Boehler if (($newDepth === self::DEPTH_INFINITY || $newDepth >= 1) && $childNode instanceof ICollection) { 868*a1a3b679SAndreas Boehler $this->addPathNodesRecursively($propFindRequests, $subPropFind); 869*a1a3b679SAndreas Boehler } 870*a1a3b679SAndreas Boehler 871*a1a3b679SAndreas Boehler } 872*a1a3b679SAndreas Boehler } 873*a1a3b679SAndreas Boehler 874*a1a3b679SAndreas Boehler /** 875*a1a3b679SAndreas Boehler * Returns a list of properties for a given path 876*a1a3b679SAndreas Boehler * 877*a1a3b679SAndreas Boehler * The path that should be supplied should have the baseUrl stripped out 878*a1a3b679SAndreas Boehler * The list of properties should be supplied in Clark notation. If the list is empty 879*a1a3b679SAndreas Boehler * 'allprops' is assumed. 880*a1a3b679SAndreas Boehler * 881*a1a3b679SAndreas Boehler * If a depth of 1 is requested child elements will also be returned. 882*a1a3b679SAndreas Boehler * 883*a1a3b679SAndreas Boehler * @param string $path 884*a1a3b679SAndreas Boehler * @param array $propertyNames 885*a1a3b679SAndreas Boehler * @param int $depth 886*a1a3b679SAndreas Boehler * @return array 887*a1a3b679SAndreas Boehler */ 888*a1a3b679SAndreas Boehler function getPropertiesForPath($path, $propertyNames = [], $depth = 0) { 889*a1a3b679SAndreas Boehler 890*a1a3b679SAndreas Boehler // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled 891*a1a3b679SAndreas Boehler if (!$this->enablePropfindDepthInfinity && $depth != 0) $depth = 1; 892*a1a3b679SAndreas Boehler 893*a1a3b679SAndreas Boehler $path = trim($path, '/'); 894*a1a3b679SAndreas Boehler 895*a1a3b679SAndreas Boehler $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; 896*a1a3b679SAndreas Boehler $propFind = new PropFind($path, (array)$propertyNames, $depth, $propFindType); 897*a1a3b679SAndreas Boehler 898*a1a3b679SAndreas Boehler $parentNode = $this->tree->getNodeForPath($path); 899*a1a3b679SAndreas Boehler 900*a1a3b679SAndreas Boehler $propFindRequests = [[ 901*a1a3b679SAndreas Boehler $propFind, 902*a1a3b679SAndreas Boehler $parentNode 903*a1a3b679SAndreas Boehler ]]; 904*a1a3b679SAndreas Boehler 905*a1a3b679SAndreas Boehler if (($depth > 0 || $depth === self::DEPTH_INFINITY) && $parentNode instanceof ICollection) { 906*a1a3b679SAndreas Boehler $this->addPathNodesRecursively($propFindRequests, $propFind); 907*a1a3b679SAndreas Boehler } 908*a1a3b679SAndreas Boehler 909*a1a3b679SAndreas Boehler $returnPropertyList = []; 910*a1a3b679SAndreas Boehler 911*a1a3b679SAndreas Boehler foreach ($propFindRequests as $propFindRequest) { 912*a1a3b679SAndreas Boehler 913*a1a3b679SAndreas Boehler list($propFind, $node) = $propFindRequest; 914*a1a3b679SAndreas Boehler $r = $this->getPropertiesByNode($propFind, $node); 915*a1a3b679SAndreas Boehler if ($r) { 916*a1a3b679SAndreas Boehler $result = $propFind->getResultForMultiStatus(); 917*a1a3b679SAndreas Boehler $result['href'] = $propFind->getPath(); 918*a1a3b679SAndreas Boehler 919*a1a3b679SAndreas Boehler // WebDAV recommends adding a slash to the path, if the path is 920*a1a3b679SAndreas Boehler // a collection. 921*a1a3b679SAndreas Boehler // Furthermore, iCal also demands this to be the case for 922*a1a3b679SAndreas Boehler // principals. This is non-standard, but we support it. 923*a1a3b679SAndreas Boehler $resourceType = $this->getResourceTypeForNode($node); 924*a1a3b679SAndreas Boehler if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { 925*a1a3b679SAndreas Boehler $result['href'] .= '/'; 926*a1a3b679SAndreas Boehler } 927*a1a3b679SAndreas Boehler $returnPropertyList[] = $result; 928*a1a3b679SAndreas Boehler } 929*a1a3b679SAndreas Boehler 930*a1a3b679SAndreas Boehler } 931*a1a3b679SAndreas Boehler 932*a1a3b679SAndreas Boehler return $returnPropertyList; 933*a1a3b679SAndreas Boehler 934*a1a3b679SAndreas Boehler } 935*a1a3b679SAndreas Boehler 936*a1a3b679SAndreas Boehler /** 937*a1a3b679SAndreas Boehler * Returns a list of properties for a list of paths. 938*a1a3b679SAndreas Boehler * 939*a1a3b679SAndreas Boehler * The path that should be supplied should have the baseUrl stripped out 940*a1a3b679SAndreas Boehler * The list of properties should be supplied in Clark notation. If the list is empty 941*a1a3b679SAndreas Boehler * 'allprops' is assumed. 942*a1a3b679SAndreas Boehler * 943*a1a3b679SAndreas Boehler * The result is returned as an array, with paths for it's keys. 944*a1a3b679SAndreas Boehler * The result may be returned out of order. 945*a1a3b679SAndreas Boehler * 946*a1a3b679SAndreas Boehler * @param array $paths 947*a1a3b679SAndreas Boehler * @param array $propertyNames 948*a1a3b679SAndreas Boehler * @return array 949*a1a3b679SAndreas Boehler */ 950*a1a3b679SAndreas Boehler function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) { 951*a1a3b679SAndreas Boehler 952*a1a3b679SAndreas Boehler $result = [ 953*a1a3b679SAndreas Boehler ]; 954*a1a3b679SAndreas Boehler 955*a1a3b679SAndreas Boehler $nodes = $this->tree->getMultipleNodes($paths); 956*a1a3b679SAndreas Boehler 957*a1a3b679SAndreas Boehler foreach ($nodes as $path => $node) { 958*a1a3b679SAndreas Boehler 959*a1a3b679SAndreas Boehler $propFind = new PropFind($path, $propertyNames); 960*a1a3b679SAndreas Boehler $r = $this->getPropertiesByNode($propFind, $node); 961*a1a3b679SAndreas Boehler if ($r) { 962*a1a3b679SAndreas Boehler $result[$path] = $propFind->getResultForMultiStatus(); 963*a1a3b679SAndreas Boehler $result[$path]['href'] = $path; 964*a1a3b679SAndreas Boehler 965*a1a3b679SAndreas Boehler $resourceType = $this->getResourceTypeForNode($node); 966*a1a3b679SAndreas Boehler if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { 967*a1a3b679SAndreas Boehler $result[$path]['href'] .= '/'; 968*a1a3b679SAndreas Boehler } 969*a1a3b679SAndreas Boehler } 970*a1a3b679SAndreas Boehler 971*a1a3b679SAndreas Boehler } 972*a1a3b679SAndreas Boehler 973*a1a3b679SAndreas Boehler return $result; 974*a1a3b679SAndreas Boehler 975*a1a3b679SAndreas Boehler } 976*a1a3b679SAndreas Boehler 977*a1a3b679SAndreas Boehler 978*a1a3b679SAndreas Boehler /** 979*a1a3b679SAndreas Boehler * Determines all properties for a node. 980*a1a3b679SAndreas Boehler * 981*a1a3b679SAndreas Boehler * This method tries to grab all properties for a node. This method is used 982*a1a3b679SAndreas Boehler * internally getPropertiesForPath and a few others. 983*a1a3b679SAndreas Boehler * 984*a1a3b679SAndreas Boehler * It could be useful to call this, if you already have an instance of your 985*a1a3b679SAndreas Boehler * target node and simply want to run through the system to get a correct 986*a1a3b679SAndreas Boehler * list of properties. 987*a1a3b679SAndreas Boehler * 988*a1a3b679SAndreas Boehler * @param PropFind $propFind 989*a1a3b679SAndreas Boehler * @param INode $node 990*a1a3b679SAndreas Boehler * @return bool 991*a1a3b679SAndreas Boehler */ 992*a1a3b679SAndreas Boehler function getPropertiesByNode(PropFind $propFind, INode $node) { 993*a1a3b679SAndreas Boehler 994*a1a3b679SAndreas Boehler return $this->emit('propFind', [$propFind, $node]); 995*a1a3b679SAndreas Boehler 996*a1a3b679SAndreas Boehler } 997*a1a3b679SAndreas Boehler 998*a1a3b679SAndreas Boehler /** 999*a1a3b679SAndreas Boehler * This method is invoked by sub-systems creating a new file. 1000*a1a3b679SAndreas Boehler * 1001*a1a3b679SAndreas Boehler * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). 1002*a1a3b679SAndreas Boehler * It was important to get this done through a centralized function, 1003*a1a3b679SAndreas Boehler * allowing plugins to intercept this using the beforeCreateFile event. 1004*a1a3b679SAndreas Boehler * 1005*a1a3b679SAndreas Boehler * This method will return true if the file was actually created 1006*a1a3b679SAndreas Boehler * 1007*a1a3b679SAndreas Boehler * @param string $uri 1008*a1a3b679SAndreas Boehler * @param resource $data 1009*a1a3b679SAndreas Boehler * @param string $etag 1010*a1a3b679SAndreas Boehler * @return bool 1011*a1a3b679SAndreas Boehler */ 1012*a1a3b679SAndreas Boehler function createFile($uri, $data, &$etag = null) { 1013*a1a3b679SAndreas Boehler 1014*a1a3b679SAndreas Boehler list($dir, $name) = URLUtil::splitPath($uri); 1015*a1a3b679SAndreas Boehler 1016*a1a3b679SAndreas Boehler if (!$this->emit('beforeBind', [$uri])) return false; 1017*a1a3b679SAndreas Boehler 1018*a1a3b679SAndreas Boehler $parent = $this->tree->getNodeForPath($dir); 1019*a1a3b679SAndreas Boehler if (!$parent instanceof ICollection) { 1020*a1a3b679SAndreas Boehler throw new Exception\Conflict('Files can only be created as children of collections'); 1021*a1a3b679SAndreas Boehler } 1022*a1a3b679SAndreas Boehler 1023*a1a3b679SAndreas Boehler // It is possible for an event handler to modify the content of the 1024*a1a3b679SAndreas Boehler // body, before it gets written. If this is the case, $modified 1025*a1a3b679SAndreas Boehler // should be set to true. 1026*a1a3b679SAndreas Boehler // 1027*a1a3b679SAndreas Boehler // If $modified is true, we must not send back an ETag. 1028*a1a3b679SAndreas Boehler $modified = false; 1029*a1a3b679SAndreas Boehler if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) return false; 1030*a1a3b679SAndreas Boehler 1031*a1a3b679SAndreas Boehler $etag = $parent->createFile($name, $data); 1032*a1a3b679SAndreas Boehler 1033*a1a3b679SAndreas Boehler if ($modified) $etag = null; 1034*a1a3b679SAndreas Boehler 1035*a1a3b679SAndreas Boehler $this->tree->markDirty($dir . '/' . $name); 1036*a1a3b679SAndreas Boehler 1037*a1a3b679SAndreas Boehler $this->emit('afterBind', [$uri]); 1038*a1a3b679SAndreas Boehler $this->emit('afterCreateFile', [$uri, $parent]); 1039*a1a3b679SAndreas Boehler 1040*a1a3b679SAndreas Boehler return true; 1041*a1a3b679SAndreas Boehler } 1042*a1a3b679SAndreas Boehler 1043*a1a3b679SAndreas Boehler /** 1044*a1a3b679SAndreas Boehler * This method is invoked by sub-systems updating a file. 1045*a1a3b679SAndreas Boehler * 1046*a1a3b679SAndreas Boehler * This method will return true if the file was actually updated 1047*a1a3b679SAndreas Boehler * 1048*a1a3b679SAndreas Boehler * @param string $uri 1049*a1a3b679SAndreas Boehler * @param resource $data 1050*a1a3b679SAndreas Boehler * @param string $etag 1051*a1a3b679SAndreas Boehler * @return bool 1052*a1a3b679SAndreas Boehler */ 1053*a1a3b679SAndreas Boehler function updateFile($uri, $data, &$etag = null) { 1054*a1a3b679SAndreas Boehler 1055*a1a3b679SAndreas Boehler $node = $this->tree->getNodeForPath($uri); 1056*a1a3b679SAndreas Boehler 1057*a1a3b679SAndreas Boehler // It is possible for an event handler to modify the content of the 1058*a1a3b679SAndreas Boehler // body, before it gets written. If this is the case, $modified 1059*a1a3b679SAndreas Boehler // should be set to true. 1060*a1a3b679SAndreas Boehler // 1061*a1a3b679SAndreas Boehler // If $modified is true, we must not send back an ETag. 1062*a1a3b679SAndreas Boehler $modified = false; 1063*a1a3b679SAndreas Boehler if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) return false; 1064*a1a3b679SAndreas Boehler 1065*a1a3b679SAndreas Boehler $etag = $node->put($data); 1066*a1a3b679SAndreas Boehler if ($modified) $etag = null; 1067*a1a3b679SAndreas Boehler $this->emit('afterWriteContent', [$uri, $node]); 1068*a1a3b679SAndreas Boehler 1069*a1a3b679SAndreas Boehler return true; 1070*a1a3b679SAndreas Boehler } 1071*a1a3b679SAndreas Boehler 1072*a1a3b679SAndreas Boehler 1073*a1a3b679SAndreas Boehler 1074*a1a3b679SAndreas Boehler /** 1075*a1a3b679SAndreas Boehler * This method is invoked by sub-systems creating a new directory. 1076*a1a3b679SAndreas Boehler * 1077*a1a3b679SAndreas Boehler * @param string $uri 1078*a1a3b679SAndreas Boehler * @return void 1079*a1a3b679SAndreas Boehler */ 1080*a1a3b679SAndreas Boehler function createDirectory($uri) { 1081*a1a3b679SAndreas Boehler 1082*a1a3b679SAndreas Boehler $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); 1083*a1a3b679SAndreas Boehler 1084*a1a3b679SAndreas Boehler } 1085*a1a3b679SAndreas Boehler 1086*a1a3b679SAndreas Boehler /** 1087*a1a3b679SAndreas Boehler * Use this method to create a new collection 1088*a1a3b679SAndreas Boehler * 1089*a1a3b679SAndreas Boehler * @param string $uri The new uri 1090*a1a3b679SAndreas Boehler * @param MkCol $mkCol 1091*a1a3b679SAndreas Boehler * @return array|null 1092*a1a3b679SAndreas Boehler */ 1093*a1a3b679SAndreas Boehler function createCollection($uri, MkCol $mkCol) { 1094*a1a3b679SAndreas Boehler 1095*a1a3b679SAndreas Boehler list($parentUri, $newName) = URLUtil::splitPath($uri); 1096*a1a3b679SAndreas Boehler 1097*a1a3b679SAndreas Boehler // Making sure the parent exists 1098*a1a3b679SAndreas Boehler try { 1099*a1a3b679SAndreas Boehler $parent = $this->tree->getNodeForPath($parentUri); 1100*a1a3b679SAndreas Boehler 1101*a1a3b679SAndreas Boehler } catch (Exception\NotFound $e) { 1102*a1a3b679SAndreas Boehler throw new Exception\Conflict('Parent node does not exist'); 1103*a1a3b679SAndreas Boehler 1104*a1a3b679SAndreas Boehler } 1105*a1a3b679SAndreas Boehler 1106*a1a3b679SAndreas Boehler // Making sure the parent is a collection 1107*a1a3b679SAndreas Boehler if (!$parent instanceof ICollection) { 1108*a1a3b679SAndreas Boehler throw new Exception\Conflict('Parent node is not a collection'); 1109*a1a3b679SAndreas Boehler } 1110*a1a3b679SAndreas Boehler 1111*a1a3b679SAndreas Boehler // Making sure the child does not already exist 1112*a1a3b679SAndreas Boehler try { 1113*a1a3b679SAndreas Boehler $parent->getChild($newName); 1114*a1a3b679SAndreas Boehler 1115*a1a3b679SAndreas Boehler // If we got here.. it means there's already a node on that url, and we need to throw a 405 1116*a1a3b679SAndreas Boehler throw new Exception\MethodNotAllowed('The resource you tried to create already exists'); 1117*a1a3b679SAndreas Boehler 1118*a1a3b679SAndreas Boehler } catch (Exception\NotFound $e) { 1119*a1a3b679SAndreas Boehler // NotFound is the expected behavior. 1120*a1a3b679SAndreas Boehler } 1121*a1a3b679SAndreas Boehler 1122*a1a3b679SAndreas Boehler 1123*a1a3b679SAndreas Boehler if (!$this->emit('beforeBind', [$uri])) return; 1124*a1a3b679SAndreas Boehler 1125*a1a3b679SAndreas Boehler if ($parent instanceof IExtendedCollection) { 1126*a1a3b679SAndreas Boehler 1127*a1a3b679SAndreas Boehler /** 1128*a1a3b679SAndreas Boehler * If the parent is an instance of IExtendedCollection, it means that 1129*a1a3b679SAndreas Boehler * we can pass the MkCol object directly as it may be able to store 1130*a1a3b679SAndreas Boehler * properties immediately. 1131*a1a3b679SAndreas Boehler */ 1132*a1a3b679SAndreas Boehler $parent->createExtendedCollection($newName, $mkCol); 1133*a1a3b679SAndreas Boehler 1134*a1a3b679SAndreas Boehler } else { 1135*a1a3b679SAndreas Boehler 1136*a1a3b679SAndreas Boehler /** 1137*a1a3b679SAndreas Boehler * If the parent is a standard ICollection, it means only 1138*a1a3b679SAndreas Boehler * 'standard' collections can be created, so we should fail any 1139*a1a3b679SAndreas Boehler * MKCOL operation that carries extra resourcetypes. 1140*a1a3b679SAndreas Boehler */ 1141*a1a3b679SAndreas Boehler if (count($mkCol->getResourceType()) > 1) { 1142*a1a3b679SAndreas Boehler throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); 1143*a1a3b679SAndreas Boehler } 1144*a1a3b679SAndreas Boehler 1145*a1a3b679SAndreas Boehler $parent->createDirectory($newName); 1146*a1a3b679SAndreas Boehler 1147*a1a3b679SAndreas Boehler } 1148*a1a3b679SAndreas Boehler 1149*a1a3b679SAndreas Boehler // If there are any properties that have not been handled/stored, 1150*a1a3b679SAndreas Boehler // we ask the 'propPatch' event to handle them. This will allow for 1151*a1a3b679SAndreas Boehler // example the propertyStorage system to store properties upon MKCOL. 1152*a1a3b679SAndreas Boehler if ($mkCol->getRemainingMutations()) { 1153*a1a3b679SAndreas Boehler $this->emit('propPatch', [$uri, $mkCol]); 1154*a1a3b679SAndreas Boehler } 1155*a1a3b679SAndreas Boehler $success = $mkCol->commit(); 1156*a1a3b679SAndreas Boehler 1157*a1a3b679SAndreas Boehler if (!$success) { 1158*a1a3b679SAndreas Boehler $result = $mkCol->getResult(); 1159*a1a3b679SAndreas Boehler // generateMkCol needs the href key to exist. 1160*a1a3b679SAndreas Boehler $result['href'] = $uri; 1161*a1a3b679SAndreas Boehler return $result; 1162*a1a3b679SAndreas Boehler } 1163*a1a3b679SAndreas Boehler 1164*a1a3b679SAndreas Boehler $this->tree->markDirty($parentUri); 1165*a1a3b679SAndreas Boehler $this->emit('afterBind', [$uri]); 1166*a1a3b679SAndreas Boehler 1167*a1a3b679SAndreas Boehler } 1168*a1a3b679SAndreas Boehler 1169*a1a3b679SAndreas Boehler /** 1170*a1a3b679SAndreas Boehler * This method updates a resource's properties 1171*a1a3b679SAndreas Boehler * 1172*a1a3b679SAndreas Boehler * The properties array must be a list of properties. Array-keys are 1173*a1a3b679SAndreas Boehler * property names in clarknotation, array-values are it's values. 1174*a1a3b679SAndreas Boehler * If a property must be deleted, the value should be null. 1175*a1a3b679SAndreas Boehler * 1176*a1a3b679SAndreas Boehler * Note that this request should either completely succeed, or 1177*a1a3b679SAndreas Boehler * completely fail. 1178*a1a3b679SAndreas Boehler * 1179*a1a3b679SAndreas Boehler * The response is an array with properties for keys, and http status codes 1180*a1a3b679SAndreas Boehler * as their values. 1181*a1a3b679SAndreas Boehler * 1182*a1a3b679SAndreas Boehler * @param string $path 1183*a1a3b679SAndreas Boehler * @param array $properties 1184*a1a3b679SAndreas Boehler * @return array 1185*a1a3b679SAndreas Boehler */ 1186*a1a3b679SAndreas Boehler function updateProperties($path, array $properties) { 1187*a1a3b679SAndreas Boehler 1188*a1a3b679SAndreas Boehler $propPatch = new PropPatch($properties); 1189*a1a3b679SAndreas Boehler $this->emit('propPatch', [$path, $propPatch]); 1190*a1a3b679SAndreas Boehler $propPatch->commit(); 1191*a1a3b679SAndreas Boehler 1192*a1a3b679SAndreas Boehler return $propPatch->getResult(); 1193*a1a3b679SAndreas Boehler 1194*a1a3b679SAndreas Boehler } 1195*a1a3b679SAndreas Boehler 1196*a1a3b679SAndreas Boehler /** 1197*a1a3b679SAndreas Boehler * This method checks the main HTTP preconditions. 1198*a1a3b679SAndreas Boehler * 1199*a1a3b679SAndreas Boehler * Currently these are: 1200*a1a3b679SAndreas Boehler * * If-Match 1201*a1a3b679SAndreas Boehler * * If-None-Match 1202*a1a3b679SAndreas Boehler * * If-Modified-Since 1203*a1a3b679SAndreas Boehler * * If-Unmodified-Since 1204*a1a3b679SAndreas Boehler * 1205*a1a3b679SAndreas Boehler * The method will return true if all preconditions are met 1206*a1a3b679SAndreas Boehler * The method will return false, or throw an exception if preconditions 1207*a1a3b679SAndreas Boehler * failed. If false is returned the operation should be aborted, and 1208*a1a3b679SAndreas Boehler * the appropriate HTTP response headers are already set. 1209*a1a3b679SAndreas Boehler * 1210*a1a3b679SAndreas Boehler * Normally this method will throw 412 Precondition Failed for failures 1211*a1a3b679SAndreas Boehler * related to If-None-Match, If-Match and If-Unmodified Since. It will 1212*a1a3b679SAndreas Boehler * set the status to 304 Not Modified for If-Modified_since. 1213*a1a3b679SAndreas Boehler * 1214*a1a3b679SAndreas Boehler * @param RequestInterface $request 1215*a1a3b679SAndreas Boehler * @param ResponseInterface $response 1216*a1a3b679SAndreas Boehler * @return bool 1217*a1a3b679SAndreas Boehler */ 1218*a1a3b679SAndreas Boehler function checkPreconditions(RequestInterface $request, ResponseInterface $response) { 1219*a1a3b679SAndreas Boehler 1220*a1a3b679SAndreas Boehler $path = $request->getPath(); 1221*a1a3b679SAndreas Boehler $node = null; 1222*a1a3b679SAndreas Boehler $lastMod = null; 1223*a1a3b679SAndreas Boehler $etag = null; 1224*a1a3b679SAndreas Boehler 1225*a1a3b679SAndreas Boehler if ($ifMatch = $request->getHeader('If-Match')) { 1226*a1a3b679SAndreas Boehler 1227*a1a3b679SAndreas Boehler // If-Match contains an entity tag. Only if the entity-tag 1228*a1a3b679SAndreas Boehler // matches we are allowed to make the request succeed. 1229*a1a3b679SAndreas Boehler // If the entity-tag is '*' we are only allowed to make the 1230*a1a3b679SAndreas Boehler // request succeed if a resource exists at that url. 1231*a1a3b679SAndreas Boehler try { 1232*a1a3b679SAndreas Boehler $node = $this->tree->getNodeForPath($path); 1233*a1a3b679SAndreas Boehler } catch (Exception\NotFound $e) { 1234*a1a3b679SAndreas Boehler throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); 1235*a1a3b679SAndreas Boehler } 1236*a1a3b679SAndreas Boehler 1237*a1a3b679SAndreas Boehler // Only need to check entity tags if they are not * 1238*a1a3b679SAndreas Boehler if ($ifMatch !== '*') { 1239*a1a3b679SAndreas Boehler 1240*a1a3b679SAndreas Boehler // There can be multiple ETags 1241*a1a3b679SAndreas Boehler $ifMatch = explode(',', $ifMatch); 1242*a1a3b679SAndreas Boehler $haveMatch = false; 1243*a1a3b679SAndreas Boehler foreach ($ifMatch as $ifMatchItem) { 1244*a1a3b679SAndreas Boehler 1245*a1a3b679SAndreas Boehler // Stripping any extra spaces 1246*a1a3b679SAndreas Boehler $ifMatchItem = trim($ifMatchItem, ' '); 1247*a1a3b679SAndreas Boehler 1248*a1a3b679SAndreas Boehler $etag = $node instanceof IFile ? $node->getETag() : null; 1249*a1a3b679SAndreas Boehler if ($etag === $ifMatchItem) { 1250*a1a3b679SAndreas Boehler $haveMatch = true; 1251*a1a3b679SAndreas Boehler } else { 1252*a1a3b679SAndreas Boehler // Evolution has a bug where it sometimes prepends the " 1253*a1a3b679SAndreas Boehler // with a \. This is our workaround. 1254*a1a3b679SAndreas Boehler if (str_replace('\\"', '"', $ifMatchItem) === $etag) { 1255*a1a3b679SAndreas Boehler $haveMatch = true; 1256*a1a3b679SAndreas Boehler } 1257*a1a3b679SAndreas Boehler } 1258*a1a3b679SAndreas Boehler 1259*a1a3b679SAndreas Boehler } 1260*a1a3b679SAndreas Boehler if (!$haveMatch) { 1261*a1a3b679SAndreas Boehler if ($etag) $response->setHeader('ETag', $etag); 1262*a1a3b679SAndreas Boehler throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.', 'If-Match'); 1263*a1a3b679SAndreas Boehler } 1264*a1a3b679SAndreas Boehler } 1265*a1a3b679SAndreas Boehler } 1266*a1a3b679SAndreas Boehler 1267*a1a3b679SAndreas Boehler if ($ifNoneMatch = $request->getHeader('If-None-Match')) { 1268*a1a3b679SAndreas Boehler 1269*a1a3b679SAndreas Boehler // The If-None-Match header contains an ETag. 1270*a1a3b679SAndreas Boehler // Only if the ETag does not match the current ETag, the request will succeed 1271*a1a3b679SAndreas Boehler // The header can also contain *, in which case the request 1272*a1a3b679SAndreas Boehler // will only succeed if the entity does not exist at all. 1273*a1a3b679SAndreas Boehler $nodeExists = true; 1274*a1a3b679SAndreas Boehler if (!$node) { 1275*a1a3b679SAndreas Boehler try { 1276*a1a3b679SAndreas Boehler $node = $this->tree->getNodeForPath($path); 1277*a1a3b679SAndreas Boehler } catch (Exception\NotFound $e) { 1278*a1a3b679SAndreas Boehler $nodeExists = false; 1279*a1a3b679SAndreas Boehler } 1280*a1a3b679SAndreas Boehler } 1281*a1a3b679SAndreas Boehler if ($nodeExists) { 1282*a1a3b679SAndreas Boehler $haveMatch = false; 1283*a1a3b679SAndreas Boehler if ($ifNoneMatch === '*') $haveMatch = true; 1284*a1a3b679SAndreas Boehler else { 1285*a1a3b679SAndreas Boehler 1286*a1a3b679SAndreas Boehler // There might be multiple ETags 1287*a1a3b679SAndreas Boehler $ifNoneMatch = explode(',', $ifNoneMatch); 1288*a1a3b679SAndreas Boehler $etag = $node instanceof IFile ? $node->getETag() : null; 1289*a1a3b679SAndreas Boehler 1290*a1a3b679SAndreas Boehler foreach ($ifNoneMatch as $ifNoneMatchItem) { 1291*a1a3b679SAndreas Boehler 1292*a1a3b679SAndreas Boehler // Stripping any extra spaces 1293*a1a3b679SAndreas Boehler $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); 1294*a1a3b679SAndreas Boehler 1295*a1a3b679SAndreas Boehler if ($etag === $ifNoneMatchItem) $haveMatch = true; 1296*a1a3b679SAndreas Boehler 1297*a1a3b679SAndreas Boehler } 1298*a1a3b679SAndreas Boehler 1299*a1a3b679SAndreas Boehler } 1300*a1a3b679SAndreas Boehler 1301*a1a3b679SAndreas Boehler if ($haveMatch) { 1302*a1a3b679SAndreas Boehler if ($etag) $response->setHeader('ETag', $etag); 1303*a1a3b679SAndreas Boehler if ($request->getMethod() === 'GET') { 1304*a1a3b679SAndreas Boehler $response->setStatus(304); 1305*a1a3b679SAndreas Boehler return false; 1306*a1a3b679SAndreas Boehler } else { 1307*a1a3b679SAndreas Boehler throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); 1308*a1a3b679SAndreas Boehler } 1309*a1a3b679SAndreas Boehler } 1310*a1a3b679SAndreas Boehler } 1311*a1a3b679SAndreas Boehler 1312*a1a3b679SAndreas Boehler } 1313*a1a3b679SAndreas Boehler 1314*a1a3b679SAndreas Boehler if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { 1315*a1a3b679SAndreas Boehler 1316*a1a3b679SAndreas Boehler // The If-Modified-Since header contains a date. We 1317*a1a3b679SAndreas Boehler // will only return the entity if it has been changed since 1318*a1a3b679SAndreas Boehler // that date. If it hasn't been changed, we return a 304 1319*a1a3b679SAndreas Boehler // header 1320*a1a3b679SAndreas Boehler // Note that this header only has to be checked if there was no If-None-Match header 1321*a1a3b679SAndreas Boehler // as per the HTTP spec. 1322*a1a3b679SAndreas Boehler $date = HTTP\Util::parseHTTPDate($ifModifiedSince); 1323*a1a3b679SAndreas Boehler 1324*a1a3b679SAndreas Boehler if ($date) { 1325*a1a3b679SAndreas Boehler if (is_null($node)) { 1326*a1a3b679SAndreas Boehler $node = $this->tree->getNodeForPath($path); 1327*a1a3b679SAndreas Boehler } 1328*a1a3b679SAndreas Boehler $lastMod = $node->getLastModified(); 1329*a1a3b679SAndreas Boehler if ($lastMod) { 1330*a1a3b679SAndreas Boehler $lastMod = new \DateTime('@' . $lastMod); 1331*a1a3b679SAndreas Boehler if ($lastMod <= $date) { 1332*a1a3b679SAndreas Boehler $response->setStatus(304); 1333*a1a3b679SAndreas Boehler $response->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod)); 1334*a1a3b679SAndreas Boehler return false; 1335*a1a3b679SAndreas Boehler } 1336*a1a3b679SAndreas Boehler } 1337*a1a3b679SAndreas Boehler } 1338*a1a3b679SAndreas Boehler } 1339*a1a3b679SAndreas Boehler 1340*a1a3b679SAndreas Boehler if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { 1341*a1a3b679SAndreas Boehler 1342*a1a3b679SAndreas Boehler // The If-Unmodified-Since will allow allow the request if the 1343*a1a3b679SAndreas Boehler // entity has not changed since the specified date. 1344*a1a3b679SAndreas Boehler $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince); 1345*a1a3b679SAndreas Boehler 1346*a1a3b679SAndreas Boehler // We must only check the date if it's valid 1347*a1a3b679SAndreas Boehler if ($date) { 1348*a1a3b679SAndreas Boehler if (is_null($node)) { 1349*a1a3b679SAndreas Boehler $node = $this->tree->getNodeForPath($path); 1350*a1a3b679SAndreas Boehler } 1351*a1a3b679SAndreas Boehler $lastMod = $node->getLastModified(); 1352*a1a3b679SAndreas Boehler if ($lastMod) { 1353*a1a3b679SAndreas Boehler $lastMod = new \DateTime('@' . $lastMod); 1354*a1a3b679SAndreas Boehler if ($lastMod > $date) { 1355*a1a3b679SAndreas Boehler throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); 1356*a1a3b679SAndreas Boehler } 1357*a1a3b679SAndreas Boehler } 1358*a1a3b679SAndreas Boehler } 1359*a1a3b679SAndreas Boehler 1360*a1a3b679SAndreas Boehler } 1361*a1a3b679SAndreas Boehler 1362*a1a3b679SAndreas Boehler // Now the hardest, the If: header. The If: header can contain multiple 1363*a1a3b679SAndreas Boehler // urls, ETags and so-called 'state tokens'. 1364*a1a3b679SAndreas Boehler // 1365*a1a3b679SAndreas Boehler // Examples of state tokens include lock-tokens (as defined in rfc4918) 1366*a1a3b679SAndreas Boehler // and sync-tokens (as defined in rfc6578). 1367*a1a3b679SAndreas Boehler // 1368*a1a3b679SAndreas Boehler // The only proper way to deal with these, is to emit events, that a 1369*a1a3b679SAndreas Boehler // Sync and Lock plugin can pick up. 1370*a1a3b679SAndreas Boehler $ifConditions = $this->getIfConditions($request); 1371*a1a3b679SAndreas Boehler 1372*a1a3b679SAndreas Boehler foreach ($ifConditions as $kk => $ifCondition) { 1373*a1a3b679SAndreas Boehler foreach ($ifCondition['tokens'] as $ii => $token) { 1374*a1a3b679SAndreas Boehler $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; 1375*a1a3b679SAndreas Boehler } 1376*a1a3b679SAndreas Boehler } 1377*a1a3b679SAndreas Boehler 1378*a1a3b679SAndreas Boehler // Plugins are responsible for validating all the tokens. 1379*a1a3b679SAndreas Boehler // If a plugin deemed a token 'valid', it will set 'validToken' to 1380*a1a3b679SAndreas Boehler // true. 1381*a1a3b679SAndreas Boehler $this->emit('validateTokens', [ $request, &$ifConditions ]); 1382*a1a3b679SAndreas Boehler 1383*a1a3b679SAndreas Boehler // Now we're going to analyze the result. 1384*a1a3b679SAndreas Boehler 1385*a1a3b679SAndreas Boehler // Every ifCondition needs to validate to true, so we exit as soon as 1386*a1a3b679SAndreas Boehler // we have an invalid condition. 1387*a1a3b679SAndreas Boehler foreach ($ifConditions as $ifCondition) { 1388*a1a3b679SAndreas Boehler 1389*a1a3b679SAndreas Boehler $uri = $ifCondition['uri']; 1390*a1a3b679SAndreas Boehler $tokens = $ifCondition['tokens']; 1391*a1a3b679SAndreas Boehler 1392*a1a3b679SAndreas Boehler // We only need 1 valid token for the condition to succeed. 1393*a1a3b679SAndreas Boehler foreach ($tokens as $token) { 1394*a1a3b679SAndreas Boehler 1395*a1a3b679SAndreas Boehler $tokenValid = $token['validToken'] || !$token['token']; 1396*a1a3b679SAndreas Boehler 1397*a1a3b679SAndreas Boehler $etagValid = false; 1398*a1a3b679SAndreas Boehler if (!$token['etag']) { 1399*a1a3b679SAndreas Boehler $etagValid = true; 1400*a1a3b679SAndreas Boehler } 1401*a1a3b679SAndreas Boehler // Checking the ETag, only if the token was already deamed 1402*a1a3b679SAndreas Boehler // valid and there is one. 1403*a1a3b679SAndreas Boehler if ($token['etag'] && $tokenValid) { 1404*a1a3b679SAndreas Boehler 1405*a1a3b679SAndreas Boehler // The token was valid, and there was an ETag. We must 1406*a1a3b679SAndreas Boehler // grab the current ETag and check it. 1407*a1a3b679SAndreas Boehler $node = $this->tree->getNodeForPath($uri); 1408*a1a3b679SAndreas Boehler $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; 1409*a1a3b679SAndreas Boehler 1410*a1a3b679SAndreas Boehler } 1411*a1a3b679SAndreas Boehler 1412*a1a3b679SAndreas Boehler 1413*a1a3b679SAndreas Boehler if (($tokenValid && $etagValid) ^ $token['negate']) { 1414*a1a3b679SAndreas Boehler // Both were valid, so we can go to the next condition. 1415*a1a3b679SAndreas Boehler continue 2; 1416*a1a3b679SAndreas Boehler } 1417*a1a3b679SAndreas Boehler 1418*a1a3b679SAndreas Boehler 1419*a1a3b679SAndreas Boehler } 1420*a1a3b679SAndreas Boehler 1421*a1a3b679SAndreas Boehler // If we ended here, it means there was no valid ETag + token 1422*a1a3b679SAndreas Boehler // combination found for the current condition. This means we fail! 1423*a1a3b679SAndreas Boehler throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for ' . $uri, 'If'); 1424*a1a3b679SAndreas Boehler 1425*a1a3b679SAndreas Boehler } 1426*a1a3b679SAndreas Boehler 1427*a1a3b679SAndreas Boehler return true; 1428*a1a3b679SAndreas Boehler 1429*a1a3b679SAndreas Boehler } 1430*a1a3b679SAndreas Boehler 1431*a1a3b679SAndreas Boehler /** 1432*a1a3b679SAndreas Boehler * This method is created to extract information from the WebDAV HTTP 'If:' header 1433*a1a3b679SAndreas Boehler * 1434*a1a3b679SAndreas Boehler * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information 1435*a1a3b679SAndreas Boehler * The function will return an array, containing structs with the following keys 1436*a1a3b679SAndreas Boehler * 1437*a1a3b679SAndreas Boehler * * uri - the uri the condition applies to. 1438*a1a3b679SAndreas Boehler * * tokens - The lock token. another 2 dimensional array containing 3 elements 1439*a1a3b679SAndreas Boehler * 1440*a1a3b679SAndreas Boehler * Example 1: 1441*a1a3b679SAndreas Boehler * 1442*a1a3b679SAndreas Boehler * If: (<opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2>) 1443*a1a3b679SAndreas Boehler * 1444*a1a3b679SAndreas Boehler * Would result in: 1445*a1a3b679SAndreas Boehler * 1446*a1a3b679SAndreas Boehler * [ 1447*a1a3b679SAndreas Boehler * [ 1448*a1a3b679SAndreas Boehler * 'uri' => '/request/uri', 1449*a1a3b679SAndreas Boehler * 'tokens' => [ 1450*a1a3b679SAndreas Boehler * [ 1451*a1a3b679SAndreas Boehler * [ 1452*a1a3b679SAndreas Boehler * 'negate' => false, 1453*a1a3b679SAndreas Boehler * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', 1454*a1a3b679SAndreas Boehler * 'etag' => "" 1455*a1a3b679SAndreas Boehler * ] 1456*a1a3b679SAndreas Boehler * ] 1457*a1a3b679SAndreas Boehler * ], 1458*a1a3b679SAndreas Boehler * ] 1459*a1a3b679SAndreas Boehler * ] 1460*a1a3b679SAndreas Boehler * 1461*a1a3b679SAndreas Boehler * Example 2: 1462*a1a3b679SAndreas Boehler * 1463*a1a3b679SAndreas Boehler * If: </path/> (Not <opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2> ["Im An ETag"]) (["Another ETag"]) </path2/> (Not ["Path2 ETag"]) 1464*a1a3b679SAndreas Boehler * 1465*a1a3b679SAndreas Boehler * Would result in: 1466*a1a3b679SAndreas Boehler * 1467*a1a3b679SAndreas Boehler * [ 1468*a1a3b679SAndreas Boehler * [ 1469*a1a3b679SAndreas Boehler * 'uri' => 'path', 1470*a1a3b679SAndreas Boehler * 'tokens' => [ 1471*a1a3b679SAndreas Boehler * [ 1472*a1a3b679SAndreas Boehler * [ 1473*a1a3b679SAndreas Boehler * 'negate' => true, 1474*a1a3b679SAndreas Boehler * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', 1475*a1a3b679SAndreas Boehler * 'etag' => '"Im An ETag"' 1476*a1a3b679SAndreas Boehler * ], 1477*a1a3b679SAndreas Boehler * [ 1478*a1a3b679SAndreas Boehler * 'negate' => false, 1479*a1a3b679SAndreas Boehler * 'token' => '', 1480*a1a3b679SAndreas Boehler * 'etag' => '"Another ETag"' 1481*a1a3b679SAndreas Boehler * ] 1482*a1a3b679SAndreas Boehler * ] 1483*a1a3b679SAndreas Boehler * ], 1484*a1a3b679SAndreas Boehler * ], 1485*a1a3b679SAndreas Boehler * [ 1486*a1a3b679SAndreas Boehler * 'uri' => 'path2', 1487*a1a3b679SAndreas Boehler * 'tokens' => [ 1488*a1a3b679SAndreas Boehler * [ 1489*a1a3b679SAndreas Boehler * [ 1490*a1a3b679SAndreas Boehler * 'negate' => true, 1491*a1a3b679SAndreas Boehler * 'token' => '', 1492*a1a3b679SAndreas Boehler * 'etag' => '"Path2 ETag"' 1493*a1a3b679SAndreas Boehler * ] 1494*a1a3b679SAndreas Boehler * ] 1495*a1a3b679SAndreas Boehler * ], 1496*a1a3b679SAndreas Boehler * ], 1497*a1a3b679SAndreas Boehler * ] 1498*a1a3b679SAndreas Boehler * 1499*a1a3b679SAndreas Boehler * @param RequestInterface $request 1500*a1a3b679SAndreas Boehler * @return array 1501*a1a3b679SAndreas Boehler */ 1502*a1a3b679SAndreas Boehler function getIfConditions(RequestInterface $request) { 1503*a1a3b679SAndreas Boehler 1504*a1a3b679SAndreas Boehler $header = $request->getHeader('If'); 1505*a1a3b679SAndreas Boehler if (!$header) return []; 1506*a1a3b679SAndreas Boehler 1507*a1a3b679SAndreas Boehler $matches = []; 1508*a1a3b679SAndreas Boehler 1509*a1a3b679SAndreas Boehler $regex = '/(?:\<(?P<uri>.*?)\>\s)?\((?P<not>Not\s)?(?:\<(?P<token>[^\>]*)\>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im'; 1510*a1a3b679SAndreas Boehler preg_match_all($regex, $header, $matches, PREG_SET_ORDER); 1511*a1a3b679SAndreas Boehler 1512*a1a3b679SAndreas Boehler $conditions = []; 1513*a1a3b679SAndreas Boehler 1514*a1a3b679SAndreas Boehler foreach ($matches as $match) { 1515*a1a3b679SAndreas Boehler 1516*a1a3b679SAndreas Boehler // If there was no uri specified in this match, and there were 1517*a1a3b679SAndreas Boehler // already conditions parsed, we add the condition to the list of 1518*a1a3b679SAndreas Boehler // conditions for the previous uri. 1519*a1a3b679SAndreas Boehler if (!$match['uri'] && count($conditions)) { 1520*a1a3b679SAndreas Boehler $conditions[count($conditions) - 1]['tokens'][] = [ 1521*a1a3b679SAndreas Boehler 'negate' => $match['not'] ? true : false, 1522*a1a3b679SAndreas Boehler 'token' => $match['token'], 1523*a1a3b679SAndreas Boehler 'etag' => isset($match['etag']) ? $match['etag'] : '' 1524*a1a3b679SAndreas Boehler ]; 1525*a1a3b679SAndreas Boehler } else { 1526*a1a3b679SAndreas Boehler 1527*a1a3b679SAndreas Boehler if (!$match['uri']) { 1528*a1a3b679SAndreas Boehler $realUri = $request->getPath(); 1529*a1a3b679SAndreas Boehler } else { 1530*a1a3b679SAndreas Boehler $realUri = $this->calculateUri($match['uri']); 1531*a1a3b679SAndreas Boehler } 1532*a1a3b679SAndreas Boehler 1533*a1a3b679SAndreas Boehler $conditions[] = [ 1534*a1a3b679SAndreas Boehler 'uri' => $realUri, 1535*a1a3b679SAndreas Boehler 'tokens' => [ 1536*a1a3b679SAndreas Boehler [ 1537*a1a3b679SAndreas Boehler 'negate' => $match['not'] ? true : false, 1538*a1a3b679SAndreas Boehler 'token' => $match['token'], 1539*a1a3b679SAndreas Boehler 'etag' => isset($match['etag']) ? $match['etag'] : '' 1540*a1a3b679SAndreas Boehler ] 1541*a1a3b679SAndreas Boehler ], 1542*a1a3b679SAndreas Boehler 1543*a1a3b679SAndreas Boehler ]; 1544*a1a3b679SAndreas Boehler } 1545*a1a3b679SAndreas Boehler 1546*a1a3b679SAndreas Boehler } 1547*a1a3b679SAndreas Boehler 1548*a1a3b679SAndreas Boehler return $conditions; 1549*a1a3b679SAndreas Boehler 1550*a1a3b679SAndreas Boehler } 1551*a1a3b679SAndreas Boehler 1552*a1a3b679SAndreas Boehler /** 1553*a1a3b679SAndreas Boehler * Returns an array with resourcetypes for a node. 1554*a1a3b679SAndreas Boehler * 1555*a1a3b679SAndreas Boehler * @param INode $node 1556*a1a3b679SAndreas Boehler * @return array 1557*a1a3b679SAndreas Boehler */ 1558*a1a3b679SAndreas Boehler function getResourceTypeForNode(INode $node) { 1559*a1a3b679SAndreas Boehler 1560*a1a3b679SAndreas Boehler $result = []; 1561*a1a3b679SAndreas Boehler foreach ($this->resourceTypeMapping as $className => $resourceType) { 1562*a1a3b679SAndreas Boehler if ($node instanceof $className) $result[] = $resourceType; 1563*a1a3b679SAndreas Boehler } 1564*a1a3b679SAndreas Boehler return $result; 1565*a1a3b679SAndreas Boehler 1566*a1a3b679SAndreas Boehler } 1567*a1a3b679SAndreas Boehler 1568*a1a3b679SAndreas Boehler // }}} 1569*a1a3b679SAndreas Boehler // {{{ XML Readers & Writers 1570*a1a3b679SAndreas Boehler 1571*a1a3b679SAndreas Boehler 1572*a1a3b679SAndreas Boehler /** 1573*a1a3b679SAndreas Boehler * Generates a WebDAV propfind response body based on a list of nodes. 1574*a1a3b679SAndreas Boehler * 1575*a1a3b679SAndreas Boehler * If 'strip404s' is set to true, all 404 responses will be removed. 1576*a1a3b679SAndreas Boehler * 1577*a1a3b679SAndreas Boehler * @param array $fileProperties The list with nodes 1578*a1a3b679SAndreas Boehler * @param bool strip404s 1579*a1a3b679SAndreas Boehler * @return string 1580*a1a3b679SAndreas Boehler */ 1581*a1a3b679SAndreas Boehler function generateMultiStatus(array $fileProperties, $strip404s = false) { 1582*a1a3b679SAndreas Boehler 1583*a1a3b679SAndreas Boehler $xml = []; 1584*a1a3b679SAndreas Boehler 1585*a1a3b679SAndreas Boehler foreach ($fileProperties as $entry) { 1586*a1a3b679SAndreas Boehler 1587*a1a3b679SAndreas Boehler $href = $entry['href']; 1588*a1a3b679SAndreas Boehler unset($entry['href']); 1589*a1a3b679SAndreas Boehler if ($strip404s) { 1590*a1a3b679SAndreas Boehler unset($entry[404]); 1591*a1a3b679SAndreas Boehler } 1592*a1a3b679SAndreas Boehler $response = new Xml\Element\Response( 1593*a1a3b679SAndreas Boehler ltrim($href, '/'), 1594*a1a3b679SAndreas Boehler $entry 1595*a1a3b679SAndreas Boehler ); 1596*a1a3b679SAndreas Boehler $xml[] = [ 1597*a1a3b679SAndreas Boehler 'name' => '{DAV:}response', 1598*a1a3b679SAndreas Boehler 'value' => $response 1599*a1a3b679SAndreas Boehler ]; 1600*a1a3b679SAndreas Boehler 1601*a1a3b679SAndreas Boehler } 1602*a1a3b679SAndreas Boehler return $this->xml->write('{DAV:}multistatus', $xml, $this->baseUri); 1603*a1a3b679SAndreas Boehler 1604*a1a3b679SAndreas Boehler } 1605*a1a3b679SAndreas Boehler 1606*a1a3b679SAndreas Boehler} 1607