DAV.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OC\Files\Storage;
  8. use Exception;
  9. use Icewind\Streams\CallbackWrapper;
  10. use Icewind\Streams\IteratorDirectory;
  11. use OC\Files\Filesystem;
  12. use OC\MemCache\ArrayCache;
  13. use OCP\AppFramework\Http;
  14. use OCP\Constants;
  15. use OCP\Diagnostics\IEventLogger;
  16. use OCP\Files\FileInfo;
  17. use OCP\Files\ForbiddenException;
  18. use OCP\Files\IMimeTypeDetector;
  19. use OCP\Files\StorageInvalidException;
  20. use OCP\Files\StorageNotAvailableException;
  21. use OCP\Http\Client\IClientService;
  22. use OCP\ICertificateManager;
  23. use OCP\IConfig;
  24. use OCP\Util;
  25. use Psr\Http\Message\ResponseInterface;
  26. use Psr\Log\LoggerInterface;
  27. use Sabre\DAV\Client;
  28. use Sabre\DAV\Xml\Property\ResourceType;
  29. use Sabre\HTTP\ClientException;
  30. use Sabre\HTTP\ClientHttpException;
  31. use Sabre\HTTP\RequestInterface;
  32. /**
  33. * Class DAV
  34. *
  35. * @package OC\Files\Storage
  36. */
  37. class DAV extends Common {
  38. /** @var string */
  39. protected $password;
  40. /** @var string */
  41. protected $user;
  42. /** @var string|null */
  43. protected $authType;
  44. /** @var string */
  45. protected $host;
  46. /** @var bool */
  47. protected $secure;
  48. /** @var string */
  49. protected $root;
  50. /** @var string */
  51. protected $certPath;
  52. /** @var bool */
  53. protected $ready;
  54. /** @var Client */
  55. protected $client;
  56. /** @var ArrayCache */
  57. protected $statCache;
  58. /** @var IClientService */
  59. protected $httpClientService;
  60. /** @var ICertificateManager */
  61. protected $certManager;
  62. protected LoggerInterface $logger;
  63. protected IEventLogger $eventLogger;
  64. protected IMimeTypeDetector $mimeTypeDetector;
  65. /** @var int */
  66. private $timeout;
  67. protected const PROPFIND_PROPS = [
  68. '{DAV:}getlastmodified',
  69. '{DAV:}getcontentlength',
  70. '{DAV:}getcontenttype',
  71. '{http://owncloud.org/ns}permissions',
  72. '{http://open-collaboration-services.org/ns}share-permissions',
  73. '{DAV:}resourcetype',
  74. '{DAV:}getetag',
  75. '{DAV:}quota-available-bytes',
  76. ];
  77. /**
  78. * @param array $params
  79. * @throws \Exception
  80. */
  81. public function __construct($params) {
  82. $this->statCache = new ArrayCache();
  83. $this->httpClientService = \OC::$server->get(IClientService::class);
  84. if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
  85. $host = $params['host'];
  86. //remove leading http[s], will be generated in createBaseUri()
  87. if (str_starts_with($host, "https://")) {
  88. $host = substr($host, 8);
  89. } elseif (str_starts_with($host, "http://")) {
  90. $host = substr($host, 7);
  91. }
  92. $this->host = $host;
  93. $this->user = $params['user'];
  94. $this->password = $params['password'];
  95. if (isset($params['authType'])) {
  96. $this->authType = $params['authType'];
  97. }
  98. if (isset($params['secure'])) {
  99. if (is_string($params['secure'])) {
  100. $this->secure = ($params['secure'] === 'true');
  101. } else {
  102. $this->secure = (bool)$params['secure'];
  103. }
  104. } else {
  105. $this->secure = false;
  106. }
  107. if ($this->secure === true) {
  108. // inject mock for testing
  109. $this->certManager = \OC::$server->getCertificateManager();
  110. }
  111. $this->root = $params['root'] ?? '/';
  112. $this->root = '/' . ltrim($this->root, '/');
  113. $this->root = rtrim($this->root, '/') . '/';
  114. } else {
  115. throw new \Exception('Invalid webdav storage configuration');
  116. }
  117. $this->logger = \OC::$server->get(LoggerInterface::class);
  118. $this->eventLogger = \OC::$server->get(IEventLogger::class);
  119. // This timeout value will be used for the download and upload of files
  120. $this->timeout = \OC::$server->get(IConfig::class)->getSystemValueInt('davstorage.request_timeout', 30);
  121. $this->mimeTypeDetector = \OC::$server->getMimeTypeDetector();
  122. }
  123. protected function init() {
  124. if ($this->ready) {
  125. return;
  126. }
  127. $this->ready = true;
  128. $settings = [
  129. 'baseUri' => $this->createBaseUri(),
  130. 'userName' => $this->user,
  131. 'password' => $this->password,
  132. ];
  133. if ($this->authType !== null) {
  134. $settings['authType'] = $this->authType;
  135. }
  136. $proxy = \OC::$server->getConfig()->getSystemValueString('proxy', '');
  137. if ($proxy !== '') {
  138. $settings['proxy'] = $proxy;
  139. }
  140. $this->client = new Client($settings);
  141. $this->client->setThrowExceptions(true);
  142. if ($this->secure === true) {
  143. $certPath = $this->certManager->getAbsoluteBundlePath();
  144. if (file_exists($certPath)) {
  145. $this->certPath = $certPath;
  146. }
  147. if ($this->certPath) {
  148. $this->client->addCurlSetting(CURLOPT_CAINFO, $this->certPath);
  149. }
  150. }
  151. $lastRequestStart = 0;
  152. $this->client->on('beforeRequest', function (RequestInterface $request) use (&$lastRequestStart) {
  153. $this->logger->debug("sending dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl(), ['app' => 'dav']);
  154. $lastRequestStart = microtime(true);
  155. $this->eventLogger->start('fs:storage:dav:request', "Sending dav request to external storage");
  156. });
  157. $this->client->on('afterRequest', function (RequestInterface $request) use (&$lastRequestStart) {
  158. $elapsed = microtime(true) - $lastRequestStart;
  159. $this->logger->debug("dav " . $request->getMethod() . " request to external storage: " . $request->getAbsoluteUrl() . " took " . round($elapsed * 1000, 1) . "ms", ['app' => 'dav']);
  160. $this->eventLogger->end('fs:storage:dav:request');
  161. });
  162. }
  163. /**
  164. * Clear the stat cache
  165. */
  166. public function clearStatCache() {
  167. $this->statCache->clear();
  168. }
  169. /** {@inheritdoc} */
  170. public function getId() {
  171. return 'webdav::' . $this->user . '@' . $this->host . '/' . $this->root;
  172. }
  173. /** {@inheritdoc} */
  174. public function createBaseUri() {
  175. $baseUri = 'http';
  176. if ($this->secure) {
  177. $baseUri .= 's';
  178. }
  179. $baseUri .= '://' . $this->host . $this->root;
  180. return $baseUri;
  181. }
  182. /** {@inheritdoc} */
  183. public function mkdir($path) {
  184. $this->init();
  185. $path = $this->cleanPath($path);
  186. $result = $this->simpleResponse('MKCOL', $path, null, 201);
  187. if ($result) {
  188. $this->statCache->set($path, true);
  189. }
  190. return $result;
  191. }
  192. /** {@inheritdoc} */
  193. public function rmdir($path) {
  194. $this->init();
  195. $path = $this->cleanPath($path);
  196. // FIXME: some WebDAV impl return 403 when trying to DELETE
  197. // a non-empty folder
  198. $result = $this->simpleResponse('DELETE', $path . '/', null, 204);
  199. $this->statCache->clear($path . '/');
  200. $this->statCache->remove($path);
  201. return $result;
  202. }
  203. /** {@inheritdoc} */
  204. public function opendir($path) {
  205. $this->init();
  206. $path = $this->cleanPath($path);
  207. try {
  208. $content = $this->getDirectoryContent($path);
  209. $files = [];
  210. foreach ($content as $child) {
  211. $files[] = $child['name'];
  212. }
  213. return IteratorDirectory::wrap($files);
  214. } catch (\Exception $e) {
  215. $this->convertException($e, $path);
  216. }
  217. return false;
  218. }
  219. /**
  220. * Propfind call with cache handling.
  221. *
  222. * First checks if information is cached.
  223. * If not, request it from the server then store to cache.
  224. *
  225. * @param string $path path to propfind
  226. *
  227. * @return array|boolean propfind response or false if the entry was not found
  228. *
  229. * @throws ClientHttpException
  230. */
  231. protected function propfind($path) {
  232. $path = $this->cleanPath($path);
  233. $cachedResponse = $this->statCache->get($path);
  234. // we either don't know it, or we know it exists but need more details
  235. if (is_null($cachedResponse) || $cachedResponse === true) {
  236. $this->init();
  237. $response = false;
  238. try {
  239. $response = $this->client->propFind(
  240. $this->encodePath($path),
  241. self::PROPFIND_PROPS
  242. );
  243. $this->statCache->set($path, $response);
  244. } catch (ClientHttpException $e) {
  245. if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
  246. $this->statCache->clear($path . '/');
  247. $this->statCache->set($path, false);
  248. } else {
  249. $this->convertException($e, $path);
  250. }
  251. } catch (\Exception $e) {
  252. $this->convertException($e, $path);
  253. }
  254. } else {
  255. $response = $cachedResponse;
  256. }
  257. return $response;
  258. }
  259. /** {@inheritdoc} */
  260. public function filetype($path) {
  261. try {
  262. $response = $this->propfind($path);
  263. if ($response === false) {
  264. return false;
  265. }
  266. $responseType = [];
  267. if (isset($response["{DAV:}resourcetype"])) {
  268. /** @var ResourceType[] $response */
  269. $responseType = $response["{DAV:}resourcetype"]->getValue();
  270. }
  271. return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
  272. } catch (\Exception $e) {
  273. $this->convertException($e, $path);
  274. }
  275. return false;
  276. }
  277. /** {@inheritdoc} */
  278. public function file_exists($path) {
  279. try {
  280. $path = $this->cleanPath($path);
  281. $cachedState = $this->statCache->get($path);
  282. if ($cachedState === false) {
  283. // we know the file doesn't exist
  284. return false;
  285. } elseif (!is_null($cachedState)) {
  286. return true;
  287. }
  288. // need to get from server
  289. return ($this->propfind($path) !== false);
  290. } catch (\Exception $e) {
  291. $this->convertException($e, $path);
  292. }
  293. return false;
  294. }
  295. /** {@inheritdoc} */
  296. public function unlink($path) {
  297. $this->init();
  298. $path = $this->cleanPath($path);
  299. $result = $this->simpleResponse('DELETE', $path, null, 204);
  300. $this->statCache->clear($path . '/');
  301. $this->statCache->remove($path);
  302. return $result;
  303. }
  304. /** {@inheritdoc} */
  305. public function fopen($path, $mode) {
  306. $this->init();
  307. $path = $this->cleanPath($path);
  308. switch ($mode) {
  309. case 'r':
  310. case 'rb':
  311. try {
  312. $response = $this->httpClientService
  313. ->newClient()
  314. ->get($this->createBaseUri() . $this->encodePath($path), [
  315. 'auth' => [$this->user, $this->password],
  316. 'stream' => true,
  317. // set download timeout for users with slow connections or large files
  318. 'timeout' => $this->timeout
  319. ]);
  320. } catch (\GuzzleHttp\Exception\ClientException $e) {
  321. if ($e->getResponse() instanceof ResponseInterface
  322. && $e->getResponse()->getStatusCode() === 404) {
  323. return false;
  324. } else {
  325. throw $e;
  326. }
  327. }
  328. if ($response->getStatusCode() !== Http::STATUS_OK) {
  329. if ($response->getStatusCode() === Http::STATUS_LOCKED) {
  330. throw new \OCP\Lock\LockedException($path);
  331. } else {
  332. \OC::$server->get(LoggerInterface::class)->error('Guzzle get returned status code ' . $response->getStatusCode(), ['app' => 'webdav client']);
  333. }
  334. }
  335. return $response->getBody();
  336. case 'w':
  337. case 'wb':
  338. case 'a':
  339. case 'ab':
  340. case 'r+':
  341. case 'w+':
  342. case 'wb+':
  343. case 'a+':
  344. case 'x':
  345. case 'x+':
  346. case 'c':
  347. case 'c+':
  348. //emulate these
  349. $tempManager = \OC::$server->getTempManager();
  350. if (strrpos($path, '.') !== false) {
  351. $ext = substr($path, strrpos($path, '.'));
  352. } else {
  353. $ext = '';
  354. }
  355. if ($this->file_exists($path)) {
  356. if (!$this->isUpdatable($path)) {
  357. return false;
  358. }
  359. if ($mode === 'w' or $mode === 'w+') {
  360. $tmpFile = $tempManager->getTemporaryFile($ext);
  361. } else {
  362. $tmpFile = $this->getCachedFile($path);
  363. }
  364. } else {
  365. if (!$this->isCreatable(dirname($path))) {
  366. return false;
  367. }
  368. $tmpFile = $tempManager->getTemporaryFile($ext);
  369. }
  370. $handle = fopen($tmpFile, $mode);
  371. return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
  372. $this->writeBack($tmpFile, $path);
  373. });
  374. }
  375. }
  376. /**
  377. * @param string $tmpFile
  378. */
  379. public function writeBack($tmpFile, $path) {
  380. $this->uploadFile($tmpFile, $path);
  381. unlink($tmpFile);
  382. }
  383. /** {@inheritdoc} */
  384. public function free_space($path) {
  385. $this->init();
  386. $path = $this->cleanPath($path);
  387. try {
  388. $response = $this->propfind($path);
  389. if ($response === false) {
  390. return FileInfo::SPACE_UNKNOWN;
  391. }
  392. if (isset($response['{DAV:}quota-available-bytes'])) {
  393. return Util::numericToNumber($response['{DAV:}quota-available-bytes']);
  394. } else {
  395. return FileInfo::SPACE_UNKNOWN;
  396. }
  397. } catch (\Exception $e) {
  398. return FileInfo::SPACE_UNKNOWN;
  399. }
  400. }
  401. /** {@inheritdoc} */
  402. public function touch($path, $mtime = null) {
  403. $this->init();
  404. if (is_null($mtime)) {
  405. $mtime = time();
  406. }
  407. $path = $this->cleanPath($path);
  408. // if file exists, update the mtime, else create a new empty file
  409. if ($this->file_exists($path)) {
  410. try {
  411. $this->statCache->remove($path);
  412. $this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
  413. // non-owncloud clients might not have accepted the property, need to recheck it
  414. $response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
  415. if (isset($response['{DAV:}getlastmodified'])) {
  416. $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
  417. if ($remoteMtime !== $mtime) {
  418. // server has not accepted the mtime
  419. return false;
  420. }
  421. }
  422. } catch (ClientHttpException $e) {
  423. if ($e->getHttpStatus() === 501) {
  424. return false;
  425. }
  426. $this->convertException($e, $path);
  427. return false;
  428. } catch (\Exception $e) {
  429. $this->convertException($e, $path);
  430. return false;
  431. }
  432. } else {
  433. $this->file_put_contents($path, '');
  434. }
  435. return true;
  436. }
  437. /**
  438. * @param string $path
  439. * @param mixed $data
  440. * @return int|float|false
  441. */
  442. public function file_put_contents($path, $data) {
  443. $path = $this->cleanPath($path);
  444. $result = parent::file_put_contents($path, $data);
  445. $this->statCache->remove($path);
  446. return $result;
  447. }
  448. /**
  449. * @param string $path
  450. * @param string $target
  451. */
  452. protected function uploadFile($path, $target) {
  453. $this->init();
  454. // invalidate
  455. $target = $this->cleanPath($target);
  456. $this->statCache->remove($target);
  457. $source = fopen($path, 'r');
  458. $this->httpClientService
  459. ->newClient()
  460. ->put($this->createBaseUri() . $this->encodePath($target), [
  461. 'body' => $source,
  462. 'auth' => [$this->user, $this->password],
  463. // set upload timeout for users with slow connections or large files
  464. 'timeout' => $this->timeout
  465. ]);
  466. $this->removeCachedFile($target);
  467. }
  468. /** {@inheritdoc} */
  469. public function rename($source, $target) {
  470. $this->init();
  471. $source = $this->cleanPath($source);
  472. $target = $this->cleanPath($target);
  473. try {
  474. // overwrite directory ?
  475. if ($this->is_dir($target)) {
  476. // needs trailing slash in destination
  477. $target = rtrim($target, '/') . '/';
  478. }
  479. $this->client->request(
  480. 'MOVE',
  481. $this->encodePath($source),
  482. null,
  483. [
  484. 'Destination' => $this->createBaseUri() . $this->encodePath($target),
  485. ]
  486. );
  487. $this->statCache->clear($source . '/');
  488. $this->statCache->clear($target . '/');
  489. $this->statCache->set($source, false);
  490. $this->statCache->set($target, true);
  491. $this->removeCachedFile($source);
  492. $this->removeCachedFile($target);
  493. return true;
  494. } catch (\Exception $e) {
  495. $this->convertException($e);
  496. }
  497. return false;
  498. }
  499. /** {@inheritdoc} */
  500. public function copy($source, $target) {
  501. $this->init();
  502. $source = $this->cleanPath($source);
  503. $target = $this->cleanPath($target);
  504. try {
  505. // overwrite directory ?
  506. if ($this->is_dir($target)) {
  507. // needs trailing slash in destination
  508. $target = rtrim($target, '/') . '/';
  509. }
  510. $this->client->request(
  511. 'COPY',
  512. $this->encodePath($source),
  513. null,
  514. [
  515. 'Destination' => $this->createBaseUri() . $this->encodePath($target),
  516. ]
  517. );
  518. $this->statCache->clear($target . '/');
  519. $this->statCache->set($target, true);
  520. $this->removeCachedFile($target);
  521. return true;
  522. } catch (\Exception $e) {
  523. $this->convertException($e);
  524. }
  525. return false;
  526. }
  527. public function getMetaData($path) {
  528. if (Filesystem::isFileBlacklisted($path)) {
  529. throw new ForbiddenException('Invalid path: ' . $path, false);
  530. }
  531. $response = $this->propfind($path);
  532. if (!$response) {
  533. return null;
  534. } else {
  535. return $this->getMetaFromPropfind($path, $response);
  536. }
  537. }
  538. private function getMetaFromPropfind(string $path, array $response): array {
  539. if (isset($response['{DAV:}getetag'])) {
  540. $etag = trim($response['{DAV:}getetag'], '"');
  541. if (strlen($etag) > 40) {
  542. $etag = md5($etag);
  543. }
  544. } else {
  545. $etag = parent::getETag($path);
  546. }
  547. $responseType = [];
  548. if (isset($response["{DAV:}resourcetype"])) {
  549. /** @var ResourceType[] $response */
  550. $responseType = $response["{DAV:}resourcetype"]->getValue();
  551. }
  552. $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
  553. if ($type === 'dir') {
  554. $mimeType = 'httpd/unix-directory';
  555. } elseif (isset($response['{DAV:}getcontenttype'])) {
  556. $mimeType = $response['{DAV:}getcontenttype'];
  557. } else {
  558. $mimeType = $this->mimeTypeDetector->detectPath($path);
  559. }
  560. if (isset($response['{http://owncloud.org/ns}permissions'])) {
  561. $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
  562. } elseif ($type === 'dir') {
  563. $permissions = Constants::PERMISSION_ALL;
  564. } else {
  565. $permissions = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
  566. }
  567. $mtime = isset($response['{DAV:}getlastmodified']) ? strtotime($response['{DAV:}getlastmodified']) : null;
  568. if ($type === 'dir') {
  569. $size = -1;
  570. } else {
  571. $size = Util::numericToNumber($response['{DAV:}getcontentlength'] ?? 0);
  572. }
  573. return [
  574. 'name' => basename($path),
  575. 'mtime' => $mtime,
  576. 'storage_mtime' => $mtime,
  577. 'size' => $size,
  578. 'permissions' => $permissions,
  579. 'etag' => $etag,
  580. 'mimetype' => $mimeType,
  581. ];
  582. }
  583. /** {@inheritdoc} */
  584. public function stat($path) {
  585. $meta = $this->getMetaData($path);
  586. if (!$meta) {
  587. return false;
  588. } else {
  589. return $meta;
  590. }
  591. }
  592. /** {@inheritdoc} */
  593. public function getMimeType($path) {
  594. $meta = $this->getMetaData($path);
  595. if ($meta) {
  596. return $meta['mimetype'];
  597. } else {
  598. return false;
  599. }
  600. }
  601. /**
  602. * @param string $path
  603. * @return string
  604. */
  605. public function cleanPath($path) {
  606. if ($path === '') {
  607. return $path;
  608. }
  609. $path = Filesystem::normalizePath($path);
  610. // remove leading slash
  611. return substr($path, 1);
  612. }
  613. /**
  614. * URL encodes the given path but keeps the slashes
  615. *
  616. * @param string $path to encode
  617. * @return string encoded path
  618. */
  619. protected function encodePath($path) {
  620. // slashes need to stay
  621. return str_replace('%2F', '/', rawurlencode($path));
  622. }
  623. /**
  624. * @param string $method
  625. * @param string $path
  626. * @param string|resource|null $body
  627. * @param int $expected
  628. * @return bool
  629. * @throws StorageInvalidException
  630. * @throws StorageNotAvailableException
  631. */
  632. protected function simpleResponse($method, $path, $body, $expected) {
  633. $path = $this->cleanPath($path);
  634. try {
  635. $response = $this->client->request($method, $this->encodePath($path), $body);
  636. return $response['statusCode'] == $expected;
  637. } catch (ClientHttpException $e) {
  638. if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
  639. $this->statCache->clear($path . '/');
  640. $this->statCache->set($path, false);
  641. return false;
  642. }
  643. $this->convertException($e, $path);
  644. } catch (\Exception $e) {
  645. $this->convertException($e, $path);
  646. }
  647. return false;
  648. }
  649. /**
  650. * check if curl is installed
  651. */
  652. public static function checkDependencies() {
  653. return true;
  654. }
  655. /** {@inheritdoc} */
  656. public function isUpdatable($path) {
  657. return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
  658. }
  659. /** {@inheritdoc} */
  660. public function isCreatable($path) {
  661. return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
  662. }
  663. /** {@inheritdoc} */
  664. public function isSharable($path) {
  665. return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
  666. }
  667. /** {@inheritdoc} */
  668. public function isDeletable($path) {
  669. return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
  670. }
  671. /** {@inheritdoc} */
  672. public function getPermissions($path) {
  673. $stat = $this->getMetaData($path);
  674. if ($stat) {
  675. return $stat['permissions'];
  676. } else {
  677. return 0;
  678. }
  679. }
  680. /** {@inheritdoc} */
  681. public function getETag($path) {
  682. $meta = $this->getMetaData($path);
  683. if ($meta) {
  684. return $meta['etag'];
  685. } else {
  686. return null;
  687. }
  688. }
  689. /**
  690. * @param string $permissionsString
  691. * @return int
  692. */
  693. protected function parsePermissions($permissionsString) {
  694. $permissions = Constants::PERMISSION_READ;
  695. if (str_contains($permissionsString, 'R')) {
  696. $permissions |= Constants::PERMISSION_SHARE;
  697. }
  698. if (str_contains($permissionsString, 'D')) {
  699. $permissions |= Constants::PERMISSION_DELETE;
  700. }
  701. if (str_contains($permissionsString, 'W')) {
  702. $permissions |= Constants::PERMISSION_UPDATE;
  703. }
  704. if (str_contains($permissionsString, 'CK')) {
  705. $permissions |= Constants::PERMISSION_CREATE;
  706. $permissions |= Constants::PERMISSION_UPDATE;
  707. }
  708. return $permissions;
  709. }
  710. /**
  711. * check if a file or folder has been updated since $time
  712. *
  713. * @param string $path
  714. * @param int $time
  715. * @throws \OCP\Files\StorageNotAvailableException
  716. * @return bool
  717. */
  718. public function hasUpdated($path, $time) {
  719. $this->init();
  720. $path = $this->cleanPath($path);
  721. try {
  722. // force refresh for $path
  723. $this->statCache->remove($path);
  724. $response = $this->propfind($path);
  725. if ($response === false) {
  726. if ($path === '') {
  727. // if root is gone it means the storage is not available
  728. throw new StorageNotAvailableException('root is gone');
  729. }
  730. return false;
  731. }
  732. if (isset($response['{DAV:}getetag'])) {
  733. $cachedData = $this->getCache()->get($path);
  734. $etag = trim($response['{DAV:}getetag'], '"');
  735. if (($cachedData === false) || (!empty($etag) && ($cachedData['etag'] !== $etag))) {
  736. return true;
  737. } elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
  738. $sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
  739. return $sharePermissions !== $cachedData['permissions'];
  740. } elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
  741. $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
  742. return $permissions !== $cachedData['permissions'];
  743. } else {
  744. return false;
  745. }
  746. } elseif (isset($response['{DAV:}getlastmodified'])) {
  747. $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
  748. return $remoteMtime > $time;
  749. } else {
  750. // neither `getetag` nor `getlastmodified` is set
  751. return false;
  752. }
  753. } catch (ClientHttpException $e) {
  754. if ($e->getHttpStatus() === 405) {
  755. if ($path === '') {
  756. // if root is gone it means the storage is not available
  757. throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
  758. }
  759. return false;
  760. }
  761. $this->convertException($e, $path);
  762. return false;
  763. } catch (\Exception $e) {
  764. $this->convertException($e, $path);
  765. return false;
  766. }
  767. }
  768. /**
  769. * Interpret the given exception and decide whether it is due to an
  770. * unavailable storage, invalid storage or other.
  771. * This will either throw StorageInvalidException, StorageNotAvailableException
  772. * or do nothing.
  773. *
  774. * @param Exception $e sabre exception
  775. * @param string $path optional path from the operation
  776. *
  777. * @throws StorageInvalidException if the storage is invalid, for example
  778. * when the authentication expired or is invalid
  779. * @throws StorageNotAvailableException if the storage is not available,
  780. * which might be temporary
  781. * @throws ForbiddenException if the action is not allowed
  782. */
  783. protected function convertException(Exception $e, $path = '') {
  784. \OC::$server->get(LoggerInterface::class)->debug($e->getMessage(), ['app' => 'files_external', 'exception' => $e]);
  785. if ($e instanceof ClientHttpException) {
  786. if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
  787. throw new \OCP\Lock\LockedException($path);
  788. }
  789. if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
  790. // either password was changed or was invalid all along
  791. throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
  792. } elseif ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
  793. // ignore exception for MethodNotAllowed, false will be returned
  794. return;
  795. } elseif ($e->getHttpStatus() === Http::STATUS_FORBIDDEN) {
  796. // The operation is forbidden. Fail somewhat gracefully
  797. throw new ForbiddenException(get_class($e) . ':' . $e->getMessage(), false);
  798. }
  799. throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
  800. } elseif ($e instanceof ClientException) {
  801. // connection timeout or refused, server could be temporarily down
  802. throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
  803. } elseif ($e instanceof \InvalidArgumentException) {
  804. // parse error because the server returned HTML instead of XML,
  805. // possibly temporarily down
  806. throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
  807. } elseif (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
  808. // rethrow
  809. throw $e;
  810. }
  811. // TODO: only log for now, but in the future need to wrap/rethrow exception
  812. }
  813. public function getDirectoryContent($directory): \Traversable {
  814. $this->init();
  815. $directory = $this->cleanPath($directory);
  816. try {
  817. $responses = $this->client->propFind(
  818. $this->encodePath($directory),
  819. self::PROPFIND_PROPS,
  820. 1
  821. );
  822. array_shift($responses); //the first entry is the current directory
  823. if (!$this->statCache->hasKey($directory)) {
  824. $this->statCache->set($directory, true);
  825. }
  826. foreach ($responses as $file => $response) {
  827. $file = rawurldecode($file);
  828. $file = substr($file, strlen($this->root));
  829. $file = $this->cleanPath($file);
  830. $this->statCache->set($file, $response);
  831. yield $this->getMetaFromPropfind($file, $response);
  832. }
  833. } catch (\Exception $e) {
  834. $this->convertException($e, $directory);
  835. }
  836. }
  837. }