From e92d34bfcb00a3eec747e1fc3981db5859924aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 8 Dec 2020 13:15:00 +0100 Subject: [PATCH 01/36] Implement share provider for deck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/AppInfo/Application20.php | 13 + lib/Db/BoardMapper.php | 10 + lib/Db/CardMapper.php | 24 +- lib/Service/BoardService.php | 12 +- lib/Service/CardService.php | 20 +- lib/Sharing/DeckShareProvider.php | 1037 +++++++++++++++++++++++++++++ lib/Sharing/Listener.php | 113 ++++ lib/Sharing/ShareAPIHelper.php | 112 ++++ 8 files changed, 1330 insertions(+), 11 deletions(-) create mode 100644 lib/Sharing/DeckShareProvider.php create mode 100644 lib/Sharing/Listener.php create mode 100644 lib/Sharing/ShareAPIHelper.php diff --git a/lib/AppInfo/Application20.php b/lib/AppInfo/Application20.php index 11fbf6619..2646e6424 100644 --- a/lib/AppInfo/Application20.php +++ b/lib/AppInfo/Application20.php @@ -26,6 +26,7 @@ use Closure; use Exception; use OC\EventDispatcher\SymfonyAdapter; +use OC\Share20\ProviderFactory; use OCA\Deck\Activity\CommentEventHandler; use OCA\Deck\Capabilities; use OCA\Deck\Collaboration\Resources\ResourceProvider; @@ -43,6 +44,8 @@ use OCA\Deck\Search\DeckProvider; use OCA\Deck\Service\FullTextSearchService; use OCA\Deck\Service\PermissionService; +use OCA\Deck\Sharing\DeckShareProvider; +use OCA\Deck\Sharing\Listener; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -62,6 +65,8 @@ use OCP\IUser; use OCP\IUserManager; use OCP\Notification\IManager as NotificationManager; +use OCP\Share\IManager; +use OCP\Share\IProviderFactory; use OCP\Util; use Psr\Container\ContainerInterface; @@ -92,6 +97,14 @@ public function boot(IBootContext $context): void { $context->injectFn(Closure::fromCallable([$this, 'registerNotifications'])); $context->injectFn(Closure::fromCallable([$this, 'registerFullTextSearch'])); $context->injectFn(Closure::fromCallable([$this, 'registerCollaborationResources'])); + + $context->injectFn(function (IManager $shareManager) { + $shareManager->registerShareProvider(DeckShareProvider::class); + }); + + $context->injectFn(function (Listener $listener, IEventDispatcher $eventDispatcher) { + $listener->register($eventDispatcher); + }); } public function register(IRegistrationContext $context): void { diff --git a/lib/Db/BoardMapper.php b/lib/Db/BoardMapper.php index 1ef690b8c..7ead31c6c 100644 --- a/lib/Db/BoardMapper.php +++ b/lib/Db/BoardMapper.php @@ -85,6 +85,16 @@ public function find($id, $withLabels = false, $withAcl = false) { return $board; } + public function findAllForUser(string $userId, int $since = -1, $includeArchived = true): array { + $groups = $this->groupManager->getUserGroupIds( + $this->userManager->get($userId) + ); + $userBoards = $this->findAllByUser($userId, null, null, $since, $includeArchived); + $groupBoards = $this->findAllByGroups($userId, $groups,null, null, $since, $includeArchived); + $circleBoards = $this->findAllByCircles($userId, null, null, $since, $includeArchived); + return array_unique(array_merge($userBoards, $groupBoards, $circleBoards)); + } + /** * Find all boards for a given user * diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index b5acf8c6f..d9837e15c 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -149,7 +149,8 @@ public function queryCardsByBoard(int $boardId): IQueryBuilder { public function queryCardsByBoards(array $boardIds): IQueryBuilder { $qb = $this->db->getQueryBuilder(); - $qb->select('c.*') + $qb->select('c.*', 's.board_id') + ->selectAlias('s.title', 'stack_title') ->from('deck_cards', 'c') ->innerJoin('c', 'deck_stacks', 's', $qb->expr()->eq('s.id', 'c.stack_id')) ->andWhere($qb->expr()->in('s.board_id', $qb->createNamedParameter($boardIds, IQueryBuilder::PARAM_INT_ARRAY))); @@ -279,6 +280,27 @@ public function search($boardIds, $term, $limit = null, $offset = null) { return $this->findEntities($qb); } + public function searchRaw($boardIds, $term, $limit = null, $offset = null) { + $qb = $this->queryCardsByBoards($boardIds); + $qb->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%')), + $qb->expr()->iLike('c.description', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%')) + ) + ); + if ($limit !== null) { + $qb->setMaxResults($limit); + } + if ($offset !== null) { + $qb->setFirstResult($offset); + } + $result = $qb->execute(); + $all = $result->fetchAll(); + $result->closeCursor(); + return $all; + } + public function delete(Entity $entity): Entity { // delete assigned labels $this->labelMapper->deleteLabelAssignmentsForCard($entity->getId()); diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index f49a5e706..c524606d5 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -113,13 +113,13 @@ public function setUserId(string $userId): void { $this->userId = $userId; } - public function getUserBoards(int $since = -1, $includeArchived = true): array { - $userInfo = $this->getBoardPrerequisites(); - $userBoards = $this->boardMapper->findAllByUser($userInfo['user'], null, null, $since, $includeArchived); - $groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'],null, null, $since, $includeArchived); - $circleBoards = $this->boardMapper->findAllByCircles($userInfo['user'], null, null, $since, $includeArchived); - return array_unique(array_merge($userBoards, $groupBoards, $circleBoards)); + /** + * Get all boards that are shared with a user, their groups or circles + */ + public function getUserBoards(int $since = -1, bool $includeArchived = true): array { + return $this->boardMapper->findAllForUser($this->userId, $since, $includeArchived); } + /** * @return array */ diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 90cb1ee26..e7d00277f 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -29,6 +29,7 @@ use OCA\Deck\Activity\ActivityManager; use OCA\Deck\Activity\ChangeSet; use OCA\Deck\Db\AssignmentMapper; +use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\Acl; @@ -116,12 +117,23 @@ public function fetchDeleted($boardId) { return $cards; } - public function search($boardIds, $term) { - $cards = $this->cardMapper->search($boardIds, $term); - return $cards; + public function search(string $term, int $limit = null, int $offset = null): array { + $boards = $this->boardService->getUserBoards(); + $boardIds = array_map(static function (Board $board) { + return $board->getId(); + }, $boards); + return $this->cardMapper->search($boardIds, $term, $limit, $offset); } - /** + public function searchRaw(string $term, int $limit = null, int $offset = null): array { + $boards = $this->boardService->getUserBoards(); + $boardIds = array_map(static function (Board $board) { + return $board->getId(); + }, $boards); + return $this->cardMapper->searchRaw($boardIds, $term, $limit, $offset); + } + + /** * @param $cardId * @return \OCA\Deck\Db\RelationalEntity * @throws \OCA\Deck\NoPermissionException diff --git a/lib/Sharing/DeckShareProvider.php b/lib/Sharing/DeckShareProvider.php new file mode 100644 index 000000000..b60ae4bc1 --- /dev/null +++ b/lib/Sharing/DeckShareProvider.php @@ -0,0 +1,1037 @@ + + * + * @author Julius Härtl + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + + +namespace OCA\Deck\Sharing; + + +use OC\Files\Cache\Cache; +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\Board; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\User; +use OCA\Deck\NoPermissionException; +use OCA\Deck\Service\PermissionService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Folder; +use OCP\Files\IMimeTypeLoader; +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; +use OCP\Share\IShare; + +/** Taken from the talk shareapicontroller helper */ +interface IShareProviderBackend { + public function parseDate(string $expireDate): \DateTime; + public function createShare(IShare $share, string $shareWith, int $permissions, string $expireDate): void; + public function formatShare(IShare $share): array; + public function canAccessShare(IShare $share, string $user): bool; +} + +class DeckShareProvider implements \OCP\Share\IShareProvider { + + public const DECK_FOLDER = '/Deck'; + public const DECK_FOLDER_PLACEHOLDER = '/{DECK_PLACEHOLDER}'; + + public const SHARE_TYPE_DECK_USER = IShare::TYPE_DECK_USER; + + /** @var IDBConnection */ + private $dbConnection; + /** @var IManager */ + private $shareManager; + /** @var BoardMapper */ + private $boardMapper; + /** @var CardMapper */ + private $cardMapper; + /** @var PermissionService */ + private $permissionService; + /** @var ITimeFactory */ + private $timeFactory; + private $l; + + public function __construct(IDBConnection $connection, IManager $shareManager, ISecureRandom $secureRandom, BoardMapper $boardMapper, CardMapper $cardMapper, PermissionService $permissionService, IL10N $l) { + $this->dbConnection = $connection; + $this->shareManager = $shareManager; + $this->boardMapper = $boardMapper; + $this->cardMapper = $cardMapper; + $this->permissionService = $permissionService; + $this->l = $l; + $this->timeFactory = \OC::$server->get(ITimeFactory::class); + } + + public static function register(IEventDispatcher $dispatcher): void { + // Register listeners to clean up shares when card/board is deleted + } + + /** + * @inheritDoc + */ + public function identifier() { + return 'deck'; + } + + /** + * @inheritDoc + */ + public function create(IShare $share) { + $cardId = $share->getSharedWith(); + $boardId = $this->cardMapper->findBoardId($cardId); + $valid = $boardId !== null; + try { + $this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_EDIT); + } catch (NoPermissionException $e) { + $valid = false; + } + + try { + $board = $this->boardMapper->find($boardId); + $valid &= !$board->getArchived(); + } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { + $valid = false; + } + + if (!$valid) { + throw new GenericShareException('Card not found', $this->l->t('Card not found'), 404); + } + + $existingShares = $this->getSharesByPath($share->getNode()); + foreach ($existingShares as $existingShare) { + if ($existingShare->getSharedWith() === $share->getSharedWith()) { + throw new GenericShareException('Already shared', $this->l->t('Path is already shared with this card'), 403); + } + } + + // Skipping token generation since we don't have public sharing in deck yet + /*$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_DECK)) + ->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(\OC::$server->get(ITimeFactory::class)->getTime())); + + if ($expirationDate !== null) { + $qb->setValue('expiration', $qb->createNamedParameter($expirationDate, 'datetime')); + } + + $qb->execute(); + + return $qb->getLastInsertId(); + } + + /** + * 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->get(IMimeTypeLoader::class))); + } + + return $share; + } + + /** + * @inheritDoc + */ + public function update(IShare $share) { + $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; + } + + /** + * @inheritDoc + */ + public function delete(IShare $share) { + $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(); + } + + /** + * @inheritDoc + */ + public function deleteFromSelf(IShare $share, $recipient) { + // Check if there is a deck_user share + $qb = $this->dbConnection->getQueryBuilder(); + $stmt = $qb->select(['id', 'permissions']) + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_DECK_USER))) + ->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_DECK_USER), + '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(); + } + } + + /** + * @inheritDoc + */ + 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_DECK_USER)) + )->andWhere( + $qb->expr()->eq('share_with', $qb->createNamedParameter($recipient)) + ); + + $qb->execute(); + + return $this->getShareById($share->getId(), $recipient); + } + + /** + * @inheritDoc + */ + public function move(IShare $share, $recipient) { + // Check if there is a deck user share + $qb = $this->dbConnection->getQueryBuilder(); + $stmt = $qb->select('id') + ->from('share') + ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(self::SHARE_TYPE_DECK_USER))) + ->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 deck user share yet. Create one. + $qb = $this->dbConnection->getQueryBuilder(); + $qb->insert('share') + ->values([ + 'share_type' => $qb->createNamedParameter(self::SHARE_TYPE_DECK_USER), + '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; + } + + /** + * @inheritDoc + */ + public function getSharesInFolder($userId, Folder $node, $reshares) { + $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_DECK)) + ); + + /** + * 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; + } + + /** + * @inheritDoc + */ + public function getSharesBy($userId, $shareType, $node, $reshares, $limit, $offset) { + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select('*') + ->from('share'); + + $qb->andWhere($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_DECK))); + + /** + * 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; + } + + /** + * @inheritDoc + */ + public function getShareById($id, $recipientId = null) { + $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_DECK))); + + $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_DECK))) + ->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 { + $allBoards = $this->boardMapper->findAllForUser($userId); + + /** @var IShare[] $shares */ + $shares = []; + + $start = 0; + while (true) { + $boards = array_slice($allBoards, $start, 100); + $start += 100; + + if ($boards === []) { + break; + } + + // select s.id, dc.title, f.fileid, f.path from oc_share as s left join oc_filecache as f on s.file_source = f.fileid left join oc_storages as st on f.storage = st.numeric_id left join oc_deck_cards as dc on dc.id = s.share_with left join oc_deck_stacks as ds on dc.stack_id = ds.id inner join oc_deck_boards as db on ds.board_id = db.id WHERE db.id = 1; + + $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')) + ->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq('dc.id', 's.share_with')) + ->leftJoin('dc', 'deck_stacks', 'ds', $qb->expr()->eq('dc.stack_id', 'ds.id')) + ->leftJoin('ds', 'deck_boards', 'db', $qb->expr()->eq('ds.board_id', 'db.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()))); + } + + $boards = array_map(function (Board $board) { + return $board->getId(); + }, $boards); + + $qb->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_DECK))) + ->andWhere($qb->expr()->in('db.id', $qb->createNamedParameter( + $boards, + 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; + } + + /** + * Get shared with the card + * + * @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 getSharedWithByType(string $cardId, int $shareType, $limit, $offset): array { + /** @var IShare[] $shares */ + $shares = []; + + $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')) + ->leftJoin('s', 'deck_cards', 'dc', $qb->expr()->eq('dc.id', 's.share_with')); + + if ($limit !== -1) { + $qb->setMaxResults($limit); + } + + $qb->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_DECK))) + ->andWhere($qb->expr()->eq('s.share_with', $qb->createNamedParameter($cardId))) + ->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, \OC::$server->getUserSession()->getUser()->getUID()); + + 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; + } + + /** + * @inheritDoc + */ + public function getShareByToken($token) { + throw new ShareNotFound(); + /*$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);*/ + } + + /** + * @inheritDoc + */ + public function userDeleted($uid, $shareType) { + // TODO: Implement userDeleted() method. + } + + /** + * @inheritDoc + */ + public function groupDeleted($gid) { + // TODO: Implement groupDeleted() method. + } + + /** + * @inheritDoc + */ + public function userDeletedFromGroup($uid, $gid) { + // TODO: Implement userDeletedFromGroup() method. + } + + /** + * 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) { + $ids = []; + foreach ($nodes as $node) { + $ids[] = $node->getId(); + } + + $qb = $this->dbConnection->getQueryBuilder(); + + $types = [IShare::TYPE_DECK]; + if ($currentAccess) { + $types[] = self::SHARE_TYPE_DECK_USER; + } + + $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_DECK) { + $cardId = $row['share_with']; + $boardId = $this->cardMapper->findBoardId($cardId); + if ($boardId === null) { + continue; + } + + $userList = $this->permissionService->findUsers($boardId); + /** @var User $user */ + foreach ($userList as $user) { + $uid = $user->getUID(); + $users[$uid] = $users[$uid] ?? []; + $users[$uid][$row['id']] = $row; + } + } elseif ($type === self::SHARE_TYPE_DECK_USER && $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 { + // Deck 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_DECK_USER) { + 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_DECK))) + ->orderBy('id'); + + $cursor = $qb->execute(); + while ($data = $cursor->fetch()) { + $children[] = $this->createShareObject($data); + } + $cursor->closeCursor(); + + return $children; + } + + /** + * @inheritDoc + */ + 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_DECK)) + ) + ); + + $cursor = $qb->execute(); + while ($data = $cursor->fetch()) { + $share = $this->createShareObject($data); + + yield $share; + } + $cursor->closeCursor(); + } +} diff --git a/lib/Sharing/Listener.php b/lib/Sharing/Listener.php new file mode 100644 index 000000000..7772a0ac0 --- /dev/null +++ b/lib/Sharing/Listener.php @@ -0,0 +1,113 @@ + + * + * @author Julius Härtl + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + + +namespace OCA\Deck\Sharing; + + +use OC\Files\Filesystem; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Share\Events\VerifyMountPointEvent; +use OCP\Share\IShare; +use Symfony\Component\EventDispatcher\GenericEvent; + +class Listener { + + public function __construct($userId) { + $this->userId = $userId; + } + + public function register(IEventDispatcher $dispatcher): void { + /** + * @psalm-suppress UndefinedClass + */ + $dispatcher->addListener('OCP\Share::preShare', [self::class, 'listenPreShare'], 1000); + $dispatcher->addListener(VerifyMountPointEvent::class, [self::class, 'listenVerifyMountPointEvent'], 1000); + } + + public static function listenPreShare(GenericEvent $event): void { + /** @var self $listener */ + $listener = \OC::$server->query(self::class); + $listener->overwriteShareTarget($event); + } + + public static function listenVerifyMountPointEvent(VerifyMountPointEvent $event): void { + /** @var self $listener */ + $listener = \OC::$server->query(self::class); + $listener->overwriteMountPoint($event); + } + + public function overwriteShareTarget(GenericEvent $event): void { + /** @var IShare $share */ + $share = $event->getSubject(); + + if ($share->getShareType() !== IShare::TYPE_DECK + && $share->getShareType() !== DeckShareProvider::SHARE_TYPE_DECK_USER) { + return; + } + + $target = DeckShareProvider::DECK_FOLDER_PLACEHOLDER . '/' . $share->getNode()->getName(); + $target = Filesystem::normalizePath($target); + $share->setTarget($target); + } + + public function overwriteMountPoint(VerifyMountPointEvent $event): void { + $share = $event->getShare(); + $view = $event->getView(); + + if ($share->getShareType() !== IShare::TYPE_DECK + && $share->getShareType() !== DeckShareProvider::SHARE_TYPE_DECK_USER) { + return; + } + + if ($event->getParent() === DeckShareProvider::DECK_FOLDER_PLACEHOLDER) { + try { + $userId = $view->getOwner('/'); + } catch (\Exception $e) { + // If we fail to get the owner of the view from the cache, + // e.g. because the user never logged in but a cron job runs + // We fallback to calculating the owner from the root of the view: + if (substr_count($view->getRoot(), '/') >= 2) { + // /37c09aa0-1b92-4cf6-8c66-86d8cac8c1d0/files + [, $userId, ] = explode('/', $view->getRoot(), 3); + } else { + // Something weird is going on, we can't fallback more + // so for now we don't overwrite the share path ¯\_(ツ)_/¯ + return; + } + } + + $parent = $this->getAttachmentFolder(); + $event->setParent($parent); + if (!$event->getView()->is_dir($parent)) { + $event->getView()->mkdir($parent); + } + } + } + + private function getAttachmentFolder() { + return \OC::$server->getConfig()->getUserValue($this->userId, 'deck', 'attachment_folder', '/Deck'); + } +} diff --git a/lib/Sharing/ShareAPIHelper.php b/lib/Sharing/ShareAPIHelper.php new file mode 100644 index 000000000..4b2dec7c4 --- /dev/null +++ b/lib/Sharing/ShareAPIHelper.php @@ -0,0 +1,112 @@ + + * + * @author Julius Härtl + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + + +namespace OCA\Deck\Sharing; + + +use OCA\Deck\Db\CardMapper; +use OCP\IURLGenerator; +use OCP\Share\IShare; + +class ShareAPIHelper { + + private $urlGenerator; + private $cardMapper; + + public function __construct(IURLGenerator $urlGenerator, CardMapper $cardMapper) { + $this->urlGenerator = $urlGenerator; + $this->cardMapper = $cardMapper; + } + + public function formatShare(IShare $share): array { + $result = []; + $card = $this->cardMapper->find($share->getSharedWith()); + $boardId = $this->cardMapper->findBoardId($card->getId()); + $result['share_with'] = $share->getSharedWith(); + $result['share_with_displayname'] = $card->getTitle(); + $result['share_with_link'] = $this->urlGenerator->linkToRouteAbsolute('deck.page.index') . '#/board/' . $boardId . '/card/' . $card->getId(); + return $result; + } + + public function createShare(IShare $share, string $shareWith, int $permissions, $expireDate) { + $share->setSharedWith($shareWith); + $share->setPermissions($permissions); + + if ($expireDate !== '') { + try { + $expireDate = $this->parseDate($expireDate); + $share->setExpirationDate($expireDate); + } catch (\Exception $e) { + throw new OCSNotFoundException($this->l->t('Invalid date, date format must be YYYY-MM-DD')); + } + } + } + + /** + * Make sure that the passed date is valid ISO 8601 + * So YYYY-MM-DD + * If not throw an exception + * + * Copied from \OCA\Files_Sharing\Controller\ShareAPIController::parseDate. + * + * @param string $expireDate + * @return \DateTime + * @throws \Exception + */ + private function parseDate(string $expireDate): \DateTime { + try { + $date = $this->timeFactory->getDateTime($expireDate); + } catch (\Exception $e) { + throw new \Exception('Invalid date. Format must be YYYY-MM-DD'); + } + + if ($date === false) { + throw new \Exception('Invalid date. Format must be YYYY-MM-DD'); + } + + $date->setTime(0, 0, 0); + + return $date; + } + + /** + * Returns whether the given user can access the given room share or not. + * + * A user can access a room share only if she is a participant of the room. + * + * @param IShare $share + * @param string $user + * @return bool + */ + public function canAccessShare(IShare $share, string $user): bool { + try { + $this->permissionService->checkPermission($this->cardMapper, $share->getSharedWith(), Acl::PERMISSION_READ, $user); + } catch (NoPermissionException $e) { + return false; + } + return true; + } +} From d9ef7afa9e47aa5dae02c4a9ea22d2225f9c31d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 8 Dec 2020 13:15:21 +0100 Subject: [PATCH 02/36] Load viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Controller/PageController.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index 68b603d10..591603608 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -26,7 +26,10 @@ use OCA\Deck\AppInfo\Application; use OCA\Deck\Service\ConfigService; use OCA\Deck\Service\PermissionService; +use OCA\Files\Event\LoadSidebar; +use OCA\Viewer\Event\LoadViewer; use OCP\AppFramework\Http\ContentSecurityPolicy; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IInitialStateService; use OCP\IRequest; use OCP\AppFramework\Http\TemplateResponse; @@ -34,23 +37,24 @@ class PageController extends Controller { private $permissionService; - private $userId; - private $l10n; private $initialState; private $configService; + private $eventDispatcher; public function __construct( $AppName, IRequest $request, PermissionService $permissionService, IInitialStateService $initialStateService, - ConfigService $configService + ConfigService $configService, + IEventDispatcher $eventDispatcher ) { parent::__construct($AppName, $request); $this->permissionService = $permissionService; $this->initialState = $initialStateService; $this->configService = $configService; + $this->eventDispatcher = $eventDispatcher; } /** @@ -65,6 +69,11 @@ public function index() { $this->initialState->provideInitialState(Application::APP_ID, 'canCreate', $this->permissionService->canCreate()); $this->initialState->provideInitialState(Application::APP_ID, 'config', $this->configService->getAll()); + $this->eventDispatcher->dispatchTyped(new LoadSidebar()); + if (class_exists(LoadViewer::class)) { + $this->eventDispatcher->dispatchTyped(new LoadViewer()); + } + $response = new TemplateResponse('deck', 'main'); if (\OC::$server->getConfig()->getSystemValueBool('debug', false)) { From ffd1f677c54a002fcf08044fc3478507bbb24b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 8 Dec 2020 13:15:48 +0100 Subject: [PATCH 03/36] Fetch new attachment type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Service/AttachmentService.php | 69 ++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/lib/Service/AttachmentService.php b/lib/Service/AttachmentService.php index 42f913439..cfb4df917 100644 --- a/lib/Service/AttachmentService.php +++ b/lib/Service/AttachmentService.php @@ -34,11 +34,15 @@ use OCA\Deck\InvalidAttachmentType; use OCA\Deck\NoPermissionException; use OCA\Deck\NotFoundException; +use OCA\Deck\Sharing\DeckShareProvider; use OCA\Deck\StatusException; use OCP\AppFramework\Http\Response; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\ICache; use OCP\ICacheFactory; +use OCP\IDBConnection; use OCP\IL10N; +use OCP\Share\IShare; class AttachmentService { private $attachmentMapper; @@ -57,20 +61,10 @@ class AttachmentService { private $activityManager; /** @var ChangeHelper */ private $changeHelper; + /** @var DeckShareProvider */ + private $shareProvider; - /** - * AttachmentService constructor. - * - * @param AttachmentMapper $attachmentMapper - * @param CardMapper $cardMapper - * @param PermissionService $permissionService - * @param Application $application - * @param ICacheFactory $cacheFactory - * @param $userId - * @param IL10N $l10n - * @throws \OCP\AppFramework\QueryException - */ - public function __construct(AttachmentMapper $attachmentMapper, CardMapper $cardMapper, ChangeHelper $changeHelper, PermissionService $permissionService, Application $application, ICacheFactory $cacheFactory, $userId, IL10N $l10n, ActivityManager $activityManager) { + public function __construct(AttachmentMapper $attachmentMapper, CardMapper $cardMapper, ChangeHelper $changeHelper, PermissionService $permissionService, Application $application, ICacheFactory $cacheFactory, $userId, IL10N $l10n, ActivityManager $activityManager, DeckShareProvider $shareProvider) { $this->attachmentMapper = $attachmentMapper; $this->cardMapper = $cardMapper; $this->permissionService = $permissionService; @@ -80,6 +74,7 @@ public function __construct(AttachmentMapper $attachmentMapper, CardMapper $card $this->l10n = $l10n; $this->activityManager = $activityManager; $this->changeHelper = $changeHelper; + $this->shareProvider = $shareProvider; // Register shipped attachment services // TODO: move this to a plugin based approach once we have different types of attachments @@ -132,7 +127,35 @@ public function findAll($cardId, $withDeleted = false) { // Ingore invalid attachment types when extending the data } } - return $attachments; + + return array_merge($attachments, $this->getFilesAppAttachments($cardId)); + } + + private function getFilesAppAttachments($cardId) { + $userFolder = \OC::$server->getRootFolder()->getUserFolder($this->userId); + $shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0); + return array_map(function (IShare $share) use ($cardId, $userFolder) { + $file = $share->getNode(); + $nodes = $userFolder->getById($file->getId()); + $userNode = array_shift($nodes); + return [ + // general attachment attributes + 'cardId' => $cardId, + 'type' => 'file', + 'data' => $file->getName(), + 'lastModified' => $file->getMTime(), + 'createdAt' => $file->getMTime(), + 'deletedAt' => 0, + // file type attributes + 'fileid' => $file->getId(), + 'path' => $userFolder->getRelativePath($userNode->getPath()), + 'extendedData' => [ + 'filesize' => $file->getSize(), + 'mimetype' => $file->getMimeType(), + 'info' => pathinfo($file->getName()) + ] + ]; + }, $shares); } /** @@ -150,6 +173,24 @@ public function count($cardId) { $count = count($this->attachmentMapper->findAll($cardId)); $this->cache->set('card-' . $cardId, $count); } + + /** @var IDBConnection $qb */ + $db = \OC::$server->getDatabaseConnection(); + $qb = $db->getQueryBuilder(); + $qb->select($qb->createFunction('count(s.id)')) + ->from('share', 's') + ->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_DECK))) + ->andWhere($qb->expr()->eq('s.share_with', $qb->createNamedParameter($cardId))) + ->andWhere($qb->expr()->isNull('s.parent')) + ->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(); + $count += $cursor->fetchColumn(0); + $cursor->closeCursor(); + return $count; } From bd032dfaf486f357c88c11b9ba0449037c5b6971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 8 Dec 2020 13:16:04 +0100 Subject: [PATCH 04/36] Open file in viewer if available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- src/components/card/AttachmentList.vue | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/card/AttachmentList.vue b/src/components/card/AttachmentList.vue index cb54e0a27..7e1339ba1 100644 --- a/src/components/card/AttachmentList.vue +++ b/src/components/card/AttachmentList.vue @@ -45,9 +45,10 @@
  • - +
    - +
    {{ attachment.data }}
    @@ -62,6 +63,12 @@ + + {{ t('deck', 'Show in files') }} + + + {{ t('deck', 'Unshare file') }} + {{ t('deck', 'Delete Attachment') }} @@ -76,7 +83,7 @@ From 715ab2249bbc2c31cddeccd011bb92af4020430e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 8 Dec 2020 13:18:02 +0100 Subject: [PATCH 05/36] Collaboration search provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- appinfo/info.xml | 9 +++- lib/Sharing/DeckPlugin.php | 84 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 lib/Sharing/DeckPlugin.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 2ea78ab09..8b58db841 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -17,7 +17,7 @@ - 🚀 Get your project organized - 1.2.2 + 1.2.2-1 agpl Julius Härtl Deck @@ -80,4 +80,11 @@ OCA\Deck\DAV\CalendarPlugin + + + + OCA\Deck\Sharing\DeckPlugin + + + diff --git a/lib/Sharing/DeckPlugin.php b/lib/Sharing/DeckPlugin.php new file mode 100644 index 000000000..6e0071143 --- /dev/null +++ b/lib/Sharing/DeckPlugin.php @@ -0,0 +1,84 @@ + + * + * @author Julius Härtl + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +declare(strict_types=1); + + +namespace OCA\Deck\Sharing; + + +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\Card; +use OCA\Deck\NoPermissionException; +use OCA\Deck\Service\BoardService; +use OCA\Deck\Service\CardService; +use OCA\Deck\Service\PermissionService; +use OCP\Collaboration\Collaborators\ISearchPlugin; +use OCP\Collaboration\Collaborators\ISearchResult; +use OCP\Collaboration\Collaborators\SearchResultType; +use OCP\Share\IShare; + +class DeckPlugin implements ISearchPlugin { + + /** + * @var BoardService + */ + private $boardService; + /** + * @var CardService + */ + private $cardService; + + public function __construct(BoardService $boardService, CardService $cardService) { + $this->boardService = $boardService; + $this->cardService = $cardService; + } + + public function search($search, $limit, $offset, ISearchResult $searchResult) { + $result = ['wide' => [], 'exact' => []]; + + $cards = $this->cardService->searchRaw($search, $limit, $offset); + /** @var PermissionService $permissionsService */ + $permissionsService = \OC::$server->get(PermissionService::class); + foreach ($cards as $card) { + try { + $permissionsService->checkPermission(null, $card['board_id'], Acl::PERMISSION_EDIT); + } catch (NoPermissionException $e) { + continue; + } + $board = $this->boardService->find($card['board_id']); + + $result['wide'][] = [ + 'label' => $card['title'], + 'value' => [ + 'shareType' => IShare::TYPE_DECK, + 'shareWith' => (string)$card['id'] + ], + 'shareWithDescription' => $board->getTitle() . ' – ' . $card['stack_title'], + ]; + } + $type = new SearchResultType('deck'); + $searchResult->addResultSet($type, $result['wide'], $result['exact']); + return false; + } +} From 02237040d49c88dc9df8f071809dc54f3ddb79b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Wed, 16 Dec 2020 15:47:04 +0100 Subject: [PATCH 06/36] Handle nonexisting share provider registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- appinfo/info.xml | 2 +- lib/AppInfo/Application20.php | 4 +++- lib/Sharing/ShareAPIHelper.php | 11 ++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 8b58db841..2e490ba6a 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -36,7 +36,7 @@ pgsql sqlite mysql - + OCA\Deck\Cron\DeleteCron diff --git a/lib/AppInfo/Application20.php b/lib/AppInfo/Application20.php index 2646e6424..80cd94b62 100644 --- a/lib/AppInfo/Application20.php +++ b/lib/AppInfo/Application20.php @@ -99,7 +99,9 @@ public function boot(IBootContext $context): void { $context->injectFn(Closure::fromCallable([$this, 'registerCollaborationResources'])); $context->injectFn(function (IManager $shareManager) { - $shareManager->registerShareProvider(DeckShareProvider::class); + if (method_exists($shareManager, 'registerShareProvider')) { + $shareManager->registerShareProvider(DeckShareProvider::class); + } }); $context->injectFn(function (Listener $listener, IEventDispatcher $eventDispatcher) { diff --git a/lib/Sharing/ShareAPIHelper.php b/lib/Sharing/ShareAPIHelper.php index 4b2dec7c4..31472000a 100644 --- a/lib/Sharing/ShareAPIHelper.php +++ b/lib/Sharing/ShareAPIHelper.php @@ -27,18 +27,27 @@ namespace OCA\Deck\Sharing; +use OCA\Deck\Db\Acl; use OCA\Deck\Db\CardMapper; +use OCA\Deck\NoPermissionException; +use OCA\Deck\Service\PermissionService; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\Utility\ITimeFactory; use OCP\IURLGenerator; use OCP\Share\IShare; class ShareAPIHelper { private $urlGenerator; + private $timeFactory; private $cardMapper; + private $permissionService; - public function __construct(IURLGenerator $urlGenerator, CardMapper $cardMapper) { + public function __construct(IURLGenerator $urlGenerator, ITimeFactory $timeFactory, CardMapper $cardMapper, PermissionService $permissionService) { $this->urlGenerator = $urlGenerator; + $this->timeFactory = $timeFactory; $this->cardMapper = $cardMapper; + $this->permissionService = $permissionService; } public function formatShare(IShare $share): array { From 67c90b1da8d1370c52b88e17413b7e63eb339cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Wed, 16 Dec 2020 16:48:11 +0100 Subject: [PATCH 07/36] Attachments sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Service/AttachmentService.php | 8 ++- src/components/card/AttachmentList.vue | 83 ++++++++++++++++++++------ 2 files changed, 71 insertions(+), 20 deletions(-) diff --git a/lib/Service/AttachmentService.php b/lib/Service/AttachmentService.php index cfb4df917..6764252b5 100644 --- a/lib/Service/AttachmentService.php +++ b/lib/Service/AttachmentService.php @@ -42,6 +42,7 @@ use OCP\ICacheFactory; use OCP\IDBConnection; use OCP\IL10N; +use OCP\IPreview; use OCP\Share\IShare; class AttachmentService { @@ -132,9 +133,11 @@ public function findAll($cardId, $withDeleted = false) { } private function getFilesAppAttachments($cardId) { + /** @var IPreview $previewManager */ + $previewManager = \OC::$server->get(IPreview::class); $userFolder = \OC::$server->getRootFolder()->getUserFolder($this->userId); $shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0); - return array_map(function (IShare $share) use ($cardId, $userFolder) { + return array_map(function (IShare $share) use ($cardId, $userFolder, $previewManager) { $file = $share->getNode(); $nodes = $userFolder->getById($file->getId()); $userNode = array_shift($nodes); @@ -152,7 +155,8 @@ private function getFilesAppAttachments($cardId) { 'extendedData' => [ 'filesize' => $file->getSize(), 'mimetype' => $file->getMimeType(), - 'info' => pathinfo($file->getName()) + 'info' => pathinfo($file->getName()), + 'hasPreview' => $previewManager->isAvailable($file), ] ]; }, $shares); diff --git a/src/components/card/AttachmentList.vue b/src/components/card/AttachmentList.vue index 7e1339ba1..048bffd73 100644 --- a/src/components/card/AttachmentList.vue +++ b/src/components/card/AttachmentList.vue @@ -22,9 +22,14 @@