diff options
Diffstat (limited to 'apps/files_sharing/lib/Command')
-rw-r--r-- | apps/files_sharing/lib/Command/CleanupRemoteStorages.php | 63 | ||||
-rw-r--r-- | apps/files_sharing/lib/Command/DeleteOrphanShares.php | 79 | ||||
-rw-r--r-- | apps/files_sharing/lib/Command/ExiprationNotification.php | 53 | ||||
-rw-r--r-- | apps/files_sharing/lib/Command/FixShareOwners.php | 65 | ||||
-rw-r--r-- | apps/files_sharing/lib/Command/ListShares.php | 161 |
5 files changed, 338 insertions, 83 deletions
diff --git a/apps/files_sharing/lib/Command/CleanupRemoteStorages.php b/apps/files_sharing/lib/Command/CleanupRemoteStorages.php index 3816a2a5124..809481e5c0f 100644 --- a/apps/files_sharing/lib/Command/CleanupRemoteStorages.php +++ b/apps/files_sharing/lib/Command/CleanupRemoteStorages.php @@ -1,27 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Calviño Sánchez <danxuliu@gmail.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * 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, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud GmbH. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Sharing\Command; @@ -39,19 +21,10 @@ use Symfony\Component\Console\Output\OutputInterface; */ class CleanupRemoteStorages extends Command { - /** - * @var IDBConnection - */ - protected $connection; - - /** - * @var ICloudIdManager - */ - private $cloudIdManager; - - public function __construct(IDBConnection $connection, ICloudIdManager $cloudIdManager) { - $this->connection = $connection; - $this->cloudIdManager = $cloudIdManager; + public function __construct( + protected IDBConnection $connection, + private ICloudIdManager $cloudIdManager, + ) { parent::__construct(); } @@ -113,8 +86,9 @@ class CleanupRemoteStorages extends Command { $queryBuilder->createNamedParameter($numericId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR) ); - $result = $queryBuilder->execute(); + $result = $queryBuilder->executeQuery(); $count = $result->fetchOne(); + $result->closeCursor(); $output->writeln("$count files can be deleted for storage $numericId"); } @@ -127,7 +101,7 @@ class CleanupRemoteStorages extends Command { IQueryBuilder::PARAM_STR) ); $output->write("deleting $id [$numericId] ... "); - $count = $queryBuilder->execute(); + $count = $queryBuilder->executeStatement(); $output->writeln("deleted $count storage"); $this->deleteFiles($numericId, $output); } @@ -141,7 +115,7 @@ class CleanupRemoteStorages extends Command { IQueryBuilder::PARAM_STR) ); $output->write("deleting files for storage $numericId ... "); - $count = $queryBuilder->execute(); + $count = $queryBuilder->executeStatement(); $output->writeln("deleted $count files"); } @@ -160,14 +134,16 @@ class CleanupRemoteStorages extends Command { // but not the ones starting with a '/', they are for normal shares $queryBuilder->createNamedParameter($this->connection->escapeLikeParameter('shared::/') . '%'), IQueryBuilder::PARAM_STR) - )->orderBy('numeric_id'); - $query = $queryBuilder->execute(); + ) + ->orderBy('numeric_id'); + $result = $queryBuilder->executeQuery(); $remoteStorages = []; - while ($row = $query->fetch()) { + while ($row = $result->fetch()) { $remoteStorages[$row['id']] = $row['numeric_id']; } + $result->closeCursor(); return $remoteStorages; } @@ -176,16 +152,17 @@ class CleanupRemoteStorages extends Command { $queryBuilder = $this->connection->getQueryBuilder(); $queryBuilder->select(['id', 'share_token', 'owner', 'remote']) ->from('share_external'); - $query = $queryBuilder->execute(); + $result = $queryBuilder->executeQuery(); $remoteShareIds = []; - while ($row = $query->fetch()) { + while ($row = $result->fetch()) { $cloudId = $this->cloudIdManager->getCloudId($row['owner'], $row['remote']); $remote = $cloudId->getRemote(); $remoteShareIds[$row['id']] = 'shared::' . md5($row['share_token'] . '@' . $remote); } + $result->closeCursor(); return $remoteShareIds; } diff --git a/apps/files_sharing/lib/Command/DeleteOrphanShares.php b/apps/files_sharing/lib/Command/DeleteOrphanShares.php new file mode 100644 index 00000000000..a7e96387d60 --- /dev/null +++ b/apps/files_sharing/lib/Command/DeleteOrphanShares.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Command; + +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; +use Symfony\Component\Console\Question\ConfirmationQuestion; + +class DeleteOrphanShares extends Base { + public function __construct( + private OrphanHelper $orphanHelper, + ) { + parent::__construct(); + } + + 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("<info>{$share['target']}</info> owned by <info>{$share['owner']}</info>"); + if ($exists) { + $output->writeln(" file still exists but the share owner lost access to it, run <info>occ info:file {$share['fileid']}</info> 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 <info>$count</info> 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/Command/ExiprationNotification.php b/apps/files_sharing/lib/Command/ExiprationNotification.php index e77b41b1835..b7ea5c5f14e 100644 --- a/apps/files_sharing/lib/Command/ExiprationNotification.php +++ b/apps/files_sharing/lib/Command/ExiprationNotification.php @@ -3,29 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Joas Schilling <coding@schilljs.com> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @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 <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Sharing\Command; +use OCA\Files_Sharing\OrphanHelper; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IDBConnection; use OCP\Notification\IManager as NotificationManager; @@ -36,25 +19,14 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ExiprationNotification extends Command { - /** @var NotificationManager */ - private $notificationManager; - /** @var IDBConnection */ - private $connection; - /** @var ITimeFactory */ - private $time; - /** @var ShareManager */ - private $shareManager; - - public function __construct(ITimeFactory $time, - NotificationManager $notificationManager, - IDBConnection $connection, - ShareManager $shareManager) { + public function __construct( + private ITimeFactory $time, + private NotificationManager $notificationManager, + private IDBConnection $connection, + private ShareManager $shareManager, + private OrphanHelper $orphanHelper, + ) { parent::__construct(); - - $this->notificationManager = $notificationManager; - $this->connection = $connection; - $this->time = $time; - $this->shareManager = $shareManager; } protected function configure() { @@ -67,7 +39,7 @@ class ExiprationNotification extends Command { //Current time $minTime = $this->time->getDateTime(); $minTime->add(new \DateInterval('P1D')); - $minTime->setTime(0,0,0); + $minTime->setTime(0, 0, 0); $maxTime = clone $minTime; $maxTime->setTime(23, 59, 59); @@ -80,7 +52,8 @@ class ExiprationNotification extends Command { foreach ($shares as $share) { if ($share->getExpirationDate() === null || $share->getExpirationDate()->getTimestamp() < $minTime->getTimestamp() - || $share->getExpirationDate()->getTimestamp() > $maxTime->getTimestamp()) { + || $share->getExpirationDate()->getTimestamp() > $maxTime->getTimestamp() + || !$this->orphanHelper->isShareValid($share->getSharedBy(), $share->getNodeId())) { continue; } diff --git a/apps/files_sharing/lib/Command/FixShareOwners.php b/apps/files_sharing/lib/Command/FixShareOwners.php new file mode 100644 index 00000000000..1cf5f82f5a8 --- /dev/null +++ b/apps/files_sharing/lib/Command/FixShareOwners.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Command; + +use OC\Core\Command\Base; +use OCA\Files_Sharing\OrphanHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class FixShareOwners extends Base { + public function __construct( + private readonly OrphanHelper $orphanHelper, + ) { + parent::__construct(); + } + + protected function configure(): void { + $this + ->setName('sharing:fix-share-owners') + ->setDescription('Fix owner of broken shares after transfer ownership on old versions') + ->addOption( + 'dry-run', + null, + InputOption::VALUE_NONE, + 'only show which shares would be updated' + ); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + $shares = $this->orphanHelper->getAllShares(); + $dryRun = $input->getOption('dry-run'); + $count = 0; + + foreach ($shares as $share) { + if ($this->orphanHelper->isShareValid($share['owner'], $share['fileid']) || !$this->orphanHelper->fileExists($share['fileid'])) { + continue; + } + + $owner = $this->orphanHelper->findOwner($share['fileid']); + + if ($owner !== null) { + if ($dryRun) { + $output->writeln("Share with id <info>{$share['id']}</info> (target: <info>{$share['target']}</info>) can be updated to owner <info>$owner</info>"); + } else { + $this->orphanHelper->updateShareOwner($share['id'], $owner); + $output->writeln("Share with id <info>{$share['id']}</info> (target: <info>{$share['target']}</info>) updated to owner <info>$owner</info>"); + } + $count++; + } + } + + if ($count === 0) { + $output->writeln('No broken shares detected'); + } + + return static::SUCCESS; + } +} diff --git a/apps/files_sharing/lib/Command/ListShares.php b/apps/files_sharing/lib/Command/ListShares.php new file mode 100644 index 00000000000..2d5cdbf7812 --- /dev/null +++ b/apps/files_sharing/lib/Command/ListShares.php @@ -0,0 +1,161 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files_Sharing\Command; + +use OC\Core\Command\Base; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\Share\IManager; +use OCP\Share\IShare; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +class ListShares extends Base { + /** @var array<string, Node> */ + private array $fileCache = []; + + private const SHARE_TYPE_NAMES = [ + IShare::TYPE_USER => 'user', + IShare::TYPE_GROUP => 'group', + IShare::TYPE_LINK => 'link', + IShare::TYPE_EMAIL => 'email', + IShare::TYPE_REMOTE => 'remote', + IShare::TYPE_REMOTE_GROUP => 'group', + IShare::TYPE_ROOM => 'room', + IShare::TYPE_DECK => 'deck', + ]; + + public function __construct( + private readonly IManager $shareManager, + private readonly IRootFolder $rootFolder, + ) { + parent::__construct(); + } + + protected function configure() { + parent::configure(); + $this + ->setName('share:list') + ->setDescription('List available shares') + ->addOption('owner', null, InputOption::VALUE_REQUIRED, 'only show shares owned by a specific user') + ->addOption('recipient', null, InputOption::VALUE_REQUIRED, 'only show shares with a specific recipient') + ->addOption('by', null, InputOption::VALUE_REQUIRED, 'only show shares with by as specific user') + ->addOption('file', null, InputOption::VALUE_REQUIRED, 'only show shares of a specific file') + ->addOption('parent', null, InputOption::VALUE_REQUIRED, 'only show shares of files inside a specific folder') + ->addOption('recursive', null, InputOption::VALUE_NONE, 'also show shares nested deep inside the specified parent folder') + ->addOption('type', null, InputOption::VALUE_REQUIRED, 'only show shares of a specific type') + ->addOption('status', null, InputOption::VALUE_REQUIRED, 'only show shares with a specific status'); + } + + public function execute(InputInterface $input, OutputInterface $output): int { + if ($input->getOption('recursive') && !$input->getOption('parent')) { + $output->writeln("<error>recursive option can't be used without parent option</error>"); + return 1; + } + + // todo: do some pre-filtering instead of first querying all shares + /** @var \Iterator<IShare> $allShares */ + $allShares = $this->shareManager->getAllShares(); + $shares = new \CallbackFilterIterator($allShares, function (IShare $share) use ($input) { + return $this->shouldShowShare($input, $share); + }); + $shares = iterator_to_array($shares); + $data = array_map(function (IShare $share) { + return [ + 'id' => $share->getId(), + 'file' => $share->getNodeId(), + 'target-path' => $share->getTarget(), + 'source-path' => $share->getNode()->getPath(), + 'owner' => $share->getShareOwner(), + 'recipient' => $share->getSharedWith(), + 'by' => $share->getSharedBy(), + 'type' => self::SHARE_TYPE_NAMES[$share->getShareType()] ?? 'unknown', + ]; + }, $shares); + + $this->writeTableInOutputFormat($input, $output, $data); + return 0; + } + + private function getFileId(string $file): int { + if (is_numeric($file)) { + return (int)$file; + } + return $this->getFile($file)->getId(); + } + + private function getFile(string $file): Node { + if (isset($this->fileCache[$file])) { + return $this->fileCache[$file]; + } + + if (is_numeric($file)) { + $node = $this->rootFolder->getFirstNodeById((int)$file); + if (!$node) { + throw new NotFoundException("File with id $file not found"); + } + } else { + $node = $this->rootFolder->get($file); + } + $this->fileCache[$file] = $node; + return $node; + } + + private function getShareType(string $type): int { + foreach (self::SHARE_TYPE_NAMES as $shareType => $shareTypeName) { + if ($shareTypeName === $type) { + return $shareType; + } + } + throw new \Exception("Unknown share type $type"); + } + + private function shouldShowShare(InputInterface $input, IShare $share): bool { + if ($input->getOption('owner') && $share->getShareOwner() !== $input->getOption('owner')) { + return false; + } + if ($input->getOption('recipient') && $share->getSharedWith() !== $input->getOption('recipient')) { + return false; + } + if ($input->getOption('by') && $share->getSharedBy() !== $input->getOption('by')) { + return false; + } + if ($input->getOption('file') && $share->getNodeId() !== $this->getFileId($input->getOption('file'))) { + return false; + } + if ($input->getOption('parent')) { + $parent = $this->getFile($input->getOption('parent')); + if (!$parent instanceof Folder) { + throw new \Exception("Parent {$parent->getPath()} is not a folder"); + } + $recursive = $input->getOption('recursive'); + if (!$recursive) { + $shareCacheEntry = $share->getNodeCacheEntry(); + if (!$shareCacheEntry) { + $shareCacheEntry = $share->getNode(); + } + if ($shareCacheEntry->getParentId() !== $parent->getId()) { + return false; + } + } else { + $shareNode = $share->getNode(); + if ($parent->getRelativePath($shareNode->getPath()) === null) { + return false; + } + } + } + if ($input->getOption('type') && $share->getShareType() !== $this->getShareType($input->getOption('type'))) { + return false; + } + return true; + } +} |