]> git.mxchange.org Git - friendica-addons.git/blob - dav/SabreDAV/lib/Sabre/DAV/Server.php
Update strings
[friendica-addons.git] / dav / SabreDAV / lib / Sabre / DAV / Server.php
1 <?php
2
3 /**
4  * Main DAV server class
5  *
6  * @package Sabre
7  * @subpackage DAV
8  * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
9  * @author Evert Pot (http://www.rooftopsolutions.nl/)
10  * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
11  */
12 class Sabre_DAV_Server {
13
14     /**
15      * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree
16      */
17     const DEPTH_INFINITY = -1;
18
19     /**
20      * Nodes that are files, should have this as the type property
21      */
22     const NODE_FILE = 1;
23
24     /**
25      * Nodes that are directories, should use this value as the type property
26      */
27     const NODE_DIRECTORY = 2;
28
29     /**
30      * XML namespace for all SabreDAV related elements
31      */
32     const NS_SABREDAV = 'http://sabredav.org/ns';
33
34     /**
35      * The tree object
36      *
37      * @var Sabre_DAV_Tree
38      */
39     public $tree;
40
41     /**
42      * The base uri
43      *
44      * @var string
45      */
46     protected $baseUri = null;
47
48     /**
49      * httpResponse
50      *
51      * @var Sabre_HTTP_Response
52      */
53     public $httpResponse;
54
55     /**
56      * httpRequest
57      *
58      * @var Sabre_HTTP_Request
59      */
60     public $httpRequest;
61
62     /**
63      * The list of addons
64      *
65      * @var array
66      */
67     protected $plugins = array();
68
69     /**
70      * This array contains a list of callbacks we should call when certain events are triggered
71      *
72      * @var array
73      */
74     protected $eventSubscriptions = array();
75
76     /**
77      * This is a default list of namespaces.
78      *
79      * If you are defining your own custom namespace, add it here to reduce
80      * bandwidth and improve legibility of xml bodies.
81      *
82      * @var array
83      */
84     public $xmlNamespaces = array(
85         'DAV:' => 'd',
86         'http://sabredav.org/ns' => 's',
87     );
88
89     /**
90      * The propertymap can be used to map properties from
91      * requests to property classes.
92      *
93      * @var array
94      */
95     public $propertyMap = array(
96         '{DAV:}resourcetype' => 'Sabre_DAV_Property_ResourceType',
97     );
98
99     public $protectedProperties = array(
100         // RFC4918
101         '{DAV:}getcontentlength',
102         '{DAV:}getetag',
103         '{DAV:}getlastmodified',
104         '{DAV:}lockdiscovery',
105         '{DAV:}resourcetype',
106         '{DAV:}supportedlock',
107
108         // RFC4331
109         '{DAV:}quota-available-bytes',
110         '{DAV:}quota-used-bytes',
111
112         // RFC3744
113         '{DAV:}supported-privilege-set',
114         '{DAV:}current-user-privilege-set',
115         '{DAV:}acl',
116         '{DAV:}acl-restrictions',
117         '{DAV:}inherited-acl-set',
118
119     );
120
121     /**
122      * This is a flag that allow or not showing file, line and code
123      * of the exception in the returned XML
124      *
125      * @var bool
126      */
127     public $debugExceptions = false;
128
129     /**
130      * This property allows you to automatically add the 'resourcetype' value
131      * based on a node's classname or interface.
132      *
133      * The preset ensures that {DAV:}collection is automaticlly added for nodes
134      * implementing Sabre_DAV_ICollection.
135      *
136      * @var array
137      */
138     public $resourceTypeMapping = array(
139         'Sabre_DAV_ICollection' => '{DAV:}collection',
140     );
141
142     /**
143      * If this setting is turned off, SabreDAV's version number will be hidden
144      * from various places.
145      *
146      * Some people feel this is a good security measure.
147      *
148      * @var bool
149      */
150     static public $exposeVersion = true;
151
152     /**
153      * Sets up the server
154      *
155      * If a Sabre_DAV_Tree object is passed as an argument, it will
156      * use it as the directory tree. If a Sabre_DAV_INode is passed, it
157      * will create a Sabre_DAV_ObjectTree and use the node as the root.
158      *
159      * If nothing is passed, a Sabre_DAV_SimpleCollection is created in
160      * a Sabre_DAV_ObjectTree.
161      *
162      * If an array is passed, we automatically create a root node, and use
163      * the nodes in the array as top-level children.
164      *
165      * @param Sabre_DAV_Tree|Sabre_DAV_INode|array|null $treeOrNode The tree object
166      */
167     public function __construct($treeOrNode = null) {
168
169         if ($treeOrNode instanceof Sabre_DAV_Tree) {
170             $this->tree = $treeOrNode;
171         } elseif ($treeOrNode instanceof Sabre_DAV_INode) {
172             $this->tree = new Sabre_DAV_ObjectTree($treeOrNode);
173         } elseif (is_array($treeOrNode)) {
174
175             // If it's an array, a list of nodes was passed, and we need to
176             // create the root node.
177             foreach($treeOrNode as $node) {
178                 if (!($node instanceof Sabre_DAV_INode)) {
179                     throw new Sabre_DAV_Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre_DAV_INode');
180                 }
181             }
182
183             $root = new Sabre_DAV_SimpleCollection('root', $treeOrNode);
184             $this->tree = new Sabre_DAV_ObjectTree($root);
185
186         } elseif (is_null($treeOrNode)) {
187             $root = new Sabre_DAV_SimpleCollection('root');
188             $this->tree = new Sabre_DAV_ObjectTree($root);
189         } else {
190             throw new Sabre_DAV_Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre_DAV_Tree, Sabre_DAV_INode, an array or null');
191         }
192         $this->httpResponse = new Sabre_HTTP_Response();
193         $this->httpRequest = new Sabre_HTTP_Request();
194
195     }
196
197     /**
198      * Starts the DAV Server
199      *
200      * @return void
201      */
202     public function exec() {
203
204         try {
205
206             $this->invokeMethod($this->httpRequest->getMethod(), $this->getRequestUri());
207
208         } catch (Exception $e) {
209
210             try {
211                 $this->broadcastEvent('exception', array($e));
212             } catch (Exception $ignore) {
213             }
214             $DOM = new DOMDocument('1.0','utf-8');
215             $DOM->formatOutput = true;
216
217             $error = $DOM->createElementNS('DAV:','d:error');
218             $error->setAttribute('xmlns:s',self::NS_SABREDAV);
219             $DOM->appendChild($error);
220
221             $error->appendChild($DOM->createElement('s:exception',get_class($e)));
222             $error->appendChild($DOM->createElement('s:message',$e->getMessage()));
223             if ($this->debugExceptions) {
224                 $error->appendChild($DOM->createElement('s:file',$e->getFile()));
225                 $error->appendChild($DOM->createElement('s:line',$e->getLine()));
226                 $error->appendChild($DOM->createElement('s:code',$e->getCode()));
227                 $error->appendChild($DOM->createElement('s:stacktrace',$e->getTraceAsString()));
228
229             }
230             if (self::$exposeVersion) {
231                 $error->appendChild($DOM->createElement('s:sabredav-version',Sabre_DAV_Version::VERSION));
232             }
233
234             if($e instanceof Sabre_DAV_Exception) {
235
236                 $httpCode = $e->getHTTPCode();
237                 $e->serialize($this,$error);
238                 $headers = $e->getHTTPHeaders($this);
239
240             } else {
241
242                 $httpCode = 500;
243                 $headers = array();
244
245             }
246             $headers['Content-Type'] = 'application/xml; charset=utf-8';
247
248             $this->httpResponse->sendStatus($httpCode);
249             $this->httpResponse->setHeaders($headers);
250             $this->httpResponse->sendBody($DOM->saveXML());
251
252         }
253
254     }
255
256     /**
257      * Sets the base server uri
258      *
259      * @param string $uri
260      * @return void
261      */
262     public function setBaseUri($uri) {
263
264         // If the baseUri does not end with a slash, we must add it
265         if ($uri[strlen($uri)-1]!=='/')
266             $uri.='/';
267
268         $this->baseUri = $uri;
269
270     }
271
272     /**
273      * Returns the base responding uri
274      *
275      * @return string
276      */
277     public function getBaseUri() {
278
279         if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
280         return $this->baseUri;
281
282     }
283
284     /**
285      * This method attempts to detect the base uri.
286      * Only the PATH_INFO variable is considered.
287      *
288      * If this variable is not set, the root (/) is assumed.
289      *
290      * @return string
291      */
292     public function guessBaseUri() {
293
294         $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
295         $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
296
297         // If PATH_INFO is found, we can assume it's accurate.
298         if (!empty($pathInfo)) {
299
300             // We need to make sure we ignore the QUERY_STRING part
301             if ($pos = strpos($uri,'?'))
302                 $uri = substr($uri,0,$pos);
303
304             // PATH_INFO is only set for urls, such as: /example.php/path
305             // in that case PATH_INFO contains '/path'.
306             // Note that REQUEST_URI is percent encoded, while PATH_INFO is
307             // not, Therefore they are only comparable if we first decode
308             // REQUEST_INFO as well.
309             $decodedUri = Sabre_DAV_URLUtil::decodePath($uri);
310
311             // A simple sanity check:
312             if(substr($decodedUri,strlen($decodedUri)-strlen($pathInfo))===$pathInfo) {
313                 $baseUri = substr($decodedUri,0,strlen($decodedUri)-strlen($pathInfo));
314                 return rtrim($baseUri,'/') . '/';
315             }
316
317             throw new Sabre_DAV_Exception('The REQUEST_URI ('. $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.');
318
319         }
320
321         // The last fallback is that we're just going to assume the server root.
322         return '/';
323
324     }
325
326     /**
327      * Adds a addon to the server
328      *
329      * For more information, console the documentation of Sabre_DAV_ServerPlugin
330      *
331      * @param Sabre_DAV_ServerPlugin $plugin
332      * @return void
333      */
334     public function addPlugin(Sabre_DAV_ServerPlugin $plugin) {
335
336         $this->plugins[$plugin->getPluginName()] = $plugin;
337         $plugin->initialize($this);
338
339     }
340
341     /**
342      * Returns an initialized addon by it's name.
343      *
344      * This function returns null if the addon was not found.
345      *
346      * @param string $name
347      * @return Sabre_DAV_ServerPlugin
348      */
349     public function getPlugin($name) {
350
351         if (isset($this->plugins[$name]))
352             return $this->plugins[$name];
353
354         // This is a fallback and deprecated.
355         foreach($this->plugins as $plugin) {
356             if (get_class($plugin)===$name) return $plugin;
357         }
358
359         return null;
360
361     }
362
363     /**
364      * Returns all addons
365      *
366      * @return array
367      */
368     public function getPlugins() {
369
370         return $this->plugins;
371
372     }
373
374
375     /**
376      * Subscribe to an event.
377      *
378      * When the event is triggered, we'll call all the specified callbacks.
379      * It is possible to control the order of the callbacks through the
380      * priority argument.
381      *
382      * This is for example used to make sure that the authentication addon
383      * is triggered before anything else. If it's not needed to change this
384      * number, it is recommended to ommit.
385      *
386      * @param string $event
387      * @param callback $callback
388      * @param int $priority
389      * @return void
390      */
391     public function subscribeEvent($event, $callback, $priority = 100) {
392
393         if (!isset($this->eventSubscriptions[$event])) {
394             $this->eventSubscriptions[$event] = array();
395         }
396         while(isset($this->eventSubscriptions[$event][$priority])) $priority++;
397         $this->eventSubscriptions[$event][$priority] = $callback;
398         ksort($this->eventSubscriptions[$event]);
399
400     }
401
402     /**
403      * Broadcasts an event
404      *
405      * This method will call all subscribers. If one of the subscribers returns false, the process stops.
406      *
407      * The arguments parameter will be sent to all subscribers
408      *
409      * @param string $eventName
410      * @param array $arguments
411      * @return bool
412      */
413     public function broadcastEvent($eventName,$arguments = array()) {
414
415         if (isset($this->eventSubscriptions[$eventName])) {
416
417             foreach($this->eventSubscriptions[$eventName] as $subscriber) {
418
419                 $result = call_user_func_array($subscriber,$arguments);
420                 if ($result===false) return false;
421
422             }
423
424         }
425
426         return true;
427
428     }
429
430     /**
431      * Handles a http request, and execute a method based on its name
432      *
433      * @param string $method
434      * @param string $uri
435      * @return void
436      */
437     public function invokeMethod($method, $uri) {
438
439         $method = strtoupper($method);
440
441         if (!$this->broadcastEvent('beforeMethod',array($method, $uri))) return;
442
443         // Make sure this is a HTTP method we support
444         $internalMethods = array(
445             'OPTIONS',
446             'GET',
447             'HEAD',
448             'DELETE',
449             'PROPFIND',
450             'MKCOL',
451             'PUT',
452             'PROPPATCH',
453             'COPY',
454             'MOVE',
455             'REPORT'
456         );
457
458         if (in_array($method,$internalMethods)) {
459
460             call_user_func(array($this,'http' . $method), $uri);
461
462         } else {
463
464             if ($this->broadcastEvent('unknownMethod',array($method, $uri))) {
465                 // Unsupported method
466                 throw new Sabre_DAV_Exception_NotImplemented('There was no handler found for this "' . $method . '" method');
467             }
468
469         }
470
471     }
472
473     // {{{ HTTP Method implementations
474
475     /**
476      * HTTP OPTIONS
477      *
478      * @param string $uri
479      * @return void
480      */
481     protected function httpOptions($uri) {
482
483         $methods = $this->getAllowedMethods($uri);
484
485         $this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods)));
486         $features = array('1','3', 'extended-mkcol');
487
488         foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
489
490         $this->httpResponse->setHeader('DAV',implode(', ',$features));
491         $this->httpResponse->setHeader('MS-Author-Via','DAV');
492         $this->httpResponse->setHeader('Accept-Ranges','bytes');
493         if (self::$exposeVersion) {
494             $this->httpResponse->setHeader('X-Sabre-Version',Sabre_DAV_Version::VERSION);
495         }
496         $this->httpResponse->setHeader('Content-Length',0);
497         $this->httpResponse->sendStatus(200);
498
499     }
500
501     /**
502      * HTTP GET
503      *
504      * This method simply fetches the contents of a uri, like normal
505      *
506      * @param string $uri
507      * @return bool
508      */
509     protected function httpGet($uri) {
510
511         $node = $this->tree->getNodeForPath($uri,0);
512
513         if (!$this->checkPreconditions(true)) return false;
514
515         if (!$node instanceof Sabre_DAV_IFile) throw new Sabre_DAV_Exception_NotImplemented('GET is only implemented on File objects');
516         $body = $node->get();
517
518         // Converting string into stream, if needed.
519         if (is_string($body)) {
520             $stream = fopen('php://temp','r+');
521             fwrite($stream,$body);
522             rewind($stream);
523             $body = $stream;
524         }
525
526         /*
527          * TODO: getetag, getlastmodified, getsize should also be used using
528          * this method
529          */
530         $httpHeaders = $this->getHTTPHeaders($uri);
531
532         /* ContentType needs to get a default, because many webservers will otherwise
533          * default to text/html, and we don't want this for security reasons.
534          */
535         if (!isset($httpHeaders['Content-Type'])) {
536             $httpHeaders['Content-Type'] = 'application/octet-stream';
537         }
538
539
540         if (isset($httpHeaders['Content-Length'])) {
541
542             $nodeSize = $httpHeaders['Content-Length'];
543
544             // Need to unset Content-Length, because we'll handle that during figuring out the range
545             unset($httpHeaders['Content-Length']);
546
547         } else {
548             $nodeSize = null;
549         }
550
551         $this->httpResponse->setHeaders($httpHeaders);
552
553         $range = $this->getHTTPRange();
554         $ifRange = $this->httpRequest->getHeader('If-Range');
555         $ignoreRangeHeader = false;
556
557         // If ifRange is set, and range is specified, we first need to check
558         // the precondition.
559         if ($nodeSize && $range && $ifRange) {
560
561             // if IfRange is parsable as a date we'll treat it as a DateTime
562             // otherwise, we must treat it as an etag.
563             try {
564                 $ifRangeDate = new DateTime($ifRange);
565
566                 // It's a date. We must check if the entity is modified since
567                 // the specified date.
568                 if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true;
569                 else {
570                     $modified = new DateTime($httpHeaders['Last-Modified']);
571                     if($modified > $ifRangeDate) $ignoreRangeHeader = true;
572                 }
573
574             } catch (Exception $e) {
575
576                 // It's an entity. We can do a simple comparison.
577                 if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true;
578                 elseif ($httpHeaders['ETag']!==$ifRange) $ignoreRangeHeader = true;
579             }
580         }
581
582         // We're only going to support HTTP ranges if the backend provided a filesize
583         if (!$ignoreRangeHeader && $nodeSize && $range) {
584
585             // Determining the exact byte offsets
586             if (!is_null($range[0])) {
587
588                 $start = $range[0];
589                 $end = $range[1]?$range[1]:$nodeSize-1;
590                 if($start >= $nodeSize)
591                     throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
592
593                 if($end < $start) throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')');
594                 if($end >= $nodeSize) $end = $nodeSize-1;
595
596             } else {
597
598                 $start = $nodeSize-$range[1];
599                 $end  = $nodeSize-1;
600
601                 if ($start<0) $start = 0;
602
603             }
604
605             // New read/write stream
606             $newStream = fopen('php://temp','r+');
607
608             stream_copy_to_stream($body, $newStream, $end-$start+1, $start);
609             rewind($newStream);
610
611             $this->httpResponse->setHeader('Content-Length', $end-$start+1);
612             $this->httpResponse->setHeader('Content-Range','bytes ' . $start . '-' . $end . '/' . $nodeSize);
613             $this->httpResponse->sendStatus(206);
614             $this->httpResponse->sendBody($newStream);
615
616
617         } else {
618
619             if ($nodeSize) $this->httpResponse->setHeader('Content-Length',$nodeSize);
620             $this->httpResponse->sendStatus(200);
621             $this->httpResponse->sendBody($body);
622
623         }
624
625     }
626
627     /**
628      * HTTP HEAD
629      *
630      * This method is normally used to take a peak at a url, and only get the HTTP response headers, without the body
631      * This is used by clients to determine if a remote file was changed, so they can use a local cached version, instead of downloading it again
632      *
633      * @param string $uri
634      * @return void
635      */
636     protected function httpHead($uri) {
637
638         $node = $this->tree->getNodeForPath($uri);
639         /* This information is only collection for File objects.
640          * Ideally we want to throw 405 Method Not Allowed for every
641          * non-file, but MS Office does not like this
642          */
643         if ($node instanceof Sabre_DAV_IFile) {
644             $headers = $this->getHTTPHeaders($this->getRequestUri());
645             if (!isset($headers['Content-Type'])) {
646                 $headers['Content-Type'] = 'application/octet-stream';
647             }
648             $this->httpResponse->setHeaders($headers);
649         }
650         $this->httpResponse->sendStatus(200);
651
652     }
653
654     /**
655      * HTTP Delete
656      *
657      * The HTTP delete method, deletes a given uri
658      *
659      * @param string $uri
660      * @return void
661      */
662     protected function httpDelete($uri) {
663
664         if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
665         $this->tree->delete($uri);
666         $this->broadcastEvent('afterUnbind',array($uri));
667
668         $this->httpResponse->sendStatus(204);
669         $this->httpResponse->setHeader('Content-Length','0');
670
671     }
672
673
674     /**
675      * WebDAV PROPFIND
676      *
677      * This WebDAV method requests information about an uri resource, or a list of resources
678      * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
679      * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
680      *
681      * The request body contains an XML data structure that has a list of properties the client understands
682      * The response body is also an xml document, containing information about every uri resource and the requested properties
683      *
684      * It has to return a HTTP 207 Multi-status status code
685      *
686      * @param string $uri
687      * @return void
688      */
689     protected function httpPropfind($uri) {
690
691         // $xml = new Sabre_DAV_XMLReader(file_get_contents('php://input'));
692         $requestedProperties = $this->parsePropfindRequest($this->httpRequest->getBody(true));
693
694         $depth = $this->getHTTPDepth(1);
695         // The only two options for the depth of a propfind is 0 or 1
696         if ($depth!=0) $depth = 1;
697
698         $newProperties = $this->getPropertiesForPath($uri,$requestedProperties,$depth);
699
700         // This is a multi-status response
701         $this->httpResponse->sendStatus(207);
702         $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
703
704         // Normally this header is only needed for OPTIONS responses, however..
705         // iCal seems to also depend on these being set for PROPFIND. Since
706         // this is not harmful, we'll add it.
707         $features = array('1','3', 'extended-mkcol');
708         foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
709         $this->httpResponse->setHeader('DAV',implode(', ',$features));
710
711         $data = $this->generateMultiStatus($newProperties);
712         $this->httpResponse->sendBody($data);
713
714     }
715
716     /**
717      * WebDAV PROPPATCH
718      *
719      * This method is called to update properties on a Node. The request is an XML body with all the mutations.
720      * In this XML body it is specified which properties should be set/updated and/or deleted
721      *
722      * @param string $uri
723      * @return void
724      */
725     protected function httpPropPatch($uri) {
726
727         $newProperties = $this->parsePropPatchRequest($this->httpRequest->getBody(true));
728
729         $result = $this->updateProperties($uri, $newProperties);
730
731         $this->httpResponse->sendStatus(207);
732         $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
733
734         $this->httpResponse->sendBody(
735             $this->generateMultiStatus(array($result))
736         );
737
738     }
739
740     /**
741      * HTTP PUT method
742      *
743      * This HTTP method updates a file, or creates a new one.
744      *
745      * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content
746      *
747      * @param string $uri
748      * @return bool
749      */
750     protected function httpPut($uri) {
751
752         $body = $this->httpRequest->getBody();
753
754         // Intercepting Content-Range
755         if ($this->httpRequest->getHeader('Content-Range')) {
756             /**
757             Content-Range is dangerous for PUT requests:  PUT per definition
758             stores a full resource.  draft-ietf-httpbis-p2-semantics-15 says
759             in section 7.6:
760               An origin server SHOULD reject any PUT request that contains a
761               Content-Range header field, since it might be misinterpreted as
762               partial content (or might be partial content that is being mistakenly
763               PUT as a full representation).  Partial content updates are possible
764               by targeting a separately identified resource with state that
765               overlaps a portion of the larger resource, or by using a different
766               method that has been specifically defined for partial updates (for
767               example, the PATCH method defined in [RFC5789]).
768             This clarifies RFC2616 section 9.6:
769               The recipient of the entity MUST NOT ignore any Content-*
770               (e.g. Content-Range) headers that it does not understand or implement
771               and MUST return a 501 (Not Implemented) response in such cases.
772             OTOH is a PUT request with a Content-Range currently the only way to
773             continue an aborted upload request and is supported by curl, mod_dav,
774             Tomcat and others.  Since some clients do use this feature which results
775             in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject
776             all PUT requests with a Content-Range for now.
777             */
778
779             throw new Sabre_DAV_Exception_NotImplemented('PUT with Content-Range is not allowed.');
780         }
781
782         // Intercepting the Finder problem
783         if (($expected = $this->httpRequest->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
784
785             /**
786             Many webservers will not cooperate well with Finder PUT requests,
787             because it uses 'Chunked' transfer encoding for the request body.
788
789             The symptom of this problem is that Finder sends files to the
790             server, but they arrive as 0-length files in PHP.
791
792             If we don't do anything, the user might think they are uploading
793             files successfully, but they end up empty on the server. Instead,
794             we throw back an error if we detect this.
795
796             The reason Finder uses Chunked, is because it thinks the files
797             might change as it's being uploaded, and therefore the
798             Content-Length can vary.
799
800             Instead it sends the X-Expected-Entity-Length header with the size
801             of the file at the very start of the request. If this header is set,
802             but we don't get a request body we will fail the request to
803             protect the end-user.
804             */
805
806             // Only reading first byte
807             $firstByte = fread($body,1);
808             if (strlen($firstByte)!==1) {
809                 throw new Sabre_DAV_Exception_Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
810             }
811
812             // The body needs to stay intact, so we copy everything to a
813             // temporary stream.
814
815             $newBody = fopen('php://temp','r+');
816             fwrite($newBody,$firstByte);
817             stream_copy_to_stream($body, $newBody);
818             rewind($newBody);
819
820             $body = $newBody;
821
822         }
823
824         if ($this->tree->nodeExists($uri)) {
825
826             $node = $this->tree->getNodeForPath($uri);
827
828             // Checking If-None-Match and related headers.
829             if (!$this->checkPreconditions()) return;
830
831             // If the node is a collection, we'll deny it
832             if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_Conflict('PUT is not allowed on non-files.');
833             if (!$this->broadcastEvent('beforeWriteContent',array($uri, $node, &$body))) return false;
834
835             $etag = $node->put($body);
836
837             $this->broadcastEvent('afterWriteContent',array($uri, $node));
838
839             $this->httpResponse->setHeader('Content-Length','0');
840             if ($etag) $this->httpResponse->setHeader('ETag',$etag);
841             $this->httpResponse->sendStatus(204);
842
843         } else {
844
845             $etag = null;
846             // If we got here, the resource didn't exist yet.
847             if (!$this->createFile($this->getRequestUri(),$body,$etag)) {
848                 // For one reason or another the file was not created.
849                 return;
850             }
851
852             $this->httpResponse->setHeader('Content-Length','0');
853             if ($etag) $this->httpResponse->setHeader('ETag', $etag);
854             $this->httpResponse->sendStatus(201);
855
856         }
857
858     }
859
860
861     /**
862      * WebDAV MKCOL
863      *
864      * The MKCOL method is used to create a new collection (directory) on the server
865      *
866      * @param string $uri
867      * @return void
868      */
869     protected function httpMkcol($uri) {
870
871         $requestBody = $this->httpRequest->getBody(true);
872
873         if ($requestBody) {
874
875             $contentType = $this->httpRequest->getHeader('Content-Type');
876             if (strpos($contentType,'application/xml')!==0 && strpos($contentType,'text/xml')!==0) {
877
878                 // We must throw 415 for unsupported mkcol bodies
879                 throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
880
881             }
882
883             $dom = Sabre_DAV_XMLUtil::loadDOMDocument($requestBody);
884             if (Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild)!=='{DAV:}mkcol') {
885
886                 // We must throw 415 for unsupported mkcol bodies
887                 throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must be a {DAV:}mkcol request construct.');
888
889             }
890
891             $properties = array();
892             foreach($dom->firstChild->childNodes as $childNode) {
893
894                 if (Sabre_DAV_XMLUtil::toClarkNotation($childNode)!=='{DAV:}set') continue;
895                 $properties = array_merge($properties, Sabre_DAV_XMLUtil::parseProperties($childNode, $this->propertyMap));
896
897             }
898             if (!isset($properties['{DAV:}resourcetype']))
899                 throw new Sabre_DAV_Exception_BadRequest('The mkcol request must include a {DAV:}resourcetype property');
900
901             $resourceType = $properties['{DAV:}resourcetype']->getValue();
902             unset($properties['{DAV:}resourcetype']);
903
904         } else {
905
906             $properties = array();
907             $resourceType = array('{DAV:}collection');
908
909         }
910
911         $result = $this->createCollection($uri, $resourceType, $properties);
912
913         if (is_array($result)) {
914             $this->httpResponse->sendStatus(207);
915             $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
916
917             $this->httpResponse->sendBody(
918                 $this->generateMultiStatus(array($result))
919             );
920
921         } else {
922             $this->httpResponse->setHeader('Content-Length','0');
923             $this->httpResponse->sendStatus(201);
924         }
925
926     }
927
928     /**
929      * WebDAV HTTP MOVE method
930      *
931      * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
932      *
933      * @param string $uri
934      * @return bool
935      */
936     protected function httpMove($uri) {
937
938         $moveInfo = $this->getCopyAndMoveInfo();
939
940         // If the destination is part of the source tree, we must fail
941         if ($moveInfo['destination']==$uri)
942             throw new Sabre_DAV_Exception_Forbidden('Source and destination uri are identical.');
943
944         if ($moveInfo['destinationExists']) {
945
946             if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false;
947             $this->tree->delete($moveInfo['destination']);
948             $this->broadcastEvent('afterUnbind',array($moveInfo['destination']));
949
950         }
951
952         if (!$this->broadcastEvent('beforeUnbind',array($uri))) return false;
953         if (!$this->broadcastEvent('beforeBind',array($moveInfo['destination']))) return false;
954         $this->tree->move($uri,$moveInfo['destination']);
955         $this->broadcastEvent('afterUnbind',array($uri));
956         $this->broadcastEvent('afterBind',array($moveInfo['destination']));
957
958         // If a resource was overwritten we should send a 204, otherwise a 201
959         $this->httpResponse->setHeader('Content-Length','0');
960         $this->httpResponse->sendStatus($moveInfo['destinationExists']?204:201);
961
962     }
963
964     /**
965      * WebDAV HTTP COPY method
966      *
967      * This method copies one uri to a different uri, and works much like the MOVE request
968      * A lot of the actual request processing is done in getCopyMoveInfo
969      *
970      * @param string $uri
971      * @return bool
972      */
973     protected function httpCopy($uri) {
974
975         $copyInfo = $this->getCopyAndMoveInfo();
976         // If the destination is part of the source tree, we must fail
977         if ($copyInfo['destination']==$uri)
978             throw new Sabre_DAV_Exception_Forbidden('Source and destination uri are identical.');
979
980         if ($copyInfo['destinationExists']) {
981             if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false;
982             $this->tree->delete($copyInfo['destination']);
983
984         }
985         if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false;
986         $this->tree->copy($uri,$copyInfo['destination']);
987         $this->broadcastEvent('afterBind',array($copyInfo['destination']));
988
989         // If a resource was overwritten we should send a 204, otherwise a 201
990         $this->httpResponse->setHeader('Content-Length','0');
991         $this->httpResponse->sendStatus($copyInfo['destinationExists']?204:201);
992
993     }
994
995
996
997     /**
998      * HTTP REPORT method implementation
999      *
1000      * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
1001      * It's used in a lot of extensions, so it made sense to implement it into the core.
1002      *
1003      * @param string $uri
1004      * @return void
1005      */
1006     protected function httpReport($uri) {
1007
1008         $body = $this->httpRequest->getBody(true);
1009         $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
1010
1011         $reportName = Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild);
1012
1013         if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) {
1014
1015             // If broadcastEvent returned true, it means the report was not supported
1016             throw new Sabre_DAV_Exception_ReportNotImplemented();
1017
1018         }
1019
1020     }
1021
1022     // }}}
1023     // {{{ HTTP/WebDAV protocol helpers
1024
1025     /**
1026      * Returns an array with all the supported HTTP methods for a specific uri.
1027      *
1028      * @param string $uri
1029      * @return array
1030      */
1031     public function getAllowedMethods($uri) {
1032
1033         $methods = array(
1034             'OPTIONS',
1035             'GET',
1036             'HEAD',
1037             'DELETE',
1038             'PROPFIND',
1039             'PUT',
1040             'PROPPATCH',
1041             'COPY',
1042             'MOVE',
1043             'REPORT'
1044         );
1045
1046         // The MKCOL is only allowed on an unmapped uri
1047         try {
1048             $this->tree->getNodeForPath($uri);
1049         } catch (Sabre_DAV_Exception_NotFound $e) {
1050             $methods[] = 'MKCOL';
1051         }
1052
1053         // We're also checking if any of the addons register any new methods
1054         foreach($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($uri));
1055         array_unique($methods);
1056
1057         return $methods;
1058
1059     }
1060
1061     /**
1062      * Gets the uri for the request, keeping the base uri into consideration
1063      *
1064      * @return string
1065      */
1066     public function getRequestUri() {
1067
1068         return $this->calculateUri($this->httpRequest->getUri());
1069
1070     }
1071
1072     /**
1073      * Calculates the uri for a request, making sure that the base uri is stripped out
1074      *
1075      * @param string $uri
1076      * @throws Sabre_DAV_Exception_Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
1077      * @return string
1078      */
1079     public function calculateUri($uri) {
1080
1081         if ($uri[0]!='/' && strpos($uri,'://')) {
1082
1083             $uri = parse_url($uri,PHP_URL_PATH);
1084
1085         }
1086
1087         $uri = str_replace('//','/',$uri);
1088
1089         if (strpos($uri,$this->getBaseUri())===0) {
1090
1091             return trim(Sabre_DAV_URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/');
1092
1093         // A special case, if the baseUri was accessed without a trailing
1094         // slash, we'll accept it as well.
1095         } elseif ($uri.'/' === $this->getBaseUri()) {
1096
1097             return '';
1098
1099         } else {
1100
1101             throw new Sabre_DAV_Exception_Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
1102
1103         }
1104
1105     }
1106
1107     /**
1108      * Returns the HTTP depth header
1109      *
1110      * 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
1111      * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
1112      *
1113      * @param mixed $default
1114      * @return int
1115      */
1116     public function getHTTPDepth($default = self::DEPTH_INFINITY) {
1117
1118         // If its not set, we'll grab the default
1119         $depth = $this->httpRequest->getHeader('Depth');
1120
1121         if (is_null($depth)) return $default;
1122
1123         if ($depth == 'infinity') return self::DEPTH_INFINITY;
1124
1125
1126         // If its an unknown value. we'll grab the default
1127         if (!ctype_digit($depth)) return $default;
1128
1129         return (int)$depth;
1130
1131     }
1132
1133     /**
1134      * Returns the HTTP range header
1135      *
1136      * This method returns null if there is no well-formed HTTP range request
1137      * header or array($start, $end).
1138      *
1139      * The first number is the offset of the first byte in the range.
1140      * The second number is the offset of the last byte in the range.
1141      *
1142      * If the second offset is null, it should be treated as the offset of the last byte of the entity
1143      * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
1144      *
1145      * @return array|null
1146      */
1147     public function getHTTPRange() {
1148
1149         $range = $this->httpRequest->getHeader('range');
1150         if (is_null($range)) return null;
1151
1152         // Matching "Range: bytes=1234-5678: both numbers are optional
1153
1154         if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null;
1155
1156         if ($matches[1]==='' && $matches[2]==='') return null;
1157
1158         return array(
1159             $matches[1]!==''?$matches[1]:null,
1160             $matches[2]!==''?$matches[2]:null,
1161         );
1162
1163     }
1164
1165
1166     /**
1167      * Returns information about Copy and Move requests
1168      *
1169      * This function is created to help getting information about the source and the destination for the
1170      * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
1171      *
1172      * The returned value is an array with the following keys:
1173      *   * destination - Destination path
1174      *   * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
1175      *
1176      * @return array
1177      */
1178     public function getCopyAndMoveInfo() {
1179
1180         // Collecting the relevant HTTP headers
1181         if (!$this->httpRequest->getHeader('Destination')) throw new Sabre_DAV_Exception_BadRequest('The destination header was not supplied');
1182         $destination = $this->calculateUri($this->httpRequest->getHeader('Destination'));
1183         $overwrite = $this->httpRequest->getHeader('Overwrite');
1184         if (!$overwrite) $overwrite = 'T';
1185         if (strtoupper($overwrite)=='T') $overwrite = true;
1186         elseif (strtoupper($overwrite)=='F') $overwrite = false;
1187         // We need to throw a bad request exception, if the header was invalid
1188         else throw new Sabre_DAV_Exception_BadRequest('The HTTP Overwrite header should be either T or F');
1189
1190         list($destinationDir) = Sabre_DAV_URLUtil::splitPath($destination);
1191
1192         try {
1193             $destinationParent = $this->tree->getNodeForPath($destinationDir);
1194             if (!($destinationParent instanceof Sabre_DAV_ICollection)) throw new Sabre_DAV_Exception_UnsupportedMediaType('The destination node is not a collection');
1195         } catch (Sabre_DAV_Exception_NotFound $e) {
1196
1197             // If the destination parent node is not found, we throw a 409
1198             throw new Sabre_DAV_Exception_Conflict('The destination node is not found');
1199         }
1200
1201         try {
1202
1203             $destinationNode = $this->tree->getNodeForPath($destination);
1204
1205             // If this succeeded, it means the destination already exists
1206             // we'll need to throw precondition failed in case overwrite is false
1207             if (!$overwrite) throw new Sabre_DAV_Exception_PreconditionFailed('The destination node already exists, and the overwrite header is set to false','Overwrite');
1208
1209         } catch (Sabre_DAV_Exception_NotFound $e) {
1210
1211             // Destination didn't exist, we're all good
1212             $destinationNode = false;
1213
1214
1215
1216         }
1217
1218         // These are the three relevant properties we need to return
1219         return array(
1220             'destination'       => $destination,
1221             'destinationExists' => $destinationNode==true,
1222             'destinationNode'   => $destinationNode,
1223         );
1224
1225     }
1226
1227     /**
1228      * Returns a list of properties for a path
1229      *
1230      * This is a simplified version getPropertiesForPath.
1231      * if you aren't interested in status codes, but you just
1232      * want to have a flat list of properties. Use this method.
1233      *
1234      * @param string $path
1235      * @param array $propertyNames
1236      */
1237     public function getProperties($path, $propertyNames) {
1238
1239         $result = $this->getPropertiesForPath($path,$propertyNames,0);
1240         return $result[0][200];
1241
1242     }
1243
1244     /**
1245      * A kid-friendly way to fetch properties for a node's children.
1246      *
1247      * The returned array will be indexed by the path of the of child node.
1248      * Only properties that are actually found will be returned.
1249      *
1250      * The parent node will not be returned.
1251      *
1252      * @param string $path
1253      * @param array $propertyNames
1254      * @return array
1255      */
1256     public function getPropertiesForChildren($path, $propertyNames) {
1257
1258         $result = array();
1259         foreach($this->getPropertiesForPath($path,$propertyNames,1) as $k=>$row) {
1260
1261             // Skipping the parent path
1262             if ($k === 0) continue;
1263
1264             $result[$row['href']] = $row[200];
1265
1266         }
1267         return $result;
1268
1269     }
1270
1271     /**
1272      * Returns a list of HTTP headers for a particular resource
1273      *
1274      * The generated http headers are based on properties provided by the
1275      * resource. The method basically provides a simple mapping between
1276      * DAV property and HTTP header.
1277      *
1278      * The headers are intended to be used for HEAD and GET requests.
1279      *
1280      * @param string $path
1281      * @return array
1282      */
1283     public function getHTTPHeaders($path) {
1284
1285         $propertyMap = array(
1286             '{DAV:}getcontenttype'   => 'Content-Type',
1287             '{DAV:}getcontentlength' => 'Content-Length',
1288             '{DAV:}getlastmodified'  => 'Last-Modified',
1289             '{DAV:}getetag'          => 'ETag',
1290         );
1291
1292         $properties = $this->getProperties($path,array_keys($propertyMap));
1293
1294         $headers = array();
1295         foreach($propertyMap as $property=>$header) {
1296             if (!isset($properties[$property])) continue;
1297
1298             if (is_scalar($properties[$property])) {
1299                 $headers[$header] = $properties[$property];
1300
1301             // GetLastModified gets special cased
1302             } elseif ($properties[$property] instanceof Sabre_DAV_Property_GetLastModified) {
1303                 $headers[$header] = Sabre_HTTP_Util::toHTTPDate($properties[$property]->getTime());
1304             }
1305
1306         }
1307
1308         return $headers;
1309
1310     }
1311
1312     /**
1313      * Returns a list of properties for a given path
1314      *
1315      * The path that should be supplied should have the baseUrl stripped out
1316      * The list of properties should be supplied in Clark notation. If the list is empty
1317      * 'allprops' is assumed.
1318      *
1319      * If a depth of 1 is requested child elements will also be returned.
1320      *
1321      * @param string $path
1322      * @param array $propertyNames
1323      * @param int $depth
1324      * @return array
1325      */
1326     public function getPropertiesForPath($path, $propertyNames = array(), $depth = 0) {
1327
1328         if ($depth!=0) $depth = 1;
1329
1330         $returnPropertyList = array();
1331
1332         $parentNode = $this->tree->getNodeForPath($path);
1333         $nodes = array(
1334             $path => $parentNode
1335         );
1336         if ($depth==1 && $parentNode instanceof Sabre_DAV_ICollection) {
1337             foreach($this->tree->getChildren($path) as $childNode)
1338                 $nodes[$path . '/' . $childNode->getName()] = $childNode;
1339         }
1340
1341         // If the propertyNames array is empty, it means all properties are requested.
1342         // We shouldn't actually return everything we know though, and only return a
1343         // sensible list.
1344         $allProperties = count($propertyNames)==0;
1345
1346         foreach($nodes as $myPath=>$node) {
1347
1348             $currentPropertyNames = $propertyNames;
1349
1350             $newProperties = array(
1351                 '200' => array(),
1352                 '404' => array(),
1353             );
1354
1355             if ($allProperties) {
1356                 // Default list of propertyNames, when all properties were requested.
1357                 $currentPropertyNames = array(
1358                     '{DAV:}getlastmodified',
1359                     '{DAV:}getcontentlength',
1360                     '{DAV:}resourcetype',
1361                     '{DAV:}quota-used-bytes',
1362                     '{DAV:}quota-available-bytes',
1363                     '{DAV:}getetag',
1364                     '{DAV:}getcontenttype',
1365                 );
1366             }
1367
1368             // If the resourceType was not part of the list, we manually add it
1369             // and mark it for removal. We need to know the resourcetype in order
1370             // to make certain decisions about the entry.
1371             // WebDAV dictates we should add a / and the end of href's for collections
1372             $removeRT = false;
1373             if (!in_array('{DAV:}resourcetype',$currentPropertyNames)) {
1374                 $currentPropertyNames[] = '{DAV:}resourcetype';
1375                 $removeRT = true;
1376             }
1377
1378             $result = $this->broadcastEvent('beforeGetProperties',array($myPath, $node, &$currentPropertyNames, &$newProperties));
1379             // If this method explicitly returned false, we must ignore this
1380             // node as it is inaccessible.
1381             if ($result===false) continue;
1382
1383             if (count($currentPropertyNames) > 0) {
1384
1385                 if ($node instanceof Sabre_DAV_IProperties)
1386                     $newProperties['200'] = $newProperties[200] + $node->getProperties($currentPropertyNames);
1387
1388             }
1389
1390
1391             foreach($currentPropertyNames as $prop) {
1392
1393                 if (isset($newProperties[200][$prop])) continue;
1394
1395                 switch($prop) {
1396                     case '{DAV:}getlastmodified'       : if ($node->getLastModified()) $newProperties[200][$prop] = new Sabre_DAV_Property_GetLastModified($node->getLastModified()); break;
1397                     case '{DAV:}getcontentlength'      :
1398                         if ($node instanceof Sabre_DAV_IFile) {
1399                             $size = $node->getSize();
1400                             if (!is_null($size)) {
1401                                 $newProperties[200][$prop] = (int)$node->getSize();
1402                             }
1403                         }
1404                         break;
1405                     case '{DAV:}quota-used-bytes'      :
1406                         if ($node instanceof Sabre_DAV_IQuota) {
1407                             $quotaInfo = $node->getQuotaInfo();
1408                             $newProperties[200][$prop] = $quotaInfo[0];
1409                         }
1410                         break;
1411                     case '{DAV:}quota-available-bytes' :
1412                         if ($node instanceof Sabre_DAV_IQuota) {
1413                             $quotaInfo = $node->getQuotaInfo();
1414                             $newProperties[200][$prop] = $quotaInfo[1];
1415                         }
1416                         break;
1417                     case '{DAV:}getetag'               : if ($node instanceof Sabre_DAV_IFile && $etag = $node->getETag())  $newProperties[200][$prop] = $etag; break;
1418                     case '{DAV:}getcontenttype'        : if ($node instanceof Sabre_DAV_IFile && $ct = $node->getContentType())  $newProperties[200][$prop] = $ct; break;
1419                     case '{DAV:}supported-report-set'  :
1420                         $reports = array();
1421                         foreach($this->plugins as $plugin) {
1422                             $reports = array_merge($reports, $plugin->getSupportedReportSet($myPath));
1423                         }
1424                         $newProperties[200][$prop] = new Sabre_DAV_Property_SupportedReportSet($reports);
1425                         break;
1426                     case '{DAV:}resourcetype' :
1427                         $newProperties[200]['{DAV:}resourcetype'] = new Sabre_DAV_Property_ResourceType();
1428                         foreach($this->resourceTypeMapping as $className => $resourceType) {
1429                             if ($node instanceof $className) $newProperties[200]['{DAV:}resourcetype']->add($resourceType);
1430                         }
1431                         break;
1432
1433                 }
1434
1435                 // If we were unable to find the property, we will list it as 404.
1436                 if (!$allProperties && !isset($newProperties[200][$prop])) $newProperties[404][$prop] = null;
1437
1438             }
1439
1440             $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties));
1441
1442             $newProperties['href'] = trim($myPath,'/');
1443
1444             // Its is a WebDAV recommendation to add a trailing slash to collectionnames.
1445             // Apple's iCal also requires a trailing slash for principals (rfc 3744).
1446             // Therefore we add a trailing / for any non-file. This might need adjustments
1447             // if we find there are other edge cases.
1448             if ($myPath!='' && isset($newProperties[200]['{DAV:}resourcetype']) && count($newProperties[200]['{DAV:}resourcetype']->getValue())>0) $newProperties['href'] .='/';
1449
1450             // If the resourcetype property was manually added to the requested property list,
1451             // we will remove it again.
1452             if ($removeRT) unset($newProperties[200]['{DAV:}resourcetype']);
1453
1454             $returnPropertyList[] = $newProperties;
1455
1456         }
1457
1458         return $returnPropertyList;
1459
1460     }
1461
1462     /**
1463      * This method is invoked by sub-systems creating a new file.
1464      *
1465      * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
1466      * It was important to get this done through a centralized function,
1467      * allowing addons to intercept this using the beforeCreateFile event.
1468      *
1469      * This method will return true if the file was actually created
1470      *
1471      * @param string   $uri
1472      * @param resource $data
1473      * @param string   $etag
1474      * @return bool
1475      */
1476     public function createFile($uri,$data, &$etag = null) {
1477
1478         list($dir,$name) = Sabre_DAV_URLUtil::splitPath($uri);
1479
1480         if (!$this->broadcastEvent('beforeBind',array($uri))) return false;
1481
1482         $parent = $this->tree->getNodeForPath($dir);
1483
1484         if (!$this->broadcastEvent('beforeCreateFile',array($uri, &$data, $parent))) return false;
1485
1486         $etag = $parent->createFile($name,$data);
1487         $this->tree->markDirty($dir);
1488
1489         $this->broadcastEvent('afterBind',array($uri));
1490         $this->broadcastEvent('afterCreateFile',array($uri, $parent));
1491
1492         return true;
1493     }
1494
1495     /**
1496      * This method is invoked by sub-systems creating a new directory.
1497      *
1498      * @param string $uri
1499      * @return void
1500      */
1501     public function createDirectory($uri) {
1502
1503         $this->createCollection($uri,array('{DAV:}collection'),array());
1504
1505     }
1506
1507     /**
1508      * Use this method to create a new collection
1509      *
1510      * The {DAV:}resourcetype is specified using the resourceType array.
1511      * At the very least it must contain {DAV:}collection.
1512      *
1513      * The properties array can contain a list of additional properties.
1514      *
1515      * @param string $uri The new uri
1516      * @param array $resourceType The resourceType(s)
1517      * @param array $properties A list of properties
1518      * @return array|null
1519      */
1520     public function createCollection($uri, array $resourceType, array $properties) {
1521
1522         list($parentUri,$newName) = Sabre_DAV_URLUtil::splitPath($uri);
1523
1524         // Making sure {DAV:}collection was specified as resourceType
1525         if (!in_array('{DAV:}collection', $resourceType)) {
1526             throw new Sabre_DAV_Exception_InvalidResourceType('The resourceType for this collection must at least include {DAV:}collection');
1527         }
1528
1529
1530         // Making sure the parent exists
1531         try {
1532
1533             $parent = $this->tree->getNodeForPath($parentUri);
1534
1535         } catch (Sabre_DAV_Exception_NotFound $e) {
1536
1537             throw new Sabre_DAV_Exception_Conflict('Parent node does not exist');
1538
1539         }
1540
1541         // Making sure the parent is a collection
1542         if (!$parent instanceof Sabre_DAV_ICollection) {
1543             throw new Sabre_DAV_Exception_Conflict('Parent node is not a collection');
1544         }
1545
1546
1547
1548         // Making sure the child does not already exist
1549         try {
1550             $parent->getChild($newName);
1551
1552             // If we got here.. it means there's already a node on that url, and we need to throw a 405
1553             throw new Sabre_DAV_Exception_MethodNotAllowed('The resource you tried to create already exists');
1554
1555         } catch (Sabre_DAV_Exception_NotFound $e) {
1556             // This is correct
1557         }
1558
1559
1560         if (!$this->broadcastEvent('beforeBind',array($uri))) return;
1561
1562         // There are 2 modes of operation. The standard collection
1563         // creates the directory, and then updates properties
1564         // the extended collection can create it directly.
1565         if ($parent instanceof Sabre_DAV_IExtendedCollection) {
1566
1567             $parent->createExtendedCollection($newName, $resourceType, $properties);
1568
1569         } else {
1570
1571             // No special resourcetypes are supported
1572             if (count($resourceType)>1) {
1573                 throw new Sabre_DAV_Exception_InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
1574             }
1575
1576             $parent->createDirectory($newName);
1577             $rollBack = false;
1578             $exception = null;
1579             $errorResult = null;
1580
1581             if (count($properties)>0) {
1582
1583                 try {
1584
1585                     $errorResult = $this->updateProperties($uri, $properties);
1586                     if (!isset($errorResult[200])) {
1587                         $rollBack = true;
1588                     }
1589
1590                 } catch (Sabre_DAV_Exception $e) {
1591
1592                     $rollBack = true;
1593                     $exception = $e;
1594
1595                 }
1596
1597             }
1598
1599             if ($rollBack) {
1600                 if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
1601                 $this->tree->delete($uri);
1602
1603                 // Re-throwing exception
1604                 if ($exception) throw $exception;
1605
1606                 return $errorResult;
1607             }
1608
1609         }
1610         $this->tree->markDirty($parentUri);
1611         $this->broadcastEvent('afterBind',array($uri));
1612
1613     }
1614
1615     /**
1616      * This method updates a resource's properties
1617      *
1618      * The properties array must be a list of properties. Array-keys are
1619      * property names in clarknotation, array-values are it's values.
1620      * If a property must be deleted, the value should be null.
1621      *
1622      * Note that this request should either completely succeed, or
1623      * completely fail.
1624      *
1625      * The response is an array with statuscodes for keys, which in turn
1626      * contain arrays with propertynames. This response can be used
1627      * to generate a multistatus body.
1628      *
1629      * @param string $uri
1630      * @param array $properties
1631      * @return array
1632      */
1633     public function updateProperties($uri, array $properties) {
1634
1635         // we'll start by grabbing the node, this will throw the appropriate
1636         // exceptions if it doesn't.
1637         $node = $this->tree->getNodeForPath($uri);
1638
1639         $result = array(
1640             200 => array(),
1641             403 => array(),
1642             424 => array(),
1643         );
1644         $remainingProperties = $properties;
1645         $hasError = false;
1646
1647         // Running through all properties to make sure none of them are protected
1648         if (!$hasError) foreach($properties as $propertyName => $value) {
1649             if(in_array($propertyName, $this->protectedProperties)) {
1650                 $result[403][$propertyName] = null;
1651                 unset($remainingProperties[$propertyName]);
1652                 $hasError = true;
1653             }
1654         }
1655
1656         if (!$hasError) {
1657             // Allowing addons to take care of property updating
1658             $hasError = !$this->broadcastEvent('updateProperties',array(
1659                 &$remainingProperties,
1660                 &$result,
1661                 $node
1662             ));
1663         }
1664
1665         // If the node is not an instance of Sabre_DAV_IProperties, every
1666         // property is 403 Forbidden
1667         if (!$hasError && count($remainingProperties) && !($node instanceof Sabre_DAV_IProperties)) {
1668             $hasError = true;
1669             foreach($properties as $propertyName=> $value) {
1670                 $result[403][$propertyName] = null;
1671             }
1672             $remainingProperties = array();
1673         }
1674
1675         // Only if there were no errors we may attempt to update the resource
1676         if (!$hasError) {
1677
1678             if (count($remainingProperties)>0) {
1679
1680                 $updateResult = $node->updateProperties($remainingProperties);
1681
1682                 if ($updateResult===true) {
1683                     // success
1684                     foreach($remainingProperties as $propertyName=>$value) {
1685                         $result[200][$propertyName] = null;
1686                     }
1687
1688                 } elseif ($updateResult===false) {
1689                     // The node failed to update the properties for an
1690                     // unknown reason
1691                     foreach($remainingProperties as $propertyName=>$value) {
1692                         $result[403][$propertyName] = null;
1693                     }
1694
1695                 } elseif (is_array($updateResult)) {
1696
1697                     // The node has detailed update information
1698                     // We need to merge the results with the earlier results.
1699                     foreach($updateResult as $status => $props) {
1700                         if (is_array($props)) {
1701                             if (!isset($result[$status]))
1702                                 $result[$status] = array();
1703
1704                             $result[$status] = array_merge($result[$status], $updateResult[$status]);
1705                         }
1706                     }
1707
1708                 } else {
1709                     throw new Sabre_DAV_Exception('Invalid result from updateProperties');
1710                 }
1711                 $remainingProperties = array();
1712             }
1713
1714         }
1715
1716         foreach($remainingProperties as $propertyName=>$value) {
1717             // if there are remaining properties, it must mean
1718             // there's a dependency failure
1719             $result[424][$propertyName] = null;
1720         }
1721
1722         // Removing empty array values
1723         foreach($result as $status=>$props) {
1724
1725             if (count($props)===0) unset($result[$status]);
1726
1727         }
1728         $result['href'] = $uri;
1729         return $result;
1730
1731     }
1732
1733     /**
1734      * This method checks the main HTTP preconditions.
1735      *
1736      * Currently these are:
1737      *   * If-Match
1738      *   * If-None-Match
1739      *   * If-Modified-Since
1740      *   * If-Unmodified-Since
1741      *
1742      * The method will return true if all preconditions are met
1743      * The method will return false, or throw an exception if preconditions
1744      * failed. If false is returned the operation should be aborted, and
1745      * the appropriate HTTP response headers are already set.
1746      *
1747      * Normally this method will throw 412 Precondition Failed for failures
1748      * related to If-None-Match, If-Match and If-Unmodified Since. It will
1749      * set the status to 304 Not Modified for If-Modified_since.
1750      *
1751      * If the $handleAsGET argument is set to true, it will also return 304
1752      * Not Modified for failure of the If-None-Match precondition. This is the
1753      * desired behaviour for HTTP GET and HTTP HEAD requests.
1754      *
1755      * @param bool $handleAsGET
1756      * @return bool
1757      */
1758     public function checkPreconditions($handleAsGET = false) {
1759
1760         $uri = $this->getRequestUri();
1761         $node = null;
1762         $lastMod = null;
1763         $etag = null;
1764
1765         if ($ifMatch = $this->httpRequest->getHeader('If-Match')) {
1766
1767             // If-Match contains an entity tag. Only if the entity-tag
1768             // matches we are allowed to make the request succeed.
1769             // If the entity-tag is '*' we are only allowed to make the
1770             // request succeed if a resource exists at that url.
1771             try {
1772                 $node = $this->tree->getNodeForPath($uri);
1773             } catch (Sabre_DAV_Exception_NotFound $e) {
1774                 throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified and the resource did not exist','If-Match');
1775             }
1776
1777             // Only need to check entity tags if they are not *
1778             if ($ifMatch!=='*') {
1779
1780                 // There can be multiple etags
1781                 $ifMatch = explode(',',$ifMatch);
1782                 $haveMatch = false;
1783                 foreach($ifMatch as $ifMatchItem) {
1784
1785                     // Stripping any extra spaces
1786                     $ifMatchItem = trim($ifMatchItem,' ');
1787
1788                     $etag = $node->getETag();
1789                     if ($etag===$ifMatchItem) {
1790                         $haveMatch = true;
1791                     } else {
1792                         // Evolution has a bug where it sometimes prepends the "
1793                         // with a \. This is our workaround.
1794                         if (str_replace('\\"','"', $ifMatchItem) === $etag) {
1795                             $haveMatch = true;
1796                         }
1797                     }
1798
1799                 }
1800                 if (!$haveMatch) {
1801                      throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
1802                 }
1803             }
1804         }
1805
1806         if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) {
1807
1808             // The If-None-Match header contains an etag.
1809             // Only if the ETag does not match the current ETag, the request will succeed
1810             // The header can also contain *, in which case the request
1811             // will only succeed if the entity does not exist at all.
1812             $nodeExists = true;
1813             if (!$node) {
1814                 try {
1815                     $node = $this->tree->getNodeForPath($uri);
1816                 } catch (Sabre_DAV_Exception_NotFound $e) {
1817                     $nodeExists = false;
1818                 }
1819             }
1820             if ($nodeExists) {
1821                 $haveMatch = false;
1822                 if ($ifNoneMatch==='*') $haveMatch = true;
1823                 else {
1824
1825                     // There might be multiple etags
1826                     $ifNoneMatch = explode(',', $ifNoneMatch);
1827                     $etag = $node->getETag();
1828
1829                     foreach($ifNoneMatch as $ifNoneMatchItem) {
1830
1831                         // Stripping any extra spaces
1832                         $ifNoneMatchItem = trim($ifNoneMatchItem,' ');
1833
1834                         if ($etag===$ifNoneMatchItem) $haveMatch = true;
1835
1836                     }
1837
1838                 }
1839
1840                 if ($haveMatch) {
1841                     if ($handleAsGET) {
1842                         $this->httpResponse->sendStatus(304);
1843                         return false;
1844                     } else {
1845                         throw new Sabre_DAV_Exception_PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match');
1846                     }
1847                 }
1848             }
1849
1850         }
1851
1852         if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) {
1853
1854             // The If-Modified-Since header contains a date. We
1855             // will only return the entity if it has been changed since
1856             // that date. If it hasn't been changed, we return a 304
1857             // header
1858             // Note that this header only has to be checked if there was no If-None-Match header
1859             // as per the HTTP spec.
1860             $date = Sabre_HTTP_Util::parseHTTPDate($ifModifiedSince);
1861
1862             if ($date) {
1863                 if (is_null($node)) {
1864                     $node = $this->tree->getNodeForPath($uri);
1865                 }
1866                 $lastMod = $node->getLastModified();
1867                 if ($lastMod) {
1868                     $lastMod = new DateTime('@' . $lastMod);
1869                     if ($lastMod <= $date) {
1870                         $this->httpResponse->sendStatus(304);
1871                         $this->httpResponse->setHeader('Last-Modified', Sabre_HTTP_Util::toHTTPDate($lastMod));
1872                         return false;
1873                     }
1874                 }
1875             }
1876         }
1877
1878         if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) {
1879
1880             // The If-Unmodified-Since will allow allow the request if the
1881             // entity has not changed since the specified date.
1882             $date = Sabre_HTTP_Util::parseHTTPDate($ifUnmodifiedSince);
1883
1884             // We must only check the date if it's valid
1885             if ($date) {
1886                 if (is_null($node)) {
1887                     $node = $this->tree->getNodeForPath($uri);
1888                 }
1889                 $lastMod = $node->getLastModified();
1890                 if ($lastMod) {
1891                     $lastMod = new DateTime('@' . $lastMod);
1892                     if ($lastMod > $date) {
1893                         throw new Sabre_DAV_Exception_PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.','If-Unmodified-Since');
1894                     }
1895                 }
1896             }
1897
1898         }
1899         return true;
1900
1901     }
1902
1903     // }}}
1904     // {{{ XML Readers & Writers
1905
1906
1907     /**
1908      * Generates a WebDAV propfind response body based on a list of nodes
1909      *
1910      * @param array $fileProperties The list with nodes
1911      * @return string
1912      */
1913     public function generateMultiStatus(array $fileProperties) {
1914
1915         $dom = new DOMDocument('1.0','utf-8');
1916         //$dom->formatOutput = true;
1917         $multiStatus = $dom->createElement('d:multistatus');
1918         $dom->appendChild($multiStatus);
1919
1920         // Adding in default namespaces
1921         foreach($this->xmlNamespaces as $namespace=>$prefix) {
1922
1923             $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
1924
1925         }
1926
1927         foreach($fileProperties as $entry) {
1928
1929             $href = $entry['href'];
1930             unset($entry['href']);
1931
1932             $response = new Sabre_DAV_Property_Response($href,$entry);
1933             $response->serialize($this,$multiStatus);
1934
1935         }
1936
1937         return $dom->saveXML();
1938
1939     }
1940
1941     /**
1942      * This method parses a PropPatch request
1943      *
1944      * PropPatch changes the properties for a resource. This method
1945      * returns a list of properties.
1946      *
1947      * The keys in the returned array contain the property name (e.g.: {DAV:}displayname,
1948      * and the value contains the property value. If a property is to be removed the value
1949      * will be null.
1950      *
1951      * @param string $body xml body
1952      * @return array list of properties in need of updating or deletion
1953      */
1954     public function parsePropPatchRequest($body) {
1955
1956         //We'll need to change the DAV namespace declaration to something else in order to make it parsable
1957         $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
1958
1959         $newProperties = array();
1960
1961         foreach($dom->firstChild->childNodes as $child) {
1962
1963             if ($child->nodeType !== XML_ELEMENT_NODE) continue;
1964
1965             $operation = Sabre_DAV_XMLUtil::toClarkNotation($child);
1966
1967             if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue;
1968
1969             $innerProperties = Sabre_DAV_XMLUtil::parseProperties($child, $this->propertyMap);
1970
1971             foreach($innerProperties as $propertyName=>$propertyValue) {
1972
1973                 if ($operation==='{DAV:}remove') {
1974                     $propertyValue = null;
1975                 }
1976
1977                 $newProperties[$propertyName] = $propertyValue;
1978
1979             }
1980
1981         }
1982
1983         return $newProperties;
1984
1985     }
1986
1987     /**
1988      * This method parses the PROPFIND request and returns its information
1989      *
1990      * This will either be a list of properties, or an empty array; in which case
1991      * an {DAV:}allprop was requested.
1992      *
1993      * @param string $body
1994      * @return array
1995      */
1996     public function parsePropFindRequest($body) {
1997
1998         // If the propfind body was empty, it means IE is requesting 'all' properties
1999         if (!$body) return array();
2000
2001         $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
2002         $elem = $dom->getElementsByTagNameNS('DAV:','propfind')->item(0);
2003         return array_keys(Sabre_DAV_XMLUtil::parseProperties($elem));
2004
2005     }
2006
2007     // }}}
2008
2009 }
2010