Файл: library/Sabre/Sabre/DAV/Server.php
Строк: 1351
<?php
/**
* Main DAV server class
*
* @package Sabre
* @subpackage DAV
* @version $Id$
* @copyright Copyright (C) 2007-2010 Rooftop Solutions. All rights reserved.
* @author Evert Pot (http://www.rooftopsolutions.nl/)
* @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
*/
class Sabre_DAV_Server {
/**
* Inifinity 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;
const PROP_SET = 1;
const PROP_REMOVE = 2;
/**
* The tree object
*
* @var Sabre_DAV_Tree
*/
public $tree;
/**
* The base uri
*
* @var string
*/
protected $baseUri = '/';
/**
* 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://www.rooftopsolutions.nl/NS/sabredav' => 's',
);
/**
* Class constructor
*
* @param Sabre_DAV_Tree $tree The tree object
* @return void
*/
public function __construct(Sabre_DAV_Tree $tree) {
$this->tree = $tree;
$this->httpResponse = new Sabre_HTTP_Response();
$this->httpRequest = new Sabre_HTTP_Request();
}
/**
* Starts the DAV Server
*
* @return void
*/
public function exec() {
try {
$this->invoke();
} catch (Exception $e) {
$DOM = new DOMDocument('1.0','utf-8');
$DOM->formatOutput = true;
$error = $DOM->createElementNS('DAV:','d:error');
$error->setAttribute('xmlns:s','http://www.rooftopsolutions.nl/NS/sabredav');
$DOM->appendChild($error);
$error->appendChild($DOM->createElement('s:exception',get_class($e)));
$error->appendChild($DOM->createElement('s:message',$e->getMessage()));
$error->appendChild($DOM->createElement('s:file',$e->getFile()));
$error->appendChild($DOM->createElement('s:line',$e->getLine()));
$error->appendChild($DOM->createElement('s:code',$e->getCode()));
$error->appendChild($DOM->createElement('s:sabredav-version',Sabre_DAV_Version::VERSION));
if($e instanceof Sabre_DAV_Exception) {
$httpCode = $e->getHTTPCode();
$e->serialize($this,$error);
} else {
$httpCode = 500;
}
$this->httpResponse->sendStatus($httpCode);
$this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
$this->httpResponse->sendBody($DOM->saveXML());
}
}
/**
* Sets the base server uri
*
* @param string $uri
* @return void
*/
public function setBaseUri($uri) {
$this->baseUri = $uri;
}
/**
* Returns the base responding uri
*
* @return string
*/
public function getBaseUri() {
return $this->baseUri;
}
/**
* Adds a plugin to the server
*
* For more information, console the documentation of Sabre_DAV_ServerPlugin
*
* @param Sabre_DAV_ServerPlugin $plugin
* @return void
*/
public function addPlugin(Sabre_DAV_ServerPlugin $plugin) {
$this->plugins[get_class($plugin)] = $plugin;
$plugin->initialize($this);
}
/**
* Returns an initialized plugin by it's classname.
*
* This function returns null if the plugin was not found.
*
* @param string $className
* @return Sabre_DAV_ServerPlugin
*/
public function getPlugin($className) {
if (isset($this->plugins[$className])) return $this->plugins[$className];
return null;
}
/**
* 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;
}
// {{{ HTTP Method implementations
/**
* HTTP OPTIONS
*
* @return void
*/
protected function httpOptions() {
$methods = $this->getAllowedMethods();
// We're also checking if any of the plugins register any new methods
foreach($this->plugins as $plugin) $methods = array_merge($methods,$plugin->getHTTPMethods());
array_unique($methods);
$this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods)));
$features = array('1','3');
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');
$this->httpResponse->setHeader('X-Sabre-Version',Sabre_DAV_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
*
* @return void
*/
protected function httpGet() {
$node = $this->tree->getNodeForPath($this->getRequestUri(),0);
if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_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;
}
if (!$contentType = $node->getContentType())
$contentType = 'application/octet-stream';
$this->httpResponse->setHeader('Content-Type', $contentType);
if($lastModified = $node->getLastModified())
$this->httpResponse->setHeader('Last-Modified', date(DateTime::RFC1123, $lastModified));
if ($etag = $node->getETag())
$this->httpResponse->setHeader('ETag',$etag);
$nodeSize = $node->getSize();
// We're only going to support HTTP ranges if the backend provided a filesize
if ($nodeSize && $range = $this->getHTTPRange()) {
// 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 Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
if($end < $start) throw new Sabre_DAV_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($body, $newStream, $end-$start+1, $start);
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
*
* @return void
*/
protected function httpHead() {
$node = $this->tree->getNodeForPath($this->getRequestUri());
/* 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 Sabre_DAV_IFile) {
if ($size = $node->getSize())
$this->httpResponse->setHeader('Content-Length',$size);
if ($etag = $node->getETag()) {
$this->httpResponse->setHeader('ETag',$etag);
}
if (!$contentType = $node->getContentType())
$contentType = 'application/octet-stream';
$this->httpResponse->setHeader('Content-Type', $contentType);
if ($lastMod = $node->getLastModified()) {
$this->httpResponse->setHeader('Last-Modified', date(DateTime::RFC1123, $node->getLastModified()));
}
}
$this->httpResponse->sendStatus(200);
}
/**
* HTTP Delete
*
* The HTTP delete method, deletes a given uri
*
* @return void
*/
protected function httpDelete() {
$uri = $this->getRequestUri();
$node = $this->tree->getNodeForPath($uri);
if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
$node->delete();
$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
*
* @return void
*/
public function httpPropfind() {
// $xml = new Sabre_DAV_XMLReader(file_get_contents('php://input'));
$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;
// The requested path
$path = $this->getRequestUri();
$newProperties = $this->getPropertiesForPath($path,$requestedProperties,$depth);
// This is a multi-status response
$this->httpResponse->sendStatus(207);
$this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
$data = $this->generateMultiStatus($newProperties);
$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
*
* @return void
*/
protected function httpPropPatch() {
$mutations = $this->parsePropPatchRequest($this->httpRequest->getBody(true));
$node = $this->tree->getNodeForPath($this->getRequestUri());
if ($node instanceof Sabre_DAV_IProperties) {
$result = $node->updateProperties($mutations);
} else {
$result = array();
foreach($mutations as $mutations) {
$result[] = array($mutations[1],403);
}
}
$this->httpResponse->sendStatus(207);
$this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
// Re-arranging result for generateMultiStatus
$multiStatusResult = array();
foreach($result as $row) {
if (!isset($multiStatusResult[$row[1]])) {
$multiStatusResult[$row[1]] = array();
}
$multiStatusResult[$row[1]][$row[0]] = null;
}
$multiStatusResult['href'] = $this->getRequestUri();
$multiStatusResult = array($multiStatusResult);
$this->httpResponse->sendBody(
$this->generateMultiStatus($multiStatusResult)
);
}
/**
* 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 200 Ok
*
* @return void
*/
protected function httpPut() {
// First we'll do a check to see if the resource already exists
try {
$node = $this->tree->getNodeForPath($this->getRequestUri());
// We got this far, this means the node already exists.
// This also means we should check for the If-None-Match header
if ($this->httpRequest->getHeader('If-None-Match')) {
throw new Sabre_DAV_Exception_PreconditionFailed('The resource already exists, and an If-None-Match header was supplied');
}
// If the node is a collection, we'll deny it
if ($node instanceof Sabre_DAV_ICollection) throw new Sabre_DAV_Exception_Conflict('PUTs on directories are not allowed');
if (!$this->broadcastEvent('beforeWriteContent',$this->getRequestUri())) return false;
$node->put($this->httpRequest->getBody());
$this->httpResponse->setHeader('Content-Length','0');
$this->httpResponse->sendStatus(200);
} catch (Sabre_DAV_Exception_FileNotFound $e) {
// If we got here, the resource didn't exist yet.
$this->createFile($this->getRequestUri(),$this->httpRequest->getBody());
$this->httpResponse->setHeader('Content-Length','0');
$this->httpResponse->sendStatus(201);
}
}
/**
* WebDAV MKCOL
*
* The MKCOL method is used to create a new collection (directory) on the server
*
* @return void
*/
protected function httpMkcol() {
// If there's a body, we're supposed to send an HTTP 415 Unsupported Media Type exception
$requestBody = $this->httpRequest->getBody(true);
if ($requestBody) throw new Sabre_DAV_Exception_UnsupportedMediaType();
// We'll check if the parent exists, and if it's a collection. If this is not the case, we need to throw a conflict exception
try {
if ($parent = $this->tree->getNodeForPath(dirname($this->getRequestUri()))) {
if (!$parent instanceof Sabre_DAV_ICollection) {
throw new Sabre_DAV_Exception_Conflict('Parent node is not a directory');
}
}
} catch (Sabre_DAV_Exception_FileNotFound $e) {
// This means the parent node doesn't exist, and we need to throw a 409 Conflict
throw new Sabre_DAV_Exception_Conflict('Parent node does not exist');
}
try {
$node = $this->tree->getNodeForPath($this->getRequestUri());
// If we got here.. it means there's already a node on that url, and we need to throw a 405
throw new Sabre_DAV_Exception_MethodNotAllowed('The directory you tried to create already exists');
} catch (Sabre_DAV_Exception_FileNotFound $e) {
// This is correct
}
$this->createDirectory($this->getRequestUri());
$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
*
* @return void
*/
protected function httpMove() {
$moveInfo = $this->getCopyAndMoveInfo();
if ($moveInfo['destinationExists']) {
if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false;
$moveInfo['destinationNode']->delete();
}
if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['source']))) return false;
if (!$this->broadcastEvent('beforeBind',array($moveInfo['destination']))) return false;
$this->tree->move($moveInfo['source'],$moveInfo['destination']);
$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
*
* @return void
*/
protected function httpCopy() {
$copyInfo = $this->getCopyAndMoveInfo();
if ($copyInfo['destinationExists']) {
if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false;
$copyInfo['destinationNode']->delete();
}
if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false;
$this->tree->copy($copyInfo['source'],$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.
*
* @return void
*/
protected function httpReport() {
$body = $this->httpRequest->getBody(true);
//We'll need to change the DAV namespace declaration to something else in order to make it parsable
$body = preg_replace("/xmlns(:[A-Za-z0-9_]*)?=("|')DAV:("|')/","xmlns\1="urn:DAV"",$body);
$errorsetting = libxml_use_internal_errors(true);
libxml_clear_errors();
$dom = new DOMDocument();
$dom->loadXML($body);
$dom->preserveWhiteSpace = false;
$namespaceUri = $dom->firstChild->namespaceURI;
if ($namespaceUri=='urn:DAV') $namespaceUri = 'DAV:';
$reportName = '{' . $namespaceUri . '}' . $dom->firstChild->localName;
if ($this->broadcastEvent('report',array($reportName,$dom))) {
// If broadcastEvent returned true, it means the report was not supported
throw new Sabre_DAV_Exception_ReportNotImplemented();
}
}
// }}}
// {{{ HTTP/WebDAV protocol helpers
/**
* Handles a http request, and execute a method based on its name
*
* @return void
*/
protected function invoke() {
$method = strtolower($this->httpRequest->getMethod());
if (!$this->broadcastEvent('beforeMethod',array(strtoupper($method)))) return;
// Make sure this is a HTTP method we support
if (in_array($method,$this->getAllowedMethods())) {
call_user_func(array($this,'http' . $method));
} else {
if ($this->broadcastEvent('unknownMethod',array(strtoupper($method)))) {
// Unsupported method
throw new Sabre_DAV_Exception_NotImplemented();
}
}
}
/**
* Returns an array with all the supported HTTP methods
*
* @return array
*/
protected function getAllowedMethods() {
$methods = array('options','get','head','delete','trace','propfind','mkcol','put','proppatch','copy','move','report');
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 Sabre_DAV_Exception_PermissionDenied 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->baseUri)===0) {
return trim(Sabre_DAV_URLUtil::decodePath(substr($uri,strlen($this->baseUri))),'/');
} else {
throw new Sabre_DAV_Exception_PermissionDenied('Requested uri (' . $uri . ') is out of base uri (' . $this->baseUri . ')');
}
}
/**
* 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-existant
*
* @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)) $depth = $default;
// Infinity
if ($depth == 'infinity') $depth = self::DEPTH_INFINITY;
else {
// If its an unknown value. we'll grab the default
if ($depth!=="0" && (int)$depth==0) $depth == $default;
}
return $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 $mixed
*/
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 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:
* * source - Source path
* * destination - Destination path
* * destinationExists - Wether or not the destination is an existing url (and should therefore be overwritten)
*
* @return array
*/
protected function getCopyAndMoveInfo() {
$source = $this->getRequestUri();
// Collecting the relevant HTTP headers
if (!$this->httpRequest->getHeader('Destination')) throw new Sabre_DAV_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 Sabre_DAV_Exception_BadRequest('The HTTP Overwrite header should be either T or F');
list($destinationDir) = Sabre_DAV_URLUtil::splitPath($destination);
// Collection information on relevant existing nodes
$sourceNode = $this->tree->getNodeForPath($source);
try {
$destinationParent = $this->tree->getNodeForPath($destinationDir);
if (!($destinationParent instanceof Sabre_DAV_ICollection)) throw new Sabre_DAV_Exception_UnsupportedMediaType('The destination node is not a collection');
} catch (Sabre_DAV_Exception_FileNotFound $e) {
// If the destination parent node is not found, we throw a 409
throw new Sabre_DAV_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 Sabre_DAV_Exception_PreconditionFailed('The destination node already exists, and the overwrite header is set to false');
} catch (Sabre_DAV_Exception_FileNotFound $e) {
// Destination didn't exist, we're all good
$destinationNode = false;
}
// These are the three relevant properties we need to return
return array(
'source' => $source,
'destination' => $destination,
'destinationExists' => $destinationNode==true,
'destinationNode' => $destinationNode,
);
}
/**
* 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;
$returnPropertyList = array();
$parentNode = $this->tree->getNodeForPath($path);
$nodes = array(
$path => $parentNode
);
if ($depth==1 && $parentNode instanceof Sabre_DAV_ICollection) {
foreach($parentNode->getChildren() 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) {
$newProperties = array(
'200' => array(),
'404' => array(),
);
if ($node instanceof Sabre_DAV_IProperties)
$newProperties['200'] = $node->getProperties($propertyNames);
if ($allProperties) {
// Default list of propertyNames.
// note that the list might be bigger due to plugins or Node objects
// returning a bigger list.
$propertyNames = array(
'{DAV:}getlastmodified',
'{DAV:}getcontentlength',
'{DAV:}resourcetype',
'{DAV:}quota-used-bytes',
'{DAV:}quota-available-bytes',
'{DAV:}getetag',
'{DAV:}getcontenttype',
);
}
// It's important we add the resourceType, because some systems depend on it
// TODO: Needs to be eliminated, we need to find out why this was
if (!in_array('{DAV:}resourcetype',$propertyNames)) $propertyNames[] = '{DAV:}resourcetype';
foreach($propertyNames as $prop) {
if (isset($newProperties[200][$prop])) continue;
switch($prop) {
case '{DAV:}getlastmodified' : if ($node->getLastModified()) $newProperties[200][$prop] = new Sabre_DAV_Property_GetLastModified($node->getLastModified()); break;
case '{DAV:}getcontentlength' : if ($node instanceof Sabre_DAV_IFile) $newProperties[200][$prop] = (int)$node->getSize(); break;
case '{DAV:}resourcetype' : $newProperties[200][$prop] = new Sabre_DAV_Property_ResourceType($node instanceof Sabre_DAV_ICollection?self::NODE_DIRECTORY:self::NODE_FILE); break;
case '{DAV:}quota-used-bytes' :
if ($node instanceof Sabre_DAV_IQuota) {
$quotaInfo = $node->getQuotaInfo();
$newProperties[200][$prop] = $quotaInfo[0];
}
break;
case '{DAV:}quota-available-bytes' :
if ($node instanceof Sabre_DAV_IQuota) {
$quotaInfo = $node->getQuotaInfo();
$newProperties[200][$prop] = $quotaInfo[1];
}
break;
case '{DAV:}getetag' : if ($node instanceof Sabre_DAV_IFile && $etag = $node->getETag()) $newProperties[200][$prop] = $etag; break;
case '{DAV:}getcontenttype' : if ($node instanceof Sabre_DAV_IFile && $ct = $node->getContentType()) $newProperties[200][$prop] = $ct; 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));
$newProperties['href'] = trim($myPath,'/');
//if (!$properties || in_array('{http://www.apple.com/webdav_fs/props/}appledoubleheader',$properties)) $newProps['{http://www.apple.com/webdav_fs/props/}appledoubleheader'] = base64_encode(str_repeat(' ',82));
$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.
*
* @param string $uri
* @param resource $data
* @return void
*/
public function createFile($uri,$data) {
list($dir,$name) = Sabre_DAV_URLUtil::splitPath($uri);
if (!$this->broadcastEvent('beforeBind',array($uri))) return;
if (!$this->broadcastEvent('beforeCreateFile',array($uri,$data))) return;
$parent = $this->tree->getNodeForPath($dir);
$parent->createFile($name,$data);
$this->broadcastEvent('afterBind',array($uri));
}
/**
* This method is invoked by sub-systems creating a new directory.
*
* @param string $uri
* @return void
*/
public function createDirectory($uri) {
list($dir, $name) = Sabre_DAV_URLUtil::splitPath($uri);
if (!$this->broadcastEvent('beforeBind',array($uri))) return;
$parent = $this->tree->getNodeForPath($dir);
$parent->createDirectory($name);
$this->broadcastEvent('afterBind',array($uri));
}
// }}}
// {{{ XML Readers & Writers
/**
* Generates a WebDAV propfind response body based on a list of nodes
*
* @param array $fileProperties The list with nodes
* @param array $requestedProperties The properties that should be returned
* @return string
*/
public function generateMultiStatus(array $fileProperties) {
$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']);
$response = new Sabre_DAV_Property_Response($href,$entry);
$response->serialize($this,$multiStatus);
}
return $dom->saveXML();
}
/**
* This method parses a PropPatch request
*
* @param string $body xml body
* @return array list of properties in need of updating or deletion
*/
protected function parsePropPatchRequest($body) {
//We'll need to change the DAV namespace declaration to something else in order to make it parsable
$body = preg_replace("/xmlns(:[A-Za-z0-9_]*)?=("|')DAV:("|')/","xmlns\1="urn:DAV"",$body);
$errorsetting = libxml_use_internal_errors(true);
libxml_clear_errors();
$dom = new DOMDocument();
$dom->loadXML($body,LIBXML_NOWARNING | LIBXML_NOERROR);
$dom->preserveWhiteSpace = false;
if ($error = libxml_get_last_error()) {
switch ($error->code) {
// Error 100 is a non-absolute namespace, which WebDAV allows
case 100 :
break;
default :
throw new Sabre_DAV_Exception_BadRequest('The request body was not a valid proppatch request: ' . print_r($error,true));
}
}
$operations = array();
foreach($dom->firstChild->childNodes as $child) {
if ($child->namespaceURI != 'urn:DAV' || ($child->localName != 'set' && $child->localName !='remove')) continue;
$propList = $this->parseProps($child);
foreach($propList as $k=>$propItem) {
$operations[] = array($child->localName=='set'?self::PROP_SET:self::PROP_REMOVE,$k,$propItem);
}
}
return $operations;
}
/**
* 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();
$errorsetting = libxml_use_internal_errors(true);
libxml_clear_errors();
$dom = new DOMDocument();
$body = preg_replace("/xmlns(:[A-Za-z0-9_]*)?=("|')DAV:(\2)/","xmlns\1="urn:DAV"",$body);
$dom->preserveWhiteSpace = false;
$dom->loadXML($body,LIBXML_NOERROR);
if($error = libxml_get_last_error()) {
switch($error->code) {
// Error 100 is a non-absolute namespace, which WebDAV allows
case 100 :
break;
default :
throw new Sabre_DAV_Exception_BadRequest('The request body was not a valid propfind request' . print_r($error,true));
}
}
libxml_use_internal_errors($errorsetting);
$elem = $dom->getElementsByTagNameNS('urn:DAV','propfind')->item(0);
return array_keys($this->parseProps($elem));
}
/**
* Part of parsePropFindRequest
*
* @param DOMNode $prop
* @return array
*/
public function parseProps(DOMNode $prop) {
$propList = array();
foreach($prop->childNodes as $propNode) {
if ($propNode->namespaceURI == 'urn:DAV' && $propNode->localName == 'prop') {
foreach($propNode->childNodes as $propNodeData) {
/* If there are no elements in here, we actually get 1 text node, this special case is dedicated to netdrive */
if ($propNodeData->nodeType != XML_ELEMENT_NODE) continue;
if ($propNodeData->namespaceURI=='urn:DAV') $ns = 'DAV:'; else $ns = $propNodeData->namespaceURI;
$propList['{' . $ns . '}' . $propNodeData->localName] = $propNodeData->textContent;
}
}
}
return $propList;
}
// }}}
}