DAV.php 25 KB

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