ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
Server.php
Go to the documentation of this file.
1<?php
2
3namespace Sabre\DAV;
4
10use Sabre\HTTP;
14use Sabre\Uri;
15
23class Server extends EventEmitter implements LoggerAwareInterface {
24
26
30 const DEPTH_INFINITY = -1;
31
35 const NS_SABREDAV = 'http://sabredav.org/ns';
36
42 public $tree;
43
49 protected $baseUri = null;
50
57
64
70 public $sapi;
71
77 protected $plugins = [];
78
91
101
102 // RFC4918
103 '{DAV:}getcontentlength',
104 '{DAV:}getetag',
105 '{DAV:}getlastmodified',
106 '{DAV:}lockdiscovery',
107 '{DAV:}supportedlock',
108
109 // RFC4331
110 '{DAV:}quota-available-bytes',
111 '{DAV:}quota-used-bytes',
112
113 // RFC3744
114 '{DAV:}supported-privilege-set',
115 '{DAV:}current-user-privilege-set',
116 '{DAV:}acl',
117 '{DAV:}acl-restrictions',
118 '{DAV:}inherited-acl-set',
119
120 // RFC3253
121 '{DAV:}supported-method-set',
122 '{DAV:}supported-report-set',
123
124 // RFC6578
125 '{DAV:}sync-token',
126
127 // calendarserver.org extensions
128 '{http://calendarserver.org/ns/}ctag',
129
130 // sabredav extensions
131 '{http://sabredav.org/ns}sync-token',
132
133 ];
134
141 public $debugExceptions = false;
142
153 'Sabre\\DAV\\ICollection' => '{DAV:}collection',
154 ];
155
168
174 public $xml;
175
184 static $exposeVersion = true;
185
201 function __construct($treeOrNode = null) {
202
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)) {
208
209 // If it's an array, a list of nodes was passed, and we need to
210 // create the root node.
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');
214 }
215 }
216
217 $root = new SimpleCollection('root', $treeOrNode);
218 $this->tree = new Tree($root);
219
220 } elseif (is_null($treeOrNode)) {
221 $root = new SimpleCollection('root');
222 $this->tree = new Tree($root);
223 } else {
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');
225 }
226
227 $this->xml = new Xml\Service();
228 $this->sapi = new HTTP\Sapi();
229 $this->httpResponse = new HTTP\Response();
230 $this->httpRequest = $this->sapi->getRequest();
231 $this->addPlugin(new CorePlugin());
232
233 }
234
240 function exec() {
241
242 try {
243
244 // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an
245 // origin, we must make sure we send back HTTP/1.0 if this was
246 // requested.
247 // This is mainly because nginx doesn't support Chunked Transfer
248 // Encoding, and this forces the webserver SabreDAV is running on,
249 // to buffer entire responses to calculate Content-Length.
250 $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion());
251
252 // Setting the base url
253 $this->httpRequest->setBaseUrl($this->getBaseUri());
254 $this->invokeMethod($this->httpRequest, $this->httpResponse);
255
256 } catch (\Exception $e) {
257
258 try {
259 $this->emit('exception', [$e]);
260 } catch (\Exception $ignore) {
261 }
262 $DOM = new \DOMDocument('1.0', 'utf-8');
263 $DOM->formatOutput = true;
264
265 $error = $DOM->createElementNS('DAV:', 'd:error');
266 $error->setAttribute('xmlns:s', self::NS_SABREDAV);
267 $DOM->appendChild($error);
268
269 $h = function($v) {
270
271 return htmlspecialchars($v, ENT_NOQUOTES, 'UTF-8');
272
273 };
274
275 if (self::$exposeVersion) {
276 $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION)));
277 }
278
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())));
286 }
287
288 if ($this->debugExceptions) {
289 $previous = $e;
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);
299 }
300 }
301
302
303 if ($e instanceof Exception) {
304
305 $httpCode = $e->getHTTPCode();
306 $e->serialize($this, $error);
307 $headers = $e->getHTTPHeaders($this);
308
309 } else {
310
311 $httpCode = 500;
312 $headers = [];
313
314 }
315 $headers['Content-Type'] = 'application/xml; charset=utf-8';
316
317 $this->httpResponse->setStatus($httpCode);
318 $this->httpResponse->setHeaders($headers);
319 $this->httpResponse->setBody($DOM->saveXML());
320 $this->sapi->sendResponse($this->httpResponse);
321
322 }
323
324 }
325
332 function setBaseUri($uri) {
333
334 // If the baseUri does not end with a slash, we must add it
335 if ($uri[strlen($uri) - 1] !== '/')
336 $uri .= '/';
337
338 $this->baseUri = $uri;
339
340 }
341
347 function getBaseUri() {
348
349 if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
350 return $this->baseUri;
351
352 }
353
362 function guessBaseUri() {
363
364 $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
365 $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
366
367 // If PATH_INFO is found, we can assume it's accurate.
368 if (!empty($pathInfo)) {
369
370 // We need to make sure we ignore the QUERY_STRING part
371 if ($pos = strpos($uri, '?'))
372 $uri = substr($uri, 0, $pos);
373
374 // PATH_INFO is only set for urls, such as: /example.php/path
375 // in that case PATH_INFO contains '/path'.
376 // Note that REQUEST_URI is percent encoded, while PATH_INFO is
377 // not, Therefore they are only comparable if we first decode
378 // REQUEST_INFO as well.
379 $decodedUri = URLUtil::decodePath($uri);
380
381 // A simple sanity check:
382 if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) {
383 $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo));
384 return rtrim($baseUri, '/') . '/';
385 }
386
387 throw new Exception('The REQUEST_URI (' . $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.');
388
389 }
390
391 // The last fallback is that we're just going to assume the server root.
392 return '/';
393
394 }
395
404 function addPlugin(ServerPlugin $plugin) {
405
406 $this->plugins[$plugin->getPluginName()] = $plugin;
407 $plugin->initialize($this);
408
409 }
410
419 function getPlugin($name) {
420
421 if (isset($this->plugins[$name]))
422 return $this->plugins[$name];
423
424 return null;
425
426 }
427
433 function getPlugins() {
434
435 return $this->plugins;
436
437 }
438
444 function getLogger() {
445
446 if (!$this->logger) {
447 $this->logger = new NullLogger();
448 }
449 return $this->logger;
450
451 }
452
462
463 $method = $request->getMethod();
464
465 if (!$this->emit('beforeMethod:' . $method, [$request, $response])) return;
466 if (!$this->emit('beforeMethod', [$request, $response])) return;
467
468 if (self::$exposeVersion) {
469 $response->setHeader('X-Sabre-Version', Version::VERSION);
470 }
471
472 $this->transactionType = strtolower($method);
473
474 if (!$this->checkPreconditions($request, $response)) {
475 $this->sapi->sendResponse($response);
476 return;
477 }
478
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.";
484 }
485
486 // Unsupported method
487 throw new Exception\NotImplemented($exMessage);
488 }
489 }
490
491 if (!$this->emit('afterMethod:' . $method, [$request, $response])) return;
492 if (!$this->emit('afterMethod', [$request, $response])) return;
493
494 if ($response->getStatus() === null) {
495 throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.');
496 }
497 if ($sendResponse) {
498 $this->sapi->sendResponse($response);
499 $this->emit('afterResponse', [$request, $response]);
500 }
501
502 }
503
504 // {{{ HTTP/WebDAV protocol helpers
505
513
514 $methods = [
515 'OPTIONS',
516 'GET',
517 'HEAD',
518 'DELETE',
519 'PROPFIND',
520 'PUT',
521 'PROPPATCH',
522 'COPY',
523 'MOVE',
524 'REPORT'
525 ];
526
527 // The MKCOL is only allowed on an unmapped uri
528 try {
529 $this->tree->getNodeForPath($path);
530 } catch (Exception\NotFound $e) {
531 $methods[] = 'MKCOL';
532 }
533
534 // We're also checking if any of the plugins register any new methods
535 foreach ($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($path));
536 array_unique($methods);
537
538 return $methods;
539
540 }
541
547 function getRequestUri() {
548
549 return $this->calculateUri($this->httpRequest->getUrl());
550
551 }
552
565 function calculateUri($uri) {
566
567 if ($uri[0] != '/' && strpos($uri, '://')) {
568
569 $uri = parse_url($uri, PHP_URL_PATH);
570
571 }
572
573 $uri = Uri\normalize(str_replace('//', '/', $uri));
575
576 if (strpos($uri, $baseUri) === 0) {
577
578 return trim(URLUtil::decodePath(substr($uri, strlen($baseUri))), '/');
579
580 // A special case, if the baseUri was accessed without a trailing
581 // slash, we'll accept it as well.
582 } elseif ($uri . '/' === $baseUri) {
583
584 return '';
585
586 } else {
587
588 throw new Exception\Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
589
590 }
591
592 }
593
603 function getHTTPDepth($default = self::DEPTH_INFINITY) {
604
605 // If its not set, we'll grab the default
606 $depth = $this->httpRequest->getHeader('Depth');
607
608 if (is_null($depth)) return $default;
609
610 if ($depth == 'infinity') return self::DEPTH_INFINITY;
611
612
613 // If its an unknown value. we'll grab the default
614 if (!ctype_digit($depth)) return $default;
615
616 return (int)$depth;
617
618 }
619
634 function getHTTPRange() {
635
636 $range = $this->httpRequest->getHeader('range');
637 if (is_null($range)) return null;
638
639 // Matching "Range: bytes=1234-5678: both numbers are optional
640
641 if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) return null;
642
643 if ($matches[1] === '' && $matches[2] === '') return null;
644
645 return [
646 $matches[1] !== '' ? $matches[1] : null,
647 $matches[2] !== '' ? $matches[2] : null,
648 ];
649
650 }
651
678 function getHTTPPrefer() {
679
680 $result = [
681 // can be true or false
682 'respond-async' => false,
683 // Could be set to 'representation' or 'minimal'.
684 'return' => null,
685 // Used as a timeout, is usually a number.
686 'wait' => null,
687 // can be 'strict' or 'lenient'.
688 'handling' => false,
689 ];
690
691 if ($prefer = $this->httpRequest->getHeader('Prefer')) {
692
693 $result = array_merge(
694 $result,
695 HTTP\parsePrefer($prefer)
696 );
697
698 } elseif ($this->httpRequest->getHeader('Brief') == 't') {
699 $result['return'] = 'minimal';
700 }
701
702 return $result;
703
704 }
705
706
730
731 // Collecting the relevant HTTP headers
732 if (!$request->getHeader('Destination')) throw new Exception\BadRequest('The destination header was not supplied');
733 $destination = $this->calculateUri($request->getHeader('Destination'));
734 $overwrite = $request->getHeader('Overwrite');
735 if (!$overwrite) $overwrite = 'T';
736 if (strtoupper($overwrite) == 'T') $overwrite = true;
737 elseif (strtoupper($overwrite) == 'F') $overwrite = false;
738 // We need to throw a bad request exception, if the header was invalid
739 else throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F');
740
741 list($destinationDir) = URLUtil::splitPath($destination);
742
743 try {
744 $destinationParent = $this->tree->getNodeForPath($destinationDir);
745 if (!($destinationParent instanceof ICollection)) throw new Exception\UnsupportedMediaType('The destination node is not a collection');
746 } catch (Exception\NotFound $e) {
747
748 // If the destination parent node is not found, we throw a 409
749 throw new Exception\Conflict('The destination node is not found');
750 }
751
752 try {
753
754 $destinationNode = $this->tree->getNodeForPath($destination);
755
756 // If this succeeded, it means the destination already exists
757 // we'll need to throw precondition failed in case overwrite is false
758 if (!$overwrite) throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite');
759
760 } catch (Exception\NotFound $e) {
761
762 // Destination didn't exist, we're all good
763 $destinationNode = false;
764
765 }
766
767 $requestPath = $request->getPath();
768 if ($destination === $requestPath) {
769 throw new Exception\Forbidden('Source and destination uri are identical.');
770 }
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.');
773 }
774
775 // These are the three relevant properties we need to return
776 return [
777 'destination' => $destination,
778 'destinationExists' => !!$destinationNode,
779 'destinationNode' => $destinationNode,
780 ];
781
782 }
783
799 function getProperties($path, $propertyNames) {
800
801 $result = $this->getPropertiesForPath($path, $propertyNames, 0);
802 if (isset($result[0][200])) {
803 return $result[0][200];
804 } else {
805 return [];
806 }
807
808 }
809
822 function getPropertiesForChildren($path, $propertyNames) {
823
824 $result = [];
825 foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) {
826
827 // Skipping the parent path
828 if ($k === 0) continue;
829
830 $result[$row['href']] = $row[200];
831
832 }
833 return $result;
834
835 }
836
850
851 $propertyMap = [
852 '{DAV:}getcontenttype' => 'Content-Type',
853 '{DAV:}getcontentlength' => 'Content-Length',
854 '{DAV:}getlastmodified' => 'Last-Modified',
855 '{DAV:}getetag' => 'ETag',
856 ];
857
858 $properties = $this->getProperties($path, array_keys($propertyMap));
859
860 $headers = [];
861 foreach ($propertyMap as $property => $header) {
862 if (!isset($properties[$property])) continue;
863
864 if (is_scalar($properties[$property])) {
865 $headers[$header] = $properties[$property];
866
867 // GetLastModified gets special cased
868 } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) {
869 $headers[$header] = HTTP\Util::toHTTPDate($properties[$property]->getTime());
870 }
871
872 }
873
874 return $headers;
875
876 }
877
885 private function generatePathNodes(PropFind $propFind, array $yieldFirst = null) {
886 if ($yieldFirst !== null) {
887 yield $yieldFirst;
888 }
889 $newDepth = $propFind->getDepth();
890 $path = $propFind->getPath();
891
892 if ($newDepth !== self::DEPTH_INFINITY) {
893 $newDepth--;
894 }
895
896 foreach ($this->tree->getChildren($path) as $childNode) {
897 $subPropFind = clone $propFind;
898 $subPropFind->setDepth($newDepth);
899 if ($path !== '') {
900 $subPath = $path . '/' . $childNode->getName();
901 } else {
902 $subPath = $childNode->getName();
903 }
904 $subPropFind->setPath($subPath);
905
906 yield [
907 $subPropFind,
908 $childNode
909 ];
910
911 if (($newDepth === self::DEPTH_INFINITY || $newDepth >= 1) && $childNode instanceof ICollection) {
912 foreach ($this->generatePathNodes($subPropFind) as $subItem) {
913 yield $subItem;
914 }
915 }
916
917 }
918 }
919
937 function getPropertiesForPath($path, $propertyNames = [], $depth = 0) {
938
939 return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth));
940
941 }
956 function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0) {
957
958 // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
959 if (!$this->enablePropfindDepthInfinity && $depth != 0) $depth = 1;
960
961 $path = trim($path, '/');
962
963 $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS;
964 $propFind = new PropFind($path, (array)$propertyNames, $depth, $propFindType);
965
966 $parentNode = $this->tree->getNodeForPath($path);
967
968 $propFindRequests = [[
969 $propFind,
970 $parentNode
971 ]];
972
973 if (($depth > 0 || $depth === self::DEPTH_INFINITY) && $parentNode instanceof ICollection) {
974 $propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests));
975 }
976
977 foreach ($propFindRequests as $propFindRequest) {
978
979 list($propFind, $node) = $propFindRequest;
980 $r = $this->getPropertiesByNode($propFind, $node);
981 if ($r) {
982 $result = $propFind->getResultForMultiStatus();
983 $result['href'] = $propFind->getPath();
984
985 // WebDAV recommends adding a slash to the path, if the path is
986 // a collection.
987 // Furthermore, iCal also demands this to be the case for
988 // principals. This is non-standard, but we support it.
989 $resourceType = $this->getResourceTypeForNode($node);
990 if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
991 $result['href'] .= '/';
992 }
993 yield $result;
994 }
995
996 }
997
998 }
999
1014 function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) {
1015
1016 $result = [
1017 ];
1018
1019 $nodes = $this->tree->getMultipleNodes($paths);
1020
1021 foreach ($nodes as $path => $node) {
1022
1023 $propFind = new PropFind($path, $propertyNames);
1024 $r = $this->getPropertiesByNode($propFind, $node);
1025 if ($r) {
1026 $result[$path] = $propFind->getResultForMultiStatus();
1027 $result[$path]['href'] = $path;
1028
1029 $resourceType = $this->getResourceTypeForNode($node);
1030 if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) {
1031 $result[$path]['href'] .= '/';
1032 }
1033 }
1034
1035 }
1036
1037 return $result;
1038
1039 }
1040
1041
1056 function getPropertiesByNode(PropFind $propFind, INode $node) {
1057
1058 return $this->emit('propFind', [$propFind, $node]);
1059
1060 }
1061
1076 function createFile($uri, $data, &$etag = null) {
1077
1078 list($dir, $name) = URLUtil::splitPath($uri);
1079
1080 if (!$this->emit('beforeBind', [$uri])) return false;
1081
1082 $parent = $this->tree->getNodeForPath($dir);
1083 if (!$parent instanceof ICollection) {
1084 throw new Exception\Conflict('Files can only be created as children of collections');
1085 }
1086
1087 // It is possible for an event handler to modify the content of the
1088 // body, before it gets written. If this is the case, $modified
1089 // should be set to true.
1090 //
1091 // If $modified is true, we must not send back an ETag.
1092 $modified = false;
1093 if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) return false;
1094
1095 $etag = $parent->createFile($name, $data);
1096
1097 if ($modified) $etag = null;
1098
1099 $this->tree->markDirty($dir . '/' . $name);
1100
1101 $this->emit('afterBind', [$uri]);
1102 $this->emit('afterCreateFile', [$uri, $parent]);
1103
1104 return true;
1105 }
1106
1117 function updateFile($uri, $data, &$etag = null) {
1118
1119 $node = $this->tree->getNodeForPath($uri);
1120
1121 // It is possible for an event handler to modify the content of the
1122 // body, before it gets written. If this is the case, $modified
1123 // should be set to true.
1124 //
1125 // If $modified is true, we must not send back an ETag.
1126 $modified = false;
1127 if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) return false;
1128
1129 $etag = $node->put($data);
1130 if ($modified) $etag = null;
1131 $this->emit('afterWriteContent', [$uri, $node]);
1132
1133 return true;
1134 }
1135
1136
1137
1144 function createDirectory($uri) {
1145
1146 $this->createCollection($uri, new MkCol(['{DAV:}collection'], []));
1147
1148 }
1149
1157 function createCollection($uri, MkCol $mkCol) {
1158
1159 list($parentUri, $newName) = URLUtil::splitPath($uri);
1160
1161 // Making sure the parent exists
1162 try {
1163 $parent = $this->tree->getNodeForPath($parentUri);
1164
1165 } catch (Exception\NotFound $e) {
1166 throw new Exception\Conflict('Parent node does not exist');
1167
1168 }
1169
1170 // Making sure the parent is a collection
1171 if (!$parent instanceof ICollection) {
1172 throw new Exception\Conflict('Parent node is not a collection');
1173 }
1174
1175 // Making sure the child does not already exist
1176 try {
1177 $parent->getChild($newName);
1178
1179 // If we got here.. it means there's already a node on that url, and we need to throw a 405
1180 throw new Exception\MethodNotAllowed('The resource you tried to create already exists');
1181
1182 } catch (Exception\NotFound $e) {
1183 // NotFound is the expected behavior.
1184 }
1185
1186
1187 if (!$this->emit('beforeBind', [$uri])) return;
1188
1189 if ($parent instanceof IExtendedCollection) {
1190
1196 $parent->createExtendedCollection($newName, $mkCol);
1197
1198 } else {
1199
1205 if (count($mkCol->getResourceType()) > 1) {
1206 throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
1207 }
1208
1209 $parent->createDirectory($newName);
1210
1211 }
1212
1213 // If there are any properties that have not been handled/stored,
1214 // we ask the 'propPatch' event to handle them. This will allow for
1215 // example the propertyStorage system to store properties upon MKCOL.
1216 if ($mkCol->getRemainingMutations()) {
1217 $this->emit('propPatch', [$uri, $mkCol]);
1218 }
1219 $success = $mkCol->commit();
1220
1221 if (!$success) {
1222 $result = $mkCol->getResult();
1223
1224 $formattedResult = [
1225 'href' => $uri,
1226 ];
1227
1228 foreach ($result as $propertyName => $status) {
1229
1230 if (!isset($formattedResult[$status])) {
1231 $formattedResult[$status] = [];
1232 }
1233 $formattedResult[$status][$propertyName] = null;
1234
1235 }
1236 return $formattedResult;
1237 }
1238
1239 $this->tree->markDirty($parentUri);
1240 $this->emit('afterBind', [$uri]);
1241
1242 }
1243
1261 function updateProperties($path, array $properties) {
1262
1263 $propPatch = new PropPatch($properties);
1264 $this->emit('propPatch', [$path, $propPatch]);
1265 $propPatch->commit();
1266
1267 return $propPatch->getResult();
1268
1269 }
1270
1294
1295 $path = $request->getPath();
1296 $node = null;
1297 $lastMod = null;
1298 $etag = null;
1299
1300 if ($ifMatch = $request->getHeader('If-Match')) {
1301
1302 // If-Match contains an entity tag. Only if the entity-tag
1303 // matches we are allowed to make the request succeed.
1304 // If the entity-tag is '*' we are only allowed to make the
1305 // request succeed if a resource exists at that url.
1306 try {
1307 $node = $this->tree->getNodeForPath($path);
1308 } catch (Exception\NotFound $e) {
1309 throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match');
1310 }
1311
1312 // Only need to check entity tags if they are not *
1313 if ($ifMatch !== '*') {
1314
1315 // There can be multiple ETags
1316 $ifMatch = explode(',', $ifMatch);
1317 $haveMatch = false;
1318 foreach ($ifMatch as $ifMatchItem) {
1319
1320 // Stripping any extra spaces
1321 $ifMatchItem = trim($ifMatchItem, ' ');
1322
1323 $etag = $node instanceof IFile ? $node->getETag() : null;
1324 if ($etag === $ifMatchItem) {
1325 $haveMatch = true;
1326 } else {
1327 // Evolution has a bug where it sometimes prepends the "
1328 // with a \. This is our workaround.
1329 if (str_replace('\\"', '"', $ifMatchItem) === $etag) {
1330 $haveMatch = true;
1331 }
1332 }
1333
1334 }
1335 if (!$haveMatch) {
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');
1338 }
1339 }
1340 }
1341
1342 if ($ifNoneMatch = $request->getHeader('If-None-Match')) {
1343
1344 // The If-None-Match header contains an ETag.
1345 // Only if the ETag does not match the current ETag, the request will succeed
1346 // The header can also contain *, in which case the request
1347 // will only succeed if the entity does not exist at all.
1348 $nodeExists = true;
1349 if (!$node) {
1350 try {
1351 $node = $this->tree->getNodeForPath($path);
1352 } catch (Exception\NotFound $e) {
1353 $nodeExists = false;
1354 }
1355 }
1356 if ($nodeExists) {
1357 $haveMatch = false;
1358 if ($ifNoneMatch === '*') $haveMatch = true;
1359 else {
1360
1361 // There might be multiple ETags
1362 $ifNoneMatch = explode(',', $ifNoneMatch);
1363 $etag = $node instanceof IFile ? $node->getETag() : null;
1364
1365 foreach ($ifNoneMatch as $ifNoneMatchItem) {
1366
1367 // Stripping any extra spaces
1368 $ifNoneMatchItem = trim($ifNoneMatchItem, ' ');
1369
1370 if ($etag === $ifNoneMatchItem) $haveMatch = true;
1371
1372 }
1373
1374 }
1375
1376 if ($haveMatch) {
1377 if ($etag) $response->setHeader('ETag', $etag);
1378 if ($request->getMethod() === 'GET') {
1379 $response->setStatus(304);
1380 return false;
1381 } else {
1382 throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match');
1383 }
1384 }
1385 }
1386
1387 }
1388
1389 if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) {
1390
1391 // The If-Modified-Since header contains a date. We
1392 // will only return the entity if it has been changed since
1393 // that date. If it hasn't been changed, we return a 304
1394 // header
1395 // Note that this header only has to be checked if there was no If-None-Match header
1396 // as per the HTTP spec.
1397 $date = HTTP\Util::parseHTTPDate($ifModifiedSince);
1398
1399 if ($date) {
1400 if (is_null($node)) {
1401 $node = $this->tree->getNodeForPath($path);
1402 }
1403 $lastMod = $node->getLastModified();
1404 if ($lastMod) {
1405 $lastMod = new \DateTime('@' . $lastMod);
1406 if ($lastMod <= $date) {
1407 $response->setStatus(304);
1408 $response->setHeader('Last-Modified', HTTP\Util::toHTTPDate($lastMod));
1409 return false;
1410 }
1411 }
1412 }
1413 }
1414
1415 if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) {
1416
1417 // The If-Unmodified-Since will allow allow the request if the
1418 // entity has not changed since the specified date.
1419 $date = HTTP\Util::parseHTTPDate($ifUnmodifiedSince);
1420
1421 // We must only check the date if it's valid
1422 if ($date) {
1423 if (is_null($node)) {
1424 $node = $this->tree->getNodeForPath($path);
1425 }
1426 $lastMod = $node->getLastModified();
1427 if ($lastMod) {
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');
1431 }
1432 }
1433 }
1434
1435 }
1436
1437 // Now the hardest, the If: header. The If: header can contain multiple
1438 // urls, ETags and so-called 'state tokens'.
1439 //
1440 // Examples of state tokens include lock-tokens (as defined in rfc4918)
1441 // and sync-tokens (as defined in rfc6578).
1442 //
1443 // The only proper way to deal with these, is to emit events, that a
1444 // Sync and Lock plugin can pick up.
1445 $ifConditions = $this->getIfConditions($request);
1446
1447 foreach ($ifConditions as $kk => $ifCondition) {
1448 foreach ($ifCondition['tokens'] as $ii => $token) {
1449 $ifConditions[$kk]['tokens'][$ii]['validToken'] = false;
1450 }
1451 }
1452
1453 // Plugins are responsible for validating all the tokens.
1454 // If a plugin deemed a token 'valid', it will set 'validToken' to
1455 // true.
1456 $this->emit('validateTokens', [$request, &$ifConditions]);
1457
1458 // Now we're going to analyze the result.
1459
1460 // Every ifCondition needs to validate to true, so we exit as soon as
1461 // we have an invalid condition.
1462 foreach ($ifConditions as $ifCondition) {
1463
1464 $uri = $ifCondition['uri'];
1465 $tokens = $ifCondition['tokens'];
1466
1467 // We only need 1 valid token for the condition to succeed.
1468 foreach ($tokens as $token) {
1469
1470 $tokenValid = $token['validToken'] || !$token['token'];
1471
1472 $etagValid = false;
1473 if (!$token['etag']) {
1474 $etagValid = true;
1475 }
1476 // Checking the ETag, only if the token was already deemed
1477 // valid and there is one.
1478 if ($token['etag'] && $tokenValid) {
1479
1480 // The token was valid, and there was an ETag. We must
1481 // grab the current ETag and check it.
1482 $node = $this->tree->getNodeForPath($uri);
1483 $etagValid = $node instanceof IFile && $node->getETag() == $token['etag'];
1484
1485 }
1486
1487
1488 if (($tokenValid && $etagValid) ^ $token['negate']) {
1489 // Both were valid, so we can go to the next condition.
1490 continue 2;
1491 }
1492
1493
1494 }
1495
1496 // If we ended here, it means there was no valid ETag + token
1497 // combination found for the current condition. This means we fail!
1498 throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for ' . $uri, 'If');
1499
1500 }
1501
1502 return true;
1503
1504 }
1505
1578
1579 $header = $request->getHeader('If');
1580 if (!$header) return [];
1581
1582 $matches = [];
1583
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);
1586
1587 $conditions = [];
1588
1589 foreach ($matches as $match) {
1590
1591 // If there was no uri specified in this match, and there were
1592 // already conditions parsed, we add the condition to the list of
1593 // conditions for the previous uri.
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'] : ''
1599 ];
1600 } else {
1601
1602 if (!$match['uri']) {
1603 $realUri = $request->getPath();
1604 } else {
1605 $realUri = $this->calculateUri($match['uri']);
1606 }
1607
1608 $conditions[] = [
1609 'uri' => $realUri,
1610 'tokens' => [
1611 [
1612 'negate' => $match['not'] ? true : false,
1613 'token' => $match['token'],
1614 'etag' => isset($match['etag']) ? $match['etag'] : ''
1615 ]
1616 ],
1617
1618 ];
1619 }
1620
1621 }
1622
1623 return $conditions;
1624
1625 }
1626
1634
1635 $result = [];
1636 foreach ($this->resourceTypeMapping as $className => $resourceType) {
1637 if ($node instanceof $className) $result[] = $resourceType;
1638 }
1639 return $result;
1640
1641 }
1642
1643 // }}}
1644 // {{{ XML Readers & Writers
1645
1646
1656 function generateMultiStatus($fileProperties, $strip404s = false) {
1657
1658 $w = $this->xml->getWriter();
1659 $w->openMemory();
1660 $w->contextUri = $this->baseUri;
1661 $w->startDocument();
1662
1663 $w->startElement('{DAV:}multistatus');
1664
1665 foreach ($fileProperties as $entry) {
1666
1667 $href = $entry['href'];
1668 unset($entry['href']);
1669 if ($strip404s) {
1670 unset($entry[404]);
1671 }
1673 ltrim($href, '/'),
1674 $entry
1675 );
1676 $w->write([
1677 'name' => '{DAV:}response',
1678 'value' => $response
1679 ]);
1680 }
1681 $w->endElement();
1682
1683 return $w->outputMemory();
1684
1685 }
1686
1687}
$result
getTime()
Definition: MetaLoader.php:492
$success
Definition: Utf8Test.php:86
while(false !==( $line=fgets( $in))) if(! $columns) $ignore
Definition: Utf8Test.php:63
$path
Definition: aliased.php:25
foreach($paths as $path) $request
Definition: asyncclient.php:32
$default
Definition: build.php:20
An exception for terminatinating execution or to throw for unit testing.
This Logger can be used to avoid conditional log calls.
Definition: NullLogger.php:14
The core plugin provides all the basic features for a WebDAV server.
Definition: CorePlugin.php:17
Main Exception class.
Definition: Exception.php:18
serialize(Server $server, \DOMElement $errorNode)
This method allows the exception to include additional information into the WebDAV error response.
Definition: Exception.php:38
getHTTPCode()
Returns the HTTP statuscode for this exception.
Definition: Exception.php:25
getHTTPHeaders(Server $server)
This method allows the exception to return any extra HTTP response headers.
Definition: Exception.php:51
This class represents a MKCOL operation.
Definition: MkCol.php:23
getResourceType()
Returns the resourcetype of the new collection.
Definition: MkCol.php:50
This class holds all the information about a PROPFIND request.
Definition: PropFind.php:11
getDepth()
Returns the depth of this propfind request.
Definition: PropFind.php:198
const NORMAL
A normal propfind.
Definition: PropFind.php:16
const ALLPROPS
An allprops request.
Definition: PropFind.php:27
getPath()
Returns the path this PROPFIND request is for.
Definition: PropFind.php:187
setDepth($depth)
Updates the depth of this propfind request.
Definition: PropFind.php:210
This class represents a set of properties that are going to be updated.
Definition: PropPatch.php:20
commit()
Performs the actual update, and calls all callbacks.
Definition: PropPatch.php:225
getRemainingMutations()
Returns the list of properties that don't have a result code yet.
Definition: PropPatch.php:184
getResult()
Returns the result of the operation.
Definition: PropPatch.php:356
The baseclass for all server plugins.
getPluginName()
Returns a plugin name.
initialize(Server $server)
This initializes the plugin.
Main DAV server class.
Definition: Server.php:23
getPlugin($name)
Returns an initialized plugin by it's name.
Definition: Server.php:419
const DEPTH_INFINITY
Infinity is used for some request supporting the HTTP Depth header and indicates that the operation s...
Definition: Server.php:30
getPlugins()
Returns all plugins.
Definition: Server.php:433
getPropertiesByNode(PropFind $propFind, INode $node)
Determines all properties for a node.
Definition: Server.php:1056
getIfConditions(RequestInterface $request)
This method is created to extract information from the WebDAV HTTP 'If:' header.
Definition: Server.php:1577
getResourceTypeForNode(INode $node)
Returns an array with resourcetypes for a node.
Definition: Server.php:1633
updateProperties($path, array $properties)
This method updates a resource's properties.
Definition: Server.php:1261
createCollection($uri, MkCol $mkCol)
Use this method to create a new collection.
Definition: Server.php:1157
getHTTPDepth($default=self::DEPTH_INFINITY)
Returns the HTTP depth header.
Definition: Server.php:603
getBaseUri()
Returns the base responding uri.
Definition: Server.php:347
updateFile($uri, $data, &$etag=null)
This method is invoked by sub-systems updating a file.
Definition: Server.php:1117
generateMultiStatus($fileProperties, $strip404s=false)
Generates a WebDAV propfind response body based on a list of nodes.
Definition: Server.php:1656
exec()
Starts the DAV Server.
Definition: Server.php:240
getPropertiesForChildren($path, $propertyNames)
A kid-friendly way to fetch properties for a node's children.
Definition: Server.php:822
getRequestUri()
Gets the uri for the request, keeping the base uri into consideration.
Definition: Server.php:547
getHTTPPrefer()
Returns the HTTP Prefer header information.
Definition: Server.php:678
invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse=true)
Handles a http request, and execute a method based on its name.
Definition: Server.php:461
guessBaseUri()
This method attempts to detect the base uri.
Definition: Server.php:362
generatePathNodes(PropFind $propFind, array $yieldFirst=null)
Small helper to support PROPFIND with DEPTH_INFINITY.
Definition: Server.php:885
calculateUri($uri)
Turns a URI such as the REQUEST_URI into a local path.
Definition: Server.php:565
getPropertiesForMultiplePaths(array $paths, array $propertyNames=[])
Returns a list of properties for a list of paths.
Definition: Server.php:1014
createFile($uri, $data, &$etag=null)
This method is invoked by sub-systems creating a new file.
Definition: Server.php:1076
const NS_SABREDAV
XML namespace for all SabreDAV related elements.
Definition: Server.php:35
setBaseUri($uri)
Sets the base server uri.
Definition: Server.php:332
static $exposeVersion
Definition: Server.php:184
getHTTPHeaders($path)
Returns a list of HTTP headers for a particular resource.
Definition: Server.php:849
getAllowedMethods($path)
Returns an array with all the supported HTTP methods for a specific uri.
Definition: Server.php:512
__construct($treeOrNode=null)
Sets up the server.
Definition: Server.php:201
getPropertiesForPath($path, $propertyNames=[], $depth=0)
Returns a list of properties for a given path.
Definition: Server.php:937
getPropertiesIteratorForPath($path, $propertyNames=[], $depth=0)
Returns a list of properties for a given path.
Definition: Server.php:956
addPlugin(ServerPlugin $plugin)
Adds a plugin to the server.
Definition: Server.php:404
getHTTPRange()
Returns the HTTP range header.
Definition: Server.php:634
$enablePropfindDepthInfinity
Definition: Server.php:167
getLogger()
Returns the PSR-3 logger object.
Definition: Server.php:444
checkPreconditions(RequestInterface $request, ResponseInterface $response)
This method checks the main HTTP preconditions.
Definition: Server.php:1293
getCopyAndMoveInfo(RequestInterface $request)
Returns information about Copy and Move requests.
Definition: Server.php:729
createDirectory($uri)
This method is invoked by sub-systems creating a new directory.
Definition: Server.php:1144
getProperties($path, $propertyNames)
Returns a list of properties for a path.
Definition: Server.php:799
The tree object is responsible for basic tree operations.
Definition: Tree.php:17
const VERSION
Full version number.
Definition: Version.php:17
WebDAV {DAV:}response parser.
Definition: Response.php:20
XML service for WebDAV.
Definition: Service.php:12
EventEmitter object.
URL utility class.
Definition: URLUtil.php:18
static decodePath($path)
Decodes a url-encoded path.
Definition: URLUtil.php:57
static splitPath($path)
Returns the 'dirname' and 'basename' for a path.
Definition: URLUtil.php:83
static parseHTTPDate($dateHeader)
Parses a RFC2616-compatible date string.
Definition: Util.php:53
static toHTTPDate(\DateTime $dateTime)
Transforms a DateTime object to HTTP's most common date format.
Definition: Util.php:69
$h
$w
$r
Definition: example_031.php:79
Describes a logger-aware instance.
Describes a logger instance.
The ICollection Interface.
Definition: ICollection.php:14
The IExtendedCollection interface.
This interface represents a file in the directory tree.
Definition: IFile.php:16
getETag()
Returns the ETag for a file.
The INode interface is the base interface, and the parent class of both ICollection and IFile.
Definition: INode.php:12
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.
$destination
$row
if($argc< 2) $paths
Definition: migrateto20.php:44
trait LoggerAwareTrait
Basic Implementation of LoggerAwareInterface.
parsePrefer($input)
Parses the Prefer header, as defined in RFC7240.
Definition: functions.php:222
normalize($uri)
Takes a URI or partial URI as its argument, and normalizes it.
Definition: functions.php:114
$response
$root
Definition: sabredav.php:45
$data
Definition: bench.php:6