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) {
211 $this->broadcastEvent('exception', array($e));
212 } catch (Exception $ignore) {
214 $DOM = new DOMDocument('1.0','utf-8');
215 $DOM->formatOutput = true;
217 $error = $DOM->createElementNS('DAV:','d:error');
218 $error->setAttribute('xmlns:s',self::NS_SABREDAV);
219 $DOM->appendChild($error);
221 $error->appendChild($DOM->createElement('s:exception',get_class($e)));
222 $error->appendChild($DOM->createElement('s:message',$e->getMessage()));
223 if ($this->debugExceptions) {
224 $error->appendChild($DOM->createElement('s:file',$e->getFile()));
225 $error->appendChild($DOM->createElement('s:line',$e->getLine()));
226 $error->appendChild($DOM->createElement('s:code',$e->getCode()));
227 $error->appendChild($DOM->createElement('s:stacktrace',$e->getTraceAsString()));
230 if (self::$exposeVersion) {
231 $error->appendChild($DOM->createElement('s:sabredav-version',Sabre_DAV_Version::VERSION));
234 if($e instanceof Sabre_DAV_Exception) {
236 $httpCode = $e->getHTTPCode();
237 $e->serialize($this,$error);
238 $headers = $e->getHTTPHeaders($this);
246 $headers['Content-Type'] = 'application/xml; charset=utf-8';
248 $this->httpResponse->sendStatus($httpCode);
249 $this->httpResponse->setHeaders($headers);
250 $this->httpResponse->sendBody($DOM->saveXML());
257 * Sets the base server uri
262 public function setBaseUri($uri) {
264 // If the baseUri does not end with a slash, we must add it
265 if ($uri[strlen($uri)-1]!=='/')
268 $this->baseUri = $uri;
273 * Returns the base responding uri
277 public function getBaseUri() {
279 if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
280 return $this->baseUri;
285 * This method attempts to detect the base uri.
286 * Only the PATH_INFO variable is considered.
288 * If this variable is not set, the root (/) is assumed.
292 public function guessBaseUri() {
294 $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
295 $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
297 // If PATH_INFO is found, we can assume it's accurate.
298 if (!empty($pathInfo)) {
300 // We need to make sure we ignore the QUERY_STRING part
301 if ($pos = strpos($uri,'?'))
302 $uri = substr($uri,0,$pos);
304 // PATH_INFO is only set for urls, such as: /example.php/path
305 // in that case PATH_INFO contains '/path'.
306 // Note that REQUEST_URI is percent encoded, while PATH_INFO is
307 // not, Therefore they are only comparable if we first decode
308 // REQUEST_INFO as well.
309 $decodedUri = Sabre_DAV_URLUtil::decodePath($uri);
311 // A simple sanity check:
312 if(substr($decodedUri,strlen($decodedUri)-strlen($pathInfo))===$pathInfo) {
313 $baseUri = substr($decodedUri,0,strlen($decodedUri)-strlen($pathInfo));
314 return rtrim($baseUri,'/') . '/';
317 throw new Sabre_DAV_Exception('The REQUEST_URI ('. $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.');
321 // The last fallback is that we're just going to assume the server root.
327 * Adds a addon to the server
329 * For more information, console the documentation of Sabre_DAV_ServerPlugin
331 * @param Sabre_DAV_ServerPlugin $plugin
334 public function addPlugin(Sabre_DAV_ServerPlugin $plugin) {
336 $this->plugins[$plugin->getPluginName()] = $plugin;
337 $plugin->initialize($this);
342 * Returns an initialized addon by it's name.
344 * This function returns null if the addon was not found.
346 * @param string $name
347 * @return Sabre_DAV_ServerPlugin
349 public function getPlugin($name) {
351 if (isset($this->plugins[$name]))
352 return $this->plugins[$name];
354 // This is a fallback and deprecated.
355 foreach($this->plugins as $plugin) {
356 if (get_class($plugin)===$name) return $plugin;
368 public function getPlugins() {
370 return $this->plugins;
376 * Subscribe to an event.
378 * When the event is triggered, we'll call all the specified callbacks.
379 * It is possible to control the order of the callbacks through the
382 * This is for example used to make sure that the authentication addon
383 * is triggered before anything else. If it's not needed to change this
384 * number, it is recommended to ommit.
386 * @param string $event
387 * @param callback $callback
388 * @param int $priority
391 public function subscribeEvent($event, $callback, $priority = 100) {
393 if (!isset($this->eventSubscriptions[$event])) {
394 $this->eventSubscriptions[$event] = array();
396 while(isset($this->eventSubscriptions[$event][$priority])) $priority++;
397 $this->eventSubscriptions[$event][$priority] = $callback;
398 ksort($this->eventSubscriptions[$event]);
403 * Broadcasts an event
405 * This method will call all subscribers. If one of the subscribers returns false, the process stops.
407 * The arguments parameter will be sent to all subscribers
409 * @param string $eventName
410 * @param array $arguments
413 public function broadcastEvent($eventName,$arguments = array()) {
415 if (isset($this->eventSubscriptions[$eventName])) {
417 foreach($this->eventSubscriptions[$eventName] as $subscriber) {
419 $result = call_user_func_array($subscriber,$arguments);
420 if ($result===false) return false;
431 * Handles a http request, and execute a method based on its name
433 * @param string $method
437 public function invokeMethod($method, $uri) {
439 $method = strtoupper($method);
441 if (!$this->broadcastEvent('beforeMethod',array($method, $uri))) return;
443 // Make sure this is a HTTP method we support
444 $internalMethods = array(
458 if (in_array($method,$internalMethods)) {
460 call_user_func(array($this,'http' . $method), $uri);
464 if ($this->broadcastEvent('unknownMethod',array($method, $uri))) {
465 // Unsupported method
466 throw new Sabre_DAV_Exception_NotImplemented('There was no handler found for this "' . $method . '" method');
473 // {{{ HTTP Method implementations
481 protected function httpOptions($uri) {
483 $methods = $this->getAllowedMethods($uri);
485 $this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods)));
486 $features = array('1','3', 'extended-mkcol');
488 foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
490 $this->httpResponse->setHeader('DAV',implode(', ',$features));
491 $this->httpResponse->setHeader('MS-Author-Via','DAV');
492 $this->httpResponse->setHeader('Accept-Ranges','bytes');
493 if (self::$exposeVersion) {
494 $this->httpResponse->setHeader('X-Sabre-Version',Sabre_DAV_Version::VERSION);
496 $this->httpResponse->setHeader('Content-Length',0);
497 $this->httpResponse->sendStatus(200);
504 * This method simply fetches the contents of a uri, like normal
509 protected function httpGet($uri) {
511 $node = $this->tree->getNodeForPath($uri,0);
513 if (!$this->checkPreconditions(true)) return false;
515 if (!$node instanceof Sabre_DAV_IFile) throw new Sabre_DAV_Exception_NotImplemented('GET is only implemented on File objects');
516 $body = $node->get();
518 // Converting string into stream, if needed.
519 if (is_string($body)) {
520 $stream = fopen('php://temp','r+');
521 fwrite($stream,$body);
527 * TODO: getetag, getlastmodified, getsize should also be used using
530 $httpHeaders = $this->getHTTPHeaders($uri);
532 /* ContentType needs to get a default, because many webservers will otherwise
533 * default to text/html, and we don't want this for security reasons.
535 if (!isset($httpHeaders['Content-Type'])) {
536 $httpHeaders['Content-Type'] = 'application/octet-stream';
540 if (isset($httpHeaders['Content-Length'])) {
542 $nodeSize = $httpHeaders['Content-Length'];
544 // Need to unset Content-Length, because we'll handle that during figuring out the range
545 unset($httpHeaders['Content-Length']);
551 $this->httpResponse->setHeaders($httpHeaders);
553 $range = $this->getHTTPRange();
554 $ifRange = $this->httpRequest->getHeader('If-Range');
555 $ignoreRangeHeader = false;
557 // If ifRange is set, and range is specified, we first need to check
559 if ($nodeSize && $range && $ifRange) {
561 // if IfRange is parsable as a date we'll treat it as a DateTime
562 // otherwise, we must treat it as an etag.
564 $ifRangeDate = new DateTime($ifRange);
566 // It's a date. We must check if the entity is modified since
567 // the specified date.
568 if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true;
570 $modified = new DateTime($httpHeaders['Last-Modified']);
571 if($modified > $ifRangeDate) $ignoreRangeHeader = true;
574 } catch (Exception $e) {
576 // It's an entity. We can do a simple comparison.
577 if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true;
578 elseif ($httpHeaders['ETag']!==$ifRange) $ignoreRangeHeader = true;
582 // We're only going to support HTTP ranges if the backend provided a filesize
583 if (!$ignoreRangeHeader && $nodeSize && $range) {
585 // Determining the exact byte offsets
586 if (!is_null($range[0])) {
589 $end = $range[1]?$range[1]:$nodeSize-1;
590 if($start >= $nodeSize)
591 throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
593 if($end < $start) throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')');
594 if($end >= $nodeSize) $end = $nodeSize-1;
598 $start = $nodeSize-$range[1];
601 if ($start<0) $start = 0;
605 // New read/write stream
606 $newStream = fopen('php://temp','r+');
608 stream_copy_to_stream($body, $newStream, $end-$start+1, $start);
611 $this->httpResponse->setHeader('Content-Length', $end-$start+1);
612 $this->httpResponse->setHeader('Content-Range','bytes ' . $start . '-' . $end . '/' . $nodeSize);
613 $this->httpResponse->sendStatus(206);
614 $this->httpResponse->sendBody($newStream);
619 if ($nodeSize) $this->httpResponse->setHeader('Content-Length',$nodeSize);
620 $this->httpResponse->sendStatus(200);
621 $this->httpResponse->sendBody($body);
630 * This method is normally used to take a peak at a url, and only get the HTTP response headers, without the body
631 * This is used by clients to determine if a remote file was changed, so they can use a local cached version, instead of downloading it again
636 protected function httpHead($uri) {
638 $node = $this->tree->getNodeForPath($uri);
639 /* This information is only collection for File objects.
640 * Ideally we want to throw 405 Method Not Allowed for every
641 * non-file, but MS Office does not like this
643 if ($node instanceof Sabre_DAV_IFile) {
644 $headers = $this->getHTTPHeaders($this->getRequestUri());
645 if (!isset($headers['Content-Type'])) {
646 $headers['Content-Type'] = 'application/octet-stream';
648 $this->httpResponse->setHeaders($headers);
650 $this->httpResponse->sendStatus(200);
657 * The HTTP delete method, deletes a given uri
662 protected function httpDelete($uri) {
664 if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
665 $this->tree->delete($uri);
666 $this->broadcastEvent('afterUnbind',array($uri));
668 $this->httpResponse->sendStatus(204);
669 $this->httpResponse->setHeader('Content-Length','0');
677 * This WebDAV method requests information about an uri resource, or a list of resources
678 * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
679 * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
681 * The request body contains an XML data structure that has a list of properties the client understands
682 * The response body is also an xml document, containing information about every uri resource and the requested properties
684 * It has to return a HTTP 207 Multi-status status code
689 protected function httpPropfind($uri) {
691 // $xml = new Sabre_DAV_XMLReader(file_get_contents('php://input'));
692 $requestedProperties = $this->parsePropfindRequest($this->httpRequest->getBody(true));
694 $depth = $this->getHTTPDepth(1);
695 // The only two options for the depth of a propfind is 0 or 1
696 if ($depth!=0) $depth = 1;
698 $newProperties = $this->getPropertiesForPath($uri,$requestedProperties,$depth);
700 // This is a multi-status response
701 $this->httpResponse->sendStatus(207);
702 $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
704 // Normally this header is only needed for OPTIONS responses, however..
705 // iCal seems to also depend on these being set for PROPFIND. Since
706 // this is not harmful, we'll add it.
707 $features = array('1','3', 'extended-mkcol');
708 foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
709 $this->httpResponse->setHeader('DAV',implode(', ',$features));
711 $data = $this->generateMultiStatus($newProperties);
712 $this->httpResponse->sendBody($data);
719 * This method is called to update properties on a Node. The request is an XML body with all the mutations.
720 * In this XML body it is specified which properties should be set/updated and/or deleted
725 protected function httpPropPatch($uri) {
727 $newProperties = $this->parsePropPatchRequest($this->httpRequest->getBody(true));
729 $result = $this->updateProperties($uri, $newProperties);
731 $this->httpResponse->sendStatus(207);
732 $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
734 $this->httpResponse->sendBody(
735 $this->generateMultiStatus(array($result))
743 * This HTTP method updates a file, or creates a new one.
745 * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content
750 protected function httpPut($uri) {
752 $body = $this->httpRequest->getBody();
754 // Intercepting Content-Range
755 if ($this->httpRequest->getHeader('Content-Range')) {
757 Content-Range is dangerous for PUT requests: PUT per definition
758 stores a full resource. draft-ietf-httpbis-p2-semantics-15 says
760 An origin server SHOULD reject any PUT request that contains a
761 Content-Range header field, since it might be misinterpreted as
762 partial content (or might be partial content that is being mistakenly
763 PUT as a full representation). Partial content updates are possible
764 by targeting a separately identified resource with state that
765 overlaps a portion of the larger resource, or by using a different
766 method that has been specifically defined for partial updates (for
767 example, the PATCH method defined in [RFC5789]).
768 This clarifies RFC2616 section 9.6:
769 The recipient of the entity MUST NOT ignore any Content-*
770 (e.g. Content-Range) headers that it does not understand or implement
771 and MUST return a 501 (Not Implemented) response in such cases.
772 OTOH is a PUT request with a Content-Range currently the only way to
773 continue an aborted upload request and is supported by curl, mod_dav,
774 Tomcat and others. Since some clients do use this feature which results
775 in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject
776 all PUT requests with a Content-Range for now.
779 throw new Sabre_DAV_Exception_NotImplemented('PUT with Content-Range is not allowed.');
782 // Intercepting the Finder problem
783 if (($expected = $this->httpRequest->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
786 Many webservers will not cooperate well with Finder PUT requests,
787 because it uses 'Chunked' transfer encoding for the request body.
789 The symptom of this problem is that Finder sends files to the
790 server, but they arrive as 0-length files in PHP.
792 If we don't do anything, the user might think they are uploading
793 files successfully, but they end up empty on the server. Instead,
794 we throw back an error if we detect this.
796 The reason Finder uses Chunked, is because it thinks the files
797 might change as it's being uploaded, and therefore the
798 Content-Length can vary.
800 Instead it sends the X-Expected-Entity-Length header with the size
801 of the file at the very start of the request. If this header is set,
802 but we don't get a request body we will fail the request to
803 protect the end-user.
806 // Only reading first byte
807 $firstByte = fread($body,1);
808 if (strlen($firstByte)!==1) {
809 throw new Sabre_DAV_Exception_Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
812 // The body needs to stay intact, so we copy everything to a
815 $newBody = fopen('php://temp','r+');
816 fwrite($newBody,$firstByte);
817 stream_copy_to_stream($body, $newBody);
824 if ($this->tree->nodeExists($uri)) {
826 $node = $this->tree->getNodeForPath($uri);
828 // Checking If-None-Match and related headers.
829 if (!$this->checkPreconditions()) return;
831 // If the node is a collection, we'll deny it
832 if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_Conflict('PUT is not allowed on non-files.');
833 if (!$this->broadcastEvent('beforeWriteContent',array($uri, $node, &$body))) return false;
835 $etag = $node->put($body);
837 $this->broadcastEvent('afterWriteContent',array($uri, $node));
839 $this->httpResponse->setHeader('Content-Length','0');
840 if ($etag) $this->httpResponse->setHeader('ETag',$etag);
841 $this->httpResponse->sendStatus(204);
846 // If we got here, the resource didn't exist yet.
847 if (!$this->createFile($this->getRequestUri(),$body,$etag)) {
848 // For one reason or another the file was not created.
852 $this->httpResponse->setHeader('Content-Length','0');
853 if ($etag) $this->httpResponse->setHeader('ETag', $etag);
854 $this->httpResponse->sendStatus(201);
864 * The MKCOL method is used to create a new collection (directory) on the server
869 protected function httpMkcol($uri) {
871 $requestBody = $this->httpRequest->getBody(true);
875 $contentType = $this->httpRequest->getHeader('Content-Type');
876 if (strpos($contentType,'application/xml')!==0 && strpos($contentType,'text/xml')!==0) {
878 // We must throw 415 for unsupported mkcol bodies
879 throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
883 $dom = Sabre_DAV_XMLUtil::loadDOMDocument($requestBody);
884 if (Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild)!=='{DAV:}mkcol') {
886 // We must throw 415 for unsupported mkcol bodies
887 throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must be a {DAV:}mkcol request construct.');
891 $properties = array();
892 foreach($dom->firstChild->childNodes as $childNode) {
894 if (Sabre_DAV_XMLUtil::toClarkNotation($childNode)!=='{DAV:}set') continue;
895 $properties = array_merge($properties, Sabre_DAV_XMLUtil::parseProperties($childNode, $this->propertyMap));
898 if (!isset($properties['{DAV:}resourcetype']))
899 throw new Sabre_DAV_Exception_BadRequest('The mkcol request must include a {DAV:}resourcetype property');
901 $resourceType = $properties['{DAV:}resourcetype']->getValue();
902 unset($properties['{DAV:}resourcetype']);
906 $properties = array();
907 $resourceType = array('{DAV:}collection');
911 $result = $this->createCollection($uri, $resourceType, $properties);
913 if (is_array($result)) {
914 $this->httpResponse->sendStatus(207);
915 $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
917 $this->httpResponse->sendBody(
918 $this->generateMultiStatus(array($result))
922 $this->httpResponse->setHeader('Content-Length','0');
923 $this->httpResponse->sendStatus(201);
929 * WebDAV HTTP MOVE method
931 * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
936 protected function httpMove($uri) {
938 $moveInfo = $this->getCopyAndMoveInfo();
940 // If the destination is part of the source tree, we must fail
941 if ($moveInfo['destination']==$uri)
942 throw new Sabre_DAV_Exception_Forbidden('Source and destination uri are identical.');
944 if ($moveInfo['destinationExists']) {
946 if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false;
947 $this->tree->delete($moveInfo['destination']);
948 $this->broadcastEvent('afterUnbind',array($moveInfo['destination']));
952 if (!$this->broadcastEvent('beforeUnbind',array($uri))) return false;
953 if (!$this->broadcastEvent('beforeBind',array($moveInfo['destination']))) return false;
954 $this->tree->move($uri,$moveInfo['destination']);
955 $this->broadcastEvent('afterUnbind',array($uri));
956 $this->broadcastEvent('afterBind',array($moveInfo['destination']));
958 // If a resource was overwritten we should send a 204, otherwise a 201
959 $this->httpResponse->setHeader('Content-Length','0');
960 $this->httpResponse->sendStatus($moveInfo['destinationExists']?204:201);
965 * WebDAV HTTP COPY method
967 * This method copies one uri to a different uri, and works much like the MOVE request
968 * A lot of the actual request processing is done in getCopyMoveInfo
973 protected function httpCopy($uri) {
975 $copyInfo = $this->getCopyAndMoveInfo();
976 // If the destination is part of the source tree, we must fail
977 if ($copyInfo['destination']==$uri)
978 throw new Sabre_DAV_Exception_Forbidden('Source and destination uri are identical.');
980 if ($copyInfo['destinationExists']) {
981 if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false;
982 $this->tree->delete($copyInfo['destination']);
985 if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false;
986 $this->tree->copy($uri,$copyInfo['destination']);
987 $this->broadcastEvent('afterBind',array($copyInfo['destination']));
989 // If a resource was overwritten we should send a 204, otherwise a 201
990 $this->httpResponse->setHeader('Content-Length','0');
991 $this->httpResponse->sendStatus($copyInfo['destinationExists']?204:201);
998 * HTTP REPORT method implementation
1000 * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
1001 * It's used in a lot of extensions, so it made sense to implement it into the core.
1003 * @param string $uri
1006 protected function httpReport($uri) {
1008 $body = $this->httpRequest->getBody(true);
1009 $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
1011 $reportName = Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild);
1013 if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) {
1015 // If broadcastEvent returned true, it means the report was not supported
1016 throw new Sabre_DAV_Exception_ReportNotImplemented();
1023 // {{{ HTTP/WebDAV protocol helpers
1026 * Returns an array with all the supported HTTP methods for a specific uri.
1028 * @param string $uri
1031 public function getAllowedMethods($uri) {
1046 // The MKCOL is only allowed on an unmapped uri
1048 $this->tree->getNodeForPath($uri);
1049 } catch (Sabre_DAV_Exception_NotFound $e) {
1050 $methods[] = 'MKCOL';
1053 // We're also checking if any of the addons register any new methods
1054 foreach($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($uri));
1055 array_unique($methods);
1062 * Gets the uri for the request, keeping the base uri into consideration
1066 public function getRequestUri() {
1068 return $this->calculateUri($this->httpRequest->getUri());
1073 * Calculates the uri for a request, making sure that the base uri is stripped out
1075 * @param string $uri
1076 * @throws Sabre_DAV_Exception_Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
1079 public function calculateUri($uri) {
1081 if ($uri[0]!='/' && strpos($uri,'://')) {
1083 $uri = parse_url($uri,PHP_URL_PATH);
1087 $uri = str_replace('//','/',$uri);
1089 if (strpos($uri,$this->getBaseUri())===0) {
1091 return trim(Sabre_DAV_URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/');
1093 // A special case, if the baseUri was accessed without a trailing
1094 // slash, we'll accept it as well.
1095 } elseif ($uri.'/' === $this->getBaseUri()) {
1101 throw new Sabre_DAV_Exception_Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
1108 * Returns the HTTP depth header
1110 * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre_DAV_Server::DEPTH_INFINITY object
1111 * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
1113 * @param mixed $default
1116 public function getHTTPDepth($default = self::DEPTH_INFINITY) {
1118 // If its not set, we'll grab the default
1119 $depth = $this->httpRequest->getHeader('Depth');
1121 if (is_null($depth)) return $default;
1123 if ($depth == 'infinity') return self::DEPTH_INFINITY;
1126 // If its an unknown value. we'll grab the default
1127 if (!ctype_digit($depth)) return $default;
1134 * Returns the HTTP range header
1136 * This method returns null if there is no well-formed HTTP range request
1137 * header or array($start, $end).
1139 * The first number is the offset of the first byte in the range.
1140 * The second number is the offset of the last byte in the range.
1142 * If the second offset is null, it should be treated as the offset of the last byte of the entity
1143 * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
1145 * @return array|null
1147 public function getHTTPRange() {
1149 $range = $this->httpRequest->getHeader('range');
1150 if (is_null($range)) return null;
1152 // Matching "Range: bytes=1234-5678: both numbers are optional
1154 if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null;
1156 if ($matches[1]==='' && $matches[2]==='') return null;
1159 $matches[1]!==''?$matches[1]:null,
1160 $matches[2]!==''?$matches[2]:null,
1167 * Returns information about Copy and Move requests
1169 * This function is created to help getting information about the source and the destination for the
1170 * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
1172 * The returned value is an array with the following keys:
1173 * * destination - Destination path
1174 * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
1178 public function getCopyAndMoveInfo() {
1180 // Collecting the relevant HTTP headers
1181 if (!$this->httpRequest->getHeader('Destination')) throw new Sabre_DAV_Exception_BadRequest('The destination header was not supplied');
1182 $destination = $this->calculateUri($this->httpRequest->getHeader('Destination'));
1183 $overwrite = $this->httpRequest->getHeader('Overwrite');
1184 if (!$overwrite) $overwrite = 'T';
1185 if (strtoupper($overwrite)=='T') $overwrite = true;
1186 elseif (strtoupper($overwrite)=='F') $overwrite = false;
1187 // We need to throw a bad request exception, if the header was invalid
1188 else throw new Sabre_DAV_Exception_BadRequest('The HTTP Overwrite header should be either T or F');
1190 list($destinationDir) = Sabre_DAV_URLUtil::splitPath($destination);
1193 $destinationParent = $this->tree->getNodeForPath($destinationDir);
1194 if (!($destinationParent instanceof Sabre_DAV_ICollection)) throw new Sabre_DAV_Exception_UnsupportedMediaType('The destination node is not a collection');
1195 } catch (Sabre_DAV_Exception_NotFound $e) {
1197 // If the destination parent node is not found, we throw a 409
1198 throw new Sabre_DAV_Exception_Conflict('The destination node is not found');
1203 $destinationNode = $this->tree->getNodeForPath($destination);
1205 // If this succeeded, it means the destination already exists
1206 // we'll need to throw precondition failed in case overwrite is false
1207 if (!$overwrite) throw new Sabre_DAV_Exception_PreconditionFailed('The destination node already exists, and the overwrite header is set to false','Overwrite');
1209 } catch (Sabre_DAV_Exception_NotFound $e) {
1211 // Destination didn't exist, we're all good
1212 $destinationNode = false;
1218 // These are the three relevant properties we need to return
1220 'destination' => $destination,
1221 'destinationExists' => $destinationNode==true,
1222 'destinationNode' => $destinationNode,
1228 * Returns a list of properties for a path
1230 * This is a simplified version getPropertiesForPath.
1231 * if you aren't interested in status codes, but you just
1232 * want to have a flat list of properties. Use this method.
1234 * @param string $path
1235 * @param array $propertyNames
1237 public function getProperties($path, $propertyNames) {
1239 $result = $this->getPropertiesForPath($path,$propertyNames,0);
1240 return $result[0][200];
1245 * A kid-friendly way to fetch properties for a node's children.
1247 * The returned array will be indexed by the path of the of child node.
1248 * Only properties that are actually found will be returned.
1250 * The parent node will not be returned.
1252 * @param string $path
1253 * @param array $propertyNames
1256 public function getPropertiesForChildren($path, $propertyNames) {
1259 foreach($this->getPropertiesForPath($path,$propertyNames,1) as $k=>$row) {
1261 // Skipping the parent path
1262 if ($k === 0) continue;
1264 $result[$row['href']] = $row[200];
1272 * Returns a list of HTTP headers for a particular resource
1274 * The generated http headers are based on properties provided by the
1275 * resource. The method basically provides a simple mapping between
1276 * DAV property and HTTP header.
1278 * The headers are intended to be used for HEAD and GET requests.
1280 * @param string $path
1283 public function getHTTPHeaders($path) {
1285 $propertyMap = array(
1286 '{DAV:}getcontenttype' => 'Content-Type',
1287 '{DAV:}getcontentlength' => 'Content-Length',
1288 '{DAV:}getlastmodified' => 'Last-Modified',
1289 '{DAV:}getetag' => 'ETag',
1292 $properties = $this->getProperties($path,array_keys($propertyMap));
1295 foreach($propertyMap as $property=>$header) {
1296 if (!isset($properties[$property])) continue;
1298 if (is_scalar($properties[$property])) {
1299 $headers[$header] = $properties[$property];
1301 // GetLastModified gets special cased
1302 } elseif ($properties[$property] instanceof Sabre_DAV_Property_GetLastModified) {
1303 $headers[$header] = Sabre_HTTP_Util::toHTTPDate($properties[$property]->getTime());
1313 * Returns a list of properties for a given path
1315 * The path that should be supplied should have the baseUrl stripped out
1316 * The list of properties should be supplied in Clark notation. If the list is empty
1317 * 'allprops' is assumed.
1319 * If a depth of 1 is requested child elements will also be returned.
1321 * @param string $path
1322 * @param array $propertyNames
1326 public function getPropertiesForPath($path, $propertyNames = array(), $depth = 0) {
1328 if ($depth!=0) $depth = 1;
1330 $returnPropertyList = array();
1332 $parentNode = $this->tree->getNodeForPath($path);
1334 $path => $parentNode
1336 if ($depth==1 && $parentNode instanceof Sabre_DAV_ICollection) {
1337 foreach($this->tree->getChildren($path) as $childNode)
1338 $nodes[$path . '/' . $childNode->getName()] = $childNode;
1341 // If the propertyNames array is empty, it means all properties are requested.
1342 // We shouldn't actually return everything we know though, and only return a
1344 $allProperties = count($propertyNames)==0;
1346 foreach($nodes as $myPath=>$node) {
1348 $currentPropertyNames = $propertyNames;
1350 $newProperties = array(
1355 if ($allProperties) {
1356 // Default list of propertyNames, when all properties were requested.
1357 $currentPropertyNames = array(
1358 '{DAV:}getlastmodified',
1359 '{DAV:}getcontentlength',
1360 '{DAV:}resourcetype',
1361 '{DAV:}quota-used-bytes',
1362 '{DAV:}quota-available-bytes',
1364 '{DAV:}getcontenttype',
1368 // If the resourceType was not part of the list, we manually add it
1369 // and mark it for removal. We need to know the resourcetype in order
1370 // to make certain decisions about the entry.
1371 // WebDAV dictates we should add a / and the end of href's for collections
1373 if (!in_array('{DAV:}resourcetype',$currentPropertyNames)) {
1374 $currentPropertyNames[] = '{DAV:}resourcetype';
1378 $result = $this->broadcastEvent('beforeGetProperties',array($myPath, $node, &$currentPropertyNames, &$newProperties));
1379 // If this method explicitly returned false, we must ignore this
1380 // node as it is inaccessible.
1381 if ($result===false) continue;
1383 if (count($currentPropertyNames) > 0) {
1385 if ($node instanceof Sabre_DAV_IProperties)
1386 $newProperties['200'] = $newProperties[200] + $node->getProperties($currentPropertyNames);
1391 foreach($currentPropertyNames as $prop) {
1393 if (isset($newProperties[200][$prop])) continue;
1396 case '{DAV:}getlastmodified' : if ($node->getLastModified()) $newProperties[200][$prop] = new Sabre_DAV_Property_GetLastModified($node->getLastModified()); break;
1397 case '{DAV:}getcontentlength' :
1398 if ($node instanceof Sabre_DAV_IFile) {
1399 $size = $node->getSize();
1400 if (!is_null($size)) {
1401 $newProperties[200][$prop] = (int)$node->getSize();
1405 case '{DAV:}quota-used-bytes' :
1406 if ($node instanceof Sabre_DAV_IQuota) {
1407 $quotaInfo = $node->getQuotaInfo();
1408 $newProperties[200][$prop] = $quotaInfo[0];
1411 case '{DAV:}quota-available-bytes' :
1412 if ($node instanceof Sabre_DAV_IQuota) {
1413 $quotaInfo = $node->getQuotaInfo();
1414 $newProperties[200][$prop] = $quotaInfo[1];
1417 case '{DAV:}getetag' : if ($node instanceof Sabre_DAV_IFile && $etag = $node->getETag()) $newProperties[200][$prop] = $etag; break;
1418 case '{DAV:}getcontenttype' : if ($node instanceof Sabre_DAV_IFile && $ct = $node->getContentType()) $newProperties[200][$prop] = $ct; break;
1419 case '{DAV:}supported-report-set' :
1421 foreach($this->plugins as $plugin) {
1422 $reports = array_merge($reports, $plugin->getSupportedReportSet($myPath));
1424 $newProperties[200][$prop] = new Sabre_DAV_Property_SupportedReportSet($reports);
1426 case '{DAV:}resourcetype' :
1427 $newProperties[200]['{DAV:}resourcetype'] = new Sabre_DAV_Property_ResourceType();
1428 foreach($this->resourceTypeMapping as $className => $resourceType) {
1429 if ($node instanceof $className) $newProperties[200]['{DAV:}resourcetype']->add($resourceType);
1435 // If we were unable to find the property, we will list it as 404.
1436 if (!$allProperties && !isset($newProperties[200][$prop])) $newProperties[404][$prop] = null;
1440 $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties));
1442 $newProperties['href'] = trim($myPath,'/');
1444 // Its is a WebDAV recommendation to add a trailing slash to collectionnames.
1445 // Apple's iCal also requires a trailing slash for principals (rfc 3744).
1446 // Therefore we add a trailing / for any non-file. This might need adjustments
1447 // if we find there are other edge cases.
1448 if ($myPath!='' && isset($newProperties[200]['{DAV:}resourcetype']) && count($newProperties[200]['{DAV:}resourcetype']->getValue())>0) $newProperties['href'] .='/';
1450 // If the resourcetype property was manually added to the requested property list,
1451 // we will remove it again.
1452 if ($removeRT) unset($newProperties[200]['{DAV:}resourcetype']);
1454 $returnPropertyList[] = $newProperties;
1458 return $returnPropertyList;
1463 * This method is invoked by sub-systems creating a new file.
1465 * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
1466 * It was important to get this done through a centralized function,
1467 * allowing addons to intercept this using the beforeCreateFile event.
1469 * This method will return true if the file was actually created
1471 * @param string $uri
1472 * @param resource $data
1473 * @param string $etag
1476 public function createFile($uri,$data, &$etag = null) {
1478 list($dir,$name) = Sabre_DAV_URLUtil::splitPath($uri);
1480 if (!$this->broadcastEvent('beforeBind',array($uri))) return false;
1482 $parent = $this->tree->getNodeForPath($dir);
1484 if (!$this->broadcastEvent('beforeCreateFile',array($uri, &$data, $parent))) return false;
1486 $etag = $parent->createFile($name,$data);
1487 $this->tree->markDirty($dir);
1489 $this->broadcastEvent('afterBind',array($uri));
1490 $this->broadcastEvent('afterCreateFile',array($uri, $parent));
1496 * This method is invoked by sub-systems creating a new directory.
1498 * @param string $uri
1501 public function createDirectory($uri) {
1503 $this->createCollection($uri,array('{DAV:}collection'),array());
1508 * Use this method to create a new collection
1510 * The {DAV:}resourcetype is specified using the resourceType array.
1511 * At the very least it must contain {DAV:}collection.
1513 * The properties array can contain a list of additional properties.
1515 * @param string $uri The new uri
1516 * @param array $resourceType The resourceType(s)
1517 * @param array $properties A list of properties
1518 * @return array|null
1520 public function createCollection($uri, array $resourceType, array $properties) {
1522 list($parentUri,$newName) = Sabre_DAV_URLUtil::splitPath($uri);
1524 // Making sure {DAV:}collection was specified as resourceType
1525 if (!in_array('{DAV:}collection', $resourceType)) {
1526 throw new Sabre_DAV_Exception_InvalidResourceType('The resourceType for this collection must at least include {DAV:}collection');
1530 // Making sure the parent exists
1533 $parent = $this->tree->getNodeForPath($parentUri);
1535 } catch (Sabre_DAV_Exception_NotFound $e) {
1537 throw new Sabre_DAV_Exception_Conflict('Parent node does not exist');
1541 // Making sure the parent is a collection
1542 if (!$parent instanceof Sabre_DAV_ICollection) {
1543 throw new Sabre_DAV_Exception_Conflict('Parent node is not a collection');
1548 // Making sure the child does not already exist
1550 $parent->getChild($newName);
1552 // If we got here.. it means there's already a node on that url, and we need to throw a 405
1553 throw new Sabre_DAV_Exception_MethodNotAllowed('The resource you tried to create already exists');
1555 } catch (Sabre_DAV_Exception_NotFound $e) {
1560 if (!$this->broadcastEvent('beforeBind',array($uri))) return;
1562 // There are 2 modes of operation. The standard collection
1563 // creates the directory, and then updates properties
1564 // the extended collection can create it directly.
1565 if ($parent instanceof Sabre_DAV_IExtendedCollection) {
1567 $parent->createExtendedCollection($newName, $resourceType, $properties);
1571 // No special resourcetypes are supported
1572 if (count($resourceType)>1) {
1573 throw new Sabre_DAV_Exception_InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
1576 $parent->createDirectory($newName);
1579 $errorResult = null;
1581 if (count($properties)>0) {
1585 $errorResult = $this->updateProperties($uri, $properties);
1586 if (!isset($errorResult[200])) {
1590 } catch (Sabre_DAV_Exception $e) {
1600 if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
1601 $this->tree->delete($uri);
1603 // Re-throwing exception
1604 if ($exception) throw $exception;
1606 return $errorResult;
1610 $this->tree->markDirty($parentUri);
1611 $this->broadcastEvent('afterBind',array($uri));
1616 * This method updates a resource's properties
1618 * The properties array must be a list of properties. Array-keys are
1619 * property names in clarknotation, array-values are it's values.
1620 * If a property must be deleted, the value should be null.
1622 * Note that this request should either completely succeed, or
1625 * The response is an array with statuscodes for keys, which in turn
1626 * contain arrays with propertynames. This response can be used
1627 * to generate a multistatus body.
1629 * @param string $uri
1630 * @param array $properties
1633 public function updateProperties($uri, array $properties) {
1635 // we'll start by grabbing the node, this will throw the appropriate
1636 // exceptions if it doesn't.
1637 $node = $this->tree->getNodeForPath($uri);
1644 $remainingProperties = $properties;
1647 // Running through all properties to make sure none of them are protected
1648 if (!$hasError) foreach($properties as $propertyName => $value) {
1649 if(in_array($propertyName, $this->protectedProperties)) {
1650 $result[403][$propertyName] = null;
1651 unset($remainingProperties[$propertyName]);
1657 // Allowing addons to take care of property updating
1658 $hasError = !$this->broadcastEvent('updateProperties',array(
1659 &$remainingProperties,
1665 // If the node is not an instance of Sabre_DAV_IProperties, every
1666 // property is 403 Forbidden
1667 if (!$hasError && count($remainingProperties) && !($node instanceof Sabre_DAV_IProperties)) {
1669 foreach($properties as $propertyName=> $value) {
1670 $result[403][$propertyName] = null;
1672 $remainingProperties = array();
1675 // Only if there were no errors we may attempt to update the resource
1678 if (count($remainingProperties)>0) {
1680 $updateResult = $node->updateProperties($remainingProperties);
1682 if ($updateResult===true) {
1684 foreach($remainingProperties as $propertyName=>$value) {
1685 $result[200][$propertyName] = null;
1688 } elseif ($updateResult===false) {
1689 // The node failed to update the properties for an
1691 foreach($remainingProperties as $propertyName=>$value) {
1692 $result[403][$propertyName] = null;
1695 } elseif (is_array($updateResult)) {
1697 // The node has detailed update information
1698 // We need to merge the results with the earlier results.
1699 foreach($updateResult as $status => $props) {
1700 if (is_array($props)) {
1701 if (!isset($result[$status]))
1702 $result[$status] = array();
1704 $result[$status] = array_merge($result[$status], $updateResult[$status]);
1709 throw new Sabre_DAV_Exception('Invalid result from updateProperties');
1711 $remainingProperties = array();
1716 foreach($remainingProperties as $propertyName=>$value) {
1717 // if there are remaining properties, it must mean
1718 // there's a dependency failure
1719 $result[424][$propertyName] = null;
1722 // Removing empty array values
1723 foreach($result as $status=>$props) {
1725 if (count($props)===0) unset($result[$status]);
1728 $result['href'] = $uri;
1734 * This method checks the main HTTP preconditions.
1736 * Currently these are:
1739 * * If-Modified-Since
1740 * * If-Unmodified-Since
1742 * The method will return true if all preconditions are met
1743 * The method will return false, or throw an exception if preconditions
1744 * failed. If false is returned the operation should be aborted, and
1745 * the appropriate HTTP response headers are already set.
1747 * Normally this method will throw 412 Precondition Failed for failures
1748 * related to If-None-Match, If-Match and If-Unmodified Since. It will
1749 * set the status to 304 Not Modified for If-Modified_since.
1751 * If the $handleAsGET argument is set to true, it will also return 304
1752 * Not Modified for failure of the If-None-Match precondition. This is the
1753 * desired behaviour for HTTP GET and HTTP HEAD requests.
1755 * @param bool $handleAsGET
1758 public function checkPreconditions($handleAsGET = false) {
1760 $uri = $this->getRequestUri();
1765 if ($ifMatch = $this->httpRequest->getHeader('If-Match')) {
1767 // If-Match contains an entity tag. Only if the entity-tag
1768 // matches we are allowed to make the request succeed.
1769 // If the entity-tag is '*' we are only allowed to make the
1770 // request succeed if a resource exists at that url.
1772 $node = $this->tree->getNodeForPath($uri);
1773 } catch (Sabre_DAV_Exception_NotFound $e) {
1774 throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified and the resource did not exist','If-Match');
1777 // Only need to check entity tags if they are not *
1778 if ($ifMatch!=='*') {
1780 // There can be multiple etags
1781 $ifMatch = explode(',',$ifMatch);
1783 foreach($ifMatch as $ifMatchItem) {
1785 // Stripping any extra spaces
1786 $ifMatchItem = trim($ifMatchItem,' ');
1788 $etag = $node->getETag();
1789 if ($etag===$ifMatchItem) {
1792 // Evolution has a bug where it sometimes prepends the "
1793 // with a \. This is our workaround.
1794 if (str_replace('\\"','"', $ifMatchItem) === $etag) {
1801 throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
1806 if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) {
1808 // The If-None-Match header contains an etag.
1809 // Only if the ETag does not match the current ETag, the request will succeed
1810 // The header can also contain *, in which case the request
1811 // will only succeed if the entity does not exist at all.
1815 $node = $this->tree->getNodeForPath($uri);
1816 } catch (Sabre_DAV_Exception_NotFound $e) {
1817 $nodeExists = false;
1822 if ($ifNoneMatch==='*') $haveMatch = true;
1825 // There might be multiple etags
1826 $ifNoneMatch = explode(',', $ifNoneMatch);
1827 $etag = $node->getETag();
1829 foreach($ifNoneMatch as $ifNoneMatchItem) {
1831 // Stripping any extra spaces
1832 $ifNoneMatchItem = trim($ifNoneMatchItem,' ');
1834 if ($etag===$ifNoneMatchItem) $haveMatch = true;
1842 $this->httpResponse->sendStatus(304);
1845 throw new Sabre_DAV_Exception_PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match');
1852 if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) {
1854 // The If-Modified-Since header contains a date. We
1855 // will only return the entity if it has been changed since
1856 // that date. If it hasn't been changed, we return a 304
1858 // Note that this header only has to be checked if there was no If-None-Match header
1859 // as per the HTTP spec.
1860 $date = Sabre_HTTP_Util::parseHTTPDate($ifModifiedSince);
1863 if (is_null($node)) {
1864 $node = $this->tree->getNodeForPath($uri);
1866 $lastMod = $node->getLastModified();
1868 $lastMod = new DateTime('@' . $lastMod);
1869 if ($lastMod <= $date) {
1870 $this->httpResponse->sendStatus(304);
1871 $this->httpResponse->setHeader('Last-Modified', Sabre_HTTP_Util::toHTTPDate($lastMod));
1878 if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) {
1880 // The If-Unmodified-Since will allow allow the request if the
1881 // entity has not changed since the specified date.
1882 $date = Sabre_HTTP_Util::parseHTTPDate($ifUnmodifiedSince);
1884 // We must only check the date if it's valid
1886 if (is_null($node)) {
1887 $node = $this->tree->getNodeForPath($uri);
1889 $lastMod = $node->getLastModified();
1891 $lastMod = new DateTime('@' . $lastMod);
1892 if ($lastMod > $date) {
1893 throw new Sabre_DAV_Exception_PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.','If-Unmodified-Since');
1904 // {{{ XML Readers & Writers
1908 * Generates a WebDAV propfind response body based on a list of nodes
1910 * @param array $fileProperties The list with nodes
1913 public function generateMultiStatus(array $fileProperties) {
1915 $dom = new DOMDocument('1.0','utf-8');
1916 //$dom->formatOutput = true;
1917 $multiStatus = $dom->createElement('d:multistatus');
1918 $dom->appendChild($multiStatus);
1920 // Adding in default namespaces
1921 foreach($this->xmlNamespaces as $namespace=>$prefix) {
1923 $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
1927 foreach($fileProperties as $entry) {
1929 $href = $entry['href'];
1930 unset($entry['href']);
1932 $response = new Sabre_DAV_Property_Response($href,$entry);
1933 $response->serialize($this,$multiStatus);
1937 return $dom->saveXML();
1942 * This method parses a PropPatch request
1944 * PropPatch changes the properties for a resource. This method
1945 * returns a list of properties.
1947 * The keys in the returned array contain the property name (e.g.: {DAV:}displayname,
1948 * and the value contains the property value. If a property is to be removed the value
1951 * @param string $body xml body
1952 * @return array list of properties in need of updating or deletion
1954 public function parsePropPatchRequest($body) {
1956 //We'll need to change the DAV namespace declaration to something else in order to make it parsable
1957 $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
1959 $newProperties = array();
1961 foreach($dom->firstChild->childNodes as $child) {
1963 if ($child->nodeType !== XML_ELEMENT_NODE) continue;
1965 $operation = Sabre_DAV_XMLUtil::toClarkNotation($child);
1967 if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue;
1969 $innerProperties = Sabre_DAV_XMLUtil::parseProperties($child, $this->propertyMap);
1971 foreach($innerProperties as $propertyName=>$propertyValue) {
1973 if ($operation==='{DAV:}remove') {
1974 $propertyValue = null;
1977 $newProperties[$propertyName] = $propertyValue;
1983 return $newProperties;
1988 * This method parses the PROPFIND request and returns its information
1990 * This will either be a list of properties, or an empty array; in which case
1991 * an {DAV:}allprop was requested.
1993 * @param string $body
1996 public function parsePropFindRequest($body) {
1998 // If the propfind body was empty, it means IE is requesting 'all' properties
1999 if (!$body) return array();
2001 $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
2002 $elem = $dom->getElementsByTagNameNS('DAV:','propfind')->item(0);
2003 return array_keys(Sabre_DAV_XMLUtil::parseProperties($elem));