mirror of
https://github.com/nextcloud/server.git
synced 2025-05-18 04:01:41 +00:00

Share the filename sanitizing with the OCP filename validator. Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
150 lines
4.5 KiB
PHP
150 lines
4.5 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
namespace OCA\Files\Command;
|
|
|
|
use Exception;
|
|
use OC\Core\Command\Base;
|
|
use OC\Files\FilenameValidator;
|
|
use OCP\Files\Folder;
|
|
use OCP\Files\IRootFolder;
|
|
use OCP\Files\NotPermittedException;
|
|
use OCP\IUser;
|
|
use OCP\IUserManager;
|
|
use OCP\IUserSession;
|
|
use OCP\L10N\IFactory;
|
|
use OCP\Lock\LockedException;
|
|
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 SanitizeFilenames extends Base {
|
|
|
|
private OutputInterface $output;
|
|
private ?string $charReplacement;
|
|
private bool $dryRun;
|
|
|
|
public function __construct(
|
|
private IUserManager $userManager,
|
|
private IRootFolder $rootFolder,
|
|
private IUserSession $session,
|
|
private IFactory $l10nFactory,
|
|
private FilenameValidator $filenameValidator,
|
|
) {
|
|
parent::__construct();
|
|
}
|
|
|
|
protected function configure(): void {
|
|
parent::configure();
|
|
|
|
$this
|
|
->setName('files:sanitize-filenames')
|
|
->setDescription('Renames files to match naming constraints')
|
|
->addArgument(
|
|
'user_id',
|
|
InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
|
|
'will only rename files the given user(s) have access to'
|
|
)
|
|
->addOption(
|
|
'dry-run',
|
|
mode: InputOption::VALUE_NONE,
|
|
description: 'Do not actually rename any files but just check filenames.',
|
|
)
|
|
->addOption(
|
|
'char-replacement',
|
|
'c',
|
|
mode: InputOption::VALUE_REQUIRED,
|
|
description: 'Replacement for invalid character (by default space, underscore or dash is used)',
|
|
);
|
|
|
|
}
|
|
|
|
protected function execute(InputInterface $input, OutputInterface $output): int {
|
|
$this->charReplacement = $input->getOption('char-replacement');
|
|
// check if replacement is needed
|
|
$c = $this->filenameValidator->getForbiddenCharacters();
|
|
if (count($c) > 0) {
|
|
try {
|
|
$this->filenameValidator->sanitizeFilename($c[0], $this->charReplacement);
|
|
} catch (\InvalidArgumentException) {
|
|
if ($this->charReplacement === null) {
|
|
$output->writeln('<error>Character replacement required</error>');
|
|
} else {
|
|
$output->writeln('<error>Invalid character replacement given</error>');
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
$this->dryRun = $input->getOption('dry-run');
|
|
if ($this->dryRun) {
|
|
$output->writeln('<info>Dry run is enabled, no actual renaming will be applied.</>');
|
|
}
|
|
|
|
$this->output = $output;
|
|
$users = $input->getArgument('user_id');
|
|
if (!empty($users)) {
|
|
foreach ($users as $userId) {
|
|
$user = $this->userManager->get($userId);
|
|
if ($user === null) {
|
|
$output->writeln("<error>User '$userId' does not exist - skipping</>");
|
|
continue;
|
|
}
|
|
$this->sanitizeUserFiles($user);
|
|
}
|
|
} else {
|
|
$this->userManager->callForSeenUsers($this->sanitizeUserFiles(...));
|
|
}
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function sanitizeUserFiles(IUser $user): void {
|
|
// Set an active user so that event listeners can correctly work (e.g. files versions)
|
|
$this->session->setVolatileActiveUser($user);
|
|
|
|
$this->output->writeln('<info>Analyzing files of ' . $user->getUID() . '</>');
|
|
|
|
$folder = $this->rootFolder->getUserFolder($user->getUID());
|
|
$this->sanitizeFiles($folder);
|
|
}
|
|
|
|
private function sanitizeFiles(Folder $folder): void {
|
|
foreach ($folder->getDirectoryListing() as $node) {
|
|
$this->output->writeln('scanning: ' . $node->getPath(), OutputInterface::VERBOSITY_VERBOSE);
|
|
|
|
try {
|
|
$oldName = $node->getName();
|
|
$newName = $this->filenameValidator->sanitizeFilename($oldName, $this->charReplacement);
|
|
if ($oldName !== $newName) {
|
|
$newName = $folder->getNonExistingName($newName);
|
|
$path = rtrim(dirname($node->getPath()), '/');
|
|
|
|
if (!$this->dryRun) {
|
|
$node->move("$path/$newName");
|
|
} elseif (!$folder->isCreatable()) {
|
|
// simulate error for dry run
|
|
throw new NotPermittedException();
|
|
}
|
|
$this->output->writeln('renamed: "' . $oldName . '" to "' . $newName . '"');
|
|
}
|
|
} catch (LockedException) {
|
|
$this->output->writeln('<comment>skipping: ' . $node->getPath() . ' (file is locked)</>');
|
|
} catch (NotPermittedException) {
|
|
$this->output->writeln('<comment>skipping: ' . $node->getPath() . ' (no permissions)</>');
|
|
} catch (Exception) {
|
|
$this->output->writeln('<error>failed: ' . $node->getPath() . '</>');
|
|
}
|
|
|
|
if ($node instanceof Folder) {
|
|
$this->sanitizeFiles($node);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|