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.

1173 lines
36 KiB

  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. *
  5. * @copyright Copyright (c) 2018, Daniel Calviño Sánchez (danxuliu@gmail.com)
  6. *
  7. * Code copied from "lib/private/Share20/DefaultShareProvider.php" and
  8. * "apps/sharebymail/lib/ShareByMailProvider.php" at d805959e819e64 in Nextcloud
  9. * server repository.
  10. *
  11. * @license GNU AGPL version 3 or any later version
  12. *
  13. * This program is free software: you can redistribute it and/or modify
  14. * it under the terms of the GNU Affero General Public License as
  15. * published by the Free Software Foundation, either version 3 of the
  16. * License, or (at your option) any later version.
  17. *
  18. * This program is distributed in the hope that it will be useful,
  19. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  20. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  21. * GNU Affero General Public License for more details.
  22. *
  23. * You should have received a copy of the GNU Affero General Public License
  24. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  25. *
  26. */
  27. namespace OCA\Talk\Share;
  28. use OC\Files\Cache\Cache;
  29. use OCA\Talk\Events\ParticipantEvent;
  30. use OCA\Talk\Events\RemoveUserEvent;
  31. use OCA\Talk\Events\RoomEvent;
  32. use OCA\Talk\Exceptions\ParticipantNotFoundException;
  33. use OCA\Talk\Exceptions\RoomNotFoundException;
  34. use OCA\Talk\Manager;
  35. use OCA\Talk\Participant;
  36. use OCA\Talk\Room;
  37. use OCA\Talk\Service\ParticipantService;
  38. use OCP\AppFramework\Utility\ITimeFactory;
  39. use OCP\DB\QueryBuilder\IQueryBuilder;
  40. use OCP\EventDispatcher\IEventDispatcher;
  41. use OCP\Files\Folder;
  42. use OCP\Files\IMimeTypeLoader;
  43. use OCP\Files\Node;
  44. use OCP\IDBConnection;
  45. use OCP\IL10N;
  46. use OCP\Security\ISecureRandom;
  47. use OCP\Share\Exceptions\GenericShareException;
  48. use OCP\Share\Exceptions\ShareNotFound;
  49. use OCP\Share\IManager as IShareManager;
  50. use OCP\Share\IShare;
  51. use OCP\Share\IShareProvider;
  52. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  53. use Symfony\Component\EventDispatcher\GenericEvent;
  54. /**
  55. * Share provider for room shares.
  56. *
  57. * Files are shared with a room identified by its token; only users currently in
  58. * the room can share with and access the shared files (although the access
  59. * checks are not enforced by the provider, but done on a higher layer).
  60. *
  61. * Like in group shares, a recipient can move or delete a share without
  62. * modifying the share for the other users in the room.
  63. */
  64. class RoomShareProvider implements IShareProvider {
  65. // Special share type for user modified room shares
  66. public const SHARE_TYPE_USERROOM = 11;
  67. public const TALK_FOLDER = '/Talk';
  68. public const TALK_FOLDER_PLACEHOLDER = '/{TALK_PLACEHOLDER}';
  69. /** @var IDBConnection */
  70. private $dbConnection;
  71. /** @var ISecureRandom */
  72. private $secureRandom;
  73. /** @var IShareManager */
  74. private $shareManager;
  75. /** @var EventDispatcherInterface */
  76. private $dispatcher;
  77. /** @var Manager */
  78. private $manager;
  79. /** @var ParticipantService */
  80. private $participantService;
  81. /** @var ITimeFactory */
  82. protected $timeFactory;
  83. /** @var IL10N */
  84. private $l;
  85. /** @var IMimeTypeLoader */
  86. private $mimeTypeLoader;
  87. public function __construct(
  88. IDBConnection $connection,
  89. ISecureRandom $secureRandom,
  90. IShareManager $shareManager,
  91. EventDispatcherInterface $dispatcher,
  92. Manager $manager,
  93. ParticipantService $participantService,
  94. ITimeFactory $timeFactory,
  95. IL10N $l,
  96. IMimeTypeLoader $mimeTypeLoader
  97. ) {
  98. $this->dbConnection = $connection;
  99. $this->secureRandom = $secureRandom;
  100. $this->shareManager = $shareManager;
  101. $this->dispatcher = $dispatcher;
  102. $this->manager = $manager;
  103. $this->participantService = $participantService;
  104. $this->timeFactory = $timeFactory;
  105. $this->l = $l;
  106. $this->mimeTypeLoader = $mimeTypeLoader;
  107. }
  108. public static function register(IEventDispatcher $dispatcher): void {
  109. $listener = static function (ParticipantEvent $event) {
  110. $room = $event->getRoom();
  111. if ($event->getParticipant()->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) {
  112. /** @var self $roomShareProvider */
  113. $roomShareProvider = \OC::$server->query(self::class);
  114. $roomShareProvider->deleteInRoom($room->getToken(), $event->getParticipant()->getAttendee()->getActorId());
  115. }
  116. };
  117. $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DISCONNECT, $listener);
  118. $listener = static function (RemoveUserEvent $event) {
  119. $room = $event->getRoom();
  120. /** @var self $roomShareProvider */
  121. $roomShareProvider = \OC::$server->query(self::class);
  122. $roomShareProvider->deleteInRoom($room->getToken(), $event->getUser()->getUID());
  123. };
  124. $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, $listener);
  125. $listener = static function (RoomEvent $event) {
  126. $room = $event->getRoom();
  127. /** @var self $roomShareProvider */
  128. $roomShareProvider = \OC::$server->query(self::class);
  129. $roomShareProvider->deleteInRoom($room->getToken());
  130. };
  131. $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DELETE, $listener);
  132. }
  133. /**
  134. * Return the identifier of this provider.
  135. *
  136. * @return string Containing only [a-zA-Z0-9]
  137. */
  138. public function identifier(): string {
  139. return 'ocRoomShare';
  140. }
  141. /**
  142. * Create a share
  143. *
  144. * @param IShare $share
  145. * @return IShare The share object
  146. * @throws GenericShareException
  147. */
  148. public function create(IShare $share): IShare {
  149. try {
  150. $room = $this->manager->getRoomByToken($share->getSharedWith(), $share->getSharedBy());
  151. } catch (RoomNotFoundException $e) {
  152. throw new GenericShareException('Room not found', $this->l->t('Conversation not found'), 404);
  153. }
  154. if ($room->getReadOnly() === Room::READ_ONLY) {
  155. throw new GenericShareException('Room not found', $this->l->t('Conversation not found'), 404);
  156. }
  157. try {
  158. $room->getParticipant($share->getSharedBy(), false);
  159. } catch (ParticipantNotFoundException $e) {
  160. // If the sharer is not a participant of the room even if the room
  161. // exists the error is still "Room not found".
  162. throw new GenericShareException('Room not found', $this->l->t('Conversation not found'), 404);
  163. }
  164. $existingShares = $this->getSharesByPath($share->getNode());
  165. foreach ($existingShares as $existingShare) {
  166. if ($existingShare->getSharedWith() === $share->getSharedWith()) {
  167. // FIXME Should be moved away from GenericEvent as soon as OCP\Share20\IManager did move too
  168. $this->dispatcher->dispatch(self::class . '::' . 'share_file_again', new GenericEvent($existingShare));
  169. throw new GenericShareException('Already shared', $this->l->t('Path is already shared with this room'), 403);
  170. }
  171. }
  172. $share->setToken(
  173. $this->secureRandom->generate(
  174. 15, // \OC\Share\Constants::TOKEN_LENGTH
  175. \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE
  176. )
  177. );
  178. $shareId = $this->addShareToDB(
  179. $share->getSharedWith(),
  180. $share->getSharedBy(),
  181. $share->getShareOwner(),
  182. $share->getNodeType(),
  183. $share->getNodeId(),
  184. $share->getTarget(),
  185. $share->getPermissions(),
  186. $share->getToken(),
  187. $share->getExpirationDate()
  188. );
  189. $data = $this->getRawShare($shareId);
  190. return $this->createShareObject($data);
  191. }
  192. /**
  193. * Add share to the database and return the ID
  194. *
  195. * @param string $shareWith
  196. * @param string $sharedBy
  197. * @param string $shareOwner
  198. * @param string $itemType
  199. * @param int $itemSource
  200. * @param string $target
  201. * @param int $permissions
  202. * @param string $token
  203. * @param \DateTime|null $expirationDate
  204. * @return int
  205. */
  206. private function addShareToDB(
  207. string $shareWith,
  208. string $sharedBy,
  209. string $shareOwner,
  210. string $itemType,
  211. int $itemSource,
  212. string $target,
  213. int $permissions,
  214. string $token,
  215. ?\DateTime $expirationDate
  216. ): int {
  217. $qb = $this->dbConnection->getQueryBuilder();
  218. $qb->insert('share')
  219. ->setValue('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))
  220. ->setValue('share_with', $qb->createNamedParameter($shareWith))
  221. ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy))
  222. ->setValue('uid_owner', $qb->createNamedParameter($shareOwner))
  223. ->setValue('item_type', $qb->createNamedParameter($itemType))
  224. ->setValue('item_source', $qb->createNamedParameter($itemSource))
  225. ->setValue('file_source', $qb->createNamedParameter($itemSource))
  226. ->setValue('file_target', $qb->createNamedParameter($target))
  227. ->setValue('permissions', $qb->createNamedParameter($permissions))
  228. ->setValue('token', $qb->createNamedParameter($token))
  229. ->setValue('stime', $qb->createNamedParameter($this->timeFactory->getTime()));
  230. if ($expirationDate !== null) {
  231. $qb->setValue('expiration', $qb->createNamedParameter($expirationDate, 'datetime'));
  232. }
  233. $qb->execute();
  234. $id = $qb->getLastInsertId();
  235. return $id;
  236. }
  237. /**
  238. * Get database row of the given share
  239. *
  240. * @param int $id
  241. * @return array
  242. * @throws ShareNotFound
  243. */
  244. private function getRawShare(int $id): array {
  245. $qb = $this->dbConnection->getQueryBuilder();
  246. $qb->select('*')
  247. ->from('share')
  248. ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
  249. $cursor = $qb->execute();
  250. $data = $cursor->fetch();
  251. $cursor->closeCursor();
  252. if ($data === false) {
  253. throw new ShareNotFound();
  254. }
  255. return $data;
  256. }
  257. /**
  258. * Create a share object from a database row
  259. *
  260. * @param array $data
  261. * @return IShare
  262. */
  263. private function createShareObject(array $data): IShare {
  264. $share = $this->shareManager->newShare();
  265. $share->setId((int)$data['id'])
  266. ->setShareType((int)$data['share_type'])
  267. ->setPermissions((int)$data['permissions'])
  268. ->setTarget($data['file_target'])
  269. ->setStatus((int)$data['accepted'])
  270. ->setToken($data['token']);
  271. $shareTime = $this->timeFactory->getDateTime();
  272. $shareTime->setTimestamp((int)$data['stime']);
  273. $share->setShareTime($shareTime);
  274. $share->setSharedWith($data['share_with']);
  275. $share->setSharedBy($data['uid_initiator']);
  276. $share->setShareOwner($data['uid_owner']);
  277. if ($data['expiration'] !== null) {
  278. $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']);
  279. if ($expiration !== false) {
  280. $share->setExpirationDate($expiration);
  281. }
  282. }
  283. $share->setNodeId((int)$data['file_source']);
  284. $share->setNodeType($data['item_type']);
  285. $share->setProviderId($this->identifier());
  286. if (isset($data['f_permissions'])) {
  287. $entryData = $data;
  288. $entryData['permissions'] = $entryData['f_permissions'];
  289. $entryData['parent'] = $entryData['f_parent'];
  290. $share->setNodeCacheEntry(Cache::cacheEntryFromData($entryData,
  291. $this->mimeTypeLoader));
  292. }
  293. return $share;
  294. }
  295. /**
  296. * Update a share
  297. *
  298. * @param IShare $share
  299. * @return IShare The share object
  300. */
  301. public function update(IShare $share): IShare {
  302. $qb = $this->dbConnection->getQueryBuilder();
  303. $qb->update('share')
  304. ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())))
  305. ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
  306. ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
  307. ->set('permissions', $qb->createNamedParameter($share->getPermissions()))
  308. ->set('item_source', $qb->createNamedParameter($share->getNode()->getId()))
  309. ->set('file_source', $qb->createNamedParameter($share->getNode()->getId()))
  310. ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
  311. ->execute();
  312. /*
  313. * Update all user defined group shares
  314. */
  315. $qb = $this->dbConnection->getQueryBuilder();
  316. $qb->update('share')
  317. ->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())))
  318. ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner()))
  319. ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy()))
  320. ->set('item_source', $qb->createNamedParameter($share->getNode()->getId()))
  321. ->set('file_source', $qb->createNamedParameter($share->getNode()->getId()))
  322. ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE))
  323. ->execute();
  324. /*
  325. * Now update the permissions for all children that have not set it to 0
  326. */
  327. $qb = $this->dbConnection->getQueryBuilder();
  328. $qb->update('share')
  329. ->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())))
  330. ->andWhere($qb->expr()->neq('permissions', $qb->createNamedParameter(0)))
  331. ->set('permissions', $qb->createNamedParameter($share->getPermissions()))
  332. ->execute();
  333. return $share;
  334. }
  335. /**
  336. * Delete a share
  337. *
  338. * @param IShare $share
  339. */
  340. public function delete(IShare $share): void {
  341. $qb = $this->dbConnection->getQueryBuilder();
  342. $qb->delete('share')
  343. ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId())));
  344. $qb->orWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())));
  345. $qb->execute();
  346. }
  347. /**
  348. * Unshare a file from self as recipient.
  349. *
  350. * If a user unshares a room share from their self then the original room
  351. * share should still exist.
  352. *
  353. * @param IShare $share
  354. * @param string $recipient UserId of the recipient
  355. */
  356. public function deleteFromSelf(IShare $share, $recipient): void {
  357. // Check if there is a userroom share
  358. $qb = $this->dbConnection->getQueryBuilder();
  359. $stmt = $qb->select(['id', 'permissions'])
  360. ->from('share')
  361. ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM)))
  362. ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($recipient)))
  363. ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())))
  364. ->andWhere($qb->expr()->orX(
  365. $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
  366. $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
  367. ))
  368. ->execute();
  369. $data = $stmt->fetch();
  370. $stmt->closeCursor();
  371. if ($data === false) {
  372. // No userroom share yet. Create one.
  373. $qb = $this->dbConnection->getQueryBuilder();
  374. $qb->insert('share')
  375. ->values([
  376. 'share_type' => $qb->createNamedParameter(self::SHARE_TYPE_USERROOM),
  377. 'share_with' => $qb->createNamedParameter($recipient),
  378. 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()),
  379. 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()),
  380. 'parent' => $qb->createNamedParameter($share->getId()),
  381. 'item_type' => $qb->createNamedParameter($share->getNodeType()),
  382. 'item_source' => $qb->createNamedParameter($share->getNodeId()),
  383. 'file_source' => $qb->createNamedParameter($share->getNodeId()),
  384. 'file_target' => $qb->createNamedParameter($share->getTarget()),
  385. 'permissions' => $qb->createNamedParameter(0),
  386. 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()),
  387. ])->execute();
  388. } elseif ($data['permissions'] !== 0) {
  389. // Already a userroom share. Update it.
  390. $qb = $this->dbConnection->getQueryBuilder();
  391. $qb->update('share')
  392. ->set('permissions', $qb->createNamedParameter(0))
  393. ->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id'])))
  394. ->execute();
  395. }
  396. }
  397. /**
  398. * Restore a share for a given recipient. The implementation could be provider independant.
  399. *
  400. * @param IShare $share
  401. * @param string $recipient
  402. * @return IShare The restored share object
  403. * @throws GenericShareException In case the share could not be restored
  404. */
  405. public function restore(IShare $share, string $recipient): IShare {
  406. $qb = $this->dbConnection->getQueryBuilder();
  407. $qb->select('permissions')
  408. ->from('share')
  409. ->where(
  410. $qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))
  411. );
  412. $cursor = $qb->execute();
  413. $data = $cursor->fetch();
  414. $cursor->closeCursor();
  415. $originalPermission = $data['permissions'];
  416. $qb = $this->dbConnection->getQueryBuilder();
  417. $qb->update('share')
  418. ->set('permissions', $qb->createNamedParameter($originalPermission))
  419. ->where(
  420. $qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))
  421. )->andWhere(
  422. $qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM))
  423. )->andWhere(
  424. $qb->expr()->eq('share_with', $qb->createNamedParameter($recipient))
  425. );
  426. $qb->execute();
  427. return $this->getShareById($share->getId(), $recipient);
  428. }
  429. /**
  430. * Move a share as a recipient.
  431. *
  432. * This is updating the share target. Thus the mount point of the recipient.
  433. * This may require special handling. If a user moves a room share
  434. * the target should only be changed for them.
  435. *
  436. * @param IShare $share
  437. * @param string $recipient userId of recipient
  438. * @return IShare
  439. */
  440. public function move(IShare $share, $recipient): IShare {
  441. // Check if there is a userroom share
  442. $qb = $this->dbConnection->getQueryBuilder();
  443. $stmt = $qb->select('id')
  444. ->from('share')
  445. ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM)))
  446. ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($recipient)))
  447. ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())))
  448. ->andWhere($qb->expr()->orX(
  449. $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
  450. $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
  451. ))
  452. ->setMaxResults(1)
  453. ->execute();
  454. $data = $stmt->fetch();
  455. $stmt->closeCursor();
  456. if ($data === false) {
  457. // No userroom share yet. Create one.
  458. $qb = $this->dbConnection->getQueryBuilder();
  459. $qb->insert('share')
  460. ->values([
  461. 'share_type' => $qb->createNamedParameter(self::SHARE_TYPE_USERROOM),
  462. 'share_with' => $qb->createNamedParameter($recipient),
  463. 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()),
  464. 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()),
  465. 'parent' => $qb->createNamedParameter($share->getId()),
  466. 'item_type' => $qb->createNamedParameter($share->getNodeType()),
  467. 'item_source' => $qb->createNamedParameter($share->getNodeId()),
  468. 'file_source' => $qb->createNamedParameter($share->getNodeId()),
  469. 'file_target' => $qb->createNamedParameter($share->getTarget()),
  470. 'permissions' => $qb->createNamedParameter($share->getPermissions()),
  471. 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()),
  472. ])->execute();
  473. } else {
  474. // Already a userroom share. Update it.
  475. $qb = $this->dbConnection->getQueryBuilder();
  476. $qb->update('share')
  477. ->set('file_target', $qb->createNamedParameter($share->getTarget()))
  478. ->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id'])))
  479. ->execute();
  480. }
  481. return $share;
  482. }
  483. /**
  484. * Get all shares by the given user in a folder
  485. *
  486. * @param string $userId
  487. * @param Folder $node
  488. * @param bool $reshares Also get the shares where $user is the owner instead of just the shares where $user is the initiator
  489. * @return IShare[][]
  490. * @psalm-return array<array-key, non-empty-list<IShare>>
  491. */
  492. public function getSharesInFolder($userId, Folder $node, $reshares): array {
  493. $qb = $this->dbConnection->getQueryBuilder();
  494. $qb->select('*')
  495. ->from('share', 's')
  496. ->andWhere($qb->expr()->orX(
  497. $qb->expr()->eq('s.item_type', $qb->createNamedParameter('file')),
  498. $qb->expr()->eq('s.item_type', $qb->createNamedParameter('folder'))
  499. ))
  500. ->andWhere(
  501. $qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))
  502. );
  503. /**
  504. * Reshares for this user are shares where they are the owner.
  505. */
  506. if ($reshares === false) {
  507. $qb->andWhere($qb->expr()->eq('s.uid_initiator', $qb->createNamedParameter($userId)));
  508. } else {
  509. $qb->andWhere(
  510. $qb->expr()->orX(
  511. $qb->expr()->eq('s.uid_owner', $qb->createNamedParameter($userId)),
  512. $qb->expr()->eq('s.uid_initiator', $qb->createNamedParameter($userId))
  513. )
  514. );
  515. }
  516. $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
  517. $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
  518. $qb->orderBy('s.id');
  519. $cursor = $qb->execute();
  520. $shares = [];
  521. while ($data = $cursor->fetch()) {
  522. $shares[$data['fileid']][] = $this->createShareObject($data);
  523. }
  524. $cursor->closeCursor();
  525. return $shares;
  526. }
  527. /**
  528. * Get all shares by the given user
  529. *
  530. * @param string $userId
  531. * @param int $shareType
  532. * @param Node|null $node
  533. * @param bool $reshares Also get the shares where $user is the owner instead of just the shares where $user is the initiator
  534. * @param int $limit The maximum number of shares to be returned, -1 for all shares
  535. * @param int $offset
  536. * @return IShare[]
  537. */
  538. public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset): array {
  539. $qb = $this->dbConnection->getQueryBuilder();
  540. $qb->select('*')
  541. ->from('share');
  542. $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)));
  543. /**
  544. * Reshares for this user are shares where they are the owner.
  545. */
  546. if ($reshares === false) {
  547. $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)));
  548. } else {
  549. $qb->andWhere(
  550. $qb->expr()->orX(
  551. $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)),
  552. $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))
  553. )
  554. );
  555. }
  556. if ($node !== null) {
  557. $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId())));
  558. }
  559. if ($limit !== -1) {
  560. $qb->setMaxResults($limit);
  561. }
  562. $qb->setFirstResult($offset);
  563. $qb->orderBy('id');
  564. $cursor = $qb->execute();
  565. $shares = [];
  566. while ($data = $cursor->fetch()) {
  567. $shares[] = $this->createShareObject($data);
  568. }
  569. $cursor->closeCursor();
  570. return $shares;
  571. }
  572. /**
  573. * Get share by id
  574. *
  575. * @param int $id
  576. * @param string|null $recipientId
  577. * @return IShare
  578. * @throws ShareNotFound
  579. */
  580. public function getShareById($id, $recipientId = null): IShare {
  581. $qb = $this->dbConnection->getQueryBuilder();
  582. $qb->select('s.*',
  583. 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash',
  584. 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime',
  585. 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum'
  586. )
  587. ->selectAlias('st.id', 'storage_string_id')
  588. ->from('share', 's')
  589. ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'))
  590. ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id'))
  591. ->where($qb->expr()->eq('s.id', $qb->createNamedParameter($id)))
  592. ->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)));
  593. $cursor = $qb->execute();
  594. $data = $cursor->fetch();
  595. $cursor->closeCursor();
  596. if ($data === false) {
  597. throw new ShareNotFound();
  598. }
  599. if (!$this->isAccessibleResult($data)) {
  600. throw new ShareNotFound();
  601. }
  602. $share = $this->createShareObject($data);
  603. if ($recipientId !== null) {
  604. $share = $this->resolveSharesForRecipient([$share], $recipientId)[0];
  605. }
  606. return $share;
  607. }
  608. /**
  609. * Returns each given share as seen by the given recipient.
  610. *
  611. * If the recipient has not modified the share the original one is returned
  612. * instead.
  613. *
  614. * @param IShare[] $shares
  615. * @param string $userId
  616. * @return IShare[]
  617. */
  618. private function resolveSharesForRecipient(array $shares, string $userId): array {
  619. $result = [];
  620. $start = 0;
  621. while (true) {
  622. /** @var IShare[] $shareSlice */
  623. $shareSlice = array_slice($shares, $start, 100);
  624. $start += 100;
  625. if ($shareSlice === []) {
  626. break;
  627. }
  628. /** @var int[] $ids */
  629. $ids = [];
  630. /** @var IShare[] $shareMap */
  631. $shareMap = [];
  632. foreach ($shareSlice as $share) {
  633. $ids[] = (int)$share->getId();
  634. $shareMap[$share->getId()] = $share;
  635. }
  636. $qb = $this->dbConnection->getQueryBuilder();
  637. $query = $qb->select('*')
  638. ->from('share')
  639. ->where($qb->expr()->in('parent', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
  640. ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId)))
  641. ->andWhere($qb->expr()->orX(
  642. $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
  643. $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
  644. ));
  645. $stmt = $query->execute();
  646. while ($data = $stmt->fetch()) {
  647. $shareMap[$data['parent']]->setPermissions((int)$data['permissions']);
  648. $shareMap[$data['parent']]->setTarget($data['file_target']);
  649. }
  650. $stmt->closeCursor();
  651. foreach ($shareMap as $share) {
  652. $result[] = $share;
  653. }
  654. }
  655. return $result;
  656. }
  657. /**
  658. * Get shares for a given path
  659. *
  660. * @param Node $path
  661. * @return IShare[]
  662. */
  663. public function getSharesByPath(Node $path): array {
  664. $qb = $this->dbConnection->getQueryBuilder();
  665. $cursor = $qb->select('*')
  666. ->from('share')
  667. ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId())))
  668. ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
  669. ->execute();
  670. $shares = [];
  671. while ($data = $cursor->fetch()) {
  672. $shares[] = $this->createShareObject($data);
  673. }
  674. $cursor->closeCursor();
  675. return $shares;
  676. }
  677. /**
  678. * Get shared with the given user
  679. *
  680. * @param string $userId get shares where this user is the recipient
  681. * @param int $shareType
  682. * @param Node|null $node
  683. * @param int $limit The max number of entries returned, -1 for all
  684. * @param int $offset
  685. * @return IShare[]
  686. */
  687. public function getSharedWith($userId, $shareType, $node, $limit, $offset): array {
  688. $allRooms = $this->manager->getRoomTokensForUser($userId);
  689. /** @var IShare[] $shares */
  690. $shares = [];
  691. $start = 0;
  692. while (true) {
  693. $rooms = array_slice($allRooms, $start, 100);
  694. $start += 100;
  695. if ($rooms === []) {
  696. break;
  697. }
  698. $qb = $this->dbConnection->getQueryBuilder();
  699. $qb->select('s.*',
  700. 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash',
  701. 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime',
  702. 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum'
  703. )
  704. ->selectAlias('st.id', 'storage_string_id')
  705. ->from('share', 's')
  706. ->orderBy('s.id')
  707. ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'))
  708. ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id'));
  709. if ($limit !== -1) {
  710. $qb->setMaxResults($limit);
  711. }
  712. // Filter by node if provided
  713. if ($node !== null) {
  714. $qb->andWhere($qb->expr()->eq('s.file_source', $qb->createNamedParameter($node->getId())));
  715. }
  716. $qb->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
  717. ->andWhere($qb->expr()->in('s.share_with', $qb->createNamedParameter(
  718. $rooms,
  719. IQueryBuilder::PARAM_STR_ARRAY
  720. )))
  721. ->andWhere($qb->expr()->orX(
  722. $qb->expr()->eq('s.item_type', $qb->createNamedParameter('file')),
  723. $qb->expr()->eq('s.item_type', $qb->createNamedParameter('folder'))
  724. ));
  725. $cursor = $qb->execute();
  726. while ($data = $cursor->fetch()) {
  727. if (!$this->isAccessibleResult($data)) {
  728. continue;
  729. }
  730. if ($offset > 0) {
  731. $offset--;
  732. continue;
  733. }
  734. $shares[] = $this->createShareObject($data);
  735. }
  736. $cursor->closeCursor();
  737. }
  738. $shares = $this->resolveSharesForRecipient($shares, $userId);
  739. return $shares;
  740. }
  741. private function isAccessibleResult(array $data): bool {
  742. // exclude shares leading to deleted file entries
  743. if ($data['fileid'] === null || $data['path'] === null) {
  744. return false;
  745. }
  746. // exclude shares leading to trashbin on home storages
  747. $pathSections = explode('/', $data['path'], 2);
  748. // FIXME: would not detect rare md5'd home storage case properly
  749. if ($pathSections[0] !== 'files'
  750. && in_array(explode(':', $data['storage_string_id'], 2)[0], ['home', 'object'])) {
  751. return false;
  752. }
  753. return true;
  754. }
  755. /**
  756. * Get a share by token
  757. *
  758. * Note that token here refers to share token, not room token.
  759. *
  760. * @param string $token
  761. * @return IShare
  762. * @throws ShareNotFound
  763. */
  764. public function getShareByToken($token): IShare {
  765. $qb = $this->dbConnection->getQueryBuilder();
  766. $cursor = $qb->select('*')
  767. ->from('share')
  768. ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
  769. ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token)))
  770. ->execute();
  771. $data = $cursor->fetch();
  772. if ($data === false) {
  773. throw new ShareNotFound();
  774. }
  775. $roomToken = $data['share_with'];
  776. try {
  777. $room = $this->manager->getRoomByToken($roomToken);
  778. } catch (RoomNotFoundException $e) {
  779. throw new ShareNotFound();
  780. }
  781. if ($room->getType() !== Room::PUBLIC_CALL) {
  782. throw new ShareNotFound();
  783. }
  784. return $this->createShareObject($data);
  785. }
  786. /**
  787. * A user is deleted from the system
  788. * So clean up the relevant shares.
  789. *
  790. * @param string $uid
  791. * @param int $shareType
  792. */
  793. public function userDeleted($uid, $shareType): void {
  794. // A deleted user is handled automatically by the room hooks due to the
  795. // user being removed from the room.
  796. }
  797. /**
  798. * A group is deleted from the system.
  799. * We have to clean up all shares to this group.
  800. * Providers not handling group shares should just return
  801. *
  802. * @param string $gid
  803. */
  804. public function groupDeleted($gid): void {
  805. }
  806. /**
  807. * A user is deleted from a group
  808. * We have to clean up all the related user specific group shares
  809. * Providers not handling group shares should just return
  810. *
  811. * @param string $uid
  812. * @param string $gid
  813. */
  814. public function userDeletedFromGroup($uid, $gid): void {
  815. }
  816. /**
  817. * Get the access list to the array of provided nodes.
  818. *
  819. * @see IManager::getAccessList() for sample docs
  820. *
  821. * @param Node[] $nodes The list of nodes to get access for
  822. * @param bool $currentAccess If current access is required (like for removed shares that might get revived later)
  823. * @return array
  824. */
  825. public function getAccessList($nodes, $currentAccess): array {
  826. $ids = [];
  827. foreach ($nodes as $node) {
  828. $ids[] = $node->getId();
  829. }
  830. $qb = $this->dbConnection->getQueryBuilder();
  831. $types = [IShare::TYPE_ROOM];
  832. if ($currentAccess) {
  833. $types[] = self::SHARE_TYPE_USERROOM;
  834. }
  835. $qb->select('id', 'parent', 'share_type', 'share_with', 'file_source', 'file_target', 'permissions')
  836. ->from('share')
  837. ->where($qb->expr()->in('share_type', $qb->createNamedParameter($types, IQueryBuilder::PARAM_INT_ARRAY)))
  838. ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
  839. ->andWhere($qb->expr()->orX(
  840. $qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
  841. $qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
  842. ));
  843. $cursor = $qb->execute();
  844. $users = [];
  845. while ($row = $cursor->fetch()) {
  846. $type = (int)$row['share_type'];
  847. if ($type === IShare::TYPE_ROOM) {
  848. $roomToken = $row['share_with'];
  849. try {
  850. $room = $this->manager->getRoomByToken($roomToken);
  851. } catch (RoomNotFoundException $e) {
  852. continue;
  853. }
  854. $userList = $this->participantService->getParticipantUserIds($room);
  855. foreach ($userList as $uid) {
  856. $users[$uid] = $users[$uid] ?? [];
  857. $users[$uid][$row['id']] = $row;
  858. }
  859. } elseif ($type === self::SHARE_TYPE_USERROOM && $currentAccess === true) {
  860. $uid = $row['share_with'];
  861. $users[$uid] = $users[$uid] ?? [];
  862. $users[$uid][$row['id']] = $row;
  863. }
  864. }
  865. $cursor->closeCursor();
  866. if ($currentAccess === true) {
  867. $users = array_map([$this, 'filterSharesOfUser'], $users);
  868. $users = array_filter($users);
  869. } else {
  870. $users = array_keys($users);
  871. }
  872. return ['users' => $users];
  873. }
  874. /**
  875. * For each user the path with the fewest slashes is returned
  876. * @param array $shares
  877. * @return array
  878. */
  879. protected function filterSharesOfUser(array $shares): array {
  880. // Room shares when the user has a share exception
  881. foreach ($shares as $id => $share) {
  882. $type = (int) $share['share_type'];
  883. $permissions = (int) $share['permissions'];
  884. if ($type === self::SHARE_TYPE_USERROOM) {
  885. unset($shares[$share['parent']]);
  886. if ($permissions === 0) {
  887. unset($shares[$id]);
  888. }
  889. }
  890. }
  891. $best = [];
  892. $bestDepth = 0;
  893. foreach ($shares as $id => $share) {
  894. $depth = substr_count($share['file_target'], '/');
  895. if (empty($best) || $depth < $bestDepth) {
  896. $bestDepth = $depth;
  897. $best = [
  898. 'node_id' => $share['file_source'],
  899. 'node_path' => $share['file_target'],
  900. ];
  901. }
  902. }
  903. return $best;
  904. }
  905. /**
  906. * Get all children of this share
  907. *
  908. * Not part of IShareProvider API, but needed by OC\Share20\Manager.
  909. *
  910. * @param IShare $parent
  911. * @return IShare[]
  912. */
  913. public function getChildren(IShare $parent): array {
  914. $children = [];
  915. $qb = $this->dbConnection->getQueryBuilder();
  916. $qb->select('*')
  917. ->from('share')
  918. ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId())))
  919. ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
  920. ->orderBy('id');
  921. $cursor = $qb->execute();
  922. while ($data = $cursor->fetch()) {
  923. $children[] = $this->createShareObject($data);
  924. }
  925. $cursor->closeCursor();
  926. return $children;
  927. }
  928. /**
  929. * Delete all shares in a room, or only those from the given user.
  930. *
  931. * When a user is given all her shares are removed, both own shares and
  932. * received shares.
  933. *
  934. * Not part of IShareProvider API, but needed by the hooks in
  935. * OCA\Talk\AppInfo\Application
  936. *
  937. * @param string $roomToken
  938. * @param string|null $user
  939. */
  940. public function deleteInRoom(string $roomToken, string $user = null): void {
  941. //First delete all custom room shares for the original shares to be removed
  942. $qb = $this->dbConnection->getQueryBuilder();
  943. $qb->select('id')
  944. ->from('share')
  945. ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
  946. ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($roomToken)));
  947. if ($user !== null) {
  948. $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($user)));
  949. }
  950. $cursor = $qb->execute();
  951. $ids = [];
  952. while ($row = $cursor->fetch()) {
  953. $ids[] = (int)$row['id'];
  954. }
  955. $cursor->closeCursor();
  956. if (!empty($ids)) {
  957. $chunks = array_chunk($ids, 100);
  958. foreach ($chunks as $chunk) {
  959. $qb->delete('share')
  960. ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM)))
  961. ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)));
  962. $qb->execute();
  963. }
  964. }
  965. // Now delete all the original room shares
  966. $qb = $this->dbConnection->getQueryBuilder();
  967. $qb->delete('share')
  968. ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
  969. ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($roomToken)));
  970. if ($user !== null) {
  971. $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($user)));
  972. }
  973. $qb->execute();
  974. // Finally delete all custom room shares leftovers for the given user
  975. if ($user !== null) {
  976. $qb = $this->dbConnection->getQueryBuilder();
  977. $qb->select('id')
  978. ->from('share')
  979. ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
  980. ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($roomToken)));
  981. $cursor = $qb->execute();
  982. $ids = [];
  983. while ($row = $cursor->fetch()) {
  984. $ids[] = (int)$row['id'];
  985. }
  986. $cursor->closeCursor();
  987. if (!empty($ids)) {
  988. $chunks = array_chunk($ids, 100);
  989. foreach ($chunks as $chunk) {
  990. $qb->delete('share')
  991. ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM)))
  992. ->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter($user)))
  993. ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)));
  994. $qb->execute();
  995. }
  996. }
  997. }
  998. }
  999. /**
  1000. * Get all the shares in this provider returned as iterable to reduce memory
  1001. * overhead
  1002. *
  1003. * @return iterable
  1004. * @since 18.0.0
  1005. */
  1006. public function getAllShares(): iterable {
  1007. $qb = $this->dbConnection->getQueryBuilder();
  1008. $qb->select('*')
  1009. ->from('share')
  1010. ->where(
  1011. $qb->expr()->orX(
  1012. $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))
  1013. )
  1014. );
  1015. $cursor = $qb->execute();
  1016. while ($data = $cursor->fetch()) {
  1017. $share = $this->createShareObject($data);
  1018. yield $share;
  1019. }
  1020. $cursor->closeCursor();
  1021. }
  1022. }