From 1e54d77f7aa357ecb561d24426ec7912fc2f3e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 17 Aug 2020 17:12:34 +0200 Subject: [PATCH 01/13] Move to IBootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- appinfo/app.php | 39 ---------------- appinfo/info.xml | 9 ++++ lib/AppInfo/Application.php | 69 +++++++++++----------------- lib/Search/CardSearchResultEntry.php | 38 +++++++++++++++ 4 files changed, 75 insertions(+), 80 deletions(-) delete mode 100644 appinfo/app.php create mode 100644 lib/Search/CardSearchResultEntry.php diff --git a/appinfo/app.php b/appinfo/app.php deleted file mode 100644 index bf0ac6c98..000000000 --- a/appinfo/app.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * @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 . - * - */ - -use OCA\Deck\AppInfo\Application; -use OCP\AppFramework\QueryException; - -if ((@include_once __DIR__ . '/../vendor/autoload.php')=== false) { - throw new Exception('Cannot include autoload. Did you run install dependencies using composer?'); -} - -try { - /** @var Application $app */ - $app = \OC::$server->query(Application::class); - $app->register(); -} catch (QueryException $e) { -} - -/** Load activity style global so it is availabile in the activity app as well */ -\OC_Util::addStyle('deck', 'activity'); diff --git a/appinfo/info.xml b/appinfo/info.xml index 34c1ed4ed..95fd78a95 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -69,4 +69,13 @@ OCA\Deck\Provider\DeckProvider + + + Deck + deck.page.index + deck.svg + 10 + + + diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d4fd3a7a9..e0adc9d17 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -38,9 +38,13 @@ use OCA\Deck\Middleware\DefaultBoardMiddleware; use OCA\Deck\Middleware\ExceptionMiddleware; use OCA\Deck\Notification\Notifier; +use OCA\Deck\Search\DeckProvider; use OCA\Deck\Service\FullTextSearchService; use OCA\Deck\Service\PermissionService; use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\Collaboration\Resources\IManager; use OCP\Collaboration\Resources\IProviderManager; use OCP\Comments\CommentsEntityEvent; @@ -48,6 +52,9 @@ use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; use OCP\FullTextSearch\IFullTextSearchManager; +use OCP\IConfig; +use OCP\IContainer; +use OCP\IDBConnection; use OCP\IGroup; use OCP\IServerContainer; use OCP\IUser; @@ -55,7 +62,7 @@ use OCP\IURLGenerator; use OCP\Util; -class Application extends App { +class Application extends App implements IBootstrap { public const APP_ID = 'deck'; public const COMMENT_ENTITY_TYPE = 'deckCard'; @@ -70,22 +77,28 @@ class Application extends App { private $fullTextSearchManager; public function __construct(array $urlParams = []) { - parent::__construct('deck', $urlParams); + parent::__construct(self::APP_ID, $urlParams); - $container = $this->getContainer(); - $server = $this->getContainer()->getServer(); + $this->server = \OC::$server; + } + + public function boot(IBootContext $context): void { + $notificationManager = $context->getServerContainer()->get(\OCP\Notification\IManager::class); + $notificationManager->registerNotifierService(Notifier::class); + \OCP\Util::addStyle('deck', 'deck'); + } - $this->server = $server; + public function register(IRegistrationContext $context): void { - $container->registerCapability(Capabilities::class); - $container->registerMiddleWare(ExceptionMiddleware::class); - $container->registerMiddleWare(DefaultBoardMiddleware::class); + $context->registerCapability(Capabilities::class); + $context->registerMiddleWare(ExceptionMiddleware::class); + $context->registerMiddleWare(DefaultBoardMiddleware::class); - $container->registerService('databaseType', static function () use ($server) { - return $server->getConfig()->getSystemValue('dbtype', 'sqlite'); + $context->registerService('databaseType', static function (IContainer $c) { + return $c->get(IConfig::class)->getSystemValue('dbtype', 'sqlite'); }); - $container->registerService('database4ByteSupport', static function () use ($server) { - return $server->getDatabaseConnection()->supports4ByteText(); + $context->registerService('database4ByteSupport', static function (IContainer $c) { + return $c->get(IDBConnection::class)->supports4ByteText(); }); $version = OC_Util::getVersion()[0]; @@ -96,31 +109,16 @@ public function __construct(array $urlParams = []) { $event->registerWidget(DeckWidget::class); }); } - } - public function register(): void { - $this->registerNavigationEntry(); + $context->registerSearchProvider(DeckProvider::class); + $this->registerUserGroupHooks(); - $this->registerNotifications(); + $this->registerCommentsEntity(); $this->registerFullTextSearch(); $this->registerCollaborationResources(); } - public function registerNavigationEntry(): void { - $container = $this->getContainer(); - $this->server->getNavigationManager()->add(static function () use ($container) { - $urlGenerator = $container->query(IURLGenerator::class); - return [ - 'id' => 'deck', - 'order' => 10, - 'href' => $urlGenerator->linkToRoute('deck.page.index'), - 'icon' => $urlGenerator->imagePath('deck', 'deck.svg'), - 'name' => 'Deck', - ]; - }); - } - private function registerUserGroupHooks(): void { $container = $this->getContainer(); // Delete user/group acl entries when they get deleted @@ -162,11 +160,6 @@ private function registerUserGroupHooks(): void { }); } - public function registerNotifications(): void { - $notificationManager = $this->server->getNotificationManager(); - $notificationManager->registerNotifierService(Notifier::class); - } - public function registerCommentsEntity(): void { $this->server->getEventDispatcher()->addListener(CommentsEntityEvent::EVENT_ENTITY, function (CommentsEntityEvent $event) { $event->addEntityCollection(self::COMMENT_ENTITY_TYPE, function ($name) { @@ -184,8 +177,6 @@ public function registerCommentsEntity(): void { $this->registerCommentsEventHandler(); } - /** - */ protected function registerCommentsEventHandler(): void { $this->server->getCommentsManager()->registerEventHandler(function () { return $this->getContainer()->query(CommentEventHandler::class); @@ -194,10 +185,6 @@ protected function registerCommentsEventHandler(): void { protected function registerCollaborationResources(): void { $version = OC_Util::getVersion()[0]; - if ($version < 16) { - return; - } - /** * Register Collaboration ResourceProvider * diff --git a/lib/Search/CardSearchResultEntry.php b/lib/Search/CardSearchResultEntry.php new file mode 100644 index 000000000..7f31604c5 --- /dev/null +++ b/lib/Search/CardSearchResultEntry.php @@ -0,0 +1,38 @@ + + * + * @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\Search; + + +use OCA\Deck\Db\Board; +use OCP\Search\SearchResultEntry; + +class BoardSearchResultEntry extends SearchResultEntry { + + public function __construct(Board $board, $urlGenerator) { + parent::__construct('', $board->getTitle(), '', $urlGenerator->linkToRoute('deck.page.index') . '#/board/' . $board->getId(), 'icon-deck'); + } +} From 45c5b1678b8fd5fd2452630a3ec0663811906502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 17 Aug 2020 17:12:51 +0200 Subject: [PATCH 02/13] Migrate CardMapper to IQBMapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/CardMapper.php | 127 +++++++++++++++++++++++++++++++++--------- 1 file changed, 100 insertions(+), 27 deletions(-) diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index bbbcdb560..27c846e57 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -24,11 +24,14 @@ namespace OCA\Deck\Db; use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Diagnostics\IQuery; use OCP\IDBConnection; use OCP\IUserManager; use OCP\Notification\IManager; -class CardMapper extends DeckMapper implements IPermissionMapper { +class CardMapper extends QBMapper implements IPermissionMapper { /** @var LabelMapper */ private $labelMapper; @@ -55,7 +58,7 @@ public function __construct( $this->database4ByteSupport = $database4ByteSupport; } - public function insert(Entity $entity) { + public function insert(Entity $entity): Entity { $entity->setDatabaseType($this->databaseType); $entity->setCreatedAt(time()); $entity->setLastModified(time()); @@ -66,7 +69,7 @@ public function insert(Entity $entity) { return parent::insert($entity); } - public function update(Entity $entity, $updateModified = true) { + public function update(Entity $entity, $updateModified = true): Entity { if (!$this->database4ByteSupport) { $description = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $entity->getDescription()); $entity->setDescription($description); @@ -93,7 +96,7 @@ public function update(Entity $entity, $updateModified = true) { return parent::update($entity); } - public function markNotified(Card $card) { + public function markNotified(Card $card): Entity { $cardUpdate = new Card(); $cardUpdate->setId($card->getId()); $cardUpdate->setNotified(true); @@ -106,11 +109,15 @@ public function markNotified(Card $card) { * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws \OCP\AppFramework\Db\DoesNotExistException */ - public function find($id) { - $sql = 'SELECT * FROM `*PREFIX*deck_cards` ' . - 'WHERE `id` = ? ORDER BY `order`, `id`'; + public function find($id): Entity { + $qb = $this->db->getQueryBuilder(); + $qb->select('*')->from('deck_cards') + ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) + ->orderBy('order') + ->addOrderBy('id'); + /** @var Card $card */ - $card = $this->findEntity($sql, [$id]); + $card = $this->findEntity($qb); $labels = $this->labelMapper->findAssignedLabelsForCard($card->id); $card->setLabels($labels); $this->mapOwner($card); @@ -118,27 +125,73 @@ public function find($id) { } public function findAll($stackId, $limit = null, $offset = null, $since = -1) { - $sql = 'SELECT * FROM `*PREFIX*deck_cards` - WHERE `stack_id` = ? AND NOT archived AND deleted_at = 0 AND last_modified > ? ORDER BY `order`, `id`'; - return $this->findEntities($sql, [$stackId, $since], $limit, $offset); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('deck_cards') + ->where($qb->expr()->eq('stack_id', $qb->createNamedParameter($stackId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->gt('last_modified', $qb->createNamedParameter($since, IQueryBuilder::PARAM_INT))) + ->setMaxResults($limit) + ->setFirstResult($offset) + ->orderBy('order') + ->addOrderBy('id'); + return $this->findEntities($qb); + } + + public function queryCardsByBoard(int $boardId): IQueryBuilder { + $qb = $this->db->getQueryBuilder(); + $qb->select('c.*') + ->from('deck_cards', 'c') + ->innerJoin('c', 'deck_stacks', 's', 'c.stack_id = s.id') + ->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT))); + return $qb; + } + + public function queryCardsByBoards(array $boardIds): IQueryBuilder { + $qb = $this->db->getQueryBuilder(); + $qb->select('c.*') + ->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))); + + return $qb; } public function findDeleted($boardId, $limit = null, $offset = null) { - $sql = 'SELECT c.* FROM `*PREFIX*deck_cards` c - INNER JOIN `*PREFIX*deck_stacks` s ON s.id = c.stack_id - WHERE `s`.`board_id` = ? AND NOT c.archived AND NOT c.deleted_at = 0 ORDER BY `c`.`order`, `c`.`id`'; - return $this->findEntities($sql, [$boardId], $limit, $offset); + $qb = $this->queryCardsByBoard($boardId); + $qb->andWhere($qb->expr()->neq('c.archived', false)) + ->andWhere($qb->expr()->neq('c.deleted_at', false)) + ->setMaxResults($limit) + ->setFirstResult($offset) + ->orderBy('order') + ->addOrderBy('id'); + return $this->findEntities($qb); } public function findAllArchived($stackId, $limit = null, $offset = null) { - $sql = 'SELECT * FROM `*PREFIX*deck_cards` WHERE `stack_id`=? AND archived ORDER BY `last_modified`'; - return $this->findEntities($sql, [$stackId], $limit, $offset); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('deck_cards') + ->where($qb->expr()->eq('stack_id', $qb->createNamedParameter($stackId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('archived', true)) + ->andWhere($qb->expr()->eq('deleted_at', 0)) + ->setMaxResults($limit) + ->setFirstResult($offset) + ->orderBy('last_modified'); + return $this->findEntities($qb); } public function findAllByStack($stackId, $limit = null, $offset = null) { - $sql = 'SELECT id FROM `*PREFIX*deck_cards` - WHERE `stack_id` = ? ORDER BY `order`, `id`'; - return $this->findEntities($sql, [$stackId], $limit, $offset); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('deck_cards') + ->where($qb->expr()->eq('stack_id', $qb->createNamedParameter($stackId, IQueryBuilder::PARAM_INT))) + ->setMaxResults($limit) + ->setFirstResult($offset) + ->orderBy('order') + ->addOrderBy('id'); + return $this->findEntities($qb); } public function findAllWithDue($boardId) { @@ -159,16 +212,32 @@ public function findAssignedCards($boardId, $username) { } public function findOverdue() { - $sql = 'SELECT id,title,duedate,notified from `*PREFIX*deck_cards` WHERE duedate < NOW() AND NOT archived AND deleted_at = 0'; - return $this->findEntities($sql); + $qb = $this->db->getQueryBuilder(); + $qb->select('id,title,duedate,notified') + ->from('deck_cards') + ->where($qb->expr()->lt('duedate', $qb->createFunction('NOW()'))) + ->andWhere($qb->expr()->eq('archived', false)) + ->andWhere($qb->expr()->eq('deleted_at', 0)); + return $this->findEntities($qb); } public function findUnexposedDescriptionChances() { - $sql = 'SELECT id,title,duedate,notified,description_prev,last_editor,description from `*PREFIX*deck_cards` WHERE last_editor IS NOT NULL AND description_prev IS NOT NULL'; - return $this->findEntities($sql); + $qb = $this->db->getQueryBuilder(); + $qb->select('id,title,duedate,notified,description_prev,last_editor,description') + ->from('deck_cards') + ->where($qb->expr()->isNotNull('last_editor')) + ->andWhere($qb->expr()->isNotNull('description_prev')); + return $this->findEntities($qb); } - public function delete(Entity $entity) { + public function search($boardIds, $term) { + $qb = $this->queryCardsByBoards($boardIds); + $qb->andWhere($qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%'))); + $qb->setMaxResults(10); + return $this->findEntities($qb); + } + + public function delete(Entity $entity): Entity { // delete assigned labels $this->labelMapper->deleteLabelAssignmentsForCard($entity->getId()); // delete card @@ -200,14 +269,18 @@ public function removeLabel($card, $label) { public function isOwner($userId, $cardId) { $sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_stacks` WHERE id IN (SELECT stack_id FROM `*PREFIX*deck_cards` WHERE id = ?))'; - $stmt = $this->execute($sql, [$cardId]); + $stmt = $this->db->prepare($sql); + $stmt->bindParam(1, $cardId, \PDO::PARAM_INT); + $stmt->execute(); $row = $stmt->fetch(); return ($row['owner'] === $userId); } public function findBoardId($cardId) { $sql = 'SELECT id FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_stacks` WHERE id IN (SELECT stack_id FROM `*PREFIX*deck_cards` WHERE id = ?))'; - $stmt = $this->execute($sql, [$cardId]); + $stmt = $this->db->prepare($sql); + $stmt->bindParam(1, $cardId, \PDO::PARAM_INT); + $stmt->execute(); $row = $stmt->fetch(); return $row['id']; } From 2059e55e30957dfcbebe5dfa3e89a5dc7ea5bf17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 17 Aug 2020 17:13:17 +0200 Subject: [PATCH 03/13] Implement new unified search API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/CardMapper.php | 17 +++- lib/Search/BoardSearchResultEntry.php | 43 ++++++++++ lib/Search/CardSearchResultEntry.php | 8 +- lib/Search/DeckProvider.php | 119 ++++++++++++++++++++++++++ lib/Service/CardService.php | 5 ++ 5 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 lib/Search/BoardSearchResultEntry.php create mode 100644 lib/Search/DeckProvider.php diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 27c846e57..502dafe48 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -230,10 +230,21 @@ public function findUnexposedDescriptionChances() { return $this->findEntities($qb); } - public function search($boardIds, $term) { + public function search($boardIds, $term, $limit = null, $offset = null) { $qb = $this->queryCardsByBoards($boardIds); - $qb->andWhere($qb->expr()->iLike('c.title', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($term) . '%'))); - $qb->setMaxResults(10); + $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); + } return $this->findEntities($qb); } diff --git a/lib/Search/BoardSearchResultEntry.php b/lib/Search/BoardSearchResultEntry.php new file mode 100644 index 000000000..7d9ffb3f6 --- /dev/null +++ b/lib/Search/BoardSearchResultEntry.php @@ -0,0 +1,43 @@ + + * + * @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\Search; + + +use OCA\Deck\Db\Board; +use OCP\Search\SearchResultEntry; + +class BoardSearchResultEntry extends SearchResultEntry { + + public function __construct(Board $board, $urlGenerator) { + parent::__construct( + '', + $board->getTitle(), + '', + $urlGenerator->linkToRoute('deck.page.index') . '#/board/' . $board->getId(), + 'icon-deck'); + } +} diff --git a/lib/Search/CardSearchResultEntry.php b/lib/Search/CardSearchResultEntry.php index 7f31604c5..c3fbfede0 100644 --- a/lib/Search/CardSearchResultEntry.php +++ b/lib/Search/CardSearchResultEntry.php @@ -28,11 +28,13 @@ use OCA\Deck\Db\Board; +use OCA\Deck\Db\Card; +use OCA\Deck\Db\Stack; use OCP\Search\SearchResultEntry; -class BoardSearchResultEntry extends SearchResultEntry { +class CardSearchResultEntry extends SearchResultEntry { - public function __construct(Board $board, $urlGenerator) { - parent::__construct('', $board->getTitle(), '', $urlGenerator->linkToRoute('deck.page.index') . '#/board/' . $board->getId(), 'icon-deck'); + public function __construct(Board $board, Stack $stack, Card $card, $urlGenerator) { + parent::__construct('', $card->getTitle(), $board->getTitle() . ' » ' . $stack->getTitle() , $urlGenerator->linkToRoute('deck.page.index') . '#/board/' . $board->getId() . '/card/' . $card->getId(), 'icon-deck'); } } diff --git a/lib/Search/DeckProvider.php b/lib/Search/DeckProvider.php new file mode 100644 index 000000000..676b1e8c5 --- /dev/null +++ b/lib/Search/DeckProvider.php @@ -0,0 +1,119 @@ + + * + * @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\Search; + + +use OCA\Deck\Db\Board; +use OCA\Deck\Db\Card; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Db\StackMapper; +use OCA\Deck\Service\BoardService; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\Search\IProvider; +use OCP\Search\ISearchQuery; +use OCP\Search\SearchResult; + +class DeckProvider implements IProvider { + + /** + * @var BoardService + */ + private $boardService; + /** + * @var CardMapper + */ + private $cardMapper; + /** + * @var StackMapper + */ + private $stackMapper; + /** + * @var IURLGenerator + */ + private $urlGenerator; + + public function __construct( + BoardService $boardService, + StackMapper $stackMapper, + CardMapper $cardMapper, + IURLGenerator $urlGenerator + ) { + $this->boardService = $boardService; + $this->stackMapper = $stackMapper; + $this->cardMapper = $cardMapper; + $this->urlGenerator = $urlGenerator; + } + + /** + * @inheritDoc + */ + public function getId(): string { + return 'deck'; + } + + public function getName(): string { + return 'Deck'; + } + + /** + * @inheritDoc + */ + public function search(IUser $user, ISearchQuery $query): SearchResult { + $boards = $this->boardService->findAll(); + $cards = $this->cardMapper->search(array_map(function (Board $board) { + return $board->getId(); + }, $boards), $query->getTerm(), $query->getLimit(), $query->getCursor()); + + $self = $this; + $results = array_merge( + array_map(function (Board $board) { + return new BoardSearchResultEntry($board, $this->urlGenerator); + }, array_filter($boards, function($board) use ($query) { + return mb_strpos($board->getTitle(), $query->getTerm()) !== -1; + })), + array_map(function (Card $card) use ($self) { + $board = $self->boardService->find($self->cardMapper->findBoardId($card->getId())); + $stack = $self->stackMapper->find($card->getStackId()); + return new CardSearchResultEntry($board, $stack, $card, $this->urlGenerator); + }, $cards) + ); + + return SearchResult::complete( + 'Deck', + $results + ); + } + + public function getOrder(string $route, array $routeParameters): int { + if ($route === 'deck.page.index') { + // Before comments + return -5; + } + return 10; + } +} diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 9d4867705..50504bc07 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -116,6 +116,11 @@ public function fetchDeleted($boardId) { return $cards; } + public function search($boardIds, $term) { + $cards = $this->cardMapper->search($boardIds, $term); + return $cards; + } + /** * @param $cardId * @return \OCA\Deck\Db\RelationalEntity From 204d520742e9c2597cf8d3eecabac102d5624c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 20 Aug 2020 09:38:05 +0200 Subject: [PATCH 04/13] Add local boards cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Service/BoardService.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index 26472150a..5c29b45d9 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -64,6 +64,8 @@ class BoardService { private $eventDispatcher; private $changeHelper; + private $boardsCache = null; + public function __construct( BoardMapper $boardMapper, StackMapper $stackMapper, @@ -109,6 +111,9 @@ public function setUserId(string $userId): void { * @return array */ public function findAll($since = -1, $details = null) { + if ($this->boardsCache) { + return $this->boardsCache; + } $userInfo = $this->getBoardPrerequisites(); $userBoards = $this->boardMapper->findAllByUser($userInfo['user'], null, null, $since); $groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'],null, null, $since); @@ -139,6 +144,7 @@ public function findAll($since = -1, $details = null) { $result[$item->getId()] = $item; } } + $this->boardsCache = $result; return array_values($result); } @@ -151,6 +157,9 @@ public function findAll($since = -1, $details = null) { * @throws BadRequestException */ public function find($boardId) { + if ($this->boardsCache && isset($this->boardsCache[$boardId])) { + return $this->boardsCache[$boardId]; + } if (is_numeric($boardId) === false) { throw new BadRequestException('board id must be a number'); } From 7faa40a20ed40e8c7d60826bafffc65f3452b86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 20 Aug 2020 09:38:35 +0200 Subject: [PATCH 05/13] Add global deck icon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- css/deck.scss | 1 + 1 file changed, 1 insertion(+) create mode 100644 css/deck.scss diff --git a/css/deck.scss b/css/deck.scss new file mode 100644 index 000000000..c142c3fbf --- /dev/null +++ b/css/deck.scss @@ -0,0 +1 @@ +@include icon-black-white('deck', 'deck', 1); From 5536188892cc84ceeb9f0a352e7830b442383ec4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 20 Aug 2020 17:38:33 +0200 Subject: [PATCH 06/13] Keep 18 compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/AppInfo/Application.php | 235 +---------------------- lib/AppInfo/Application20.php | 249 ++++++++++++++++++++++++ lib/AppInfo/ApplicationLegacy.php | 264 ++++++++++++++++++++++++++ lib/Db/CardMapper.php | 5 +- lib/Db/StackMapper.php | 2 +- lib/Search/BoardSearchResultEntry.php | 2 - lib/Search/CardSearchResultEntry.php | 2 - lib/Search/DeckProvider.php | 24 +-- lib/Service/BoardService.php | 62 +++--- 9 files changed, 566 insertions(+), 279 deletions(-) create mode 100644 lib/AppInfo/Application20.php create mode 100644 lib/AppInfo/ApplicationLegacy.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index e0adc9d17..048dc5b3c 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -23,236 +23,11 @@ namespace OCA\Deck\AppInfo; -use Exception; -use OC_Util; -use OCA\Deck\Activity\CommentEventHandler; -use OCA\Deck\Capabilities; -use OCA\Deck\Collaboration\Resources\ResourceProvider; -use OCA\Deck\Collaboration\Resources\ResourceProviderCard; -use OCA\Deck\Dashboard\DeckWidget; -use OCA\Deck\Db\Acl; -use OCA\Deck\Db\AclMapper; -use OCA\Deck\Db\AssignedUsersMapper; -use OCA\Deck\Db\BoardMapper; -use OCA\Deck\Db\CardMapper; -use OCA\Deck\Middleware\DefaultBoardMiddleware; -use OCA\Deck\Middleware\ExceptionMiddleware; -use OCA\Deck\Notification\Notifier; -use OCA\Deck\Search\DeckProvider; -use OCA\Deck\Service\FullTextSearchService; -use OCA\Deck\Service\PermissionService; -use OCP\AppFramework\App; -use OCP\AppFramework\Bootstrap\IBootContext; -use OCP\AppFramework\Bootstrap\IBootstrap; -use OCP\AppFramework\Bootstrap\IRegistrationContext; -use OCP\Collaboration\Resources\IManager; -use OCP\Collaboration\Resources\IProviderManager; -use OCP\Comments\CommentsEntityEvent; -use OCP\Dashboard\RegisterWidgetEvent; -use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\FullTextSearch\IFullTextSearchManager; -use OCP\IConfig; -use OCP\IContainer; -use OCP\IDBConnection; -use OCP\IGroup; -use OCP\IServerContainer; -use OCP\IUser; -use OCP\IUserManager; -use OCP\IURLGenerator; -use OCP\Util; - -class Application extends App implements IBootstrap { - public const APP_ID = 'deck'; - - public const COMMENT_ENTITY_TYPE = 'deckCard'; - - /** @var IServerContainer */ - private $server; - - /** @var FullTextSearchService */ - private $fullTextSearchService; - - /** @var IFullTextSearchManager */ - private $fullTextSearchManager; - - public function __construct(array $urlParams = []) { - parent::__construct(self::APP_ID, $urlParams); - - $this->server = \OC::$server; - } - - public function boot(IBootContext $context): void { - $notificationManager = $context->getServerContainer()->get(\OCP\Notification\IManager::class); - $notificationManager->registerNotifierService(Notifier::class); - \OCP\Util::addStyle('deck', 'deck'); - } - - public function register(IRegistrationContext $context): void { - - $context->registerCapability(Capabilities::class); - $context->registerMiddleWare(ExceptionMiddleware::class); - $context->registerMiddleWare(DefaultBoardMiddleware::class); - - $context->registerService('databaseType', static function (IContainer $c) { - return $c->get(IConfig::class)->getSystemValue('dbtype', 'sqlite'); - }); - $context->registerService('database4ByteSupport', static function (IContainer $c) { - return $c->get(IDBConnection::class)->supports4ByteText(); - }); - - $version = OC_Util::getVersion()[0]; - if ($version >= 20) { - /** @var IEventDispatcher $dispatcher */ - $dispatcher = $container->getServer()->query(IEventDispatcher::class); - $dispatcher->addListener(RegisterWidgetEvent::class, function (RegisterWidgetEvent $event) use ($container) { - $event->registerWidget(DeckWidget::class); - }); - } - - $context->registerSearchProvider(DeckProvider::class); - - $this->registerUserGroupHooks(); - - $this->registerCommentsEntity(); - $this->registerFullTextSearch(); - $this->registerCollaborationResources(); +$version = \OC_Util::getVersion()[0]; +if ($version >= 20) { + class Application extends Application20 { } - - private function registerUserGroupHooks(): void { - $container = $this->getContainer(); - // Delete user/group acl entries when they get deleted - /** @var IUserManager $userManager */ - $userManager = $this->server->getUserManager(); - $userManager->listen('\OC\User', 'postDelete', static function (IUser $user) use ($container) { - // delete existing acl entries for deleted user - /** @var AclMapper $aclMapper */ - $aclMapper = $container->query(AclMapper::class); - $acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_USER, $user->getUID()); - foreach ($acls as $acl) { - $aclMapper->delete($acl); - } - // delete existing user assignments - $assignmentMapper = $container->query(AssignedUsersMapper::class); - $assignments = $assignmentMapper->findByUserId($user->getUID()); - foreach ($assignments as $assignment) { - $assignmentMapper->delete($assignment); - } - - /** @var BoardMapper $boardMapper */ - $boardMapper = $container->query(BoardMapper::class); - $boards = $boardMapper->findAllByOwner($user->getUID()); - foreach ($boards as $board) { - $boardMapper->delete($board); - } - }); - - /** @var IUserManager $userManager */ - $groupManager = $this->server->getGroupManager(); - $groupManager->listen('\OC\Group', 'postDelete', static function (IGroup $group) use ($container) { - /** @var AclMapper $aclMapper */ - $aclMapper = $container->query(AclMapper::class); - $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID()); - $acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID()); - foreach ($acls as $acl) { - $aclMapper->delete($acl); - } - }); - } - - public function registerCommentsEntity(): void { - $this->server->getEventDispatcher()->addListener(CommentsEntityEvent::EVENT_ENTITY, function (CommentsEntityEvent $event) { - $event->addEntityCollection(self::COMMENT_ENTITY_TYPE, function ($name) { - /** @var CardMapper */ - $cardMapper = $this->getContainer()->query(CardMapper::class); - $permissionService = $this->getContainer()->query(PermissionService::class); - - try { - return $permissionService->checkPermission($cardMapper, (int) $name, Acl::PERMISSION_READ); - } catch (\Exception $e) { - return false; - } - }); - }); - $this->registerCommentsEventHandler(); - } - - protected function registerCommentsEventHandler(): void { - $this->server->getCommentsManager()->registerEventHandler(function () { - return $this->getContainer()->query(CommentEventHandler::class); - }); - } - - protected function registerCollaborationResources(): void { - $version = OC_Util::getVersion()[0]; - /** - * Register Collaboration ResourceProvider - * - * @Todo: Remove if min-version is 18 - */ - if ($version < 18) { - /** @var IManager $resourceManager */ - $resourceManager = $this->getContainer()->query(IManager::class); - } else { - /** @var IProviderManager $resourceManager */ - $resourceManager = $this->getContainer()->query(IProviderManager::class); - } - $resourceManager->registerResourceProvider(ResourceProvider::class); - $resourceManager->registerResourceProvider(ResourceProviderCard::class); - - $this->server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () { - Util::addScript('deck', 'collections'); - }); - } - - public function registerFullTextSearch(): void { - if (Util::getVersion()[0] < 16) { - return; - } - - $c = $this->getContainer(); - try { - $this->fullTextSearchService = $c->query(FullTextSearchService::class); - $this->fullTextSearchManager = $c->query(IFullTextSearchManager::class); - } catch (Exception $e) { - return; - } - - if (!$this->fullTextSearchManager->isAvailable()) { - return; - } - - /** @var IEventDispatcher $eventDispatcher */ - $eventDispatcher = $this->server->query(IEventDispatcher::class); - $eventDispatcher->addListener( - '\OCA\Deck\Card::onCreate', function (Event $e) { - $this->fullTextSearchService->onCardCreated($e); - } - ); - $eventDispatcher->addListener( - '\OCA\Deck\Card::onUpdate', function (Event $e) { - $this->fullTextSearchService->onCardUpdated($e); - } - ); - $eventDispatcher->addListener( - '\OCA\Deck\Card::onDelete', function (Event $e) { - $this->fullTextSearchService->onCardDeleted($e); - } - ); - $eventDispatcher->addListener( - '\OCA\Deck\Board::onShareNew', function (Event $e) { - $this->fullTextSearchService->onBoardShares($e); - } - ); - $eventDispatcher->addListener( - '\OCA\Deck\Board::onShareEdit', function (Event $e) { - $this->fullTextSearchService->onBoardShares($e); - } - ); - $eventDispatcher->addListener( - '\OCA\Deck\Board::onShareDelete', function (Event $e) { - $this->fullTextSearchService->onBoardShares($e); - } - ); +} else { + class Application extends ApplicationLegacy { } } diff --git a/lib/AppInfo/Application20.php b/lib/AppInfo/Application20.php new file mode 100644 index 000000000..7e672b944 --- /dev/null +++ b/lib/AppInfo/Application20.php @@ -0,0 +1,249 @@ + + * + * @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 . + * + */ + +namespace OCA\Deck\AppInfo; + +use Exception; +use OC_Util; +use OCA\Deck\Activity\CommentEventHandler; +use OCA\Deck\Capabilities; +use OCA\Deck\Collaboration\Resources\ResourceProvider; +use OCA\Deck\Collaboration\Resources\ResourceProviderCard; +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\AssignedUsersMapper; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Middleware\DefaultBoardMiddleware; +use OCA\Deck\Middleware\ExceptionMiddleware; +use OCA\Deck\Notification\Notifier; +use OCA\Deck\Search\DeckProvider; +use OCA\Deck\Service\FullTextSearchService; +use OCA\Deck\Service\PermissionService; +use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\Collaboration\Resources\IManager; +use OCP\Collaboration\Resources\IProviderManager; +use OCP\Comments\CommentsEntityEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\FullTextSearch\IFullTextSearchManager; +use OCP\IConfig; +use OCP\IContainer; +use OCP\IDBConnection; +use OCP\IGroup; +use OCP\IServerContainer; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Util; + +class Application20 extends App implements IBootstrap { + public const APP_ID = 'deck'; + + public const COMMENT_ENTITY_TYPE = 'deckCard'; + + /** @var IServerContainer */ + private $server; + + /** @var FullTextSearchService */ + private $fullTextSearchService; + + /** @var IFullTextSearchManager */ + private $fullTextSearchManager; + + public function __construct(array $urlParams = []) { + parent::__construct(self::APP_ID, $urlParams); + + $this->server = \OC::$server; + } + + public function boot(IBootContext $context): void { + $notificationManager = $context->getServerContainer()->get(\OCP\Notification\IManager::class); + $notificationManager->registerNotifierService(Notifier::class); + \OCP\Util::addStyle('deck', 'deck'); + } + + public function register(IRegistrationContext $context): void { + if ((@include_once __DIR__ . '/../../vendor/autoload.php') === false) { + throw new Exception('Cannot include autoload. Did you run install dependencies using composer?'); + } + + $context->registerCapability(Capabilities::class); + $context->registerMiddleWare(ExceptionMiddleware::class); + $context->registerMiddleWare(DefaultBoardMiddleware::class); + + $context->registerService('databaseType', static function (IContainer $c) { + return $c->get(IConfig::class)->getSystemValue('dbtype', 'sqlite'); + }); + $context->registerService('database4ByteSupport', static function (IContainer $c) { + return $c->get(IDBConnection::class)->supports4ByteText(); + }); + + $context->registerSearchProvider(DeckProvider::class); + + $this->registerUserGroupHooks(); + + $this->registerCommentsEntity(); + $this->registerFullTextSearch(); + $this->registerCollaborationResources(); + } + + private function registerUserGroupHooks(): void { + $container = $this->getContainer(); + // Delete user/group acl entries when they get deleted + /** @var IUserManager $userManager */ + $userManager = $this->server->getUserManager(); + $userManager->listen('\OC\User', 'postDelete', static function (IUser $user) use ($container) { + // delete existing acl entries for deleted user + /** @var AclMapper $aclMapper */ + $aclMapper = $container->query(AclMapper::class); + $acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_USER, $user->getUID()); + foreach ($acls as $acl) { + $aclMapper->delete($acl); + } + // delete existing user assignments + $assignmentMapper = $container->query(AssignedUsersMapper::class); + $assignments = $assignmentMapper->findByUserId($user->getUID()); + foreach ($assignments as $assignment) { + $assignmentMapper->delete($assignment); + } + + /** @var BoardMapper $boardMapper */ + $boardMapper = $container->query(BoardMapper::class); + $boards = $boardMapper->findAllByOwner($user->getUID()); + foreach ($boards as $board) { + $boardMapper->delete($board); + } + }); + + /** @var IUserManager $userManager */ + $groupManager = $this->server->getGroupManager(); + $groupManager->listen('\OC\Group', 'postDelete', static function (IGroup $group) use ($container) { + /** @var AclMapper $aclMapper */ + $aclMapper = $container->query(AclMapper::class); + $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID()); + $acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID()); + foreach ($acls as $acl) { + $aclMapper->delete($acl); + } + }); + } + + public function registerCommentsEntity(): void { + $this->server->getEventDispatcher()->addListener(CommentsEntityEvent::EVENT_ENTITY, function (CommentsEntityEvent $event) { + $event->addEntityCollection(self::COMMENT_ENTITY_TYPE, function ($name) { + /** @var CardMapper */ + $cardMapper = $this->getContainer()->query(CardMapper::class); + $permissionService = $this->getContainer()->query(PermissionService::class); + + try { + return $permissionService->checkPermission($cardMapper, (int) $name, Acl::PERMISSION_READ); + } catch (\Exception $e) { + return false; + } + }); + }); + $this->registerCommentsEventHandler(); + } + + protected function registerCommentsEventHandler(): void { + $this->server->getCommentsManager()->registerEventHandler(function () { + return $this->getContainer()->query(CommentEventHandler::class); + }); + } + + protected function registerCollaborationResources(): void { + $version = OC_Util::getVersion()[0]; + /** + * Register Collaboration ResourceProvider + * + * @Todo: Remove if min-version is 18 + */ + if ($version < 18) { + /** @var IManager $resourceManager */ + $resourceManager = $this->getContainer()->query(IManager::class); + } else { + /** @var IProviderManager $resourceManager */ + $resourceManager = $this->getContainer()->query(IProviderManager::class); + } + $resourceManager->registerResourceProvider(ResourceProvider::class); + $resourceManager->registerResourceProvider(ResourceProviderCard::class); + + $this->server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () { + Util::addScript('deck', 'collections'); + }); + } + + public function registerFullTextSearch(): void { + if (Util::getVersion()[0] < 16) { + return; + } + + $c = $this->getContainer(); + try { + $this->fullTextSearchService = $c->query(FullTextSearchService::class); + $this->fullTextSearchManager = $c->query(IFullTextSearchManager::class); + } catch (Exception $e) { + return; + } + + if (!$this->fullTextSearchManager->isAvailable()) { + return; + } + + /** @var IEventDispatcher $eventDispatcher */ + $eventDispatcher = $this->server->query(IEventDispatcher::class); + $eventDispatcher->addListener( + '\OCA\Deck\Card::onCreate', function (Event $e) { + $this->fullTextSearchService->onCardCreated($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Card::onUpdate', function (Event $e) { + $this->fullTextSearchService->onCardUpdated($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Card::onDelete', function (Event $e) { + $this->fullTextSearchService->onCardDeleted($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Board::onShareNew', function (Event $e) { + $this->fullTextSearchService->onBoardShares($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Board::onShareEdit', function (Event $e) { + $this->fullTextSearchService->onBoardShares($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Board::onShareDelete', function (Event $e) { + $this->fullTextSearchService->onBoardShares($e); + } + ); + } +} diff --git a/lib/AppInfo/ApplicationLegacy.php b/lib/AppInfo/ApplicationLegacy.php new file mode 100644 index 000000000..4a299c429 --- /dev/null +++ b/lib/AppInfo/ApplicationLegacy.php @@ -0,0 +1,264 @@ + + * + * @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 . + * + */ + +namespace OCA\Deck\AppInfo; + +use Exception; +use OC_Util; +use OCA\Deck\Activity\CommentEventHandler; +use OCA\Deck\Capabilities; +use OCA\Deck\Collaboration\Resources\ResourceProvider; +use OCA\Deck\Collaboration\Resources\ResourceProviderCard; +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\AclMapper; +use OCA\Deck\Db\AssignedUsersMapper; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\Middleware\DefaultBoardMiddleware; +use OCA\Deck\Middleware\ExceptionMiddleware; +use OCA\Deck\Notification\Notifier; +use OCA\Deck\Service\FullTextSearchService; +use OCA\Deck\Service\PermissionService; +use OCP\AppFramework\App; +use OCP\Collaboration\Resources\IManager; +use OCP\Collaboration\Resources\IProviderManager; +use OCP\Comments\CommentsEntityEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\FullTextSearch\IFullTextSearchManager; +use OCP\IGroup; +use OCP\IServerContainer; +use OCP\IUser; +use OCP\IUserManager; +use OCP\IURLGenerator; +use OCP\Util; + +if ((@include_once __DIR__ . '/../../vendor/autoload.php') === false) { + throw new Exception('Cannot include autoload. Did you run install dependencies using composer?'); +} + +class ApplicationLegacy extends App { + public const APP_ID = 'deck'; + + public const COMMENT_ENTITY_TYPE = 'deckCard'; + + /** @var IServerContainer */ + private $server; + + /** @var FullTextSearchService */ + private $fullTextSearchService; + + /** @var IFullTextSearchManager */ + private $fullTextSearchManager; + + public function __construct(array $urlParams = []) { + parent::__construct('deck', $urlParams); + + $container = $this->getContainer(); + $server = $this->getContainer()->getServer(); + + $this->server = $server; + + $container->registerCapability(Capabilities::class); + $container->registerMiddleWare(ExceptionMiddleware::class); + $container->registerMiddleWare(DefaultBoardMiddleware::class); + + $container->registerService('databaseType', static function () use ($server) { + return $server->getConfig()->getSystemValue('dbtype', 'sqlite'); + }); + $container->registerService('database4ByteSupport', static function () use ($server) { + return $server->getDatabaseConnection()->supports4ByteText(); + }); + } + + public function register(): void { + $this->registerNavigationEntry(); + $this->registerUserGroupHooks(); + $this->registerNotifications(); + $this->registerCommentsEntity(); + $this->registerFullTextSearch(); + $this->registerCollaborationResources(); + } + + public function registerNavigationEntry(): void { + $container = $this->getContainer(); + $this->server->getNavigationManager()->add(static function () use ($container) { + $urlGenerator = $container->query(IURLGenerator::class); + return [ + 'id' => 'deck', + 'order' => 10, + 'href' => $urlGenerator->linkToRoute('deck.page.index'), + 'icon' => $urlGenerator->imagePath('deck', 'deck.svg'), + 'name' => 'Deck', + ]; + }); + } + + private function registerUserGroupHooks(): void { + $container = $this->getContainer(); + // Delete user/group acl entries when they get deleted + /** @var IUserManager $userManager */ + $userManager = $this->server->getUserManager(); + $userManager->listen('\OC\User', 'postDelete', static function (IUser $user) use ($container) { + // delete existing acl entries for deleted user + /** @var AclMapper $aclMapper */ + $aclMapper = $container->query(AclMapper::class); + $acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_USER, $user->getUID()); + foreach ($acls as $acl) { + $aclMapper->delete($acl); + } + // delete existing user assignments + $assignmentMapper = $container->query(AssignedUsersMapper::class); + $assignments = $assignmentMapper->findByUserId($user->getUID()); + foreach ($assignments as $assignment) { + $assignmentMapper->delete($assignment); + } + + /** @var BoardMapper $boardMapper */ + $boardMapper = $container->query(BoardMapper::class); + $boards = $boardMapper->findAllByOwner($user->getUID()); + foreach ($boards as $board) { + $boardMapper->delete($board); + } + }); + + /** @var IUserManager $userManager */ + $groupManager = $this->server->getGroupManager(); + $groupManager->listen('\OC\Group', 'postDelete', static function (IGroup $group) use ($container) { + /** @var AclMapper $aclMapper */ + $aclMapper = $container->query(AclMapper::class); + $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID()); + $acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID()); + foreach ($acls as $acl) { + $aclMapper->delete($acl); + } + }); + } + + public function registerNotifications(): void { + $notificationManager = $this->server->getNotificationManager(); + $notificationManager->registerNotifierService(Notifier::class); + } + + public function registerCommentsEntity(): void { + $this->server->getEventDispatcher()->addListener(CommentsEntityEvent::EVENT_ENTITY, function (CommentsEntityEvent $event) { + $event->addEntityCollection(self::COMMENT_ENTITY_TYPE, function ($name) { + /** @var CardMapper */ + $cardMapper = $this->getContainer()->query(CardMapper::class); + $permissionService = $this->getContainer()->query(PermissionService::class); + + try { + return $permissionService->checkPermission($cardMapper, (int) $name, Acl::PERMISSION_READ); + } catch (\Exception $e) { + return false; + } + }); + }); + $this->registerCommentsEventHandler(); + } + + /** + */ + protected function registerCommentsEventHandler(): void { + $this->server->getCommentsManager()->registerEventHandler(function () { + return $this->getContainer()->query(CommentEventHandler::class); + }); + } + + protected function registerCollaborationResources(): void { + $version = OC_Util::getVersion()[0]; + if ($version < 16) { + return; + } + + /** + * Register Collaboration ResourceProvider + * + * @Todo: Remove if min-version is 18 + */ + if ($version < 18) { + /** @var IManager $resourceManager */ + $resourceManager = $this->getContainer()->query(IManager::class); + } else { + /** @var IProviderManager $resourceManager */ + $resourceManager = $this->getContainer()->query(IProviderManager::class); + } + $resourceManager->registerResourceProvider(ResourceProvider::class); + $resourceManager->registerResourceProvider(ResourceProviderCard::class); + + $this->server->getEventDispatcher()->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () { + Util::addScript('deck', 'collections'); + }); + } + + public function registerFullTextSearch(): void { + if (Util::getVersion()[0] < 16) { + return; + } + + $c = $this->getContainer(); + try { + $this->fullTextSearchService = $c->query(FullTextSearchService::class); + $this->fullTextSearchManager = $c->query(IFullTextSearchManager::class); + } catch (Exception $e) { + return; + } + + if (!$this->fullTextSearchManager->isAvailable()) { + return; + } + + /** @var IEventDispatcher $eventDispatcher */ + $eventDispatcher = $this->server->query(IEventDispatcher::class); + $eventDispatcher->addListener( + '\OCA\Deck\Card::onCreate', function (Event $e) { + $this->fullTextSearchService->onCardCreated($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Card::onUpdate', function (Event $e) { + $this->fullTextSearchService->onCardUpdated($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Card::onDelete', function (Event $e) { + $this->fullTextSearchService->onCardDeleted($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Board::onShareNew', function (Event $e) { + $this->fullTextSearchService->onBoardShares($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Board::onShareEdit', function (Event $e) { + $this->fullTextSearchService->onBoardShares($e); + } + ); + $eventDispatcher->addListener( + '\OCA\Deck\Board::onShareDelete', function (Event $e) { + $this->fullTextSearchService->onBoardShares($e); + } + ); + } +} diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 502dafe48..683a4fe30 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -26,7 +26,6 @@ use OCP\AppFramework\Db\Entity; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\Diagnostics\IQuery; use OCP\IDBConnection; use OCP\IUserManager; use OCP\Notification\IManager; @@ -216,8 +215,8 @@ public function findOverdue() { $qb->select('id,title,duedate,notified') ->from('deck_cards') ->where($qb->expr()->lt('duedate', $qb->createFunction('NOW()'))) - ->andWhere($qb->expr()->eq('archived', false)) - ->andWhere($qb->expr()->eq('deleted_at', 0)); + ->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); return $this->findEntities($qb); } diff --git a/lib/Db/StackMapper.php b/lib/Db/StackMapper.php index 40d01929b..58b343929 100644 --- a/lib/Db/StackMapper.php +++ b/lib/Db/StackMapper.php @@ -41,7 +41,7 @@ public function __construct(IDBConnection $db, CardMapper $cardMapper) { * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws \OCP\AppFramework\Db\DoesNotExistException */ - public function find($id) { + public function find($id): Stack { $sql = 'SELECT * FROM `*PREFIX*deck_stacks` ' . 'WHERE `id` = ?'; return $this->findEntity($sql, [$id]); diff --git a/lib/Search/BoardSearchResultEntry.php b/lib/Search/BoardSearchResultEntry.php index 7d9ffb3f6..f66b7377d 100644 --- a/lib/Search/BoardSearchResultEntry.php +++ b/lib/Search/BoardSearchResultEntry.php @@ -26,12 +26,10 @@ namespace OCA\Deck\Search; - use OCA\Deck\Db\Board; use OCP\Search\SearchResultEntry; class BoardSearchResultEntry extends SearchResultEntry { - public function __construct(Board $board, $urlGenerator) { parent::__construct( '', diff --git a/lib/Search/CardSearchResultEntry.php b/lib/Search/CardSearchResultEntry.php index c3fbfede0..72b9c5f3a 100644 --- a/lib/Search/CardSearchResultEntry.php +++ b/lib/Search/CardSearchResultEntry.php @@ -26,14 +26,12 @@ namespace OCA\Deck\Search; - use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; use OCA\Deck\Db\Stack; use OCP\Search\SearchResultEntry; class CardSearchResultEntry extends SearchResultEntry { - public function __construct(Board $board, Stack $stack, Card $card, $urlGenerator) { parent::__construct('', $card->getTitle(), $board->getTitle() . ' » ' . $stack->getTitle() , $urlGenerator->linkToRoute('deck.page.index') . '#/board/' . $board->getId() . '/card/' . $card->getId(), 'icon-deck'); } diff --git a/lib/Search/DeckProvider.php b/lib/Search/DeckProvider.php index 676b1e8c5..84015e90d 100644 --- a/lib/Search/DeckProvider.php +++ b/lib/Search/DeckProvider.php @@ -26,7 +26,6 @@ namespace OCA\Deck\Search; - use OCA\Deck\Db\Board; use OCA\Deck\Db\Card; use OCA\Deck\Db\CardMapper; @@ -69,9 +68,6 @@ public function __construct( $this->urlGenerator = $urlGenerator; } - /** - * @inheritDoc - */ public function getId(): string { return 'deck'; } @@ -80,12 +76,14 @@ public function getName(): string { return 'Deck'; } - /** - * @inheritDoc - */ public function search(IUser $user, ISearchQuery $query): SearchResult { - $boards = $this->boardService->findAll(); - $cards = $this->cardMapper->search(array_map(function (Board $board) { + $boards = $this->boardService->getUserBoards(); + + $matchedBoards = array_filter($this->getUserBoards(-1), static function (Board $board) use ($query) { + return mb_stripos($board->getTitle(), $query->getTerm()) > -1; + }); + + $matchedCards = $this->cardMapper->search(array_map(static function (Board $board) { return $board->getId(); }, $boards), $query->getTerm(), $query->getLimit(), $query->getCursor()); @@ -93,14 +91,13 @@ public function search(IUser $user, ISearchQuery $query): SearchResult { $results = array_merge( array_map(function (Board $board) { return new BoardSearchResultEntry($board, $this->urlGenerator); - }, array_filter($boards, function($board) use ($query) { - return mb_strpos($board->getTitle(), $query->getTerm()) !== -1; - })), + }, $matchedBoards), + array_map(function (Card $card) use ($self) { $board = $self->boardService->find($self->cardMapper->findBoardId($card->getId())); $stack = $self->stackMapper->find($card->getStackId()); return new CardSearchResultEntry($board, $stack, $card, $this->urlGenerator); - }, $cards) + }, $matchedCards) ); return SearchResult::complete( @@ -111,7 +108,6 @@ public function search(IUser $user, ISearchQuery $query): SearchResult { public function getOrder(string $route, array $routeParameters): int { if ($route === 'deck.page.index') { - // Before comments return -5; } return 10; diff --git a/lib/Service/BoardService.php b/lib/Service/BoardService.php index 5c29b45d9..df7ee1c4c 100644 --- a/lib/Service/BoardService.php +++ b/lib/Service/BoardService.php @@ -107,6 +107,21 @@ public function setUserId(string $userId): void { $this->userId = $userId; } + public function getUserBoards(int $since = -1): array { + $userInfo = $this->getBoardPrerequisites(); + $userBoards = $this->boardMapper->findAllByUser($userInfo['user'], null, null, $since); + $groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'],null, null, $since); + $circleBoards = $this->boardMapper->findAllByCircles($userInfo['user'], null, null, $since); + $mergedBoards = array_merge($userBoards, $groupBoards, $circleBoards); + $result = []; + /** @var Board $item */ + foreach ($mergedBoards as &$item) { + if (!array_key_exists($item->getId(), $result)) { + $result[$item->getId()] = $item; + } + } + return array_values($result); + } /** * @return array */ @@ -114,37 +129,30 @@ public function findAll($since = -1, $details = null) { if ($this->boardsCache) { return $this->boardsCache; } - $userInfo = $this->getBoardPrerequisites(); - $userBoards = $this->boardMapper->findAllByUser($userInfo['user'], null, null, $since); - $groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'],null, null, $since); - $circleBoards = $this->boardMapper->findAllByCircles($userInfo['user'], null, null, $since); - $complete = array_merge($userBoards, $groupBoards, $circleBoards); - $result = []; + $complete = $this->getUserBoards($since); /** @var Board $item */ foreach ($complete as &$item) { - if (!array_key_exists($item->getId(), $result)) { - $this->boardMapper->mapOwner($item); - if ($item->getAcl() !== null) { - foreach ($item->getAcl() as &$acl) { - $this->boardMapper->mapAcl($acl); - } - } - if ($details !== null) { - $this->enrichWithStacks($item); - $this->enrichWithLabels($item); - $this->enrichWithUsers($item); + $this->boardMapper->mapOwner($item); + if ($item->getAcl() !== null) { + foreach ($item->getAcl() as &$acl) { + $this->boardMapper->mapAcl($acl); } - $permissions = $this->permissionService->matchPermissions($item); - $item->setPermissions([ - 'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ] ?? false, - 'PERMISSION_EDIT' => $permissions[Acl::PERMISSION_EDIT] ?? false, - 'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE] ?? false, - 'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false - ]); - $result[$item->getId()] = $item; } - } - $this->boardsCache = $result; + if ($details !== null) { + $this->enrichWithStacks($item); + $this->enrichWithLabels($item); + $this->enrichWithUsers($item); + } + $permissions = $this->permissionService->matchPermissions($item); + $item->setPermissions([ + 'PERMISSION_READ' => $permissions[Acl::PERMISSION_READ] ?? false, + 'PERMISSION_EDIT' => $permissions[Acl::PERMISSION_EDIT] ?? false, + 'PERMISSION_MANAGE' => $permissions[Acl::PERMISSION_MANAGE] ?? false, + 'PERMISSION_SHARE' => $permissions[Acl::PERMISSION_SHARE] ?? false + ]); + $result[$item->getId()] = $item; + } + $this->boardsCache = $complete; return array_values($result); } From c62411c7eee3e15133389071c24c5537365cfbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Thu, 20 Aug 2020 18:23:46 +0200 Subject: [PATCH 07/13] Remove useless test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- tests/integration/app/AppTest.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/integration/app/AppTest.php b/tests/integration/app/AppTest.php index 3c7401d79..c25e0c513 100644 --- a/tests/integration/app/AppTest.php +++ b/tests/integration/app/AppTest.php @@ -42,9 +42,4 @@ public function testAppInstalled() { $appManager = $this->container->query('OCP\App\IAppManager'); $this->assertTrue($appManager->isInstalled('deck')); } - - public function testNavigationEntry() { - $this->app->registerNavigationEntry(); - $this->assertTrue(true); - } } From e5cc5fcd1ee547dc3810cd6a99017778f7322438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Fri, 21 Aug 2020 10:01:11 +0200 Subject: [PATCH 08/13] Fix getUserBoards call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Search/DeckProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Search/DeckProvider.php b/lib/Search/DeckProvider.php index 84015e90d..fa961557c 100644 --- a/lib/Search/DeckProvider.php +++ b/lib/Search/DeckProvider.php @@ -79,7 +79,7 @@ public function getName(): string { public function search(IUser $user, ISearchQuery $query): SearchResult { $boards = $this->boardService->getUserBoards(); - $matchedBoards = array_filter($this->getUserBoards(-1), static function (Board $board) use ($query) { + $matchedBoards = array_filter($this->boardService->getUserBoards(), static function (Board $board) use ($query) { return mb_stripos($board->getTitle(), $query->getTerm()) > -1; }); From 7bbf50b9cf0637379008a6a4e4f0df42938fdfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 24 Aug 2020 17:30:06 +0200 Subject: [PATCH 09/13] Fix duplicate navigation registration on old Nextcloud versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/AppInfo/ApplicationLegacy.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/AppInfo/ApplicationLegacy.php b/lib/AppInfo/ApplicationLegacy.php index 4a299c429..6fac3c6a8 100644 --- a/lib/AppInfo/ApplicationLegacy.php +++ b/lib/AppInfo/ApplicationLegacy.php @@ -92,7 +92,6 @@ public function __construct(array $urlParams = []) { } public function register(): void { - $this->registerNavigationEntry(); $this->registerUserGroupHooks(); $this->registerNotifications(); $this->registerCommentsEntity(); @@ -100,20 +99,6 @@ public function register(): void { $this->registerCollaborationResources(); } - public function registerNavigationEntry(): void { - $container = $this->getContainer(); - $this->server->getNavigationManager()->add(static function () use ($container) { - $urlGenerator = $container->query(IURLGenerator::class); - return [ - 'id' => 'deck', - 'order' => 10, - 'href' => $urlGenerator->linkToRoute('deck.page.index'), - 'icon' => $urlGenerator->imagePath('deck', 'deck.svg'), - 'name' => 'Deck', - ]; - }); - } - private function registerUserGroupHooks(): void { $container = $this->getContainer(); // Delete user/group acl entries when they get deleted From b5862b482aa019de4ff5aa95f7ac6537232701f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 24 Aug 2020 17:30:22 +0200 Subject: [PATCH 10/13] Move to query builder (pt.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/CardMapper.php | 46 +++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 683a4fe30..52abdc89d 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -159,8 +159,8 @@ public function queryCardsByBoards(array $boardIds): IQueryBuilder { public function findDeleted($boardId, $limit = null, $offset = null) { $qb = $this->queryCardsByBoard($boardId); - $qb->andWhere($qb->expr()->neq('c.archived', false)) - ->andWhere($qb->expr()->neq('c.deleted_at', false)) + $qb->andWhere($qb->expr()->neq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->neq('c.deleted_at', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->setMaxResults($limit) ->setFirstResult($offset) ->orderBy('order') @@ -173,8 +173,8 @@ public function findAllArchived($stackId, $limit = null, $offset = null) { $qb->select('*') ->from('deck_cards') ->where($qb->expr()->eq('stack_id', $qb->createNamedParameter($stackId, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('archived', true)) - ->andWhere($qb->expr()->eq('deleted_at', 0)) + ->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->setMaxResults($limit) ->setFirstResult($offset) ->orderBy('last_modified'); @@ -194,20 +194,36 @@ public function findAllByStack($stackId, $limit = null, $offset = null) { } public function findAllWithDue($boardId) { - $sql = 'SELECT c.* FROM `*PREFIX*deck_cards` c - INNER JOIN `*PREFIX*deck_stacks` s ON s.id = c.stack_id - INNER JOIN `*PREFIX*deck_boards` b ON b.id = s.board_id - WHERE `s`.`board_id` = ? AND duedate IS NOT NULL AND NOT c.archived AND c.deleted_at = 0 AND s.deleted_at = 0 AND NOT b.archived AND b.deleted_at = 0'; - return $this->findEntities($sql, [$boardId]); + $qb = $this->db->getQueryBuilder(); + $qb->select('c.*') + ->from('deck_cards', 'c') + ->innerJoin('c', 'deck_stacks', 's', 's.id = c.stack_id') + ->innerJoin('s', 'deck_boards', 'b', 'b.id = s.board_id') + ->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->isNotNull('c.duedate')) + ->andWhere($qb->expr()->eq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('b.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('b.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + return $this->findEntities($qb); } public function findAssignedCards($boardId, $username) { - $sql = 'SELECT c.* FROM `*PREFIX*deck_cards` c - INNER JOIN `*PREFIX*deck_stacks` s ON s.id = c.stack_id - INNER JOIN `*PREFIX*deck_boards` b ON b.id = s.board_id - INNER JOIN `*PREFIX*deck_assigned_users` u ON c.id = card_id - WHERE `s`.`board_id` = ? AND participant = ? AND NOT c.archived AND c.deleted_at = 0 AND s.deleted_at = 0 AND NOT b.archived AND b.deleted_at = 0'; - return $this->findEntities($sql, [$boardId, $username]); + $qb = $this->db->getQueryBuilder(); + $qb->select('c.*') + ->from('deck_cards', 'c') + ->innerJoin('c', 'deck_stacks', 's', 's.id = c.stack_id') + ->innerJoin('s', 'deck_boards', 'b', 'b.id = s.board_id') + ->innerJoin('c', 'deck_assigned_users', 'u', 'c.id = u.card_id') + ->where($qb->expr()->eq('s.board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('u.participant', $qb->createNamedParameter($username, IQueryBuilder::PARAM_STR))) + ->andWhere($qb->expr()->eq('u.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_USER, IQueryBuilder::PARAM_INT))) + // Filter out archived/deleted cards and board + ->andWhere($qb->expr()->eq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('b.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->andWhere($qb->expr()->eq('b.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))); + return $this->findEntities($qb); } public function findOverdue() { From 7e183d6e99a4d4a1d9cb7a8e7bab23800c4b398c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 24 Aug 2020 18:13:18 +0200 Subject: [PATCH 11/13] Properly filter archived view and deleted cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/Db/CardMapper.php | 4 ++-- src/components/board/Stack.vue | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index 52abdc89d..1b3e73f46 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -159,8 +159,7 @@ public function queryCardsByBoards(array $boardIds): IQueryBuilder { public function findDeleted($boardId, $limit = null, $offset = null) { $qb = $this->queryCardsByBoard($boardId); - $qb->andWhere($qb->expr()->neq('c.archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) - ->andWhere($qb->expr()->neq('c.deleted_at', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + $qb->andWhere($qb->expr()->neq('c.deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->setMaxResults($limit) ->setFirstResult($offset) ->orderBy('order') @@ -186,6 +185,7 @@ public function findAllByStack($stackId, $limit = null, $offset = null) { $qb->select('*') ->from('deck_cards') ->where($qb->expr()->eq('stack_id', $qb->createNamedParameter($stackId, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) ->setMaxResults($limit) ->setFirstResult($offset) ->orderBy('order') diff --git a/src/components/board/Stack.vue b/src/components/board/Stack.vue index 82b11f197..de79bb857 100644 --- a/src/components/board/Stack.vue +++ b/src/components/board/Stack.vue @@ -162,7 +162,12 @@ export default { showArchived: state => state.showArchived, }), cardsByStack() { - return this.$store.getters.cardsByStack(this.stack.id) + return this.$store.getters.cardsByStack(this.stack.id).filter((card) => { + if (this.showArchived) { + return card.archived + } + return !card.archived + }) }, dragHandleSelector() { return this.canEdit ? null : '.no-drag' From 0fcae45b847b1c3b800b97db9130eb0c2125004e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 1 Sep 2020 11:03:38 +0200 Subject: [PATCH 12/13] Keep dashboard widget registered 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 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/AppInfo/Application20.php b/lib/AppInfo/Application20.php index 7e672b944..91020d60c 100644 --- a/lib/AppInfo/Application20.php +++ b/lib/AppInfo/Application20.php @@ -29,6 +29,7 @@ use OCA\Deck\Capabilities; use OCA\Deck\Collaboration\Resources\ResourceProvider; use OCA\Deck\Collaboration\Resources\ResourceProviderCard; +use OCA\Deck\Dashboard\DeckWidget; use OCA\Deck\Db\Acl; use OCA\Deck\Db\AclMapper; use OCA\Deck\Db\AssignedUsersMapper; @@ -103,6 +104,8 @@ public function register(IRegistrationContext $context): void { $context->registerSearchProvider(DeckProvider::class); + $context->registerDashboardWidget(DeckWidget::class); + $this->registerUserGroupHooks(); $this->registerCommentsEntity(); From 081a5185416d5d2f6b7cb5c3aaa39e6d7d5e623d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Tue, 1 Sep 2020 11:03:54 +0200 Subject: [PATCH 13/13] Fix php cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- lib/AppInfo/ApplicationLegacy.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/AppInfo/ApplicationLegacy.php b/lib/AppInfo/ApplicationLegacy.php index 6fac3c6a8..b194dd37f 100644 --- a/lib/AppInfo/ApplicationLegacy.php +++ b/lib/AppInfo/ApplicationLegacy.php @@ -50,7 +50,6 @@ use OCP\IServerContainer; use OCP\IUser; use OCP\IUserManager; -use OCP\IURLGenerator; use OCP\Util; if ((@include_once __DIR__ . '/../../vendor/autoload.php') === false) {