aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_sharing/lib/Command
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_sharing/lib/Command')
-rw-r--r--apps/files_sharing/lib/Command/CleanupRemoteStorages.php63
-rw-r--r--apps/files_sharing/lib/Command/DeleteOrphanShares.php79
-rw-r--r--apps/files_sharing/lib/Command/ExiprationNotification.php53
-rw-r--r--apps/files_sharing/lib/Command/FixShareOwners.php65
-rw-r--r--apps/files_sharing/lib/Command/ListShares.php161
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;
+ }
+}