xref: /plugin/davcal/vendor/sabre/dav/lib/DAV/Server.php (revision a1a3b6794e0e143a4a8b51d3185ce2d339be61ab)
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