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