ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
Server.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Sabre\DAV;
4 
10 use Sabre\HTTP;
14 use Sabre\Uri;
15 
23 class Server extends EventEmitter implements LoggerAwareInterface {
24 
25  use LoggerAwareTrait;
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 
56  public $httpResponse;
57 
63  public $httpRequest;
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));
574  $baseUri = Uri\normalize($this->getBaseUri());
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 
849  function getHTTPHeaders($path) {
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 
1633  function getResourceTypeForNode(INode $node) {
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 }
This interface represents a HTTP response.
getPropertiesForChildren($path, $propertyNames)
A kid-friendly way to fetch properties for a node&#39;s children.
Definition: Server.php:822
The RequestInterface represents a HTTP request.
XML service for WebDAV.
Definition: Service.php:12
$path
Definition: aliased.php:25
getResourceTypeForNode(INode $node)
Returns an array with resourcetypes for a node.
Definition: Server.php:1633
const NS_SABREDAV
XML namespace for all SabreDAV related elements.
Definition: Server.php:35
The baseclass for all server plugins.
generatePathNodes(PropFind $propFind, array $yieldFirst=null)
Small helper to support PROPFIND with DEPTH_INFINITY.
Definition: Server.php:885
This class represents a set of properties that are going to be updated.
Definition: PropPatch.php:20
$result
updateFile($uri, $data, &$etag=null)
This method is invoked by sub-systems updating a file.
Definition: Server.php:1117
getAllowedMethods($path)
Returns an array with all the supported HTTP methods for a specific uri.
Definition: Server.php:512
if($argc< 2) $paths
Definition: migrateto20.php:44
foreach($paths as $path) $request
Definition: asyncclient.php:32
getProperties($path, $propertyNames)
Returns a list of properties for a path.
Definition: Server.php:799
getPath()
Returns the path this PROPFIND request is for.
Definition: PropFind.php:187
static toHTTPDate(\DateTime $dateTime)
Transforms a DateTime object to HTTP&#39;s most common date format.
Definition: Util.php:69
$h
getHTTPHeaders($path)
Returns a list of HTTP headers for a particular resource.
Definition: Server.php:849
getDepth()
Returns the depth of this propfind request.
Definition: PropFind.php:198
getETag()
Returns the ETag for a file.
getPlugins()
Returns all plugins.
Definition: Server.php:433
$destination
getPropertiesForPath($path, $propertyNames=[], $depth=0)
Returns a list of properties for a given path.
Definition: Server.php:937
$enablePropfindDepthInfinity
Definition: Server.php:167
This class holds all the information about a PROPFIND request.
Definition: PropFind.php:11
parsePrefer($input)
Parses the Prefer header, as defined in RFC7240.
Definition: functions.php:222
getBaseUri()
Returns the base responding uri.
Definition: Server.php:347
const VERSION
Full version number.
Definition: Version.php:17
$w
The IExtendedCollection interface.
The ICollection Interface.
Definition: ICollection.php:14
setDepth($depth)
Updates the depth of this propfind request.
Definition: PropFind.php:210
serialize(Server $server, \DOMElement $errorNode)
This method allows the exception to include additional information into the WebDAV error response...
Definition: Exception.php:38
checkPreconditions(RequestInterface $request, ResponseInterface $response)
This method checks the main HTTP preconditions.
Definition: Server.php:1293
const DEPTH_INFINITY
Infinity is used for some request supporting the HTTP Depth header and indicates that the operation s...
Definition: Server.php:30
static parseHTTPDate($dateHeader)
Parses a RFC2616-compatible date string.
Definition: Util.php:53
addPlugin(ServerPlugin $plugin)
Adds a plugin to the server.
Definition: Server.php:404
$r
Definition: example_031.php:79
trait LoggerAwareTrait
Basic Implementation of LoggerAwareInterface.
getPropertiesIteratorForPath($path, $propertyNames=[], $depth=0)
Returns a list of properties for a given path.
Definition: Server.php:956
$success
Definition: Utf8Test.php:86
emit($eventName, array $arguments=[], callable $continueCallBack=null)
Emits an event.
getHTTPPrefer()
Returns the HTTP Prefer header information.
Definition: Server.php:678
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.
Definition: Server.php:461
createFile($uri, $data, &$etag=null)
This method is invoked by sub-systems creating a new file.
Definition: Server.php:1076
getHTTPDepth($default=self::DEPTH_INFINITY)
Returns the HTTP depth header.
Definition: Server.php:603
getRemainingMutations()
Returns the list of properties that don&#39;t have a result code yet.
Definition: PropPatch.php:184
updateProperties($path, array $properties)
This method updates a resource&#39;s properties.
Definition: Server.php:1261
Describes a logger-aware instance.
getPlugin($name)
Returns an initialized plugin by it&#39;s name.
Definition: Server.php:419
Main DAV server class.
Definition: Server.php:23
getHeader($name)
Returns a specific HTTP header, based on it&#39;s name.
getHTTPRange()
Returns the HTTP range header.
Definition: Server.php:634
getResult()
Returns the result of the operation.
Definition: PropPatch.php:356
guessBaseUri()
This method attempts to detect the base uri.
Definition: Server.php:362
The core plugin provides all the basic features for a WebDAV server.
Definition: CorePlugin.php:17
This interface represents a file in the directory tree.
Definition: IFile.php:16
$root
Definition: sabredav.php:45
The INode interface is the base interface, and the parent class of both ICollection and IFile...
Definition: INode.php:12
getHTTPCode()
Returns the HTTP statuscode for this exception.
Definition: Exception.php:25
getPropertiesForMultiplePaths(array $paths, array $propertyNames=[])
Returns a list of properties for a list of paths.
Definition: Server.php:1014
static $exposeVersion
Definition: Server.php:184
$default
Definition: build.php:20
const ALLPROPS
An allprops request.
Definition: PropFind.php:27
$row
WebDAV {DAV:}response parser.
Definition: Response.php:20
exec()
Starts the DAV Server.
Definition: Server.php:240
calculateUri($uri)
Turns a URI such as the REQUEST_URI into a local path.
Definition: Server.php:565
getPath()
Returns the relative path.
generateMultiStatus($fileProperties, $strip404s=false)
Generates a WebDAV propfind response body based on a list of nodes.
Definition: Server.php:1656
This class represents a MKCOL operation.
Definition: MkCol.php:23
getMethod()
Returns the current HTTP method.
setBaseUri($uri)
Sets the base server uri.
Definition: Server.php:332
static decodePath($path)
Decodes a url-encoded path.
Definition: URLUtil.php:57
getCopyAndMoveInfo(RequestInterface $request)
Returns information about Copy and Move requests.
Definition: Server.php:729
EventEmitter object.
const NORMAL
A normal propfind.
Definition: PropFind.php:16
getRequestUri()
Gets the uri for the request, keeping the base uri into consideration.
Definition: Server.php:547
createDirectory($uri)
This method is invoked by sub-systems creating a new directory.
Definition: Server.php:1144
getHTTPHeaders(Server $server)
This method allows the exception to return any extra HTTP response headers.
Definition: Exception.php:51
createCollection($uri, MkCol $mkCol)
Use this method to create a new collection.
Definition: Server.php:1157
while(false !==($line=fgets($in))) if(! $columns) $ignore
Definition: Utf8Test.php:63
commit()
Performs the actual update, and calls all callbacks.
Definition: PropPatch.php:225
getPluginName()
Returns a plugin name.
getLogger()
Returns the PSR-3 logger object.
Definition: Server.php:444
$response
normalize($uri)
Takes a URI or partial URI as its argument, and normalizes it.
Definition: functions.php:114
__construct($treeOrNode=null)
Sets up the server.
Definition: Server.php:201
static splitPath($path)
Returns the &#39;dirname&#39; and &#39;basename&#39; for a path.
Definition: URLUtil.php:83
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.
Definition: Server.php:1056
This Logger can be used to avoid conditional log calls.
Definition: NullLogger.php:13
getStatus()
Returns the current HTTP status code.
The tree object is responsible for basic tree operations.
Definition: Tree.php:17
$data
Definition: bench.php:6
getIfConditions(RequestInterface $request)
This method is created to extract information from the WebDAV HTTP &#39;If:&#39; header.
Definition: Server.php:1577
getResourceType()
Returns the resourcetype of the new collection.
Definition: MkCol.php:50