4 * Main DAV server class
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
12 class Sabre_DAV_Server {
15 * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree
17 const DEPTH_INFINITY = -1;
20 * Nodes that are files, should have this as the type property
25 * Nodes that are directories, should use this value as the type property
27 const NODE_DIRECTORY = 2;
30 * XML namespace for all SabreDAV related elements
32 const NS_SABREDAV = 'http://sabredav.org/ns';
46 protected $baseUri = null;
51 * @var Sabre_HTTP_Response
58 * @var Sabre_HTTP_Request
67 protected $plugins = array();
70 * This array contains a list of callbacks we should call when certain events are triggered
74 protected $eventSubscriptions = array();
77 * This is a default list of namespaces.
79 * If you are defining your own custom namespace, add it here to reduce
80 * bandwidth and improve legibility of xml bodies.
84 public $xmlNamespaces = array(
86 'http://sabredav.org/ns' => 's',
90 * The propertymap can be used to map properties from
91 * requests to property classes.
95 public $propertyMap = array(
96 '{DAV:}resourcetype' => 'Sabre_DAV_Property_ResourceType',
99 public $protectedProperties = array(
101 '{DAV:}getcontentlength',
103 '{DAV:}getlastmodified',
104 '{DAV:}lockdiscovery',
105 '{DAV:}resourcetype',
106 '{DAV:}supportedlock',
109 '{DAV:}quota-available-bytes',
110 '{DAV:}quota-used-bytes',
113 '{DAV:}supported-privilege-set',
114 '{DAV:}current-user-privilege-set',
116 '{DAV:}acl-restrictions',
117 '{DAV:}inherited-acl-set',
122 * This is a flag that allow or not showing file, line and code
123 * of the exception in the returned XML
127 public $debugExceptions = false;
130 * This property allows you to automatically add the 'resourcetype' value
131 * based on a node's classname or interface.
133 * The preset ensures that {DAV:}collection is automaticlly added for nodes
134 * implementing Sabre_DAV_ICollection.
138 public $resourceTypeMapping = array(
139 'Sabre_DAV_ICollection' => '{DAV:}collection',
143 * If this setting is turned off, SabreDAV's version number will be hidden
144 * from various places.
146 * Some people feel this is a good security measure.
150 static public $exposeVersion = true;
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.
159 * If nothing is passed, a Sabre_DAV_SimpleCollection is created in
160 * a Sabre_DAV_ObjectTree.
162 * If an array is passed, we automatically create a root node, and use
163 * the nodes in the array as top-level children.
165 * @param Sabre_DAV_Tree|Sabre_DAV_INode|array|null $treeOrNode The tree object
167 public function __construct($treeOrNode = null) {
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)) {
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');
183 $root = new Sabre_DAV_SimpleCollection('root', $treeOrNode);
184 $this->tree = new Sabre_DAV_ObjectTree($root);
186 } elseif (is_null($treeOrNode)) {
187 $root = new Sabre_DAV_SimpleCollection('root');
188 $this->tree = new Sabre_DAV_ObjectTree($root);
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');
192 $this->httpResponse = new Sabre_HTTP_Response();
193 $this->httpRequest = new Sabre_HTTP_Request();
198 * Starts the DAV Server
202 public function exec() {
206 $this->invokeMethod($this->httpRequest->getMethod(), $this->getRequestUri());
208 } catch (Exception $e) {
210 $DOM = new DOMDocument('1.0','utf-8');
211 $DOM->formatOutput = true;
213 $error = $DOM->createElementNS('DAV:','d:error');
214 $error->setAttribute('xmlns:s',self::NS_SABREDAV);
215 $DOM->appendChild($error);
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()));
226 if (self::$exposeVersion) {
227 $error->appendChild($DOM->createElement('s:sabredav-version',Sabre_DAV_Version::VERSION));
230 if($e instanceof Sabre_DAV_Exception) {
232 $httpCode = $e->getHTTPCode();
233 $e->serialize($this,$error);
234 $headers = $e->getHTTPHeaders($this);
242 $headers['Content-Type'] = 'application/xml; charset=utf-8';
244 $this->httpResponse->sendStatus($httpCode);
245 $this->httpResponse->setHeaders($headers);
246 $this->httpResponse->sendBody($DOM->saveXML());
253 * Sets the base server uri
258 public function setBaseUri($uri) {
260 // If the baseUri does not end with a slash, we must add it
261 if ($uri[strlen($uri)-1]!=='/')
264 $this->baseUri = $uri;
269 * Returns the base responding uri
273 public function getBaseUri() {
275 if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
276 return $this->baseUri;
281 * This method attempts to detect the base uri.
282 * Only the PATH_INFO variable is considered.
284 * If this variable is not set, the root (/) is assumed.
288 public function guessBaseUri() {
290 $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
291 $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
293 // If PATH_INFO is found, we can assume it's accurate.
294 if (!empty($pathInfo)) {
296 // We need to make sure we ignore the QUERY_STRING part
297 if ($pos = strpos($uri,'?'))
298 $uri = substr($uri,0,$pos);
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);
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,'/') . '/';
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.');
317 // The last fallback is that we're just going to assume the server root.
323 * Adds a plugin to the server
325 * For more information, console the documentation of Sabre_DAV_ServerPlugin
327 * @param Sabre_DAV_ServerPlugin $plugin
330 public function addPlugin(Sabre_DAV_ServerPlugin $plugin) {
332 $this->plugins[$plugin->getPluginName()] = $plugin;
333 $plugin->initialize($this);
338 * Returns an initialized plugin by it's name.
340 * This function returns null if the plugin was not found.
342 * @param string $name
343 * @return Sabre_DAV_ServerPlugin
345 public function getPlugin($name) {
347 if (isset($this->plugins[$name]))
348 return $this->plugins[$name];
350 // This is a fallback and deprecated.
351 foreach($this->plugins as $plugin) {
352 if (get_class($plugin)===$name) return $plugin;
360 * Returns all plugins
364 public function getPlugins() {
366 return $this->plugins;
372 * Subscribe to an event.
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
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.
382 * @param string $event
383 * @param callback $callback
384 * @param int $priority
387 public function subscribeEvent($event, $callback, $priority = 100) {
389 if (!isset($this->eventSubscriptions[$event])) {
390 $this->eventSubscriptions[$event] = array();
392 while(isset($this->eventSubscriptions[$event][$priority])) $priority++;
393 $this->eventSubscriptions[$event][$priority] = $callback;
394 ksort($this->eventSubscriptions[$event]);
399 * Broadcasts an event
401 * This method will call all subscribers. If one of the subscribers returns false, the process stops.
403 * The arguments parameter will be sent to all subscribers
405 * @param string $eventName
406 * @param array $arguments
409 public function broadcastEvent($eventName,$arguments = array()) {
411 if (isset($this->eventSubscriptions[$eventName])) {
413 foreach($this->eventSubscriptions[$eventName] as $subscriber) {
415 $result = call_user_func_array($subscriber,$arguments);
416 if ($result===false) return false;
427 * Handles a http request, and execute a method based on its name
429 * @param string $method
433 public function invokeMethod($method, $uri) {
435 $method = strtoupper($method);
437 if (!$this->broadcastEvent('beforeMethod',array($method, $uri))) return;
439 // Make sure this is a HTTP method we support
440 $internalMethods = array(
454 if (in_array($method,$internalMethods)) {
456 call_user_func(array($this,'http' . $method), $uri);
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');
469 // {{{ HTTP Method implementations
477 protected function httpOptions($uri) {
479 $methods = $this->getAllowedMethods($uri);
481 $this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods)));
482 $features = array('1','3', 'extended-mkcol');
484 foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
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);
492 $this->httpResponse->setHeader('Content-Length',0);
493 $this->httpResponse->sendStatus(200);
500 * This method simply fetches the contents of a uri, like normal
505 protected function httpGet($uri) {
507 $node = $this->tree->getNodeForPath($uri,0);
509 if (!$this->checkPreconditions(true)) return false;
511 if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_NotImplemented('GET is only implemented on File objects');
512 $body = $node->get();
514 // Converting string into stream, if needed.
515 if (is_string($body)) {
516 $stream = fopen('php://temp','r+');
517 fwrite($stream,$body);
523 * TODO: getetag, getlastmodified, getsize should also be used using
526 $httpHeaders = $this->getHTTPHeaders($uri);
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.
531 if (!isset($httpHeaders['Content-Type'])) {
532 $httpHeaders['Content-Type'] = 'application/octet-stream';
536 if (isset($httpHeaders['Content-Length'])) {
538 $nodeSize = $httpHeaders['Content-Length'];
540 // Need to unset Content-Length, because we'll handle that during figuring out the range
541 unset($httpHeaders['Content-Length']);
547 $this->httpResponse->setHeaders($httpHeaders);
549 $range = $this->getHTTPRange();
550 $ifRange = $this->httpRequest->getHeader('If-Range');
551 $ignoreRangeHeader = false;
553 // If ifRange is set, and range is specified, we first need to check
555 if ($nodeSize && $range && $ifRange) {
557 // if IfRange is parsable as a date we'll treat it as a DateTime
558 // otherwise, we must treat it as an etag.
560 $ifRangeDate = new DateTime($ifRange);
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;
566 $modified = new DateTime($httpHeaders['Last-Modified']);
567 if($modified > $ifRangeDate) $ignoreRangeHeader = true;
570 } catch (Exception $e) {
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;
578 // We're only going to support HTTP ranges if the backend provided a filesize
579 if (!$ignoreRangeHeader && $nodeSize && $range) {
581 // Determining the exact byte offsets
582 if (!is_null($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 . ')');
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;
594 $start = $nodeSize-$range[1];
597 if ($start<0) $start = 0;
601 // New read/write stream
602 $newStream = fopen('php://temp','r+');
604 stream_copy_to_stream($body, $newStream, $end-$start+1, $start);
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);
615 if ($nodeSize) $this->httpResponse->setHeader('Content-Length',$nodeSize);
616 $this->httpResponse->sendStatus(200);
617 $this->httpResponse->sendBody($body);
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
632 protected function httpHead($uri) {
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
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';
644 $this->httpResponse->setHeaders($headers);
646 $this->httpResponse->sendStatus(200);
653 * The HTTP delete method, deletes a given uri
658 protected function httpDelete($uri) {
660 if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
661 $this->tree->delete($uri);
662 $this->broadcastEvent('afterUnbind',array($uri));
664 $this->httpResponse->sendStatus(204);
665 $this->httpResponse->setHeader('Content-Length','0');
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)
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
680 * It has to return a HTTP 207 Multi-status status code
685 protected function httpPropfind($uri) {
687 // $xml = new Sabre_DAV_XMLReader(file_get_contents('php://input'));
688 $requestedProperties = $this->parsePropfindRequest($this->httpRequest->getBody(true));
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;
694 $newProperties = $this->getPropertiesForPath($uri,$requestedProperties,$depth);
696 // This is a multi-status response
697 $this->httpResponse->sendStatus(207);
698 $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
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));
707 $data = $this->generateMultiStatus($newProperties);
708 $this->httpResponse->sendBody($data);
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
721 protected function httpPropPatch($uri) {
723 $newProperties = $this->parsePropPatchRequest($this->httpRequest->getBody(true));
725 $result = $this->updateProperties($uri, $newProperties);
727 $this->httpResponse->sendStatus(207);
728 $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
730 $this->httpResponse->sendBody(
731 $this->generateMultiStatus(array($result))
739 * This HTTP method updates a file, or creates a new one.
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
746 protected function httpPut($uri) {
748 $body = $this->httpRequest->getBody();
750 // Intercepting Content-Range
751 if ($this->httpRequest->getHeader('Content-Range')) {
753 Content-Range is dangerous for PUT requests: PUT per definition
754 stores a full resource. draft-ietf-httpbis-p2-semantics-15 says
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.
775 throw new Sabre_DAV_Exception_NotImplemented('PUT with Content-Range is not allowed.');
778 // Intercepting the Finder problem
779 if (($expected = $this->httpRequest->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
782 Many webservers will not cooperate well with Finder PUT requests,
783 because it uses 'Chunked' transfer encoding for the request body.
785 The symptom of this problem is that Finder sends files to the
786 server, but they arrive as 0-length files in PHP.
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.
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.
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.
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.');
808 // The body needs to stay intact, so we copy everything to a
811 $newBody = fopen('php://temp','r+');
812 fwrite($newBody,$firstByte);
813 stream_copy_to_stream($body, $newBody);
820 if ($this->tree->nodeExists($uri)) {
822 $node = $this->tree->getNodeForPath($uri);
824 // Checking If-None-Match and related headers.
825 if (!$this->checkPreconditions()) return;
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;
831 $etag = $node->put($body);
833 $this->broadcastEvent('afterWriteContent',array($uri, $node));
835 $this->httpResponse->setHeader('Content-Length','0');
836 if ($etag) $this->httpResponse->setHeader('ETag',$etag);
837 $this->httpResponse->sendStatus(204);
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.
848 $this->httpResponse->setHeader('Content-Length','0');
849 if ($etag) $this->httpResponse->setHeader('ETag', $etag);
850 $this->httpResponse->sendStatus(201);
860 * The MKCOL method is used to create a new collection (directory) on the server
865 protected function httpMkcol($uri) {
867 $requestBody = $this->httpRequest->getBody(true);
871 $contentType = $this->httpRequest->getHeader('Content-Type');
872 if (strpos($contentType,'application/xml')!==0 && strpos($contentType,'text/xml')!==0) {
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');
879 $dom = Sabre_DAV_XMLUtil::loadDOMDocument($requestBody);
880 if (Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild)!=='{DAV:}mkcol') {
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.');
887 $properties = array();
888 foreach($dom->firstChild->childNodes as $childNode) {
890 if (Sabre_DAV_XMLUtil::toClarkNotation($childNode)!=='{DAV:}set') continue;
891 $properties = array_merge($properties, Sabre_DAV_XMLUtil::parseProperties($childNode, $this->propertyMap));
894 if (!isset($properties['{DAV:}resourcetype']))
895 throw new Sabre_DAV_Exception_BadRequest('The mkcol request must include a {DAV:}resourcetype property');
897 $resourceType = $properties['{DAV:}resourcetype']->getValue();
898 unset($properties['{DAV:}resourcetype']);
902 $properties = array();
903 $resourceType = array('{DAV:}collection');
907 $result = $this->createCollection($uri, $resourceType, $properties);
909 if (is_array($result)) {
910 $this->httpResponse->sendStatus(207);
911 $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
913 $this->httpResponse->sendBody(
914 $this->generateMultiStatus(array($result))
918 $this->httpResponse->setHeader('Content-Length','0');
919 $this->httpResponse->sendStatus(201);
925 * WebDAV HTTP MOVE method
927 * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
932 protected function httpMove($uri) {
934 $moveInfo = $this->getCopyAndMoveInfo();
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.');
940 if ($moveInfo['destinationExists']) {
942 if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false;
943 $this->tree->delete($moveInfo['destination']);
944 $this->broadcastEvent('afterUnbind',array($moveInfo['destination']));
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']));
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);
961 * WebDAV HTTP COPY method
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
969 protected function httpCopy($uri) {
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.');
976 if ($copyInfo['destinationExists']) {
977 if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false;
978 $this->tree->delete($copyInfo['destination']);
981 if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false;
982 $this->tree->copy($uri,$copyInfo['destination']);
983 $this->broadcastEvent('afterBind',array($copyInfo['destination']));
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);
994 * HTTP REPORT method implementation
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.
1002 protected function httpReport($uri) {
1004 $body = $this->httpRequest->getBody(true);
1005 $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
1007 $reportName = Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild);
1009 if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) {
1011 // If broadcastEvent returned true, it means the report was not supported
1012 throw new Sabre_DAV_Exception_ReportNotImplemented();
1019 // {{{ HTTP/WebDAV protocol helpers
1022 * Returns an array with all the supported HTTP methods for a specific uri.
1024 * @param string $uri
1027 public function getAllowedMethods($uri) {
1042 // The MKCOL is only allowed on an unmapped uri
1044 $this->tree->getNodeForPath($uri);
1045 } catch (Sabre_DAV_Exception_NotFound $e) {
1046 $methods[] = 'MKCOL';
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);
1058 * Gets the uri for the request, keeping the base uri into consideration
1062 public function getRequestUri() {
1064 return $this->calculateUri($this->httpRequest->getUri());
1069 * Calculates the uri for a request, making sure that the base uri is stripped out
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
1075 public function calculateUri($uri) {
1077 if ($uri[0]!='/' && strpos($uri,'://')) {
1079 $uri = parse_url($uri,PHP_URL_PATH);
1083 $uri = str_replace('//','/',$uri);
1085 if (strpos($uri,$this->getBaseUri())===0) {
1087 return trim(Sabre_DAV_URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/');
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()) {
1097 throw new Sabre_DAV_Exception_Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
1104 * Returns the HTTP depth header
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
1109 * @param mixed $default
1112 public function getHTTPDepth($default = self::DEPTH_INFINITY) {
1114 // If its not set, we'll grab the default
1115 $depth = $this->httpRequest->getHeader('Depth');
1117 if (is_null($depth)) return $default;
1119 if ($depth == 'infinity') return self::DEPTH_INFINITY;
1122 // If its an unknown value. we'll grab the default
1123 if (!ctype_digit($depth)) return $default;
1130 * Returns the HTTP range header
1132 * This method returns null if there is no well-formed HTTP range request
1133 * header or array($start, $end).
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.
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
1141 * @return array|null
1143 public function getHTTPRange() {
1145 $range = $this->httpRequest->getHeader('range');
1146 if (is_null($range)) return null;
1148 // Matching "Range: bytes=1234-5678: both numbers are optional
1150 if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null;
1152 if ($matches[1]==='' && $matches[2]==='') return null;
1155 $matches[1]!==''?$matches[1]:null,
1156 $matches[2]!==''?$matches[2]:null,
1163 * Returns information about Copy and Move requests
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
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)
1174 public function getCopyAndMoveInfo() {
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');
1186 list($destinationDir) = Sabre_DAV_URLUtil::splitPath($destination);
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) {
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');
1199 $destinationNode = $this->tree->getNodeForPath($destination);
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');
1205 } catch (Sabre_DAV_Exception_NotFound $e) {
1207 // Destination didn't exist, we're all good
1208 $destinationNode = false;
1214 // These are the three relevant properties we need to return
1216 'destination' => $destination,
1217 'destinationExists' => $destinationNode==true,
1218 'destinationNode' => $destinationNode,
1224 * Returns a list of properties for a path
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.
1230 * @param string $path
1231 * @param array $propertyNames
1233 public function getProperties($path, $propertyNames) {
1235 $result = $this->getPropertiesForPath($path,$propertyNames,0);
1236 return $result[0][200];
1241 * A kid-friendly way to fetch properties for a node's children.
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.
1246 * The parent node will not be returned.
1248 * @param string $path
1249 * @param array $propertyNames
1252 public function getPropertiesForChildren($path, $propertyNames) {
1255 foreach($this->getPropertiesForPath($path,$propertyNames,1) as $k=>$row) {
1257 // Skipping the parent path
1258 if ($k === 0) continue;
1260 $result[$row['href']] = $row[200];
1268 * Returns a list of HTTP headers for a particular resource
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.
1274 * The headers are intended to be used for HEAD and GET requests.
1276 * @param string $path
1279 public function getHTTPHeaders($path) {
1281 $propertyMap = array(
1282 '{DAV:}getcontenttype' => 'Content-Type',
1283 '{DAV:}getcontentlength' => 'Content-Length',
1284 '{DAV:}getlastmodified' => 'Last-Modified',
1285 '{DAV:}getetag' => 'ETag',
1288 $properties = $this->getProperties($path,array_keys($propertyMap));
1291 foreach($propertyMap as $property=>$header) {
1292 if (!isset($properties[$property])) continue;
1294 if (is_scalar($properties[$property])) {
1295 $headers[$header] = $properties[$property];
1297 // GetLastModified gets special cased
1298 } elseif ($properties[$property] instanceof Sabre_DAV_Property_GetLastModified) {
1299 $headers[$header] = Sabre_HTTP_Util::toHTTPDate($properties[$property]->getTime());
1309 * Returns a list of properties for a given path
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.
1315 * If a depth of 1 is requested child elements will also be returned.
1317 * @param string $path
1318 * @param array $propertyNames
1322 public function getPropertiesForPath($path, $propertyNames = array(), $depth = 0) {
1324 if ($depth!=0) $depth = 1;
1326 $returnPropertyList = array();
1328 $parentNode = $this->tree->getNodeForPath($path);
1330 $path => $parentNode
1332 if ($depth==1 && $parentNode instanceof Sabre_DAV_ICollection) {
1333 foreach($this->tree->getChildren($path) as $childNode)
1334 $nodes[$path . '/' . $childNode->getName()] = $childNode;
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
1340 $allProperties = count($propertyNames)==0;
1342 foreach($nodes as $myPath=>$node) {
1344 $currentPropertyNames = $propertyNames;
1346 $newProperties = array(
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',
1360 '{DAV:}getcontenttype',
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
1369 if (!in_array('{DAV:}resourcetype',$currentPropertyNames)) {
1370 $currentPropertyNames[] = '{DAV:}resourcetype';
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;
1379 if (count($currentPropertyNames) > 0) {
1381 if ($node instanceof Sabre_DAV_IProperties)
1382 $newProperties['200'] = $newProperties[200] + $node->getProperties($currentPropertyNames);
1387 foreach($currentPropertyNames as $prop) {
1389 if (isset($newProperties[200][$prop])) continue;
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();
1401 case '{DAV:}quota-used-bytes' :
1402 if ($node instanceof Sabre_DAV_IQuota) {
1403 $quotaInfo = $node->getQuotaInfo();
1404 $newProperties[200][$prop] = $quotaInfo[0];
1407 case '{DAV:}quota-available-bytes' :
1408 if ($node instanceof Sabre_DAV_IQuota) {
1409 $quotaInfo = $node->getQuotaInfo();
1410 $newProperties[200][$prop] = $quotaInfo[1];
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' :
1417 foreach($this->plugins as $plugin) {
1418 $reports = array_merge($reports, $plugin->getSupportedReportSet($myPath));
1420 $newProperties[200][$prop] = new Sabre_DAV_Property_SupportedReportSet($reports);
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);
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;
1436 $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties));
1438 $newProperties['href'] = trim($myPath,'/');
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'] .='/';
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']);
1450 $returnPropertyList[] = $newProperties;
1454 return $returnPropertyList;
1459 * This method is invoked by sub-systems creating a new file.
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.
1465 * This method will return true if the file was actually created
1467 * @param string $uri
1468 * @param resource $data
1469 * @param string $etag
1472 public function createFile($uri,$data, &$etag = null) {
1474 list($dir,$name) = Sabre_DAV_URLUtil::splitPath($uri);
1476 if (!$this->broadcastEvent('beforeBind',array($uri))) return false;
1478 $parent = $this->tree->getNodeForPath($dir);
1480 if (!$this->broadcastEvent('beforeCreateFile',array($uri, &$data, $parent))) return false;
1482 $etag = $parent->createFile($name,$data);
1483 $this->tree->markDirty($dir);
1485 $this->broadcastEvent('afterBind',array($uri));
1486 $this->broadcastEvent('afterCreateFile',array($uri, $parent));
1492 * This method is invoked by sub-systems creating a new directory.
1494 * @param string $uri
1497 public function createDirectory($uri) {
1499 $this->createCollection($uri,array('{DAV:}collection'),array());
1504 * Use this method to create a new collection
1506 * The {DAV:}resourcetype is specified using the resourceType array.
1507 * At the very least it must contain {DAV:}collection.
1509 * The properties array can contain a list of additional properties.
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
1516 public function createCollection($uri, array $resourceType, array $properties) {
1518 list($parentUri,$newName) = Sabre_DAV_URLUtil::splitPath($uri);
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');
1526 // Making sure the parent exists
1529 $parent = $this->tree->getNodeForPath($parentUri);
1531 } catch (Sabre_DAV_Exception_NotFound $e) {
1533 throw new Sabre_DAV_Exception_Conflict('Parent node does not exist');
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');
1544 // Making sure the child does not already exist
1546 $parent->getChild($newName);
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');
1551 } catch (Sabre_DAV_Exception_NotFound $e) {
1556 if (!$this->broadcastEvent('beforeBind',array($uri))) return;
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) {
1563 $parent->createExtendedCollection($newName, $resourceType, $properties);
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.');
1572 $parent->createDirectory($newName);
1575 $errorResult = null;
1577 if (count($properties)>0) {
1581 $errorResult = $this->updateProperties($uri, $properties);
1582 if (!isset($errorResult[200])) {
1586 } catch (Sabre_DAV_Exception $e) {
1596 if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
1597 $this->tree->delete($uri);
1599 // Re-throwing exception
1600 if ($exception) throw $exception;
1602 return $errorResult;
1606 $this->tree->markDirty($parentUri);
1607 $this->broadcastEvent('afterBind',array($uri));
1612 * This method updates a resource's properties
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.
1618 * Note that this request should either completely succeed, or
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.
1625 * @param string $uri
1626 * @param array $properties
1629 public function updateProperties($uri, array $properties) {
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);
1640 $remainingProperties = $properties;
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]);
1653 // Allowing plugins to take care of property updating
1654 $hasError = !$this->broadcastEvent('updateProperties',array(
1655 &$remainingProperties,
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)) {
1665 foreach($properties as $propertyName=> $value) {
1666 $result[403][$propertyName] = null;
1668 $remainingProperties = array();
1671 // Only if there were no errors we may attempt to update the resource
1674 if (count($remainingProperties)>0) {
1676 $updateResult = $node->updateProperties($remainingProperties);
1678 if ($updateResult===true) {
1680 foreach($remainingProperties as $propertyName=>$value) {
1681 $result[200][$propertyName] = null;
1684 } elseif ($updateResult===false) {
1685 // The node failed to update the properties for an
1687 foreach($remainingProperties as $propertyName=>$value) {
1688 $result[403][$propertyName] = null;
1691 } elseif (is_array($updateResult)) {
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();
1700 $result[$status] = array_merge($result[$status], $updateResult[$status]);
1705 throw new Sabre_DAV_Exception('Invalid result from updateProperties');
1707 $remainingProperties = array();
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;
1718 // Removing empty array values
1719 foreach($result as $status=>$props) {
1721 if (count($props)===0) unset($result[$status]);
1724 $result['href'] = $uri;
1730 * This method checks the main HTTP preconditions.
1732 * Currently these are:
1735 * * If-Modified-Since
1736 * * If-Unmodified-Since
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.
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.
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.
1751 * @param bool $handleAsGET
1754 public function checkPreconditions($handleAsGET = false) {
1756 $uri = $this->getRequestUri();
1761 if ($ifMatch = $this->httpRequest->getHeader('If-Match')) {
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.
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');
1773 // Only need to check entity tags if they are not *
1774 if ($ifMatch!=='*') {
1776 // There can be multiple etags
1777 $ifMatch = explode(',',$ifMatch);
1779 foreach($ifMatch as $ifMatchItem) {
1781 // Stripping any extra spaces
1782 $ifMatchItem = trim($ifMatchItem,' ');
1784 $etag = $node->getETag();
1785 if ($etag===$ifMatchItem) {
1788 // Evolution has a bug where it sometimes prepends the "
1789 // with a \. This is our workaround.
1790 if (str_replace('\\"','"', $ifMatchItem) === $etag) {
1797 throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
1802 if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) {
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.
1811 $node = $this->tree->getNodeForPath($uri);
1812 } catch (Sabre_DAV_Exception_NotFound $e) {
1813 $nodeExists = false;
1818 if ($ifNoneMatch==='*') $haveMatch = true;
1821 // There might be multiple etags
1822 $ifNoneMatch = explode(',', $ifNoneMatch);
1823 $etag = $node->getETag();
1825 foreach($ifNoneMatch as $ifNoneMatchItem) {
1827 // Stripping any extra spaces
1828 $ifNoneMatchItem = trim($ifNoneMatchItem,' ');
1830 if ($etag===$ifNoneMatchItem) $haveMatch = true;
1838 $this->httpResponse->sendStatus(304);
1841 throw new Sabre_DAV_Exception_PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match');
1848 if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) {
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
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);
1859 if (is_null($node)) {
1860 $node = $this->tree->getNodeForPath($uri);
1862 $lastMod = $node->getLastModified();
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));
1874 if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) {
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);
1880 // We must only check the date if it's valid
1882 if (is_null($node)) {
1883 $node = $this->tree->getNodeForPath($uri);
1885 $lastMod = $node->getLastModified();
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');
1900 // {{{ XML Readers & Writers
1904 * Generates a WebDAV propfind response body based on a list of nodes
1906 * @param array $fileProperties The list with nodes
1909 public function generateMultiStatus(array $fileProperties) {
1911 $dom = new DOMDocument('1.0','utf-8');
1912 //$dom->formatOutput = true;
1913 $multiStatus = $dom->createElement('d:multistatus');
1914 $dom->appendChild($multiStatus);
1916 // Adding in default namespaces
1917 foreach($this->xmlNamespaces as $namespace=>$prefix) {
1919 $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
1923 foreach($fileProperties as $entry) {
1925 $href = $entry['href'];
1926 unset($entry['href']);
1928 $response = new Sabre_DAV_Property_Response($href,$entry);
1929 $response->serialize($this,$multiStatus);
1933 return $dom->saveXML();
1938 * This method parses a PropPatch request
1940 * PropPatch changes the properties for a resource. This method
1941 * returns a list of properties.
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
1947 * @param string $body xml body
1948 * @return array list of properties in need of updating or deletion
1950 public function parsePropPatchRequest($body) {
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);
1955 $newProperties = array();
1957 foreach($dom->firstChild->childNodes as $child) {
1959 if ($child->nodeType !== XML_ELEMENT_NODE) continue;
1961 $operation = Sabre_DAV_XMLUtil::toClarkNotation($child);
1963 if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue;
1965 $innerProperties = Sabre_DAV_XMLUtil::parseProperties($child, $this->propertyMap);
1967 foreach($innerProperties as $propertyName=>$propertyValue) {
1969 if ($operation==='{DAV:}remove') {
1970 $propertyValue = null;
1973 $newProperties[$propertyName] = $propertyValue;
1979 return $newProperties;
1984 * This method parses the PROPFIND request and returns its information
1986 * This will either be a list of properties, or an empty array; in which case
1987 * an {DAV:}allprop was requested.
1989 * @param string $body
1992 public function parsePropFindRequest($body) {
1994 // If the propfind body was empty, it means IE is requesting 'all' properties
1995 if (!$body) return array();
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));