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');
220 } elseif (is_null($treeOrNode)) {
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;
466 if (!$this->
emit(
'beforeMethod', [$request,
$response]))
return;
468 if (self::$exposeVersion) {
472 $this->transactionType = strtolower($method);
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.";
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.');
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) {
606 $depth = $this->httpRequest->getHeader(
'Depth');
608 if (is_null($depth))
return $default;
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;
744 $destinationParent = $this->tree->getNodeForPath($destinationDir);
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;
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)) {
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);
1177 $parent->getChild($newName);
1187 if (!$this->
emit(
'beforeBind', [$uri]))
return;
1196 $parent->createExtendedCollection($newName, $mkCol);
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);
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);
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);
1378 if (
$request->getMethod() ===
'GET') {
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'];
1478 if (
$token[
'etag'] && $tokenValid) {
1482 $node = $this->tree->getNodeForPath($uri);
1488 if (($tokenValid && $etagValid) ^
$token[
'negate']) {
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']) {
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();
while(false !==( $line=fgets( $in))) if(! $columns) $ignore
foreach($paths as $path) $request
An exception for terminatinating execution or to throw for unit testing.
This Logger can be used to avoid conditional log calls.
The core plugin provides all the basic features for a WebDAV server.
serialize(Server $server, \DOMElement $errorNode)
This method allows the exception to include additional information into the WebDAV error response.
getHTTPCode()
Returns the HTTP statuscode for this exception.
getHTTPHeaders(Server $server)
This method allows the exception to return any extra HTTP response headers.
This class represents a MKCOL operation.
getResourceType()
Returns the resourcetype of the new collection.
This class holds all the information about a PROPFIND request.
getDepth()
Returns the depth of this propfind request.
const NORMAL
A normal propfind.
const ALLPROPS
An allprops request.
getPath()
Returns the path this PROPFIND request is for.
setDepth($depth)
Updates the depth of this propfind request.
This class represents a set of properties that are going to be updated.
commit()
Performs the actual update, and calls all callbacks.
getRemainingMutations()
Returns the list of properties that don't have a result code yet.
getResult()
Returns the result of the operation.
The baseclass for all server plugins.
getPluginName()
Returns a plugin name.
initialize(Server $server)
This initializes the plugin.
getPlugin($name)
Returns an initialized plugin by it's name.
const DEPTH_INFINITY
Infinity is used for some request supporting the HTTP Depth header and indicates that the operation s...
getPlugins()
Returns all plugins.
getPropertiesByNode(PropFind $propFind, INode $node)
Determines all properties for a node.
getIfConditions(RequestInterface $request)
This method is created to extract information from the WebDAV HTTP 'If:' header.
getResourceTypeForNode(INode $node)
Returns an array with resourcetypes for a node.
updateProperties($path, array $properties)
This method updates a resource's properties.
createCollection($uri, MkCol $mkCol)
Use this method to create a new collection.
getHTTPDepth($default=self::DEPTH_INFINITY)
Returns the HTTP depth header.
getBaseUri()
Returns the base responding uri.
updateFile($uri, $data, &$etag=null)
This method is invoked by sub-systems updating a file.
generateMultiStatus($fileProperties, $strip404s=false)
Generates a WebDAV propfind response body based on a list of nodes.
exec()
Starts the DAV Server.
getPropertiesForChildren($path, $propertyNames)
A kid-friendly way to fetch properties for a node's children.
getRequestUri()
Gets the uri for the request, keeping the base uri into consideration.
getHTTPPrefer()
Returns the HTTP Prefer header information.
invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse=true)
Handles a http request, and execute a method based on its name.
guessBaseUri()
This method attempts to detect the base uri.
generatePathNodes(PropFind $propFind, array $yieldFirst=null)
Small helper to support PROPFIND with DEPTH_INFINITY.
calculateUri($uri)
Turns a URI such as the REQUEST_URI into a local path.
getPropertiesForMultiplePaths(array $paths, array $propertyNames=[])
Returns a list of properties for a list of paths.
createFile($uri, $data, &$etag=null)
This method is invoked by sub-systems creating a new file.
const NS_SABREDAV
XML namespace for all SabreDAV related elements.
setBaseUri($uri)
Sets the base server uri.
getHTTPHeaders($path)
Returns a list of HTTP headers for a particular resource.
getAllowedMethods($path)
Returns an array with all the supported HTTP methods for a specific uri.
__construct($treeOrNode=null)
Sets up the server.
getPropertiesForPath($path, $propertyNames=[], $depth=0)
Returns a list of properties for a given path.
getPropertiesIteratorForPath($path, $propertyNames=[], $depth=0)
Returns a list of properties for a given path.
addPlugin(ServerPlugin $plugin)
Adds a plugin to the server.
getHTTPRange()
Returns the HTTP range header.
$enablePropfindDepthInfinity
getLogger()
Returns the PSR-3 logger object.
checkPreconditions(RequestInterface $request, ResponseInterface $response)
This method checks the main HTTP preconditions.
getCopyAndMoveInfo(RequestInterface $request)
Returns information about Copy and Move requests.
createDirectory($uri)
This method is invoked by sub-systems creating a new directory.
getProperties($path, $propertyNames)
Returns a list of properties for a path.
The tree object is responsible for basic tree operations.
const VERSION
Full version number.
WebDAV {DAV:}response parser.
static decodePath($path)
Decodes a url-encoded path.
static splitPath($path)
Returns the 'dirname' and 'basename' for a path.
static parseHTTPDate($dateHeader)
Parses a RFC2616-compatible date string.
static toHTTPDate(\DateTime $dateTime)
Transforms a DateTime object to HTTP's most common date format.
Describes a logger-aware instance.
Describes a logger instance.
The ICollection Interface.
The IExtendedCollection interface.
This interface represents a file in the directory tree.
getETag()
Returns the ETag for a file.
The INode interface is the base interface, and the parent class of both ICollection and IFile.
emit($eventName, array $arguments=[], callable $continueCallBack=null)
Emits an event.
getHeader($name)
Returns a specific HTTP header, based on it's name.
The RequestInterface represents a HTTP request.
This interface represents a HTTP response.
trait LoggerAwareTrait
Basic Implementation of LoggerAwareInterface.
parsePrefer($input)
Parses the Prefer header, as defined in RFC7240.
normalize($uri)
Takes a URI or partial URI as its argument, and normalizes it.