Skip to content

Commit bcdfc45

Browse files
committed
feat: perform share mount validation on share instead of on mount
Signed-off-by: Robin Appelman <[email protected]>
1 parent c614a13 commit bcdfc45

File tree

15 files changed

+396
-383
lines changed

15 files changed

+396
-383
lines changed

apps/files_sharing/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => $baseDir . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',
7070
'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
7171
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php',
72+
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => $baseDir . '/../lib/Listener/SharesUpdatedListener.php',
7273
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php',
7374
'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => $baseDir . '/../lib/Listener/UserShareAcceptanceListener.php',
7475
'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => $baseDir . '/../lib/Middleware/OCSShareAPIMiddleware.php',
@@ -94,6 +95,7 @@
9495
'OCA\\Files_Sharing\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php',
9596
'OCA\\Files_Sharing\\ShareBackend\\File' => $baseDir . '/../lib/ShareBackend/File.php',
9697
'OCA\\Files_Sharing\\ShareBackend\\Folder' => $baseDir . '/../lib/ShareBackend/Folder.php',
98+
'OCA\\Files_Sharing\\ShareTargetValidator' => $baseDir . '/../lib/ShareTargetValidator.php',
9799
'OCA\\Files_Sharing\\SharedMount' => $baseDir . '/../lib/SharedMount.php',
98100
'OCA\\Files_Sharing\\SharedStorage' => $baseDir . '/../lib/SharedStorage.php',
99101
'OCA\\Files_Sharing\\SharesReminderJob' => $baseDir . '/../lib/SharesReminderJob.php',

apps/files_sharing/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class ComposerStaticInitFiles_Sharing
8484
'OCA\\Files_Sharing\\Listener\\LoadPublicFileRequestAuthListener' => __DIR__ . '/..' . '/../lib/Listener/LoadPublicFileRequestAuthListener.php',
8585
'OCA\\Files_Sharing\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
8686
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php',
87+
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => __DIR__ . '/..' . '/../lib/Listener/SharesUpdatedListener.php',
8788
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php',
8889
'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => __DIR__ . '/..' . '/../lib/Listener/UserShareAcceptanceListener.php',
8990
'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/OCSShareAPIMiddleware.php',
@@ -109,6 +110,7 @@ class ComposerStaticInitFiles_Sharing
109110
'OCA\\Files_Sharing\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php',
110111
'OCA\\Files_Sharing\\ShareBackend\\File' => __DIR__ . '/..' . '/../lib/ShareBackend/File.php',
111112
'OCA\\Files_Sharing\\ShareBackend\\Folder' => __DIR__ . '/..' . '/../lib/ShareBackend/Folder.php',
113+
'OCA\\Files_Sharing\\ShareTargetValidator' => __DIR__ . '/..' . '/../lib/ShareTargetValidator.php',
112114
'OCA\\Files_Sharing\\SharedMount' => __DIR__ . '/..' . '/../lib/SharedMount.php',
113115
'OCA\\Files_Sharing\\SharedStorage' => __DIR__ . '/..' . '/../lib/SharedStorage.php',
114116
'OCA\\Files_Sharing\\SharesReminderJob' => __DIR__ . '/..' . '/../lib/SharesReminderJob.php',

apps/files_sharing/lib/AppInfo/Application.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use OCA\Files_Sharing\Listener\LoadPublicFileRequestAuthListener;
2525
use OCA\Files_Sharing\Listener\LoadSidebarListener;
2626
use OCA\Files_Sharing\Listener\ShareInteractionListener;
27+
use OCA\Files_Sharing\Listener\SharesUpdatedListener;
2728
use OCA\Files_Sharing\Listener\UserAddedToGroupListener;
2829
use OCA\Files_Sharing\Listener\UserShareAcceptanceListener;
2930
use OCA\Files_Sharing\Middleware\OCSShareAPIMiddleware;
@@ -49,9 +50,11 @@
4950
use OCP\Group\Events\GroupChangedEvent;
5051
use OCP\Group\Events\GroupDeletedEvent;
5152
use OCP\Group\Events\UserAddedEvent;
53+
use OCP\Group\Events\UserRemovedEvent;
5254
use OCP\IDBConnection;
5355
use OCP\IGroup;
5456
use OCP\Share\Events\ShareCreatedEvent;
57+
use OCP\Share\Events\ShareDeletedEvent;
5558
use OCP\User\Events\UserChangedEvent;
5659
use OCP\User\Events\UserDeletedEvent;
5760
use OCP\Util;
@@ -109,6 +112,12 @@ function () use ($c) {
109112
// File request auth
110113
$context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadPublicFileRequestAuthListener::class);
111114

115+
// Update mounts
116+
$context->registerEventListener(ShareCreatedEvent::class, SharesUpdatedListener::class);
117+
$context->registerEventListener(ShareDeletedEvent::class, SharesUpdatedListener::class);
118+
$context->registerEventListener(UserAddedEvent::class, SharesUpdatedListener::class);
119+
$context->registerEventListener(UserRemovedEvent::class, SharesUpdatedListener::class);
120+
112121
$context->registerConfigLexicon(ConfigLexicon::class);
113122
}
114123

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Robin Appelman <[email protected]>
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Files_Sharing\Listener;
10+
11+
use OCA\Files_Sharing\MountProvider;
12+
use OCA\Files_Sharing\ShareTargetValidator;
13+
use OCP\EventDispatcher\Event;
14+
use OCP\EventDispatcher\IEventListener;
15+
use OCP\Files\Config\ICachedMountInfo;
16+
use OCP\Files\Config\IUserMountCache;
17+
use OCP\Group\Events\UserAddedEvent;
18+
use OCP\Group\Events\UserRemovedEvent;
19+
use OCP\IUser;
20+
use OCP\Share\Events\ShareCreatedEvent;
21+
use OCP\Share\Events\ShareDeletedEvent;
22+
use OCP\Share\IManager;
23+
24+
/**
25+
* Listen to various events that can change what shares a user has access to
26+
*
27+
* @template-implements IEventListener<UserAddedEvent|UserRemovedEvent|ShareCreatedEvent|ShareDeletedEvent>
28+
*/
29+
class SharesUpdatedListener implements IEventListener {
30+
public function __construct(
31+
private readonly IManager $shareManager,
32+
private readonly IUserMountCache $userMountCache,
33+
private readonly MountProvider $shareMountProvider,
34+
private readonly ShareTargetValidator $shareTargetValidator,
35+
) {
36+
}
37+
38+
public function handle(Event $event): void {
39+
if ($event instanceof UserAddedEvent || $event instanceof UserRemovedEvent) {
40+
$this->updateForUser($event->getUser());
41+
}
42+
if ($event instanceof ShareCreatedEvent || $event instanceof ShareDeletedEvent) {
43+
foreach ($this->shareManager->getUsersForShare($event->getShare()) as $user) {
44+
$this->updateForUser($user);
45+
}
46+
}
47+
}
48+
49+
private function updateForUser(IUser $user): void {
50+
$cachedMounts = $this->userMountCache->getMountsForUser($user);
51+
$mountPoints = array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts);
52+
$mountsByPath = array_combine($mountPoints, $cachedMounts);
53+
54+
$shares = $this->shareMountProvider->getSuperSharesForUser($user);
55+
56+
foreach ($shares as &$share) {
57+
[$parentShare, $groupedShares] = $share;
58+
$mountPoint = '/' . $user->getUID() . '/files/' . trim($parentShare->getTarget(), '/') . '/';
59+
$mountKey = $parentShare->getNodeId() . '::' . $mountPoint;
60+
if (!isset($cachedMounts[$mountKey])) {
61+
$this->shareTargetValidator->verifyMountPoint($user, $parentShare, $mountsByPath, $groupedShares);
62+
}
63+
}
64+
}
65+
}

apps/files_sharing/lib/MountProvider.php

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ public function __construct(
5050
* @return IMountPoint[]
5151
*/
5252
public function getMountsForUser(IUser $user, IStorageFactory $loader) {
53+
return $this->getMountsFromSuperShares($user, $this->getSuperSharesForUser($user), $loader);
54+
}
55+
56+
/**
57+
* @param IUser $user
58+
* @return list<array{IShare, array<IShare>}> Tuple of [superShare, groupedShares]
59+
*/
60+
public function getSuperSharesForUser(IUser $user): array {
5361
$userId = $user->getUID();
5462
$shares = array_merge(
5563
$this->shareManager->getSharedWith($userId, IShare::TYPE_USER, null, -1),
@@ -60,9 +68,7 @@ public function getMountsForUser(IUser $user, IStorageFactory $loader) {
6068
);
6169

6270
$shares = $this->filterShares($shares, $userId);
63-
$superShares = $this->buildSuperShares($shares, $user);
64-
65-
return $this->getMountsFromSuperShares($userId, $superShares, $loader, $user);
71+
return $this->buildSuperShares($shares, $user);
6672
}
6773

6874
/**
@@ -245,18 +251,18 @@ private function adjustTarget(
245251
}
246252
/**
247253
* @param string $userId
248-
* @param array $superShares
254+
* @param list<array{IShare, array<IShare>}> $superShares
249255
* @param IStorageFactory $loader
250256
* @param IUser $user
251257
* @return array
252258
* @throws Exception
253259
*/
254-
private function getMountsFromSuperShares(
255-
string $userId,
260+
public function getMountsFromSuperShares(
261+
IUser $user,
256262
array $superShares,
257263
IStorageFactory $loader,
258-
IUser $user,
259264
): array {
265+
$userId = $user->getUID();
260266
$allMounts = $this->mountManager->getAll();
261267
$mounts = [];
262268
$view = new View('/' . $userId . '/files');
@@ -289,7 +295,6 @@ private function getMountsFromSuperShares(
289295
$shareId = (int)$parentShare->getId();
290296
$mount = new SharedMount(
291297
'\OCA\Files_Sharing\SharedStorage',
292-
$allMounts,
293298
[
294299
'user' => $userId,
295300
// parent share
@@ -300,11 +305,8 @@ private function getMountsFromSuperShares(
300305
'sharingDisabledForUser' => $sharingDisabledForUser
301306
],
302307
$loader,
303-
$view,
304-
$foldersExistCache,
305308
$this->eventDispatcher,
306309
$user,
307-
$shareId <= $maxValidatedShare,
308310
);
309311

310312
$newMaxValidatedShare = max($shareId, $newMaxValidatedShare);
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2025 Robin Appelman <[email protected]>
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
9+
namespace OCA\Files_Sharing;
10+
11+
use OC\Files\Filesystem;
12+
use OC\Files\SetupManager;
13+
use OC\Files\View;
14+
use OCP\Cache\CappedMemoryCache;
15+
use OCP\EventDispatcher\IEventDispatcher;
16+
use OCP\Files\Config\ICachedMountInfo;
17+
use OCP\Files\Mount\IMountManager;
18+
use OCP\Files\Mount\IMountPoint;
19+
use OCP\IUser;
20+
use OCP\Share\Events\VerifyMountPointEvent;
21+
use OCP\Share\IManager;
22+
use OCP\Share\IShare;
23+
24+
/**
25+
* Validate that mount target is valid
26+
*/
27+
class ShareTargetValidator {
28+
private CappedMemoryCache $folderExistsCache;
29+
30+
public function __construct(
31+
private readonly IManager $shareManager,
32+
private readonly IEventDispatcher $eventDispatcher,
33+
private readonly SetupManager $setupManager,
34+
private readonly IMountManager $mountManager,
35+
) {
36+
$this->folderExistsCache = new CappedMemoryCache();
37+
}
38+
39+
private function getViewForUser(IUser $user): View {
40+
/**
41+
* @psalm-suppress InternalClass
42+
* @psalm-suppress InternalMethod
43+
*/
44+
return new View('/' . $user->getUID() . '/files');
45+
}
46+
47+
/**
48+
* check if the parent folder exists otherwise move the mount point up
49+
*
50+
* @param array<string, ICachedMountInfo> $allCachedMounts Other mounts for the user, indexed by path
51+
* @param IShare[] $childShares
52+
* @return string
53+
*/
54+
public function verifyMountPoint(
55+
IUser $user,
56+
IShare &$share,
57+
array $allCachedMounts,
58+
array $childShares,
59+
): string {
60+
$mountPoint = basename($share->getTarget());
61+
$parent = dirname($share->getTarget());
62+
63+
$recipientView = $this->getViewForUser($user);
64+
$event = new VerifyMountPointEvent($share, $recipientView, $parent);
65+
$this->eventDispatcher->dispatchTyped($event);
66+
$parent = $event->getParent();
67+
68+
/** @psalm-suppress InternalMethod */
69+
$absoluteParent = $recipientView->getAbsolutePath($parent);
70+
$this->setupManager->setupForPath($absoluteParent);
71+
$parentMount = $this->mountManager->find($absoluteParent);
72+
73+
$cached = $this->folderExistsCache->get($parent);
74+
if ($cached) {
75+
$parentExists = $cached;
76+
} else {
77+
$parentCache = $parentMount->getStorage()->getCache();
78+
$parentExists = $parentCache->inCache($parentMount->getInternalPath($absoluteParent));
79+
$this->folderExistsCache->set($parent, $parentExists);
80+
}
81+
if (!$parentExists) {
82+
$parent = Helper::getShareFolder($recipientView, $user->getUID());
83+
/** @psalm-suppress InternalMethod */
84+
$absoluteParent = $recipientView->getAbsolutePath($parent);
85+
}
86+
87+
$newAbsoluteMountPoint = $this->generateUniqueTarget(
88+
Filesystem::normalizePath($absoluteParent . '/' . $mountPoint),
89+
$parentMount,
90+
$allCachedMounts,
91+
);
92+
93+
/** @psalm-suppress InternalMethod */
94+
$newMountPoint = $recipientView->getRelativePath($newAbsoluteMountPoint);
95+
if ($newMountPoint === null) {
96+
return $share->getTarget();
97+
}
98+
99+
if ($newMountPoint !== $share->getTarget()) {
100+
$this->updateFileTarget($user, $newMountPoint, $share, $childShares);
101+
}
102+
103+
return $newMountPoint;
104+
}
105+
106+
107+
/**
108+
* @param ICachedMountInfo[] $allCachedMounts
109+
*/
110+
private function generateUniqueTarget(string $absolutePath, IMountPoint $parentMount, array $allCachedMounts): string {
111+
$pathInfo = pathinfo($absolutePath);
112+
$ext = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';
113+
$name = $pathInfo['filename'];
114+
$dir = $pathInfo['dirname'];
115+
116+
$i = 2;
117+
$parentCache = $parentMount->getStorage()->getCache();
118+
$internalPath = $parentMount->getInternalPath($absolutePath);
119+
while ($parentCache->inCache($internalPath) || isset($allCachedMounts[$absolutePath . '/'])) {
120+
$absolutePath = Filesystem::normalizePath($dir . '/' . $name . ' (' . $i . ')' . $ext);
121+
$internalPath = $parentMount->getInternalPath($absolutePath);
122+
$i++;
123+
}
124+
125+
return $absolutePath;
126+
}
127+
128+
/**
129+
* update fileTarget in the database if the mount point changed
130+
*
131+
* @param IShare[] $childShares
132+
*/
133+
private function updateFileTarget(IUser $user, string $newPath, IShare &$share, array $childShares) {
134+
$share->setTarget($newPath);
135+
136+
foreach ($childShares as $tmpShare) {
137+
$tmpShare->setTarget($newPath);
138+
$this->shareManager->moveShare($tmpShare, $user->getUID());
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)