DAV.php 23 KB

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