Skip to content
2 changes: 2 additions & 0 deletions apps/files_sharing/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => $baseDir . '/../lib/Listener/ShareInteractionListener.php',
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => $baseDir . '/../lib/Listener/SharesUpdatedListener.php',
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => $baseDir . '/../lib/Listener/UserAddedToGroupListener.php',
'OCA\\Files_Sharing\\Listener\\UserHomeSetupListener' => $baseDir . '/../lib/Listener/UserHomeSetupListener.php',
'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => $baseDir . '/../lib/Listener/UserShareAcceptanceListener.php',
'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => $baseDir . '/../lib/Middleware/OCSShareAPIMiddleware.php',
'OCA\\Files_Sharing\\Middleware\\ShareInfoMiddleware' => $baseDir . '/../lib/Middleware/ShareInfoMiddleware.php',
Expand All @@ -98,6 +99,7 @@
'OCA\\Files_Sharing\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php',
'OCA\\Files_Sharing\\ShareBackend\\File' => $baseDir . '/../lib/ShareBackend/File.php',
'OCA\\Files_Sharing\\ShareBackend\\Folder' => $baseDir . '/../lib/ShareBackend/Folder.php',
'OCA\\Files_Sharing\\ShareRecipientUpdater' => $baseDir . '/../lib/ShareRecipientUpdater.php',
'OCA\\Files_Sharing\\ShareTargetValidator' => $baseDir . '/../lib/ShareTargetValidator.php',
'OCA\\Files_Sharing\\SharedMount' => $baseDir . '/../lib/SharedMount.php',
'OCA\\Files_Sharing\\SharedStorage' => $baseDir . '/../lib/SharedStorage.php',
Expand Down
2 changes: 2 additions & 0 deletions apps/files_sharing/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Listener\\ShareInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/ShareInteractionListener.php',
'OCA\\Files_Sharing\\Listener\\SharesUpdatedListener' => __DIR__ . '/..' . '/../lib/Listener/SharesUpdatedListener.php',
'OCA\\Files_Sharing\\Listener\\UserAddedToGroupListener' => __DIR__ . '/..' . '/../lib/Listener/UserAddedToGroupListener.php',
'OCA\\Files_Sharing\\Listener\\UserHomeSetupListener' => __DIR__ . '/..' . '/../lib/Listener/UserHomeSetupListener.php',
'OCA\\Files_Sharing\\Listener\\UserShareAcceptanceListener' => __DIR__ . '/..' . '/../lib/Listener/UserShareAcceptanceListener.php',
'OCA\\Files_Sharing\\Middleware\\OCSShareAPIMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/OCSShareAPIMiddleware.php',
'OCA\\Files_Sharing\\Middleware\\ShareInfoMiddleware' => __DIR__ . '/..' . '/../lib/Middleware/ShareInfoMiddleware.php',
Expand All @@ -113,6 +114,7 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php',
'OCA\\Files_Sharing\\ShareBackend\\File' => __DIR__ . '/..' . '/../lib/ShareBackend/File.php',
'OCA\\Files_Sharing\\ShareBackend\\Folder' => __DIR__ . '/..' . '/../lib/ShareBackend/Folder.php',
'OCA\\Files_Sharing\\ShareRecipientUpdater' => __DIR__ . '/..' . '/../lib/ShareRecipientUpdater.php',
'OCA\\Files_Sharing\\ShareTargetValidator' => __DIR__ . '/..' . '/../lib/ShareTargetValidator.php',
'OCA\\Files_Sharing\\SharedMount' => __DIR__ . '/..' . '/../lib/SharedMount.php',
'OCA\\Files_Sharing\\SharedStorage' => __DIR__ . '/..' . '/../lib/SharedStorage.php',
Expand Down
3 changes: 3 additions & 0 deletions apps/files_sharing/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use OCA\Files_Sharing\Listener\ShareInteractionListener;
use OCA\Files_Sharing\Listener\SharesUpdatedListener;
use OCA\Files_Sharing\Listener\UserAddedToGroupListener;
use OCA\Files_Sharing\Listener\UserHomeSetupListener;
use OCA\Files_Sharing\Listener\UserShareAcceptanceListener;
use OCA\Files_Sharing\Middleware\OCSShareAPIMiddleware;
use OCA\Files_Sharing\Middleware\ShareInfoMiddleware;
Expand All @@ -48,6 +49,7 @@
use OCP\Files\Events\BeforeDirectFileDownloadEvent;
use OCP\Files\Events\BeforeZipCreatedEvent;
use OCP\Files\Events\Node\BeforeNodeReadEvent;
use OCP\Files\Events\UserHomeSetupEvent;
use OCP\Group\Events\GroupChangedEvent;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\Group\Events\UserAddedEvent;
Expand Down Expand Up @@ -121,6 +123,7 @@ function () use ($c) {
$context->registerEventListener(UserAddedEvent::class, SharesUpdatedListener::class);
$context->registerEventListener(UserRemovedEvent::class, SharesUpdatedListener::class);
$context->registerEventListener(UserShareAccessUpdatedEvent::class, SharesUpdatedListener::class);
$context->registerEventListener(UserHomeSetupEvent::class, UserHomeSetupListener::class);

$context->registerConfigLexicon(ConfigLexicon::class);
}
Expand Down
10 changes: 9 additions & 1 deletion apps/files_sharing/lib/Config/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class ConfigLexicon implements ILexicon {
public const SHOW_FEDERATED_AS_INTERNAL = 'show_federated_shares_as_internal';
public const SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL = 'show_federated_shares_to_trusted_servers_as_internal';
public const EXCLUDE_RESHARE_FROM_EDIT = 'shareapi_exclude_reshare_from_edit';
public const UPDATE_SINGLE_CUTOFF = 'update_single_cutoff';
public const UPDATE_ALL_CUTOFF = 'update_all_cutoff';
public const USER_NEEDS_SHARE_REFRESH = 'user_needs_share_refresh';

public function getStrictness(): Strictness {
return Strictness::IGNORE;
Expand All @@ -34,10 +37,15 @@ public function getAppConfigs(): array {
new Entry(self::SHOW_FEDERATED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares as internal shares', true),
new Entry(self::SHOW_FEDERATED_TO_TRUSTED_AS_INTERNAL, ValueType::BOOL, false, 'shows federated shares to trusted servers as internal shares', true),
new Entry(self::EXCLUDE_RESHARE_FROM_EDIT, ValueType::BOOL, false, 'Exclude reshare permission from "Allow editing" bundled permissions'),

new Entry(self::UPDATE_SINGLE_CUTOFF, ValueType::INT, 10, 'For how many users do we update the share date immediately for single-share updates'),
Copy link
Contributor

Choose a reason for hiding this comment

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

Date or data?

Suggested change
new Entry(self::UPDATE_SINGLE_CUTOFF, ValueType::INT, 10, 'For how many users do we update the share date immediately for single-share updates'),
new Entry(self::UPDATE_SINGLE_CUTOFF, ValueType::INT, 10, 'For how many users do we update the share data immediately for single-share updates'),

new Entry(self::UPDATE_ALL_CUTOFF, ValueType::INT, 3, 'For how many users do we update the share date immediately for all-share updates'),
];
}

public function getUserConfigs(): array {
return [];
return [
new Entry(self::USER_NEEDS_SHARE_REFRESH, ValueType::BOOL, false, 'whether a user needs to have the receiving share data refreshed for possible changes'),
];
}
}
102 changes: 55 additions & 47 deletions apps/files_sharing/lib/Listener/SharesUpdatedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@

namespace OCA\Files_Sharing\Listener;

use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Sharing\Config\ConfigLexicon;
use OCA\Files_Sharing\Event\UserShareAccessUpdatedEvent;
use OCA\Files_Sharing\MountProvider;
use OCA\Files_Sharing\ShareTargetValidator;
use OCA\Files_Sharing\ShareRecipientUpdater;
use OCP\Config\IUserConfig;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Storage\IStorageFactory;
use OCP\Group\Events\UserAddedEvent;
use OCP\Group\Events\UserRemovedEvent;
use OCP\IAppConfig;
use OCP\IUser;
use OCP\Share\Events\BeforeShareDeletedEvent;
use OCP\Share\Events\ShareCreatedEvent;
Expand All @@ -30,71 +30,79 @@
* @template-implements IEventListener<UserAddedEvent|UserRemovedEvent|ShareCreatedEvent|ShareTransferredEvent|BeforeShareDeletedEvent|UserShareAccessUpdatedEvent>
*/
class SharesUpdatedListener implements IEventListener {
private array $inUpdate = [];
/**
* for how many users do we update the share date immediately,
* before just marking the other users when we know the relevant share
*/
private int $cutOffMarkAllSingleShare;
/**
* for how many users do we update the share date immediately,
* before just marking the other users when the relevant shares are unknown
*/
private int $cutOffMarkAllShares ;

private int $updatedCount = 0;

public function __construct(
private readonly IManager $shareManager,
private readonly IUserMountCache $userMountCache,
private readonly MountProvider $shareMountProvider,
private readonly ShareTargetValidator $shareTargetValidator,
private readonly IStorageFactory $storageFactory,
private readonly ShareRecipientUpdater $shareUpdater,
private readonly IUserConfig $userConfig,
IAppConfig $appConfig,
) {
$this->cutOffMarkAllSingleShare = $appConfig->getValueInt(Application::APP_ID, ConfigLexicon::UPDATE_SINGLE_CUTOFF, 10);
$this->cutOffMarkAllShares = $appConfig->getValueInt(Application::APP_ID, ConfigLexicon::UPDATE_ALL_CUTOFF, 3);
}

public function handle(Event $event): void {
if ($event instanceof UserShareAccessUpdatedEvent) {
foreach ($event->getUsers() as $user) {
$this->updateForUser($user, true);
$this->updateOrMarkUser($user, true);
}
}
if ($event instanceof UserAddedEvent || $event instanceof UserRemovedEvent) {
$this->updateForUser($event->getUser(), true);
$this->updateOrMarkUser($event->getUser(), true);
}
if (
$event instanceof ShareCreatedEvent
|| $event instanceof ShareTransferredEvent
) {
foreach ($this->shareManager->getUsersForShare($event->getShare()) as $user) {
$this->updateForUser($user, true);
if ($event instanceof ShareCreatedEvent || $event instanceof ShareTransferredEvent) {
$share = $event->getShare();
$shareTarget = $share->getTarget();
foreach ($this->shareManager->getUsersForShare($share) as $user) {
if ($share->getSharedBy() !== $user->getUID()) {
$this->updatedCount++;
if ($this->cutOffMarkAllSingleShare === -1 || $this->updatedCount <= $this->cutOffMarkAllSingleShare) {
$this->shareUpdater->updateForShare($user, $share);
// Share target validation might have changed the target, restore it for the next user
$share->setTarget($shareTarget);
} else {
$this->markUserForRefresh($user);
}
Comment on lines +71 to +77
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure that a user count make sense. I would rather base it on a time limit.

}
}
}
if ($event instanceof BeforeShareDeletedEvent) {
foreach ($this->shareManager->getUsersForShare($event->getShare()) as $user) {
$this->updateForUser($user, false, [$event->getShare()]);
$this->updateOrMarkUser($user, false, [$event->getShare()]);
}
}
}

private function updateForUser(IUser $user, bool $verifyMountPoints, array $ignoreShares = []): void {
// prevent recursion
if (isset($this->inUpdate[$user->getUID()])) {
return;
private function updateOrMarkUser(IUser $user, bool $verifyMountPoints, array $ignoreShares = []): void {
$this->updatedCount++;
if ($this->cutOffMarkAllShares === -1 || $this->updatedCount <= $this->cutOffMarkAllShares) {
$this->shareUpdater->updateForUser($user, $verifyMountPoints, $ignoreShares);
} else {
$this->markUserForRefresh($user);
}
$this->inUpdate[$user->getUID()] = true;
$cachedMounts = $this->userMountCache->getMountsForUser($user);
$shareMounts = array_filter($cachedMounts, fn (ICachedMountInfo $mount) => $mount->getMountProvider() === MountProvider::class);
$mountPoints = array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts);
$mountsByPath = array_combine($mountPoints, $cachedMounts);

$shares = $this->shareMountProvider->getSuperSharesForUser($user, $ignoreShares);
}

$mountsChanged = count($shares) !== count($shareMounts);
foreach ($shares as &$share) {
[$parentShare, $groupedShares] = $share;
$mountPoint = '/' . $user->getUID() . '/files/' . trim($parentShare->getTarget(), '/') . '/';
$mountKey = $parentShare->getNodeId() . '::' . $mountPoint;
if (!isset($cachedMounts[$mountKey])) {
$mountsChanged = true;
if ($verifyMountPoints) {
$this->shareTargetValidator->verifyMountPoint($user, $parentShare, $mountsByPath, $groupedShares);
}
}
}
private function markUserForRefresh(IUser $user): void {
$this->userConfig->setValueBool($user->getUID(), Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, true);
}

if ($mountsChanged) {
$newMounts = $this->shareMountProvider->getMountsFromSuperShares($user, $shares, $this->storageFactory);
$this->userMountCache->registerMounts($user, $newMounts, [MountProvider::class]);
}
public function setCutOffMarkAllSingleShare(int $cutOffMarkAllSingleShare): void {
$this->cutOffMarkAllSingleShare = $cutOffMarkAllSingleShare;
}

unset($this->inUpdate[$user->getUID()]);
public function setCutOffMarkAllShares(int $cutOffMarkAllShares): void {
$this->cutOffMarkAllShares = $cutOffMarkAllShares;
}
}
44 changes: 44 additions & 0 deletions apps/files_sharing/lib/Listener/UserHomeSetupListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files_Sharing\Listener;

use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Sharing\Config\ConfigLexicon;
use OCA\Files_Sharing\ShareRecipientUpdater;
use OCP\Config\IUserConfig;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\UserHomeSetupEvent;

/**
* Listen to the users filesystem setup being started, to perform any receiving share
* work that was postponed.
*
* @template-implements IEventListener<UserHomeSetupEvent>
*/
class UserHomeSetupListener implements IEventListener {
public function __construct(
private readonly ShareRecipientUpdater $updater,
private readonly IUserConfig $userConfig,
) {
}

public function handle(Event $event): void {
if (!$event instanceof UserHomeSetupEvent) {
return;
}

$user = $event->getUser();
if ($this->userConfig->getValueBool($user->getUID(), Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH)) {
$this->updater->updateForUser($user);
$this->userConfig->setValueBool($user->getUID(), Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, false);
}
}

}
79 changes: 79 additions & 0 deletions apps/files_sharing/lib/ShareRecipientUpdater.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Files_Sharing;

use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Storage\IStorageFactory;
use OCP\IUser;
use OCP\Share\IShare;

class ShareRecipientUpdater {
private array $inUpdate = [];

public function __construct(
private readonly IUserMountCache $userMountCache,
private readonly MountProvider $shareMountProvider,
private readonly ShareTargetValidator $shareTargetValidator,
private readonly IStorageFactory $storageFactory,
) {
}

/**
* Validate all received shares for a user
*/
public function updateForUser(IUser $user, bool $verifyMountPoints = true, array $ignoreShares = []): void {
// prevent recursion
if (isset($this->inUpdate[$user->getUID()])) {
return;
}
$this->inUpdate[$user->getUID()] = true;

$cachedMounts = $this->userMountCache->getMountsForUser($user);
$shareMounts = array_filter($cachedMounts, fn (ICachedMountInfo $mount) => $mount->getMountProvider() === MountProvider::class);
$mountPoints = array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts);
$mountsByPath = array_combine($mountPoints, $cachedMounts);

$shares = $this->shareMountProvider->getSuperSharesForUser($user, $ignoreShares);

$mountsChanged = count($shares) !== count($shareMounts);
Copy link
Member

Choose a reason for hiding this comment

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

What does this mean? Should it be set to default, given that the loop might set it to true?

foreach ($shares as &$share) {
[$parentShare, $groupedShares] = $share;
$mountPoint = '/' . $user->getUID() . '/files/' . trim($parentShare->getTarget(), '/') . '/';
$mountKey = $parentShare->getNodeId() . '::' . $mountPoint;
if (!isset($cachedMounts[$mountKey])) {
$mountsChanged = true;
if ($verifyMountPoints) {
$this->shareTargetValidator->verifyMountPoint($user, $parentShare, $mountsByPath, $groupedShares);
}
}
}

if ($mountsChanged) {
$newMounts = $this->shareMountProvider->getMountsFromSuperShares($user, $shares, $this->storageFactory);
$this->userMountCache->registerMounts($user, $newMounts, [MountProvider::class]);
}

unset($this->inUpdate[$user->getUID()]);
}

/**
* Validate a single received share for a user
*/
public function updateForShare(IUser $user, IShare $share): void {
$cachedMounts = $this->userMountCache->getMountsForUser($user);
$mountPoints = array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts);
$mountsByPath = array_combine($mountPoints, $cachedMounts);

$target = $this->shareTargetValidator->verifyMountPoint($user, $share, $mountsByPath, [$share]);
$mountPoint = '/' . $user->getUID() . '/files/' . trim($target, '/') . '/';

$this->userMountCache->addMount($user, $mountPoint, $share->getNode()->getData(), MountProvider::class);
}
}
4 changes: 4 additions & 0 deletions apps/files_sharing/tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use OC\User\DisplayNameCache;
use OCA\Files_Sharing\AppInfo\Application;
use OCA\Files_Sharing\External\MountProvider as ExternalMountProvider;
use OCA\Files_Sharing\Listener\SharesUpdatedListener;
use OCA\Files_Sharing\MountProvider;
use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\IRootFolder;
Expand Down Expand Up @@ -99,6 +100,9 @@ public static function setUpBeforeClass(): void {
$groupBackend->addToGroup(self::TEST_FILES_SHARING_API_USER4, 'group3');
$groupBackend->addToGroup(self::TEST_FILES_SHARING_API_USER2, self::TEST_FILES_SHARING_API_GROUP1);
Server::get(IGroupManager::class)->addBackend($groupBackend);

Server::get(SharesUpdatedListener::class)->setCutOffMarkAllShares(-1);
Server::get(SharesUpdatedListener::class)->setCutOffMarkAllSingleShare(-1);
}

protected function setUp(): void {
Expand Down
5 changes: 5 additions & 0 deletions apps/files_trashbin/lib/Trash/TrashItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
namespace OCA\Files_Trashbin\Trash;

use OCP\Files\Cache\ICacheEntry;
use OCP\Files\FileInfo;
use OCP\IUser;

Expand Down Expand Up @@ -169,4 +170,8 @@ public function getDeletedBy(): ?IUser {
public function getMetadata(): array {
return $this->fileInfo->getMetadata();
}

public function getData(): ICacheEntry {
return $this->fileInfo->getData();
}
}
2 changes: 2 additions & 0 deletions build/integration/features/bootstrap/SharingContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ protected function resetAppConfigs() {
$this->deleteServerConfig('core', 'shareapi_allow_federation_on_public_shares');
$this->deleteServerConfig('files_sharing', 'outgoing_server2server_share_enabled');
$this->deleteServerConfig('core', 'shareapi_allow_view_without_download');
$this->deleteServerConfig('files_sharing', 'update_single_cutoff');
$this->deleteServerConfig('files_sharing', 'update_all_cutoff');

$this->runOcc(['config:system:delete', 'share_folder']);
}
Expand Down
Loading
Loading