diff --git a/apps/files/lib/Command/Mount/ListMounts.php b/apps/files/lib/Command/Mount/ListMounts.php
index b4abeac5ab8af..487e769ad2c94 100644
--- a/apps/files/lib/Command/Mount/ListMounts.php
+++ b/apps/files/lib/Command/Mount/ListMounts.php
@@ -8,17 +8,18 @@
namespace OCA\Files\Command\Mount;
+use OC\Core\Command\Base;
use OCP\Files\Config\ICachedMountInfo;
use OCP\Files\Config\IMountProviderCollection;
use OCP\Files\Config\IUserMountCache;
use OCP\Files\Mount\IMountPoint;
use OCP\IUserManager;
-use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-class ListMounts extends Command {
+class ListMounts extends Base {
public function __construct(
private readonly IUserManager $userManager,
private readonly IUserMountCache $userMountCache,
@@ -28,52 +29,81 @@ public function __construct(
}
protected function configure(): void {
+ parent::configure();
$this
->setName('files:mount:list')
->setDescription('List of mounts for a user')
- ->addArgument('user', InputArgument::REQUIRED, 'User to list mounts for');
+ ->addArgument('user', InputArgument::REQUIRED, 'User to list mounts for')
+ ->addOption('cached-only', null, InputOption::VALUE_NONE, 'Only return cached mounts, prevents filesystem setup');
}
public function execute(InputInterface $input, OutputInterface $output): int {
$userId = $input->getArgument('user');
+ $cachedOnly = $input->getOption('cached-only');
$user = $this->userManager->get($userId);
if (!$user) {
$output->writeln("User $userId not found");
return 1;
}
- $mounts = $this->mountProviderCollection->getMountsForUser($user);
- $mounts[] = $this->mountProviderCollection->getHomeMountForUser($user);
- /** @var array $cachedByMountpoint */
- $mountsByMountpoint = array_combine(array_map(fn (IMountPoint $mount) => $mount->getMountPoint(), $mounts), $mounts);
+ if ($cachedOnly) {
+ $mounts = [];
+ } else {
+ $mounts = $this->mountProviderCollection->getMountsForUser($user);
+ $mounts[] = $this->mountProviderCollection->getHomeMountForUser($user);
+ }
+ /** @var array $cachedByMountPoint */
+ $mountsByMountPoint = array_combine(array_map(fn (IMountPoint $mount) => $mount->getMountPoint(), $mounts), $mounts);
usort($mounts, fn (IMountPoint $a, IMountPoint $b) => $a->getMountPoint() <=> $b->getMountPoint());
$cachedMounts = $this->userMountCache->getMountsForUser($user);
usort($cachedMounts, fn (ICachedMountInfo $a, ICachedMountInfo $b) => $a->getMountPoint() <=> $b->getMountPoint());
/** @var array $cachedByMountpoint */
- $cachedByMountpoint = array_combine(array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts), $cachedMounts);
+ $cachedByMountPoint = array_combine(array_map(fn (ICachedMountInfo $mount) => $mount->getMountPoint(), $cachedMounts), $cachedMounts);
+
+ $format = $input->getOption('output');
- foreach ($mounts as $mount) {
- $output->writeln('' . $mount->getMountPoint() . ': ' . $mount->getStorageId());
- if (isset($cachedByMountpoint[$mount->getMountPoint()])) {
- $cached = $cachedByMountpoint[$mount->getMountPoint()];
- $output->writeln("\t- provider: " . $cached->getMountProvider());
- $output->writeln("\t- storage id: " . $cached->getStorageId());
- $output->writeln("\t- root id: " . $cached->getRootId());
- } else {
- $output->writeln("\tnot registered");
+ if ($format === self::OUTPUT_FORMAT_PLAIN) {
+ foreach ($mounts as $mount) {
+ $output->writeln('' . $mount->getMountPoint() . ': ' . $mount->getStorageId());
+ if (isset($cachedByMountPoint[$mount->getMountPoint()])) {
+ $cached = $cachedByMountPoint[$mount->getMountPoint()];
+ $output->writeln("\t- provider: " . $cached->getMountProvider());
+ $output->writeln("\t- storage id: " . $cached->getStorageId());
+ $output->writeln("\t- root id: " . $cached->getRootId());
+ } else {
+ $output->writeln("\tnot registered");
+ }
}
- }
- foreach ($cachedMounts as $cachedMount) {
- if (!isset($mountsByMountpoint[$cachedMount->getMountPoint()])) {
- $output->writeln('' . $cachedMount->getMountPoint() . ':');
- $output->writeln("\tregistered but no longer provided");
- $output->writeln("\t- provider: " . $cachedMount->getMountProvider());
- $output->writeln("\t- storage id: " . $cachedMount->getStorageId());
- $output->writeln("\t- root id: " . $cachedMount->getRootId());
+ foreach ($cachedMounts as $cachedMount) {
+ if ($cachedOnly || !isset($mountsByMountPoint[$cachedMount->getMountPoint()])) {
+ $output->writeln('' . $cachedMount->getMountPoint() . ':');
+ if (!$cachedOnly) {
+ $output->writeln("\tregistered but no longer provided");
+ }
+ $output->writeln("\t- provider: " . $cachedMount->getMountProvider());
+ $output->writeln("\t- storage id: " . $cachedMount->getStorageId());
+ $output->writeln("\t- root id: " . $cachedMount->getRootId());
+ }
}
+ } else {
+ $cached = array_map(fn (ICachedMountInfo $cachedMountInfo) => [
+ 'mountpoint' => $cachedMountInfo->getMountPoint(),
+ 'provider' => $cachedMountInfo->getMountProvider(),
+ 'storage_id' => $cachedMountInfo->getStorageId(),
+ 'root_id' => $cachedMountInfo->getRootId(),
+ ], $cachedMounts);
+ $provided = array_map(fn (IMountPoint $cachedMountInfo) => [
+ 'mountpoint' => $cachedMountInfo->getMountPoint(),
+ 'provider' => $cachedMountInfo->getMountProvider(),
+ 'storage_id' => $cachedMountInfo->getStorageId(),
+ 'root_id' => $cachedMountInfo->getStorageRootId(),
+ ], $mounts);
+ $this->writeArrayInOutputFormat($input, $output, array_filter([
+ 'cached' => $cached,
+ 'provided' => $cachedOnly ? null : $provided,
+ ]));
}
-
return 0;
}
diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php
index 0ba6ba1b8167f..138746aad8f83 100644
--- a/apps/files_sharing/composer/composer/autoload_classmap.php
+++ b/apps/files_sharing/composer/composer/autoload_classmap.php
@@ -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',
@@ -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',
diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php
index 03906cda0473b..3decf0b9c1acc 100644
--- a/apps/files_sharing/composer/composer/autoload_static.php
+++ b/apps/files_sharing/composer/composer/autoload_static.php
@@ -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',
@@ -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',
diff --git a/apps/files_sharing/lib/AppInfo/Application.php b/apps/files_sharing/lib/AppInfo/Application.php
index da4984d378533..370d8fe8fa7c7 100644
--- a/apps/files_sharing/lib/AppInfo/Application.php
+++ b/apps/files_sharing/lib/AppInfo/Application.php
@@ -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;
@@ -48,6 +49,8 @@
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\BeforeGroupDeletedEvent;
use OCP\Group\Events\GroupChangedEvent;
use OCP\Group\Events\GroupDeletedEvent;
use OCP\Group\Events\UserAddedEvent;
@@ -120,7 +123,10 @@ function () use ($c) {
$context->registerEventListener(ShareTransferredEvent::class, SharesUpdatedListener::class);
$context->registerEventListener(UserAddedEvent::class, SharesUpdatedListener::class);
$context->registerEventListener(UserRemovedEvent::class, SharesUpdatedListener::class);
+ $context->registerEventListener(BeforeGroupDeletedEvent::class, SharesUpdatedListener::class);
+ $context->registerEventListener(GroupDeletedEvent::class, SharesUpdatedListener::class);
$context->registerEventListener(UserShareAccessUpdatedEvent::class, SharesUpdatedListener::class);
+ $context->registerEventListener(UserHomeSetupEvent::class, UserHomeSetupListener::class);
$context->registerConfigLexicon(ConfigLexicon::class);
}
diff --git a/apps/files_sharing/lib/Config/ConfigLexicon.php b/apps/files_sharing/lib/Config/ConfigLexicon.php
index c063153765e26..623d1340f2617 100644
--- a/apps/files_sharing/lib/Config/ConfigLexicon.php
+++ b/apps/files_sharing/lib/Config/ConfigLexicon.php
@@ -24,6 +24,8 @@ 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_CUTOFF_TIME = 'update_cutoff_time';
+ public const USER_NEEDS_SHARE_REFRESH = 'user_needs_share_refresh';
public function getStrictness(): Strictness {
return Strictness::IGNORE;
@@ -34,10 +36,14 @@ 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_CUTOFF_TIME, ValueType::FLOAT, 3.0, 'For how how long do we update the share data immediately before switching to only marking the user'),
];
}
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'),
+ ];
}
}
diff --git a/apps/files_sharing/lib/Listener/SharesUpdatedListener.php b/apps/files_sharing/lib/Listener/SharesUpdatedListener.php
index ccef71bad5c87..e17e392c906d7 100644
--- a/apps/files_sharing/lib/Listener/SharesUpdatedListener.php
+++ b/apps/files_sharing/lib/Listener/SharesUpdatedListener.php
@@ -8,93 +8,117 @@
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\BeforeGroupDeletedEvent;
+use OCP\Group\Events\GroupDeletedEvent;
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;
use OCP\Share\Events\ShareTransferredEvent;
use OCP\Share\IManager;
+use Psr\Clock\ClockInterface;
/**
* Listen to various events that can change what shares a user has access to
*
- * @template-implements IEventListener
+ * @psalm-type GroupEvents = UserAddedEvent|UserRemovedEvent|GroupDeletedEvent|BeforeGroupDeletedEvent
+ * @template-implements IEventListener
*/
class SharesUpdatedListener implements IEventListener {
- private array $inUpdate = [];
+ /**
+ * for how long do we update the share date immediately,
+ * before just marking the other users
+ */
+ private float $cutOffMarkTime;
+
+ /**
+ * The total amount of time we've spent so far processing updates
+ */
+ private float $updatedTime = 0.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,
+ private readonly ClockInterface $clock,
+ IAppConfig $appConfig,
) {
+ $this->cutOffMarkTime = $appConfig->getValueFloat(Application::APP_ID, ConfigLexicon::UPDATE_CUTOFF_TIME, 3.0);
}
+
public function handle(Event $event): void {
if ($event instanceof UserShareAccessUpdatedEvent) {
foreach ($event->getUsers() as $user) {
- $this->updateForUser($user, true);
+ $this->updateOrMarkUser($user);
+ }
+ }
+ if ($event instanceof BeforeGroupDeletedEvent) {
+ // ensure the group users are loaded before the group is deleted
+ $event->getGroup()->getUsers();
+ }
+ if ($event instanceof GroupDeletedEvent) {
+ // so we can iterate them after the group is deleted
+ foreach ($event->getGroup()->getUsers() as $user) {
+ $this->updateOrMarkUser($user);
}
}
if ($event instanceof UserAddedEvent || $event instanceof UserRemovedEvent) {
- $this->updateForUser($event->getUser(), true);
+ $this->updateOrMarkUser($event->getUser());
}
- 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->markOrRun($user, function () use ($user, $share) {
+ $this->shareUpdater->updateForAddedShare($user, $share);
+ });
+ // Share target validation might have changed the target, restore it for the next user
+ $share->setTarget($shareTarget);
+ }
}
}
if ($event instanceof BeforeShareDeletedEvent) {
- foreach ($this->shareManager->getUsersForShare($event->getShare()) as $user) {
- $this->updateForUser($user, false, [$event->getShare()]);
+ $share = $event->getShare();
+ foreach ($this->shareManager->getUsersForShare($share) as $user) {
+ $this->markOrRun($user, function () use ($user, $share) {
+ $this->shareUpdater->updateForDeletedShare($user, $share);
+ });
}
}
}
- private function updateForUser(IUser $user, bool $verifyMountPoints, array $ignoreShares = []): void {
- // prevent recursion
- if (isset($this->inUpdate[$user->getUID()])) {
- return;
+ private function markOrRun(IUser $user, callable $callback): void {
+ $start = floatval($this->clock->now()->format('U.u'));
+ if ($this->cutOffMarkTime === -1.0 || $this->updatedTime < $this->cutOffMarkTime) {
+ $callback();
+ } 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);
+ $end = floatval($this->clock->now()->format('U.u'));
+ $this->updatedTime += $end - $start;
+ }
- $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 updateOrMarkUser(IUser $user): void {
+ $this->markOrRun($user, function () use ($user) {
+ $this->shareUpdater->updateForUser($user);
+ });
+ }
- if ($mountsChanged) {
- $newMounts = $this->shareMountProvider->getMountsFromSuperShares($user, $shares, $this->storageFactory);
- $this->userMountCache->registerMounts($user, $newMounts, [MountProvider::class]);
- }
+ private function markUserForRefresh(IUser $user): void {
+ $this->userConfig->setValueBool($user->getUID(), Application::APP_ID, ConfigLexicon::USER_NEEDS_SHARE_REFRESH, true);
+ }
- unset($this->inUpdate[$user->getUID()]);
+ public function setCutOffMarkTime(float|int $cutOffMarkTime): void {
+ $this->cutOffMarkTime = (float)$cutOffMarkTime;
}
}
diff --git a/apps/files_sharing/lib/Listener/UserHomeSetupListener.php b/apps/files_sharing/lib/Listener/UserHomeSetupListener.php
new file mode 100644
index 0000000000000..8886660879fa9
--- /dev/null
+++ b/apps/files_sharing/lib/Listener/UserHomeSetupListener.php
@@ -0,0 +1,44 @@
+
+ */
+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);
+ }
+ }
+
+}
diff --git a/apps/files_sharing/lib/ShareRecipientUpdater.php b/apps/files_sharing/lib/ShareRecipientUpdater.php
new file mode 100644
index 0000000000000..83cf681344cab
--- /dev/null
+++ b/apps/files_sharing/lib/ShareRecipientUpdater.php
@@ -0,0 +1,88 @@
+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);
+
+ // the share mounts have changed if either the number of shares doesn't matched the number of share mounts
+ // or there is a share for which we don't have a mount yet.
+ $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;
+ $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 updateForAddedShare(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);
+ }
+
+ /**
+ * Process a single deleted share for a user
+ */
+ public function updateForDeletedShare(IUser $user, IShare $share): void {
+ $mountPoint = '/' . $user->getUID() . '/files/' . trim($share->getTarget(), '/') . '/';
+
+ $this->userMountCache->removeMount($mountPoint);
+ }
+}
diff --git a/apps/files_sharing/tests/ShareRecipientUpdaterTest.php b/apps/files_sharing/tests/ShareRecipientUpdaterTest.php
new file mode 100644
index 0000000000000..2316e6b8b7e56
--- /dev/null
+++ b/apps/files_sharing/tests/ShareRecipientUpdaterTest.php
@@ -0,0 +1,206 @@
+userMountCache = $this->createMock(IUserMountCache::class);
+ $this->shareMountProvider = $this->createMock(MountProvider::class);
+ $this->shareTargetValidator = $this->createMock(ShareTargetValidator::class);
+ $this->storageFactory = $this->createMock(IStorageFactory::class);
+
+ $this->updater = new ShareRecipientUpdater(
+ $this->userMountCache,
+ $this->shareMountProvider,
+ $this->shareTargetValidator,
+ $this->storageFactory,
+ );
+ }
+
+ public function testUpdateForShare() {
+ $share = $this->createMock(IShare::class);
+ $node = $this->createMock(Node::class);
+ $cacheEntry = $this->createMock(ICacheEntry::class);
+ $share->method('getNode')
+ ->willReturn($node);
+ $node->method('getData')
+ ->willReturn($cacheEntry);
+ $user1 = $this->createUser('user1', '');
+
+ $this->userMountCache->method('getMountsForUser')
+ ->with($user1)
+ ->willReturn([]);
+
+ $this->shareTargetValidator->method('verifyMountPoint')
+ ->with($user1, $share, [], [$share])
+ ->willReturn('/new-target');
+
+ $this->userMountCache->expects($this->exactly(1))
+ ->method('addMount')
+ ->with($user1, '/user1/files/new-target/', $cacheEntry, MountProvider::class);
+
+ $this->updater->updateForAddedShare($user1, $share);
+ }
+
+ /**
+ * @param IUser $user
+ * @param list $mounts
+ * @return void
+ */
+ private function setCachedMounts(IUser $user, array $mounts) {
+ $cachedMounts = array_map(function (array $mount): ICachedMountInfo {
+ $cachedMount = $this->createMock(ICachedMountInfo::class);
+ $cachedMount->method('getRootId')
+ ->willReturn($mount['fileid']);
+ $cachedMount->method('getMountPoint')
+ ->willReturn($mount['mount_point']);
+ $cachedMount->method('getMountProvider')
+ ->willReturn($mount['provider']);
+ return $cachedMount;
+ }, $mounts);
+ $mountKeys = array_map(function (array $mount): string {
+ return $mount['fileid'] . '::' . $mount['mount_point'];
+ }, $mounts);
+
+ $this->userMountCache->method('getMountsForUser')
+ ->with($user)
+ ->willReturn(array_combine($mountKeys, $cachedMounts));
+ }
+
+ public function testUpdateForUserAddedNoExisting() {
+ $share = $this->createMock(IShare::class);
+ $share->method('getTarget')
+ ->willReturn('/target');
+ $share->method('getNodeId')
+ ->willReturn(111);
+ $user1 = $this->createUser('user1', '');
+ $newMount = $this->createMock(IMountPoint::class);
+
+ $this->shareMountProvider->method('getSuperSharesForUser')
+ ->with($user1, [])
+ ->willReturn([[
+ $share,
+ [$share],
+ ]]);
+
+ $this->shareMountProvider->method('getMountsFromSuperShares')
+ ->with($user1, [[
+ $share,
+ [$share],
+ ]], $this->storageFactory)
+ ->willReturn([$newMount]);
+
+ $this->setCachedMounts($user1, []);
+
+ $this->shareTargetValidator->method('verifyMountPoint')
+ ->with($user1, $share, [], [$share])
+ ->willReturn('/new-target');
+
+ $this->userMountCache->expects($this->exactly(1))
+ ->method('registerMounts')
+ ->with($user1, [$newMount], [MountProvider::class]);
+
+ $this->updater->updateForUser($user1);
+ }
+
+ public function testUpdateForUserNoChanges() {
+ $share = $this->createMock(IShare::class);
+ $share->method('getTarget')
+ ->willReturn('/target');
+ $share->method('getNodeId')
+ ->willReturn(111);
+ $user1 = $this->createUser('user1', '');
+
+ $this->shareMountProvider->method('getSuperSharesForUser')
+ ->with($user1, [])
+ ->willReturn([[
+ $share,
+ [$share],
+ ]]);
+
+ $this->setCachedMounts($user1, [
+ ['fileid' => 111, 'mount_point' => '/user1/files/target/', 'provider' => MountProvider::class],
+ ]);
+
+ $this->shareTargetValidator->expects($this->never())
+ ->method('verifyMountPoint');
+
+ $this->userMountCache->expects($this->never())
+ ->method('registerMounts');
+
+ $this->updater->updateForUser($user1);
+ }
+
+ public function testUpdateForUserRemoved() {
+ $share = $this->createMock(IShare::class);
+ $share->method('getTarget')
+ ->willReturn('/target');
+ $share->method('getNodeId')
+ ->willReturn(111);
+ $user1 = $this->createUser('user1', '');
+
+ $this->shareMountProvider->method('getSuperSharesForUser')
+ ->with($user1, [])
+ ->willReturn([]);
+
+ $this->setCachedMounts($user1, [
+ ['fileid' => 111, 'mount_point' => '/user1/files/target/', 'provider' => MountProvider::class],
+ ]);
+
+ $this->shareTargetValidator->expects($this->never())
+ ->method('verifyMountPoint');
+
+ $this->userMountCache->expects($this->exactly(1))
+ ->method('registerMounts')
+ ->with($user1, [], [MountProvider::class]);
+
+ $this->updater->updateForUser($user1);
+ }
+
+ public function testDeletedShare() {
+ $share = $this->createMock(IShare::class);
+ $share->method('getTarget')
+ ->willReturn('/target');
+ $share->method('getNodeId')
+ ->willReturn(111);
+ $user1 = $this->createUser('user1', '');
+
+ $this->shareTargetValidator->expects($this->never())
+ ->method('verifyMountPoint');
+
+ $this->userMountCache->expects($this->exactly(1))
+ ->method('removeMount')
+ ->with('/user1/files/target/');
+
+ $this->updater->updateForDeletedShare($user1, $share);
+ }
+}
diff --git a/apps/files_sharing/tests/SharesUpdatedListenerTest.php b/apps/files_sharing/tests/SharesUpdatedListenerTest.php
new file mode 100644
index 0000000000000..a6ec4ace499bf
--- /dev/null
+++ b/apps/files_sharing/tests/SharesUpdatedListenerTest.php
@@ -0,0 +1,183 @@
+shareRecipientUpdater = $this->createMock(ShareRecipientUpdater::class);
+ $this->manager = $this->createMock(IManager::class);
+ $this->appConfig = new MockAppConfig([
+ ConfigLexicon::UPDATE_CUTOFF_TIME => -1,
+ ]);
+ $this->userConfig = new MockUserConfig();
+ $this->clock = $this->createMock(ClockInterface::class);
+ $this->clockFn = function () {
+ return new \DateTimeImmutable('@0');
+ };
+ $this->clock->method('now')
+ ->willReturnCallback(function () {
+ // extra wrapper so we can modify clockFn
+ return ($this->clockFn)();
+ });
+ $this->sharesUpdatedListener = new SharesUpdatedListener(
+ $this->manager,
+ $this->shareRecipientUpdater,
+ $this->userConfig,
+ $this->clock,
+ $this->appConfig,
+ );
+ }
+
+ public function testShareAdded() {
+ $share = $this->createMock(IShare::class);
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+
+ $this->manager->method('getUsersForShare')
+ ->willReturn([$user1, $user2]);
+
+ $event = new ShareCreatedEvent($share);
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly(2))
+ ->method('updateForAddedShare')
+ ->willReturnCallback(function (IUser $user, IShare $eventShare) use ($user1, $user2, $share) {
+ $this->assertContains($user, [$user1, $user2]);
+ $this->assertEquals($share, $eventShare);
+ });
+
+ $this->sharesUpdatedListener->handle($event);
+ }
+
+ public function testShareAddedFilterOwner() {
+ $share = $this->createMock(IShare::class);
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+ $share->method('getSharedBy')
+ ->willReturn($user1->getUID());
+
+ $this->manager->method('getUsersForShare')
+ ->willReturn([$user1, $user2]);
+
+ $event = new ShareCreatedEvent($share);
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly(1))
+ ->method('updateForAddedShare')
+ ->willReturnCallback(function (IUser $user, IShare $eventShare) use ($user2, $share) {
+ $this->assertEquals($user, $user2);
+ $this->assertEquals($share, $eventShare);
+ });
+
+ $this->sharesUpdatedListener->handle($event);
+ }
+
+ public function testShareAccessUpdated() {
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+
+ $event = new UserShareAccessUpdatedEvent([$user1, $user2]);
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly(2))
+ ->method('updateForUser')
+ ->willReturnCallback(function (IUser $user) use ($user1, $user2) {
+ $this->assertContains($user, [$user1, $user2]);
+ });
+
+ $this->sharesUpdatedListener->handle($event);
+ }
+
+ public function testShareDeleted() {
+ $share = $this->createMock(IShare::class);
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+
+ $this->manager->method('getUsersForShare')
+ ->willReturn([$user1, $user2]);
+
+ $event = new BeforeShareDeletedEvent($share);
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly(2))
+ ->method('updateForDeletedShare')
+ ->willReturnCallback(function (IUser $user) use ($user1, $user2, $share) {
+ $this->assertContains($user, [$user1, $user2]);
+ });
+
+ $this->sharesUpdatedListener->handle($event);
+ }
+
+ public static function shareMarkAfterTimeProvider(): array {
+ // note that each user will take exactly 1s in this test
+ return [
+ [0, 0],
+ [0.9, 1],
+ [1.1, 2],
+ [-1, 2],
+ ];
+ }
+
+ #[DataProvider('shareMarkAfterTimeProvider')]
+ public function testShareMarkAfterTime(float $cutOff, int $expectedCount) {
+ $share = $this->createMock(IShare::class);
+ $user1 = $this->createUser('user1', '');
+ $user2 = $this->createUser('user2', '');
+
+ $this->manager->method('getUsersForShare')
+ ->willReturn([$user1, $user2]);
+
+ $event = new ShareCreatedEvent($share);
+
+ $this->sharesUpdatedListener->setCutOffMarkTime($cutOff);
+ $time = 0;
+ $this->clockFn = function () use (&$time) {
+ $time++;
+ return new \DateTimeImmutable('@' . $time);
+ };
+
+ $this->shareRecipientUpdater
+ ->expects($this->exactly($expectedCount))
+ ->method('updateForAddedShare');
+
+ $this->sharesUpdatedListener->handle($event);
+
+ $this->assertEquals($expectedCount < 1, $this->userConfig->getValueBool($user1->getUID(), 'files_sharing', ConfigLexicon::USER_NEEDS_SHARE_REFRESH));
+ $this->assertEquals($expectedCount < 2, $this->userConfig->getValueBool($user2->getUID(), 'files_sharing', ConfigLexicon::USER_NEEDS_SHARE_REFRESH));
+ }
+}
diff --git a/apps/files_sharing/tests/TestCase.php b/apps/files_sharing/tests/TestCase.php
index 6b72ecb259cab..02ee66d096118 100644
--- a/apps/files_sharing/tests/TestCase.php
+++ b/apps/files_sharing/tests/TestCase.php
@@ -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;
@@ -99,6 +100,8 @@ 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)->setCutOffMarkTime(-1);
}
protected function setUp(): void {
diff --git a/apps/files_trashbin/lib/Trash/TrashItem.php b/apps/files_trashbin/lib/Trash/TrashItem.php
index 70d5164747f0b..2864a8cd942f4 100644
--- a/apps/files_trashbin/lib/Trash/TrashItem.php
+++ b/apps/files_trashbin/lib/Trash/TrashItem.php
@@ -6,6 +6,7 @@
*/
namespace OCA\Files_Trashbin\Trash;
+use OCP\Files\Cache\ICacheEntry;
use OCP\Files\FileInfo;
use OCP\IUser;
@@ -169,4 +170,8 @@ public function getDeletedBy(): ?IUser {
public function getMetadata(): array {
return $this->fileInfo->getMetadata();
}
+
+ public function getData(): ICacheEntry {
+ return $this->fileInfo->getData();
+ }
}
diff --git a/build/integration/features/bootstrap/Sharing.php b/build/integration/features/bootstrap/Sharing.php
index 72f6902af167e..5fe98aff35d1e 100644
--- a/build/integration/features/bootstrap/Sharing.php
+++ b/build/integration/features/bootstrap/Sharing.php
@@ -5,6 +5,7 @@
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+
use Behat\Gherkin\Node\TableNode;
use GuzzleHttp\Client;
use PHPUnit\Framework\Assert;
@@ -13,7 +14,6 @@
require __DIR__ . '/autoload.php';
-
trait Sharing {
use Provisioning;
@@ -566,17 +566,17 @@ public function shareXIsReturnedWith(int $number, TableNode $body) {
$expectedFields = array_merge($defaultExpectedFields, $body->getRowsHash());
if (!array_key_exists('uid_file_owner', $expectedFields)
- && array_key_exists('uid_owner', $expectedFields)) {
+ && array_key_exists('uid_owner', $expectedFields)) {
$expectedFields['uid_file_owner'] = $expectedFields['uid_owner'];
}
if (!array_key_exists('displayname_file_owner', $expectedFields)
- && array_key_exists('displayname_owner', $expectedFields)) {
+ && array_key_exists('displayname_owner', $expectedFields)) {
$expectedFields['displayname_file_owner'] = $expectedFields['displayname_owner'];
}
if (array_key_exists('share_type', $expectedFields)
- && $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */
- && array_key_exists('share_with', $expectedFields)) {
+ && $expectedFields['share_type'] == 10 /* IShare::TYPE_ROOM */
+ && array_key_exists('share_with', $expectedFields)) {
if ($expectedFields['share_with'] === 'private_conversation') {
$expectedFields['share_with'] = 'REGEXP /^private_conversation_[0-9a-f]{6}$/';
} else {
@@ -782,4 +782,34 @@ public function getArrayOfShareesResponded(ResponseInterface $response, $shareeT
}
return $sharees;
}
+
+ /**
+ * @Then /^Share mounts for "([^"]*)" match$/
+ */
+ public function checkShareMounts(string $user, ?TableNode $body) {
+ if ($body instanceof TableNode) {
+ $fd = $body->getRows();
+
+ $expected = [];
+ foreach ($fd as $row) {
+ $expected[] = $row[0];
+ }
+ $this->runOcc(['files:mount:list', '--output', 'json', '--cached-only', $user]);
+ $mounts = json_decode($this->lastStdOut, true)['cached'];
+ $shareMounts = array_filter($mounts, fn (array $data) => $data['provider'] === \OCA\Files_Sharing\MountProvider::class);
+ $actual = array_values(array_map(fn (array $data) => $data['mountpoint'], $shareMounts));
+ Assert::assertEquals($expected, $actual);
+ }
+ }
+
+ /**
+ * @Then /^Share mounts for "([^"]*)" are empty$/
+ */
+ public function checkShareMountsEmpty(string $user) {
+ $this->runOcc(['files:mount:list', '--output', 'json', '--cached-only', $user]);
+ $mounts = json_decode($this->lastStdOut, true)['cached'];
+ $shareMounts = array_filter($mounts, fn (array $data) => $data['provider'] === \OCA\Files_Sharing\MountProvider::class);
+ $actual = array_values(array_map(fn (array $data) => $data['mountpoint'], $shareMounts));
+ Assert::assertEquals([], $actual);
+ }
}
diff --git a/build/integration/features/bootstrap/SharingContext.php b/build/integration/features/bootstrap/SharingContext.php
index c442317a32a38..9f70382438467 100644
--- a/build/integration/features/bootstrap/SharingContext.php
+++ b/build/integration/features/bootstrap/SharingContext.php
@@ -32,6 +32,7 @@ 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_cutoff_time');
$this->runOcc(['config:system:delete', 'share_folder']);
}
diff --git a/build/integration/features/bootstrap/WebDav.php b/build/integration/features/bootstrap/WebDav.php
index fb552ce785b75..fb2e441d93791 100644
--- a/build/integration/features/bootstrap/WebDav.php
+++ b/build/integration/features/bootstrap/WebDav.php
@@ -1011,7 +1011,7 @@ public function checkIfETAGHasChanged($path, $user) {
*/
public function connectingToDavEndpoint() {
try {
- $this->response = $this->makeDavRequest(null, 'PROPFIND', '', []);
+ $this->response = $this->makeDavRequest($this->currentUser, 'PROPFIND', '', []);
} catch (\GuzzleHttp\Exception\ClientException $e) {
$this->response = $e->getResponse();
}
diff --git a/build/integration/sharing_features/sharing-v1-part2.feature b/build/integration/sharing_features/sharing-v1-part2.feature
index 0c83975fc39b5..36ddcba92d29a 100644
--- a/build/integration/sharing_features/sharing-v1-part2.feature
+++ b/build/integration/sharing_features/sharing-v1-part2.feature
@@ -47,6 +47,49 @@ Feature: sharing
| share_with | user2 |
| share_with_displayname | user2 |
+Scenario: getting all shares of a file with a received share after revoking the resharing rights with delayed share check
+ Given user "user0" exists
+ And parameter "update_cutoff_time" of app "files_sharing" is set to "0"
+ And user "user1" exists
+ And user "user2" exists
+ And file "textfile0.txt" of user "user1" is shared with user "user0"
+ And user "user0" accepts last share
+ And Updating last share with
+ | permissions | 1 |
+ And file "textfile0.txt" of user "user1" is shared with user "user2"
+ When As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=/textfile0 (2).txt"
+ Then the list of returned shares has 1 shares
+ And share 0 is returned with
+ | share_type | 0 |
+ | uid_owner | user1 |
+ | displayname_owner | user1 |
+ | path | /textfile0 (2).txt |
+ | item_type | file |
+ | mimetype | text/plain |
+ | storage_id | shared::/textfile0 (2).txt |
+ | file_target | /textfile0.txt |
+ | share_with | user2 |
+ | share_with_displayname | user2 |
+ # After user2 does an FS setup the share is renamed
+ When As an "user2"
+ And Downloading file "/textfile0 (2).txt" with range "bytes=10-18"
+ Then Downloaded content should be "test text"
+ When As an "user0"
+ And sending "GET" to "/apps/files_sharing/api/v1/shares?reshares=true&path=/textfile0 (2).txt"
+ Then the list of returned shares has 1 shares
+ And share 0 is returned with
+ | share_type | 0 |
+ | uid_owner | user1 |
+ | displayname_owner | user1 |
+ | path | /textfile0 (2).txt |
+ | item_type | file |
+ | mimetype | text/plain |
+ | storage_id | shared::/textfile0 (2).txt |
+ | file_target | /textfile0 (2).txt |
+ | share_with | user2 |
+ | share_with_displayname | user2 |
+
Scenario: getting all shares of a file with a received share also reshared after revoking the resharing rights
Given user "user0" exists
And user "user1" exists
diff --git a/build/integration/sharing_features/sharing-v1-part4.feature b/build/integration/sharing_features/sharing-v1-part4.feature
index 3b825aebd1813..746ac93d6a36d 100644
--- a/build/integration/sharing_features/sharing-v1-part4.feature
+++ b/build/integration/sharing_features/sharing-v1-part4.feature
@@ -315,3 +315,102 @@ Scenario: Can copy file between shares if share permissions
And the OCS status code should be "100"
When User "user1" copies file "/share/test.txt" to "/re-share/movetest.txt"
Then the HTTP status code should be "201"
+
+Scenario: Group deletes removes mount without marking
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And group "group0" exists
+ And user "user0" belongs to group "group0"
+ And file "textfile0.txt" of user "user1" is shared with group "group0"
+ And As an "user0"
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ And group "group0" does not exist
+ Then Share mounts for "user0" are empty
+
+Scenario: Group deletes removes mount with marking
+ Given As an "admin"
+ And parameter "update_cutoff_time" of app "files_sharing" is set to "0"
+ And user "user0" exists
+ And user "user1" exists
+ And group "group0" exists
+ And user "user0" belongs to group "group0"
+ And file "textfile0.txt" of user "user1" is shared with group "group0"
+ And As an "user0"
+ Then Share mounts for "user0" are empty
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ And group "group0" does not exist
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" are empty
+
+Scenario: User share mount without marking
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And file "textfile0.txt" of user "user1" is shared with user "user0"
+ And As an "user0"
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Deleting last share
+ Then Share mounts for "user0" are empty
+
+Scenario: User share mount with marking
+ Given As an "admin"
+ And parameter "update_cutoff_time" of app "files_sharing" is set to "0"
+ And user "user0" exists
+ And user "user1" exists
+ And file "textfile0.txt" of user "user1" is shared with user "user0"
+ And As an "user0"
+ Then Share mounts for "user0" are empty
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Deleting last share
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" are empty
+
+Scenario: User added/removed to group share without marking
+ Given As an "admin"
+ And user "user0" exists
+ And user "user1" exists
+ And group "group0" exists
+ And file "textfile0.txt" of user "user1" is shared with group "group0"
+ And As an "user0"
+ Then Share mounts for "user0" are empty
+ When user "user0" belongs to group "group0"
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When As an "admin"
+ Then sending "DELETE" to "/cloud/users/user0/groups" with
+ | groupid | group0 |
+ Then As an "user0"
+ And Share mounts for "user0" are empty
+
+Scenario: User added/removed to group share with marking
+ Given As an "admin"
+ And parameter "update_cutoff_time" of app "files_sharing" is set to "0"
+ And user "user0" exists
+ And user "user1" exists
+ And group "group0" exists
+ And file "textfile0.txt" of user "user1" is shared with group "group0"
+ And As an "user0"
+ When user "user0" belongs to group "group0"
+ Then Share mounts for "user0" are empty
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When As an "admin"
+ Then sending "DELETE" to "/cloud/users/user0/groups" with
+ | groupid | group0 |
+ Then As an "user0"
+ And Share mounts for "user0" match
+ | /user0/files/textfile0 (2).txt/ |
+ When Connecting to dav endpoint
+ Then Share mounts for "user0" are empty
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 5b6de5ff356d1..1ad36ab1679f0 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -471,6 +471,7 @@
'OCP\\Files\\Events\\Node\\NodeRenamedEvent' => $baseDir . '/lib/public/Files/Events/Node/NodeRenamedEvent.php',
'OCP\\Files\\Events\\Node\\NodeTouchedEvent' => $baseDir . '/lib/public/Files/Events/Node/NodeTouchedEvent.php',
'OCP\\Files\\Events\\Node\\NodeWrittenEvent' => $baseDir . '/lib/public/Files/Events/Node/NodeWrittenEvent.php',
+ 'OCP\\Files\\Events\\UserHomeSetupEvent' => $baseDir . '/lib/public/Files/Events/UserHomeSetupEvent.php',
'OCP\\Files\\File' => $baseDir . '/lib/public/Files/File.php',
'OCP\\Files\\FileInfo' => $baseDir . '/lib/public/Files/FileInfo.php',
'OCP\\Files\\FileNameTooLongException' => $baseDir . '/lib/public/Files/FileNameTooLongException.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index fbee07dafc6b4..7ffedacb39312 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -512,6 +512,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Files\\Events\\Node\\NodeRenamedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/Node/NodeRenamedEvent.php',
'OCP\\Files\\Events\\Node\\NodeTouchedEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/Node/NodeTouchedEvent.php',
'OCP\\Files\\Events\\Node\\NodeWrittenEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/Node/NodeWrittenEvent.php',
+ 'OCP\\Files\\Events\\UserHomeSetupEvent' => __DIR__ . '/../../..' . '/lib/public/Files/Events/UserHomeSetupEvent.php',
'OCP\\Files\\File' => __DIR__ . '/../../..' . '/lib/public/Files/File.php',
'OCP\\Files\\FileInfo' => __DIR__ . '/../../..' . '/lib/public/Files/FileInfo.php',
'OCP\\Files\\FileNameTooLongException' => __DIR__ . '/../../..' . '/lib/public/Files/FileNameTooLongException.php',
diff --git a/lib/private/Files/Config/UserMountCache.php b/lib/private/Files/Config/UserMountCache.php
index ebdfc64d83ece..5233cebc5da5a 100644
--- a/lib/private/Files/Config/UserMountCache.php
+++ b/lib/private/Files/Config/UserMountCache.php
@@ -536,6 +536,12 @@ public function removeMount(string $mountPoint): void {
$query->delete('mounts')
->where($query->expr()->eq('mount_point', $query->createNamedParameter($mountPoint)));
$query->executeStatement();
+
+ $parts = explode('/', $mountPoint);
+ if (count($parts) > 3) {
+ [, $userId] = $parts;
+ unset($this->mountsForUsers[$userId]);
+ }
}
public function addMount(IUser $user, string $mountPoint, ICacheEntry $rootCacheEntry, string $mountProvider, ?int $mountId = null): void {
@@ -553,6 +559,7 @@ public function addMount(IUser $user, string $mountPoint, ICacheEntry $rootCache
try {
$query->executeStatement();
+ unset($this->mountsForUsers[$user->getUID()]);
} catch (DbalException $e) {
if ($e->getReason() !== DbalException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
throw $e;
diff --git a/lib/private/Files/FileInfo.php b/lib/private/Files/FileInfo.php
index 967d404b8a4f0..cc01c8f6c79fc 100644
--- a/lib/private/Files/FileInfo.php
+++ b/lib/private/Files/FileInfo.php
@@ -7,8 +7,8 @@
*/
namespace OC\Files;
+use OC\Files\Cache\CacheEntry;
use OC\Files\Mount\HomeMountPoint;
-use OCA\Files_Sharing\External\Mount;
use OCA\Files_Sharing\ISharedMountPoint;
use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Mount\IMountPoint;
@@ -223,8 +223,12 @@ public function getType() {
return $this->data['type'];
}
- public function getData() {
- return $this->data;
+ public function getData(): ICacheEntry {
+ if ($this->data instanceof ICacheEntry) {
+ return $this->data;
+ } else {
+ return new CacheEntry($this->data);
+ }
}
/**
diff --git a/lib/private/Files/Node/LazyFolder.php b/lib/private/Files/Node/LazyFolder.php
index d04c8aefb7e06..0e11855fc32c0 100644
--- a/lib/private/Files/Node/LazyFolder.php
+++ b/lib/private/Files/Node/LazyFolder.php
@@ -10,6 +10,7 @@
use OC\Files\Filesystem;
use OC\Files\Utils\PathHelper;
use OCP\Constants;
+use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Mount\IMountPoint;
@@ -563,6 +564,10 @@ public function getMetadata(): array {
return $this->data['metadata'] ?? $this->__call(__FUNCTION__, func_get_args());
}
+ public function getData(): ICacheEntry {
+ return $this->__call(__FUNCTION__, func_get_args());
+ }
+
public function verifyPath($fileName, $readonly = false): void {
$this->__call(__FUNCTION__, func_get_args());
}
diff --git a/lib/private/Files/Node/Node.php b/lib/private/Files/Node/Node.php
index fd8d84883d964..7a7867e6a4e7a 100644
--- a/lib/private/Files/Node/Node.php
+++ b/lib/private/Files/Node/Node.php
@@ -12,6 +12,7 @@
use OC\Files\Utils\PathHelper;
use OCP\EventDispatcher\GenericEvent;
use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Cache\ICacheEntry;
use OCP\Files\FileInfo;
use OCP\Files\InvalidPathException;
use OCP\Files\IRootFolder;
@@ -486,4 +487,8 @@ public function getParentId(): int {
public function getMetadata(): array {
return $this->fileInfo->getMetadata();
}
+
+ public function getData(): ICacheEntry {
+ return $this->fileInfo->getData();
+ }
}
diff --git a/lib/private/Files/SetupManager.php b/lib/private/Files/SetupManager.php
index e058be0663b93..af2c05387048f 100644
--- a/lib/private/Files/SetupManager.php
+++ b/lib/private/Files/SetupManager.php
@@ -43,6 +43,7 @@
use OCP\Files\Events\InvalidateMountCacheEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\Events\Node\FilesystemTornDownEvent;
+use OCP\Files\Events\UserHomeSetupEvent;
use OCP\Files\ISetupManager;
use OCP\Files\Mount\IMountManager;
use OCP\Files\Mount\IMountPoint;
@@ -334,6 +335,9 @@ private function oneTimeUserSetup(IUser $user) {
$this->eventLogger->end('fs:setup:user:home:scan');
}
$this->eventLogger->end('fs:setup:user:home');
+
+ $event = new UserHomeSetupEvent($user, $homeMount);
+ $this->eventDispatcher->dispatchTyped($event);
} else {
$this->mountManager->addMount(new MountPoint(
new NullStorage([]),
diff --git a/lib/public/Files/Events/UserHomeSetupEvent.php b/lib/public/Files/Events/UserHomeSetupEvent.php
new file mode 100644
index 0000000000000..2b49f64a28b92
--- /dev/null
+++ b/lib/public/Files/Events/UserHomeSetupEvent.php
@@ -0,0 +1,46 @@
+user;
+ }
+
+ /**
+ * @since 34.0.0
+ */
+ public function getHomeMount(): IMountPoint {
+ return $this->homeMount;
+ }
+}
diff --git a/lib/public/Files/FileInfo.php b/lib/public/Files/FileInfo.php
index 95419d6354ac4..117408f23bc63 100644
--- a/lib/public/Files/FileInfo.php
+++ b/lib/public/Files/FileInfo.php
@@ -8,6 +8,7 @@
namespace OCP\Files;
use OCP\AppFramework\Attribute\Consumable;
+use OCP\Files\Cache\ICacheEntry;
use OCP\Files\Storage\IStorage;
/**
@@ -298,4 +299,12 @@ public function getParentId(): int;
* @since 28.0.0
*/
public function getMetadata(): array;
+
+ /**
+ * Get the filecache data for the file
+ *
+ * @return ICacheEntry
+ * @since 34.0.0
+ */
+ public function getData(): ICacheEntry;
}
diff --git a/tests/lib/Mock/Config/MockAppConfig.php b/tests/lib/Mock/Config/MockAppConfig.php
new file mode 100644
index 0000000000000..f601bf6dbc570
--- /dev/null
+++ b/tests/lib/Mock/Config/MockAppConfig.php
@@ -0,0 +1,169 @@
+config[$app][$key]);
+ }
+
+ public function getValues($app, $key): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getFilteredValues($app): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getApps(): array {
+ return array_keys($this->config);
+ }
+
+ public function getKeys(string $app): array {
+ return array_keys($this->config[$app] ?? []);
+ }
+
+ public function isSensitive(string $app, string $key, ?bool $lazy = false): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function isLazy(string $app, string $key): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function getAllValues(string $app, string $prefix = '', bool $filtered = false): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchValues(string $key, bool $lazy = false, ?int $typedAs = null): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValueString(string $app, string $key, string $default = '', bool $lazy = false): string {
+ return (string)(($this->config[$app] ?? [])[$key] ?? $default);
+ }
+
+ public function getValueInt(string $app, string $key, int $default = 0, bool $lazy = false): int {
+ return (int)(($this->config[$app] ?? [])[$key] ?? $default);
+ }
+
+ public function getValueFloat(string $app, string $key, float $default = 0, bool $lazy = false): float {
+ return (float)(($this->config[$app] ?? [])[$key] ?? $default);
+ }
+
+ public function getValueBool(string $app, string $key, bool $default = false, bool $lazy = false): bool {
+ return (bool)(($this->config[$app] ?? [])[$key] ?? $default);
+ }
+
+ public function getValueArray(string $app, string $key, array $default = [], bool $lazy = false): array {
+ return ($this->config[$app] ?? [])[$key] ?? $default;
+ }
+
+ public function getValueType(string $app, string $key, ?bool $lazy = null): int {
+ throw new \Exception('not implemented');
+ }
+
+ public function setValueString(string $app, string $key, string $value, bool $lazy = false, bool $sensitive = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueInt(string $app, string $key, int $value, bool $lazy = false, bool $sensitive = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueFloat(string $app, string $key, float $value, bool $lazy = false, bool $sensitive = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueBool(string $app, string $key, bool $value, bool $lazy = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueArray(string $app, string $key, array $value, bool $lazy = false, bool $sensitive = false): bool {
+ $this->config[$app][$key] = $value;
+ return true;
+ }
+
+ public function updateSensitive(string $app, string $key, bool $sensitive): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateLazy(string $app, string $key, bool $lazy): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function getDetails(string $app, string $key): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function convertTypeToInt(string $type): int {
+ return match (strtolower($type)) {
+ 'mixed' => IAppConfig::VALUE_MIXED,
+ 'string' => IAppConfig::VALUE_STRING,
+ 'integer' => IAppConfig::VALUE_INT,
+ 'float' => IAppConfig::VALUE_FLOAT,
+ 'boolean' => IAppConfig::VALUE_BOOL,
+ 'array' => IAppConfig::VALUE_ARRAY,
+ default => throw new AppConfigIncorrectTypeException('Unknown type ' . $type)
+ };
+ }
+
+ public function convertTypeToString(int $type): string {
+ $type &= ~self::VALUE_SENSITIVE;
+
+ return match ($type) {
+ IAppConfig::VALUE_MIXED => 'mixed',
+ IAppConfig::VALUE_STRING => 'string',
+ IAppConfig::VALUE_INT => 'integer',
+ IAppConfig::VALUE_FLOAT => 'float',
+ IAppConfig::VALUE_BOOL => 'boolean',
+ IAppConfig::VALUE_ARRAY => 'array',
+ default => throw new AppConfigIncorrectTypeException('Unknown numeric type ' . $type)
+ };
+ }
+
+ public function deleteKey(string $app, string $key): void {
+ if ($this->hasKey($app, $key)) {
+ unset($this->config[$app][$key]);
+ }
+ }
+
+ public function deleteApp(string $app): void {
+ if (isset($this->config[$app])) {
+ unset($this->config[$app]);
+ }
+ }
+
+ public function clearCache(bool $reload = false): void {
+ }
+
+ public function searchKeys(string $app, string $prefix = '', bool $lazy = false): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getKeyDetails(string $app, string $key): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getAppInstalledVersions(bool $onlyEnabled = false): array {
+ throw new \Exception('not implemented');
+ }
+}
diff --git a/tests/lib/Mock/Config/MockUserConfig.php b/tests/lib/Mock/Config/MockUserConfig.php
new file mode 100644
index 0000000000000..cc4619ef191ef
--- /dev/null
+++ b/tests/lib/Mock/Config/MockUserConfig.php
@@ -0,0 +1,209 @@
+config);
+ }
+
+ public function getApps(string $userId): array {
+ return array_keys($this->config[$userId] ?? []);
+ }
+
+ public function getKeys(string $userId, string $app): array {
+ if (isset($this->config[$userId][$app])) {
+ return array_keys($this->config[$userId][$app]);
+ } else {
+ return [];
+ }
+ }
+
+ public function hasKey(string $userId, string $app, string $key, ?bool $lazy = false): bool {
+ return isset($this->config[$userId][$app][$key]);
+ }
+
+ public function isSensitive(string $userId, string $app, string $key, ?bool $lazy = false): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function isIndexed(string $userId, string $app, string $key, ?bool $lazy = false): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function isLazy(string $userId, string $app, string $key): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValues(string $userId, string $app, string $prefix = '', bool $filtered = false): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getAllValues(string $userId, bool $filtered = false): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValuesByApps(string $userId, string $key, bool $lazy = false, ?ValueType $typedAs = null): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValuesByUsers(string $app, string $key, ?ValueType $typedAs = null, ?array $userIds = null): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchUsersByValueString(string $app, string $key, string $value, bool $caseInsensitive = false): Generator {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchUsersByValueInt(string $app, string $key, int $value): Generator {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchUsersByValues(string $app, string $key, array $values): Generator {
+ throw new \Exception('not implemented');
+ }
+
+ public function searchUsersByValueBool(string $app, string $key, bool $value): Generator {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValueString(string $userId, string $app, string $key, string $default = '', bool $lazy = false): string {
+ if (isset($this->config[$userId][$app])) {
+ return (string)$this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueInt(string $userId, string $app, string $key, int $default = 0, bool $lazy = false): int {
+ if (isset($this->config[$userId][$app])) {
+ return (int)$this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueFloat(string $userId, string $app, string $key, float $default = 0, bool $lazy = false): float {
+ if (isset($this->config[$userId][$app])) {
+ return (float)$this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueBool(string $userId, string $app, string $key, bool $default = false, bool $lazy = false): bool {
+ if (isset($this->config[$userId][$app])) {
+ return (bool)$this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueArray(string $userId, string $app, string $key, array $default = [], bool $lazy = false): array {
+ if (isset($this->config[$userId][$app])) {
+ return $this->config[$userId][$app][$key];
+ } else {
+ return $default;
+ }
+ }
+
+ public function getValueType(string $userId, string $app, string $key, ?bool $lazy = null): ValueType {
+ throw new \Exception('not implemented');
+ }
+
+ public function getValueFlags(string $userId, string $app, string $key, bool $lazy = false): int {
+ throw new \Exception('not implemented');
+ }
+
+ public function setValueString(string $userId, string $app, string $key, string $value, bool $lazy = false, int $flags = 0): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueInt(string $userId, string $app, string $key, int $value, bool $lazy = false, int $flags = 0): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueFloat(string $userId, string $app, string $key, float $value, bool $lazy = false, int $flags = 0): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueBool(string $userId, string $app, string $key, bool $value, bool $lazy = false): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function setValueArray(string $userId, string $app, string $key, array $value, bool $lazy = false, int $flags = 0): bool {
+ $this->config[$userId][$app][$key] = $value;
+ return true;
+ }
+
+ public function updateSensitive(string $userId, string $app, string $key, bool $sensitive): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateGlobalSensitive(string $app, string $key, bool $sensitive): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateIndexed(string $userId, string $app, string $key, bool $indexed): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateGlobalIndexed(string $app, string $key, bool $indexed): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateLazy(string $userId, string $app, string $key, bool $lazy): bool {
+ throw new \Exception('not implemented');
+ }
+
+ public function updateGlobalLazy(string $app, string $key, bool $lazy): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function getDetails(string $userId, string $app, string $key): array {
+ throw new \Exception('not implemented');
+ }
+
+ public function deleteUserConfig(string $userId, string $app, string $key): void {
+ unset($this->config[$userId][$app][$key]);
+ }
+
+ public function deleteKey(string $app, string $key): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function deleteApp(string $app): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function deleteAllUserConfig(string $userId): void {
+ unset($this->config[$userId]);
+ }
+
+ public function clearCache(string $userId, bool $reload = false): void {
+ throw new \Exception('not implemented');
+ }
+
+ public function clearCacheAll(): void {
+ throw new \Exception('not implemented');
+ }
+}