DAV.php 24 KB

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