diff options
-rw-r--r-- | apps/files/appinfo/info.xml | 1 | ||||
-rw-r--r-- | apps/files/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/files/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/files/lib/Command/SanitizeFilenames.php | 128 |
4 files changed, 131 insertions, 0 deletions
diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index cdcc79e47fb..df20902ef6c 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -46,6 +46,7 @@ <command>OCA\Files\Command\Delete</command> <command>OCA\Files\Command\Copy</command> <command>OCA\Files\Command\Move</command> + <command>OCA\Files\Command\SanitizeFilenames</command> <command>OCA\Files\Command\Object\Delete</command> <command>OCA\Files\Command\Object\Get</command> <command>OCA\Files\Command\Object\Put</command> diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 0d9e6aa2d77..956abce5682 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -39,6 +39,7 @@ return array( 'OCA\\Files\\Command\\Object\\Put' => $baseDir . '/../lib/Command/Object/Put.php', 'OCA\\Files\\Command\\Put' => $baseDir . '/../lib/Command/Put.php', 'OCA\\Files\\Command\\RepairTree' => $baseDir . '/../lib/Command/RepairTree.php', + 'OCA\\Files\\Command\\SanitizeFilenames' => $baseDir . '/../lib/Command/SanitizeFilenames.php', 'OCA\\Files\\Command\\Scan' => $baseDir . '/../lib/Command/Scan.php', 'OCA\\Files\\Command\\ScanAppData' => $baseDir . '/../lib/Command/ScanAppData.php', 'OCA\\Files\\Command\\TransferOwnership' => $baseDir . '/../lib/Command/TransferOwnership.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 5ece9073178..f6f253f2327 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -54,6 +54,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Command\\Object\\Put' => __DIR__ . '/..' . '/../lib/Command/Object/Put.php', 'OCA\\Files\\Command\\Put' => __DIR__ . '/..' . '/../lib/Command/Put.php', 'OCA\\Files\\Command\\RepairTree' => __DIR__ . '/..' . '/../lib/Command/RepairTree.php', + 'OCA\\Files\\Command\\SanitizeFilenames' => __DIR__ . '/..' . '/../lib/Command/SanitizeFilenames.php', 'OCA\\Files\\Command\\Scan' => __DIR__ . '/..' . '/../lib/Command/Scan.php', 'OCA\\Files\\Command\\ScanAppData' => __DIR__ . '/..' . '/../lib/Command/ScanAppData.php', 'OCA\\Files\\Command\\TransferOwnership' => __DIR__ . '/..' . '/../lib/Command/TransferOwnership.php', diff --git a/apps/files/lib/Command/SanitizeFilenames.php b/apps/files/lib/Command/SanitizeFilenames.php new file mode 100644 index 00000000000..a4fb3c93f1e --- /dev/null +++ b/apps/files/lib/Command/SanitizeFilenames.php @@ -0,0 +1,128 @@ +<?php + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files\Command; + +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\Output\OutputInterface; + +class SanitizeFilenames extends Base { + + private OutputInterface $output; + + 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' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int { + $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</error>"); + 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); + + $folder = $this->rootFolder->getUserFolder($user->getUID()); + $this->sanitizeFiles($folder); + } + + private function sanitizeFiles(Folder $folder): void { + foreach ($folder->getDirectoryListing() as $node) { + $this->output->writeln('start: ' . $node->getPath()); + if ($folder->isCreatable()) { + try { + $oldName = $node->getName(); + if (!$this->filenameValidator->isFilenameValid($oldName)) { + $newName = $this->sanitizeName($oldName); + $newName = $folder->getNonExistingName($newName); + $path = rtrim(dirname($node->getPath()), '/'); + $node->move("$path/$newName"); + $this->output->writeln('renamed: ' . $oldName . ' to ' . $newName); + } + } catch (LockedException) { + $this->output->writeln('skipping: ' . $node->getPath() . ' (file is locked)'); + } catch (NotPermittedException) { + $this->output->writeln('<error>failed: ' . $node->getPath() . ' (denied)</error>'); + } + } else { + $this->output->writeln('Skipping: ' . $node->getPath() . ' (no permissions)'); + } + if ($node instanceof Folder) { + $this->sanitizeFiles($node); + } + } + } + + private function sanitizeName(string $name): string { + $l10n = $this->l10nFactory->get('files'); + + foreach ($this->filenameValidator->getForbiddenExtensions() as $extension) { + if (str_ends_with($name, $extension)) { + $name = substr($name, 0, strlen($name) - strlen($extension)); + } + } + + $basename = substr($name, 0, strpos($name, '.', 1) ?: null); + if (in_array($basename, $this->filenameValidator->getForbiddenBasenames())) { + $name = str_replace($basename, $l10n->t('%1$s (renamed)', [$basename]), $name); + } + + if ($name === '') { + $name = $l10n->t('renamed file'); + } + + $forbiddenCharacter = $this->filenameValidator->getForbiddenCharacters(); + $charReplacement = reset(array_diff([' ', '_', '-', '.'], $forbiddenCharacter)); + $name = str_replace($forbiddenCharacter, $charReplacement, $name); + + return $name; + } +} |