1
0

DAV.php 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898
  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. try {
  238. $response = $this->client->propFind(
  239. $this->encodePath($path),
  240. self::PROPFIND_PROPS
  241. );
  242. $this->statCache->set($path, $response);
  243. } catch (ClientHttpException $e) {
  244. if ($e->getHttpStatus() === 404 || $e->getHttpStatus() === 405) {
  245. $this->statCache->clear($path . '/');
  246. $this->statCache->set($path, false);
  247. return false;
  248. }
  249. $this->convertException($e, $path);
  250. } catch (\Exception $e) {
  251. $this->convertException($e, $path);
  252. }
  253. } else {
  254. $response = $cachedResponse;
  255. }
  256. return $response;
  257. }
  258. /** {@inheritdoc} */
  259. public function filetype($path) {
  260. try {
  261. $response = $this->propfind($path);
  262. if ($response === false) {
  263. return false;
  264. }
  265. $responseType = [];
  266. if (isset($response["{DAV:}resourcetype"])) {
  267. /** @var ResourceType[] $response */
  268. $responseType = $response["{DAV:}resourcetype"]->getValue();
  269. }
  270. return (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
  271. } catch (\Exception $e) {
  272. $this->convertException($e, $path);
  273. }
  274. return false;
  275. }
  276. /** {@inheritdoc} */
  277. public function file_exists($path) {
  278. try {
  279. $path = $this->cleanPath($path);
  280. $cachedState = $this->statCache->get($path);
  281. if ($cachedState === false) {
  282. // we know the file doesn't exist
  283. return false;
  284. } elseif (!is_null($cachedState)) {
  285. return true;
  286. }
  287. // need to get from server
  288. return ($this->propfind($path) !== false);
  289. } catch (\Exception $e) {
  290. $this->convertException($e, $path);
  291. }
  292. return false;
  293. }
  294. /** {@inheritdoc} */
  295. public function unlink($path) {
  296. $this->init();
  297. $path = $this->cleanPath($path);
  298. $result = $this->simpleResponse('DELETE', $path, null, 204);
  299. $this->statCache->clear($path . '/');
  300. $this->statCache->remove($path);
  301. return $result;
  302. }
  303. /** {@inheritdoc} */
  304. public function fopen($path, $mode) {
  305. $this->init();
  306. $path = $this->cleanPath($path);
  307. switch ($mode) {
  308. case 'r':
  309. case 'rb':
  310. try {
  311. $response = $this->httpClientService
  312. ->newClient()
  313. ->get($this->createBaseUri() . $this->encodePath($path), [
  314. 'auth' => [$this->user, $this->password],
  315. 'stream' => true,
  316. // set download timeout for users with slow connections or large files
  317. 'timeout' => $this->timeout
  318. ]);
  319. } catch (\GuzzleHttp\Exception\ClientException $e) {
  320. if ($e->getResponse() instanceof ResponseInterface
  321. && $e->getResponse()->getStatusCode() === 404) {
  322. return false;
  323. } else {
  324. throw $e;
  325. }
  326. }
  327. if ($response->getStatusCode() !== Http::STATUS_OK) {
  328. if ($response->getStatusCode() === Http::STATUS_LOCKED) {
  329. throw new \OCP\Lock\LockedException($path);
  330. } else {
  331. \OC::$server->get(LoggerInterface::class)->error('Guzzle get returned status code ' . $response->getStatusCode(), ['app' => 'webdav client']);
  332. }
  333. }
  334. return $response->getBody();
  335. case 'w':
  336. case 'wb':
  337. case 'a':
  338. case 'ab':
  339. case 'r+':
  340. case 'w+':
  341. case 'wb+':
  342. case 'a+':
  343. case 'x':
  344. case 'x+':
  345. case 'c':
  346. case 'c+':
  347. //emulate these
  348. $tempManager = \OC::$server->getTempManager();
  349. if (strrpos($path, '.') !== false) {
  350. $ext = substr($path, strrpos($path, '.'));
  351. } else {
  352. $ext = '';
  353. }
  354. if ($this->file_exists($path)) {
  355. if (!$this->isUpdatable($path)) {
  356. return false;
  357. }
  358. if ($mode === 'w' or $mode === 'w+') {
  359. $tmpFile = $tempManager->getTemporaryFile($ext);
  360. } else {
  361. $tmpFile = $this->getCachedFile($path);
  362. }
  363. } else {
  364. if (!$this->isCreatable(dirname($path))) {
  365. return false;
  366. }
  367. $tmpFile = $tempManager->getTemporaryFile($ext);
  368. }
  369. $handle = fopen($tmpFile, $mode);
  370. return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile) {
  371. $this->writeBack($tmpFile, $path);
  372. });
  373. }
  374. }
  375. /**
  376. * @param string $tmpFile
  377. */
  378. public function writeBack($tmpFile, $path) {
  379. $this->uploadFile($tmpFile, $path);
  380. unlink($tmpFile);
  381. }
  382. /** {@inheritdoc} */
  383. public function free_space($path) {
  384. $this->init();
  385. $path = $this->cleanPath($path);
  386. try {
  387. $response = $this->propfind($path);
  388. if ($response === false) {
  389. return FileInfo::SPACE_UNKNOWN;
  390. }
  391. if (isset($response['{DAV:}quota-available-bytes'])) {
  392. return Util::numericToNumber($response['{DAV:}quota-available-bytes']);
  393. } else {
  394. return FileInfo::SPACE_UNKNOWN;
  395. }
  396. } catch (\Exception $e) {
  397. return FileInfo::SPACE_UNKNOWN;
  398. }
  399. }
  400. /** {@inheritdoc} */
  401. public function touch($path, $mtime = null) {
  402. $this->init();
  403. if (is_null($mtime)) {
  404. $mtime = time();
  405. }
  406. $path = $this->cleanPath($path);
  407. // if file exists, update the mtime, else create a new empty file
  408. if ($this->file_exists($path)) {
  409. try {
  410. $this->statCache->remove($path);
  411. $this->client->proppatch($this->encodePath($path), ['{DAV:}lastmodified' => $mtime]);
  412. // non-owncloud clients might not have accepted the property, need to recheck it
  413. $response = $this->client->propfind($this->encodePath($path), ['{DAV:}getlastmodified'], 0);
  414. if (isset($response['{DAV:}getlastmodified'])) {
  415. $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
  416. if ($remoteMtime !== $mtime) {
  417. // server has not accepted the mtime
  418. return false;
  419. }
  420. }
  421. } catch (ClientHttpException $e) {
  422. if ($e->getHttpStatus() === 501) {
  423. return false;
  424. }
  425. $this->convertException($e, $path);
  426. return false;
  427. } catch (\Exception $e) {
  428. $this->convertException($e, $path);
  429. return false;
  430. }
  431. } else {
  432. $this->file_put_contents($path, '');
  433. }
  434. return true;
  435. }
  436. /**
  437. * @param string $path
  438. * @param mixed $data
  439. * @return int|float|false
  440. */
  441. public function file_put_contents($path, $data) {
  442. $path = $this->cleanPath($path);
  443. $result = parent::file_put_contents($path, $data);
  444. $this->statCache->remove($path);
  445. return $result;
  446. }
  447. /**
  448. * @param string $path
  449. * @param string $target
  450. */
  451. protected function uploadFile($path, $target) {
  452. $this->init();
  453. // invalidate
  454. $target = $this->cleanPath($target);
  455. $this->statCache->remove($target);
  456. $source = fopen($path, 'r');
  457. $this->httpClientService
  458. ->newClient()
  459. ->put($this->createBaseUri() . $this->encodePath($target), [
  460. 'body' => $source,
  461. 'auth' => [$this->user, $this->password],
  462. // set upload timeout for users with slow connections or large files
  463. 'timeout' => $this->timeout
  464. ]);
  465. $this->removeCachedFile($target);
  466. }
  467. /** {@inheritdoc} */
  468. public function rename($source, $target) {
  469. $this->init();
  470. $source = $this->cleanPath($source);
  471. $target = $this->cleanPath($target);
  472. try {
  473. // overwrite directory ?
  474. if ($this->is_dir($target)) {
  475. // needs trailing slash in destination
  476. $target = rtrim($target, '/') . '/';
  477. }
  478. $this->client->request(
  479. 'MOVE',
  480. $this->encodePath($source),
  481. null,
  482. [
  483. 'Destination' => $this->createBaseUri() . $this->encodePath($target),
  484. ]
  485. );
  486. $this->statCache->clear($source . '/');
  487. $this->statCache->clear($target . '/');
  488. $this->statCache->set($source, false);
  489. $this->statCache->set($target, true);
  490. $this->removeCachedFile($source);
  491. $this->removeCachedFile($target);
  492. return true;
  493. } catch (\Exception $e) {
  494. $this->convertException($e);
  495. }
  496. return false;
  497. }
  498. /** {@inheritdoc} */
  499. public function copy($source, $target) {
  500. $this->init();
  501. $source = $this->cleanPath($source);
  502. $target = $this->cleanPath($target);
  503. try {
  504. // overwrite directory ?
  505. if ($this->is_dir($target)) {
  506. // needs trailing slash in destination
  507. $target = rtrim($target, '/') . '/';
  508. }
  509. $this->client->request(
  510. 'COPY',
  511. $this->encodePath($source),
  512. null,
  513. [
  514. 'Destination' => $this->createBaseUri() . $this->encodePath($target),
  515. ]
  516. );
  517. $this->statCache->clear($target . '/');
  518. $this->statCache->set($target, true);
  519. $this->removeCachedFile($target);
  520. return true;
  521. } catch (\Exception $e) {
  522. $this->convertException($e);
  523. }
  524. return false;
  525. }
  526. public function getMetaData($path) {
  527. if (Filesystem::isFileBlacklisted($path)) {
  528. throw new ForbiddenException('Invalid path: ' . $path, false);
  529. }
  530. $response = $this->propfind($path);
  531. if (!$response) {
  532. return null;
  533. } else {
  534. return $this->getMetaFromPropfind($path, $response);
  535. }
  536. }
  537. private function getMetaFromPropfind(string $path, array $response): array {
  538. if (isset($response['{DAV:}getetag'])) {
  539. $etag = trim($response['{DAV:}getetag'], '"');
  540. if (strlen($etag) > 40) {
  541. $etag = md5($etag);
  542. }
  543. } else {
  544. $etag = parent::getETag($path);
  545. }
  546. $responseType = [];
  547. if (isset($response["{DAV:}resourcetype"])) {
  548. /** @var ResourceType[] $response */
  549. $responseType = $response["{DAV:}resourcetype"]->getValue();
  550. }
  551. $type = (count($responseType) > 0 and $responseType[0] == "{DAV:}collection") ? 'dir' : 'file';
  552. if ($type === 'dir') {
  553. $mimeType = 'httpd/unix-directory';
  554. } elseif (isset($response['{DAV:}getcontenttype'])) {
  555. $mimeType = $response['{DAV:}getcontenttype'];
  556. } else {
  557. $mimeType = $this->mimeTypeDetector->detectPath($path);
  558. }
  559. if (isset($response['{http://owncloud.org/ns}permissions'])) {
  560. $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
  561. } elseif ($type === 'dir') {
  562. $permissions = Constants::PERMISSION_ALL;
  563. } else {
  564. $permissions = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
  565. }
  566. $mtime = isset($response['{DAV:}getlastmodified']) ? strtotime($response['{DAV:}getlastmodified']) : null;
  567. if ($type === 'dir') {
  568. $size = -1;
  569. } else {
  570. $size = Util::numericToNumber($response['{DAV:}getcontentlength'] ?? 0);
  571. }
  572. return [
  573. 'name' => basename($path),
  574. 'mtime' => $mtime,
  575. 'storage_mtime' => $mtime,
  576. 'size' => $size,
  577. 'permissions' => $permissions,
  578. 'etag' => $etag,
  579. 'mimetype' => $mimeType,
  580. ];
  581. }
  582. /** {@inheritdoc} */
  583. public function stat($path) {
  584. $meta = $this->getMetaData($path);
  585. if (!$meta) {
  586. return false;
  587. } else {
  588. return $meta;
  589. }
  590. }
  591. /** {@inheritdoc} */
  592. public function getMimeType($path) {
  593. $meta = $this->getMetaData($path);
  594. if ($meta) {
  595. return $meta['mimetype'];
  596. } else {
  597. return false;
  598. }
  599. }
  600. /**
  601. * @param string $path
  602. * @return string
  603. */
  604. public function cleanPath($path) {
  605. if ($path === '') {
  606. return $path;
  607. }
  608. $path = Filesystem::normalizePath($path);
  609. // remove leading slash
  610. return substr($path, 1);
  611. }
  612. /**
  613. * URL encodes the given path but keeps the slashes
  614. *
  615. * @param string $path to encode
  616. * @return string encoded path
  617. */
  618. protected function encodePath($path) {
  619. // slashes need to stay
  620. return str_replace('%2F', '/', rawurlencode($path));
  621. }
  622. /**
  623. * @param string $method
  624. * @param string $path
  625. * @param string|resource|null $body
  626. * @param int $expected
  627. * @return bool
  628. * @throws StorageInvalidException
  629. * @throws StorageNotAvailableException
  630. */
  631. protected function simpleResponse($method, $path, $body, $expected) {
  632. $path = $this->cleanPath($path);
  633. try {
  634. $response = $this->client->request($method, $this->encodePath($path), $body);
  635. return $response['statusCode'] == $expected;
  636. } catch (ClientHttpException $e) {
  637. if ($e->getHttpStatus() === 404 && $method === 'DELETE') {
  638. $this->statCache->clear($path . '/');
  639. $this->statCache->set($path, false);
  640. return false;
  641. }
  642. $this->convertException($e, $path);
  643. } catch (\Exception $e) {
  644. $this->convertException($e, $path);
  645. }
  646. return false;
  647. }
  648. /**
  649. * check if curl is installed
  650. */
  651. public static function checkDependencies() {
  652. return true;
  653. }
  654. /** {@inheritdoc} */
  655. public function isUpdatable($path) {
  656. return (bool)($this->getPermissions($path) & Constants::PERMISSION_UPDATE);
  657. }
  658. /** {@inheritdoc} */
  659. public function isCreatable($path) {
  660. return (bool)($this->getPermissions($path) & Constants::PERMISSION_CREATE);
  661. }
  662. /** {@inheritdoc} */
  663. public function isSharable($path) {
  664. return (bool)($this->getPermissions($path) & Constants::PERMISSION_SHARE);
  665. }
  666. /** {@inheritdoc} */
  667. public function isDeletable($path) {
  668. return (bool)($this->getPermissions($path) & Constants::PERMISSION_DELETE);
  669. }
  670. /** {@inheritdoc} */
  671. public function getPermissions($path) {
  672. $stat = $this->getMetaData($path);
  673. if ($stat) {
  674. return $stat['permissions'];
  675. } else {
  676. return 0;
  677. }
  678. }
  679. /** {@inheritdoc} */
  680. public function getETag($path) {
  681. $meta = $this->getMetaData($path);
  682. if ($meta) {
  683. return $meta['etag'];
  684. } else {
  685. return null;
  686. }
  687. }
  688. /**
  689. * @param string $permissionsString
  690. * @return int
  691. */
  692. protected function parsePermissions($permissionsString) {
  693. $permissions = Constants::PERMISSION_READ;
  694. if (str_contains($permissionsString, 'R')) {
  695. $permissions |= Constants::PERMISSION_SHARE;
  696. }
  697. if (str_contains($permissionsString, 'D')) {
  698. $permissions |= Constants::PERMISSION_DELETE;
  699. }
  700. if (str_contains($permissionsString, 'W')) {
  701. $permissions |= Constants::PERMISSION_UPDATE;
  702. }
  703. if (str_contains($permissionsString, 'CK')) {
  704. $permissions |= Constants::PERMISSION_CREATE;
  705. $permissions |= Constants::PERMISSION_UPDATE;
  706. }
  707. return $permissions;
  708. }
  709. /**
  710. * check if a file or folder has been updated since $time
  711. *
  712. * @param string $path
  713. * @param int $time
  714. * @throws \OCP\Files\StorageNotAvailableException
  715. * @return bool
  716. */
  717. public function hasUpdated($path, $time) {
  718. $this->init();
  719. $path = $this->cleanPath($path);
  720. try {
  721. // force refresh for $path
  722. $this->statCache->remove($path);
  723. $response = $this->propfind($path);
  724. if ($response === false) {
  725. if ($path === '') {
  726. // if root is gone it means the storage is not available
  727. throw new StorageNotAvailableException('root is gone');
  728. }
  729. return false;
  730. }
  731. if (isset($response['{DAV:}getetag'])) {
  732. $cachedData = $this->getCache()->get($path);
  733. $etag = trim($response['{DAV:}getetag'], '"');
  734. if (($cachedData === false) || (!empty($etag) && ($cachedData['etag'] !== $etag))) {
  735. return true;
  736. } elseif (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
  737. $sharePermissions = (int)$response['{http://open-collaboration-services.org/ns}share-permissions'];
  738. return $sharePermissions !== $cachedData['permissions'];
  739. } elseif (isset($response['{http://owncloud.org/ns}permissions'])) {
  740. $permissions = $this->parsePermissions($response['{http://owncloud.org/ns}permissions']);
  741. return $permissions !== $cachedData['permissions'];
  742. } else {
  743. return false;
  744. }
  745. } elseif (isset($response['{DAV:}getlastmodified'])) {
  746. $remoteMtime = strtotime($response['{DAV:}getlastmodified']);
  747. return $remoteMtime > $time;
  748. } else {
  749. // neither `getetag` nor `getlastmodified` is set
  750. return false;
  751. }
  752. } catch (ClientHttpException $e) {
  753. if ($e->getHttpStatus() === 405) {
  754. if ($path === '') {
  755. // if root is gone it means the storage is not available
  756. throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
  757. }
  758. return false;
  759. }
  760. $this->convertException($e, $path);
  761. return false;
  762. } catch (\Exception $e) {
  763. $this->convertException($e, $path);
  764. return false;
  765. }
  766. }
  767. /**
  768. * Interpret the given exception and decide whether it is due to an
  769. * unavailable storage, invalid storage or other.
  770. * This will either throw StorageInvalidException, StorageNotAvailableException
  771. * or do nothing.
  772. *
  773. * @param Exception $e sabre exception
  774. * @param string $path optional path from the operation
  775. *
  776. * @throws StorageInvalidException if the storage is invalid, for example
  777. * when the authentication expired or is invalid
  778. * @throws StorageNotAvailableException if the storage is not available,
  779. * which might be temporary
  780. * @throws ForbiddenException if the action is not allowed
  781. */
  782. protected function convertException(Exception $e, $path = '') {
  783. \OC::$server->get(LoggerInterface::class)->debug($e->getMessage(), ['app' => 'files_external', 'exception' => $e]);
  784. if ($e instanceof ClientHttpException) {
  785. if ($e->getHttpStatus() === Http::STATUS_LOCKED) {
  786. throw new \OCP\Lock\LockedException($path);
  787. }
  788. if ($e->getHttpStatus() === Http::STATUS_UNAUTHORIZED) {
  789. // either password was changed or was invalid all along
  790. throw new StorageInvalidException(get_class($e) . ': ' . $e->getMessage());
  791. } elseif ($e->getHttpStatus() === Http::STATUS_METHOD_NOT_ALLOWED) {
  792. // ignore exception for MethodNotAllowed, false will be returned
  793. return;
  794. } elseif ($e->getHttpStatus() === Http::STATUS_FORBIDDEN) {
  795. // The operation is forbidden. Fail somewhat gracefully
  796. throw new ForbiddenException(get_class($e) . ':' . $e->getMessage(), false);
  797. }
  798. throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
  799. } elseif ($e instanceof ClientException) {
  800. // connection timeout or refused, server could be temporarily down
  801. throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
  802. } elseif ($e instanceof \InvalidArgumentException) {
  803. // parse error because the server returned HTML instead of XML,
  804. // possibly temporarily down
  805. throw new StorageNotAvailableException(get_class($e) . ': ' . $e->getMessage());
  806. } elseif (($e instanceof StorageNotAvailableException) || ($e instanceof StorageInvalidException)) {
  807. // rethrow
  808. throw $e;
  809. }
  810. // TODO: only log for now, but in the future need to wrap/rethrow exception
  811. }
  812. public function getDirectoryContent($directory): \Traversable {
  813. $this->init();
  814. $directory = $this->cleanPath($directory);
  815. try {
  816. $responses = $this->client->propFind(
  817. $this->encodePath($directory),
  818. self::PROPFIND_PROPS,
  819. 1
  820. );
  821. array_shift($responses); //the first entry is the current directory
  822. if (!$this->statCache->hasKey($directory)) {
  823. $this->statCache->set($directory, true);
  824. }
  825. foreach ($responses as $file => $response) {
  826. $file = rawurldecode($file);
  827. $file = substr($file, strlen($this->root));
  828. $file = $this->cleanPath($file);
  829. $this->statCache->set($file, $response);
  830. yield $this->getMetaFromPropfind($file, $response);
  831. }
  832. } catch (\Exception $e) {
  833. $this->convertException($e, $directory);
  834. }
  835. }
  836. }