diff --git a/apps/files_sharing/appinfo/info.xml b/apps/files_sharing/appinfo/info.xml index 1ba21de712c3a..fa042cdc58b71 100644 --- a/apps/files_sharing/appinfo/info.xml +++ b/apps/files_sharing/appinfo/info.xml @@ -42,6 +42,7 @@ Turning the feature off removes shared files and folders on the server for all s OCA\Files_Sharing\Command\CleanupRemoteStorages OCA\Files_Sharing\Command\ExiprationNotification + OCA\Files_Sharing\Command\DeleteOrphanShares diff --git a/apps/files_sharing/composer/composer/autoload_classmap.php b/apps/files_sharing/composer/composer/autoload_classmap.php index 50cbfe40d8aae..a82d721b1bd07 100644 --- a/apps/files_sharing/composer/composer/autoload_classmap.php +++ b/apps/files_sharing/composer/composer/autoload_classmap.php @@ -24,6 +24,7 @@ 'OCA\\Files_Sharing\\Capabilities' => $baseDir . '/../lib/Capabilities.php', 'OCA\\Files_Sharing\\Collaboration\\ShareRecipientSorter' => $baseDir . '/../lib/Collaboration/ShareRecipientSorter.php', 'OCA\\Files_Sharing\\Command\\CleanupRemoteStorages' => $baseDir . '/../lib/Command/CleanupRemoteStorages.php', + 'OCA\\Files_Sharing\\Command\\DeleteOrphanShares' => $baseDir . '/../lib/Command/DeleteOrphanShares.php', 'OCA\\Files_Sharing\\Command\\ExiprationNotification' => $baseDir . '/../lib/Command/ExiprationNotification.php', 'OCA\\Files_Sharing\\Controller\\AcceptController' => $baseDir . '/../lib/Controller/AcceptController.php', 'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => $baseDir . '/../lib/Controller/DeletedShareAPIController.php', @@ -74,6 +75,7 @@ 'OCA\\Files_Sharing\\MountProvider' => $baseDir . '/../lib/MountProvider.php', 'OCA\\Files_Sharing\\Notification\\Listener' => $baseDir . '/../lib/Notification/Listener.php', 'OCA\\Files_Sharing\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php', + 'OCA\\Files_Sharing\\OrphanHelper' => $baseDir . '/../lib/OrphanHelper.php', 'OCA\\Files_Sharing\\Scanner' => $baseDir . '/../lib/Scanner.php', 'OCA\\Files_Sharing\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php', 'OCA\\Files_Sharing\\ShareBackend\\File' => $baseDir . '/../lib/ShareBackend/File.php', diff --git a/apps/files_sharing/composer/composer/autoload_static.php b/apps/files_sharing/composer/composer/autoload_static.php index 4ba0fd52421a0..63d0bae899578 100644 --- a/apps/files_sharing/composer/composer/autoload_static.php +++ b/apps/files_sharing/composer/composer/autoload_static.php @@ -39,6 +39,7 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', 'OCA\\Files_Sharing\\Collaboration\\ShareRecipientSorter' => __DIR__ . '/..' . '/../lib/Collaboration/ShareRecipientSorter.php', 'OCA\\Files_Sharing\\Command\\CleanupRemoteStorages' => __DIR__ . '/..' . '/../lib/Command/CleanupRemoteStorages.php', + 'OCA\\Files_Sharing\\Command\\DeleteOrphanShares' => __DIR__ . '/..' . '/../lib/Command/DeleteOrphanShares.php', 'OCA\\Files_Sharing\\Command\\ExiprationNotification' => __DIR__ . '/..' . '/../lib/Command/ExiprationNotification.php', 'OCA\\Files_Sharing\\Controller\\AcceptController' => __DIR__ . '/..' . '/../lib/Controller/AcceptController.php', 'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/DeletedShareAPIController.php', @@ -89,6 +90,7 @@ class ComposerStaticInitFiles_Sharing 'OCA\\Files_Sharing\\MountProvider' => __DIR__ . '/..' . '/../lib/MountProvider.php', 'OCA\\Files_Sharing\\Notification\\Listener' => __DIR__ . '/..' . '/../lib/Notification/Listener.php', 'OCA\\Files_Sharing\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php', + 'OCA\\Files_Sharing\\OrphanHelper' => __DIR__ . '/..' . '/../lib/OrphanHelper.php', 'OCA\\Files_Sharing\\Scanner' => __DIR__ . '/..' . '/../lib/Scanner.php', 'OCA\\Files_Sharing\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php', 'OCA\\Files_Sharing\\ShareBackend\\File' => __DIR__ . '/..' . '/../lib/ShareBackend/File.php', diff --git a/apps/files_sharing/lib/Command/DeleteOrphanShares.php b/apps/files_sharing/lib/Command/DeleteOrphanShares.php new file mode 100644 index 0000000000000..310f27ebfa3e0 --- /dev/null +++ b/apps/files_sharing/lib/Command/DeleteOrphanShares.php @@ -0,0 +1,96 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Files_Sharing\Command; + + +use Symfony\Component\Console\Question\ConfirmationQuestion; +use OC\Core\Command\Base; +use OCA\Files_Sharing\OrphanHelper; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class DeleteOrphanShares extends Base { + private OrphanHelper $orphanHelper; + + public function __construct(OrphanHelper $orphanHelper) { + parent::__construct(); + $this->orphanHelper = $orphanHelper; + } + + protected function configure(): void { + $this + ->setName('sharing:delete-orphan-shares') + ->setDescription('Delete shares where the owner no longer has access to the file') + ->addOption( + 'force', + 'f', + InputOption::VALUE_NONE, + 'delete the shares without asking' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $force = $input->getOption('force'); + $shares = $this->orphanHelper->getAllShares(); + + $orphans = []; + foreach ($shares as $share) { + if (!$this->orphanHelper->isShareValid($share['owner'], $share['fileid'])) { + $orphans[] = $share['id']; + $exists = $this->orphanHelper->fileExists($share['fileid']); + $output->writeln("{$share['target']} owned by {$share['owner']}"); + if ($exists) { + $output->writeln(" file still exists but the share owner lost access to it, run occ info:file {$share['fileid']} for more information about the file"); + } else { + $output->writeln(" file no longer exists"); + } + } + } + + $count = count($orphans); + + if ($count === 0) { + $output->writeln("No orphan shares detected"); + return 0; + } + + if ($force) { + $doDelete = true; + } else { + $output->writeln(""); + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion("Delete $count orphan shares? [y/N] ", false); + $doDelete = $helper->ask($input, $output, $question); + } + + if ($doDelete) { + $this->orphanHelper->deleteShares($orphans); + } + + return 0; + } +} diff --git a/apps/files_sharing/lib/OrphanHelper.php b/apps/files_sharing/lib/OrphanHelper.php new file mode 100644 index 0000000000000..6d15680f882d8 --- /dev/null +++ b/apps/files_sharing/lib/OrphanHelper.php @@ -0,0 +1,86 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Files_Sharing; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\Files\IRootFolder; +use OCP\IDBConnection; + +class OrphanHelper { + private IDBConnection $connection; + private IRootFolder $rootFolder; + + public function __construct( + IDBConnection $connection, + IRootFolder $rootFolder + ) { + $this->connection = $connection; + $this->rootFolder = $rootFolder; + } + + public function isShareValid(string $owner, int $fileId): bool { + $userFolder = $this->rootFolder->getUserFolder($owner); + $nodes = $userFolder->getById($fileId); + return count($nodes) > 0; + } + + /** + * @param int[] $ids + * @return void + */ + public function deleteShares(array $ids): void { + $query = $this->connection->getQueryBuilder(); + $query->delete('share') + ->where($query->expr()->in('id', $query->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY))); + $query->executeStatement(); + } + + public function fileExists(int $fileId): bool { + $query = $this->connection->getQueryBuilder(); + $query->select('fileid') + ->from('filecache') + ->where($query->expr()->eq('fileid', $query->createNamedParameter($fileId, IQueryBuilder::PARAM_INT))); + return $query->executeQuery()->fetchOne() !== false; + } + + /** + * @return \Traversable + */ + public function getAllShares() { + $query = $this->connection->getQueryBuilder(); + $query->select('id', 'file_source', 'uid_owner', 'file_target') + ->from('share') + ->where($query->expr()->eq('item_type', $query->createNamedParameter('file'))) + ->orWhere($query->expr()->eq('item_type', $query->createNamedParameter('folder'))); + $result = $query->executeQuery(); + while ($row = $result->fetch()) { + yield [ + 'id' => (int)$row['id'], + 'owner' => (string)$row['uid_owner'], + 'fileid' => (int)$row['file_source'], + 'target' => (string)$row['file_target'], + ]; + } + } +}