Storage.php 10 KB

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