Skip to content

Commit f1a8618

Browse files
Merge pull request #19240 from nextcloud/bugfix/noid/share-cleanup
fix(sharing): Add a repair step to remove orphan entries
2 parents aed601b + ad70ce1 commit f1a8618

8 files changed

Lines changed: 710 additions & 35 deletions

File tree

appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
<step>OCA\Talk\Migration\FixNamespaceInDatabaseTables</step>
8787
</pre-migration>
8888
<post-migration>
89+
<step>OCA\Talk\Migration\CleanupOldShares</step>
8990
<step>OCA\Talk\Migration\ClearResourceAccessCache</step>
9091
<step>OCA\Talk\Migration\CacheUserDisplayNames</step>
9192
<step>OCA\Talk\Migration\FixLastReadMessageZero</step>

lib/AppInfo/Application.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ public function register(IRegistrationContext $context): void {
274274
// Sharing listeners
275275
$context->registerEventListener(BeforeShareCreatedEvent::class, ShareListener::class, 1000);
276276
$context->registerEventListener(VerifyMountPointEvent::class, ShareListener::class, 1000);
277+
$context->registerEventListener(AttendeesRemovedEvent::class, ShareListener::class);
277278
$context->registerEventListener(RoomDeletedEvent::class, ShareListener::class);
278279

279280
// Group and Circles listeners

lib/Migration/CleanupOldShares.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Talk\Migration;
10+
11+
use OCA\Talk\Model\Attendee;
12+
use OCA\Talk\Share\RoomShareProvider;
13+
use OCP\DB\QueryBuilder\IQueryBuilder;
14+
use OCP\IDBConnection;
15+
use OCP\Migration\IOutput;
16+
use OCP\Migration\IRepairStep;
17+
use OCP\Share\IShare;
18+
19+
/**
20+
* Remove old @see RoomShareProvider::SHARE_TYPE_USERROOM shares when the user
21+
* is no longer part of the conversation
22+
*/
23+
class CleanupOldShares implements IRepairStep {
24+
25+
public const int SELECT_CHUNK_SIZE = 50_000;
26+
public function __construct(
27+
private readonly IDBConnection $connection,
28+
) {
29+
}
30+
31+
#[\Override]
32+
public function getName(): string {
33+
return 'Remove old user-room-shares';
34+
}
35+
36+
#[\Override]
37+
public function run(IOutput $output): void {
38+
// Select SHARE_TYPE_USERROOM shares when the user is no longer an attendee in the room
39+
$query = $this->connection->getQueryBuilder();
40+
$query->select('su.id')
41+
->from('share', 'su')
42+
->leftJoin('su', 'share', 'sr', $query->expr()->andX(
43+
$query->expr()->eq('su.parent', 'sr.id'),
44+
$query->expr()->eq('sr.share_type', $query->createNamedParameter(IShare::TYPE_ROOM)),
45+
))
46+
->leftJoin('sr', 'talk_rooms', 'r', $query->expr()->eq('sr.share_with', 'r.token'))
47+
->leftJoin('r', 'talk_attendees', 'a', $query->expr()->andX(
48+
$query->expr()->eq('r.id', 'a.room_id'),
49+
$query->expr()->eq('su.share_with', 'a.actor_id'),
50+
$query->expr()->eq('a.actor_type', $query->createNamedParameter(Attendee::ACTOR_USERS)),
51+
))
52+
->where($query->expr()->eq('su.share_type', $query->createNamedParameter(RoomShareProvider::SHARE_TYPE_USERROOM)))
53+
->andWhere($query->expr()->isNull('a.id'))
54+
->setMaxResults(self::SELECT_CHUNK_SIZE);
55+
56+
// Deleting the SHARE_TYPE_USERROOM shares based on their ID
57+
$delete = $this->connection->getQueryBuilder();
58+
$delete->delete('share')
59+
->where($delete->expr()->eq('share_type', $delete->createNamedParameter(RoomShareProvider::SHARE_TYPE_USERROOM)))
60+
->andWhere($delete->expr()->in('id', $delete->createParameter('ids')));
61+
62+
$output->startProgress();
63+
$numDeleted = 0;
64+
do {
65+
$deletedInChunk = $this->deleteSomeFormerShares($output, $query, $delete);
66+
$numDeleted += $deletedInChunk;
67+
} while ($deletedInChunk !== 0);
68+
$output->finishProgress();
69+
70+
$output->info('Deleted ' . $numDeleted . ' stray shares');
71+
}
72+
73+
protected function deleteSomeFormerShares(IOutput $output, IQueryBuilder $select, IQueryBuilder $delete): int {
74+
$result = $select->executeQuery();
75+
$ids = [];
76+
while ($row = $result->fetchAssociative()) {
77+
$ids[] = (int)$row['id'];
78+
}
79+
$result->closeCursor();
80+
81+
if (empty($ids)) {
82+
return 0;
83+
}
84+
85+
$count = 0;
86+
$chunks = array_chunk($ids, IQueryBuilder::MAX_IN_PARAMETERS);
87+
foreach ($chunks as $chunk) {
88+
$delete->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
89+
$delete->executeStatement();
90+
$output->advance(count($chunk));
91+
$count += count($chunk);
92+
}
93+
return $count;
94+
}
95+
}

lib/Share/Listener.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010

1111
use OC\Files\Filesystem;
1212
use OCA\Talk\Config;
13+
use OCA\Talk\Events\AttendeesRemovedEvent;
1314
use OCA\Talk\Events\RoomDeletedEvent;
1415
use OCA\Talk\Exceptions\RoomNotFoundException;
1516
use OCA\Talk\Manager;
17+
use OCA\Talk\Model\Attendee;
1618
use OCA\Talk\Service\ConversationFolderService;
1719
use OCP\EventDispatcher\Event;
1820
use OCP\EventDispatcher\IEventListener;
@@ -38,6 +40,7 @@ public function handle(Event $event): void {
3840
$event instanceof BeforeShareCreatedEvent => $this->overwriteShareTarget($event),
3941
$event instanceof VerifyMountPointEvent => $this->overwriteMountPoint($event),
4042
$event instanceof RoomDeletedEvent => $this->roomDeletedEvent($event),
43+
$event instanceof AttendeesRemovedEvent => $this->roomAttendeesRemovedEvent($event),
4144
default => null,
4245
};
4346
}
@@ -147,4 +150,16 @@ protected function overwriteMountPoint(VerifyMountPointEvent $event): void {
147150
protected function roomDeletedEvent(RoomDeletedEvent $event): void {
148151
$this->roomShareProvider->deleteInRoom($event->getRoom()->getToken());
149152
}
153+
154+
protected function roomAttendeesRemovedEvent(AttendeesRemovedEvent $event): void {
155+
$userIds = array_values(array_map(
156+
static fn (Attendee $attendee): string => $attendee->getActorId(),
157+
array_filter(
158+
$event->getAttendees(),
159+
static fn (Attendee $attendee): bool => $attendee->getActorType() === Attendee::ACTOR_USERS
160+
)
161+
));
162+
163+
$this->roomShareProvider->deleteReceivedSharesInRoom($event->getRoom()->getToken(), $userIds);
164+
}
150165
}

lib/Share/RoomShareProvider.php

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,18 +1234,14 @@ public function getChildren(IShare $parent): array {
12341234
}
12351235

12361236
/**
1237-
* Delete all shares in a room, or only those from the given user.
1238-
*
1239-
* When a user is given all their shares are removed, both own shares and
1240-
* received shares.
1237+
* Delete all shares in a room
12411238
*
12421239
* Not part of IShareProvider API, but needed by the hooks in
1243-
* OCA\Talk\AppInfo\Application
1240+
* {@see Listener::roomDeletedEvent()}
12441241
*
12451242
* @param string $roomToken
1246-
* @param string|null $user
12471243
*/
1248-
public function deleteInRoom(string $roomToken, ?string $user = null): void {
1244+
public function deleteInRoom(string $roomToken): void {
12491245
$this->cleanSharesByIdCache();
12501246

12511247
//First delete all custom room shares for the original shares to be removed
@@ -1255,10 +1251,6 @@ public function deleteInRoom(string $roomToken, ?string $user = null): void {
12551251
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
12561252
->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($roomToken)));
12571253

1258-
if ($user !== null) {
1259-
$qb->andWhere($qb->expr()->eq('uid_initiator', $qb->createNamedParameter($user)));
1260-
}
1261-
12621254
$cursor = $qb->executeQuery();
12631255
$ids = [];
12641256
while ($row = $cursor->fetchAssociative()) {
@@ -1285,36 +1277,49 @@ public function deleteInRoom(string $roomToken, ?string $user = null): void {
12851277
$delete->delete('share')
12861278
->where($delete->expr()->eq('share_type', $delete->createNamedParameter(IShare::TYPE_ROOM)))
12871279
->andWhere($delete->expr()->eq('share_with', $delete->createNamedParameter($roomToken)));
1280+
$delete->executeStatement();
1281+
}
12881282

1289-
if ($user !== null) {
1290-
$delete->andWhere($delete->expr()->eq('uid_initiator', $delete->createNamedParameter($user)));
1291-
}
1283+
/**
1284+
* Delete all received shares for a user in a room
1285+
*
1286+
* Not part of IShareProvider API, but needed by the hooks in
1287+
* {@see Listener::roomAttendeesRemovedEvent()}
1288+
*
1289+
* @param string $roomToken
1290+
* @param list<string> $userIds
1291+
*/
1292+
public function deleteReceivedSharesInRoom(string $roomToken, array $userIds): void {
1293+
$this->cleanSharesByIdCache();
12921294

1293-
$delete->executeStatement();
1295+
$qb = $this->dbConnection->getQueryBuilder();
1296+
$qb->select('id')
1297+
->from('share')
1298+
->where($qb->expr()->eq('share_type', $qb->createNamedParameter(IShare::TYPE_ROOM)))
1299+
->andWhere($qb->expr()->eq('share_with', $qb->createNamedParameter($roomToken)));
12941300

1295-
// Finally delete all custom room shares leftovers for the given user
1296-
if ($user !== null) {
1297-
$query = $this->dbConnection->getQueryBuilder();
1298-
$query->select('id')
1299-
->from('share')
1300-
->where($query->expr()->eq('share_type', $query->createNamedParameter(IShare::TYPE_ROOM)))
1301-
->andWhere($query->expr()->eq('share_with', $query->createNamedParameter($roomToken)));
1301+
$cursor = $qb->executeQuery();
1302+
$ids = [];
1303+
while ($row = $cursor->fetchAssociative()) {
1304+
$ids[] = (int)$row['id'];
1305+
}
1306+
$cursor->closeCursor();
13021307

1303-
$cursor = $query->executeQuery();
1304-
$ids = [];
1305-
while ($row = $cursor->fetchAssociative()) {
1306-
$ids[] = (int)$row['id'];
1307-
}
1308-
$cursor->closeCursor();
1308+
if (!empty($ids)) {
1309+
$delete = $this->dbConnection->getQueryBuilder();
1310+
$delete->delete('share')
1311+
->where($delete->expr()->eq('share_type', $delete->createNamedParameter(self::SHARE_TYPE_USERROOM)))
1312+
->andWhere($delete->expr()->in('share_with', $delete->createParameter('userids')))
1313+
->andWhere($delete->expr()->in('parent', $delete->createParameter('ids')));
1314+
1315+
$chunkSize = min(100, IQueryBuilder::MAX_IN_PARAMETERS);
13091316

1310-
if (!empty($ids)) {
1311-
$chunkSize = min(100, IQueryBuilder::MAX_IN_PARAMETERS);
1312-
$chunks = array_chunk($ids, $chunkSize);
1317+
$userChunks = array_chunk($userIds, $chunkSize);
1318+
$chunks = array_chunk($ids, $chunkSize);
1319+
foreach ($userChunks as $userChunk) {
13131320
foreach ($chunks as $chunk) {
1314-
$delete->delete('share')
1315-
->where($delete->expr()->eq('share_type', $delete->createNamedParameter(self::SHARE_TYPE_USERROOM)))
1316-
->andWhere($delete->expr()->in('share_with', $delete->createNamedParameter($user)))
1317-
->andWhere($delete->expr()->in('parent', $delete->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)));
1321+
$delete->setParameter('userids', $userChunk, IQueryBuilder::PARAM_STR_ARRAY)
1322+
->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY);
13181323
$delete->executeStatement();
13191324
}
13201325
}

0 commit comments

Comments
 (0)