Skip to content

Commit c7652bb

Browse files
feat(sharereview): expose tables(table/view/context) shares to share-review apps via OCP\Share\ShareReview
Implement IShareReviewSource listing all tables(table/view/context) shares with their capabilities mapped to ShareReviewPermission entries, streaming share rows from the database via a generator to keep the memory footprint low on large instances, gate deletions behind the ShareReviewAccessCheckEvent authorization check with share-delete activity entries for auditability, and register the source via RegisterShareReviewSourceEvent. Assisted-by: Claude Code:claude-fable-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
1 parent 5872964 commit c7652bb

13 files changed

Lines changed: 1067 additions & 0 deletions

lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
use OCA\Tables\Search\SearchTablesProvider;
3535
use OCA\Tables\Service\Support\AuditLogServiceInterface;
3636
use OCA\Tables\Service\Support\DefaultAuditLogService;
37+
use OCA\Tables\ShareReview\ShareReviewListener;
3738
use OCA\Tables\UserMigration\TablesMigrator;
3839
use OCP\AppFramework\App;
3940
use OCP\AppFramework\Bootstrap\IBootContext;
@@ -44,6 +45,7 @@
4445
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
4546
use OCP\DB\Events\AddMissingIndicesEvent;
4647
use OCP\Group\Events\GroupDeletedEvent;
48+
use OCP\Share\ShareReview\RegisterShareReviewSourceEvent;
4749
use OCP\User\Events\BeforeUserDeletedEvent;
4850
use OCP\User\Events\UserDeletedEvent;
4951
use Psr\Container\ContainerInterface;
@@ -84,6 +86,7 @@ public function register(IRegistrationContext $context): void {
8486

8587
$context->registerEventListener(BeforeUserDeletedEvent::class, UserDeletedListener::class);
8688
$context->registerEventListener(DatasourceEvent::class, AnalyticsDatasourceListener::class);
89+
$context->registerEventListener(RegisterShareReviewSourceEvent::class, ShareReviewListener::class);
8790
$context->registerEventListener(RenderReferenceEvent::class, TablesReferenceListener::class);
8891
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
8992
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);

lib/Db/ContextMapper.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
/** @template-extends QBMapper<Context> */
2121
class ContextMapper extends QBMapper {
22+
private const DB_CHUNK_SIZE = 1_000;
23+
2224
protected string $table = 'tables_contexts_context';
2325

2426
public function __construct(
@@ -279,6 +281,33 @@ public function findAllContainingNode(int $nodeType, int $nodeId, string $userId
279281
return $resultEntities;
280282
}
281283

284+
/**
285+
* Fetch a map of id → name for the given context IDs.
286+
*
287+
* @param int[] $ids
288+
* @return array<int, string>
289+
* @throws Exception
290+
*/
291+
public function findIdToNameMap(array $ids): array {
292+
if ($ids === []) {
293+
return [];
294+
}
295+
$qb = $this->db->getQueryBuilder();
296+
$qb->select('id', 'name')
297+
->from($this->table)
298+
->where($qb->expr()->in('id', $qb->createParameter('ids')));
299+
$map = [];
300+
foreach (array_chunk($ids, self::DB_CHUNK_SIZE) as $chunk) {
301+
$qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
302+
$result = $qb->executeQuery();
303+
foreach ($result->fetchAll() as $row) {
304+
$map[(int)$row['id']] = (string)$row['name'];
305+
}
306+
$result->closeCursor();
307+
}
308+
return $map;
309+
}
310+
282311
protected function applyOwnedOrSharedQuery(IQueryBuilder $qb, string $userId): void {
283312
$sharedToConditions = $qb->expr()->orX();
284313

lib/Db/ShareMapper.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,54 @@ public function findAllSharesForTablesAndContexts(array $tableIds, array $contex
236236
return $this->findEntities($qb);
237237
}
238238

239+
/**
240+
* Yield all shares as raw associative arrays, ordered by id.
241+
*
242+
* Implemented as a generator so the full share list is never held in
243+
* memory alongside whatever the consumer builds from it.
244+
*
245+
* @return \Generator<int, array<string, mixed>>
246+
* @throws Exception
247+
*/
248+
public function findAllRaw(): \Generator {
249+
$qb = $this->db->getQueryBuilder();
250+
$qb->select(
251+
'id', 'sender', 'receiver', 'receiver_type', 'node_id', 'node_type',
252+
'token', 'password',
253+
'permission_read', 'permission_create', 'permission_update',
254+
'permission_delete', 'permission_manage',
255+
'created_at', 'last_edit_at'
256+
)->from($this->table)
257+
->orderBy('id', 'ASC');
258+
$result = $qb->executeQuery();
259+
try {
260+
while (($row = $result->fetch()) !== false) {
261+
yield $row;
262+
}
263+
} finally {
264+
$result->closeCursor();
265+
}
266+
}
267+
268+
/**
269+
* Fetch the distinct node IDs that have shares, grouped by node type.
270+
*
271+
* @return array<string, list<int>>
272+
* @throws Exception
273+
*/
274+
public function findSharedNodeIdsByType(): array {
275+
$qb = $this->db->getQueryBuilder();
276+
$qb->selectDistinct(['node_id', 'node_type'])
277+
->from($this->table);
278+
$result = $qb->executeQuery();
279+
$nodeIdsByType = [];
280+
while (($row = $result->fetch()) !== false) {
281+
$nodeIdsByType[(string)$row['node_type']][] = (int)$row['node_id'];
282+
}
283+
$result->closeCursor();
284+
return $nodeIdsByType;
285+
}
286+
239287
/**
240288
* @throws Exception
241289
*/

lib/Db/TableMapper.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
/** @template-extends QBMapper<Table> */
2121
class TableMapper extends QBMapper {
22+
private const DB_CHUNK_SIZE = 1_000;
23+
2224
protected string $table = 'tables_tables';
2325
protected CappedMemoryCache $cache;
2426
public function __construct(
@@ -176,4 +178,31 @@ public function insert(Entity $entity): Table {
176178
public function getDbConnection() {
177179
return $this->db;
178180
}
181+
182+
/**
183+
* Fetch a map of id → title for the given table IDs.
184+
*
185+
* @param int[] $ids
186+
* @return array<int, string>
187+
* @throws Exception
188+
*/
189+
public function findIdToTitleMap(array $ids): array {
190+
if ($ids === []) {
191+
return [];
192+
}
193+
$qb = $this->db->getQueryBuilder();
194+
$qb->select('id', 'title')
195+
->from($this->table)
196+
->where($qb->expr()->in('id', $qb->createParameter('ids')));
197+
$map = [];
198+
foreach (array_chunk($ids, self::DB_CHUNK_SIZE) as $chunk) {
199+
$qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
200+
$result = $qb->executeQuery();
201+
foreach ($result->fetchAll() as $row) {
202+
$map[(int)$row['id']] = (string)$row['title'];
203+
}
204+
$result->closeCursor();
205+
}
206+
return $map;
207+
}
179208
}

lib/Db/ViewMapper.php

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
/** @template-extends QBMapper<View> */
2121
class ViewMapper extends QBMapper {
22+
private const DB_CHUNK_SIZE = 1_000;
23+
2224
protected string $table = 'tables_views';
2325

2426
protected CappedMemoryCache $cache;
@@ -178,4 +180,31 @@ public function search(?string $term = null, ?string $userId = null, ?int $limit
178180

179181
return $this->findEntities($qb);
180182
}
183+
184+
/**
185+
* Fetch a map of id → title for the given view IDs.
186+
*
187+
* @param int[] $ids
188+
* @return array<int, string>
189+
* @throws Exception
190+
*/
191+
public function findIdToTitleMap(array $ids): array {
192+
if ($ids === []) {
193+
return [];
194+
}
195+
$qb = $this->db->getQueryBuilder();
196+
$qb->select('id', 'title')
197+
->from($this->table)
198+
->where($qb->expr()->in('id', $qb->createParameter('ids')));
199+
$map = [];
200+
foreach (array_chunk($ids, self::DB_CHUNK_SIZE) as $chunk) {
201+
$qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
202+
$result = $qb->executeQuery();
203+
foreach ($result->fetchAll() as $row) {
204+
$map[(int)$row['id']] = (string)$row['title'];
205+
}
206+
$result->closeCursor();
207+
}
208+
return $map;
209+
}
181210
}

lib/Service/ShareService.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,6 +767,25 @@ private function addReceiverDisplayNames(array $shares): array {
767767
return $shares;
768768
}
769769

770+
/**
771+
* Delete a share on behalf of a trusted share-review operation.
772+
*
773+
* PERMISSION_MANAGE is intentionally not checked. The caller must verify
774+
* operator access via ShareReviewAccessCheckEvent before invoking this
775+
* method. All other side effects are preserved so the deletion is auditable.
776+
*
777+
* @throws \OCP\AppFramework\Db\DoesNotExistException if $id does not exist
778+
* @throws Exception on database failure
779+
*/
780+
public function deleteForShareReview(int $id): void {
781+
$share = $this->mapper->find($id);
782+
$this->triggerShareActivity($share, ActivityManager::SUBJECT_SHARE_DELETE);
783+
$this->mapper->delete($share);
784+
if ($share->getNodeType() === 'context') {
785+
$this->contextNavigationMapper->deleteByShareId($share->getId());
786+
}
787+
}
788+
770789
public function deleteAllForTable(Table $table):void {
771790
try {
772791
$this->mapper->deleteByNode($table->getId(), 'table');
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Tables\ShareReview;
11+
12+
use OCP\EventDispatcher\Event;
13+
use OCP\EventDispatcher\IEventListener;
14+
use OCP\Share\ShareReview\RegisterShareReviewSourceEvent;
15+
16+
/** @template-implements IEventListener<RegisterShareReviewSourceEvent> */
17+
class ShareReviewListener implements IEventListener {
18+
public function __construct() {
19+
}
20+
21+
public function handle(Event $event): void {
22+
if (!$event instanceof RegisterShareReviewSourceEvent) {
23+
return;
24+
}
25+
$event->registerSource(ShareReviewSource::class);
26+
}
27+
}

0 commit comments

Comments
 (0)