Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1754b60
feat(migration): add created_at and last_modified_at timestamp column…
Jun 16, 2026
510a8db
feat(migration): backfill board last_modified into deck_board_acl sha…
Jun 16, 2026
ad2c131
feat(db): record creation and modification timestamps on deck_board_a…
Jun 16, 2026
1f1a8ae
test(migration): verify AclTimestampBackfill backfill logic and AclMa…
Jun 16, 2026
157cce2
perf(migration): replace N+1 updates in AclTimestampBackfill with gro…
Jun 16, 2026
2278db5
fix(meta-data): set/update share meta-data on create/update
AndyScherzinger Jun 19, 2026
7e3e603
docs(version): Bump version to trigger DB migration
AndyScherzinger Jun 19, 2026
ff12607
feat(sharereview): add listener registering Deck as a share source
AndyScherzinger Jun 11, 2026
ca82100
feat(sharereview): add ShareReviewSource with constructor and getName()
AndyScherzinger Jun 11, 2026
33f3fc2
feat(sharereview): implement getShares() with board name lookup via JOIN
AndyScherzinger Jun 11, 2026
6d601fa
feat(sharereview): implement deleteShare() via direct SQL with logging
AndyScherzinger Jun 11, 2026
32f9d96
feat(sharereview): register ShareReview listener on SourceEvent
AndyScherzinger Jun 11, 2026
cfcdc45
style(sharereview): apply coding standards and Psalm fixes
AndyScherzinger Jun 11, 2026
70a6961
test(sharereview): add unit tests for ShareReviewSource
AndyScherzinger Jun 11, 2026
20dacc8
fix(sharereview): harden and optimize implementation and testing
AndyScherzinger Jun 11, 2026
28a991f
fix: Use the new share meta-data instead of a hard-coded date
AndyScherzinger Jun 19, 2026
dbdce8e
feat(sharereview): gate ACL deletion behind event-based access check
AndyScherzinger Jun 22, 2026
b4e6d14
refactor(sharereview): address pre-review feedback on ShareReviewSource
AndyScherzinger Jun 22, 2026
b43a910
refactor(sharereview): use OCP ShareReviewAccessCheckEvent from server
AndyScherzinger Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 97 additions & 96 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
@@ -1,96 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
- SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>deck</id>
<name>Deck</name>
<summary>Personal planning and team project organization</summary>
<description>Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.


- 📥 Add your tasks to cards and put them in order
- 📄 Write down additional notes in Markdown
- 🔖 Assign labels for even better organization
- 👥 Share with your team, friends or family
- 📎 Attach files and embed them in your Markdown description
- 💬 Discuss with your team using comments
- ⚡ Keep track of changes in the activity stream
- 🚀 Get your project organized

</description>
<version>4.0.0-dev.1</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
<types>
<dav/>
</types>
<documentation>
<user>https://deck.readthedocs.io/en/latest/User_documentation_en/</user>
<developer>https://deck.readthedocs.io/en/latest/API/</developer>
</documentation>
<category>organization</category>
<category>office</category>
<website>https://github.com/nextcloud/deck</website>
<bugs>https://github.com/nextcloud/deck/issues</bugs>
<repository type="git">https://github.com/nextcloud/deck.git</repository>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-1.png</screenshot>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-2.png</screenshot>
<dependencies>
<database min-version="9.4">pgsql</database>
<database>sqlite</database>
<database min-version="8.0">mysql</database>
<nextcloud min-version="35" max-version="35"/>
</dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>
<job>OCA\Deck\Cron\ScheduledNotifications</job>
<job>OCA\Deck\Cron\CardDescriptionActivity</job>
<job>OCA\Deck\Cron\SessionsCleanup</job>
</background-jobs>
<repair-steps>
<live-migration>
<step>OCA\Deck\Migration\DeletedCircleCleanup</step>
</live-migration>
<post-migration>
<step>OCA\Deck\Migration\LabelMismatchCleanup</step>
</post-migration>
</repair-steps>
<commands>
<command>OCA\Deck\Command\UserExport</command>
<command>OCA\Deck\Command\BoardImport</command>
<command>OCA\Deck\Command\TransferOwnership</command>
<command>OCA\Deck\Command\CalendarToggle</command>
</commands>
<activity>
<settings>
<setting>OCA\Deck\Activity\SettingChanges</setting>
<setting>OCA\Deck\Activity\SettingDescription</setting>
<setting>OCA\Deck\Activity\SettingComment</setting>
</settings>
<filters>
<filter>OCA\Deck\Activity\Filter</filter>
</filters>
<providers>
<provider>OCA\Deck\Activity\DeckProvider</provider>
</providers>
</activity>
<fulltextsearch>
<provider min-version="16">OCA\Deck\Provider\DeckProvider</provider>
</fulltextsearch>
<navigations>
<navigation>
<name>Deck</name>
<route>deck.page.index</route>
<icon>deck.svg</icon>
<order>10</order>
</navigation>
</navigations>
<sabre>
<calendar-plugins>
<plugin>OCA\Deck\DAV\CalendarPlugin</plugin>
</calendar-plugins>
</sabre>
</info>
<?xml version="1.0" encoding="utf-8"?>
<!--
- SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>deck</id>
<name>Deck</name>
<summary>Personal planning and team project organization</summary>
<description>Deck is a kanban style organization tool aimed at personal planning and project organization for teams integrated with Nextcloud.


- 📥 Add your tasks to cards and put them in order
- 📄 Write down additional notes in Markdown
- 🔖 Assign labels for even better organization
- 👥 Share with your team, friends or family
- 📎 Attach files and embed them in your Markdown description
- 💬 Discuss with your team using comments
- ⚡ Keep track of changes in the activity stream
- 🚀 Get your project organized

</description>
<version>4.0.0-dev.2</version>
<licence>agpl</licence>
<author>Julius Härtl</author>
<namespace>Deck</namespace>
<types>
<dav/>
</types>
<documentation>
<user>https://deck.readthedocs.io/en/latest/User_documentation_en/</user>
<developer>https://deck.readthedocs.io/en/latest/API/</developer>
</documentation>
<category>organization</category>
<category>office</category>
<website>https://github.com/nextcloud/deck</website>
<bugs>https://github.com/nextcloud/deck/issues</bugs>
<repository type="git">https://github.com/nextcloud/deck.git</repository>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-1.png</screenshot>
<screenshot>https://download.bitgrid.net/nextcloud/deck/screenshots/1.0/Deck-2.png</screenshot>
<dependencies>
<database min-version="9.4">pgsql</database>
<database>sqlite</database>
<database min-version="8.0">mysql</database>
<nextcloud min-version="35" max-version="35"/>
</dependencies>
<background-jobs>
<job>OCA\Deck\Cron\DeleteCron</job>
<job>OCA\Deck\Cron\ScheduledNotifications</job>
<job>OCA\Deck\Cron\CardDescriptionActivity</job>
<job>OCA\Deck\Cron\SessionsCleanup</job>
</background-jobs>
<repair-steps>
<live-migration>
<step>OCA\Deck\Migration\DeletedCircleCleanup</step>
</live-migration>
<post-migration>
<step>OCA\Deck\Migration\LabelMismatchCleanup</step>
<step>OCA\Deck\Migration\AclTimestampBackfill</step>
</post-migration>
</repair-steps>
<commands>
<command>OCA\Deck\Command\UserExport</command>
<command>OCA\Deck\Command\BoardImport</command>
<command>OCA\Deck\Command\TransferOwnership</command>
<command>OCA\Deck\Command\CalendarToggle</command>
</commands>
<activity>
<settings>
<setting>OCA\Deck\Activity\SettingChanges</setting>
<setting>OCA\Deck\Activity\SettingDescription</setting>
<setting>OCA\Deck\Activity\SettingComment</setting>
</settings>
<filters>
<filter>OCA\Deck\Activity\Filter</filter>
</filters>
<providers>
<provider>OCA\Deck\Activity\DeckProvider</provider>
</providers>
</activity>
<fulltextsearch>
<provider min-version="16">OCA\Deck\Provider\DeckProvider</provider>
</fulltextsearch>
<navigations>
<navigation>
<name>Deck</name>
<route>deck.page.index</route>
<icon>deck.svg</icon>
<order>10</order>
</navigation>
</navigations>
<sabre>
<calendar-plugins>
<plugin>OCA\Deck\DAV\CalendarPlugin</plugin>
</calendar-plugins>
</sabre>
</info>
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@
use OCA\Deck\Search\CardCommentProvider;
use OCA\Deck\Search\DeckProvider;
use OCA\Deck\Service\PermissionService;
use OCA\Deck\ShareReview\ShareReviewListener;
use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\Sharing\Listener;
use OCA\Deck\Teams\DeckTeamResourceProvider;
use OCA\Deck\UserMigration\DeckMigrator;
use OCA\ShareReview\Sources\SourceEvent;
use OCA\Text\Event\LoadEditor;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
Expand Down Expand Up @@ -189,6 +191,8 @@ public function register(IRegistrationContext $context): void {
$context->registerTeamResourceProvider(DeckTeamResourceProvider::class);

$context->registerUserMigrator(DeckMigrator::class);

$context->registerEventListener(SourceEvent::class, ShareReviewListener::class);
}

public function registerCommentsEntity(IEventDispatcher $eventDispatcher): void {
Expand Down
8 changes: 8 additions & 0 deletions lib/Db/Acl.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
* @method void setOwner(int $owner)
* @method void setToken(string $token)
* @method string getToken()
* @method int getCreatedAt()
* @method void setCreatedAt(int $createdAt)
* @method int getLastModifiedAt()
* @method void setLastModifiedAt(int $lastModifiedAt)
*
*/
class Acl extends RelationalEntity {
Expand All @@ -42,6 +46,8 @@ class Acl extends RelationalEntity {
protected $permissionManage = false;
protected $owner = false;
protected $token = null;
protected $createdAt = 0;
protected $lastModifiedAt = 0;

public function __construct() {
$this->addType('id', 'integer');
Expand All @@ -52,6 +58,8 @@ public function __construct() {
$this->addType('type', 'integer');
$this->addType('owner', 'boolean');
$this->addType('token', 'string');
$this->addType('createdAt', 'integer');
$this->addType('lastModifiedAt', 'integer');
$this->addRelation('owner');
$this->addResolvable('participant');
}
Expand Down
49 changes: 45 additions & 4 deletions lib/Db/AclMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@
namespace OCA\Deck\Db;

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\Entity;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

/** @template-extends DeckMapper<Acl> */
class AclMapper extends DeckMapper implements IPermissionMapper {
public const TABLE_NAME = 'deck_board_acl';

public function __construct(IDBConnection $db) {
parent::__construct($db, 'deck_board_acl', Acl::class);
parent::__construct($db, self::TABLE_NAME, Acl::class);
}

public function findByAccessToken(string $accessToken) {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'token')
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'token', 'created_at', 'last_modified_at')
->from('deck_board_acl')
->where($qb->expr()->eq('token', $qb->createNamedParameter($accessToken, IQueryBuilder::PARAM_STR)))
->setMaxResults(1);
Expand All @@ -34,7 +38,7 @@ public function findByAccessToken(string $accessToken) {
*/
public function findAll(int $boardId, ?int $limit = null, ?int $offset = null) {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'token')
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'token', 'created_at', 'last_modified_at')
->from('deck_board_acl')
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
->setMaxResults($limit)
Expand All @@ -45,7 +49,7 @@ public function findAll(int $boardId, ?int $limit = null, ?int $offset = null) {

public function findIn(array $boardIds, ?int $limit = null, ?int $offset = null): array {
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage')
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage', 'created_at', 'last_modified_at')
->from('deck_board_acl')
->where($qb->expr()->in('board_id', $qb->createParameter('boardIds')))
->setMaxResults($limit)
Expand Down Expand Up @@ -127,4 +131,41 @@ public function findByType(int $type): array {
->where($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
return $this->findEntities($qb);
}

/**
* Fetch all ACL rows with their board title and owner for ShareReview.
*
* @return list<array<string, mixed>>
* @throws Exception
*/
public function findAllForShareReview(): array {
$qb = $this->db->getQueryBuilder();
$qb->select(
'a.id', 'a.board_id', 'a.type', 'a.participant',
'a.permission_edit', 'a.permission_share', 'a.permission_manage', 'a.created_at', 'a.last_modified_at'
)
->selectAlias('b.title', 'board_title')
->selectAlias('b.owner', 'board_owner')
->from(self::TABLE_NAME, 'a')
->leftJoin('a', 'deck_boards', 'b', $qb->expr()->eq('a.board_id', 'b.id'))
->orderBy('a.id', 'ASC');
$result = $qb->executeQuery();
$rows = $result->fetchAll();
$result->closeCursor();
return $rows;
}

public function insert(Entity $entity): Entity {
/** @var Acl $entity */
$now = time();
$entity->setCreatedAt($now);
$entity->setLastModifiedAt($now);
return parent::insert($entity);
}

public function update(Entity $entity): Entity {
/** @var Acl $entity */
$entity->setLastModifiedAt(time());
return parent::update($entity);
}
}
3 changes: 2 additions & 1 deletion lib/Db/BoardMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

/** @template-extends QBMapper<Board> */
class BoardMapper extends QBMapper implements IPermissionMapper {
public const TABLE_NAME = 'deck_boards';
/** @var CappedMemoryCache<Board[]> */
private CappedMemoryCache $userBoardCache;
/** @var CappedMemoryCache<Board> */
Expand All @@ -36,7 +37,7 @@ public function __construct(
private ICloudIdManager $cloudIdManager,
private LoggerInterface $logger,
) {
parent::__construct($db, 'deck_boards', Board::class);
parent::__construct($db, self::TABLE_NAME, Board::class);

$this->userBoardCache = new CappedMemoryCache();
$this->boardCache = new CappedMemoryCache();
Expand Down
Loading
Loading