Skip to content
Draft
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
27 changes: 27 additions & 0 deletions lib/ShareReview/ShareReviewListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\ShareReview;

use OCA\ShareReview\Sources\SourceEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;

/** @template-implements IEventListener<SourceEvent> */
class ShareReviewListener implements IEventListener {
public function __construct() {
}

public function handle(Event $event): void {
if (!$event instanceof SourceEvent) {
return;
}
$event->registerSource(ShareReviewSource::class);
}
}
128 changes: 128 additions & 0 deletions lib/ShareReview/ShareReviewSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Deck\ShareReview;

use OCA\Deck\Db\Acl;
use OCA\ShareReview\Sources\ISource;
use OCP\Constants;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Share\IShare;
use Psr\Log\LoggerInterface;

class ShareReviewSource implements ISource {

private const ACL_TABLE = 'deck_board_acl';
private const BOARDS_TABLE = 'deck_boards';
private const PERMISSION_MANAGE = 32;

public function __construct(
private IDBConnection $db,
private LoggerInterface $logger,
) {
}

public function getName(): string {
return 'Deck';
}

/**
* @return list<array{id: int, app: string, object: string, initiator: string, type: int, recipient: string, permissions: int, password: bool, time: string, action: string}>
*/
public function getShares(): array {
$rawShares = $this->fetchAllShares();
$appName = $this->getName();
$formatted = [];
foreach ($rawShares as $share) {
$formatted[] = [
'id' => (int)$share['id'],
'app' => $appName,
'object' => $this->resolveObjectName($share),
'initiator' => (string)$share['board_owner'],
'type' => $this->mapParticipantType((int)$share['type']),
'recipient' => (string)$share['participant'],
'permissions' => $this->computePermissions($share),
'password' => false,
'time' => '1970-01-01 01:00:00',

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems we do not have a creation/modification date of share records

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarified, no creation/modification metadata present

'action' => '',
];
}
return $formatted;
}

public function deleteShare(string $shareId): bool {
$this->logger->info('Deck ShareReview: deleting share {id}', ['id' => $shareId]);
try {
$qb = $this->db->getQueryBuilder();
$qb->delete(self::ACL_TABLE)
->where($qb->expr()->eq('id', $qb->createNamedParameter((int)$shareId, IQueryBuilder::PARAM_INT)));
return $qb->executeStatement() > 0;
} catch (Exception $e) {
$this->logger->error('Deck ShareReview: failed to delete share {id}: {message}', ['id' => $shareId, 'message' => $e->getMessage()]);
return false;
}
}

/** @return list<array<string, mixed>> */
private function fetchAllShares(): array {
try {
$qb = $this->db->getQueryBuilder();
$qb->select(
'a.id', 'a.board_id', 'a.type', 'a.participant',
'a.permission_edit', 'a.permission_share', 'a.permission_manage'
)
->selectAlias('b.title', 'board_title')
->selectAlias('b.owner', 'board_owner')
->from(self::ACL_TABLE, 'a')
->leftJoin('a', self::BOARDS_TABLE, 'b', $qb->expr()->eq('a.board_id', 'b.id'))
->orderBy('a.id', 'ASC');
$result = $qb->executeQuery();
$rows = $result->fetchAll();
$result->closeCursor();
return $rows;
} catch (Exception $e) {
$this->logger->error('Deck ShareReview: failed to fetch shares: {message}', ['message' => $e->getMessage()]);
return [];
}
}

/** @param array<string, mixed> $share */
private function resolveObjectName(array $share): string {
$title = (string)($share['board_title'] ?? '');
$boardId = (int)($share['board_id'] ?? $share['id']);
return ($title !== '' ? $title : "Board $boardId") . ' (Board)';
}

private function mapParticipantType(int $type): int {
return match($type) {
Acl::PERMISSION_TYPE_USER => IShare::TYPE_USER,
Acl::PERMISSION_TYPE_GROUP => IShare::TYPE_GROUP,
Acl::PERMISSION_TYPE_REMOTE => IShare::TYPE_REMOTE,
Acl::PERMISSION_TYPE_CIRCLE => IShare::TYPE_CIRCLE,
default => IShare::TYPE_USER,
};
}

/** @param array<string, mixed> $share */
private function computePermissions(array $share): int {
$permissions = Constants::PERMISSION_READ;
if ($share['permission_edit']) {
$permissions |= Constants::PERMISSION_UPDATE | Constants::PERMISSION_CREATE | Constants::PERMISSION_DELETE;
}
if ($share['permission_share']) {
$permissions |= Constants::PERMISSION_SHARE;
}
if ($share['permission_manage']) {
$permissions |= self::PERMISSION_MANAGE;
}
return $permissions;
}
}
4 changes: 4 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@

require_once __DIR__ . '/../../../tests/bootstrap.php';
require_once __DIR__ . '/../appinfo/autoload.php';

if (!interface_exists('OCA\ShareReview\Sources\ISource')) {
require_once __DIR__ . '/unit/ShareReview/Stubs.php';
}
12 changes: 12 additions & 0 deletions tests/stub.phpstub
Original file line number Diff line number Diff line change
Expand Up @@ -725,3 +725,15 @@ namespace OCA\NotifyPush\Queue {
namespace OCA\Text\Event {
class LoadEditor extends \OCP\EventDispatcher\Event {}
}

namespace OCA\ShareReview\Sources {
class SourceEvent extends \OCP\EventDispatcher\Event {
abstract public function registerSource(string $source): void {}
}

interface ISource {
public function getName(): string;
public function getShares(): array;
public function deleteShare(string $shareId): bool;
}
}
Loading
Loading