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
65a7b27
feat(sharereview): add listener registering Tables as a share source
AndyScherzinger Jun 7, 2026
7e23687
feat(sharereview): add ShareReviewSource with constructor and getName()
AndyScherzinger Jun 7, 2026
ff805c8
feat(sharereview): implement getShares() with batched name lookups
AndyScherzinger Jun 7, 2026
55ca8a7
feat(sharereview): implement deleteShare() via direct SQL with logging
AndyScherzinger Jun 7, 2026
e80072c
feat(sharereview): register ShareReview listener on SourceEvent
AndyScherzinger Jun 7, 2026
41bb345
style(sharereview): apply coding standards and Psalm fixes
AndyScherzinger Jun 7, 2026
d21d6f2
test(sharereview): add unit tests for ShareReviewSource
AndyScherzinger Jun 7, 2026
1032d1e
fix(sharereview): harden and optimize implementation and testing
AndyScherzinger Jun 7, 2026
3b8266b
refactor(sharereview): replace raw SQL in deleteShare() with mapper l…
AndyScherzinger Jun 15, 2026
4d80200
fix(sharereview): remove redundant app key from getShares() output
AndyScherzinger Jun 15, 2026
03397be
fix(sharereview): localize user-facing strings in ShareReviewSource
AndyScherzinger Jun 15, 2026
844e4fe
fix(sharereview): log warnings for unknown node and receiver types of…
AndyScherzinger Jun 15, 2026
6004cbf
feat(sharereview): expose last-edit time instead of creation time to …
AndyScherzinger Jun 16, 2026
ed816f1
refactor(sharereview): move DB queries to mapper layer
AndyScherzinger Jun 22, 2026
dea5e08
refactor(sharereview): introduce ShareInfo DTO for typed share entries
AndyScherzinger Jun 22, 2026
3cc9ce3
fix(sharereview): use 'Application' as user-facing label for contexts
AndyScherzinger Jun 22, 2026
8f0518c
fix(sharereview): skip context navigation cleanup for non-context shares
AndyScherzinger Jun 22, 2026
b0444a4
feat(sharereview): gate ACL deletion behind event-based access check
AndyScherzinger Jun 22, 2026
ee92159
refactor(sharereview): adopt OCP\Share\ShareReview interface and even…
AndyScherzinger Jul 2, 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
3 changes: 3 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use OCA\Tables\Search\SearchTablesProvider;
use OCA\Tables\Service\Support\AuditLogServiceInterface;
use OCA\Tables\Service\Support\DefaultAuditLogService;
use OCA\Tables\ShareReview\ShareReviewListener;
use OCA\Tables\UserMigration\TablesMigrator;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
Expand All @@ -40,6 +41,7 @@
use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
use OCP\DB\Events\AddMissingIndicesEvent;
use OCP\Share\ShareReview\RegisterShareReviewSourceEvent;
use OCP\User\Events\BeforeUserDeletedEvent;
use Psr\Container\ContainerInterface;

Expand Down Expand Up @@ -79,6 +81,7 @@ public function register(IRegistrationContext $context): void {

$context->registerEventListener(BeforeUserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(DatasourceEvent::class, AnalyticsDatasourceListener::class);
$context->registerEventListener(RegisterShareReviewSourceEvent::class, ShareReviewListener::class);
$context->registerEventListener(RenderReferenceEvent::class, TablesReferenceListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
Expand Down
27 changes: 27 additions & 0 deletions lib/Db/ContextMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,33 @@ public function findAllContainingNode(int $nodeType, int $nodeId, string $userId
return $resultEntities;
}

/**
* Fetch a map of id β†’ name for the given context IDs.
*
* @param int[] $ids
* @return array<int, string>
* @throws Exception
*/
public function findIdToNameMap(array $ids): array {
if ($ids === []) {
return [];
}
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'name')
->from($this->table)
->where($qb->expr()->in('id', $qb->createParameter('ids')));
$map = [];
foreach (array_chunk($ids, 1_000) as $chunk) {
$qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
$result = $qb->executeQuery();
foreach ($result->fetchAll() as $row) {
$map[(int)$row['id']] = (string)$row['name'];
}
$result->closeCursor();
}
return $map;
}

protected function applyOwnedOrSharedQuery(IQueryBuilder $qb, string $userId): void {
$sharedToConditions = $qb->expr()->orX();

Expand Down
32 changes: 32 additions & 0 deletions lib/Db/ShareMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,16 @@ public function findAllSharesForNodeTo(string $nodeType, int $nodeId, string $re
return $this->findEntities($qb);
}

/**
* @throws Exception
*/
public function deleteById(int $id): bool {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->table)
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
return $qb->executeStatement() > 0;
}

/**
* @param int $nodeId
* @param string $nodeType
Expand Down Expand Up @@ -236,6 +246,28 @@ public function findAllSharesForTablesAndContexts(array $tableIds, array $contex
return $this->findEntities($qb);
}

/**
* Return all shares as raw associative arrays, ordered by id.
*
* @return list<array<string, mixed>>
* @throws Exception
*/
public function findAllRaw(): array {
$qb = $this->db->getQueryBuilder();
$qb->select(
'id', 'sender', 'receiver', 'receiver_type', 'node_id', 'node_type',
'token', 'password',
'permission_read', 'permission_create', 'permission_update',
'permission_delete', 'permission_manage',
'created_at', 'last_edit_at'
)->from($this->table)
->orderBy('id', 'ASC');
$result = $qb->executeQuery();
$rows = $result->fetchAll();
$result->closeCursor();
return $rows;
}

/**
* @throws Exception
*/
Expand Down
27 changes: 27 additions & 0 deletions lib/Db/TableMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,31 @@ public function insert(Entity $entity): Table {
public function getDbConnection() {
return $this->db;
}

/**
* Fetch a map of id β†’ title for the given table IDs.
*
* @param int[] $ids
* @return array<int, string>
* @throws Exception
*/
public function findIdToTitleMap(array $ids): array {
if ($ids === []) {
return [];
}
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'title')
->from($this->table)
->where($qb->expr()->in('id', $qb->createParameter('ids')));
$map = [];
foreach (array_chunk($ids, 1_000) as $chunk) {
$qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
$result = $qb->executeQuery();
foreach ($result->fetchAll() as $row) {
$map[(int)$row['id']] = (string)$row['title'];
}
$result->closeCursor();
}
return $map;
}
}
27 changes: 27 additions & 0 deletions lib/Db/ViewMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,31 @@ public function search(?string $term = null, ?string $userId = null, ?int $limit

return $this->findEntities($qb);
}

/**
* Fetch a map of id β†’ title for the given view IDs.
*
* @param int[] $ids
* @return array<int, string>
* @throws Exception
*/
public function findIdToTitleMap(array $ids): array {
if ($ids === []) {
return [];
}
$qb = $this->db->getQueryBuilder();
$qb->select('id', 'title')
->from($this->table)
->where($qb->expr()->in('id', $qb->createParameter('ids')));
$map = [];
foreach (array_chunk($ids, 1_000) as $chunk) {
$qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
$result = $qb->executeQuery();
foreach ($result->fetchAll() as $row) {
$map[(int)$row['id']] = (string)$row['title'];
}
$result->closeCursor();
}
return $map;
}
}
18 changes: 18 additions & 0 deletions lib/Service/ShareService.php
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,24 @@ private function addReceiverDisplayNames(array $shares): array {
return $shares;
}

/**
* Delete a share on behalf of a trusted share-review operation.
*
* PERMISSION_MANAGE is intentionally not checked. The caller must verify
* operator access via ShareReviewAccessCheckEvent before invoking this
* method. All other side effects are preserved so the deletion is auditable.
*
* @throws \OCP\AppFramework\Db\DoesNotExistException if $id does not exist
* @throws Exception on database failure
*/
public function deleteForShareReview(int $id): void {
$share = $this->mapper->find($id);
$this->mapper->delete($share);
if ($share->getNodeType() === 'context') {
$this->contextNavigationMapper->deleteByShareId($share->getId());
}
}

public function deleteAllForTable(Table $table):void {
try {
$this->mapper->deleteByNode($table->getId(), 'table');
Expand Down
56 changes: 56 additions & 0 deletions lib/ShareReview/ShareInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

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

namespace OCA\Tables\ShareReview;

/**
* Typed container for a single share entry returned by ShareReviewSource.
*
* @psalm-type ShareInfoArray = array{
* id: int,
* object: string,
* initiator: string,
* type: int,
* recipient: string,
* permissions: int,
* password: bool,
* time: string,
* action: string,
* }
*/
class ShareInfo {
public function __construct(
public readonly int $id,
public readonly string $object,
public readonly string $initiator,
public readonly int $type,
public readonly string $recipient,
public readonly int $permissions,
public readonly bool $password,
public readonly string $time,
) {
}

/**
* @return array{id: int, object: string, initiator: string, type: int, recipient: string, permissions: int, password: bool, time: string, action: string}
*/
public function toArray(): array {
return [
'id' => $this->id,
'object' => $this->object,
'initiator' => $this->initiator,
'type' => $this->type,
'recipient' => $this->recipient,
'permissions' => $this->permissions,
'password' => $this->password,
'time' => $this->time,
'action' => '',
];
}
}
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\Tables\ShareReview;

use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Share\ShareReview\RegisterShareReviewSourceEvent;

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

public function handle(Event $event): void {
if (!$event instanceof RegisterShareReviewSourceEvent) {
return;
}
$event->registerSource(ShareReviewSource::class);
}
}
Loading
Loading