Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add commands to list shares and set share owner #38489

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/files_sharing/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Turning the feature off removes shared files and folders on the server for all s
<commands>
<command>OCA\Files_Sharing\Command\CleanupRemoteStorages</command>
<command>OCA\Files_Sharing\Command\ExiprationNotification</command>
<command>OCA\Files_Sharing\Command\ListCommand</command>
<command>OCA\Files_Sharing\Command\SetOwner</command>
</commands>

<settings>
Expand Down
2 changes: 2 additions & 0 deletions apps/files_sharing/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
'OCA\\Files_Sharing\\Collaboration\\ShareRecipientSorter' => $baseDir . '/../lib/Collaboration/ShareRecipientSorter.php',
'OCA\\Files_Sharing\\Command\\CleanupRemoteStorages' => $baseDir . '/../lib/Command/CleanupRemoteStorages.php',
'OCA\\Files_Sharing\\Command\\ExiprationNotification' => $baseDir . '/../lib/Command/ExiprationNotification.php',
'OCA\\Files_Sharing\\Command\\ListCommand' => $baseDir . '/../lib/Command/ListCommand.php',
'OCA\\Files_Sharing\\Command\\SetOwner' => $baseDir . '/../lib/Command/SetOwner.php',
'OCA\\Files_Sharing\\Controller\\AcceptController' => $baseDir . '/../lib/Controller/AcceptController.php',
'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => $baseDir . '/../lib/Controller/DeletedShareAPIController.php',
'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => $baseDir . '/../lib/Controller/ExternalSharesController.php',
Expand Down
2 changes: 2 additions & 0 deletions apps/files_sharing/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class ComposerStaticInitFiles_Sharing
'OCA\\Files_Sharing\\Collaboration\\ShareRecipientSorter' => __DIR__ . '/..' . '/../lib/Collaboration/ShareRecipientSorter.php',
'OCA\\Files_Sharing\\Command\\CleanupRemoteStorages' => __DIR__ . '/..' . '/../lib/Command/CleanupRemoteStorages.php',
'OCA\\Files_Sharing\\Command\\ExiprationNotification' => __DIR__ . '/..' . '/../lib/Command/ExiprationNotification.php',
'OCA\\Files_Sharing\\Command\\ListCommand' => __DIR__ . '/..' . '/../lib/Command/ListCommand.php',
'OCA\\Files_Sharing\\Command\\SetOwner' => __DIR__ . '/..' . '/../lib/Command/SetOwner.php',
'OCA\\Files_Sharing\\Controller\\AcceptController' => __DIR__ . '/..' . '/../lib/Controller/AcceptController.php',
'OCA\\Files_Sharing\\Controller\\DeletedShareAPIController' => __DIR__ . '/..' . '/../lib/Controller/DeletedShareAPIController.php',
'OCA\\Files_Sharing\\Controller\\ExternalSharesController' => __DIR__ . '/..' . '/../lib/Controller/ExternalSharesController.php',
Expand Down
197 changes: 197 additions & 0 deletions apps/files_sharing/lib/Command/ListCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <[email protected]>
*
* @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/>.
*
*/

namespace OCA\Files_Sharing\Command;

use OC\Core\Command\Base;
use OCP\IUserManager;
use OCP\Share\IManager;
use OCP\Share\IShare;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class ListCommand extends Base {
private IManager $shareManager;

public function __construct(
IManager $shareManager
) {
$this->shareManager = $shareManager;
parent::__construct();
}

protected function configure(): void {
parent::configure();
$this
->setName('sharing:list')
->setDescription('List shares')
->addOption('owner', null, InputOption::VALUE_REQUIRED, "Limit shares by share owner")
->addOption('shared-by', null, InputOption::VALUE_REQUIRED, "Limit shares by share initiator")
->addOption('shared-with', null, InputOption::VALUE_REQUIRED, "Limit shares by share recipient")
->addOption('share-type', null, InputOption::VALUE_REQUIRED, "Limit shares by share recipient")
icewind1991 marked this conversation as resolved.
Show resolved Hide resolved
->addOption('file-id', null, InputOption::VALUE_REQUIRED, "Limit shares to a specific file or folder id");
}

public function execute(InputInterface $input, OutputInterface $output): int {
$ownerInput = $input->getOption('owner');
$sharedByInput = $input->getOption('shared-by');
$sharedWithInput = $input->getOption('shared-with');
$shareTypeInput = $input->getOption('share-type');
$fileInput = $input->getOption('file-id');

if ($shareTypeInput) {
$shareType = $this->parseShareType($shareTypeInput);
if ($shareType === null) {
$output->writeln("<error>Unknown share type $shareTypeInput</error>");
$output->writeln("possible values: <info>user</info>, <info>group</info>, <info>link</info>, " .
"<info>email</info>, <info>remote</info>, <info>circle</info>, <info>guest</info>, <info>remote_group</info>, " .
"<info>room</info>, <info>deck</info>, <info>deck_user</info>, <info>science-mesh</info>");
return 1;
}
} else {
$shareTypeInput = null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
$shareTypeInput = null;
$shareType = null;

}

$allShares = $this->shareManager->getAllShares();

$filteredShares = new \CallbackFilterIterator($allShares, function(IShare $share) use ($ownerInput, $shareType, $sharedByInput, $sharedWithInput, $fileInput) {

Check failure

Code scanning / Psalm

InvalidArgument

Argument 1 of CallbackFilterIterator::__construct expects Iterator<mixed, mixed>, but iterable<mixed, mixed> provided

Check notice

Code scanning / Psalm

PossiblyUndefinedVariable

Possibly undefined variable $shareType, first seen on line 65
return $this->filterShare($share, $ownerInput, $sharedByInput, $sharedWithInput, $shareType, $fileInput);
});

$shareData = [];
foreach ($filteredShares as $share) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the easiest to avoid type problems with the iterable is to just call filterShare in an if there and continue.

/** @var IShare $share */
$shareData[] = [
'share-id' => $share->getFullId(),
'share-owner' => $share->getShareOwner(),
'shared-by' => $share->getSharedBy(),
'shared-with' => $share->getSharedWith(),
'share-type' => $this->formatShareType($share->getShareType()),
'file-id' => $share->getNodeId(),
];
}

$outputFormat = $input->getOption('output');
if ($outputFormat === self::OUTPUT_FORMAT_JSON || $outputFormat === self::OUTPUT_FORMAT_JSON_PRETTY) {
$this->writeArrayInOutputFormat($input, $output, $shareData);
} else {
$table = new Table($output);
$table
->setHeaders(['share-id', 'share-owner', 'shared-by', 'shared-with', 'share-type', 'file-id'])
->setRows($shareData);
$table->render();
}

return 0;
}

private function filterShare(
IShare $share,
string $ownerInput = null,
string $sharedByInput = null,
string $sharedWithInput = null,
int $shareType = null,
int $fileInput = null
Comment on lines +112 to +116
Copy link
Contributor

Choose a reason for hiding this comment

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

Why the default values?
If it’s just to get nullable types prefix type with ? instead.

): bool {
if ($ownerInput && $share->getShareOwner() !== $ownerInput) {
return false;
}
if ($sharedByInput && $share->getSharedBy() !== $sharedByInput) {
return false;
}
if ($sharedWithInput && $share->getSharedWith() !== $sharedWithInput) {
return false;
}
if ($shareType && $share->getShareType() !== $shareType) {
return false;
}
if ($fileInput && $share->getNodeId() !== $fileInput) {
return false;
}
return true;
}

private function parseShareType(string $type): ?int {
switch ($type) {
case 'user':
return IShare::TYPE_USER;
case 'group':
return IShare::TYPE_GROUP;
case 'link':
return IShare::TYPE_LINK;
case 'email':
return IShare::TYPE_EMAIL;
case 'remote':
return IShare::TYPE_REMOTE;
case 'circle':
return IShare::TYPE_CIRCLE;
case 'guest':
return IShare::TYPE_GUEST;
case 'remote_group':
return IShare::TYPE_REMOTE_GROUP;
case 'room':
return IShare::TYPE_ROOM;
case 'deck':
return IShare::TYPE_DECK;
case 'deck_user':
return IShare::TYPE_DECK_USER;
case 'science-mesh':
return IShare::TYPE_SCIENCEMESH;
default:
return null;
}
}

private function formatShareType(int $type): string {
switch ($type) {
case IShare::TYPE_USER:
return 'user';
case IShare::TYPE_GROUP:
return 'group';
case IShare::TYPE_LINK:
return 'link';
case IShare::TYPE_EMAIL:
return 'email';
case IShare::TYPE_REMOTE:
return 'remote';
case IShare::TYPE_CIRCLE:
return 'circle';
case IShare::TYPE_GUEST:
return 'guest';
case IShare::TYPE_REMOTE_GROUP:
return 'remote_group';
case IShare::TYPE_ROOM:
return 'room';
case IShare::TYPE_DECK:
return 'deck';
case IShare::TYPE_DECK_USER:
return 'deck_user';
case IShare::TYPE_SCIENCEMESH:
return 'science-mesh';
default:
return 'other';
}
}
}
127 changes: 127 additions & 0 deletions apps/files_sharing/lib/Command/SetOwner.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Robin Appelman <[email protected]>
*
* @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/>.
*
*/

namespace OCA\Files_Sharing\Command;

use OC\Core\Command\Info\FileUtils;
use OCA\Files_Sharing\SharedMount;
use OCA\Files_Sharing\SharedStorage;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IUserManager;
use OCP\Share\IManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;

class SetOwner extends Command {
private FileUtils $fileUtils;
private IRootFolder $rootFolder;
private IManager $shareManager;
private IUserManager $userManager;

public function __construct(
FileUtils $fileUtils,
IRootFolder $rootFolder,
IManager $shareManager,
IUserManager $userManager
) {
$this->fileUtils = $fileUtils;
$this->rootFolder = $rootFolder;
$this->shareManager = $shareManager;
$this->userManager = $userManager;
parent::__construct();
}

protected function configure(): void {
$this
->setName('sharing:set-owner')
->setDescription('Change the owner of a share, note that the new owner must already have access to the file')
->addArgument('share-id', InputArgument::REQUIRED, "Id of the share to set the owner of")
->addArgument('new-owner', InputArgument::REQUIRED, "User id of to set the owner to");
}

public function execute(InputInterface $input, OutputInterface $output): int {
$shareId = $input->getArgument('share-id');
$targetId = $input->getArgument('new-owner');

$target = $this->userManager->get($targetId);
if (!$target) {
$output->writeln("<error>User $targetId not found</error>");
return 1;
}

$share = $this->shareManager->getShareById($shareId);
if (!$share) {

Check notice

Code scanning / Psalm

DocblockTypeContradiction

Operand of type false is always falsy
Copy link
Contributor

Choose a reason for hiding this comment

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

Replace by try/catch or remove

Check notice

Code scanning / Psalm

DocblockTypeContradiction

Docblock-defined type OCP\Share\IShare for $share is never falsy
$output->writeln("<error>Share $shareId not found</error>");
return 1;
}

$sourceFile = $share->getNode();
$usersWithAccessToSource = $this->fileUtils->getFilesByUser($sourceFile);

$targetHasNonSharedAccess = false;
$targetHasSharedAccess = false;
$fileOrFolder = ($sourceFile instanceof File) ? "file" : "folder";;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
$fileOrFolder = ($sourceFile instanceof File) ? "file" : "folder";;
$fileOrFolder = ($sourceFile instanceof File) ? "file" : "folder";

$sourceName = $sourceFile->getName();
$targetNode = null;

if (isset($usersWithAccessToSource[$target->getUID()])) {
$targetSourceNodes = $usersWithAccessToSource[$target->getUID()];
foreach ($targetSourceNodes as $targetSourceNode) {
$targetNode = $targetSourceNode;
$mount = $targetSourceNode->getMountPoint();
if ($mount instanceof SharedMount) {
if ($mount->getShare()->getId() === $share->getId()) {
$targetHasSharedAccess = true;
continue;
}
}
$targetHasNonSharedAccess = true;
}
}

if (!$targetHasSharedAccess && !$targetHasNonSharedAccess) {
$output->writeln("<error>$targetId has no access to the $fileOrFolder $sourceName shared by $shareId</error>");
return 1;
}

if (!$targetHasNonSharedAccess && $targetHasSharedAccess) {

Check failure

Code scanning / Psalm

RedundantCondition

Operand of type true is always truthy
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (!$targetHasNonSharedAccess && $targetHasSharedAccess) {
if (!$targetHasNonSharedAccess) {

$output->writeln("<error>Target user $targetId only has access to the shared $fileOrFolder $sourceName through the share $shareId.</error>");
return 1;
}

$share->setNode($targetNode);

Check notice

Code scanning / Psalm

PossiblyNullArgument

Argument 1 of OCP\Share\IShare::setNode cannot be null, possibly null value provided
$share->setShareOwner($target->getUID());
$share->setSharedBy($target->getUID());

$this->shareManager->updateShare($share);

return 0;
}

}