Server.php 67 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000
  1. <?php
  2. /**
  3. * Main DAV server class
  4. *
  5. * @package Sabre
  6. * @subpackage DAV
  7. * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
  8. * @author Evert Pot (http://www.rooftopsolutions.nl/)
  9. * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
  10. */
  11. class Sabre_DAV_Server {
  12. /**
  13. * Inifinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree
  14. */
  15. const DEPTH_INFINITY = -1;
  16. /**
  17. * Nodes that are files, should have this as the type property
  18. */
  19. const NODE_FILE = 1;
  20. /**
  21. * Nodes that are directories, should use this value as the type property
  22. */
  23. const NODE_DIRECTORY = 2;
  24. /**
  25. * XML namespace for all SabreDAV related elements
  26. */
  27. const NS_SABREDAV = 'http://sabredav.org/ns';
  28. /**
  29. * The tree object
  30. *
  31. * @var Sabre_DAV_Tree
  32. */
  33. public $tree;
  34. /**
  35. * The base uri
  36. *
  37. * @var string
  38. */
  39. protected $baseUri = null;
  40. /**
  41. * httpResponse
  42. *
  43. * @var Sabre_HTTP_Response
  44. */
  45. public $httpResponse;
  46. /**
  47. * httpRequest
  48. *
  49. * @var Sabre_HTTP_Request
  50. */
  51. public $httpRequest;
  52. /**
  53. * The list of plugins
  54. *
  55. * @var array
  56. */
  57. protected $plugins = array();
  58. /**
  59. * This array contains a list of callbacks we should call when certain events are triggered
  60. *
  61. * @var array
  62. */
  63. protected $eventSubscriptions = array();
  64. /**
  65. * This is a default list of namespaces.
  66. *
  67. * If you are defining your own custom namespace, add it here to reduce
  68. * bandwidth and improve legibility of xml bodies.
  69. *
  70. * @var array
  71. */
  72. public $xmlNamespaces = array(
  73. 'DAV:' => 'd',
  74. 'http://sabredav.org/ns' => 's',
  75. );
  76. /**
  77. * The propertymap can be used to map properties from
  78. * requests to property classes.
  79. *
  80. * @var array
  81. */
  82. public $propertyMap = array(
  83. '{DAV:}resourcetype' => 'Sabre_DAV_Property_ResourceType',
  84. );
  85. public $protectedProperties = array(
  86. // RFC4918
  87. '{DAV:}getcontentlength',
  88. '{DAV:}getetag',
  89. '{DAV:}getlastmodified',
  90. '{DAV:}lockdiscovery',
  91. '{DAV:}resourcetype',
  92. '{DAV:}supportedlock',
  93. // RFC4331
  94. '{DAV:}quota-available-bytes',
  95. '{DAV:}quota-used-bytes',
  96. // RFC3744
  97. '{DAV:}supported-privilege-set',
  98. '{DAV:}current-user-privilege-set',
  99. '{DAV:}acl',
  100. '{DAV:}acl-restrictions',
  101. '{DAV:}inherited-acl-set',
  102. );
  103. /**
  104. * This is a flag that allow or not showing file, line and code
  105. * of the exception in the returned XML
  106. *
  107. * @var bool
  108. */
  109. public $debugExceptions = false;
  110. /**
  111. * This property allows you to automatically add the 'resourcetype' value
  112. * based on a node's classname or interface.
  113. *
  114. * The preset ensures that {DAV:}collection is automaticlly added for nodes
  115. * implementing Sabre_DAV_ICollection.
  116. *
  117. * @var array
  118. */
  119. public $resourceTypeMapping = array(
  120. 'Sabre_DAV_ICollection' => '{DAV:}collection',
  121. );
  122. /**
  123. * If this setting is turned off, SabreDAV's version number will be hidden
  124. * from various places.
  125. *
  126. * Some people feel this is a good security measure.
  127. *
  128. * @var bool
  129. */
  130. static public $exposeVersion = true;
  131. /**
  132. * Sets up the server
  133. *
  134. * If a Sabre_DAV_Tree object is passed as an argument, it will
  135. * use it as the directory tree. If a Sabre_DAV_INode is passed, it
  136. * will create a Sabre_DAV_ObjectTree and use the node as the root.
  137. *
  138. * If nothing is passed, a Sabre_DAV_SimpleCollection is created in
  139. * a Sabre_DAV_ObjectTree.
  140. *
  141. * If an array is passed, we automatically create a root node, and use
  142. * the nodes in the array as top-level children.
  143. *
  144. * @param Sabre_DAV_Tree|Sabre_DAV_INode|null $treeOrNode The tree object
  145. */
  146. public function __construct($treeOrNode = null) {
  147. if ($treeOrNode instanceof Sabre_DAV_Tree) {
  148. $this->tree = $treeOrNode;
  149. } elseif ($treeOrNode instanceof Sabre_DAV_INode) {
  150. $this->tree = new Sabre_DAV_ObjectTree($treeOrNode);
  151. } elseif (is_array($treeOrNode)) {
  152. // If it's an array, a list of nodes was passed, and we need to
  153. // create the root node.
  154. foreach($treeOrNode as $node) {
  155. if (!($node instanceof Sabre_DAV_INode)) {
  156. throw new Sabre_DAV_Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre_DAV_INode');
  157. }
  158. }
  159. $root = new Sabre_DAV_SimpleCollection('root', $treeOrNode);
  160. $this->tree = new Sabre_DAV_ObjectTree($root);
  161. } elseif (is_null($treeOrNode)) {
  162. $root = new Sabre_DAV_SimpleCollection('root');
  163. $this->tree = new Sabre_DAV_ObjectTree($root);
  164. } else {
  165. throw new Sabre_DAV_Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre_DAV_Tree, Sabre_DAV_INode, an array or null');
  166. }
  167. $this->httpResponse = new Sabre_HTTP_Response();
  168. $this->httpRequest = new Sabre_HTTP_Request();
  169. }
  170. /**
  171. * Starts the DAV Server
  172. *
  173. * @return void
  174. */
  175. public function exec() {
  176. try {
  177. $this->invokeMethod($this->httpRequest->getMethod(), $this->getRequestUri());
  178. } catch (Exception $e) {
  179. $DOM = new DOMDocument('1.0','utf-8');
  180. $DOM->formatOutput = true;
  181. $error = $DOM->createElementNS('DAV:','d:error');
  182. $error->setAttribute('xmlns:s',self::NS_SABREDAV);
  183. $DOM->appendChild($error);
  184. $error->appendChild($DOM->createElement('s:exception',get_class($e)));
  185. $error->appendChild($DOM->createElement('s:message',htmlentities($e->getMessage())));
  186. if ($this->debugExceptions) {
  187. $error->appendChild($DOM->createElement('s:file',$e->getFile()));
  188. $error->appendChild($DOM->createElement('s:line',$e->getLine()));
  189. $error->appendChild($DOM->createElement('s:code',$e->getCode()));
  190. $error->appendChild($DOM->createElement('s:stacktrace',$e->getTraceAsString()));
  191. }
  192. if (self::$exposeVersion) {
  193. $error->appendChild($DOM->createElement('s:sabredav-version',Sabre_DAV_Version::VERSION));
  194. }
  195. if($e instanceof Sabre_DAV_Exception) {
  196. $httpCode = $e->getHTTPCode();
  197. $e->serialize($this,$error);
  198. $headers = $e->getHTTPHeaders($this);
  199. } else {
  200. $httpCode = 500;
  201. $headers = array();
  202. }
  203. $headers['Content-Type'] = 'application/xml; charset=utf-8';
  204. $this->httpResponse->sendStatus($httpCode);
  205. $this->httpResponse->setHeaders($headers);
  206. $this->httpResponse->sendBody($DOM->saveXML());
  207. }
  208. }
  209. /**
  210. * Sets the base server uri
  211. *
  212. * @param string $uri
  213. * @return void
  214. */
  215. public function setBaseUri($uri) {
  216. // If the baseUri does not end with a slash, we must add it
  217. if ($uri[strlen($uri)-1]!=='/')
  218. $uri.='/';
  219. $this->baseUri = $uri;
  220. }
  221. /**
  222. * Returns the base responding uri
  223. *
  224. * @return string
  225. */
  226. public function getBaseUri() {
  227. if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
  228. return $this->baseUri;
  229. }
  230. /**
  231. * This method attempts to detect the base uri.
  232. * Only the PATH_INFO variable is considered.
  233. *
  234. * If this variable is not set, the root (/) is assumed.
  235. *
  236. * @return string
  237. */
  238. public function guessBaseUri() {
  239. $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
  240. $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
  241. // If PATH_INFO is found, we can assume it's accurate.
  242. if (!empty($pathInfo)) {
  243. // We need to make sure we ignore the QUERY_STRING part
  244. if ($pos = strpos($uri,'?'))
  245. $uri = substr($uri,0,$pos);
  246. // PATH_INFO is only set for urls, such as: /example.php/path
  247. // in that case PATH_INFO contains '/path'.
  248. // Note that REQUEST_URI is percent encoded, while PATH_INFO is
  249. // not, Therefore they are only comparable if we first decode
  250. // REQUEST_INFO as well.
  251. $decodedUri = Sabre_DAV_URLUtil::decodePath($uri);
  252. // A simple sanity check:
  253. if(substr($decodedUri,strlen($decodedUri)-strlen($pathInfo))===$pathInfo) {
  254. $baseUri = substr($decodedUri,0,strlen($decodedUri)-strlen($pathInfo));
  255. return rtrim($baseUri,'/') . '/';
  256. }
  257. throw new Sabre_DAV_Exception('The REQUEST_URI ('. $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.');
  258. }
  259. // The last fallback is that we're just going to assume the server root.
  260. return '/';
  261. }
  262. /**
  263. * Adds a plugin to the server
  264. *
  265. * For more information, console the documentation of Sabre_DAV_ServerPlugin
  266. *
  267. * @param Sabre_DAV_ServerPlugin $plugin
  268. * @return void
  269. */
  270. public function addPlugin(Sabre_DAV_ServerPlugin $plugin) {
  271. $this->plugins[$plugin->getPluginName()] = $plugin;
  272. $plugin->initialize($this);
  273. }
  274. /**
  275. * Returns an initialized plugin by it's name.
  276. *
  277. * This function returns null if the plugin was not found.
  278. *
  279. * @param string $name
  280. * @return Sabre_DAV_ServerPlugin
  281. */
  282. public function getPlugin($name) {
  283. if (isset($this->plugins[$name]))
  284. return $this->plugins[$name];
  285. // This is a fallback and deprecated.
  286. foreach($this->plugins as $plugin) {
  287. if (get_class($plugin)===$name) return $plugin;
  288. }
  289. return null;
  290. }
  291. /**
  292. * Returns all plugins
  293. *
  294. * @return array
  295. */
  296. public function getPlugins() {
  297. return $this->plugins;
  298. }
  299. /**
  300. * Subscribe to an event.
  301. *
  302. * When the event is triggered, we'll call all the specified callbacks.
  303. * It is possible to control the order of the callbacks through the
  304. * priority argument.
  305. *
  306. * This is for example used to make sure that the authentication plugin
  307. * is triggered before anything else. If it's not needed to change this
  308. * number, it is recommended to ommit.
  309. *
  310. * @param string $event
  311. * @param callback $callback
  312. * @param int $priority
  313. * @return void
  314. */
  315. public function subscribeEvent($event, $callback, $priority = 100) {
  316. if (!isset($this->eventSubscriptions[$event])) {
  317. $this->eventSubscriptions[$event] = array();
  318. }
  319. while(isset($this->eventSubscriptions[$event][$priority])) $priority++;
  320. $this->eventSubscriptions[$event][$priority] = $callback;
  321. ksort($this->eventSubscriptions[$event]);
  322. }
  323. /**
  324. * Broadcasts an event
  325. *
  326. * This method will call all subscribers. If one of the subscribers returns false, the process stops.
  327. *
  328. * The arguments parameter will be sent to all subscribers
  329. *
  330. * @param string $eventName
  331. * @param array $arguments
  332. * @return bool
  333. */
  334. public function broadcastEvent($eventName,$arguments = array()) {
  335. if (isset($this->eventSubscriptions[$eventName])) {
  336. foreach($this->eventSubscriptions[$eventName] as $subscriber) {
  337. $result = call_user_func_array($subscriber,$arguments);
  338. if ($result===false) return false;
  339. }
  340. }
  341. return true;
  342. }
  343. /**
  344. * Handles a http request, and execute a method based on its name
  345. *
  346. * @param string $method
  347. * @param string $uri
  348. * @return void
  349. */
  350. public function invokeMethod($method, $uri) {
  351. $method = strtoupper($method);
  352. if (!$this->broadcastEvent('beforeMethod',array($method, $uri))) return;
  353. // Make sure this is a HTTP method we support
  354. $internalMethods = array(
  355. 'OPTIONS',
  356. 'GET',
  357. 'HEAD',
  358. 'DELETE',
  359. 'PROPFIND',
  360. 'MKCOL',
  361. 'PUT',
  362. 'PROPPATCH',
  363. 'COPY',
  364. 'MOVE',
  365. 'REPORT'
  366. );
  367. if (in_array($method,$internalMethods)) {
  368. call_user_func(array($this,'http' . $method), $uri);
  369. } else {
  370. if ($this->broadcastEvent('unknownMethod',array($method, $uri))) {
  371. // Unsupported method
  372. throw new Sabre_DAV_Exception_NotImplemented('There was no handler found for this "' . $method . '" method');
  373. }
  374. }
  375. }
  376. // {{{ HTTP Method implementations
  377. /**
  378. * HTTP OPTIONS
  379. *
  380. * @param string $uri
  381. * @return void
  382. */
  383. protected function httpOptions($uri) {
  384. $methods = $this->getAllowedMethods($uri);
  385. $this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods)));
  386. $features = array('1','3', 'extended-mkcol');
  387. foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
  388. $this->httpResponse->setHeader('DAV',implode(', ',$features));
  389. $this->httpResponse->setHeader('MS-Author-Via','DAV');
  390. $this->httpResponse->setHeader('Accept-Ranges','bytes');
  391. if (self::$exposeVersion) {
  392. $this->httpResponse->setHeader('X-Sabre-Version',Sabre_DAV_Version::VERSION);
  393. }
  394. $this->httpResponse->setHeader('Content-Length',0);
  395. $this->httpResponse->sendStatus(200);
  396. }
  397. /**
  398. * HTTP GET
  399. *
  400. * This method simply fetches the contents of a uri, like normal
  401. *
  402. * @param string $uri
  403. * @return bool
  404. */
  405. protected function httpGet($uri) {
  406. $node = $this->tree->getNodeForPath($uri,0);
  407. if (!$this->checkPreconditions(true)) return false;
  408. if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_NotImplemented('GET is only implemented on File objects');
  409. $body = $node->get();
  410. // Converting string into stream, if needed.
  411. if (is_string($body)) {
  412. $stream = fopen('php://temp','r+');
  413. fwrite($stream,$body);
  414. rewind($stream);
  415. $body = $stream;
  416. }
  417. /*
  418. * TODO: getetag, getlastmodified, getsize should also be used using
  419. * this method
  420. */
  421. $httpHeaders = $this->getHTTPHeaders($uri);
  422. /* ContentType needs to get a default, because many webservers will otherwise
  423. * default to text/html, and we don't want this for security reasons.
  424. */
  425. if (!isset($httpHeaders['Content-Type'])) {
  426. $httpHeaders['Content-Type'] = 'application/octet-stream';
  427. }
  428. if (isset($httpHeaders['Content-Length'])) {
  429. $nodeSize = $httpHeaders['Content-Length'];
  430. // Need to unset Content-Length, because we'll handle that during figuring out the range
  431. unset($httpHeaders['Content-Length']);
  432. } else {
  433. $nodeSize = null;
  434. }
  435. $this->httpResponse->setHeaders($httpHeaders);
  436. $range = $this->getHTTPRange();
  437. $ifRange = $this->httpRequest->getHeader('If-Range');
  438. $ignoreRangeHeader = false;
  439. // If ifRange is set, and range is specified, we first need to check
  440. // the precondition.
  441. if ($nodeSize && $range && $ifRange) {
  442. // if IfRange is parsable as a date we'll treat it as a DateTime
  443. // otherwise, we must treat it as an etag.
  444. try {
  445. $ifRangeDate = new DateTime($ifRange);
  446. // It's a date. We must check if the entity is modified since
  447. // the specified date.
  448. if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true;
  449. else {
  450. $modified = new DateTime($httpHeaders['Last-Modified']);
  451. if($modified > $ifRangeDate) $ignoreRangeHeader = true;
  452. }
  453. } catch (Exception $e) {
  454. // It's an entity. We can do a simple comparison.
  455. if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true;
  456. elseif ($httpHeaders['ETag']!==$ifRange) $ignoreRangeHeader = true;
  457. }
  458. }
  459. // We're only going to support HTTP ranges if the backend provided a filesize
  460. if (!$ignoreRangeHeader && $nodeSize && $range) {
  461. // Determining the exact byte offsets
  462. if (!is_null($range[0])) {
  463. $start = $range[0];
  464. $end = $range[1]?$range[1]:$nodeSize-1;
  465. if($start >= $nodeSize)
  466. throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
  467. if($end < $start) throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')');
  468. if($end >= $nodeSize) $end = $nodeSize-1;
  469. } else {
  470. $start = $nodeSize-$range[1];
  471. $end = $nodeSize-1;
  472. if ($start<0) $start = 0;
  473. }
  474. // New read/write stream
  475. $newStream = fopen('php://temp','r+');
  476. stream_copy_to_stream($body, $newStream, $end-$start+1, $start);
  477. rewind($newStream);
  478. $this->httpResponse->setHeader('Content-Length', $end-$start+1);
  479. $this->httpResponse->setHeader('Content-Range','bytes ' . $start . '-' . $end . '/' . $nodeSize);
  480. $this->httpResponse->sendStatus(206);
  481. $this->httpResponse->sendBody($newStream);
  482. } else {
  483. if ($nodeSize) $this->httpResponse->setHeader('Content-Length',$nodeSize);
  484. $this->httpResponse->sendStatus(200);
  485. $this->httpResponse->sendBody($body);
  486. }
  487. }
  488. /**
  489. * HTTP HEAD
  490. *
  491. * This method is normally used to take a peak at a url, and only get the HTTP response headers, without the body
  492. * This is used by clients to determine if a remote file was changed, so they can use a local cached version, instead of downloading it again
  493. *
  494. * @param string $uri
  495. * @return void
  496. */
  497. protected function httpHead($uri) {
  498. $node = $this->tree->getNodeForPath($uri);
  499. /* This information is only collection for File objects.
  500. * Ideally we want to throw 405 Method Not Allowed for every
  501. * non-file, but MS Office does not like this
  502. */
  503. if ($node instanceof Sabre_DAV_IFile) {
  504. $headers = $this->getHTTPHeaders($this->getRequestUri());
  505. if (!isset($headers['Content-Type'])) {
  506. $headers['Content-Type'] = 'application/octet-stream';
  507. }
  508. $this->httpResponse->setHeaders($headers);
  509. }
  510. $this->httpResponse->sendStatus(200);
  511. }
  512. /**
  513. * HTTP Delete
  514. *
  515. * The HTTP delete method, deletes a given uri
  516. *
  517. * @param string $uri
  518. * @return void
  519. */
  520. protected function httpDelete($uri) {
  521. if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
  522. $this->tree->delete($uri);
  523. $this->broadcastEvent('afterUnbind',array($uri));
  524. $this->httpResponse->sendStatus(204);
  525. $this->httpResponse->setHeader('Content-Length','0');
  526. }
  527. /**
  528. * WebDAV PROPFIND
  529. *
  530. * This WebDAV method requests information about an uri resource, or a list of resources
  531. * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
  532. * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
  533. *
  534. * The request body contains an XML data structure that has a list of properties the client understands
  535. * The response body is also an xml document, containing information about every uri resource and the requested properties
  536. *
  537. * It has to return a HTTP 207 Multi-status status code
  538. *
  539. * @param string $uri
  540. * @return void
  541. */
  542. protected function httpPropfind($uri) {
  543. // $xml = new Sabre_DAV_XMLReader(file_get_contents('php://input'));
  544. $requestedProperties = $this->parsePropfindRequest($this->httpRequest->getBody(true));
  545. $depth = $this->getHTTPDepth(1);
  546. // The only two options for the depth of a propfind is 0 or 1
  547. if ($depth!=0) $depth = 1;
  548. $newProperties = $this->getPropertiesForPath($uri,$requestedProperties,$depth);
  549. // This is a multi-status response
  550. $this->httpResponse->sendStatus(207);
  551. $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
  552. // Normally this header is only needed for OPTIONS responses, however..
  553. // iCal seems to also depend on these being set for PROPFIND. Since
  554. // this is not harmful, we'll add it.
  555. $features = array('1','3', 'extended-mkcol');
  556. foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
  557. $this->httpResponse->setHeader('DAV',implode(', ',$features));
  558. $data = $this->generateMultiStatus($newProperties);
  559. $this->httpResponse->sendBody($data);
  560. }
  561. /**
  562. * WebDAV PROPPATCH
  563. *
  564. * This method is called to update properties on a Node. The request is an XML body with all the mutations.
  565. * In this XML body it is specified which properties should be set/updated and/or deleted
  566. *
  567. * @param string $uri
  568. * @return void
  569. */
  570. protected function httpPropPatch($uri) {
  571. $newProperties = $this->parsePropPatchRequest($this->httpRequest->getBody(true));
  572. $result = $this->updateProperties($uri, $newProperties);
  573. $this->httpResponse->sendStatus(207);
  574. $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
  575. $this->httpResponse->sendBody(
  576. $this->generateMultiStatus(array($result))
  577. );
  578. }
  579. /**
  580. * HTTP PUT method
  581. *
  582. * This HTTP method updates a file, or creates a new one.
  583. *
  584. * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content
  585. *
  586. * @param string $uri
  587. * @return bool
  588. */
  589. protected function httpPut($uri) {
  590. $body = $this->httpRequest->getBody();
  591. // Intercepting Content-Range
  592. if ($this->httpRequest->getHeader('Content-Range')) {
  593. /**
  594. Content-Range is dangerous for PUT requests: PUT per definition
  595. stores a full resource. draft-ietf-httpbis-p2-semantics-15 says
  596. in section 7.6:
  597. An origin server SHOULD reject any PUT request that contains a
  598. Content-Range header field, since it might be misinterpreted as
  599. partial content (or might be partial content that is being mistakenly
  600. PUT as a full representation). Partial content updates are possible
  601. by targeting a separately identified resource with state that
  602. overlaps a portion of the larger resource, or by using a different
  603. method that has been specifically defined for partial updates (for
  604. example, the PATCH method defined in [RFC5789]).
  605. This clarifies RFC2616 section 9.6:
  606. The recipient of the entity MUST NOT ignore any Content-*
  607. (e.g. Content-Range) headers that it does not understand or implement
  608. and MUST return a 501 (Not Implemented) response in such cases.
  609. OTOH is a PUT request with a Content-Range currently the only way to
  610. continue an aborted upload request and is supported by curl, mod_dav,
  611. Tomcat and others. Since some clients do use this feature which results
  612. in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject
  613. all PUT requests with a Content-Range for now.
  614. */
  615. throw new Sabre_DAV_Exception_NotImplemented('PUT with Content-Range is not allowed.');
  616. }
  617. // Intercepting the Finder problem
  618. if (($expected = $this->httpRequest->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
  619. /**
  620. Many webservers will not cooperate well with Finder PUT requests,
  621. because it uses 'Chunked' transfer encoding for the request body.
  622. The symptom of this problem is that Finder sends files to the
  623. server, but they arrive as 0-length files in PHP.
  624. If we don't do anything, the user might think they are uploading
  625. files successfully, but they end up empty on the server. Instead,
  626. we throw back an error if we detect this.
  627. The reason Finder uses Chunked, is because it thinks the files
  628. might change as it's being uploaded, and therefore the
  629. Content-Length can vary.
  630. Instead it sends the X-Expected-Entity-Length header with the size
  631. of the file at the very start of the request. If this header is set,
  632. but we don't get a request body we will fail the request to
  633. protect the end-user.
  634. */
  635. // Only reading first byte
  636. $firstByte = fread($body,1);
  637. if (strlen($firstByte)!==1) {
  638. throw new Sabre_DAV_Exception_Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
  639. }
  640. // The body needs to stay intact, so we copy everything to a
  641. // temporary stream.
  642. $newBody = fopen('php://temp','r+');
  643. fwrite($newBody,$firstByte);
  644. stream_copy_to_stream($body, $newBody);
  645. rewind($newBody);
  646. $body = $newBody;
  647. }
  648. if ($this->tree->nodeExists($uri)) {
  649. $node = $this->tree->getNodeForPath($uri);
  650. // Checking If-None-Match and related headers.
  651. if (!$this->checkPreconditions()) return;
  652. // If the node is a collection, we'll deny it
  653. if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_Conflict('PUT is not allowed on non-files.');
  654. if (!$this->broadcastEvent('beforeWriteContent',array($uri, $node, &$body))) return false;
  655. $etag = $node->put($body);
  656. $this->broadcastEvent('afterWriteContent',array($uri, $node));
  657. $this->httpResponse->setHeader('Content-Length','0');
  658. if ($etag) $this->httpResponse->setHeader('ETag',$etag);
  659. $this->httpResponse->sendStatus(204);
  660. } else {
  661. $etag = null;
  662. // If we got here, the resource didn't exist yet.
  663. if (!$this->createFile($this->getRequestUri(),$body,$etag)) {
  664. // For one reason or another the file was not created.
  665. return;
  666. }
  667. $this->httpResponse->setHeader('Content-Length','0');
  668. if ($etag) $this->httpResponse->setHeader('ETag', $etag);
  669. $this->httpResponse->sendStatus(201);
  670. }
  671. }
  672. /**
  673. * WebDAV MKCOL
  674. *
  675. * The MKCOL method is used to create a new collection (directory) on the server
  676. *
  677. * @param string $uri
  678. * @return void
  679. */
  680. protected function httpMkcol($uri) {
  681. $requestBody = $this->httpRequest->getBody(true);
  682. if ($requestBody) {
  683. $contentType = $this->httpRequest->getHeader('Content-Type');
  684. if (strpos($contentType,'application/xml')!==0 && strpos($contentType,'text/xml')!==0) {
  685. // We must throw 415 for unsupported mkcol bodies
  686. throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
  687. }
  688. $dom = Sabre_DAV_XMLUtil::loadDOMDocument($requestBody);
  689. if (Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild)!=='{DAV:}mkcol') {
  690. // We must throw 415 for unsupported mkcol bodies
  691. throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must be a {DAV:}mkcol request construct.');
  692. }
  693. $properties = array();
  694. foreach($dom->firstChild->childNodes as $childNode) {
  695. if (Sabre_DAV_XMLUtil::toClarkNotation($childNode)!=='{DAV:}set') continue;
  696. $properties = array_merge($properties, Sabre_DAV_XMLUtil::parseProperties($childNode, $this->propertyMap));
  697. }
  698. if (!isset($properties['{DAV:}resourcetype']))
  699. throw new Sabre_DAV_Exception_BadRequest('The mkcol request must include a {DAV:}resourcetype property');
  700. $resourceType = $properties['{DAV:}resourcetype']->getValue();
  701. unset($properties['{DAV:}resourcetype']);
  702. } else {
  703. $properties = array();
  704. $resourceType = array('{DAV:}collection');
  705. }
  706. $result = $this->createCollection($uri, $resourceType, $properties);
  707. if (is_array($result)) {
  708. $this->httpResponse->sendStatus(207);
  709. $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
  710. $this->httpResponse->sendBody(
  711. $this->generateMultiStatus(array($result))
  712. );
  713. } else {
  714. $this->httpResponse->setHeader('Content-Length','0');
  715. $this->httpResponse->sendStatus(201);
  716. }
  717. }
  718. /**
  719. * WebDAV HTTP MOVE method
  720. *
  721. * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
  722. *
  723. * @param string $uri
  724. * @return void
  725. */
  726. protected function httpMove($uri) {
  727. $moveInfo = $this->getCopyAndMoveInfo();
  728. // If the destination is part of the source tree, we must fail
  729. if ($moveInfo['destination']==$uri)
  730. throw new Sabre_DAV_Exception_Forbidden('Source and destination uri are identical.');
  731. if ($moveInfo['destinationExists']) {
  732. if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false;
  733. $this->tree->delete($moveInfo['destination']);
  734. $this->broadcastEvent('afterUnbind',array($moveInfo['destination']));
  735. }
  736. if (!$this->broadcastEvent('beforeUnbind',array($uri))) return false;
  737. if (!$this->broadcastEvent('beforeBind',array($moveInfo['destination']))) return false;
  738. $this->tree->move($uri,$moveInfo['destination']);
  739. $this->broadcastEvent('afterUnbind',array($uri));
  740. $this->broadcastEvent('afterBind',array($moveInfo['destination']));
  741. // If a resource was overwritten we should send a 204, otherwise a 201
  742. $this->httpResponse->setHeader('Content-Length','0');
  743. $this->httpResponse->sendStatus($moveInfo['destinationExists']?204:201);
  744. }
  745. /**
  746. * WebDAV HTTP COPY method
  747. *
  748. * This method copies one uri to a different uri, and works much like the MOVE request
  749. * A lot of the actual request processing is done in getCopyMoveInfo
  750. *
  751. * @param string $uri
  752. * @return bool
  753. */
  754. protected function httpCopy($uri) {
  755. $copyInfo = $this->getCopyAndMoveInfo();
  756. // If the destination is part of the source tree, we must fail
  757. if ($copyInfo['destination']==$uri)
  758. throw new Sabre_DAV_Exception_Forbidden('Source and destination uri are identical.');
  759. if ($copyInfo['destinationExists']) {
  760. if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false;
  761. $this->tree->delete($copyInfo['destination']);
  762. }
  763. if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false;
  764. $this->tree->copy($uri,$copyInfo['destination']);
  765. $this->broadcastEvent('afterBind',array($copyInfo['destination']));
  766. // If a resource was overwritten we should send a 204, otherwise a 201
  767. $this->httpResponse->setHeader('Content-Length','0');
  768. $this->httpResponse->sendStatus($copyInfo['destinationExists']?204:201);
  769. }
  770. /**
  771. * HTTP REPORT method implementation
  772. *
  773. * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
  774. * It's used in a lot of extensions, so it made sense to implement it into the core.
  775. *
  776. * @param string $uri
  777. * @return void
  778. */
  779. protected function httpReport($uri) {
  780. $body = $this->httpRequest->getBody(true);
  781. $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
  782. $reportName = Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild);
  783. if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) {
  784. // If broadcastEvent returned true, it means the report was not supported
  785. throw new Sabre_DAV_Exception_ReportNotImplemented();
  786. }
  787. }
  788. // }}}
  789. // {{{ HTTP/WebDAV protocol helpers
  790. /**
  791. * Returns an array with all the supported HTTP methods for a specific uri.
  792. *
  793. * @param string $uri
  794. * @return array
  795. */
  796. public function getAllowedMethods($uri) {
  797. $methods = array(
  798. 'OPTIONS',
  799. 'GET',
  800. 'HEAD',
  801. 'DELETE',
  802. 'PROPFIND',
  803. 'PUT',
  804. 'PROPPATCH',
  805. 'COPY',
  806. 'MOVE',
  807. 'REPORT'
  808. );
  809. // The MKCOL is only allowed on an unmapped uri
  810. try {
  811. $this->tree->getNodeForPath($uri);
  812. } catch (Sabre_DAV_Exception_NotFound $e) {
  813. $methods[] = 'MKCOL';
  814. }
  815. // We're also checking if any of the plugins register any new methods
  816. foreach($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($uri));
  817. array_unique($methods);
  818. return $methods;
  819. }
  820. /**
  821. * Gets the uri for the request, keeping the base uri into consideration
  822. *
  823. * @return string
  824. */
  825. public function getRequestUri() {
  826. return $this->calculateUri($this->httpRequest->getUri());
  827. }
  828. /**
  829. * Calculates the uri for a request, making sure that the base uri is stripped out
  830. *
  831. * @param string $uri
  832. * @throws Sabre_DAV_Exception_Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
  833. * @return string
  834. */
  835. public function calculateUri($uri) {
  836. if ($uri[0]!='/' && strpos($uri,'://')) {
  837. $uri = parse_url($uri,PHP_URL_PATH);
  838. }
  839. $uri = str_replace('//','/',$uri);
  840. if (strpos($uri,$this->getBaseUri())===0) {
  841. return trim(Sabre_DAV_URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/');
  842. // A special case, if the baseUri was accessed without a trailing
  843. // slash, we'll accept it as well.
  844. } elseif ($uri.'/' === $this->getBaseUri()) {
  845. return '';
  846. } else {
  847. throw new Sabre_DAV_Exception_Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
  848. }
  849. }
  850. /**
  851. * Returns the HTTP depth header
  852. *
  853. * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre_DAV_Server::DEPTH_INFINITY object
  854. * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
  855. *
  856. * @param mixed $default
  857. * @return int
  858. */
  859. public function getHTTPDepth($default = self::DEPTH_INFINITY) {
  860. // If its not set, we'll grab the default
  861. $depth = $this->httpRequest->getHeader('Depth');
  862. if (is_null($depth)) return $default;
  863. if ($depth == 'infinity') return self::DEPTH_INFINITY;
  864. // If its an unknown value. we'll grab the default
  865. if (!ctype_digit($depth)) return $default;
  866. return (int)$depth;
  867. }
  868. /**
  869. * Returns the HTTP range header
  870. *
  871. * This method returns null if there is no well-formed HTTP range request
  872. * header or array($start, $end).
  873. *
  874. * The first number is the offset of the first byte in the range.
  875. * The second number is the offset of the last byte in the range.
  876. *
  877. * If the second offset is null, it should be treated as the offset of the last byte of the entity
  878. * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
  879. *
  880. * @return array|null
  881. */
  882. public function getHTTPRange() {
  883. $range = $this->httpRequest->getHeader('range');
  884. if (is_null($range)) return null;
  885. // Matching "Range: bytes=1234-5678: both numbers are optional
  886. if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null;
  887. if ($matches[1]==='' && $matches[2]==='') return null;
  888. return array(
  889. $matches[1]!==''?$matches[1]:null,
  890. $matches[2]!==''?$matches[2]:null,
  891. );
  892. }
  893. /**
  894. * Returns information about Copy and Move requests
  895. *
  896. * This function is created to help getting information about the source and the destination for the
  897. * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
  898. *
  899. * The returned value is an array with the following keys:
  900. * * destination - Destination path
  901. * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
  902. *
  903. * @return array
  904. */
  905. public function getCopyAndMoveInfo() {
  906. // Collecting the relevant HTTP headers
  907. if (!$this->httpRequest->getHeader('Destination')) throw new Sabre_DAV_Exception_BadRequest('The destination header was not supplied');
  908. $destination = $this->calculateUri($this->httpRequest->getHeader('Destination'));
  909. $overwrite = $this->httpRequest->getHeader('Overwrite');
  910. if (!$overwrite) $overwrite = 'T';
  911. if (strtoupper($overwrite)=='T') $overwrite = true;
  912. elseif (strtoupper($overwrite)=='F') $overwrite = false;
  913. // We need to throw a bad request exception, if the header was invalid
  914. else throw new Sabre_DAV_Exception_BadRequest('The HTTP Overwrite header should be either T or F');
  915. list($destinationDir) = Sabre_DAV_URLUtil::splitPath($destination);
  916. try {
  917. $destinationParent = $this->tree->getNodeForPath($destinationDir);
  918. if (!($destinationParent instanceof Sabre_DAV_ICollection)) throw new Sabre_DAV_Exception_UnsupportedMediaType('The destination node is not a collection');
  919. } catch (Sabre_DAV_Exception_NotFound $e) {
  920. // If the destination parent node is not found, we throw a 409
  921. throw new Sabre_DAV_Exception_Conflict('The destination node is not found');
  922. }
  923. try {
  924. $destinationNode = $this->tree->getNodeForPath($destination);
  925. // If this succeeded, it means the destination already exists
  926. // we'll need to throw precondition failed in case overwrite is false
  927. if (!$overwrite) throw new Sabre_DAV_Exception_PreconditionFailed('The destination node already exists, and the overwrite header is set to false','Overwrite');
  928. } catch (Sabre_DAV_Exception_NotFound $e) {
  929. // Destination didn't exist, we're all good
  930. $destinationNode = false;
  931. }
  932. // These are the three relevant properties we need to return
  933. return array(
  934. 'destination' => $destination,
  935. 'destinationExists' => $destinationNode==true,
  936. 'destinationNode' => $destinationNode,
  937. );
  938. }
  939. /**
  940. * Returns a list of properties for a path
  941. *
  942. * This is a simplified version getPropertiesForPath.
  943. * if you aren't interested in status codes, but you just
  944. * want to have a flat list of properties. Use this method.
  945. *
  946. * @param string $path
  947. * @param array $propertyNames
  948. */
  949. public function getProperties($path, $propertyNames) {
  950. $result = $this->getPropertiesForPath($path,$propertyNames,0);
  951. return $result[0][200];
  952. }
  953. /**
  954. * A kid-friendly way to fetch properties for a node's children.
  955. *
  956. * The returned array will be indexed by the path of the of child node.
  957. * Only properties that are actually found will be returned.
  958. *
  959. * The parent node will not be returned.
  960. *
  961. * @param string $path
  962. * @param array $propertyNames
  963. * @return array
  964. */
  965. public function getPropertiesForChildren($path, $propertyNames) {
  966. $result = array();
  967. foreach($this->getPropertiesForPath($path,$propertyNames,1) as $k=>$row) {
  968. // Skipping the parent path
  969. if ($k === 0) continue;
  970. $result[$row['href']] = $row[200];
  971. }
  972. return $result;
  973. }
  974. /**
  975. * Returns a list of HTTP headers for a particular resource
  976. *
  977. * The generated http headers are based on properties provided by the
  978. * resource. The method basically provides a simple mapping between
  979. * DAV property and HTTP header.
  980. *
  981. * The headers are intended to be used for HEAD and GET requests.
  982. *
  983. * @param string $path
  984. * @return array
  985. */
  986. public function getHTTPHeaders($path) {
  987. $propertyMap = array(
  988. '{DAV:}getcontenttype' => 'Content-Type',
  989. '{DAV:}getcontentlength' => 'Content-Length',
  990. '{DAV:}getlastmodified' => 'Last-Modified',
  991. '{DAV:}getetag' => 'ETag',
  992. );
  993. $properties = $this->getProperties($path,array_keys($propertyMap));
  994. $headers = array();
  995. foreach($propertyMap as $property=>$header) {
  996. if (!isset($properties[$property])) continue;
  997. if (is_scalar($properties[$property])) {
  998. $headers[$header] = $properties[$property];
  999. // GetLastModified gets special cased
  1000. } elseif ($properties[$property] instanceof Sabre_DAV_Property_GetLastModified) {
  1001. $headers[$header] = Sabre_HTTP_Util::toHTTPDate($properties[$property]->getTime());
  1002. }
  1003. }
  1004. return $headers;
  1005. }
  1006. /**
  1007. * Returns a list of properties for a given path
  1008. *
  1009. * The path that should be supplied should have the baseUrl stripped out
  1010. * The list of properties should be supplied in Clark notation. If the list is empty
  1011. * 'allprops' is assumed.
  1012. *
  1013. * If a depth of 1 is requested child elements will also be returned.
  1014. *
  1015. * @param string $path
  1016. * @param array $propertyNames
  1017. * @param int $depth
  1018. * @return array
  1019. */
  1020. public function getPropertiesForPath($path, $propertyNames = array(), $depth = 0) {
  1021. if ($depth!=0) $depth = 1;
  1022. $returnPropertyList = array();
  1023. $parentNode = $this->tree->getNodeForPath($path);
  1024. $nodes = array(
  1025. $path => $parentNode
  1026. );
  1027. if ($depth==1 && $parentNode instanceof Sabre_DAV_ICollection) {
  1028. foreach($this->tree->getChildren($path) as $childNode)
  1029. $nodes[$path . '/' . $childNode->getName()] = $childNode;
  1030. }
  1031. // If the propertyNames array is empty, it means all properties are requested.
  1032. // We shouldn't actually return everything we know though, and only return a
  1033. // sensible list.
  1034. $allProperties = count($propertyNames)==0;
  1035. foreach($nodes as $myPath=>$node) {
  1036. $currentPropertyNames = $propertyNames;
  1037. $newProperties = array(
  1038. '200' => array(),
  1039. '404' => array(),
  1040. );
  1041. if ($allProperties) {
  1042. // Default list of propertyNames, when all properties were requested.
  1043. $currentPropertyNames = array(
  1044. '{DAV:}getlastmodified',
  1045. '{DAV:}getcontentlength',
  1046. '{DAV:}resourcetype',
  1047. '{DAV:}quota-used-bytes',
  1048. '{DAV:}quota-available-bytes',
  1049. '{DAV:}getetag',
  1050. '{DAV:}getcontenttype',
  1051. );
  1052. }
  1053. // If the resourceType was not part of the list, we manually add it
  1054. // and mark it for removal. We need to know the resourcetype in order
  1055. // to make certain decisions about the entry.
  1056. // WebDAV dictates we should add a / and the end of href's for collections
  1057. $removeRT = false;
  1058. if (!in_array('{DAV:}resourcetype',$currentPropertyNames)) {
  1059. $currentPropertyNames[] = '{DAV:}resourcetype';
  1060. $removeRT = true;
  1061. }
  1062. $result = $this->broadcastEvent('beforeGetProperties',array($myPath, $node, &$currentPropertyNames, &$newProperties));
  1063. // If this method explicitly returned false, we must ignore this
  1064. // node as it is inaccessible.
  1065. if ($result===false) continue;
  1066. if (count($currentPropertyNames) > 0) {
  1067. if ($node instanceof Sabre_DAV_IProperties)
  1068. $newProperties['200'] = $newProperties[200] + $node->getProperties($currentPropertyNames);
  1069. }
  1070. foreach($currentPropertyNames as $prop) {
  1071. if (isset($newProperties[200][$prop])) continue;
  1072. switch($prop) {
  1073. case '{DAV:}getlastmodified' : if ($node->getLastModified()) $newProperties[200][$prop] = new Sabre_DAV_Property_GetLastModified($node->getLastModified()); break;
  1074. case '{DAV:}getcontentlength' :
  1075. if ($node instanceof Sabre_DAV_IFile) {
  1076. $size = $node->getSize();
  1077. if (!is_null($size)) {
  1078. $newProperties[200][$prop] = (int)$node->getSize();
  1079. }
  1080. }
  1081. break;
  1082. case '{DAV:}quota-used-bytes' :
  1083. if ($node instanceof Sabre_DAV_IQuota) {
  1084. $quotaInfo = $node->getQuotaInfo();
  1085. $newProperties[200][$prop] = $quotaInfo[0];
  1086. }
  1087. break;
  1088. case '{DAV:}quota-available-bytes' :
  1089. if ($node instanceof Sabre_DAV_IQuota) {
  1090. $quotaInfo = $node->getQuotaInfo();
  1091. $newProperties[200][$prop] = $quotaInfo[1];
  1092. }
  1093. break;
  1094. case '{DAV:}getetag' : if ($node instanceof Sabre_DAV_IFile && $etag = $node->getETag()) $newProperties[200][$prop] = $etag; break;
  1095. case '{DAV:}getcontenttype' : if ($node instanceof Sabre_DAV_IFile && $ct = $node->getContentType()) $newProperties[200][$prop] = $ct; break;
  1096. case '{DAV:}supported-report-set' :
  1097. $reports = array();
  1098. foreach($this->plugins as $plugin) {
  1099. $reports = array_merge($reports, $plugin->getSupportedReportSet($myPath));
  1100. }
  1101. $newProperties[200][$prop] = new Sabre_DAV_Property_SupportedReportSet($reports);
  1102. break;
  1103. case '{DAV:}resourcetype' :
  1104. $newProperties[200]['{DAV:}resourcetype'] = new Sabre_DAV_Property_ResourceType();
  1105. foreach($this->resourceTypeMapping as $className => $resourceType) {
  1106. if ($node instanceof $className) $newProperties[200]['{DAV:}resourcetype']->add($resourceType);
  1107. }
  1108. break;
  1109. }
  1110. // If we were unable to find the property, we will list it as 404.
  1111. if (!$allProperties && !isset($newProperties[200][$prop])) $newProperties[404][$prop] = null;
  1112. }
  1113. $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties));
  1114. $newProperties['href'] = trim($myPath,'/');
  1115. // Its is a WebDAV recommendation to add a trailing slash to collectionnames.
  1116. // Apple's iCal also requires a trailing slash for principals (rfc 3744).
  1117. // Therefore we add a trailing / for any non-file. This might need adjustments
  1118. // if we find there are other edge cases.
  1119. if ($myPath!='' && isset($newProperties[200]['{DAV:}resourcetype']) && count($newProperties[200]['{DAV:}resourcetype']->getValue())>0) $newProperties['href'] .='/';
  1120. // If the resourcetype property was manually added to the requested property list,
  1121. // we will remove it again.
  1122. if ($removeRT) unset($newProperties[200]['{DAV:}resourcetype']);
  1123. $returnPropertyList[] = $newProperties;
  1124. }
  1125. return $returnPropertyList;
  1126. }
  1127. /**
  1128. * This method is invoked by sub-systems creating a new file.
  1129. *
  1130. * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
  1131. * It was important to get this done through a centralized function,
  1132. * allowing plugins to intercept this using the beforeCreateFile event.
  1133. *
  1134. * This method will return true if the file was actually created
  1135. *
  1136. * @param string $uri
  1137. * @param resource $data
  1138. * @param string $etag
  1139. * @return bool
  1140. */
  1141. public function createFile($uri,$data, &$etag = null) {
  1142. list($dir,$name) = Sabre_DAV_URLUtil::splitPath($uri);
  1143. if (!$this->broadcastEvent('beforeBind',array($uri))) return false;
  1144. $parent = $this->tree->getNodeForPath($dir);
  1145. if (!$this->broadcastEvent('beforeCreateFile',array($uri, &$data, $parent))) return false;
  1146. $etag = $parent->createFile($name,$data);
  1147. $this->tree->markDirty($dir);
  1148. $this->broadcastEvent('afterBind',array($uri));
  1149. $this->broadcastEvent('afterCreateFile',array($uri, $parent));
  1150. return true;
  1151. }
  1152. /**
  1153. * This method is invoked by sub-systems creating a new directory.
  1154. *
  1155. * @param string $uri
  1156. * @return void
  1157. */
  1158. public function createDirectory($uri) {
  1159. $this->createCollection($uri,array('{DAV:}collection'),array());
  1160. }
  1161. /**
  1162. * Use this method to create a new collection
  1163. *
  1164. * The {DAV:}resourcetype is specified using the resourceType array.
  1165. * At the very least it must contain {DAV:}collection.
  1166. *
  1167. * The properties array can contain a list of additional properties.
  1168. *
  1169. * @param string $uri The new uri
  1170. * @param array $resourceType The resourceType(s)
  1171. * @param array $properties A list of properties
  1172. * @return array|null
  1173. */
  1174. public function createCollection($uri, array $resourceType, array $properties) {
  1175. list($parentUri,$newName) = Sabre_DAV_URLUtil::splitPath($uri);
  1176. // Making sure {DAV:}collection was specified as resourceType
  1177. if (!in_array('{DAV:}collection', $resourceType)) {
  1178. throw new Sabre_DAV_Exception_InvalidResourceType('The resourceType for this collection must at least include {DAV:}collection');
  1179. }
  1180. // Making sure the parent exists
  1181. try {
  1182. $parent = $this->tree->getNodeForPath($parentUri);
  1183. } catch (Sabre_DAV_Exception_NotFound $e) {
  1184. throw new Sabre_DAV_Exception_Conflict('Parent node does not exist');
  1185. }
  1186. // Making sure the parent is a collection
  1187. if (!$parent instanceof Sabre_DAV_ICollection) {
  1188. throw new Sabre_DAV_Exception_Conflict('Parent node is not a collection');
  1189. }
  1190. // Making sure the child does not already exist
  1191. try {
  1192. $parent->getChild($newName);
  1193. // If we got here.. it means there's already a node on that url, and we need to throw a 405
  1194. throw new Sabre_DAV_Exception_MethodNotAllowed('The resource you tried to create already exists');
  1195. } catch (Sabre_DAV_Exception_NotFound $e) {
  1196. // This is correct
  1197. }
  1198. if (!$this->broadcastEvent('beforeBind',array($uri))) return;
  1199. // There are 2 modes of operation. The standard collection
  1200. // creates the directory, and then updates properties
  1201. // the extended collection can create it directly.
  1202. if ($parent instanceof Sabre_DAV_IExtendedCollection) {
  1203. $parent->createExtendedCollection($newName, $resourceType, $properties);
  1204. } else {
  1205. // No special resourcetypes are supported
  1206. if (count($resourceType)>1) {
  1207. throw new Sabre_DAV_Exception_InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
  1208. }
  1209. $parent->createDirectory($newName);
  1210. $rollBack = false;
  1211. $exception = null;
  1212. $errorResult = null;
  1213. if (count($properties)>0) {
  1214. try {
  1215. $errorResult = $this->updateProperties($uri, $properties);
  1216. if (!isset($errorResult[200])) {
  1217. $rollBack = true;
  1218. }
  1219. } catch (Sabre_DAV_Exception $e) {
  1220. $rollBack = true;
  1221. $exception = $e;
  1222. }
  1223. }
  1224. if ($rollBack) {
  1225. if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
  1226. $this->tree->delete($uri);
  1227. // Re-throwing exception
  1228. if ($exception) throw $exception;
  1229. return $errorResult;
  1230. }
  1231. }
  1232. $this->tree->markDirty($parentUri);
  1233. $this->broadcastEvent('afterBind',array($uri));
  1234. }
  1235. /**
  1236. * This method updates a resource's properties
  1237. *
  1238. * The properties array must be a list of properties. Array-keys are
  1239. * property names in clarknotation, array-values are it's values.
  1240. * If a property must be deleted, the value should be null.
  1241. *
  1242. * Note that this request should either completely succeed, or
  1243. * completely fail.
  1244. *
  1245. * The response is an array with statuscodes for keys, which in turn
  1246. * contain arrays with propertynames. This response can be used
  1247. * to generate a multistatus body.
  1248. *
  1249. * @param string $uri
  1250. * @param array $properties
  1251. * @return array
  1252. */
  1253. public function updateProperties($uri, array $properties) {
  1254. // we'll start by grabbing the node, this will throw the appropriate
  1255. // exceptions if it doesn't.
  1256. $node = $this->tree->getNodeForPath($uri);
  1257. $result = array(
  1258. 200 => array(),
  1259. 403 => array(),
  1260. 424 => array(),
  1261. );
  1262. $remainingProperties = $properties;
  1263. $hasError = false;
  1264. // Running through all properties to make sure none of them are protected
  1265. if (!$hasError) foreach($properties as $propertyName => $value) {
  1266. if(in_array($propertyName, $this->protectedProperties)) {
  1267. $result[403][$propertyName] = null;
  1268. unset($remainingProperties[$propertyName]);
  1269. $hasError = true;
  1270. }
  1271. }
  1272. if (!$hasError) {
  1273. // Allowing plugins to take care of property updating
  1274. $hasError = !$this->broadcastEvent('updateProperties',array(
  1275. &$remainingProperties,
  1276. &$result,
  1277. $node
  1278. ));
  1279. }
  1280. // If the node is not an instance of Sabre_DAV_IProperties, every
  1281. // property is 403 Forbidden
  1282. if (!$hasError && count($remainingProperties) && !($node instanceof Sabre_DAV_IProperties)) {
  1283. $hasError = true;
  1284. foreach($properties as $propertyName=> $value) {
  1285. $result[403][$propertyName] = null;
  1286. }
  1287. $remainingProperties = array();
  1288. }
  1289. // Only if there were no errors we may attempt to update the resource
  1290. if (!$hasError) {
  1291. if (count($remainingProperties)>0) {
  1292. $updateResult = $node->updateProperties($remainingProperties);
  1293. if ($updateResult===true) {
  1294. // success
  1295. foreach($remainingProperties as $propertyName=>$value) {
  1296. $result[200][$propertyName] = null;
  1297. }
  1298. } elseif ($updateResult===false) {
  1299. // The node failed to update the properties for an
  1300. // unknown reason
  1301. foreach($remainingProperties as $propertyName=>$value) {
  1302. $result[403][$propertyName] = null;
  1303. }
  1304. } elseif (is_array($updateResult)) {
  1305. // The node has detailed update information
  1306. // We need to merge the results with the earlier results.
  1307. foreach($updateResult as $status => $props) {
  1308. if (is_array($props)) {
  1309. if (!isset($result[$status]))
  1310. $result[$status] = array();
  1311. $result[$status] = array_merge($result[$status], $updateResult[$status]);
  1312. }
  1313. }
  1314. } else {
  1315. throw new Sabre_DAV_Exception('Invalid result from updateProperties');
  1316. }
  1317. $remainingProperties = array();
  1318. }
  1319. }
  1320. foreach($remainingProperties as $propertyName=>$value) {
  1321. // if there are remaining properties, it must mean
  1322. // there's a dependency failure
  1323. $result[424][$propertyName] = null;
  1324. }
  1325. // Removing empty array values
  1326. foreach($result as $status=>$props) {
  1327. if (count($props)===0) unset($result[$status]);
  1328. }
  1329. $result['href'] = $uri;
  1330. return $result;
  1331. }
  1332. /**
  1333. * This method checks the main HTTP preconditions.
  1334. *
  1335. * Currently these are:
  1336. * * If-Match
  1337. * * If-None-Match
  1338. * * If-Modified-Since
  1339. * * If-Unmodified-Since
  1340. *
  1341. * The method will return true if all preconditions are met
  1342. * The method will return false, or throw an exception if preconditions
  1343. * failed. If false is returned the operation should be aborted, and
  1344. * the appropriate HTTP response headers are already set.
  1345. *
  1346. * Normally this method will throw 412 Precondition Failed for failures
  1347. * related to If-None-Match, If-Match and If-Unmodified Since. It will
  1348. * set the status to 304 Not Modified for If-Modified_since.
  1349. *
  1350. * If the $handleAsGET argument is set to true, it will also return 304
  1351. * Not Modified for failure of the If-None-Match precondition. This is the
  1352. * desired behaviour for HTTP GET and HTTP HEAD requests.
  1353. *
  1354. * @param bool $handleAsGET
  1355. * @return bool
  1356. */
  1357. public function checkPreconditions($handleAsGET = false) {
  1358. $uri = $this->getRequestUri();
  1359. $node = null;
  1360. $lastMod = null;
  1361. $etag = null;
  1362. if ($ifMatch = $this->httpRequest->getHeader('If-Match')) {
  1363. // If-Match contains an entity tag. Only if the entity-tag
  1364. // matches we are allowed to make the request succeed.
  1365. // If the entity-tag is '*' we are only allowed to make the
  1366. // request succeed if a resource exists at that url.
  1367. try {
  1368. $node = $this->tree->getNodeForPath($uri);
  1369. } catch (Sabre_DAV_Exception_NotFound $e) {
  1370. throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified and the resource did not exist','If-Match');
  1371. }
  1372. // Only need to check entity tags if they are not *
  1373. if ($ifMatch!=='*') {
  1374. // There can be multiple etags
  1375. $ifMatch = explode(',',$ifMatch);
  1376. $haveMatch = false;
  1377. foreach($ifMatch as $ifMatchItem) {
  1378. // Stripping any extra spaces
  1379. $ifMatchItem = trim($ifMatchItem,' ');
  1380. $etag = $node->getETag();
  1381. if ($etag===$ifMatchItem) {
  1382. $haveMatch = true;
  1383. }
  1384. }
  1385. if (!$haveMatch) {
  1386. throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
  1387. }
  1388. }
  1389. }
  1390. if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) {
  1391. // The If-None-Match header contains an etag.
  1392. // Only if the ETag does not match the current ETag, the request will succeed
  1393. // The header can also contain *, in which case the request
  1394. // will only succeed if the entity does not exist at all.
  1395. $nodeExists = true;
  1396. if (!$node) {
  1397. try {
  1398. $node = $this->tree->getNodeForPath($uri);
  1399. } catch (Sabre_DAV_Exception_NotFound $e) {
  1400. $nodeExists = false;
  1401. }
  1402. }
  1403. if ($nodeExists) {
  1404. $haveMatch = false;
  1405. if ($ifNoneMatch==='*') $haveMatch = true;
  1406. else {
  1407. // There might be multiple etags
  1408. $ifNoneMatch = explode(',', $ifNoneMatch);
  1409. $etag = $node->getETag();
  1410. foreach($ifNoneMatch as $ifNoneMatchItem) {
  1411. // Stripping any extra spaces
  1412. $ifNoneMatchItem = trim($ifNoneMatchItem,' ');
  1413. if ($etag===$ifNoneMatchItem) $haveMatch = true;
  1414. }
  1415. }
  1416. if ($haveMatch) {
  1417. if ($handleAsGET) {
  1418. $this->httpResponse->sendStatus(304);
  1419. return false;
  1420. } else {
  1421. throw new Sabre_DAV_Exception_PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match');
  1422. }
  1423. }
  1424. }
  1425. }
  1426. if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) {
  1427. // The If-Modified-Since header contains a date. We
  1428. // will only return the entity if it has been changed since
  1429. // that date. If it hasn't been changed, we return a 304
  1430. // header
  1431. // Note that this header only has to be checked if there was no If-None-Match header
  1432. // as per the HTTP spec.
  1433. $date = Sabre_HTTP_Util::parseHTTPDate($ifModifiedSince);
  1434. if ($date) {
  1435. if (is_null($node)) {
  1436. $node = $this->tree->getNodeForPath($uri);
  1437. }
  1438. $lastMod = $node->getLastModified();
  1439. if ($lastMod) {
  1440. $lastMod = new DateTime('@' . $lastMod);
  1441. if ($lastMod <= $date) {
  1442. $this->httpResponse->sendStatus(304);
  1443. $this->httpResponse->setHeader('Last-Modified', Sabre_HTTP_Util::toHTTPDate($lastMod));
  1444. return false;
  1445. }
  1446. }
  1447. }
  1448. }
  1449. if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) {
  1450. // The If-Unmodified-Since will allow allow the request if the
  1451. // entity has not changed since the specified date.
  1452. $date = Sabre_HTTP_Util::parseHTTPDate($ifUnmodifiedSince);
  1453. // We must only check the date if it's valid
  1454. if ($date) {
  1455. if (is_null($node)) {
  1456. $node = $this->tree->getNodeForPath($uri);
  1457. }
  1458. $lastMod = $node->getLastModified();
  1459. if ($lastMod) {
  1460. $lastMod = new DateTime('@' . $lastMod);
  1461. if ($lastMod > $date) {
  1462. throw new Sabre_DAV_Exception_PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.','If-Unmodified-Since');
  1463. }
  1464. }
  1465. }
  1466. }
  1467. return true;
  1468. }
  1469. // }}}
  1470. // {{{ XML Readers & Writers
  1471. /**
  1472. * Generates a WebDAV propfind response body based on a list of nodes
  1473. *
  1474. * @param array $fileProperties The list with nodes
  1475. * @return string
  1476. */
  1477. public function generateMultiStatus(array $fileProperties) {
  1478. $dom = new DOMDocument('1.0','utf-8');
  1479. //$dom->formatOutput = true;
  1480. $multiStatus = $dom->createElement('d:multistatus');
  1481. $dom->appendChild($multiStatus);
  1482. // Adding in default namespaces
  1483. foreach($this->xmlNamespaces as $namespace=>$prefix) {
  1484. $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
  1485. }
  1486. foreach($fileProperties as $entry) {
  1487. $href = $entry['href'];
  1488. unset($entry['href']);
  1489. $response = new Sabre_DAV_Property_Response($href,$entry);
  1490. $response->serialize($this,$multiStatus);
  1491. }
  1492. return $dom->saveXML();
  1493. }
  1494. /**
  1495. * This method parses a PropPatch request
  1496. *
  1497. * PropPatch changes the properties for a resource. This method
  1498. * returns a list of properties.
  1499. *
  1500. * The keys in the returned array contain the property name (e.g.: {DAV:}displayname,
  1501. * and the value contains the property value. If a property is to be removed the value
  1502. * will be null.
  1503. *
  1504. * @param string $body xml body
  1505. * @return array list of properties in need of updating or deletion
  1506. */
  1507. public function parsePropPatchRequest($body) {
  1508. //We'll need to change the DAV namespace declaration to something else in order to make it parsable
  1509. $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
  1510. $newProperties = array();
  1511. foreach($dom->firstChild->childNodes as $child) {
  1512. if ($child->nodeType !== XML_ELEMENT_NODE) continue;
  1513. $operation = Sabre_DAV_XMLUtil::toClarkNotation($child);
  1514. if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue;
  1515. $innerProperties = Sabre_DAV_XMLUtil::parseProperties($child, $this->propertyMap);
  1516. foreach($innerProperties as $propertyName=>$propertyValue) {
  1517. if ($operation==='{DAV:}remove') {
  1518. $propertyValue = null;
  1519. }
  1520. $newProperties[$propertyName] = $propertyValue;
  1521. }
  1522. }
  1523. return $newProperties;
  1524. }
  1525. /**
  1526. * This method parses the PROPFIND request and returns its information
  1527. *
  1528. * This will either be a list of properties, or an empty array; in which case
  1529. * an {DAV:}allprop was requested.
  1530. *
  1531. * @param string $body
  1532. * @return array
  1533. */
  1534. public function parsePropFindRequest($body) {
  1535. // If the propfind body was empty, it means IE is requesting 'all' properties
  1536. if (!$body) return array();
  1537. $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
  1538. $elem = $dom->getElementsByTagNameNS('urn:DAV','propfind')->item(0);
  1539. return array_keys(Sabre_DAV_XMLUtil::parseProperties($elem));
  1540. }
  1541. // }}}
  1542. }