103 '{DAV:}getcontentlength',
105 '{DAV:}getlastmodified',
106 '{DAV:}lockdiscovery',
107 '{DAV:}supportedlock',
110 '{DAV:}quota-available-bytes',
111 '{DAV:}quota-used-bytes',
114 '{DAV:}supported-privilege-set',
115 '{DAV:}current-user-privilege-set',
117 '{DAV:}acl-restrictions',
118 '{DAV:}inherited-acl-set',
121 '{DAV:}supported-method-set',
122 '{DAV:}supported-report-set',
128 '{http://calendarserver.org/ns/}ctag',
131 '{http://sabredav.org/ns}sync-token',
153 'Sabre\\DAV\\ICollection' =>
'{DAV:}collection',
203 if ($treeOrNode instanceof
Tree) {
204 $this->tree = $treeOrNode;
205 } elseif ($treeOrNode instanceof
INode) {
206 $this->tree =
new Tree($treeOrNode);
207 } elseif (is_array($treeOrNode)) {
211 foreach ($treeOrNode as $node) {
212 if (!($node instanceof INode)) {
213 throw new Exception(
'Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre\\DAV\\INode');
218 $this->tree =
new Tree(
$root);
220 } elseif (is_null($treeOrNode)) {
222 $this->tree =
new Tree(
$root);
224 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');
228 $this->sapi =
new HTTP\Sapi();
229 $this->httpResponse =
new HTTP\Response();
230 $this->httpRequest = $this->sapi->getRequest();
250 $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion());
253 $this->httpRequest->setBaseUrl($this->
getBaseUri());
254 $this->
invokeMethod($this->httpRequest, $this->httpResponse);
259 $this->
emit(
'exception', [$e]);
262 $DOM = new \DOMDocument(
'1.0',
'utf-8');
263 $DOM->formatOutput =
true;
265 $error = $DOM->createElementNS(
'DAV:',
'd:error');
266 $error->setAttribute(
'xmlns:s', self::NS_SABREDAV);
267 $DOM->appendChild($error);
271 return htmlspecialchars($v, ENT_NOQUOTES,
'UTF-8');
275 if (self::$exposeVersion) {
279 $error->appendChild($DOM->createElement(
's:exception',
$h(get_class($e))));
280 $error->appendChild($DOM->createElement(
's:message',
$h($e->getMessage())));
281 if ($this->debugExceptions) {
282 $error->appendChild($DOM->createElement(
's:file',
$h($e->getFile())));
283 $error->appendChild($DOM->createElement(
's:line',
$h($e->getLine())));
284 $error->appendChild($DOM->createElement(
's:code',
$h($e->getCode())));
285 $error->appendChild($DOM->createElement(
's:stacktrace',
$h($e->getTraceAsString())));
288 if ($this->debugExceptions) {
290 while ($previous = $previous->getPrevious()) {
291 $xPrevious = $DOM->createElement(
's:previous-exception');
292 $xPrevious->appendChild($DOM->createElement(
's:exception',
$h(get_class($previous))));
293 $xPrevious->appendChild($DOM->createElement(
's:message',
$h($previous->getMessage())));
294 $xPrevious->appendChild($DOM->createElement(
's:file',
$h($previous->getFile())));
295 $xPrevious->appendChild($DOM->createElement(
's:line',
$h($previous->getLine())));
296 $xPrevious->appendChild($DOM->createElement(
's:code',
$h($previous->getCode())));
297 $xPrevious->appendChild($DOM->createElement(
's:stacktrace',
$h($previous->getTraceAsString())));
298 $error->appendChild($xPrevious);
315 $headers[
'Content-Type'] =
'application/xml; charset=utf-8';
317 $this->httpResponse->setStatus($httpCode);
318 $this->httpResponse->setHeaders($headers);
319 $this->httpResponse->setBody($DOM->saveXML());
320 $this->sapi->sendResponse($this->httpResponse);
335 if ($uri[strlen($uri) - 1] !==
'/')
338 $this->baseUri = $uri;
349 if (is_null($this->baseUri)) $this->baseUri = $this->
guessBaseUri();
364 $pathInfo = $this->httpRequest->getRawServerValue(
'PATH_INFO');
365 $uri = $this->httpRequest->getRawServerValue(
'REQUEST_URI');
368 if (!empty($pathInfo)) {
371 if ($pos = strpos($uri,
'?'))
372 $uri = substr($uri, 0, $pos);
382 if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) {
383 $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo));
387 throw new Exception(
'The REQUEST_URI (' . $uri .
') did not end with the contents of PATH_INFO (' . $pathInfo .
'). This server might be misconfigured.');
421 if (isset($this->plugins[
$name]))
422 return $this->plugins[
$name];
446 if (!$this->logger) {
449 return $this->logger;
465 if (!$this->
emit(
'beforeMethod:' . $method, [$request, $response]))
return;
466 if (!$this->
emit(
'beforeMethod', [$request, $response]))
return;
468 if (self::$exposeVersion) {
472 $this->transactionType = strtolower($method);
475 $this->sapi->sendResponse($response);
479 if ($this->
emit(
'method:' . $method, [$request, $response])) {
480 if ($this->
emit(
'method', [$request, $response])) {
481 $exMessage =
"There was no plugin in the system that was willing to handle this " . $method .
" method.";
482 if ($method ===
"GET") {
483 $exMessage .=
" Enable the Browser plugin to get a better result here.";
487 throw new Exception\NotImplemented($exMessage);
491 if (!$this->
emit(
'afterMethod:' . $method, [$request, $response]))
return;
492 if (!$this->
emit(
'afterMethod', [$request, $response]))
return;
495 throw new Exception(
'No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.');
498 $this->sapi->sendResponse($response);
499 $this->
emit(
'afterResponse', [$request, $response]);
529 $this->tree->getNodeForPath(
$path);
531 $methods[] =
'MKCOL';
535 foreach ($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods(
$path));
536 array_unique($methods);
549 return $this->
calculateUri($this->httpRequest->getUrl());
567 if ($uri[0] !=
'/' && strpos($uri,
'://')) {
569 $uri = parse_url($uri, PHP_URL_PATH);
582 } elseif ($uri .
'/' ===
$baseUri) {
588 throw new Exception\Forbidden(
'Requested uri (' . $uri .
') is out of base uri (' . $this->
getBaseUri() .
')');
606 $depth = $this->httpRequest->getHeader(
'Depth');
608 if (is_null($depth))
return $default;
610 if ($depth ==
'infinity')
return self::DEPTH_INFINITY;
614 if (!ctype_digit($depth))
return $default;
636 $range = $this->httpRequest->getHeader(
'range');
637 if (is_null($range))
return null;
641 if (!preg_match(
'/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches))
return null;
643 if ($matches[1] ===
'' && $matches[2] ===
'')
return null;
646 $matches[1] !==
'' ? $matches[1] : null,
647 $matches[2] !==
'' ? $matches[2] : null,
682 'respond-async' =>
false,
691 if ($prefer = $this->httpRequest->getHeader(
'Prefer')) {
698 } elseif ($this->httpRequest->getHeader(
'Brief') ==
't') {
734 $overwrite = $request->
getHeader(
'Overwrite');
735 if (!$overwrite) $overwrite =
'T';
736 if (strtoupper($overwrite) ==
'T') $overwrite =
true;
737 elseif (strtoupper($overwrite) ==
'F') $overwrite =
false;
739 else throw new Exception\BadRequest(
'The HTTP Overwrite header should be either T or F');
744 $destinationParent = $this->tree->getNodeForPath($destinationDir);
745 if (!($destinationParent instanceof
ICollection))
throw new Exception\UnsupportedMediaType(
'The destination node is not a collection');
749 throw new Exception\Conflict(
'The destination node is not found');
754 $destinationNode = $this->tree->getNodeForPath(
$destination);
758 if (!$overwrite)
throw new Exception\PreconditionFailed(
'The destination node already exists, and the overwrite header is set to false',
'Overwrite');
763 $destinationNode =
false;
767 $requestPath = $request->
getPath();
769 throw new Exception\Forbidden(
'Source and destination uri are identical.');
771 if (substr(
$destination, 0, strlen($requestPath) + 1) === $requestPath .
'/') {
772 throw new Exception\Conflict(
'The destination may not be part of the same subtree as the source path.');
778 'destinationExists' => !!$destinationNode,
779 'destinationNode' => $destinationNode,
828 if ($k === 0)
continue;
852 '{DAV:}getcontenttype' =>
'Content-Type',
853 '{DAV:}getcontentlength' =>
'Content-Length',
854 '{DAV:}getlastmodified' =>
'Last-Modified',
855 '{DAV:}getetag' =>
'ETag',
861 foreach ($propertyMap as $property =>
$header) {
862 if (!isset($properties[$property]))
continue;
864 if (is_scalar($properties[$property])) {
865 $headers[
$header] = $properties[$property];
868 } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) {
886 if ($yieldFirst !== null) {
892 if ($newDepth !== self::DEPTH_INFINITY) {
896 foreach ($this->tree->getChildren(
$path) as $childNode) {
897 $subPropFind = clone $propFind;
900 $subPath =
$path .
'/' . $childNode->getName();
902 $subPath = $childNode->getName();
904 $subPropFind->setPath($subPath);
911 if (($newDepth === self::DEPTH_INFINITY || $newDepth >= 1) && $childNode instanceof
ICollection) {
959 if (!$this->enablePropfindDepthInfinity && $depth != 0) $depth = 1;
964 $propFind =
new PropFind(
$path, (array)$propertyNames, $depth, $propFindType);
966 $parentNode = $this->tree->getNodeForPath(
$path);
968 $propFindRequests = [[
973 if (($depth > 0 || $depth === self::DEPTH_INFINITY) && $parentNode instanceof
ICollection) {
974 $propFindRequests = $this->
generatePathNodes(clone $propFind, current($propFindRequests));
977 foreach ($propFindRequests as $propFindRequest) {
979 list($propFind, $node) = $propFindRequest;
982 $result = $propFind->getResultForMultiStatus();
983 $result[
'href'] = $propFind->getPath();
990 if (in_array(
'{DAV:}collection', $resourceType) || in_array(
'{DAV:}principal', $resourceType)) {
1019 $nodes = $this->tree->getMultipleNodes($paths);
1030 if (in_array(
'{DAV:}collection', $resourceType) || in_array(
'{DAV:}principal', $resourceType)) {
1058 return $this->
emit(
'propFind', [$propFind, $node]);
1080 if (!$this->
emit(
'beforeBind', [$uri]))
return false;
1082 $parent = $this->tree->getNodeForPath($dir);
1084 throw new Exception\Conflict(
'Files can only be created as children of collections');
1093 if (!$this->
emit(
'beforeCreateFile', [$uri, &
$data, $parent, &$modified]))
return false;
1097 if ($modified) $etag = null;
1099 $this->tree->markDirty($dir .
'/' .
$name);
1101 $this->
emit(
'afterBind', [$uri]);
1102 $this->
emit(
'afterCreateFile', [$uri, $parent]);
1119 $node = $this->tree->getNodeForPath($uri);
1127 if (!$this->
emit(
'beforeWriteContent', [$uri, $node, &
$data, &$modified]))
return false;
1129 $etag = $node->put(
$data);
1130 if ($modified) $etag = null;
1131 $this->
emit(
'afterWriteContent', [$uri, $node]);
1163 $parent = $this->tree->getNodeForPath($parentUri);
1166 throw new Exception\Conflict(
'Parent node does not exist');
1172 throw new Exception\Conflict(
'Parent node is not a collection');
1177 $parent->getChild($newName);
1180 throw new Exception\MethodNotAllowed(
'The resource you tried to create already exists');
1187 if (!$this->
emit(
'beforeBind', [$uri]))
return;
1196 $parent->createExtendedCollection($newName, $mkCol);
1206 throw new Exception\InvalidResourceType(
'The {DAV:}resourcetype you specified is not supported here.');
1209 $parent->createDirectory($newName);
1217 $this->
emit(
'propPatch', [$uri, $mkCol]);
1224 $formattedResult = [
1228 foreach (
$result as $propertyName => $status) {
1230 if (!isset($formattedResult[$status])) {
1231 $formattedResult[$status] = [];
1233 $formattedResult[$status][$propertyName] = null;
1236 return $formattedResult;
1239 $this->tree->markDirty($parentUri);
1240 $this->
emit(
'afterBind', [$uri]);
1263 $propPatch =
new PropPatch($properties);
1264 $this->
emit(
'propPatch', [
$path, $propPatch]);
1265 $propPatch->commit();
1267 return $propPatch->getResult();
1300 if ($ifMatch = $request->
getHeader(
'If-Match')) {
1307 $node = $this->tree->getNodeForPath(
$path);
1309 throw new Exception\PreconditionFailed(
'An If-Match header was specified and the resource did not exist',
'If-Match');
1313 if ($ifMatch !==
'*') {
1316 $ifMatch = explode(
',', $ifMatch);
1318 foreach ($ifMatch as $ifMatchItem) {
1321 $ifMatchItem = trim($ifMatchItem,
' ');
1323 $etag = $node instanceof
IFile ? $node->
getETag() : null;
1324 if ($etag === $ifMatchItem) {
1329 if (str_replace(
'\\"',
'"', $ifMatchItem) === $etag) {
1336 if ($etag) $response->
setHeader(
'ETag', $etag);
1337 throw new Exception\PreconditionFailed(
'An If-Match header was specified, but none of the specified the ETags matched.',
'If-Match');
1342 if ($ifNoneMatch = $request->
getHeader(
'If-None-Match')) {
1351 $node = $this->tree->getNodeForPath(
$path);
1353 $nodeExists =
false;
1358 if ($ifNoneMatch ===
'*') $haveMatch =
true;
1362 $ifNoneMatch = explode(
',', $ifNoneMatch);
1363 $etag = $node instanceof
IFile ? $node->
getETag() : null;
1365 foreach ($ifNoneMatch as $ifNoneMatchItem) {
1368 $ifNoneMatchItem = trim($ifNoneMatchItem,
' ');
1370 if ($etag === $ifNoneMatchItem) $haveMatch =
true;
1377 if ($etag) $response->
setHeader(
'ETag', $etag);
1382 throw new Exception\PreconditionFailed(
'An If-None-Match header was specified, but the ETag matched (or * was specified).',
'If-None-Match');
1389 if (!$ifNoneMatch && ($ifModifiedSince = $request->
getHeader(
'If-Modified-Since'))) {
1400 if (is_null($node)) {
1401 $node = $this->tree->getNodeForPath(
$path);
1403 $lastMod = $node->getLastModified();
1405 $lastMod = new \DateTime(
'@' . $lastMod);
1406 if ($lastMod <= $date) {
1415 if ($ifUnmodifiedSince = $request->
getHeader(
'If-Unmodified-Since')) {
1423 if (is_null($node)) {
1424 $node = $this->tree->getNodeForPath(
$path);
1426 $lastMod = $node->getLastModified();
1428 $lastMod = new \DateTime(
'@' . $lastMod);
1429 if ($lastMod > $date) {
1430 throw new Exception\PreconditionFailed(
'An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.',
'If-Unmodified-Since');
1447 foreach ($ifConditions as $kk => $ifCondition) {
1448 foreach ($ifCondition[
'tokens'] as
$ii =>
$token) {
1449 $ifConditions[$kk][
'tokens'][
$ii][
'validToken'] =
false;
1456 $this->
emit(
'validateTokens', [$request, &$ifConditions]);
1462 foreach ($ifConditions as $ifCondition) {
1464 $uri = $ifCondition[
'uri'];
1465 $tokens = $ifCondition[
'tokens'];
1468 foreach ($tokens as
$token) {
1470 $tokenValid = $token[
'validToken'] || !$token[
'token'];
1473 if (!$token[
'etag']) {
1478 if ($token[
'etag'] && $tokenValid) {
1482 $node = $this->tree->getNodeForPath($uri);
1483 $etagValid = $node instanceof
IFile && $node->
getETag() == $token[
'etag'];
1488 if (($tokenValid && $etagValid) ^ $token[
'negate']) {
1498 throw new Exception\PreconditionFailed(
'Failed to find a valid token/etag combination for ' . $uri,
'If');
1584 $regex =
'/(?:<(?P<uri>.*?)>\s)?\((?P<not>Not\s)?(?:<(?P<token>[^>]*)>)?(?:\s?)(?:\[(?P<etag>[^\]]*)\])?\)/im';
1585 preg_match_all($regex,
$header, $matches, PREG_SET_ORDER);
1589 foreach ($matches as $match) {
1594 if (!$match[
'uri'] && count($conditions)) {
1595 $conditions[count($conditions) - 1][
'tokens'][] = [
1596 'negate' => $match[
'not'] ? true :
false,
1597 'token' => $match[
'token'],
1598 'etag' => isset($match[
'etag']) ? $match[
'etag'] :
'' 1602 if (!$match[
'uri']) {
1603 $realUri = $request->
getPath();
1612 'negate' => $match[
'not'] ? true :
false,
1613 'token' => $match[
'token'],
1614 'etag' => isset($match[
'etag']) ? $match[
'etag'] :
'' 1636 foreach ($this->resourceTypeMapping as $className => $resourceType) {
1637 if ($node instanceof $className)
$result[] = $resourceType;
1658 $w = $this->xml->getWriter();
1661 $w->startDocument();
1663 $w->startElement(
'{DAV:}multistatus');
1665 foreach ($fileProperties as $entry) {
1667 $href = $entry[
'href'];
1668 unset($entry[
'href']);
1677 'name' =>
'{DAV:}response',
1683 return $w->outputMemory();
This interface represents a HTTP response.
getPropertiesForChildren($path, $propertyNames)
A kid-friendly way to fetch properties for a node's children.
The RequestInterface represents a HTTP request.
getResourceTypeForNode(INode $node)
Returns an array with resourcetypes for a node.
const NS_SABREDAV
XML namespace for all SabreDAV related elements.
The baseclass for all server plugins.
generatePathNodes(PropFind $propFind, array $yieldFirst=null)
Small helper to support PROPFIND with DEPTH_INFINITY.
This class represents a set of properties that are going to be updated.
updateFile($uri, $data, &$etag=null)
This method is invoked by sub-systems updating a file.
getAllowedMethods($path)
Returns an array with all the supported HTTP methods for a specific uri.
foreach($paths as $path) $request
getProperties($path, $propertyNames)
Returns a list of properties for a path.
getPath()
Returns the path this PROPFIND request is for.
static toHTTPDate(\DateTime $dateTime)
Transforms a DateTime object to HTTP's most common date format.
getHTTPHeaders($path)
Returns a list of HTTP headers for a particular resource.
getDepth()
Returns the depth of this propfind request.
getETag()
Returns the ETag for a file.
getPlugins()
Returns all plugins.
getPropertiesForPath($path, $propertyNames=[], $depth=0)
Returns a list of properties for a given path.
$enablePropfindDepthInfinity
This class holds all the information about a PROPFIND request.
parsePrefer($input)
Parses the Prefer header, as defined in RFC7240.
getBaseUri()
Returns the base responding uri.
const VERSION
Full version number.
The IExtendedCollection interface.
The ICollection Interface.
setDepth($depth)
Updates the depth of this propfind request.
serialize(Server $server, \DOMElement $errorNode)
This method allows the exception to include additional information into the WebDAV error response...
checkPreconditions(RequestInterface $request, ResponseInterface $response)
This method checks the main HTTP preconditions.
const DEPTH_INFINITY
Infinity is used for some request supporting the HTTP Depth header and indicates that the operation s...
static parseHTTPDate($dateHeader)
Parses a RFC2616-compatible date string.
addPlugin(ServerPlugin $plugin)
Adds a plugin to the server.
trait LoggerAwareTrait
Basic Implementation of LoggerAwareInterface.
getPropertiesIteratorForPath($path, $propertyNames=[], $depth=0)
Returns a list of properties for a given path.
emit($eventName, array $arguments=[], callable $continueCallBack=null)
Emits an event.
getHTTPPrefer()
Returns the HTTP Prefer header information.
setStatus($status)
Sets the HTTP status code.
invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse=true)
Handles a http request, and execute a method based on its name.
createFile($uri, $data, &$etag=null)
This method is invoked by sub-systems creating a new file.
getHTTPDepth($default=self::DEPTH_INFINITY)
Returns the HTTP depth header.
getRemainingMutations()
Returns the list of properties that don't have a result code yet.
updateProperties($path, array $properties)
This method updates a resource's properties.
Describes a logger-aware instance.
getPlugin($name)
Returns an initialized plugin by it's name.
getHeader($name)
Returns a specific HTTP header, based on it's name.
getHTTPRange()
Returns the HTTP range header.
getResult()
Returns the result of the operation.
guessBaseUri()
This method attempts to detect the base uri.
The core plugin provides all the basic features for a WebDAV server.
This interface represents a file in the directory tree.
The INode interface is the base interface, and the parent class of both ICollection and IFile...
getHTTPCode()
Returns the HTTP statuscode for this exception.
getPropertiesForMultiplePaths(array $paths, array $propertyNames=[])
Returns a list of properties for a list of paths.
const ALLPROPS
An allprops request.
WebDAV {DAV:}response parser.
exec()
Starts the DAV Server.
calculateUri($uri)
Turns a URI such as the REQUEST_URI into a local path.
getPath()
Returns the relative path.
generateMultiStatus($fileProperties, $strip404s=false)
Generates a WebDAV propfind response body based on a list of nodes.
This class represents a MKCOL operation.
getMethod()
Returns the current HTTP method.
setBaseUri($uri)
Sets the base server uri.
static decodePath($path)
Decodes a url-encoded path.
getCopyAndMoveInfo(RequestInterface $request)
Returns information about Copy and Move requests.
const NORMAL
A normal propfind.
getRequestUri()
Gets the uri for the request, keeping the base uri into consideration.
createDirectory($uri)
This method is invoked by sub-systems creating a new directory.
getHTTPHeaders(Server $server)
This method allows the exception to return any extra HTTP response headers.
createCollection($uri, MkCol $mkCol)
Use this method to create a new collection.
while(false !==($line=fgets($in))) if(! $columns) $ignore
commit()
Performs the actual update, and calls all callbacks.
getPluginName()
Returns a plugin name.
getLogger()
Returns the PSR-3 logger object.
normalize($uri)
Takes a URI or partial URI as its argument, and normalizes it.
__construct($treeOrNode=null)
Sets up the server.
static splitPath($path)
Returns the 'dirname' and 'basename' for a path.
initialize(Server $server)
This initializes the plugin.
setHeader($name, $value)
Updates a HTTP header.
getPropertiesByNode(PropFind $propFind, INode $node)
Determines all properties for a node.
This Logger can be used to avoid conditional log calls.
getStatus()
Returns the current HTTP status code.
The tree object is responsible for basic tree operations.
getIfConditions(RequestInterface $request)
This method is created to extract information from the WebDAV HTTP 'If:' header.
getResourceType()
Returns the resourcetype of the new collection.