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.

Manager.php 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  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 Hansson <daniel@techandme.se>
  9. * @author Joas Schilling <coding@schilljs.com>
  10. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  11. * @author Julius Härtl <jus@bitgrid.net>
  12. * @author Lukas Reschke <lukas@statuscode.ch>
  13. * @author Morris Jobke <hey@morrisjobke.de>
  14. * @author Robin Appelman <robin@icewind.nl>
  15. * @author Roeland Jago Douma <roeland@famdouma.nl>
  16. * @author Stefan Weil <sw@weilnetz.de>
  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 OCA\Files_Sharing\External;
  34. use OC\Files\Filesystem;
  35. use OCA\FederatedFileSharing\Events\FederatedShareAddedEvent;
  36. use OCA\Files_Sharing\Helper;
  37. use OCP\EventDispatcher\IEventDispatcher;
  38. use OCP\Federation\ICloudFederationFactory;
  39. use OCP\Federation\ICloudFederationProviderManager;
  40. use OCP\Files;
  41. use OCP\Files\Storage\IStorageFactory;
  42. use OCP\Http\Client\IClientService;
  43. use OCP\IDBConnection;
  44. use OCP\IGroupManager;
  45. use OCP\IUserManager;
  46. use OCP\Notification\IManager;
  47. use OCP\OCS\IDiscoveryService;
  48. use OCP\Share;
  49. use OCP\Share\IShare;
  50. class Manager {
  51. public const STORAGE = '\OCA\Files_Sharing\External\Storage';
  52. /** @var string|null */
  53. private $uid;
  54. /** @var IDBConnection */
  55. private $connection;
  56. /** @var \OC\Files\Mount\Manager */
  57. private $mountManager;
  58. /** @var IStorageFactory */
  59. private $storageLoader;
  60. /** @var IClientService */
  61. private $clientService;
  62. /** @var IManager */
  63. private $notificationManager;
  64. /** @var IDiscoveryService */
  65. private $discoveryService;
  66. /** @var ICloudFederationProviderManager */
  67. private $cloudFederationProviderManager;
  68. /** @var ICloudFederationFactory */
  69. private $cloudFederationFactory;
  70. /** @var IGroupManager */
  71. private $groupManager;
  72. /** @var IUserManager */
  73. private $userManager;
  74. /** @var IEventDispatcher */
  75. private $eventDispatcher;
  76. public function __construct(IDBConnection $connection,
  77. \OC\Files\Mount\Manager $mountManager,
  78. IStorageFactory $storageLoader,
  79. IClientService $clientService,
  80. IManager $notificationManager,
  81. IDiscoveryService $discoveryService,
  82. ICloudFederationProviderManager $cloudFederationProviderManager,
  83. ICloudFederationFactory $cloudFederationFactory,
  84. IGroupManager $groupManager,
  85. IUserManager $userManager,
  86. ?string $uid,
  87. IEventDispatcher $eventDispatcher) {
  88. $this->connection = $connection;
  89. $this->mountManager = $mountManager;
  90. $this->storageLoader = $storageLoader;
  91. $this->clientService = $clientService;
  92. $this->uid = $uid;
  93. $this->notificationManager = $notificationManager;
  94. $this->discoveryService = $discoveryService;
  95. $this->cloudFederationProviderManager = $cloudFederationProviderManager;
  96. $this->cloudFederationFactory = $cloudFederationFactory;
  97. $this->groupManager = $groupManager;
  98. $this->userManager = $userManager;
  99. $this->eventDispatcher = $eventDispatcher;
  100. }
  101. /**
  102. * add new server-to-server share
  103. *
  104. * @param string $remote
  105. * @param string $token
  106. * @param string $password
  107. * @param string $name
  108. * @param string $owner
  109. * @param int $shareType
  110. * @param boolean $accepted
  111. * @param string $user
  112. * @param int $remoteId
  113. * @param int $parent
  114. * @return Mount|null
  115. * @throws \Doctrine\DBAL\DBALException
  116. */
  117. public function addShare($remote, $token, $password, $name, $owner, $shareType, $accepted=false, $user = null, $remoteId = -1, $parent = -1) {
  118. $user = $user ? $user : $this->uid;
  119. $accepted = $accepted ? IShare::STATUS_ACCEPTED : IShare::STATUS_PENDING;
  120. $name = Filesystem::normalizePath('/' . $name);
  121. if ($accepted !== IShare::STATUS_ACCEPTED) {
  122. // To avoid conflicts with the mount point generation later,
  123. // we only use a temporary mount point name here. The real
  124. // mount point name will be generated when accepting the share,
  125. // using the original share item name.
  126. $tmpMountPointName = '{{TemporaryMountPointName#' . $name . '}}';
  127. $mountPoint = $tmpMountPointName;
  128. $hash = md5($tmpMountPointName);
  129. $data = [
  130. 'remote' => $remote,
  131. 'share_token' => $token,
  132. 'password' => $password,
  133. 'name' => $name,
  134. 'owner' => $owner,
  135. 'user' => $user,
  136. 'mountpoint' => $mountPoint,
  137. 'mountpoint_hash' => $hash,
  138. 'accepted' => $accepted,
  139. 'remote_id' => $remoteId,
  140. 'share_type' => $shareType,
  141. ];
  142. $i = 1;
  143. while (!$this->connection->insertIfNotExist('*PREFIX*share_external', $data, ['user', 'mountpoint_hash'])) {
  144. // The external share already exists for the user
  145. $data['mountpoint'] = $tmpMountPointName . '-' . $i;
  146. $data['mountpoint_hash'] = md5($data['mountpoint']);
  147. $i++;
  148. }
  149. return null;
  150. }
  151. $mountPoint = Files::buildNotExistingFileName('/', $name);
  152. $mountPoint = Filesystem::normalizePath('/' . $mountPoint);
  153. $hash = md5($mountPoint);
  154. $this->writeShareToDb($remote, $token, $password, $name, $owner, $user, $mountPoint, $hash, $accepted, $remoteId, $parent, $shareType);
  155. $options = [
  156. 'remote' => $remote,
  157. 'token' => $token,
  158. 'password' => $password,
  159. 'mountpoint' => $mountPoint,
  160. 'owner' => $owner
  161. ];
  162. return $this->mountShare($options);
  163. }
  164. /**
  165. * write remote share to the database
  166. *
  167. * @param $remote
  168. * @param $token
  169. * @param $password
  170. * @param $name
  171. * @param $owner
  172. * @param $user
  173. * @param $mountPoint
  174. * @param $hash
  175. * @param $accepted
  176. * @param $remoteId
  177. * @param $parent
  178. * @param $shareType
  179. * @return bool
  180. */
  181. private function writeShareToDb($remote, $token, $password, $name, $owner, $user, $mountPoint, $hash, $accepted, $remoteId, $parent, $shareType) {
  182. $query = $this->connection->prepare('
  183. INSERT INTO `*PREFIX*share_external`
  184. (`remote`, `share_token`, `password`, `name`, `owner`, `user`, `mountpoint`, `mountpoint_hash`, `accepted`, `remote_id`, `parent`, `share_type`)
  185. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  186. ');
  187. return $query->execute([$remote, $token, $password, $name, $owner, $user, $mountPoint, $hash, $accepted, $remoteId, $parent, $shareType]);
  188. }
  189. /**
  190. * get share
  191. *
  192. * @param int $id share id
  193. * @return mixed share of false
  194. */
  195. public function getShare($id) {
  196. $getShare = $this->connection->prepare('
  197. SELECT `id`, `remote`, `remote_id`, `share_token`, `name`, `owner`, `user`, `mountpoint`, `accepted`, `parent`, `share_type`, `password`, `mountpoint_hash`
  198. FROM `*PREFIX*share_external`
  199. WHERE `id` = ?');
  200. $result = $getShare->execute([$id]);
  201. $share = $result ? $getShare->fetch() : [];
  202. $validShare = is_array($share) && isset($share['share_type']) && isset($share['user']);
  203. // check if the user is allowed to access it
  204. if ($validShare && (int)$share['share_type'] === IShare::TYPE_USER && $share['user'] === $this->uid) {
  205. return $share;
  206. } elseif ($validShare && (int)$share['share_type'] === IShare::TYPE_GROUP) {
  207. $user = $this->userManager->get($this->uid);
  208. if ($this->groupManager->get($share['user'])->inGroup($user)) {
  209. return $share;
  210. }
  211. }
  212. return false;
  213. }
  214. /**
  215. * accept server-to-server share
  216. *
  217. * @param int $id
  218. * @return bool True if the share could be accepted, false otherwise
  219. */
  220. public function acceptShare($id) {
  221. $share = $this->getShare($id);
  222. $result = false;
  223. if ($share) {
  224. \OC_Util::setupFS($this->uid);
  225. $shareFolder = Helper::getShareFolder();
  226. $mountPoint = Files::buildNotExistingFileName($shareFolder, $share['name']);
  227. $mountPoint = Filesystem::normalizePath($mountPoint);
  228. $hash = md5($mountPoint);
  229. $userShareAccepted = false;
  230. if ((int)$share['share_type'] === IShare::TYPE_USER) {
  231. $acceptShare = $this->connection->prepare('
  232. UPDATE `*PREFIX*share_external`
  233. SET `accepted` = ?,
  234. `mountpoint` = ?,
  235. `mountpoint_hash` = ?
  236. WHERE `id` = ? AND `user` = ?');
  237. $userShareAccepted = $acceptShare->execute([1, $mountPoint, $hash, $id, $this->uid]);
  238. } else {
  239. $result = $this->writeShareToDb(
  240. $share['remote'],
  241. $share['share_token'],
  242. $share['password'],
  243. $share['name'],
  244. $share['owner'],
  245. $this->uid,
  246. $mountPoint, $hash, 1,
  247. $share['remote_id'],
  248. $id,
  249. $share['share_type']);
  250. }
  251. if ($userShareAccepted === true) {
  252. $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'accept');
  253. $event = new FederatedShareAddedEvent($share['remote']);
  254. $this->eventDispatcher->dispatchTyped($event);
  255. $result = true;
  256. }
  257. }
  258. // Make sure the user has no notification for something that does not exist anymore.
  259. $this->processNotification($id);
  260. return $result;
  261. }
  262. /**
  263. * decline server-to-server share
  264. *
  265. * @param int $id
  266. * @return bool True if the share could be declined, false otherwise
  267. */
  268. public function declineShare($id) {
  269. $share = $this->getShare($id);
  270. $result = false;
  271. if ($share && (int)$share['share_type'] === IShare::TYPE_USER) {
  272. $removeShare = $this->connection->prepare('
  273. DELETE FROM `*PREFIX*share_external` WHERE `id` = ? AND `user` = ?');
  274. $removeShare->execute([$id, $this->uid]);
  275. $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline');
  276. $this->processNotification($id);
  277. $result = true;
  278. } elseif ($share && (int)$share['share_type'] === IShare::TYPE_GROUP) {
  279. $result = $this->writeShareToDb(
  280. $share['remote'],
  281. $share['share_token'],
  282. $share['password'],
  283. $share['name'],
  284. $share['owner'],
  285. $this->uid,
  286. $share['mountpoint'],
  287. $share['mountpoint_hash'],
  288. 0,
  289. $share['remote_id'],
  290. $id,
  291. $share['share_type']);
  292. $this->processNotification($id);
  293. }
  294. return $result;
  295. }
  296. /**
  297. * @param int $remoteShare
  298. */
  299. public function processNotification($remoteShare) {
  300. $filter = $this->notificationManager->createNotification();
  301. $filter->setApp('files_sharing')
  302. ->setUser($this->uid)
  303. ->setObject('remote_share', (int) $remoteShare);
  304. $this->notificationManager->markProcessed($filter);
  305. }
  306. /**
  307. * inform remote server whether server-to-server share was accepted/declined
  308. *
  309. * @param string $remote
  310. * @param string $token
  311. * @param int $remoteId Share id on the remote host
  312. * @param string $feedback
  313. * @return boolean
  314. */
  315. private function sendFeedbackToRemote($remote, $token, $remoteId, $feedback) {
  316. $result = $this->tryOCMEndPoint($remote, $token, $remoteId, $feedback);
  317. if ($result === true) {
  318. return true;
  319. }
  320. $federationEndpoints = $this->discoveryService->discover($remote, 'FEDERATED_SHARING');
  321. $endpoint = isset($federationEndpoints['share']) ? $federationEndpoints['share'] : '/ocs/v2.php/cloud/shares';
  322. $url = rtrim($remote, '/') . $endpoint . '/' . $remoteId . '/' . $feedback . '?format=' . Share::RESPONSE_FORMAT;
  323. $fields = ['token' => $token];
  324. $client = $this->clientService->newClient();
  325. try {
  326. $response = $client->post(
  327. $url,
  328. [
  329. 'body' => $fields,
  330. 'connect_timeout' => 10,
  331. ]
  332. );
  333. } catch (\Exception $e) {
  334. return false;
  335. }
  336. $status = json_decode($response->getBody(), true);
  337. return ($status['ocs']['meta']['statuscode'] === 100 || $status['ocs']['meta']['statuscode'] === 200);
  338. }
  339. /**
  340. * try send accept message to ocm end-point
  341. *
  342. * @param string $remoteDomain
  343. * @param string $token
  344. * @param $remoteId id of the share
  345. * @param string $feedback
  346. * @return bool
  347. */
  348. protected function tryOCMEndPoint($remoteDomain, $token, $remoteId, $feedback) {
  349. switch ($feedback) {
  350. case 'accept':
  351. $notification = $this->cloudFederationFactory->getCloudFederationNotification();
  352. $notification->setMessage(
  353. 'SHARE_ACCEPTED',
  354. 'file',
  355. $remoteId,
  356. [
  357. 'sharedSecret' => $token,
  358. 'message' => 'Recipient accept the share'
  359. ]
  360. );
  361. return $this->cloudFederationProviderManager->sendNotification($remoteDomain, $notification);
  362. case 'decline':
  363. $notification = $this->cloudFederationFactory->getCloudFederationNotification();
  364. $notification->setMessage(
  365. 'SHARE_DECLINED',
  366. 'file',
  367. $remoteId,
  368. [
  369. 'sharedSecret' => $token,
  370. 'message' => 'Recipient declined the share'
  371. ]
  372. );
  373. return $this->cloudFederationProviderManager->sendNotification($remoteDomain, $notification);
  374. }
  375. return false;
  376. }
  377. /**
  378. * remove '/user/files' from the path and trailing slashes
  379. *
  380. * @param string $path
  381. * @return string
  382. */
  383. protected function stripPath($path) {
  384. $prefix = '/' . $this->uid . '/files';
  385. return rtrim(substr($path, strlen($prefix)), '/');
  386. }
  387. public function getMount($data) {
  388. $data['manager'] = $this;
  389. $mountPoint = '/' . $this->uid . '/files' . $data['mountpoint'];
  390. $data['mountpoint'] = $mountPoint;
  391. $data['certificateManager'] = \OC::$server->getCertificateManager($this->uid);
  392. return new Mount(self::STORAGE, $mountPoint, $data, $this, $this->storageLoader);
  393. }
  394. /**
  395. * @param array $data
  396. * @return Mount
  397. */
  398. protected function mountShare($data) {
  399. $mount = $this->getMount($data);
  400. $this->mountManager->addMount($mount);
  401. return $mount;
  402. }
  403. /**
  404. * @return \OC\Files\Mount\Manager
  405. */
  406. public function getMountManager() {
  407. return $this->mountManager;
  408. }
  409. /**
  410. * @param string $source
  411. * @param string $target
  412. * @return bool
  413. */
  414. public function setMountPoint($source, $target) {
  415. $source = $this->stripPath($source);
  416. $target = $this->stripPath($target);
  417. $sourceHash = md5($source);
  418. $targetHash = md5($target);
  419. $query = $this->connection->prepare('
  420. UPDATE `*PREFIX*share_external`
  421. SET `mountpoint` = ?, `mountpoint_hash` = ?
  422. WHERE `mountpoint_hash` = ?
  423. AND `user` = ?
  424. ');
  425. $result = (bool)$query->execute([$target, $targetHash, $sourceHash, $this->uid]);
  426. return $result;
  427. }
  428. public function removeShare($mountPoint) {
  429. $mountPointObj = $this->mountManager->find($mountPoint);
  430. $id = $mountPointObj->getStorage()->getCache()->getId('');
  431. $mountPoint = $this->stripPath($mountPoint);
  432. $hash = md5($mountPoint);
  433. $getShare = $this->connection->prepare('
  434. SELECT `remote`, `share_token`, `remote_id`, `share_type`, `id`
  435. FROM `*PREFIX*share_external`
  436. WHERE `mountpoint_hash` = ? AND `user` = ?');
  437. $result = $getShare->execute([$hash, $this->uid]);
  438. $share = $getShare->fetch();
  439. $getShare->closeCursor();
  440. if ($result && $share !== false && (int)$share['share_type'] === IShare::TYPE_USER) {
  441. try {
  442. $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline');
  443. } catch (\Throwable $e) {
  444. // if we fail to notify the remote (probably cause the remote is down)
  445. // we still want the share to be gone to prevent undeletable remotes
  446. }
  447. $query = $this->connection->prepare('
  448. DELETE FROM `*PREFIX*share_external`
  449. WHERE `id` = ?
  450. ');
  451. $result = (bool)$query->execute([(int)$share['id']]);
  452. } elseif ($result && $share !== false && (int)$share['share_type'] === IShare::TYPE_GROUP) {
  453. $query = $this->connection->prepare('
  454. UPDATE `*PREFIX*share_external`
  455. SET `accepted` = ?
  456. WHERE `id` = ?');
  457. $result = (bool)$query->execute([0, (int)$share['id']]);
  458. }
  459. if ($result) {
  460. $this->removeReShares($id);
  461. }
  462. return $result;
  463. }
  464. /**
  465. * remove re-shares from share table and mapping in the federated_reshares table
  466. *
  467. * @param $mountPointId
  468. */
  469. protected function removeReShares($mountPointId) {
  470. $selectQuery = $this->connection->getQueryBuilder();
  471. $query = $this->connection->getQueryBuilder();
  472. $selectQuery->select('id')->from('share')
  473. ->where($selectQuery->expr()->eq('file_source', $query->createNamedParameter($mountPointId)));
  474. $select = $selectQuery->getSQL();
  475. $query->delete('federated_reshares')
  476. ->where($query->expr()->in('share_id', $query->createFunction('(' . $select . ')')));
  477. $query->execute();
  478. $deleteReShares = $this->connection->getQueryBuilder();
  479. $deleteReShares->delete('share')
  480. ->where($deleteReShares->expr()->eq('file_source', $deleteReShares->createNamedParameter($mountPointId)));
  481. $deleteReShares->execute();
  482. }
  483. /**
  484. * remove all shares for user $uid if the user was deleted
  485. *
  486. * @param string $uid
  487. * @return bool
  488. */
  489. public function removeUserShares($uid) {
  490. $getShare = $this->connection->prepare('
  491. SELECT `remote`, `share_token`, `remote_id`
  492. FROM `*PREFIX*share_external`
  493. WHERE `user` = ?');
  494. $result = $getShare->execute([$uid]);
  495. if ($result) {
  496. $shares = $getShare->fetchAll();
  497. foreach ($shares as $share) {
  498. $this->sendFeedbackToRemote($share['remote'], $share['share_token'], $share['remote_id'], 'decline');
  499. }
  500. }
  501. $query = $this->connection->prepare('
  502. DELETE FROM `*PREFIX*share_external`
  503. WHERE `user` = ?
  504. ');
  505. return (bool)$query->execute([$uid]);
  506. }
  507. /**
  508. * return a list of shares which are not yet accepted by the user
  509. *
  510. * @return array list of open server-to-server shares
  511. */
  512. public function getOpenShares() {
  513. return $this->getShares(false);
  514. }
  515. /**
  516. * return a list of shares which are accepted by the user
  517. *
  518. * @return array list of accepted server-to-server shares
  519. */
  520. public function getAcceptedShares() {
  521. return $this->getShares(true);
  522. }
  523. /**
  524. * return a list of shares for the user
  525. *
  526. * @param bool|null $accepted True for accepted only,
  527. * false for not accepted,
  528. * null for all shares of the user
  529. * @return array list of open server-to-server shares
  530. */
  531. private function getShares($accepted) {
  532. $user = $this->userManager->get($this->uid);
  533. $groups = $this->groupManager->getUserGroups($user);
  534. $userGroups = [];
  535. foreach ($groups as $group) {
  536. $userGroups[] = $group->getGID();
  537. }
  538. $query = 'SELECT `id`, `remote`, `remote_id`, `share_token`, `name`, `owner`, `user`, `mountpoint`, `accepted`
  539. FROM `*PREFIX*share_external`
  540. WHERE (`user` = ? OR `user` IN (?))';
  541. $parameters = [$this->uid, implode(',',$userGroups)];
  542. if (!is_null($accepted)) {
  543. $query .= ' AND `accepted` = ?';
  544. $parameters[] = (int) $accepted;
  545. }
  546. $query .= ' ORDER BY `id` ASC';
  547. $shares = $this->connection->prepare($query);
  548. $result = $shares->execute($parameters);
  549. return $result ? $shares->fetchAll() : [];
  550. }
  551. }