You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Storage.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bjoern Schiessle <bjoern@schiessle.org>
  6. * @author Björn Schießle <bjoern@schiessle.org>
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  9. * @author Joas Schilling <coding@schilljs.com>
  10. * @author Lukas Reschke <lukas@statuscode.ch>
  11. * @author Morris Jobke <hey@morrisjobke.de>
  12. * @author Robin Appelman <robin@icewind.nl>
  13. * @author Roeland Jago Douma <roeland@famdouma.nl>
  14. * @author Thomas Müller <thomas.mueller@tmit.eu>
  15. * @author Vincent Petry <vincent@nextcloud.com>
  16. *
  17. * @license AGPL-3.0
  18. *
  19. * This code is free software: you can redistribute it and/or modify
  20. * it under the terms of the GNU Affero General Public License, version 3,
  21. * as published by the Free Software Foundation.
  22. *
  23. * This program is distributed in the hope that it will be useful,
  24. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  25. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  26. * GNU Affero General Public License for more details.
  27. *
  28. * You should have received a copy of the GNU Affero General Public License, version 3,
  29. * along with this program. If not, see <http://www.gnu.org/licenses/>
  30. *
  31. */
  32. namespace OCA\Files_Sharing\External;
  33. use GuzzleHttp\Exception\ClientException;
  34. use GuzzleHttp\Exception\ConnectException;
  35. use GuzzleHttp\Exception\RequestException;
  36. use OC\Files\Storage\DAV;
  37. use OC\ForbiddenException;
  38. use OCA\Files_Sharing\ISharedStorage;
  39. use OCP\AppFramework\Http;
  40. use OCP\Constants;
  41. use OCP\Federation\ICloudId;
  42. use OCP\Files\NotFoundException;
  43. use OCP\Files\Storage\IDisableEncryptionStorage;
  44. use OCP\Files\StorageInvalidException;
  45. use OCP\Files\StorageNotAvailableException;
  46. class Storage extends DAV implements ISharedStorage, IDisableEncryptionStorage {
  47. /** @var ICloudId */
  48. private $cloudId;
  49. /** @var string */
  50. private $mountPoint;
  51. /** @var string */
  52. private $token;
  53. /** @var \OCP\ICacheFactory */
  54. private $memcacheFactory;
  55. /** @var \OCP\Http\Client\IClientService */
  56. private $httpClient;
  57. /** @var bool */
  58. private $updateChecked = false;
  59. /**
  60. * @var \OCA\Files_Sharing\External\Manager
  61. */
  62. private $manager;
  63. public function __construct($options) {
  64. $this->memcacheFactory = \OC::$server->getMemCacheFactory();
  65. $this->httpClient = $options['HttpClientService'];
  66. $this->manager = $options['manager'];
  67. $this->cloudId = $options['cloudId'];
  68. $discoveryService = \OC::$server->query(\OCP\OCS\IDiscoveryService::class);
  69. [$protocol, $remote] = explode('://', $this->cloudId->getRemote());
  70. if (strpos($remote, '/')) {
  71. [$host, $root] = explode('/', $remote, 2);
  72. } else {
  73. $host = $remote;
  74. $root = '';
  75. }
  76. $secure = $protocol === 'https';
  77. $federatedSharingEndpoints = $discoveryService->discover($this->cloudId->getRemote(), 'FEDERATED_SHARING');
  78. $webDavEndpoint = isset($federatedSharingEndpoints['webdav']) ? $federatedSharingEndpoints['webdav'] : '/public.php/webdav';
  79. $root = rtrim($root, '/') . $webDavEndpoint;
  80. $this->mountPoint = $options['mountpoint'];
  81. $this->token = $options['token'];
  82. parent::__construct([
  83. 'secure' => $secure,
  84. 'host' => $host,
  85. 'root' => $root,
  86. 'user' => $options['token'],
  87. 'password' => (string)$options['password']
  88. ]);
  89. }
  90. public function getWatcher($path = '', $storage = null) {
  91. if (!$storage) {
  92. $storage = $this;
  93. }
  94. if (!isset($this->watcher)) {
  95. $this->watcher = new Watcher($storage);
  96. $this->watcher->setPolicy(\OC\Files\Cache\Watcher::CHECK_ONCE);
  97. }
  98. return $this->watcher;
  99. }
  100. public function getRemoteUser() {
  101. return $this->cloudId->getUser();
  102. }
  103. public function getRemote() {
  104. return $this->cloudId->getRemote();
  105. }
  106. public function getMountPoint() {
  107. return $this->mountPoint;
  108. }
  109. public function getToken() {
  110. return $this->token;
  111. }
  112. public function getPassword() {
  113. return $this->password;
  114. }
  115. /**
  116. * @brief get id of the mount point
  117. * @return string
  118. */
  119. public function getId() {
  120. return 'shared::' . md5($this->token . '@' . $this->getRemote());
  121. }
  122. public function getCache($path = '', $storage = null) {
  123. if (is_null($this->cache)) {
  124. $this->cache = new Cache($this, $this->cloudId);
  125. }
  126. return $this->cache;
  127. }
  128. /**
  129. * @param string $path
  130. * @param \OC\Files\Storage\Storage $storage
  131. * @return \OCA\Files_Sharing\External\Scanner
  132. */
  133. public function getScanner($path = '', $storage = null) {
  134. if (!$storage) {
  135. $storage = $this;
  136. }
  137. if (!isset($this->scanner)) {
  138. $this->scanner = new Scanner($storage);
  139. }
  140. return $this->scanner;
  141. }
  142. /**
  143. * check if a file or folder has been updated since $time
  144. *
  145. * @param string $path
  146. * @param int $time
  147. * @throws \OCP\Files\StorageNotAvailableException
  148. * @throws \OCP\Files\StorageInvalidException
  149. * @return bool
  150. */
  151. public function hasUpdated($path, $time) {
  152. // since for owncloud webdav servers we can rely on etag propagation we only need to check the root of the storage
  153. // because of that we only do one check for the entire storage per request
  154. if ($this->updateChecked) {
  155. return false;
  156. }
  157. $this->updateChecked = true;
  158. try {
  159. return parent::hasUpdated('', $time);
  160. } catch (StorageInvalidException $e) {
  161. // check if it needs to be removed
  162. $this->checkStorageAvailability();
  163. throw $e;
  164. } catch (StorageNotAvailableException $e) {
  165. // check if it needs to be removed or just temp unavailable
  166. $this->checkStorageAvailability();
  167. throw $e;
  168. }
  169. }
  170. public function test() {
  171. try {
  172. return parent::test();
  173. } catch (StorageInvalidException $e) {
  174. // check if it needs to be removed
  175. $this->checkStorageAvailability();
  176. throw $e;
  177. } catch (StorageNotAvailableException $e) {
  178. // check if it needs to be removed or just temp unavailable
  179. $this->checkStorageAvailability();
  180. throw $e;
  181. }
  182. }
  183. /**
  184. * Check whether this storage is permanently or temporarily
  185. * unavailable
  186. *
  187. * @throws \OCP\Files\StorageNotAvailableException
  188. * @throws \OCP\Files\StorageInvalidException
  189. */
  190. public function checkStorageAvailability() {
  191. // see if we can find out why the share is unavailable
  192. try {
  193. $this->getShareInfo();
  194. } catch (NotFoundException $e) {
  195. // a 404 can either mean that the share no longer exists or there is no Nextcloud on the remote
  196. if ($this->testRemote()) {
  197. // valid Nextcloud instance means that the public share no longer exists
  198. // since this is permanent (re-sharing the file will create a new token)
  199. // we remove the invalid storage
  200. $this->manager->removeShare($this->mountPoint);
  201. $this->manager->getMountManager()->removeMount($this->mountPoint);
  202. throw new StorageInvalidException();
  203. } else {
  204. // Nextcloud instance is gone, likely to be a temporary server configuration error
  205. throw new StorageNotAvailableException();
  206. }
  207. } catch (ForbiddenException $e) {
  208. // auth error, remove share for now (provide a dialog in the future)
  209. $this->manager->removeShare($this->mountPoint);
  210. $this->manager->getMountManager()->removeMount($this->mountPoint);
  211. throw new StorageInvalidException();
  212. } catch (\GuzzleHttp\Exception\ConnectException $e) {
  213. throw new StorageNotAvailableException();
  214. } catch (\GuzzleHttp\Exception\RequestException $e) {
  215. throw new StorageNotAvailableException();
  216. } catch (\Exception $e) {
  217. throw $e;
  218. }
  219. }
  220. public function file_exists($path) {
  221. if ($path === '') {
  222. return true;
  223. } else {
  224. return parent::file_exists($path);
  225. }
  226. }
  227. /**
  228. * check if the configured remote is a valid federated share provider
  229. *
  230. * @return bool
  231. */
  232. protected function testRemote() {
  233. try {
  234. return $this->testRemoteUrl($this->getRemote() . '/ocs-provider/index.php')
  235. || $this->testRemoteUrl($this->getRemote() . '/ocs-provider/')
  236. || $this->testRemoteUrl($this->getRemote() . '/status.php');
  237. } catch (\Exception $e) {
  238. return false;
  239. }
  240. }
  241. /**
  242. * @param string $url
  243. * @return bool
  244. */
  245. private function testRemoteUrl($url) {
  246. $cache = $this->memcacheFactory->createDistributed('files_sharing_remote_url');
  247. if ($cache->hasKey($url)) {
  248. return (bool)$cache->get($url);
  249. }
  250. $client = $this->httpClient->newClient();
  251. try {
  252. $result = $client->get($url, [
  253. 'timeout' => 10,
  254. 'connect_timeout' => 10,
  255. ])->getBody();
  256. $data = json_decode($result);
  257. $returnValue = (is_object($data) && !empty($data->version));
  258. } catch (ConnectException $e) {
  259. $returnValue = false;
  260. } catch (ClientException $e) {
  261. $returnValue = false;
  262. } catch (RequestException $e) {
  263. $returnValue = false;
  264. }
  265. $cache->set($url, $returnValue, 60 * 60 * 24);
  266. return $returnValue;
  267. }
  268. /**
  269. * Whether the remote is an ownCloud/Nextcloud, used since some sharing features are not
  270. * standardized. Let's use this to detect whether to use it.
  271. *
  272. * @return bool
  273. */
  274. public function remoteIsOwnCloud() {
  275. if (defined('PHPUNIT_RUN') || !$this->testRemoteUrl($this->getRemote() . '/status.php')) {
  276. return false;
  277. }
  278. return true;
  279. }
  280. /**
  281. * @return mixed
  282. * @throws ForbiddenException
  283. * @throws NotFoundException
  284. * @throws \Exception
  285. */
  286. public function getShareInfo() {
  287. $remote = $this->getRemote();
  288. $token = $this->getToken();
  289. $password = $this->getPassword();
  290. // If remote is not an ownCloud do not try to get any share info
  291. if (!$this->remoteIsOwnCloud()) {
  292. return ['status' => 'unsupported'];
  293. }
  294. $url = rtrim($remote, '/') . '/index.php/apps/files_sharing/shareinfo?t=' . $token;
  295. // TODO: DI
  296. $client = \OC::$server->getHTTPClientService()->newClient();
  297. try {
  298. $response = $client->post($url, [
  299. 'body' => ['password' => $password],
  300. 'timeout' => 10,
  301. 'connect_timeout' => 10,
  302. ]);
  303. } catch (\GuzzleHttp\Exception\RequestException $e) {
  304. if ($e->getCode() === Http::STATUS_UNAUTHORIZED || $e->getCode() === Http::STATUS_FORBIDDEN) {
  305. throw new ForbiddenException();
  306. }
  307. if ($e->getCode() === Http::STATUS_NOT_FOUND) {
  308. throw new NotFoundException();
  309. }
  310. // throw this to be on the safe side: the share will still be visible
  311. // in the UI in case the failure is intermittent, and the user will
  312. // be able to decide whether to remove it if it's really gone
  313. throw new StorageNotAvailableException();
  314. }
  315. return json_decode($response->getBody(), true);
  316. }
  317. public function getOwner($path) {
  318. return $this->cloudId->getDisplayId();
  319. }
  320. public function isSharable($path) {
  321. if (\OCP\Util::isSharingDisabledForUser() || !\OC\Share\Share::isResharingAllowed()) {
  322. return false;
  323. }
  324. return ($this->getPermissions($path) & Constants::PERMISSION_SHARE);
  325. }
  326. public function getPermissions($path) {
  327. $response = $this->propfind($path);
  328. // old federated sharing permissions
  329. if (isset($response['{http://open-collaboration-services.org/ns}share-permissions'])) {
  330. $permissions = $response['{http://open-collaboration-services.org/ns}share-permissions'];
  331. } elseif (isset($response['{http://open-cloud-mesh.org/ns}share-permissions'])) {
  332. // permissions provided by the OCM API
  333. $permissions = $this->ocmPermissions2ncPermissions($response['{http://open-collaboration-services.org/ns}share-permissions'], $path);
  334. } else {
  335. // use default permission if remote server doesn't provide the share permissions
  336. $permissions = $this->getDefaultPermissions($path);
  337. }
  338. return $permissions;
  339. }
  340. public function needsPartFile() {
  341. return false;
  342. }
  343. /**
  344. * translate OCM Permissions to Nextcloud permissions
  345. *
  346. * @param string $ocmPermissions json encoded OCM permissions
  347. * @param string $path path to file
  348. * @return int
  349. */
  350. protected function ocmPermissions2ncPermissions($ocmPermissions, $path) {
  351. try {
  352. $ocmPermissions = json_decode($ocmPermissions);
  353. $ncPermissions = 0;
  354. foreach ($ocmPermissions as $permission) {
  355. switch (strtolower($permission)) {
  356. case 'read':
  357. $ncPermissions += Constants::PERMISSION_READ;
  358. break;
  359. case 'write':
  360. $ncPermissions += Constants::PERMISSION_CREATE + Constants::PERMISSION_UPDATE;
  361. break;
  362. case 'share':
  363. $ncPermissions += Constants::PERMISSION_SHARE;
  364. break;
  365. default:
  366. throw new \Exception();
  367. }
  368. }
  369. } catch (\Exception $e) {
  370. $ncPermissions = $this->getDefaultPermissions($path);
  371. }
  372. return $ncPermissions;
  373. }
  374. /**
  375. * calculate default permissions in case no permissions are provided
  376. *
  377. * @param $path
  378. * @return int
  379. */
  380. protected function getDefaultPermissions($path) {
  381. if ($this->is_dir($path)) {
  382. $permissions = Constants::PERMISSION_ALL;
  383. } else {
  384. $permissions = Constants::PERMISSION_ALL & ~Constants::PERMISSION_CREATE;
  385. }
  386. return $permissions;
  387. }
  388. }