From 805fe3e15b68beb5a3cf0ce8f1263e9030d3c04d Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 28 Aug 2025 17:43:35 +0200 Subject: [PATCH] feat(files): provide UI to sanitize filenames after enabling WCF Signed-off-by: Ferdinand Thiessen --- apps/files/appinfo/info.xml | 1 + .../composer/composer/autoload_classmap.php | 4 +- .../composer/composer/autoload_static.php | 4 +- apps/files/lib/AppInfo/Application.php | 3 - .../lib/BackgroundJob/SanitizeFilenames.php | 224 ++++++++++++++++++ apps/files/lib/Command/SanitizeFilenames.php | 9 + .../lib/Controller/FilenamesController.php | 102 ++++++++ apps/files/lib/Service/SettingsService.php | 49 ++++ apps/files/lib/Settings/AdminSettings.php | 48 ++++ .../lib/Settings/DeclarativeAdminSettings.php | 67 ------ .../Settings/SettingsSanitizeFilenames.vue | 181 ++++++++++++++ apps/files/src/main-settings-admin.ts | 17 ++ ...-settings.js => main-settings-personal.ts} | 4 +- apps/files/src/views/SettingsAdmin.vue | 78 ++++++ apps/files/templates/settings-admin.php | 8 + webpack.modules.js | 3 +- 16 files changed, 727 insertions(+), 75 deletions(-) create mode 100644 apps/files/lib/BackgroundJob/SanitizeFilenames.php create mode 100644 apps/files/lib/Controller/FilenamesController.php create mode 100644 apps/files/lib/Settings/AdminSettings.php delete mode 100644 apps/files/lib/Settings/DeclarativeAdminSettings.php create mode 100644 apps/files/src/components/Settings/SettingsSanitizeFilenames.vue create mode 100644 apps/files/src/main-settings-admin.ts rename apps/files/src/{main-personal-settings.js => main-settings-personal.ts} (92%) create mode 100644 apps/files/src/views/SettingsAdmin.vue create mode 100644 apps/files/templates/settings-admin.php diff --git a/apps/files/appinfo/info.xml b/apps/files/appinfo/info.xml index fb53cef79b8..4c9a6e95e74 100644 --- a/apps/files/appinfo/info.xml +++ b/apps/files/appinfo/info.xml @@ -59,6 +59,7 @@ + OCA\Files\Settings\AdminSettings OCA\Files\Settings\PersonalSettings diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 0c0f734251f..25f9c0eaf3f 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -23,6 +23,7 @@ return array( 'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => $baseDir . '/../lib/BackgroundJob/CleanupFileLocks.php', 'OCA\\Files\\BackgroundJob\\DeleteExpiredOpenLocalEditor' => $baseDir . '/../lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php', 'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => $baseDir . '/../lib/BackgroundJob/DeleteOrphanedItems.php', + 'OCA\\Files\\BackgroundJob\\SanitizeFilenames' => $baseDir . '/../lib/BackgroundJob/SanitizeFilenames.php', 'OCA\\Files\\BackgroundJob\\ScanFiles' => $baseDir . '/../lib/BackgroundJob/ScanFiles.php', 'OCA\\Files\\BackgroundJob\\TransferOwnership' => $baseDir . '/../lib/BackgroundJob/TransferOwnership.php', 'OCA\\Files\\Capabilities' => $baseDir . '/../lib/Capabilities.php', @@ -53,6 +54,7 @@ return array( 'OCA\\Files\\Controller\\ConversionApiController' => $baseDir . '/../lib/Controller/ConversionApiController.php', 'OCA\\Files\\Controller\\DirectEditingController' => $baseDir . '/../lib/Controller/DirectEditingController.php', 'OCA\\Files\\Controller\\DirectEditingViewController' => $baseDir . '/../lib/Controller/DirectEditingViewController.php', + 'OCA\\Files\\Controller\\FilenamesController' => $baseDir . '/../lib/Controller/FilenamesController.php', 'OCA\\Files\\Controller\\OpenLocalEditorController' => $baseDir . '/../lib/Controller/OpenLocalEditorController.php', 'OCA\\Files\\Controller\\TemplateController' => $baseDir . '/../lib/Controller/TemplateController.php', 'OCA\\Files\\Controller\\TransferOwnershipController' => $baseDir . '/../lib/Controller/TransferOwnershipController.php', @@ -88,6 +90,6 @@ return array( 'OCA\\Files\\Service\\TagService' => $baseDir . '/../lib/Service/TagService.php', 'OCA\\Files\\Service\\UserConfig' => $baseDir . '/../lib/Service/UserConfig.php', 'OCA\\Files\\Service\\ViewConfig' => $baseDir . '/../lib/Service/ViewConfig.php', - 'OCA\\Files\\Settings\\DeclarativeAdminSettings' => $baseDir . '/../lib/Settings/DeclarativeAdminSettings.php', + 'OCA\\Files\\Settings\\AdminSettings' => $baseDir . '/../lib/Settings/AdminSettings.php', 'OCA\\Files\\Settings\\PersonalSettings' => $baseDir . '/../lib/Settings/PersonalSettings.php', ); diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 19310ed4e92..75c5f40cd81 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -38,6 +38,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\BackgroundJob\\CleanupFileLocks' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupFileLocks.php', 'OCA\\Files\\BackgroundJob\\DeleteExpiredOpenLocalEditor' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteExpiredOpenLocalEditor.php', 'OCA\\Files\\BackgroundJob\\DeleteOrphanedItems' => __DIR__ . '/..' . '/../lib/BackgroundJob/DeleteOrphanedItems.php', + 'OCA\\Files\\BackgroundJob\\SanitizeFilenames' => __DIR__ . '/..' . '/../lib/BackgroundJob/SanitizeFilenames.php', 'OCA\\Files\\BackgroundJob\\ScanFiles' => __DIR__ . '/..' . '/../lib/BackgroundJob/ScanFiles.php', 'OCA\\Files\\BackgroundJob\\TransferOwnership' => __DIR__ . '/..' . '/../lib/BackgroundJob/TransferOwnership.php', 'OCA\\Files\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', @@ -68,6 +69,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Controller\\ConversionApiController' => __DIR__ . '/..' . '/../lib/Controller/ConversionApiController.php', 'OCA\\Files\\Controller\\DirectEditingController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingController.php', 'OCA\\Files\\Controller\\DirectEditingViewController' => __DIR__ . '/..' . '/../lib/Controller/DirectEditingViewController.php', + 'OCA\\Files\\Controller\\FilenamesController' => __DIR__ . '/..' . '/../lib/Controller/FilenamesController.php', 'OCA\\Files\\Controller\\OpenLocalEditorController' => __DIR__ . '/..' . '/../lib/Controller/OpenLocalEditorController.php', 'OCA\\Files\\Controller\\TemplateController' => __DIR__ . '/..' . '/../lib/Controller/TemplateController.php', 'OCA\\Files\\Controller\\TransferOwnershipController' => __DIR__ . '/..' . '/../lib/Controller/TransferOwnershipController.php', @@ -103,7 +105,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Service\\TagService' => __DIR__ . '/..' . '/../lib/Service/TagService.php', 'OCA\\Files\\Service\\UserConfig' => __DIR__ . '/..' . '/../lib/Service/UserConfig.php', 'OCA\\Files\\Service\\ViewConfig' => __DIR__ . '/..' . '/../lib/Service/ViewConfig.php', - 'OCA\\Files\\Settings\\DeclarativeAdminSettings' => __DIR__ . '/..' . '/../lib/Settings/DeclarativeAdminSettings.php', + 'OCA\\Files\\Settings\\AdminSettings' => __DIR__ . '/..' . '/../lib/Settings/AdminSettings.php', 'OCA\\Files\\Settings\\PersonalSettings' => __DIR__ . '/..' . '/../lib/Settings/PersonalSettings.php', ); diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 2761b44ecf9..1de8e60ab5a 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -29,7 +29,6 @@ use OCA\Files\Search\FilesSearchProvider; use OCA\Files\Service\TagService; use OCA\Files\Service\UserConfig; use OCA\Files\Service\ViewConfig; -use OCA\Files\Settings\DeclarativeAdminSettings; use OCP\Activity\IManager as IActivityManager; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; @@ -111,8 +110,6 @@ class Application extends App implements IBootstrap { $context->registerCapability(AdvancedCapabilities::class); $context->registerCapability(DirectEditingCapabilities::class); - $context->registerDeclarativeSettings(DeclarativeAdminSettings::class); - $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class); $context->registerEventListener(RenderReferenceEvent::class, RenderReferenceEventListener::class); $context->registerEventListener(BeforeNodeRenamedEvent::class, SyncLivePhotosListener::class); diff --git a/apps/files/lib/BackgroundJob/SanitizeFilenames.php b/apps/files/lib/BackgroundJob/SanitizeFilenames.php new file mode 100644 index 00000000000..855fa4b2d04 --- /dev/null +++ b/apps/files/lib/BackgroundJob/SanitizeFilenames.php @@ -0,0 +1,224 @@ +setAllowParallelRuns(false); + } + + /** + * Makes the background job do its work + * + * @param array $argument unused argument + * @throws \Exception + */ + public function run($argument) { + $this->charReplacement = strval($argument['charReplacement']) ?: null; + if (isset($argument['errorsOnly'])) { + $this->retryFailedNodes(); + return; + } + + $this->offset = intval($argument['offset']); + $this->limit = intval($argument['limit']); + if ($this->offset === 0) { + $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_RUNNING); + } + + $this->currentIndex = 0; + foreach ($this->manager->getSeenUsers($this->offset) as $user) { + $this->sanitizeUserFiles($user); + $this->currentIndex++; + $this->appConfig->setAppValueInt('sanitize_filenames_index', $this->currentIndex); + + if ($this->currentIndex === $this->limit) { + break; + } + } + + if ($this->currentIndex === $this->limit) { + $this->offset += $this->limit; + $this->jobList->add(self::class, ['limit' => $this->limit, 'offset' => $this->offset, 'charReplacement' => $this->charReplacement]); + return; + } + + // No index to process anymore, we are done + $this->appConfig->deleteAppValue('sanitize_filenames_index'); + + $hasErrors = !empty($this->userConfig->getValuesByUsers(Application::APP_ID, 'sanitize_filenames_errors')); + if ($hasErrors) { + $this->logger->info('Filename sanitization finished with errors. Retrying failed files in next background job run.'); + $this->jobList->add(self::class, ['errorsOnly' => true, 'charReplacement' => $this->charReplacement]); + return; + } + + // we are really done! + $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_DONE); + } + + /** + * Retry to sanitize files that failed in the first run + */ + private function retryFailedNodes(): void { + $this->logger->debug('Retry sanitizing failed filename sanitization.'); + $results = $this->userConfig->getValuesByUsers(Application::APP_ID, 'sanitize_filenames_errors'); + + $hasErrors = false; + foreach ($results as $userId => $errors) { + $user = $this->manager->get($userId); + if ($user === null) { + // user got deleted meanwhile, ignore + continue; + } + + $hasErrors = $hasErrors || $this->retryFailedUserNodes($user, $errors); + $this->userConfig->deleteUserConfig($userId, Application::APP_ID, 'sanitize_filenames_errors'); + } + + if ($hasErrors) { + $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_ERROR); + $this->logger->error('Retrying filename sanitization failed permanently.'); + } else { + $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_DONE); + $this->logger->info('Retrying filename sanitization succeeded.'); + } + } + + private function retryFailedUserNodes(IUser $user, array $errors): bool { + $this->session->setVolatileActiveUser($user); + $folder = $this->rootFolder->getUserFolder($user->getUID()); + + $this->logger->debug("filename sanitization retry: started for user '{$user->getUID()}'"); + $hasErrors = false; + foreach ($errors as $path) { + try { + $node = $folder->get($path); + $this->sanitizeNode($node); + } catch (NotFoundException) { + // file got deleted meanwhile, ignore + } catch (\Exception $error) { + $this->logger->error('filename sanitization failed when retried: ' . $path, ['exception' => $error]); + $hasErrors = true; + } + } + + // tear down FS for user to make sure we do not run out of memory due to cached user FS + $this->setupManager->tearDown(); + + return $hasErrors; + } + + + 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->logger->debug("filename sanitization: started for user '{$user->getUID()}'"); + $errors = $this->sanitizeFolder($folder); + + // tear down FS for user to make sure we do not run out of memory due to cached user FS + $this->setupManager->tearDown(); + + if (!empty($errors)) { + $this->userConfig->setValueArray($user->getUID(), 'files', 'sanitize_filenames_errors', $errors, true); + } + } + + /** + * Sanitizes the filenames of all nodes in a folder + * + * @return list list of nodes that could not be sanitized + */ + private function sanitizeFolder(Folder $folder): array { + $errors = []; + foreach ($folder->getDirectoryListing() as $node) { + try { + $this->sanitizeNode($node); + } catch (LockedException) { + $this->logger->debug('filename sanitization skipped: ' . $node->getPath() . ' (file is locked)'); + $errors[] = $node->getPath(); + } catch (\Exception $error) { + $this->logger->warning('filename sanitization failed: ' . $node->getPath(), ['exception' => $error]); + $errors[] = $node->getPath(); + } + + if ($node instanceof Folder) { + $errors = array_merge($errors, $this->sanitizeFolder($node)); + } + } + return $errors; + } + + /** + * Sanitizes the filename of a single node + * + * @throws LockedException If the file is locked + * @throws \Exception Unknown error + */ + private function sanitizeNode(Node $node): void { + if ($node->isShared() && !$node->isUpdateable()) { + // we cannot rename files in shares where we do not have permissions - we do it when sanitizing the owner's files + return; + } + + try { + $oldName = $node->getName(); + $newName = $this->filenameValidator->sanitizeFilename($oldName, $this->charReplacement); + if ($oldName !== $newName) { + $newName = $node->getParent()->getNonExistingName($newName); + $path = rtrim(dirname($node->getPath()), '/'); + + $node->move("$path/$newName"); + } + } catch (NotFoundException) { + // file got deleted meanwhile, ignore + // or this is shared without permissions to rename it, ignore (owner will rename it) + } + } +} diff --git a/apps/files/lib/Command/SanitizeFilenames.php b/apps/files/lib/Command/SanitizeFilenames.php index 88d41d1cb5e..5cdebffdeba 100644 --- a/apps/files/lib/Command/SanitizeFilenames.php +++ b/apps/files/lib/Command/SanitizeFilenames.php @@ -11,6 +11,8 @@ namespace OCA\Files\Command; use Exception; use OC\Core\Command\Base; use OC\Files\FilenameValidator; +use OCA\Files\Service\SettingsService; +use OCP\AppFramework\Services\IAppConfig; use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\NotPermittedException; @@ -29,6 +31,7 @@ class SanitizeFilenames extends Base { private OutputInterface $output; private ?string $charReplacement; private bool $dryRun; + private bool $errorsOrSkipped = false; public function __construct( private IUserManager $userManager, @@ -36,6 +39,8 @@ class SanitizeFilenames extends Base { private IUserSession $session, private IFactory $l10nFactory, private FilenameValidator $filenameValidator, + private SettingsService $service, + private IAppConfig $appConfig, ) { parent::__construct(); } @@ -100,6 +105,10 @@ class SanitizeFilenames extends Base { } } else { $this->userManager->callForSeenUsers($this->sanitizeUserFiles(...)); + if ($this->service->hasFilesWindowsSupport() && $this->appConfig->getAppValueInt('sanitize_filenames_status') === 0) { + // we are done - if this is for sanitizing all users for windows filename support then set this UI flag + $this->appConfig->setAppValueInt('sanitize_filenames_status', SettingsService::STATUS_WCF_DONE); + } } return self::SUCCESS; } diff --git a/apps/files/lib/Controller/FilenamesController.php b/apps/files/lib/Controller/FilenamesController.php new file mode 100644 index 00000000000..2dc05005dcd --- /dev/null +++ b/apps/files/lib/Controller/FilenamesController.php @@ -0,0 +1,102 @@ +settingsService->setFilesWindowsSupport($enabled); + return new DataResponse(['enabled' => $enabled]); + } + + /** + * Start a filename sanitization job + * + * @param null|int $limit Limit the number of users to be sanitized per run + * @param null|string $charReplacement Optionally specify a character to replace forbidden characters with + * @return DataResponse + * @throws OCSBadRequestException On invalid parameters or if a sanitization is already running + */ + #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] + #[Route(type: Route::TYPE_API, verb: 'POST', url: '/api/v1/filenames/sanitization')] + public function sanitizeFilenames(?int $limit = 10, ?string $charReplacement = null): DataResponse { + if ($limit < 1) { + throw new OCSBadRequestException($this->l10n->t('Limit must be a positive integer.')); + } + if ($charReplacement !== null && ($charReplacement === '' || mb_strlen($charReplacement) > 1)) { + throw new OCSBadRequestException($this->l10n->t('The replacement character may only be a single character.')); + } + + if ($this->settingsService->isFilenameSanitizationRunning()) { + throw new OCSBadRequestException($this->l10n->t('Filename sanitization already started.')); + } + + $this->jobList->add(SanitizeFilenames::class, [ + 'offset' => 0, + 'limit' => $limit, + 'charReplacement' => $charReplacement, + ]); + + return new DataResponse([]); + } + + #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] + #[Route(type: Route::TYPE_API, verb: 'GET', url: '/api/v1/filenames/sanitization')] + public function getStatus(): DataResponse { + return new DataResponse($this->settingsService->getSanitizationStatus()); + } + + /** + * @return DataResponse + * @throws OCSBadRequestException If there is no filename sanitization in progress + */ + #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] + #[Route(type: Route::TYPE_API, verb: 'DELETE', url: '/api/v1/filenames/sanitization')] + public function stopSanitization(): DataResponse { + if (!$this->settingsService->isFilenameSanitizationRunning()) { + throw new OCSBadRequestException($this->l10n->t('No filename sanitization inprogress.')); + } + + $this->jobList->remove(SanitizeFilenames::class); + return new DataResponse([]); + } +} diff --git a/apps/files/lib/Service/SettingsService.php b/apps/files/lib/Service/SettingsService.php index d07e907a5f6..6afec4e6549 100644 --- a/apps/files/lib/Service/SettingsService.php +++ b/apps/files/lib/Service/SettingsService.php @@ -8,7 +8,13 @@ declare(strict_types=1); namespace OCA\Files\Service; use OC\Files\FilenameValidator; +use OCA\Files\AppInfo\Application; +use OCA\Files\BackgroundJob\SanitizeFilenames; +use OCP\AppFramework\Services\IAppConfig; +use OCP\BackgroundJob\IJobList; +use OCP\Config\IUserConfig; use OCP\IConfig; +use OCP\IUserManager; use Psr\Log\LoggerInterface; class SettingsService { @@ -30,10 +36,20 @@ class SettingsService { '*', ]; + public const STATUS_WCF_UNKNOWN = 0; + public const STATUS_WCF_SCHEDULED = 1; + public const STATUS_WCF_RUNNING = 2; + public const STATUS_WCF_DONE = 3; + public const STATUS_WCF_ERROR = 4; + public function __construct( private IConfig $config, + private IAppConfig $appConfig, + private IUserConfig $userConfig, private FilenameValidator $filenameValidator, private LoggerInterface $logger, + private IUserManager $userManager, + private IJobList $jobList, ) { } @@ -59,5 +75,38 @@ class SettingsService { 'forbidden_filename_extensions' => empty($extensions) ? null : $extensions, ]; $this->config->setSystemValues($values); + + // reset any sanitization status + $this->appConfig->deleteAppValue('sanitize_filenames_status'); + $this->appConfig->deleteAppValue('sanitize_filenames_index'); + $this->userConfig->deleteKey(Application::APP_ID, 'sanitize_filenames_errors'); + } + + public function isFilenameSanitizationRunning(): bool { + $jobs = $this->jobList->getJobsIterator(SanitizeFilenames::class, 1, 0); + foreach ($jobs as $job) { + return true; + } + return false; + } + + /** + * Get the current status of the filename sanitization. + * + * @psalm-return array{total: int, processed: int, status: self::STATUS_WCF_*, errors: array} + */ + public function getSanitizationStatus(): array { + /** @var self::STATUS_WCF_* */ + $status = $this->appConfig->getAppValueInt('sanitize_filenames_status'); + $index = $this->appConfig->getAppValueInt('sanitize_filenames_index', -1); + $total = $this->userManager->countSeenUsers(); + /** @var array */ + $errors = $this->userConfig->getValuesByUsers(Application::APP_ID, 'sanitize_filenames_errors'); + + if ($status === 0 && $this->isFilenameSanitizationRunning()) { + $status = 1; // we know its scheduled + } + + return ['status' => $status, 'processed' => $index, 'total' => $total, 'errors' => $errors]; } } diff --git a/apps/files/lib/Settings/AdminSettings.php b/apps/files/lib/Settings/AdminSettings.php new file mode 100644 index 00000000000..6c609ef6331 --- /dev/null +++ b/apps/files/lib/Settings/AdminSettings.php @@ -0,0 +1,48 @@ +service->hasFilesWindowsSupport(); + $this->initialState->provideInitialState('filesCompatibilitySettings', [ + 'docUrl' => $this->urlGenerator->linkToDocs(''), + 'status' => $this->service->getSanitizationStatus(), + 'windowsSupport' => $windowSupport, + ]); + + Util::addScript(Application::APP_ID, 'settings-admin'); + return new TemplateResponse(Application::APP_ID, 'settings-admin', renderAs: TemplateResponse::RENDER_AS_BLANK); + } +} diff --git a/apps/files/lib/Settings/DeclarativeAdminSettings.php b/apps/files/lib/Settings/DeclarativeAdminSettings.php deleted file mode 100644 index bbf97cc4d32..00000000000 --- a/apps/files/lib/Settings/DeclarativeAdminSettings.php +++ /dev/null @@ -1,67 +0,0 @@ - $this->service->hasFilesWindowsSupport(), - default => throw new \InvalidArgumentException('Unexpected field id ' . $fieldId), - }; - } - - public function setValue(string $fieldId, mixed $value, IUser $user): void { - switch ($fieldId) { - case 'windows_support': - $this->service->setFilesWindowsSupport((bool)$value); - break; - } - } - - public function getSchema(): array { - return [ - 'id' => 'files-filename-support', - 'priority' => 10, - 'section_type' => DeclarativeSettingsTypes::SECTION_TYPE_ADMIN, - 'section_id' => 'server', - 'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL, - 'title' => $this->l->t('Files compatibility'), - 'doc_url' => $this->urlGenerator->linkToDocs('admin-windows-compatible-filenames'), - 'description' => ( - $this->l->t('Allow to restrict filenames to ensure files can be synced with all clients. By default all filenames valid on POSIX (e.g. Linux or macOS) are allowed.') - . "\n" . $this->l->t('After enabling the Windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner.') - . "\n" . $this->l->t('It is also possible to migrate files automatically after enabling this setting, please refer to the documentation about the occ command.') - ), - - 'fields' => [ - [ - 'id' => 'windows_support', - 'title' => $this->l->t('Enforce Windows compatibility'), - 'description' => $this->l->t('This will block filenames not valid on Windows systems, like using reserved names or special characters. But this will not enforce compatibility of case sensitivity.'), - 'type' => DeclarativeSettingsTypes::CHECKBOX, - 'default' => false, - ], - ], - ]; - } -} diff --git a/apps/files/src/components/Settings/SettingsSanitizeFilenames.vue b/apps/files/src/components/Settings/SettingsSanitizeFilenames.vue new file mode 100644 index 00000000000..0aa6b13b6bd --- /dev/null +++ b/apps/files/src/components/Settings/SettingsSanitizeFilenames.vue @@ -0,0 +1,181 @@ + + + + + + + diff --git a/apps/files/src/main-settings-admin.ts b/apps/files/src/main-settings-admin.ts new file mode 100644 index 00000000000..b4afc0b13ff --- /dev/null +++ b/apps/files/src/main-settings-admin.ts @@ -0,0 +1,17 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getCSPNonce } from '@nextcloud/auth' +import { t } from '@nextcloud/l10n' +import Vue from 'vue' +import PersonalSettings from './views/SettingsAdmin.vue' + +// eslint-disable-next-line camelcase +__webpack_nonce__ = getCSPNonce() + +Vue.prototype.t = t +const View = Vue.extend(PersonalSettings) +const instance = new View() +instance.$mount('#files-admin-settings') diff --git a/apps/files/src/main-personal-settings.js b/apps/files/src/main-settings-personal.ts similarity index 92% rename from apps/files/src/main-personal-settings.js rename to apps/files/src/main-settings-personal.ts index dce190f0160..39e3645f6bb 100644 --- a/apps/files/src/main-personal-settings.js +++ b/apps/files/src/main-settings-personal.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import Vue from 'vue' import { getCSPNonce } from '@nextcloud/auth' - +import { t } from '@nextcloud/l10n' +import Vue from 'vue' import PersonalSettings from './components/PersonalSettings.vue' // eslint-disable-next-line camelcase diff --git a/apps/files/src/views/SettingsAdmin.vue b/apps/files/src/views/SettingsAdmin.vue new file mode 100644 index 00000000000..857a051044a --- /dev/null +++ b/apps/files/src/views/SettingsAdmin.vue @@ -0,0 +1,78 @@ + + + + + + + diff --git a/apps/files/templates/settings-admin.php b/apps/files/templates/settings-admin.php new file mode 100644 index 00000000000..ac6f683d60c --- /dev/null +++ b/apps/files/templates/settings-admin.php @@ -0,0 +1,8 @@ + +
diff --git a/webpack.modules.js b/webpack.modules.js index 38b03920fae..79ddf007771 100644 --- a/webpack.modules.js +++ b/webpack.modules.js @@ -42,7 +42,8 @@ module.exports = { main: path.join(__dirname, 'apps/files/src', 'main.ts'), init: path.join(__dirname, 'apps/files/src', 'init.ts'), search: path.join(__dirname, 'apps/files/src/plugins/search', 'folderSearch.ts'), - 'settings-personal': path.join(__dirname, 'apps/files/src', 'main-personal-settings.js'), + 'settings-admin': path.join(__dirname, 'apps/files/src', 'main-settings-admin.ts'), + 'settings-personal': path.join(__dirname, 'apps/files/src', 'main-settings-personal.ts'), 'reference-files': path.join(__dirname, 'apps/files/src', 'reference-files.ts'), }, files_external: {