. * */ namespace OCA\Talk\Share; use OC\Files\Cache\Cache; use OCA\Talk\Events\ParticipantEvent; use OCA\Talk\Events\RemoveUserEvent; use OCA\Talk\Events\RoomEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Manager; use OCA\Talk\Participant; use OCA\Talk\Room; use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Folder; use OCP\Files\Node; use OCP\IDBConnection; use OCP\IL10N; use OCP\Security\ISecureRandom; use OCP\Share\Exceptions\GenericShareException; use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\IManager as IShareManager; use OCP\Share\IShare; use OCP\Share\IShareProvider; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; /** * Share provider for room shares. * * Files are shared with a room identified by its token; only users currently in * the room can share with and access the shared files (although the access * checks are not enforced by the provider, but done on a higher layer). * * Like in group shares, a recipient can move or delete a share without * modifying the share for the other users in the room. */ class RoomShareProvider implements IShareProvider { // Special share type for user modified room shares public const SHARE_TYPE_USERROOM = 11; public const TALK_FOLDER = '/Talk'; public const TALK_FOLDER_PLACEHOLDER = '/{TALK_PLACEHOLDER}'; /** @var IDBConnection */ private $dbConnection; /** @var ISecureRandom */ private $secureRandom; /** @var IShareManager */ private $shareManager; /** @var EventDispatcherInterface */ private $dispatcher; /** @var Manager */ private $manager; /** @var ITimeFactory */ protected $timeFactory; /** @var IL10N */ private $l; public function __construct( IDBConnection $connection, ISecureRandom $secureRandom, IShareManager $shareManager, EventDispatcherInterface $dispatcher, Manager $manager, ITimeFactory $timeFactory, IL10N $l ) { $this->dbConnection = $connection; $this->secureRandom = $secureRandom; $this->shareManager = $shareManager; $this->dispatcher = $dispatcher; $this->manager = $manager; $this->timeFactory = $timeFactory; $this->l = $l; } public static function register(IEventDispatcher $dispatcher): void { $listener = static function (ParticipantEvent $event) { $room = $event->getRoom(); if ($event->getParticipant()->getParticipantType() === Participant::USER_SELF_JOINED) { /** @var self $roomShareProvider */ $roomShareProvider = \OC::$server->query(self::class); $roomShareProvider->deleteInRoom($room->getToken(), $event->getParticipant()->getUser()); } }; $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DISCONNECT, $listener); $listener = static function (RemoveUserEvent $event) { $room = $event->getRoom(); /** @var self $roomShareProvider */ $roomShareProvider = \OC::$server->query(self::class); $roomShareProvider->deleteInRoom($room->getToken(), $event->getUser()->getUID()); }; $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, $listener); $listener = static function (RoomEvent $event) { $room = $event->getRoom(); /** @var self $roomShareProvider */ $roomShareProvider = \OC::$server->query(self::class); $roomShareProvider->deleteInRoom($room->getToken()); }; $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DELETE, $listener); } /** * Return the identifier of this provider. * * @return string Containing only [a-zA-Z0-9] */ public function identifier(): string { return 'ocRoomShare'; } /** * Create a share * * @param IShare $share * @return IShare The share object * @throws GenericShareException */ public function create(IShare $share): IShare { try { $room = $this->manager->getRoomByToken($share->getSharedWith(), $share->getSharedBy()); } catch (RoomNotFoundException $e) { throw new GenericShareException('Room not found', $this->l->t('Conversation not found'), 404); } if ($room->getReadOnly() === Room::READ_ONLY) { throw new GenericShareException('Room not found', $this->l->t('Conversation not found'), 404); } try { $room->getParticipant($share->getSharedBy()); } catch (ParticipantNotFoundException $e) { // If the sharer is not a participant of the room even if the room // exists the error is still "Room not found". throw new GenericShareException('Room not found', $this->l->t('Conversation not found'), 404); } $existingShares = $this->getSharesByPath($share->getNode()); foreach ($existingShares as $existingShare) { if ($existingShare->getSharedWith() === $share->getSharedWith()) { // FIXME Should be moved away from GenericEvent as soon as OCP\Share20\IManager did move too $this->dispatcher->dispatch(self::class . '::' . 'share_file_again', new GenericEvent($existingShare)); throw new GenericShareException('Already shared', $this->l->t('Path is already shared with this room'), 403); } } $share->setToken( $this->secureRandom->generate( 15, // \OC\Share\Constants::TOKEN_LENGTH \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE ) ); $shareId = $this->addShareToDB( $share->getSharedWith(), $share->getSharedBy(), $share->getShareOwner(), $share->getNodeType(), $share->getNodeId(), $share->getTarget(), $share->getPermissions(), $share->getToken(), $share->getExpirationDate() ); $data = $this->getRawShare($shareId); return $this->createShareObject($data); } /** * Add share to the database and return the ID * * @param string $shareWith * @param string $sharedBy * @param string $shareOwner * @param string $itemType * @param int $itemSource * @param string $target * @param int $permissions * @param string $token * @param \DateTime|null $expirationDate * @return int */ private function addShareToDB( string $shareWith, string $sharedBy, string $shareOwner, string $itemType, int $itemSource, string $target, int $permissions, string $token, ?\DateTime $expirationDate ): int { $qb = $this->dbConnection->getQueryBuilder(); $qb->insert('share') ->setValue('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)) ->setValue('share_with', $qb->createNamedParameter($shareWith)) ->setValue('uid_initiator', $qb->createNamedParameter($sharedBy)) ->setValue('uid_owner', $qb->createNamedParameter($shareOwner)) ->setValue('item_type', $qb->createNamedParameter($itemType)) ->setValue('item_source', $qb->createNamedParameter($itemSource)) ->setValue('file_source', $qb->createNamedParameter($itemSource)) ->setValue('file_target', $qb->createNamedParameter($target)) ->setValue('permissions', $qb->createNamedParameter($permissions)) ->setValue('token', $qb->createNamedParameter($token)) ->setValue('stime', $qb->createNamedParameter($this->timeFactory->getTime())); if ($expirationDate !== null) { $qb->setValue('expiration', $qb->createNamedParameter($expirationDate, 'datetime')); } $qb->execute(); $id = $qb->getLastInsertId(); return $id; } /** * Get database row of the given share * * @param int $id * @return array * @throws ShareNotFound */ private function getRawShare(int $id): array { $qb = $this->dbConnection->getQueryBuilder(); $qb->select('*') ->from('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); $cursor = $qb->execute(); $data = $cursor->fetch(); $cursor->closeCursor(); if ($data === false) { throw new ShareNotFound(); } return $data; } /** * Create a share object from a database row * * @param array $data * @return IShare */ private function createShareObject(array $data): IShare { $share = $this->shareManager->newShare(); $share->setId((int)$data['id']) ->setShareType((int)$data['share_type']) ->setPermissions((int)$data['permissions']) ->setTarget($data['file_target']) ->setStatus((int)$data['accepted']) ->setToken($data['token']); $shareTime = $this->timeFactory->getDateTime(); $shareTime->setTimestamp((int)$data['stime']); $share->setShareTime($shareTime); $share->setSharedWith($data['share_with']); $share->setSharedBy($data['uid_initiator']); $share->setShareOwner($data['uid_owner']); if ($data['expiration'] !== null) { $expiration = \DateTime::createFromFormat('Y-m-d H:i:s', $data['expiration']); if ($expiration !== false) { $share->setExpirationDate($expiration); } } $share->setNodeId((int)$data['file_source']); $share->setNodeType($data['item_type']); $share->setProviderId($this->identifier()); if (isset($data['f_permissions'])) { $entryData = $data; $entryData['permissions'] = $entryData['f_permissions']; $entryData['parent'] = $entryData['f_parent']; $share->setNodeCacheEntry(Cache::cacheEntryFromData($entryData, \OC::$server->getMimeTypeLoader())); } return $share; } /** * Update a share * * @param IShare $share * @return IShare The share object */ public function update(IShare $share): IShare { $qb = $this->dbConnection->getQueryBuilder(); $qb->update('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))) ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) ->execute(); /* * Update all user defined group shares */ $qb = $this->dbConnection->getQueryBuilder(); $qb->update('share') ->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) ->set('uid_owner', $qb->createNamedParameter($share->getShareOwner())) ->set('uid_initiator', $qb->createNamedParameter($share->getSharedBy())) ->set('item_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('file_source', $qb->createNamedParameter($share->getNode()->getId())) ->set('expiration', $qb->createNamedParameter($share->getExpirationDate(), IQueryBuilder::PARAM_DATE)) ->execute(); /* * Now update the permissions for all children that have not set it to 0 */ $qb = $this->dbConnection->getQueryBuilder(); $qb->update('share') ->where($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) ->andWhere($qb->expr()->neq('permissions', $qb->createNamedParameter(0))) ->set('permissions', $qb->createNamedParameter($share->getPermissions())) ->execute(); return $share; } /** * Delete a share * * @param IShare $share */ public function delete(IShare $share): void { $qb = $this->dbConnection->getQueryBuilder(); $qb->delete('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($share->getId()))); $qb->orWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))); $qb->execute(); } /** * Unshare a file from self as recipient. * * If a user unshares a room share from their self then the original room * share should still exist. * * @param IShare $share * @param string $recipient UserId of the recipient */ public function deleteFromSelf(IShare $share, $recipient): void { // Check if there is a userroom share $qb = $this->dbConnection->getQueryBuilder(); $stmt = $qb->select(['id', 'permissions']) ->from('share') ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($recipient))) ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) ->andWhere($qb->expr()->orX( $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) )) ->execute(); $data = $stmt->fetch(); $stmt->closeCursor(); if ($data === false) { // No userroom share yet. Create one. $qb = $this->dbConnection->getQueryBuilder(); $qb->insert('share') ->values([ 'share_type' => $qb->createNamedParameter(self::SHARE_TYPE_USERROOM), 'share_with' => $qb->createNamedParameter($recipient), 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()), 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()), 'parent' => $qb->createNamedParameter($share->getId()), 'item_type' => $qb->createNamedParameter($share->getNodeType()), 'item_source' => $qb->createNamedParameter($share->getNodeId()), 'file_source' => $qb->createNamedParameter($share->getNodeId()), 'file_target' => $qb->createNamedParameter($share->getTarget()), 'permissions' => $qb->createNamedParameter(0), 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()), ])->execute(); } elseif ($data['permissions'] !== 0) { // Already a userroom share. Update it. $qb = $this->dbConnection->getQueryBuilder(); $qb->update('share') ->set('permissions', $qb->createNamedParameter(0)) ->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id']))) ->execute(); } } /** * Restore a share for a given recipient. The implementation could be provider independant. * * @param IShare $share * @param string $recipient * @return IShare The restored share object * @throws GenericShareException In case the share could not be restored */ public function restore(IShare $share, string $recipient): IShare { $qb = $this->dbConnection->getQueryBuilder(); $qb->select('permissions') ->from('share') ->where( $qb->expr()->eq('id', $qb->createNamedParameter($share->getId())) ); $cursor = $qb->execute(); $data = $cursor->fetch(); $cursor->closeCursor(); $originalPermission = $data['permissions']; $qb = $this->dbConnection->getQueryBuilder(); $qb->update('share') ->set('permissions', $qb->createNamedParameter($originalPermission)) ->where( $qb->expr()->eq('parent', $qb->createNamedParameter($share->getId())) )->andWhere( $qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM)) )->andWhere( $qb->expr()->eq('share_with', $qb->createNamedParameter($recipient)) ); $qb->execute(); return $this->getShareById($share->getId(), $recipient); } /** * Move a share as a recipient. * * This is updating the share target. Thus the mount point of the recipient. * This may require special handling. If a user moves a room share * the target should only be changed for them. * * @param IShare $share * @param string $recipient userId of recipient * @return IShare */ public function move(IShare $share, $recipient): IShare { // Check if there is a userroom share $qb = $this->dbConnection->getQueryBuilder(); $stmt = $qb->select('id') ->from('share') ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($recipient))) ->andWhere($qb->expr()->eq('parent', $qb->createNamedParameter($share->getId()))) ->andWhere($qb->expr()->orX( $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) )) ->setMaxResults(1) ->execute(); $data = $stmt->fetch(); $stmt->closeCursor(); if ($data === false) { // No userroom share yet. Create one. $qb = $this->dbConnection->getQueryBuilder(); $qb->insert('share') ->values([ 'share_type' => $qb->createNamedParameter(self::SHARE_TYPE_USERROOM), 'share_with' => $qb->createNamedParameter($recipient), 'uid_owner' => $qb->createNamedParameter($share->getShareOwner()), 'uid_initiator' => $qb->createNamedParameter($share->getSharedBy()), 'parent' => $qb->createNamedParameter($share->getId()), 'item_type' => $qb->createNamedParameter($share->getNodeType()), 'item_source' => $qb->createNamedParameter($share->getNodeId()), 'file_source' => $qb->createNamedParameter($share->getNodeId()), 'file_target' => $qb->createNamedParameter($share->getTarget()), 'permissions' => $qb->createNamedParameter($share->getPermissions()), 'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()), ])->execute(); } else { // Already a userroom share. Update it. $qb = $this->dbConnection->getQueryBuilder(); $qb->update('share') ->set('file_target', $qb->createNamedParameter($share->getTarget())) ->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id']))) ->execute(); } return $share; } /** * Get all shares by the given user in a folder * * @param string $userId * @param Folder $node * @param bool $reshares Also get the shares where $user is the owner instead of just the shares where $user is the initiator * @return IShare[] */ public function getSharesInFolder($userId, Folder $node, $reshares): array { $qb = $this->dbConnection->getQueryBuilder(); $qb->select('*') ->from('share', 's') ->andWhere($qb->expr()->orX( $qb->expr()->eq('s.item_type', $qb->createNamedParameter('file')), $qb->expr()->eq('s.item_type', $qb->createNamedParameter('folder')) )) ->andWhere( $qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)) ); /** * Reshares for this user are shares where they are the owner. */ if ($reshares === false) { $qb->andWhere($qb->expr()->eq('s.uid_initiator', $qb->createNamedParameter($userId))); } else { $qb->andWhere( $qb->expr()->orX( $qb->expr()->eq('s.uid_owner', $qb->createNamedParameter($userId)), $qb->expr()->eq('s.uid_initiator', $qb->createNamedParameter($userId)) ) ); } $qb->innerJoin('s', 'filecache' ,'f', $qb->expr()->eq('s.file_source', 'f.fileid')); $qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId()))); $qb->orderBy('s.id'); $cursor = $qb->execute(); $shares = []; while ($data = $cursor->fetch()) { $shares[$data['fileid']][] = $this->createShareObject($data); } $cursor->closeCursor(); return $shares; } /** * Get all shares by the given user * * @param string $userId * @param int $shareType * @param Node|null $node * @param bool $reshares Also get the shares where $user is the owner instead of just the shares where $user is the initiator * @param int $limit The maximum number of shares to be returned, -1 for all shares * @param int $offset * @return IShare[] */ public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset): array { $qb = $this->dbConnection->getQueryBuilder(); $qb->select('*') ->from('share'); $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))); /** * Reshares for this user are shares where they are the owner. */ if ($reshares === false) { $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId))); } else { $qb->andWhere( $qb->expr()->orX( $qb->expr()->eq('uid_owner', $qb->createNamedParameter($userId)), $qb->expr()->eq('uid_initiator', $qb->createNamedParameter($userId)) ) ); } if ($node !== null) { $qb->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($node->getId()))); } if ($limit !== -1) { $qb->setMaxResults($limit); } $qb->setFirstResult($offset); $qb->orderBy('id'); $cursor = $qb->execute(); $shares = []; while ($data = $cursor->fetch()) { $shares[] = $this->createShareObject($data); } $cursor->closeCursor(); return $shares; } /** * Get share by id * * @param int $id * @param string|null $recipientId * @return IShare * @throws ShareNotFound */ public function getShareById($id, $recipientId = null): IShare { $qb = $this->dbConnection->getQueryBuilder(); $qb->select('s.*', 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' ) ->selectAlias('st.id', 'storage_string_id') ->from('share', 's') ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')) ->where($qb->expr()->eq('s.id', $qb->createNamedParameter($id))) ->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))); $cursor = $qb->execute(); $data = $cursor->fetch(); $cursor->closeCursor(); if ($data === false) { throw new ShareNotFound(); } if (!$this->isAccessibleResult($data)) { throw new ShareNotFound(); } $share = $this->createShareObject($data); if ($recipientId !== null) { $share = $this->resolveSharesForRecipient([$share], $recipientId)[0]; } return $share; } /** * Returns each given share as seen by the given recipient. * * If the recipient has not modified the share the original one is returned * instead. * * @param IShare[] $shares * @param string $userId * @return IShare[] */ private function resolveSharesForRecipient(array $shares, string $userId): array { $result = []; $start = 0; while (true) { /** @var IShare[] $shareSlice */ $shareSlice = array_slice($shares, $start, 100); $start += 100; if ($shareSlice === []) { break; } /** @var int[] $ids */ $ids = []; /** @var IShare[] $shareMap */ $shareMap = []; foreach ($shareSlice as $share) { $ids[] = (int)$share->getId(); $shareMap[$share->getId()] = $share; } $qb = $this->dbConnection->getQueryBuilder(); $query = $qb->select('*') ->from('share') ->where($qb->expr()->in('parent', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($userId))) ->andWhere($qb->expr()->orX( $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) )); $stmt = $query->execute(); while ($data = $stmt->fetch()) { $shareMap[$data['parent']]->setPermissions((int)$data['permissions']); $shareMap[$data['parent']]->setTarget($data['file_target']); } $stmt->closeCursor(); foreach ($shareMap as $share) { $result[] = $share; } } return $result; } /** * Get shares for a given path * * @param Node $path * @return IShare[] */ public function getSharesByPath(Node $path): array { $qb = $this->dbConnection->getQueryBuilder(); $cursor = $qb->select('*') ->from('share') ->andWhere($qb->expr()->eq('file_source', $qb->createNamedParameter($path->getId()))) ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) ->execute(); $shares = []; while ($data = $cursor->fetch()) { $shares[] = $this->createShareObject($data); } $cursor->closeCursor(); return $shares; } /** * Get shared with the given user * * @param string $userId get shares where this user is the recipient * @param int $shareType * @param Node|null $node * @param int $limit The max number of entries returned, -1 for all * @param int $offset * @return IShare[] */ public function getSharedWith($userId, $shareType, $node, $limit, $offset): array { $allRooms = $this->manager->getRoomsForParticipant($userId); /** @var IShare[] $shares */ $shares = []; $start = 0; while (true) { $rooms = array_slice($allRooms, $start, 100); $start += 100; if ($rooms === []) { break; } $qb = $this->dbConnection->getQueryBuilder(); $qb->select('s.*', 'f.fileid', 'f.path', 'f.permissions AS f_permissions', 'f.storage', 'f.path_hash', 'f.parent AS f_parent', 'f.name', 'f.mimetype', 'f.mimepart', 'f.size', 'f.mtime', 'f.storage_mtime', 'f.encrypted', 'f.unencrypted_size', 'f.etag', 'f.checksum' ) ->selectAlias('st.id', 'storage_string_id') ->from('share', 's') ->orderBy('s.id') ->leftJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid')) ->leftJoin('f', 'storages', 'st', $qb->expr()->eq('f.storage', 'st.numeric_id')); if ($limit !== -1) { $qb->setMaxResults($limit); } // Filter by node if provided if ($node !== null) { $qb->andWhere($qb->expr()->eq('s.file_source', $qb->createNamedParameter($node->getId()))); } $rooms = array_map(function (Room $room) { return $room->getToken(); }, $rooms); $qb->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) ->andWhere($qb->expr()->in('s.share_with', $qb->createNamedParameter( $rooms, IQueryBuilder::PARAM_STR_ARRAY ))) ->andWhere($qb->expr()->orX( $qb->expr()->eq('s.item_type', $qb->createNamedParameter('file')), $qb->expr()->eq('s.item_type', $qb->createNamedParameter('folder')) )); $cursor = $qb->execute(); while ($data = $cursor->fetch()) { if (!$this->isAccessibleResult($data)) { continue; } if ($offset > 0) { $offset--; continue; } $shares[] = $this->createShareObject($data); } $cursor->closeCursor(); } $shares = $this->resolveSharesForRecipient($shares, $userId); return $shares; } private function isAccessibleResult(array $data): bool { // exclude shares leading to deleted file entries if ($data['fileid'] === null || $data['path'] === null) { return false; } // exclude shares leading to trashbin on home storages $pathSections = explode('/', $data['path'], 2); // FIXME: would not detect rare md5'd home storage case properly if ($pathSections[0] !== 'files' && in_array(explode(':', $data['storage_string_id'], 2)[0], ['home', 'object'])) { return false; } return true; } /** * Get a share by token * * Note that token here refers to share token, not room token. * * @param string $token * @return IShare * @throws ShareNotFound */ public function getShareByToken($token): IShare { $qb = $this->dbConnection->getQueryBuilder(); $cursor = $qb->select('*') ->from('share') ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) ->andWhere($qb->expr()->eq('token', $qb->createNamedParameter($token))) ->execute(); $data = $cursor->fetch(); if ($data === false) { throw new ShareNotFound(); } $roomToken = $data['share_with']; try { $room = $this->manager->getRoomByToken($roomToken); } catch (RoomNotFoundException $e) { throw new ShareNotFound(); } if ($room->getType() !== Room::PUBLIC_CALL) { throw new ShareNotFound(); } return $this->createShareObject($data); } /** * A user is deleted from the system * So clean up the relevant shares. * * @param string $uid * @param int $shareType */ public function userDeleted($uid, $shareType): void { // A deleted user is handled automatically by the room hooks due to the // user being removed from the room. } /** * A group is deleted from the system. * We have to clean up all shares to this group. * Providers not handling group shares should just return * * @param string $gid */ public function groupDeleted($gid): void { } /** * A user is deleted from a group * We have to clean up all the related user specific group shares * Providers not handling group shares should just return * * @param string $uid * @param string $gid */ public function userDeletedFromGroup($uid, $gid): void { } /** * Get the access list to the array of provided nodes. * * @see IManager::getAccessList() for sample docs * * @param Node[] $nodes The list of nodes to get access for * @param bool $currentAccess If current access is required (like for removed shares that might get revived later) * @return array */ public function getAccessList($nodes, $currentAccess): array { $ids = []; foreach ($nodes as $node) { $ids[] = $node->getId(); } $qb = $this->dbConnection->getQueryBuilder(); $types = [IShare::TYPE_ROOM]; if ($currentAccess) { $types[] = self::SHARE_TYPE_USERROOM; } $qb->select('id', 'parent', 'share_type', 'share_with', 'file_source', 'file_target', 'permissions') ->from('share') ->where($qb->expr()->in('share_type', $qb->createNamedParameter($types, IQueryBuilder::PARAM_INT_ARRAY))) ->andWhere($qb->expr()->in('file_source', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))) ->andWhere($qb->expr()->orX( $qb->expr()->eq('item_type', $qb->createNamedParameter('file')), $qb->expr()->eq('item_type', $qb->createNamedParameter('folder')) )); $cursor = $qb->execute(); $users = []; while ($row = $cursor->fetch()) { $type = (int)$row['share_type']; if ($type === IShare::TYPE_ROOM) { $roomToken = $row['share_with']; try { $room = $this->manager->getRoomByToken($roomToken); } catch (RoomNotFoundException $e) { continue; } $userList = $room->getParticipantUserIds(); foreach ($userList as $uid) { $users[$uid] = $users[$uid] ?? []; $users[$uid][$row['id']] = $row; } } elseif ($type === self::SHARE_TYPE_USERROOM && $currentAccess === true) { $uid = $row['share_with']; $users[$uid] = $users[$uid] ?? []; $users[$uid][$row['id']] = $row; } } $cursor->closeCursor(); if ($currentAccess === true) { $users = array_map([$this, 'filterSharesOfUser'], $users); $users = array_filter($users); } else { $users = array_keys($users); } return ['users' => $users]; } /** * For each user the path with the fewest slashes is returned * @param array $shares * @return array */ protected function filterSharesOfUser(array $shares): array { // Room shares when the user has a share exception foreach ($shares as $id => $share) { $type = (int) $share['share_type']; $permissions = (int) $share['permissions']; if ($type === self::SHARE_TYPE_USERROOM) { unset($shares[$share['parent']]); if ($permissions === 0) { unset($shares[$id]); } } } $best = []; $bestDepth = 0; foreach ($shares as $id => $share) { $depth = substr_count($share['file_target'], '/'); if (empty($best) || $depth < $bestDepth) { $bestDepth = $depth; $best = [ 'node_id' => $share['file_source'], 'node_path' => $share['file_target'], ]; } } return $best; } /** * Get all children of this share * * Not part of IShareProvider API, but needed by OC\Share20\Manager. * * @param IShare $parent * @return IShare[] */ public function getChildren(IShare $parent): array { $children = []; $qb = $this->dbConnection->getQueryBuilder(); $qb->select('*') ->from('share') ->where($qb->expr()->eq('parent', $qb->createNamedParameter($parent->getId()))) ->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) ->orderBy('id'); $cursor = $qb->execute(); while ($data = $cursor->fetch()) { $children[] = $this->createShareObject($data); } $cursor->closeCursor(); return $children; } /** * Delete all shares in a room, or only those from the given user. * * When a user is given all her shares are removed, both own shares and * received shares. * * Not part of IShareProvider API, but needed by the hooks in * OCA\Talk\AppInfo\Application * * @param string $roomToken * @param string|null $user */ public function deleteInRoom(string $roomToken, string $user = null): void { //First delete all custom room shares for the original shares to be removed $qb = $this->dbConnection->getQueryBuilder(); $qb->select('id') ->from('share') ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($roomToken))); if ($user !== null) { $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($user))); } $cursor = $qb->execute(); $ids = []; while ($row = $cursor->fetch()) { $ids[] = (int)$row['id']; } $cursor->closeCursor(); if (!empty($ids)) { $chunks = array_chunk($ids, 100); foreach ($chunks as $chunk) { $qb->delete('share') ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM))) ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); $qb->execute(); } } // Now delete all the original room shares $qb = $this->dbConnection->getQueryBuilder(); $qb->delete('share') ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($roomToken))); if ($user !== null) { $qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($user))); } $qb->execute(); // Finally delete all custom room shares leftovers for the given user if ($user !== null) { $qb = $this->dbConnection->getQueryBuilder(); $qb->select('id') ->from('share') ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM))) ->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($roomToken))); $cursor = $qb->execute(); $ids = []; while ($row = $cursor->fetch()) { $ids[] = (int)$row['id']; } $cursor->closeCursor(); if (!empty($ids)) { $chunks = array_chunk($ids, 100); foreach ($chunks as $chunk) { $qb->delete('share') ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_USERROOM))) ->andWhere($qb->expr()->in('share_with', $qb->createNamedParameter($user))) ->andWhere($qb->expr()->in('parent', $qb->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY))); $qb->execute(); } } } } /** * Get all the shares in this provider returned as iterable to reduce memory * overhead * * @return iterable * @since 18.0.0 */ public function getAllShares(): iterable { $qb = $this->dbConnection->getQueryBuilder(); $qb->select('*') ->from('share') ->where( $qb->expr()->orX( $qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)) ) ); $cursor = $qb->execute(); while ($data = $cursor->fetch()) { $share = $this->createShareObject($data); yield $share; } $cursor->closeCursor(); } }