Storage.php 11 KB

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