2179 lines
		
	
	
		
			72 KiB
		
	
	
	
		
			PHP
		
	
	
	
			
		
		
	
	
			2179 lines
		
	
	
		
			72 KiB
		
	
	
	
		
			PHP
		
	
	
	
| <?php
 | |
| 
 | |
| namespace Sabre\DAV;
 | |
| use Sabre\HTTP;
 | |
| 
 | |
| /**
 | |
|  * Main DAV server class
 | |
|  *
 | |
|  * @copyright Copyright (C) 2007-2015 fruux GmbH (https://fruux.com/).
 | |
|  * @author Evert Pot (http://evertpot.com/)
 | |
|  * @license http://sabre.io/license/ Modified BSD License
 | |
|  */
 | |
| class Server {
 | |
| 
 | |
|     /**
 | |
|      * Infinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree
 | |
|      */
 | |
|     const DEPTH_INFINITY = -1;
 | |
| 
 | |
|     /**
 | |
|      * Nodes that are files, should have this as the type property
 | |
|      */
 | |
|     const NODE_FILE = 1;
 | |
| 
 | |
|     /**
 | |
|      * Nodes that are directories, should use this value as the type property
 | |
|      */
 | |
|     const NODE_DIRECTORY = 2;
 | |
| 
 | |
|     /**
 | |
|      * XML namespace for all SabreDAV related elements
 | |
|      */
 | |
|     const NS_SABREDAV = 'http://sabredav.org/ns';
 | |
| 
 | |
|     /**
 | |
|      * The tree object
 | |
|      *
 | |
|      * @var Sabre\DAV\Tree
 | |
|      */
 | |
|     public $tree;
 | |
| 
 | |
|     /**
 | |
|      * The base uri
 | |
|      *
 | |
|      * @var string
 | |
|      */
 | |
|     protected $baseUri = null;
 | |
| 
 | |
|     /**
 | |
|      * httpResponse
 | |
|      *
 | |
|      * @var Sabre\HTTP\Response
 | |
|      */
 | |
|     public $httpResponse;
 | |
| 
 | |
|     /**
 | |
|      * httpRequest
 | |
|      *
 | |
|      * @var Sabre\HTTP\Request
 | |
|      */
 | |
|     public $httpRequest;
 | |
| 
 | |
|     /**
 | |
|      * The list of plugins
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     protected $plugins = array();
 | |
| 
 | |
|     /**
 | |
|      * This array contains a list of callbacks we should call when certain events are triggered
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     protected $eventSubscriptions = array();
 | |
| 
 | |
|     /**
 | |
|      * This is a default list of namespaces.
 | |
|      *
 | |
|      * If you are defining your own custom namespace, add it here to reduce
 | |
|      * bandwidth and improve legibility of xml bodies.
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     public $xmlNamespaces = array(
 | |
|         'DAV:' => 'd',
 | |
|         'http://sabredav.org/ns' => 's',
 | |
|     );
 | |
| 
 | |
|     /**
 | |
|      * The propertymap can be used to map properties from
 | |
|      * requests to property classes.
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     public $propertyMap = array(
 | |
|         '{DAV:}resourcetype' => 'Sabre\\DAV\\Property\\ResourceType',
 | |
|     );
 | |
| 
 | |
|     public $protectedProperties = array(
 | |
|         // RFC4918
 | |
|         '{DAV:}getcontentlength',
 | |
|         '{DAV:}getetag',
 | |
|         '{DAV:}getlastmodified',
 | |
|         '{DAV:}lockdiscovery',
 | |
|         '{DAV:}supportedlock',
 | |
| 
 | |
|         // RFC4331
 | |
|         '{DAV:}quota-available-bytes',
 | |
|         '{DAV:}quota-used-bytes',
 | |
| 
 | |
|         // RFC3744
 | |
|         '{DAV:}supported-privilege-set',
 | |
|         '{DAV:}current-user-privilege-set',
 | |
|         '{DAV:}acl',
 | |
|         '{DAV:}acl-restrictions',
 | |
|         '{DAV:}inherited-acl-set',
 | |
| 
 | |
|     );
 | |
| 
 | |
|     /**
 | |
|      * This is a flag that allow or not showing file, line and code
 | |
|      * of the exception in the returned XML
 | |
|      *
 | |
|      * @var bool
 | |
|      */
 | |
|     public $debugExceptions = false;
 | |
| 
 | |
|     /**
 | |
|      * This property allows you to automatically add the 'resourcetype' value
 | |
|      * based on a node's classname or interface.
 | |
|      *
 | |
|      * The preset ensures that {DAV:}collection is automaticlly added for nodes
 | |
|      * implementing Sabre\DAV\ICollection.
 | |
|      *
 | |
|      * @var array
 | |
|      */
 | |
|     public $resourceTypeMapping = array(
 | |
|         'Sabre\\DAV\\ICollection' => '{DAV:}collection',
 | |
|     );
 | |
| 
 | |
|     /**
 | |
|      * If this setting is turned off, SabreDAV's version number will be hidden
 | |
|      * from various places.
 | |
|      *
 | |
|      * Some people feel this is a good security measure.
 | |
|      *
 | |
|      * @var bool
 | |
|      */
 | |
|     static public $exposeVersion = true;
 | |
| 
 | |
|     /**
 | |
|      * Sets up the server
 | |
|      *
 | |
|      * If a Sabre\DAV\Tree object is passed as an argument, it will
 | |
|      * use it as the directory tree. If a Sabre\DAV\INode is passed, it
 | |
|      * will create a Sabre\DAV\ObjectTree and use the node as the root.
 | |
|      *
 | |
|      * If nothing is passed, a Sabre\DAV\SimpleCollection is created in
 | |
|      * a Sabre\DAV\ObjectTree.
 | |
|      *
 | |
|      * If an array is passed, we automatically create a root node, and use
 | |
|      * the nodes in the array as top-level children.
 | |
|      *
 | |
|      * @param Tree|INode|array|null $treeOrNode The tree object
 | |
|      */
 | |
|     public function __construct($treeOrNode = null) {
 | |
| 
 | |
|         if ($treeOrNode instanceof Tree) {
 | |
|             $this->tree = $treeOrNode;
 | |
|         } elseif ($treeOrNode instanceof INode) {
 | |
|             $this->tree = new ObjectTree($treeOrNode);
 | |
|         } elseif (is_array($treeOrNode)) {
 | |
| 
 | |
|             // If it's an array, a list of nodes was passed, and we need to
 | |
|             // create the root node.
 | |
|             foreach($treeOrNode as $node) {
 | |
|                 if (!($node instanceof INode)) {
 | |
|                     throw new Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre\\DAV\\INode');
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             $root = new SimpleCollection('root', $treeOrNode);
 | |
|             $this->tree = new ObjectTree($root);
 | |
| 
 | |
|         } elseif (is_null($treeOrNode)) {
 | |
|             $root = new SimpleCollection('root');
 | |
|             $this->tree = new ObjectTree($root);
 | |
|         } else {
 | |
|             throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null');
 | |
|         }
 | |
|         $this->httpResponse = new HTTP\Response();
 | |
|         $this->httpRequest = new HTTP\Request();
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Starts the DAV Server
 | |
|      *
 | |
|      * @return void
 | |
|      */
 | |
|     public function exec() {
 | |
| 
 | |
|         try {
 | |
| 
 | |
|             // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
 | |
|             // origin, we must make sure we send back HTTP/1.0 if this was
 | |
|             // requested.
 | |
|             // This is mainly because nginx doesn't support Chunked Transfer
 | |
|             // Encoding, and this forces the webserver SabreDAV is running on,
 | |
|             // to buffer entire responses to calculate Content-Length.
 | |
|             $this->httpResponse->defaultHttpVersion = $this->httpRequest->getHTTPVersion();
 | |
| 
 | |
|             $this->invokeMethod($this->httpRequest->getMethod(), $this->getRequestUri());
 | |
| 
 | |
|         } catch (Exception $e) {
 | |
| 
 | |
|             try {
 | |
|                 $this->broadcastEvent('exception', array($e));
 | |
|             } catch (Exception $ignore) {
 | |
|             }
 | |
|             $DOM = new \DOMDocument('1.0','utf-8');
 | |
|             $DOM->formatOutput = true;
 | |
| 
 | |
|             $error = $DOM->createElementNS('DAV:','d:error');
 | |
|             $error->setAttribute('xmlns:s',self::NS_SABREDAV);
 | |
|             $DOM->appendChild($error);
 | |
| 
 | |
|             $h = function($v) {
 | |
| 
 | |
|                 return htmlspecialchars($v, ENT_NOQUOTES, 'UTF-8');
 | |
| 
 | |
|             };
 | |
| 
 | |
|             $error->appendChild($DOM->createElement('s:exception',$h(get_class($e))));
 | |
|             $error->appendChild($DOM->createElement('s:message',$h($e->getMessage())));
 | |
|             if ($this->debugExceptions) {
 | |
|                 $error->appendChild($DOM->createElement('s:file',$h($e->getFile())));
 | |
|                 $error->appendChild($DOM->createElement('s:line',$h($e->getLine())));
 | |
|                 $error->appendChild($DOM->createElement('s:code',$h($e->getCode())));
 | |
|                 $error->appendChild($DOM->createElement('s:stacktrace',$h($e->getTraceAsString())));
 | |
| 
 | |
|             }
 | |
|             if (self::$exposeVersion) {
 | |
|                 $error->appendChild($DOM->createElement('s:sabredav-version',$h(Version::VERSION)));
 | |
|             }
 | |
| 
 | |
|             if($e instanceof Exception) {
 | |
| 
 | |
|                 $httpCode = $e->getHTTPCode();
 | |
|                 $e->serialize($this,$error);
 | |
|                 $headers = $e->getHTTPHeaders($this);
 | |
| 
 | |
|             } else {
 | |
| 
 | |
|                 $httpCode = 500;
 | |
|                 $headers = array();
 | |
| 
 | |
|             }
 | |
|             $headers['Content-Type'] = 'application/xml; charset=utf-8';
 | |
| 
 | |
|             $this->httpResponse->sendStatus($httpCode);
 | |
|             $this->httpResponse->setHeaders($headers);
 | |
|             $this->httpResponse->sendBody($DOM->saveXML());
 | |
| 
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Sets the base server uri
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     public function setBaseUri($uri) {
 | |
| 
 | |
|         // If the baseUri does not end with a slash, we must add it
 | |
|         if ($uri[strlen($uri)-1]!=='/')
 | |
|             $uri.='/';
 | |
| 
 | |
|         $this->baseUri = $uri;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the base responding uri
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     public function getBaseUri() {
 | |
| 
 | |
|         if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
 | |
|         return $this->baseUri;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method attempts to detect the base uri.
 | |
|      * Only the PATH_INFO variable is considered.
 | |
|      *
 | |
|      * If this variable is not set, the root (/) is assumed.
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     public function guessBaseUri() {
 | |
| 
 | |
|         $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
 | |
|         $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
 | |
| 
 | |
|         // If PATH_INFO is found, we can assume it's accurate.
 | |
|         if (!empty($pathInfo)) {
 | |
| 
 | |
|             // We need to make sure we ignore the QUERY_STRING part
 | |
|             if ($pos = strpos($uri,'?'))
 | |
|                 $uri = substr($uri,0,$pos);
 | |
| 
 | |
|             // PATH_INFO is only set for urls, such as: /example.php/path
 | |
|             // in that case PATH_INFO contains '/path'.
 | |
|             // Note that REQUEST_URI is percent encoded, while PATH_INFO is
 | |
|             // not, Therefore they are only comparable if we first decode
 | |
|             // REQUEST_INFO as well.
 | |
|             $decodedUri = URLUtil::decodePath($uri);
 | |
| 
 | |
|             // A simple sanity check:
 | |
|             if(substr($decodedUri,strlen($decodedUri)-strlen($pathInfo))===$pathInfo) {
 | |
|                 $baseUri = substr($decodedUri,0,strlen($decodedUri)-strlen($pathInfo));
 | |
|                 return rtrim($baseUri,'/') . '/';
 | |
|             }
 | |
| 
 | |
|             throw new Exception('The REQUEST_URI ('. $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.');
 | |
| 
 | |
|         }
 | |
| 
 | |
|         // The last fallback is that we're just going to assume the server root.
 | |
|         return '/';
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Adds a plugin to the server
 | |
|      *
 | |
|      * For more information, console the documentation of Sabre\DAV\ServerPlugin
 | |
|      *
 | |
|      * @param ServerPlugin $plugin
 | |
|      * @return void
 | |
|      */
 | |
|     public function addPlugin(ServerPlugin $plugin) {
 | |
| 
 | |
|         $this->plugins[$plugin->getPluginName()] = $plugin;
 | |
|         $plugin->initialize($this);
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns an initialized plugin by it's name.
 | |
|      *
 | |
|      * This function returns null if the plugin was not found.
 | |
|      *
 | |
|      * @param string $name
 | |
|      * @return ServerPlugin
 | |
|      */
 | |
|     public function getPlugin($name) {
 | |
| 
 | |
|         if (isset($this->plugins[$name]))
 | |
|             return $this->plugins[$name];
 | |
| 
 | |
|         // This is a fallback and deprecated.
 | |
|         foreach($this->plugins as $plugin) {
 | |
|             if (get_class($plugin)===$name) return $plugin;
 | |
|         }
 | |
| 
 | |
|         return null;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns all plugins
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     public function getPlugins() {
 | |
| 
 | |
|         return $this->plugins;
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Subscribe to an event.
 | |
|      *
 | |
|      * When the event is triggered, we'll call all the specified callbacks.
 | |
|      * It is possible to control the order of the callbacks through the
 | |
|      * priority argument.
 | |
|      *
 | |
|      * This is for example used to make sure that the authentication plugin
 | |
|      * is triggered before anything else. If it's not needed to change this
 | |
|      * number, it is recommended to ommit.
 | |
|      *
 | |
|      * @param string $event
 | |
|      * @param callback $callback
 | |
|      * @param int $priority
 | |
|      * @return void
 | |
|      */
 | |
|     public function subscribeEvent($event, $callback, $priority = 100) {
 | |
| 
 | |
|         if (!isset($this->eventSubscriptions[$event])) {
 | |
|             $this->eventSubscriptions[$event] = array();
 | |
|         }
 | |
|         while(isset($this->eventSubscriptions[$event][$priority])) $priority++;
 | |
|         $this->eventSubscriptions[$event][$priority] = $callback;
 | |
|         ksort($this->eventSubscriptions[$event]);
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Broadcasts an event
 | |
|      *
 | |
|      * This method will call all subscribers. If one of the subscribers returns false, the process stops.
 | |
|      *
 | |
|      * The arguments parameter will be sent to all subscribers
 | |
|      *
 | |
|      * @param string $eventName
 | |
|      * @param array $arguments
 | |
|      * @return bool
 | |
|      */
 | |
|     public function broadcastEvent($eventName,$arguments = array()) {
 | |
| 
 | |
|         if (isset($this->eventSubscriptions[$eventName])) {
 | |
| 
 | |
|             foreach($this->eventSubscriptions[$eventName] as $subscriber) {
 | |
| 
 | |
|                 $result = call_user_func_array($subscriber,$arguments);
 | |
|                 if ($result===false) return false;
 | |
| 
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return true;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Handles a http request, and execute a method based on its name
 | |
|      *
 | |
|      * @param string $method
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     public function invokeMethod($method, $uri) {
 | |
| 
 | |
|         $method = strtoupper($method);
 | |
| 
 | |
|         if (!$this->broadcastEvent('beforeMethod',array($method, $uri))) return;
 | |
| 
 | |
|         // Make sure this is a HTTP method we support
 | |
|         $internalMethods = array(
 | |
|             'OPTIONS',
 | |
|             'GET',
 | |
|             'HEAD',
 | |
|             'DELETE',
 | |
|             'PROPFIND',
 | |
|             'MKCOL',
 | |
|             'PUT',
 | |
|             'PROPPATCH',
 | |
|             'COPY',
 | |
|             'MOVE',
 | |
|             'REPORT'
 | |
|         );
 | |
| 
 | |
|         if (in_array($method,$internalMethods)) {
 | |
| 
 | |
|             call_user_func(array($this,'http' . $method), $uri);
 | |
| 
 | |
|         } else {
 | |
| 
 | |
|             if ($this->broadcastEvent('unknownMethod',array($method, $uri))) {
 | |
|                 // Unsupported method
 | |
|                 throw new Exception\NotImplemented('There was no handler found for this "' . $method . '" method');
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     // {{{ HTTP Method implementations
 | |
| 
 | |
|     /**
 | |
|      * HTTP OPTIONS
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     protected function httpOptions($uri) {
 | |
| 
 | |
|         $methods = $this->getAllowedMethods($uri);
 | |
| 
 | |
|         $this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods)));
 | |
|         $features = array('1','3', 'extended-mkcol');
 | |
| 
 | |
|         foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
 | |
| 
 | |
|         $this->httpResponse->setHeader('DAV',implode(', ',$features));
 | |
|         $this->httpResponse->setHeader('MS-Author-Via','DAV');
 | |
|         $this->httpResponse->setHeader('Accept-Ranges','bytes');
 | |
|         if (self::$exposeVersion) {
 | |
|             $this->httpResponse->setHeader('X-Sabre-Version',Version::VERSION);
 | |
|         }
 | |
|         $this->httpResponse->setHeader('Content-Length',0);
 | |
|         $this->httpResponse->sendStatus(200);
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * HTTP GET
 | |
|      *
 | |
|      * This method simply fetches the contents of a uri, like normal
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return bool
 | |
|      */
 | |
|     protected function httpGet($uri) {
 | |
| 
 | |
|         $node = $this->tree->getNodeForPath($uri,0);
 | |
| 
 | |
|         if (!$this->checkPreconditions(true)) return false;
 | |
|         if (!$node instanceof IFile) throw new Exception\NotImplemented('GET is only implemented on File objects');
 | |
| 
 | |
|         $body = $node->get();
 | |
| 
 | |
|         // Converting string into stream, if needed.
 | |
|         if (is_string($body)) {
 | |
|             $stream = fopen('php://temp','r+');
 | |
|             fwrite($stream,$body);
 | |
|             rewind($stream);
 | |
|             $body = $stream;
 | |
|         }
 | |
| 
 | |
|         /*
 | |
|          * TODO: getetag, getlastmodified, getsize should also be used using
 | |
|          * this method
 | |
|          */
 | |
|         $httpHeaders = $this->getHTTPHeaders($uri);
 | |
| 
 | |
|         /* ContentType needs to get a default, because many webservers will otherwise
 | |
|          * default to text/html, and we don't want this for security reasons.
 | |
|          */
 | |
|         if (!isset($httpHeaders['Content-Type'])) {
 | |
|             $httpHeaders['Content-Type'] = 'application/octet-stream';
 | |
|         }
 | |
| 
 | |
| 
 | |
|         if (isset($httpHeaders['Content-Length'])) {
 | |
| 
 | |
|             $nodeSize = $httpHeaders['Content-Length'];
 | |
| 
 | |
|             // Need to unset Content-Length, because we'll handle that during figuring out the range
 | |
|             unset($httpHeaders['Content-Length']);
 | |
| 
 | |
|         } else {
 | |
|             $nodeSize = null;
 | |
|         }
 | |
| 
 | |
|         $this->httpResponse->setHeaders($httpHeaders);
 | |
| 
 | |
|         $range = $this->getHTTPRange();
 | |
|         $ifRange = $this->httpRequest->getHeader('If-Range');
 | |
|         $ignoreRangeHeader = false;
 | |
| 
 | |
|         // If ifRange is set, and range is specified, we first need to check
 | |
|         // the precondition.
 | |
|         if ($nodeSize && $range && $ifRange) {
 | |
| 
 | |
|             // if IfRange is parsable as a date we'll treat it as a DateTime
 | |
|             // otherwise, we must treat it as an etag.
 | |
|             try {
 | |
|                 $ifRangeDate = new \DateTime($ifRange);
 | |
| 
 | |
|                 // It's a date. We must check if the entity is modified since
 | |
|                 // the specified date.
 | |
|                 if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true;
 | |
|                 else {
 | |
|                     $modified = new \DateTime($httpHeaders['Last-Modified']);
 | |
|                     if($modified > $ifRangeDate) $ignoreRangeHeader = true;
 | |
|                 }
 | |
| 
 | |
|             } catch (\Exception $e) {
 | |
| 
 | |
|                 // It's an entity. We can do a simple comparison.
 | |
|                 if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true;
 | |
|                 elseif ($httpHeaders['ETag']!==$ifRange) $ignoreRangeHeader = true;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         // We're only going to support HTTP ranges if the backend provided a filesize
 | |
|         if (!$ignoreRangeHeader && $nodeSize && $range) {
 | |
| 
 | |
|             // Determining the exact byte offsets
 | |
|             if (!is_null($range[0])) {
 | |
| 
 | |
|                 $start = $range[0];
 | |
|                 $end = $range[1]?$range[1]:$nodeSize-1;
 | |
|                 if($start >= $nodeSize)
 | |
|                     throw new Exception\RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
 | |
| 
 | |
|                 if($end < $start) throw new Exception\RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')');
 | |
|                 if($end >= $nodeSize) $end = $nodeSize-1;
 | |
| 
 | |
|             } else {
 | |
| 
 | |
|                 $start = $nodeSize-$range[1];
 | |
|                 $end  = $nodeSize-1;
 | |
| 
 | |
|                 if ($start<0) $start = 0;
 | |
| 
 | |
|             }
 | |
| 
 | |
|             // New read/write stream
 | |
|             $newStream = fopen('php://temp','r+');
 | |
| 
 | |
|             // stream_copy_to_stream() has a bug/feature: the `whence` argument
 | |
|             // is interpreted as SEEK_SET (count from absolute offset 0), while
 | |
|             // for a stream it should be SEEK_CUR (count from current offset).
 | |
|             // If a stream is nonseekable, the function fails. So we *emulate*
 | |
|             // the correct behaviour with fseek():
 | |
|             if ($start > 0) {
 | |
|                 if (($curOffs = ftell($body)) === false) $curOffs = 0;
 | |
|                 fseek($body, $start - $curOffs, SEEK_CUR);
 | |
|             }
 | |
|             stream_copy_to_stream($body, $newStream, $end-$start+1);
 | |
|             rewind($newStream);
 | |
| 
 | |
|             $this->httpResponse->setHeader('Content-Length', $end-$start+1);
 | |
|             $this->httpResponse->setHeader('Content-Range','bytes ' . $start . '-' . $end . '/' . $nodeSize);
 | |
|             $this->httpResponse->sendStatus(206);
 | |
|             $this->httpResponse->sendBody($newStream);
 | |
| 
 | |
| 
 | |
|         } else {
 | |
| 
 | |
|             if ($nodeSize) $this->httpResponse->setHeader('Content-Length',$nodeSize);
 | |
|             $this->httpResponse->sendStatus(200);
 | |
|             $this->httpResponse->sendBody($body);
 | |
| 
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * HTTP HEAD
 | |
|      *
 | |
|      * This method is normally used to take a peak at a url, and only get the HTTP response headers, without the body
 | |
|      * 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
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     protected function httpHead($uri) {
 | |
| 
 | |
|         $node = $this->tree->getNodeForPath($uri);
 | |
|         /* This information is only collection for File objects.
 | |
|          * Ideally we want to throw 405 Method Not Allowed for every
 | |
|          * non-file, but MS Office does not like this
 | |
|          */
 | |
|         if ($node instanceof IFile) {
 | |
|             $headers = $this->getHTTPHeaders($this->getRequestUri());
 | |
|             if (!isset($headers['Content-Type'])) {
 | |
|                 $headers['Content-Type'] = 'application/octet-stream';
 | |
|             }
 | |
|             $this->httpResponse->setHeaders($headers);
 | |
|         }
 | |
|         $this->httpResponse->sendStatus(200);
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * HTTP Delete
 | |
|      *
 | |
|      * The HTTP delete method, deletes a given uri
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     protected function httpDelete($uri) {
 | |
| 
 | |
|         // Checking If-None-Match and related headers.
 | |
|         if (!$this->checkPreconditions()) return;
 | |
| 
 | |
|         if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
 | |
|         $this->tree->delete($uri);
 | |
|         $this->broadcastEvent('afterUnbind',array($uri));
 | |
| 
 | |
|         $this->httpResponse->sendStatus(204);
 | |
|         $this->httpResponse->setHeader('Content-Length','0');
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * WebDAV PROPFIND
 | |
|      *
 | |
|      * This WebDAV method requests information about an uri resource, or a list of resources
 | |
|      * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
 | |
|      * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
 | |
|      *
 | |
|      * The request body contains an XML data structure that has a list of properties the client understands
 | |
|      * The response body is also an xml document, containing information about every uri resource and the requested properties
 | |
|      *
 | |
|      * It has to return a HTTP 207 Multi-status status code
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     protected function httpPropfind($uri) {
 | |
| 
 | |
|         $requestedProperties = $this->parsePropFindRequest($this->httpRequest->getBody(true));
 | |
| 
 | |
|         $depth = $this->getHTTPDepth(1);
 | |
|         // The only two options for the depth of a propfind is 0 or 1
 | |
|         if ($depth!=0) $depth = 1;
 | |
| 
 | |
|         $newProperties = $this->getPropertiesForPath($uri,$requestedProperties,$depth);
 | |
| 
 | |
|         // This is a multi-status response
 | |
|         $this->httpResponse->sendStatus(207);
 | |
|         $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
 | |
|         $this->httpResponse->setHeader('Vary','Brief,Prefer');
 | |
| 
 | |
|         // Normally this header is only needed for OPTIONS responses, however..
 | |
|         // iCal seems to also depend on these being set for PROPFIND. Since
 | |
|         // this is not harmful, we'll add it.
 | |
|         $features = array('1','3', 'extended-mkcol');
 | |
|         foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
 | |
|         $this->httpResponse->setHeader('DAV',implode(', ',$features));
 | |
| 
 | |
|         $prefer = $this->getHTTPPrefer();
 | |
|         $minimal = $prefer['return-minimal'];
 | |
| 
 | |
|         $data = $this->generateMultiStatus($newProperties, $minimal);
 | |
|         $this->httpResponse->sendBody($data);
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * WebDAV PROPPATCH
 | |
|      *
 | |
|      * This method is called to update properties on a Node. The request is an XML body with all the mutations.
 | |
|      * In this XML body it is specified which properties should be set/updated and/or deleted
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     protected function httpPropPatch($uri) {
 | |
| 
 | |
|         $newProperties = $this->parsePropPatchRequest($this->httpRequest->getBody(true));
 | |
| 
 | |
|         $result = $this->updateProperties($uri, $newProperties);
 | |
| 
 | |
|         $prefer = $this->getHTTPPrefer();
 | |
|         $this->httpResponse->setHeader('Vary','Brief,Prefer');
 | |
| 
 | |
|         if ($prefer['return-minimal']) {
 | |
| 
 | |
|             // If return-minimal is specified, we only have to check if the
 | |
|             // request was succesful, and don't need to return the
 | |
|             // multi-status.
 | |
|             $ok = true;
 | |
|             foreach($result as $code=>$prop) {
 | |
|                 if ((int)$code > 299) {
 | |
|                     $ok = false;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if ($ok) {
 | |
| 
 | |
|                 $this->httpResponse->sendStatus(204);
 | |
|                 return;
 | |
| 
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|         $this->httpResponse->sendStatus(207);
 | |
|         $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
 | |
| 
 | |
|         $this->httpResponse->sendBody(
 | |
|             $this->generateMultiStatus(array($result))
 | |
|         );
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * HTTP PUT method
 | |
|      *
 | |
|      * This HTTP method updates a file, or creates a new one.
 | |
|      *
 | |
|      * 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
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return bool
 | |
|      */
 | |
|     protected function httpPut($uri) {
 | |
| 
 | |
|         $body = $this->httpRequest->getBody();
 | |
| 
 | |
|         // Intercepting Content-Range
 | |
|         if ($this->httpRequest->getHeader('Content-Range')) {
 | |
|             /**
 | |
|             Content-Range is dangerous for PUT requests:  PUT per definition
 | |
|             stores a full resource.  draft-ietf-httpbis-p2-semantics-15 says
 | |
|             in section 7.6:
 | |
|               An origin server SHOULD reject any PUT request that contains a
 | |
|               Content-Range header field, since it might be misinterpreted as
 | |
|               partial content (or might be partial content that is being mistakenly
 | |
|               PUT as a full representation).  Partial content updates are possible
 | |
|               by targeting a separately identified resource with state that
 | |
|               overlaps a portion of the larger resource, or by using a different
 | |
|               method that has been specifically defined for partial updates (for
 | |
|               example, the PATCH method defined in [RFC5789]).
 | |
|             This clarifies RFC2616 section 9.6:
 | |
|               The recipient of the entity MUST NOT ignore any Content-*
 | |
|               (e.g. Content-Range) headers that it does not understand or implement
 | |
|               and MUST return a 501 (Not Implemented) response in such cases.
 | |
|             OTOH is a PUT request with a Content-Range currently the only way to
 | |
|             continue an aborted upload request and is supported by curl, mod_dav,
 | |
|             Tomcat and others.  Since some clients do use this feature which results
 | |
|             in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject
 | |
|             all PUT requests with a Content-Range for now.
 | |
|             */
 | |
| 
 | |
|             throw new Exception\NotImplemented('PUT with Content-Range is not allowed.');
 | |
|         }
 | |
| 
 | |
|         // Intercepting the Finder problem
 | |
|         if (($expected = $this->httpRequest->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
 | |
| 
 | |
|             /**
 | |
|             Many webservers will not cooperate well with Finder PUT requests,
 | |
|             because it uses 'Chunked' transfer encoding for the request body.
 | |
| 
 | |
|             The symptom of this problem is that Finder sends files to the
 | |
|             server, but they arrive as 0-length files in PHP.
 | |
| 
 | |
|             If we don't do anything, the user might think they are uploading
 | |
|             files successfully, but they end up empty on the server. Instead,
 | |
|             we throw back an error if we detect this.
 | |
| 
 | |
|             The reason Finder uses Chunked, is because it thinks the files
 | |
|             might change as it's being uploaded, and therefore the
 | |
|             Content-Length can vary.
 | |
| 
 | |
|             Instead it sends the X-Expected-Entity-Length header with the size
 | |
|             of the file at the very start of the request. If this header is set,
 | |
|             but we don't get a request body we will fail the request to
 | |
|             protect the end-user.
 | |
|             */
 | |
| 
 | |
|             // Only reading first byte
 | |
|             $firstByte = fread($body,1);
 | |
|             if (strlen($firstByte)!==1) {
 | |
|                 throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
 | |
|             }
 | |
| 
 | |
|             // The body needs to stay intact, so we copy everything to a
 | |
|             // temporary stream.
 | |
| 
 | |
|             $newBody = fopen('php://temp','r+');
 | |
|             fwrite($newBody,$firstByte);
 | |
|             stream_copy_to_stream($body, $newBody);
 | |
|             rewind($newBody);
 | |
| 
 | |
|             $body = $newBody;
 | |
| 
 | |
|         }
 | |
| 
 | |
|         // Checking If-None-Match and related headers.
 | |
|         if (!$this->checkPreconditions()) return;
 | |
| 
 | |
|         if ($this->tree->nodeExists($uri)) {
 | |
| 
 | |
|             $node = $this->tree->getNodeForPath($uri);
 | |
| 
 | |
|             // If the node is a collection, we'll deny it
 | |
|             if (!($node instanceof IFile)) throw new Exception\Conflict('PUT is not allowed on non-files.');
 | |
|             if (!$this->broadcastEvent('beforeWriteContent',array($uri, $node, &$body))) return false;
 | |
| 
 | |
|             $etag = $node->put($body);
 | |
| 
 | |
|             $this->broadcastEvent('afterWriteContent',array($uri, $node));
 | |
| 
 | |
|             $this->httpResponse->setHeader('Content-Length','0');
 | |
|             if ($etag) $this->httpResponse->setHeader('ETag',$etag);
 | |
|             $this->httpResponse->sendStatus(204);
 | |
| 
 | |
|         } else {
 | |
| 
 | |
|             $etag = null;
 | |
|             // If we got here, the resource didn't exist yet.
 | |
|             if (!$this->createFile($this->getRequestUri(),$body,$etag)) {
 | |
|                 // For one reason or another the file was not created.
 | |
|                 return;
 | |
|             }
 | |
| 
 | |
|             $this->httpResponse->setHeader('Content-Length','0');
 | |
|             if ($etag) $this->httpResponse->setHeader('ETag', $etag);
 | |
|             $this->httpResponse->sendStatus(201);
 | |
| 
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * WebDAV MKCOL
 | |
|      *
 | |
|      * The MKCOL method is used to create a new collection (directory) on the server
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     protected function httpMkcol($uri) {
 | |
| 
 | |
|         $requestBody = $this->httpRequest->getBody(true);
 | |
| 
 | |
|         if ($requestBody) {
 | |
| 
 | |
|             $contentType = $this->httpRequest->getHeader('Content-Type');
 | |
|             if (strpos($contentType,'application/xml')!==0 && strpos($contentType,'text/xml')!==0) {
 | |
| 
 | |
|                 // We must throw 415 for unsupported mkcol bodies
 | |
|                 throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
 | |
| 
 | |
|             }
 | |
| 
 | |
|             $dom = XMLUtil::loadDOMDocument($requestBody);
 | |
|             if (XMLUtil::toClarkNotation($dom->firstChild)!=='{DAV:}mkcol') {
 | |
| 
 | |
|                 // We must throw 415 for unsupported mkcol bodies
 | |
|                 throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must be a {DAV:}mkcol request construct.');
 | |
| 
 | |
|             }
 | |
| 
 | |
|             $properties = array();
 | |
|             foreach($dom->firstChild->childNodes as $childNode) {
 | |
| 
 | |
|                 if (XMLUtil::toClarkNotation($childNode)!=='{DAV:}set') continue;
 | |
|                 $properties = array_merge($properties, XMLUtil::parseProperties($childNode, $this->propertyMap));
 | |
| 
 | |
|             }
 | |
|             if (!isset($properties['{DAV:}resourcetype']))
 | |
|                 throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property');
 | |
| 
 | |
|             $resourceType = $properties['{DAV:}resourcetype']->getValue();
 | |
|             unset($properties['{DAV:}resourcetype']);
 | |
| 
 | |
|         } else {
 | |
| 
 | |
|             $properties = array();
 | |
|             $resourceType = array('{DAV:}collection');
 | |
| 
 | |
|         }
 | |
| 
 | |
|         $result = $this->createCollection($uri, $resourceType, $properties);
 | |
| 
 | |
|         if (is_array($result)) {
 | |
|             $this->httpResponse->sendStatus(207);
 | |
|             $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
 | |
| 
 | |
|             $this->httpResponse->sendBody(
 | |
|                 $this->generateMultiStatus(array($result))
 | |
|             );
 | |
| 
 | |
|         } else {
 | |
|             $this->httpResponse->setHeader('Content-Length','0');
 | |
|             $this->httpResponse->sendStatus(201);
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * WebDAV HTTP MOVE method
 | |
|      *
 | |
|      * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return bool
 | |
|      */
 | |
|     protected function httpMove($uri) {
 | |
| 
 | |
|         $moveInfo = $this->getCopyAndMoveInfo();
 | |
| 
 | |
|         // If the destination is part of the source tree, we must fail
 | |
|         if ($moveInfo['destination']==$uri)
 | |
|             throw new Exception\Forbidden('Source and destination uri are identical.');
 | |
| 
 | |
|         if ($moveInfo['destinationExists']) {
 | |
| 
 | |
|             if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false;
 | |
|             $this->tree->delete($moveInfo['destination']);
 | |
|             $this->broadcastEvent('afterUnbind',array($moveInfo['destination']));
 | |
| 
 | |
|         }
 | |
| 
 | |
|         if (!$this->broadcastEvent('beforeUnbind',array($uri))) return false;
 | |
|         if (!$this->broadcastEvent('beforeBind',array($moveInfo['destination']))) return false;
 | |
|         $this->tree->move($uri,$moveInfo['destination']);
 | |
|         $this->broadcastEvent('afterUnbind',array($uri));
 | |
|         $this->broadcastEvent('afterBind',array($moveInfo['destination']));
 | |
| 
 | |
|         // If a resource was overwritten we should send a 204, otherwise a 201
 | |
|         $this->httpResponse->setHeader('Content-Length','0');
 | |
|         $this->httpResponse->sendStatus($moveInfo['destinationExists']?204:201);
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * WebDAV HTTP COPY method
 | |
|      *
 | |
|      * This method copies one uri to a different uri, and works much like the MOVE request
 | |
|      * A lot of the actual request processing is done in getCopyMoveInfo
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return bool
 | |
|      */
 | |
|     protected function httpCopy($uri) {
 | |
| 
 | |
|         $copyInfo = $this->getCopyAndMoveInfo();
 | |
|         // If the destination is part of the source tree, we must fail
 | |
|         if ($copyInfo['destination']==$uri)
 | |
|             throw new Exception\Forbidden('Source and destination uri are identical.');
 | |
| 
 | |
|         if ($copyInfo['destinationExists']) {
 | |
|             if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false;
 | |
|             $this->tree->delete($copyInfo['destination']);
 | |
| 
 | |
|         }
 | |
|         if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false;
 | |
|         $this->tree->copy($uri,$copyInfo['destination']);
 | |
|         $this->broadcastEvent('afterBind',array($copyInfo['destination']));
 | |
| 
 | |
|         // If a resource was overwritten we should send a 204, otherwise a 201
 | |
|         $this->httpResponse->setHeader('Content-Length','0');
 | |
|         $this->httpResponse->sendStatus($copyInfo['destinationExists']?204:201);
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * HTTP REPORT method implementation
 | |
|      *
 | |
|      * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
 | |
|      * It's used in a lot of extensions, so it made sense to implement it into the core.
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     protected function httpReport($uri) {
 | |
| 
 | |
|         $body = $this->httpRequest->getBody(true);
 | |
|         $dom = XMLUtil::loadDOMDocument($body);
 | |
| 
 | |
|         $reportName = XMLUtil::toClarkNotation($dom->firstChild);
 | |
| 
 | |
|         if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) {
 | |
| 
 | |
|             // If broadcastEvent returned true, it means the report was not supported
 | |
|             throw new Exception\ReportNotSupported();
 | |
| 
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     // }}}
 | |
|     // {{{ HTTP/WebDAV protocol helpers
 | |
| 
 | |
|     /**
 | |
|      * Returns an array with all the supported HTTP methods for a specific uri.
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return array
 | |
|      */
 | |
|     public function getAllowedMethods($uri) {
 | |
| 
 | |
|         $methods = array(
 | |
|             'OPTIONS',
 | |
|             'GET',
 | |
|             'HEAD',
 | |
|             'DELETE',
 | |
|             'PROPFIND',
 | |
|             'PUT',
 | |
|             'PROPPATCH',
 | |
|             'COPY',
 | |
|             'MOVE',
 | |
|             'REPORT'
 | |
|         );
 | |
| 
 | |
|         // The MKCOL is only allowed on an unmapped uri
 | |
|         try {
 | |
|             $this->tree->getNodeForPath($uri);
 | |
|         } catch (Exception\NotFound $e) {
 | |
|             $methods[] = 'MKCOL';
 | |
|         }
 | |
| 
 | |
|         // We're also checking if any of the plugins register any new methods
 | |
|         foreach($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($uri));
 | |
|         array_unique($methods);
 | |
| 
 | |
|         return $methods;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Gets the uri for the request, keeping the base uri into consideration
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     public function getRequestUri() {
 | |
| 
 | |
|         return $this->calculateUri($this->httpRequest->getUri());
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Calculates the uri for a request, making sure that the base uri is stripped out
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
 | |
|      * @return string
 | |
|      */
 | |
|     public function calculateUri($uri) {
 | |
| 
 | |
|         if ($uri[0]!='/' && strpos($uri,'://')) {
 | |
| 
 | |
|             $uri = parse_url($uri,PHP_URL_PATH);
 | |
| 
 | |
|         }
 | |
| 
 | |
|         $uri = str_replace('//','/',$uri);
 | |
| 
 | |
|         if (strpos($uri,$this->getBaseUri())===0) {
 | |
| 
 | |
|             return trim(URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/');
 | |
| 
 | |
|         // A special case, if the baseUri was accessed without a trailing
 | |
|         // slash, we'll accept it as well.
 | |
|         } elseif ($uri.'/' === $this->getBaseUri()) {
 | |
| 
 | |
|             return '';
 | |
| 
 | |
|         } else {
 | |
| 
 | |
|             throw new Exception\Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
 | |
| 
 | |
|         }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the HTTP depth header
 | |
|      *
 | |
|      * 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
 | |
|      * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
 | |
|      *
 | |
|      * @param mixed $default
 | |
|      * @return int
 | |
|      */
 | |
|     public function getHTTPDepth($default = self::DEPTH_INFINITY) {
 | |
| 
 | |
|         // If its not set, we'll grab the default
 | |
|         $depth = $this->httpRequest->getHeader('Depth');
 | |
| 
 | |
|         if (is_null($depth)) return $default;
 | |
| 
 | |
|         if ($depth == 'infinity') return self::DEPTH_INFINITY;
 | |
| 
 | |
| 
 | |
|         // If its an unknown value. we'll grab the default
 | |
|         if (!ctype_digit($depth)) return $default;
 | |
| 
 | |
|         return (int)$depth;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the HTTP range header
 | |
|      *
 | |
|      * This method returns null if there is no well-formed HTTP range request
 | |
|      * header or array($start, $end).
 | |
|      *
 | |
|      * The first number is the offset of the first byte in the range.
 | |
|      * The second number is the offset of the last byte in the range.
 | |
|      *
 | |
|      * If the second offset is null, it should be treated as the offset of the last byte of the entity
 | |
|      * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
 | |
|      *
 | |
|      * @return array|null
 | |
|      */
 | |
|     public function getHTTPRange() {
 | |
| 
 | |
|         $range = $this->httpRequest->getHeader('range');
 | |
|         if (is_null($range)) return null;
 | |
| 
 | |
|         // Matching "Range: bytes=1234-5678: both numbers are optional
 | |
| 
 | |
|         if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null;
 | |
| 
 | |
|         if ($matches[1]==='' && $matches[2]==='') return null;
 | |
| 
 | |
|         return array(
 | |
|             $matches[1]!==''?$matches[1]:null,
 | |
|             $matches[2]!==''?$matches[2]:null,
 | |
|         );
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the HTTP Prefer header information.
 | |
|      *
 | |
|      * The prefer header is defined in:
 | |
|      * http://tools.ietf.org/html/draft-snell-http-prefer-14
 | |
|      *
 | |
|      * This method will return an array with options.
 | |
|      *
 | |
|      * Currently, the following options may be returned:
 | |
|      *   array(
 | |
|      *      'return-asynch'         => true,
 | |
|      *      'return-minimal'        => true,
 | |
|      *      'return-representation' => true,
 | |
|      *      'wait'                  => 30,
 | |
|      *      'strict'                => true,
 | |
|      *      'lenient'               => true,
 | |
|      *   )
 | |
|      *
 | |
|      * This method also supports the Brief header, and will also return
 | |
|      * 'return-minimal' if the brief header was set to 't'.
 | |
|      *
 | |
|      * For the boolean options, false will be returned if the headers are not
 | |
|      * specified. For the integer options it will be 'null'.
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     public function getHTTPPrefer() {
 | |
| 
 | |
|         $result = array(
 | |
|             'return-asynch'         => false,
 | |
|             'return-minimal'        => false,
 | |
|             'return-representation' => false,
 | |
|             'wait'                  => null,
 | |
|             'strict'                => false,
 | |
|             'lenient'               => false,
 | |
|         );
 | |
| 
 | |
|         if ($prefer = $this->httpRequest->getHeader('Prefer')) {
 | |
| 
 | |
|             $parameters = array_map('trim',
 | |
|                 explode(',', $prefer)
 | |
|             );
 | |
| 
 | |
|             foreach($parameters as $parameter) {
 | |
| 
 | |
|                 // Right now our regex only supports the tokens actually
 | |
|                 // specified in the draft. We may need to expand this if new
 | |
|                 // tokens get registered.
 | |
|                 if(!preg_match('/^(?P<token>[a-z0-9-]+)(?:=(?P<value>[0-9]+))?$/', $parameter, $matches)) {
 | |
|                     continue;
 | |
|                 }
 | |
| 
 | |
|                 switch($matches['token']) {
 | |
| 
 | |
|                     case 'return-asynch' :
 | |
|                     case 'return-minimal' :
 | |
|                     case 'return-representation' :
 | |
|                     case 'strict' :
 | |
|                     case 'lenient' :
 | |
|                         $result[$matches['token']] = true;
 | |
|                         break;
 | |
|                     case 'wait' :
 | |
|                         $result[$matches['token']] = $matches['value'];
 | |
|                         break;
 | |
| 
 | |
|                 }
 | |
| 
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|         if ($this->httpRequest->getHeader('Brief')=='t') {
 | |
|             $result['return-minimal'] = true;
 | |
|         }
 | |
| 
 | |
|         return $result;
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Returns information about Copy and Move requests
 | |
|      *
 | |
|      * This function is created to help getting information about the source and the destination for the
 | |
|      * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
 | |
|      *
 | |
|      * The returned value is an array with the following keys:
 | |
|      *   * destination - Destination path
 | |
|      *   * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
 | |
|      *
 | |
|      * @return array
 | |
|      */
 | |
|     public function getCopyAndMoveInfo() {
 | |
| 
 | |
|         // Collecting the relevant HTTP headers
 | |
|         if (!$this->httpRequest->getHeader('Destination')) throw new Exception\BadRequest('The destination header was not supplied');
 | |
|         $destination = $this->calculateUri($this->httpRequest->getHeader('Destination'));
 | |
|         $overwrite = $this->httpRequest->getHeader('Overwrite');
 | |
|         if (!$overwrite) $overwrite = 'T';
 | |
|         if (strtoupper($overwrite)=='T') $overwrite = true;
 | |
|         elseif (strtoupper($overwrite)=='F') $overwrite = false;
 | |
|         // We need to throw a bad request exception, if the header was invalid
 | |
|         else throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F');
 | |
| 
 | |
|         list($destinationDir) = URLUtil::splitPath($destination);
 | |
| 
 | |
|         try {
 | |
|             $destinationParent = $this->tree->getNodeForPath($destinationDir);
 | |
|             if (!($destinationParent instanceof ICollection)) throw new Exception\UnsupportedMediaType('The destination node is not a collection');
 | |
|         } catch (Exception\NotFound $e) {
 | |
| 
 | |
|             // If the destination parent node is not found, we throw a 409
 | |
|             throw new Exception\Conflict('The destination node is not found');
 | |
|         }
 | |
| 
 | |
|         try {
 | |
| 
 | |
|             $destinationNode = $this->tree->getNodeForPath($destination);
 | |
| 
 | |
|             // If this succeeded, it means the destination already exists
 | |
|             // we'll need to throw precondition failed in case overwrite is false
 | |
|             if (!$overwrite) throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false','Overwrite');
 | |
| 
 | |
|         } catch (Exception\NotFound $e) {
 | |
| 
 | |
|             // Destination didn't exist, we're all good
 | |
|             $destinationNode = false;
 | |
| 
 | |
| 
 | |
| 
 | |
|         }
 | |
| 
 | |
|         // These are the three relevant properties we need to return
 | |
|         return array(
 | |
|             'destination'       => $destination,
 | |
|             'destinationExists' => $destinationNode==true,
 | |
|             'destinationNode'   => $destinationNode,
 | |
|         );
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns a list of properties for a path
 | |
|      *
 | |
|      * This is a simplified version getPropertiesForPath.
 | |
|      * if you aren't interested in status codes, but you just
 | |
|      * want to have a flat list of properties. Use this method.
 | |
|      *
 | |
|      * @param string $path
 | |
|      * @param array $propertyNames
 | |
|      */
 | |
|     public function getProperties($path, $propertyNames) {
 | |
| 
 | |
|         $result = $this->getPropertiesForPath($path,$propertyNames,0);
 | |
|         return $result[0][200];
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * A kid-friendly way to fetch properties for a node's children.
 | |
|      *
 | |
|      * The returned array will be indexed by the path of the of child node.
 | |
|      * Only properties that are actually found will be returned.
 | |
|      *
 | |
|      * The parent node will not be returned.
 | |
|      *
 | |
|      * @param string $path
 | |
|      * @param array $propertyNames
 | |
|      * @return array
 | |
|      */
 | |
|     public function getPropertiesForChildren($path, $propertyNames) {
 | |
| 
 | |
|         $result = array();
 | |
|         foreach($this->getPropertiesForPath($path,$propertyNames,1) as $k=>$row) {
 | |
| 
 | |
|             // Skipping the parent path
 | |
|             if ($k === 0) continue;
 | |
| 
 | |
|             $result[$row['href']] = $row[200];
 | |
| 
 | |
|         }
 | |
|         return $result;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns a list of HTTP headers for a particular resource
 | |
|      *
 | |
|      * The generated http headers are based on properties provided by the
 | |
|      * resource. The method basically provides a simple mapping between
 | |
|      * DAV property and HTTP header.
 | |
|      *
 | |
|      * The headers are intended to be used for HEAD and GET requests.
 | |
|      *
 | |
|      * @param string $path
 | |
|      * @return array
 | |
|      */
 | |
|     public function getHTTPHeaders($path) {
 | |
| 
 | |
|         $propertyMap = array(
 | |
|             '{DAV:}getcontenttype'   => 'Content-Type',
 | |
|             '{DAV:}getcontentlength' => 'Content-Length',
 | |
|             '{DAV:}getlastmodified'  => 'Last-Modified',
 | |
|             '{DAV:}getetag'          => 'ETag',
 | |
|         );
 | |
| 
 | |
|         $properties = $this->getProperties($path,array_keys($propertyMap));
 | |
| 
 | |
|         $headers = array();
 | |
|         foreach($propertyMap as $property=>$header) {
 | |
|             if (!isset($properties[$property])) continue;
 | |
| 
 | |
|             if (is_scalar($properties[$property])) {
 | |
|                 $headers[$header] = $properties[$property];
 | |
| 
 | |
|             // GetLastModified gets special cased
 | |
|             } elseif ($properties[$property] instanceof Property\GetLastModified) {
 | |
|                 $headers[$header] = HTTP\Util::toHTTPDate($properties[$property]->getTime());
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return $headers;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns a list of properties for a given path
 | |
|      *
 | |
|      * The path that should be supplied should have the baseUrl stripped out
 | |
|      * The list of properties should be supplied in Clark notation. If the list is empty
 | |
|      * 'allprops' is assumed.
 | |
|      *
 | |
|      * If a depth of 1 is requested child elements will also be returned.
 | |
|      *
 | |
|      * @param string $path
 | |
|      * @param array $propertyNames
 | |
|      * @param int $depth
 | |
|      * @return array
 | |
|      */
 | |
|     public function getPropertiesForPath($path, $propertyNames = array(), $depth = 0) {
 | |
| 
 | |
|         if ($depth!=0) $depth = 1;
 | |
| 
 | |
|         $path = rtrim($path,'/');
 | |
| 
 | |
|         // This event allows people to intercept these requests early on in the
 | |
|         // process.
 | |
|         //
 | |
|         // We're not doing anything with the result, but this can be helpful to
 | |
|         // pre-fetch certain expensive live properties.
 | |
|         $this->broadCastEvent('beforeGetPropertiesForPath', array($path, $propertyNames, $depth));
 | |
| 
 | |
|         $returnPropertyList = array();
 | |
| 
 | |
|         $parentNode = $this->tree->getNodeForPath($path);
 | |
|         $nodes = array(
 | |
|             $path => $parentNode
 | |
|         );
 | |
|         if ($depth==1 && $parentNode instanceof ICollection) {
 | |
|             foreach($this->tree->getChildren($path) as $childNode)
 | |
|                 $nodes[$path . '/' . $childNode->getName()] = $childNode;
 | |
|         }
 | |
| 
 | |
|         // If the propertyNames array is empty, it means all properties are requested.
 | |
|         // We shouldn't actually return everything we know though, and only return a
 | |
|         // sensible list.
 | |
|         $allProperties = count($propertyNames)==0;
 | |
| 
 | |
|         foreach($nodes as $myPath=>$node) {
 | |
| 
 | |
|             $currentPropertyNames = $propertyNames;
 | |
| 
 | |
|             $newProperties = array(
 | |
|                 '200' => array(),
 | |
|                 '404' => array(),
 | |
|             );
 | |
| 
 | |
|             if ($allProperties) {
 | |
|                 // Default list of propertyNames, when all properties were requested.
 | |
|                 $currentPropertyNames = array(
 | |
|                     '{DAV:}getlastmodified',
 | |
|                     '{DAV:}getcontentlength',
 | |
|                     '{DAV:}resourcetype',
 | |
|                     '{DAV:}quota-used-bytes',
 | |
|                     '{DAV:}quota-available-bytes',
 | |
|                     '{DAV:}getetag',
 | |
|                     '{DAV:}getcontenttype',
 | |
|                 );
 | |
|             }
 | |
| 
 | |
|             // If the resourceType was not part of the list, we manually add it
 | |
|             // and mark it for removal. We need to know the resourcetype in order
 | |
|             // to make certain decisions about the entry.
 | |
|             // WebDAV dictates we should add a / and the end of href's for collections
 | |
|             $removeRT = false;
 | |
|             if (!in_array('{DAV:}resourcetype',$currentPropertyNames)) {
 | |
|                 $currentPropertyNames[] = '{DAV:}resourcetype';
 | |
|                 $removeRT = true;
 | |
|             }
 | |
| 
 | |
|             $result = $this->broadcastEvent('beforeGetProperties',array($myPath, $node, &$currentPropertyNames, &$newProperties));
 | |
|             // If this method explicitly returned false, we must ignore this
 | |
|             // node as it is inaccessible.
 | |
|             if ($result===false) continue;
 | |
| 
 | |
|             if (count($currentPropertyNames) > 0) {
 | |
| 
 | |
|                 if ($node instanceof IProperties) {
 | |
|                     $nodeProperties = $node->getProperties($currentPropertyNames);
 | |
| 
 | |
|                     // The getProperties method may give us too much,
 | |
|                     // properties, in case the implementor was lazy.
 | |
|                     //
 | |
|                     // So as we loop through this list, we will only take the
 | |
|                     // properties that were actually requested and discard the
 | |
|                     // rest.
 | |
|                     foreach($currentPropertyNames as $k=>$currentPropertyName) {
 | |
|                         if (isset($nodeProperties[$currentPropertyName])) {
 | |
|                             unset($currentPropertyNames[$k]);
 | |
|                             $newProperties[200][$currentPropertyName] = $nodeProperties[$currentPropertyName];
 | |
|                         }
 | |
|                     }
 | |
| 
 | |
|                 }
 | |
| 
 | |
|             }
 | |
| 
 | |
|             foreach($currentPropertyNames as $prop) {
 | |
| 
 | |
|                 if (isset($newProperties[200][$prop])) continue;
 | |
| 
 | |
|                 switch($prop) {
 | |
|                     case '{DAV:}getlastmodified'       : if ($node->getLastModified()) $newProperties[200][$prop] = new Property\GetLastModified($node->getLastModified()); break;
 | |
|                     case '{DAV:}getcontentlength'      :
 | |
|                         if ($node instanceof IFile) {
 | |
|                             $size = $node->getSize();
 | |
|                             if (!is_null($size)) {
 | |
|                                 $newProperties[200][$prop] = (int)$node->getSize();
 | |
|                             }
 | |
|                         }
 | |
|                         break;
 | |
|                     case '{DAV:}quota-used-bytes'      :
 | |
|                         if ($node instanceof IQuota) {
 | |
|                             $quotaInfo = $node->getQuotaInfo();
 | |
|                             $newProperties[200][$prop] = $quotaInfo[0];
 | |
|                         }
 | |
|                         break;
 | |
|                     case '{DAV:}quota-available-bytes' :
 | |
|                         if ($node instanceof IQuota) {
 | |
|                             $quotaInfo = $node->getQuotaInfo();
 | |
|                             $newProperties[200][$prop] = $quotaInfo[1];
 | |
|                         }
 | |
|                         break;
 | |
|                     case '{DAV:}getetag'               : if ($node instanceof IFile && $etag = $node->getETag())  $newProperties[200][$prop] = $etag; break;
 | |
|                     case '{DAV:}getcontenttype'        : if ($node instanceof IFile && $ct = $node->getContentType())  $newProperties[200][$prop] = $ct; break;
 | |
|                     case '{DAV:}supported-report-set'  :
 | |
|                         $reports = array();
 | |
|                         foreach($this->plugins as $plugin) {
 | |
|                             $reports = array_merge($reports, $plugin->getSupportedReportSet($myPath));
 | |
|                         }
 | |
|                         $newProperties[200][$prop] = new Property\SupportedReportSet($reports);
 | |
|                         break;
 | |
|                     case '{DAV:}resourcetype' :
 | |
|                         $newProperties[200]['{DAV:}resourcetype'] = new Property\ResourceType();
 | |
|                         foreach($this->resourceTypeMapping as $className => $resourceType) {
 | |
|                             if ($node instanceof $className) $newProperties[200]['{DAV:}resourcetype']->add($resourceType);
 | |
|                         }
 | |
|                         break;
 | |
| 
 | |
|                 }
 | |
| 
 | |
|                 // If we were unable to find the property, we will list it as 404.
 | |
|                 if (!$allProperties && !isset($newProperties[200][$prop])) $newProperties[404][$prop] = null;
 | |
| 
 | |
|             }
 | |
| 
 | |
|             $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties, $node));
 | |
| 
 | |
|             $newProperties['href'] = trim($myPath,'/');
 | |
| 
 | |
|             // Its is a WebDAV recommendation to add a trailing slash to collectionnames.
 | |
|             // Apple's iCal also requires a trailing slash for principals (rfc 3744), though this is non-standard.
 | |
|             if ($myPath!='' && isset($newProperties[200]['{DAV:}resourcetype'])) {
 | |
|                 $rt = $newProperties[200]['{DAV:}resourcetype'];
 | |
|                 if ($rt->is('{DAV:}collection') || $rt->is('{DAV:}principal')) {
 | |
|                     $newProperties['href'] .='/';
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             // If the resourcetype property was manually added to the requested property list,
 | |
|             // we will remove it again.
 | |
|             if ($removeRT) unset($newProperties[200]['{DAV:}resourcetype']);
 | |
| 
 | |
|             $returnPropertyList[] = $newProperties;
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return $returnPropertyList;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method is invoked by sub-systems creating a new file.
 | |
|      *
 | |
|      * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
 | |
|      * It was important to get this done through a centralized function,
 | |
|      * allowing plugins to intercept this using the beforeCreateFile event.
 | |
|      *
 | |
|      * This method will return true if the file was actually created
 | |
|      *
 | |
|      * @param string   $uri
 | |
|      * @param resource $data
 | |
|      * @param string   $etag
 | |
|      * @return bool
 | |
|      */
 | |
|     public function createFile($uri,$data, &$etag = null) {
 | |
| 
 | |
|         list($dir,$name) = URLUtil::splitPath($uri);
 | |
| 
 | |
|         if (!$this->broadcastEvent('beforeBind',array($uri))) return false;
 | |
| 
 | |
|         $parent = $this->tree->getNodeForPath($dir);
 | |
|         if (!$parent instanceof ICollection) {
 | |
|             throw new Exception\Conflict('Files can only be created as children of collections');
 | |
|         }
 | |
| 
 | |
|         if (!$this->broadcastEvent('beforeCreateFile',array($uri, &$data, $parent))) return false;
 | |
| 
 | |
|         $etag = $parent->createFile($name,$data);
 | |
|         $this->tree->markDirty($dir . '/' . $name);
 | |
| 
 | |
|         $this->broadcastEvent('afterBind',array($uri));
 | |
|         $this->broadcastEvent('afterCreateFile',array($uri, $parent));
 | |
| 
 | |
|         return true;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method is invoked by sub-systems creating a new directory.
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @return void
 | |
|      */
 | |
|     public function createDirectory($uri) {
 | |
| 
 | |
|         $this->createCollection($uri,array('{DAV:}collection'),array());
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Use this method to create a new collection
 | |
|      *
 | |
|      * The {DAV:}resourcetype is specified using the resourceType array.
 | |
|      * At the very least it must contain {DAV:}collection.
 | |
|      *
 | |
|      * The properties array can contain a list of additional properties.
 | |
|      *
 | |
|      * @param string $uri The new uri
 | |
|      * @param array $resourceType The resourceType(s)
 | |
|      * @param array $properties A list of properties
 | |
|      * @return array|null
 | |
|      */
 | |
|     public function createCollection($uri, array $resourceType, array $properties) {
 | |
| 
 | |
|         list($parentUri,$newName) = URLUtil::splitPath($uri);
 | |
| 
 | |
|         // Making sure {DAV:}collection was specified as resourceType
 | |
|         if (!in_array('{DAV:}collection', $resourceType)) {
 | |
|             throw new Exception\InvalidResourceType('The resourceType for this collection must at least include {DAV:}collection');
 | |
|         }
 | |
| 
 | |
| 
 | |
|         // Making sure the parent exists
 | |
|         try {
 | |
| 
 | |
|             $parent = $this->tree->getNodeForPath($parentUri);
 | |
| 
 | |
|         } catch (Exception\NotFound $e) {
 | |
| 
 | |
|             throw new Exception\Conflict('Parent node does not exist');
 | |
| 
 | |
|         }
 | |
| 
 | |
|         // Making sure the parent is a collection
 | |
|         if (!$parent instanceof ICollection) {
 | |
|             throw new Exception\Conflict('Parent node is not a collection');
 | |
|         }
 | |
| 
 | |
| 
 | |
| 
 | |
|         // Making sure the child does not already exist
 | |
|         try {
 | |
|             $parent->getChild($newName);
 | |
| 
 | |
|             // If we got here.. it means there's already a node on that url, and we need to throw a 405
 | |
|             throw new Exception\MethodNotAllowed('The resource you tried to create already exists');
 | |
| 
 | |
|         } catch (Exception\NotFound $e) {
 | |
|             // This is correct
 | |
|         }
 | |
| 
 | |
| 
 | |
|         if (!$this->broadcastEvent('beforeBind',array($uri))) return;
 | |
| 
 | |
|         // There are 2 modes of operation. The standard collection
 | |
|         // creates the directory, and then updates properties
 | |
|         // the extended collection can create it directly.
 | |
|         if ($parent instanceof IExtendedCollection) {
 | |
| 
 | |
|             $parent->createExtendedCollection($newName, $resourceType, $properties);
 | |
| 
 | |
|         } else {
 | |
| 
 | |
|             // No special resourcetypes are supported
 | |
|             if (count($resourceType)>1) {
 | |
|                 throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
 | |
|             }
 | |
| 
 | |
|             $parent->createDirectory($newName);
 | |
|             $rollBack = false;
 | |
|             $exception = null;
 | |
|             $errorResult = null;
 | |
| 
 | |
|             if (count($properties)>0) {
 | |
| 
 | |
|                 try {
 | |
| 
 | |
|                     $errorResult = $this->updateProperties($uri, $properties);
 | |
|                     if (!isset($errorResult[200])) {
 | |
|                         $rollBack = true;
 | |
|                     }
 | |
| 
 | |
|                 } catch (Exception $e) {
 | |
| 
 | |
|                     $rollBack = true;
 | |
|                     $exception = $e;
 | |
| 
 | |
|                 }
 | |
| 
 | |
|             }
 | |
| 
 | |
|             if ($rollBack) {
 | |
|                 if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
 | |
|                 $this->tree->delete($uri);
 | |
| 
 | |
|                 // Re-throwing exception
 | |
|                 if ($exception) throw $exception;
 | |
| 
 | |
|                 return $errorResult;
 | |
|             }
 | |
| 
 | |
|         }
 | |
|         $this->tree->markDirty($parentUri);
 | |
|         $this->broadcastEvent('afterBind',array($uri));
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method updates a resource's properties
 | |
|      *
 | |
|      * The properties array must be a list of properties. Array-keys are
 | |
|      * property names in clarknotation, array-values are it's values.
 | |
|      * If a property must be deleted, the value should be null.
 | |
|      *
 | |
|      * Note that this request should either completely succeed, or
 | |
|      * completely fail.
 | |
|      *
 | |
|      * The response is an array with statuscodes for keys, which in turn
 | |
|      * contain arrays with propertynames. This response can be used
 | |
|      * to generate a multistatus body.
 | |
|      *
 | |
|      * @param string $uri
 | |
|      * @param array $properties
 | |
|      * @return array
 | |
|      */
 | |
|     public function updateProperties($uri, array $properties) {
 | |
| 
 | |
|         // we'll start by grabbing the node, this will throw the appropriate
 | |
|         // exceptions if it doesn't.
 | |
|         $node = $this->tree->getNodeForPath($uri);
 | |
| 
 | |
|         $result = array(
 | |
|             200 => array(),
 | |
|             403 => array(),
 | |
|             424 => array(),
 | |
|         );
 | |
|         $remainingProperties = $properties;
 | |
|         $hasError = false;
 | |
| 
 | |
|         // Running through all properties to make sure none of them are protected
 | |
|         if (!$hasError) foreach($properties as $propertyName => $value) {
 | |
|             if(in_array($propertyName, $this->protectedProperties)) {
 | |
|                 $result[403][$propertyName] = null;
 | |
|                 unset($remainingProperties[$propertyName]);
 | |
|                 $hasError = true;
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (!$hasError) {
 | |
|             // Allowing plugins to take care of property updating
 | |
|             $hasError = !$this->broadcastEvent('updateProperties',array(
 | |
|                 &$remainingProperties,
 | |
|                 &$result,
 | |
|                 $node
 | |
|             ));
 | |
|         }
 | |
| 
 | |
|         // If the node is not an instance of Sabre\DAV\IProperties, every
 | |
|         // property is 403 Forbidden
 | |
|         if (!$hasError && count($remainingProperties) && !($node instanceof IProperties)) {
 | |
|             $hasError = true;
 | |
|             foreach($properties as $propertyName=> $value) {
 | |
|                 $result[403][$propertyName] = null;
 | |
|             }
 | |
|             $remainingProperties = array();
 | |
|         }
 | |
| 
 | |
|         // Only if there were no errors we may attempt to update the resource
 | |
|         if (!$hasError) {
 | |
| 
 | |
|             if (count($remainingProperties)>0) {
 | |
| 
 | |
|                 $updateResult = $node->updateProperties($remainingProperties);
 | |
| 
 | |
|                 if ($updateResult===true) {
 | |
|                     // success
 | |
|                     foreach($remainingProperties as $propertyName=>$value) {
 | |
|                         $result[200][$propertyName] = null;
 | |
|                     }
 | |
| 
 | |
|                 } elseif ($updateResult===false) {
 | |
|                     // The node failed to update the properties for an
 | |
|                     // unknown reason
 | |
|                     foreach($remainingProperties as $propertyName=>$value) {
 | |
|                         $result[403][$propertyName] = null;
 | |
|                     }
 | |
| 
 | |
|                 } elseif (is_array($updateResult)) {
 | |
| 
 | |
|                     // The node has detailed update information
 | |
|                     // We need to merge the results with the earlier results.
 | |
|                     foreach($updateResult as $status => $props) {
 | |
|                         if (is_array($props)) {
 | |
|                             if (!isset($result[$status]))
 | |
|                                 $result[$status] = array();
 | |
| 
 | |
|                             $result[$status] = array_merge($result[$status], $updateResult[$status]);
 | |
|                         }
 | |
|                     }
 | |
| 
 | |
|                 } else {
 | |
|                     throw new Exception('Invalid result from updateProperties');
 | |
|                 }
 | |
|                 $remainingProperties = array();
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|         foreach($remainingProperties as $propertyName=>$value) {
 | |
|             // if there are remaining properties, it must mean
 | |
|             // there's a dependency failure
 | |
|             $result[424][$propertyName] = null;
 | |
|         }
 | |
| 
 | |
|         // Removing empty array values
 | |
|         foreach($result as $status=>$props) {
 | |
| 
 | |
|             if (count($props)===0) unset($result[$status]);
 | |
| 
 | |
|         }
 | |
|         $result['href'] = $uri;
 | |
|         return $result;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method checks the main HTTP preconditions.
 | |
|      *
 | |
|      * Currently these are:
 | |
|      *   * If-Match
 | |
|      *   * If-None-Match
 | |
|      *   * If-Modified-Since
 | |
|      *   * If-Unmodified-Since
 | |
|      *
 | |
|      * The method will return true if all preconditions are met
 | |
|      * The method will return false, or throw an exception if preconditions
 | |
|      * failed. If false is returned the operation should be aborted, and
 | |
|      * the appropriate HTTP response headers are already set.
 | |
|      *
 | |
|      * Normally this method will throw 412 Precondition Failed for failures
 | |
|      * related to If-None-Match, If-Match and If-Unmodified Since. It will
 | |
|      * set the status to 304 Not Modified for If-Modified_since.
 | |
|      *
 | |
|      * If the $handleAsGET argument is set to true, it will also return 304
 | |
|      * Not Modified for failure of the If-None-Match precondition. This is the
 | |
|      * desired behaviour for HTTP GET and HTTP HEAD requests.
 | |
|      *
 | |
|      * @param bool $handleAsGET
 | |
|      * @return bool
 | |
|      */
 | |
|     public function checkPreconditions($handleAsGET = false) {
 | |
| 
 | |
|         $uri = $this->getRequestUri();
 | |
|         $node = null;
 | |
|         $lastMod = null;
 | |
|         $etag = null;
 | |
| 
 | |
|         if ($ifMatch = $this->httpRequest->getHeader('If-Match')) {
 | |
| 
 | |
|             // If-Match contains an entity tag. Only if the entity-tag
 | |
|             // matches we are allowed to make the request succeed.
 | |
|             // If the entity-tag is '*' we are only allowed to make the
 | |
|             // request succeed if a resource exists at that url.
 | |
|             try {
 | |
|                 $node = $this->tree->getNodeForPath($uri);
 | |
|             } catch (Exception\NotFound $e) {
 | |
|                 throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist','If-Match');
 | |
|             }
 | |
| 
 | |
|             // Only need to check entity tags if they are not *
 | |
|             if ($ifMatch!=='*') {
 | |
| 
 | |
|                 // There can be multiple etags
 | |
|                 $ifMatch = explode(',',$ifMatch);
 | |
|                 $haveMatch = false;
 | |
|                 foreach($ifMatch as $ifMatchItem) {
 | |
| 
 | |
|                     // Stripping any extra spaces
 | |
|                     $ifMatchItem = trim($ifMatchItem,' ');
 | |
| 
 | |
|                     $etag = $node->getETag();
 | |
|                     if ($etag===$ifMatchItem) {
 | |
|                         $haveMatch = true;
 | |
|                     } else {
 | |
|                         // Evolution has a bug where it sometimes prepends the "
 | |
|                         // with a \. This is our workaround.
 | |
|                         if (str_replace('\\"','"', $ifMatchItem) === $etag) {
 | |
|                             $haveMatch = true;
 | |
|                         }
 | |
|                     }
 | |
| 
 | |
|                 }
 | |
|                 if (!$haveMatch) {
 | |
|                      throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) {
 | |
| 
 | |
|             // The If-None-Match header contains an etag.
 | |
|             // Only if the ETag does not match the current ETag, the request will succeed
 | |
|             // The header can also contain *, in which case the request
 | |
|             // will only succeed if the entity does not exist at all.
 | |
|             $nodeExists = true;
 | |
|             if (!$node) {
 | |
|                 try {
 | |
|                     $node = $this->tree->getNodeForPath($uri);
 | |
|                 } catch (Exception\NotFound $e) {
 | |
|                     $nodeExists = false;
 | |
|                 }
 | |
|             }
 | |
|             if ($nodeExists) {
 | |
|                 $haveMatch = false;
 | |
|                 if ($ifNoneMatch==='*') $haveMatch = true;
 | |
|                 else {
 | |
| 
 | |
|                     // There might be multiple etags
 | |
|                     $ifNoneMatch = explode(',', $ifNoneMatch);
 | |
|                     $etag = $node->getETag();
 | |
| 
 | |
|                     foreach($ifNoneMatch as $ifNoneMatchItem) {
 | |
| 
 | |
|                         // Stripping any extra spaces
 | |
|                         $ifNoneMatchItem = trim($ifNoneMatchItem,' ');
 | |
| 
 | |
|                         if ($etag===$ifNoneMatchItem) $haveMatch = true;
 | |
| 
 | |
|                     }
 | |
| 
 | |
|                 }
 | |
| 
 | |
|                 if ($haveMatch) {
 | |
|                     if ($handleAsGET) {
 | |
|                         $this->httpResponse->sendStatus(304);
 | |
|                         return false;
 | |
|                     } else {
 | |
|                         throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match');
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|         if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) {
 | |
| 
 | |
|             // The If-Modified-Since header contains a date. We
 | |
|             // will only return the entity if it has been changed since
 | |
|             // that date. If it hasn't been changed, we return a 304
 | |
|             // header
 | |
|             // Note that this header only has to be checked if there was no If-None-Match header
 | |
|             // as per the HTTP spec.
 | |
|             $date = HTTP\Util::parseHTTPDate($ifModifiedSince);
 | |
| 
 | |
|             if ($date) {
 | |
|                 if (is_null($node)) {
 | |
|                     $node = $this->tree->getNodeForPath($uri);
 | |
|                 }
 | |
|                 $lastMod = $node->getLastModified();
 | |
|                 if ($lastMod) {
 | |
|                     $lastMod = new \DateTime('@' . $lastMod);
 | |
|                     if ($lastMod <= $date) {
 | |
|                         $this->httpResponse->sendStatus(304);
 | |
|                         $this->httpResponse->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod));
 | |
|                         return false;
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) {
 | |
| 
 | |
|             // The If-Unmodified-Since will allow allow the request if the
 | |
|             // entity has not changed since the specified date.
 | |
|             $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince);
 | |
| 
 | |
|             // We must only check the date if it's valid
 | |
|             if ($date) {
 | |
|                 if (is_null($node)) {
 | |
|                     $node = $this->tree->getNodeForPath($uri);
 | |
|                 }
 | |
|                 $lastMod = $node->getLastModified();
 | |
|                 if ($lastMod) {
 | |
|                     $lastMod = new \DateTime('@' . $lastMod);
 | |
|                     if ($lastMod > $date) {
 | |
|                         throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.','If-Unmodified-Since');
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|         }
 | |
|         return true;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     // }}}
 | |
|     // {{{ XML Readers & Writers
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Generates a WebDAV propfind response body based on a list of nodes.
 | |
|      *
 | |
|      * If 'strip404s' is set to true, all 404 responses will be removed.
 | |
|      *
 | |
|      * @param array $fileProperties The list with nodes
 | |
|      * @param bool strip404s
 | |
|      * @return string
 | |
|      */
 | |
|     public function generateMultiStatus(array $fileProperties, $strip404s = false) {
 | |
| 
 | |
|         $dom = new \DOMDocument('1.0','utf-8');
 | |
|         //$dom->formatOutput = true;
 | |
|         $multiStatus = $dom->createElement('d:multistatus');
 | |
|         $dom->appendChild($multiStatus);
 | |
| 
 | |
|         // Adding in default namespaces
 | |
|         foreach($this->xmlNamespaces as $namespace=>$prefix) {
 | |
| 
 | |
|             $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
 | |
| 
 | |
|         }
 | |
| 
 | |
|         foreach($fileProperties as $entry) {
 | |
| 
 | |
|             $href = $entry['href'];
 | |
|             unset($entry['href']);
 | |
| 
 | |
|             if ($strip404s && isset($entry[404])) {
 | |
|                 unset($entry[404]);
 | |
|             }
 | |
| 
 | |
|             $response = new Property\Response($href,$entry);
 | |
|             $response->serialize($this,$multiStatus);
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return $dom->saveXML();
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method parses a PropPatch request
 | |
|      *
 | |
|      * PropPatch changes the properties for a resource. This method
 | |
|      * returns a list of properties.
 | |
|      *
 | |
|      * The keys in the returned array contain the property name (e.g.: {DAV:}displayname,
 | |
|      * and the value contains the property value. If a property is to be removed the value
 | |
|      * will be null.
 | |
|      *
 | |
|      * @param string $body xml body
 | |
|      * @return array list of properties in need of updating or deletion
 | |
|      */
 | |
|     public function parsePropPatchRequest($body) {
 | |
| 
 | |
|         //We'll need to change the DAV namespace declaration to something else in order to make it parsable
 | |
|         $dom = XMLUtil::loadDOMDocument($body);
 | |
| 
 | |
|         $newProperties = array();
 | |
| 
 | |
|         foreach($dom->firstChild->childNodes as $child) {
 | |
| 
 | |
|             if ($child->nodeType !== XML_ELEMENT_NODE) continue;
 | |
| 
 | |
|             $operation = XMLUtil::toClarkNotation($child);
 | |
| 
 | |
|             if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue;
 | |
| 
 | |
|             $innerProperties = XMLUtil::parseProperties($child, $this->propertyMap);
 | |
| 
 | |
|             foreach($innerProperties as $propertyName=>$propertyValue) {
 | |
| 
 | |
|                 if ($operation==='{DAV:}remove') {
 | |
|                     $propertyValue = null;
 | |
|                 }
 | |
| 
 | |
|                 $newProperties[$propertyName] = $propertyValue;
 | |
| 
 | |
|             }
 | |
| 
 | |
|         }
 | |
| 
 | |
|         return $newProperties;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method parses the PROPFIND request and returns its information
 | |
|      *
 | |
|      * This will either be a list of properties, or an empty array; in which case
 | |
|      * an {DAV:}allprop was requested.
 | |
|      *
 | |
|      * @param string $body
 | |
|      * @return array
 | |
|      */
 | |
|     public function parsePropFindRequest($body) {
 | |
| 
 | |
|         // If the propfind body was empty, it means IE is requesting 'all' properties
 | |
|         if (!$body) return array();
 | |
| 
 | |
|         $dom = XMLUtil::loadDOMDocument($body);
 | |
|         $elem = $dom->getElementsByTagNameNS('urn:DAV','propfind')->item(0);
 | |
|         return array_keys(XMLUtil::parseProperties($elem));
 | |
| 
 | |
|     }
 | |
| 
 | |
|     // }}}
 | |
| 
 | |
| }
 | |
| 
 |