Merge pull request #54722 from nextcloud/feat/filename-sanitize-ui

feat(files): provide UI to sanitize filenames after enabling WCF
This commit is contained in:
Ferdinand Thiessen 2025-09-02 14:30:03 +02:00 committed by GitHub
commit 9c3acefe53
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 906 additions and 105 deletions

View file

@ -59,6 +59,7 @@
</commands>
<settings>
<admin>OCA\Files\Settings\AdminSettings</admin>
<personal>OCA\Files\Settings\PersonalSettings</personal>
</settings>

View file

@ -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',
);

View file

@ -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',
);

View file

@ -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);

View file

@ -0,0 +1,224 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\BackgroundJob;
use OC\Files\SetupManager;
use OCA\Files\AppInfo\Application;
use OCA\Files\Service\SettingsService;
use OCP\AppFramework\Services\IAppConfig;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\QueuedJob;
use OCP\Config\IUserConfig;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IFilenameValidator;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\IUser;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Lock\LockedException;
use Psr\Log\LoggerInterface;
class SanitizeFilenames extends QueuedJob {
private int $offset;
private int $limit;
private int $currentIndex;
private ?string $charReplacement = null;
public function __construct(
ITimeFactory $time,
private IJobList $jobList,
private IUserSession $session,
private IUserManager $manager,
private IAppConfig $appConfig,
private IUserConfig $userConfig,
private IRootFolder $rootFolder,
private SetupManager $setupManager,
private IFilenameValidator $filenameValidator,
private LoggerInterface $logger,
) {
parent::__construct($time);
$this->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<string> 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)
}
}
}

View file

@ -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;
}

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Controller;
use OCA\Files\BackgroundJob\SanitizeFilenames;
use OCA\Files\Service\SettingsService;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\Route;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Services\IAppConfig;
use OCP\BackgroundJob\IJobList;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserManager;
class FilenamesController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private IL10N $l10n,
private IJobList $jobList,
private IAppConfig $appConfig,
private IUserManager $userManager,
private SettingsService $settingsService,
) {
parent::__construct($appName, $request);
}
/**
* Toggle the Windows filename support feature.
*
* @param bool $enabled - The new state of the Windows filename support
* @return DataResponse
*/
#[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
#[Route(type: Route::TYPE_API, verb: 'POST', url: '/api/v1/filenames/windows-compatibility')]
public function toggleWindowFilenameSupport(bool $enabled): DataResponse {
$this->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([]);
}
}

View file

@ -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<string, string>}
*/
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<string, string> */
$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];
}
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Settings;
use OCA\Files\AppInfo\Application;
use OCA\Files\Service\SettingsService;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\ISettings;
use OCP\Util;
class AdminSettings implements ISettings {
public function __construct(
private IL10N $l,
private SettingsService $service,
private IURLGenerator $urlGenerator,
private IInitialState $initialState,
) {
}
public function getSection(): string {
return 'server';
}
public function getPriority(): int {
return 10;
}
public function getForm(): TemplateResponse {
$windowSupport = $this->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);
}
}

View file

@ -1,67 +0,0 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Files\Settings;
use OCA\Files\Service\SettingsService;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Settings\DeclarativeSettingsTypes;
use OCP\Settings\IDeclarativeSettingsFormWithHandlers;
class DeclarativeAdminSettings implements IDeclarativeSettingsFormWithHandlers {
public function __construct(
private IL10N $l,
private SettingsService $service,
private IURLGenerator $urlGenerator,
) {
}
public function getValue(string $fieldId, IUser $user): mixed {
return match($fieldId) {
'windows_support' => $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,
],
],
];
}
}

View file

@ -0,0 +1,182 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import type { OCSResponse } from '@nextcloud/typings/ocs'
import axios, { isAxiosError } from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { generateOcsUrl } from '@nextcloud/router'
import { computed, ref, shallowRef } from 'vue'
import NcButton from '@nextcloud/vue/components/NcButton'
import NcInputField from '@nextcloud/vue/components/NcInputField'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcProgressBar from '@nextcloud/vue/components/NcProgressBar'
import { SanitizeFilenameStatus } from '../../models/SanitizeFilenameStatus.ts'
import logger from '../../logger.ts'
type ApiStatus = { total: number, processed: number, errors?: Record<string, string[]>, status: SanitizeFilenameStatus }
const { status: initialStatus } = loadState<{ isRunningSanitization: boolean, status: ApiStatus }>('files', 'filesCompatibilitySettings')
const loading = ref(false)
const renameLimit = ref(10)
const status = ref(initialStatus.status)
const processedUsers = ref(initialStatus.processed)
const totalUsers = ref(initialStatus.total)
const errors = shallowRef<ApiStatus['errors']>(initialStatus.errors || {})
const progress = computed(() => processedUsers.value > 0 ? Math.round((processedUsers.value * 100) / totalUsers.value) : 0)
const isRunning = computed(() => status.value === SanitizeFilenameStatus.Scheduled || status.value === SanitizeFilenameStatus.Running)
/**
* Start the sanitization process
*/
async function startSanitization() {
if (isRunning.value) {
return
}
try {
loading.value = true
await axios.post(generateOcsUrl('apps/files/api/v1/filenames/sanitization'), {
limit: renameLimit.value,
})
status.value = SanitizeFilenameStatus.Scheduled
} catch (error) {
logger.error('Failed to start filename sanitization.', { error })
if (isAxiosError(error) && error.response?.data?.ocs) {
showError((error.response.data as OCSResponse).ocs.meta.message!)
} else {
showError(t('files', 'Failed to start filename sanitization.'))
}
} finally {
loading.value = false
}
}
/**
* Refresh the filename sanitization status
*/
async function refreshStatus() {
if (loading.value) {
return
}
try {
loading.value = true
const { data } = await axios.get<OCSResponse<ApiStatus>>(generateOcsUrl('apps/files/api/v1/filenames/sanitization'))
status.value = data.ocs.data.status
totalUsers.value = data.ocs.data.total
processedUsers.value = data.ocs.data.processed
errors.value = data.ocs.data.errors || {}
} catch (error) {
logger.error('Failed to refresh filename sanitization status.', { error })
showError(t('files', 'Failed to refresh filename sanitization status.'))
} finally {
loading.value = false
}
}
</script>
<template>
<NcNoteCard v-if="isRunning">
<div class="sanitize-filenames__progress-container">
<p>
{{ t('files', 'Filename sanitization in progress.') }}
<br>
<template v-if="processedUsers > 0">
{{ t('files', 'Currently {processedUsers} of {totalUsers} accounts are already processed.', { processedUsers, totalUsers }) }}
</template>
<template v-else>
{{ t('files', 'Preparing …') }}
</template>
</p>
<NcProgressBar :value="progress" :size="12" />
<NcButton variant="tertiary" @click="refreshStatus">
<template v-if="loading" #icon>
<NcLoadingIcon />
</template>
{{ t('files', 'Refresh') }}
</NcButton>
</div>
</NcNoteCard>
<NcNoteCard v-else-if="status === SanitizeFilenameStatus.Done" type="success">
{{ t('files', 'All files have been santized for Windows filename support.') }}
</NcNoteCard>
<form v-else
class="sanitize-filenames__form"
:disabled="loading"
@submit.stop.prevent="startSanitization">
<NcNoteCard v-if="status === SanitizeFilenameStatus.Error" type="error">
{{ t('files', 'Some files could not be sanitized, please check your logs.') }}
<ul class="sanitize-filenames__errors" :aria-label="t('files', 'Sanitization errors')">
<li v-for="[user, failedFiles] of Object.entries(errors)" :key="user">
<h4>{{ user }}:</h4>
<ul :aria-label="t('files', 'Not sanitized filenames')">
<li v-for="file of failedFiles" :key="file">
{{ file }}
</li>
</ul>
</li>
</ul>
</NcNoteCard>
<NcNoteCard>
{{ t('files', 'Windows filename support has been enabled.') }}
<br>
{{ t('files', 'While this blocks users from creating new files with unsupported filenames, existing files are not yet renamed and thus still may break sync on Windows.') }}
{{ t('files', 'You can trigger a rename of files with invalid filenames, this will be done in the background and may take some time.') }}
{{ t('files', 'Please note that this may cause high workload on the sync clients.') }}
</NcNoteCard>
<fieldset class="sanitize-filenames__fields">
<NcInputField v-model="renameLimit"
:label="t('files', 'Limit')"
:helper-text="t('files', 'This allows to configure how many users should be processed in one background job run.')"
min="1"
type="number" />
<NcButton type="submit" variant="error">
<template v-if="loading" #icon>
<NcLoadingIcon />
</template>
{{ t('files', 'Sanitize filenames') }}
<span v-if="loading" class="hidden-visually">
{{ t('files', '(starting)') }}
</span>
</NcButton>
</fieldset>
</form>
</template>
<style scoped>
.sanitize-filenames__progress-container {
align-items: end;
display: flex;
flex-direction: column;
gap: var(--default-grid-baseline);
}
.sanitize-filenames__form {
display: flex;
flex-direction: column;
gap: var(--default-grid-baseline);
}
.sanitize-filenames__fields {
display: flex;
flex-direction: column;
gap: var(--default-grid-baseline);
align-items: end;
max-width: 400px;
}
</style>

View file

@ -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')

View file

@ -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

View file

@ -0,0 +1,15 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
/**
* The current status of the filename sanitization
*/
export enum SanitizeFilenameStatus {
Unknown = 0,
Scheduled = 1,
Running = 2,
Done = 3,
Error = 4,
}

View file

@ -0,0 +1,78 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="ts">
import axios from '@nextcloud/axios'
import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { generateOcsUrl } from '@nextcloud/router'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import SettingsSanitizeFilenames from '../components/Settings/SettingsSanitizeFilenames.vue'
import { ref } from 'vue'
import logger from '../logger'
const {
docUrl,
isRunningSanitization,
windowsSupport,
} = loadState<{ docUrl: string, isRunningSanitization: boolean, windowsSupport: boolean }>('files', 'filesCompatibilitySettings')
const description = t('files', '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' + t('files', 'After enabling the Windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner.')
const loading = ref(false)
const hasWindowsSupport = ref(windowsSupport)
/**
* Toggle the Windows filename support on the backend.
*
* @param enabled - The new state to be set
*/
async function toggleWindowsFilenameSupport(enabled: boolean) {
if (loading.value) {
return
}
try {
loading.value = true
await axios.post(generateOcsUrl('apps/files/api/v1/filenames/windows-compatibility'), { enabled })
hasWindowsSupport.value = enabled
} catch (error) {
showError(t('files', 'Failed to toggle Windows filename support'))
logger.error('Failed to toggle Windows filename support', { error })
} finally {
loading.value = false
}
}
</script>
<template>
<NcSettingsSection :doc-url="docUrl"
:name="t('files', 'Files compatibility')"
:description="description">
<NcCheckboxRadioSwitch :model-value="hasWindowsSupport"
:disabled="isRunningSanitization"
:loading="loading"
type="switch"
@update:model-value="toggleWindowsFilenameSupport">
{{ t('files', 'Enforce Windows compatibility') }}
</NcCheckboxRadioSwitch>
<p class="hint">
{{ t('files', '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.') }}
</p>
<SettingsSanitizeFilenames v-if="hasWindowsSupport" />
</NcSettingsSection>
</template>
<style scoped>
.hint {
color: var(--color-text-maxcontrast);
margin-inline-start: var(--border-radius-element);
margin-block-end: 1em;
}
</style>

View file

@ -0,0 +1,8 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
?>
<div id="files-admin-settings"></div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

4
dist/core-common.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
(()=>{"use strict";var e,r,t,i={97986:(e,r,t)=>{var i=t(61338),o=t(85168),a=t(63814),n=t(53334);const l=(0,t(35947).YK)().setApp("files").detectUser().build();document.addEventListener("DOMContentLoaded",function(){const e=window.OCA;e.UnifiedSearch&&(l.info("Initializing unified search plugin: folder search from files app"),e.UnifiedSearch.registerFilterAction({id:"in-folder",appId:"files",searchFrom:"files",label:(0,n.Tl)("files","In folder"),icon:(0,a.d0)("files","app.svg"),callback:function(){arguments.length>0&&void 0!==arguments[0]&&!arguments[0]?l.debug("Folder search callback was handled without showing the file picker, it might already be open"):(0,o.a1)("Pick plain text files").addMimeTypeFilter("httpd/unix-directory").allowDirectories(!0).addButton({label:"Pick",callback:e=>{l.info("Folder picked",{folder:e[0]});const r=e[0],t=r.root==="/files/"+r.basename?(0,n.Tl)("files","Search in all files"):(0,n.Tl)("files","Search in folder: {folder}",{folder:r.basename});(0,i.Ic)("nextcloud:unified-search:add-filter",{id:"in-folder",appId:"files",searchFrom:"files",payload:r,filterUpdateText:t,filterParams:{path:r.path}})}}).build().pick()}}))})}},o={};function a(e){var r=o[e];if(void 0!==r)return r.exports;var t=o[e]={id:e,loaded:!1,exports:{}};return i[e].call(t.exports,t,t.exports,a),t.loaded=!0,t.exports}a.m=i,e=[],a.O=(r,t,i,o)=>{if(!t){var n=1/0;for(s=0;s<e.length;s++){t=e[s][0],i=e[s][1],o=e[s][2];for(var l=!0,d=0;d<t.length;d++)(!1&o||n>=o)&&Object.keys(a.O).every(e=>a.O[e](t[d]))?t.splice(d--,1):(l=!1,o<n&&(n=o));if(l){e.splice(s--,1);var c=i();void 0!==c&&(r=c)}}return r}o=o||0;for(var s=e.length;s>0&&e[s-1][2]>o;s--)e[s]=e[s-1];e[s]=[t,i,o]},a.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return a.d(r,{a:r}),r},a.d=(e,r)=>{for(var t in r)a.o(r,t)&&!a.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((r,t)=>(a.f[t](e,r),r),[])),a.u=e=>e+"-"+e+".js?v="+{640:"d3d98600d88fd55c7b27",5771:"d141d1ad8187d99738b9",5810:"fc51f8aa95a9854d22fd",7471:"6423b9b898ffefeb7d1d",8474:"d060bb2e97b1499bd6b0"}[e],a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",a.l=(e,i,o,n)=>{if(r[e])r[e].push(i);else{var l,d;if(void 0!==o)for(var c=document.getElementsByTagName("script"),s=0;s<c.length;s++){var f=c[s];if(f.getAttribute("src")==e||f.getAttribute("data-webpack")==t+o){l=f;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",t+o),l.src=e),r[e]=[i];var u=(t,i)=>{l.onerror=l.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),o&&o.forEach(e=>e(i)),t)return t(i)},p=setTimeout(u.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=u.bind(null,l.onerror),l.onload=u.bind(null,l.onload),d&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=2277,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var r=a.g.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var i=t.length-1;i>-1&&(!e||!/^http(s?):/.test(e));)e=t[i--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={2277:0};a.f.j=(r,t)=>{var i=a.o(e,r)?e[r]:void 0;if(0!==i)if(i)t.push(i[2]);else{var o=new Promise((t,o)=>i=e[r]=[t,o]);t.push(i[2]=o);var n=a.p+a.u(r),l=new Error;a.l(n,t=>{if(a.o(e,r)&&(0!==(i=e[r])&&(e[r]=void 0),i)){var o=t&&("load"===t.type?"missing":t.type),n=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+o+": "+n+")",l.name="ChunkLoadError",l.type=o,l.request=n,i[1](l)}},"chunk-"+r,r)}},a.O.j=r=>0===e[r];var r=(r,t)=>{var i,o,n=t[0],l=t[1],d=t[2],c=0;if(n.some(r=>0!==e[r])){for(i in l)a.o(l,i)&&(a.m[i]=l[i]);if(d)var s=d(a)}for(r&&r(t);c<n.length;c++)o=n[c],a.o(e,o)&&e[o]&&e[o][0](),e[o]=0;return a.O(s)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),a.nc=void 0;var n=a.O(void 0,[4208],()=>a(97986));n=a.O(n)})();
//# sourceMappingURL=files-search.js.map?v=5f1f6590e5c1e2fa57d5
(()=>{"use strict";var e,r,t,i={97986:(e,r,t)=>{var i=t(61338),a=t(85168),o=t(63814),n=t(53334);const l=(0,t(35947).YK)().setApp("files").detectUser().build();document.addEventListener("DOMContentLoaded",function(){const e=window.OCA;e.UnifiedSearch&&(l.info("Initializing unified search plugin: folder search from files app"),e.UnifiedSearch.registerFilterAction({id:"in-folder",appId:"files",searchFrom:"files",label:(0,n.Tl)("files","In folder"),icon:(0,o.d0)("files","app.svg"),callback:function(){arguments.length>0&&void 0!==arguments[0]&&!arguments[0]?l.debug("Folder search callback was handled without showing the file picker, it might already be open"):(0,a.a1)("Pick plain text files").addMimeTypeFilter("httpd/unix-directory").allowDirectories(!0).addButton({label:"Pick",callback:e=>{l.info("Folder picked",{folder:e[0]});const r=e[0],t=r.root==="/files/"+r.basename?(0,n.Tl)("files","Search in all files"):(0,n.Tl)("files","Search in folder: {folder}",{folder:r.basename});(0,i.Ic)("nextcloud:unified-search:add-filter",{id:"in-folder",appId:"files",searchFrom:"files",payload:r,filterUpdateText:t,filterParams:{path:r.path}})}}).build().pick()}}))})}},a={};function o(e){var r=a[e];if(void 0!==r)return r.exports;var t=a[e]={id:e,loaded:!1,exports:{}};return i[e].call(t.exports,t,t.exports,o),t.loaded=!0,t.exports}o.m=i,e=[],o.O=(r,t,i,a)=>{if(!t){var n=1/0;for(s=0;s<e.length;s++){t=e[s][0],i=e[s][1],a=e[s][2];for(var l=!0,d=0;d<t.length;d++)(!1&a||n>=a)&&Object.keys(o.O).every(e=>o.O[e](t[d]))?t.splice(d--,1):(l=!1,a<n&&(n=a));if(l){e.splice(s--,1);var c=i();void 0!==c&&(r=c)}}return r}a=a||0;for(var s=e.length;s>0&&e[s-1][2]>a;s--)e[s]=e[s-1];e[s]=[t,i,a]},o.n=e=>{var r=e&&e.__esModule?()=>e.default:()=>e;return o.d(r,{a:r}),r},o.d=(e,r)=>{for(var t in r)o.o(r,t)&&!o.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce((r,t)=>(o.f[t](e,r),r),[])),o.u=e=>e+"-"+e+".js?v="+{640:"d3d98600d88fd55c7b27",5771:"d141d1ad8187d99738b9",5810:"fc51f8aa95a9854d22fd",7471:"6423b9b898ffefeb7d1d",8474:"d060bb2e97b1499bd6b0"}[e],o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),r={},t="nextcloud:",o.l=(e,i,a,n)=>{if(r[e])r[e].push(i);else{var l,d;if(void 0!==a)for(var c=document.getElementsByTagName("script"),s=0;s<c.length;s++){var f=c[s];if(f.getAttribute("src")==e||f.getAttribute("data-webpack")==t+a){l=f;break}}l||(d=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,o.nc&&l.setAttribute("nonce",o.nc),l.setAttribute("data-webpack",t+a),l.src=e),r[e]=[i];var u=(t,i)=>{l.onerror=l.onload=null,clearTimeout(p);var a=r[e];if(delete r[e],l.parentNode&&l.parentNode.removeChild(l),a&&a.forEach(e=>e(i)),t)return t(i)},p=setTimeout(u.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=u.bind(null,l.onerror),l.onload=u.bind(null,l.onload),d&&document.head.appendChild(l)}},o.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},o.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),o.j=2277,(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var r=o.g.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var i=t.length-1;i>-1&&(!e||!/^http(s?):/.test(e));)e=t[i--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{o.b=document.baseURI||self.location.href;var e={2277:0};o.f.j=(r,t)=>{var i=o.o(e,r)?e[r]:void 0;if(0!==i)if(i)t.push(i[2]);else{var a=new Promise((t,a)=>i=e[r]=[t,a]);t.push(i[2]=a);var n=o.p+o.u(r),l=new Error;o.l(n,t=>{if(o.o(e,r)&&(0!==(i=e[r])&&(e[r]=void 0),i)){var a=t&&("load"===t.type?"missing":t.type),n=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+a+": "+n+")",l.name="ChunkLoadError",l.type=a,l.request=n,i[1](l)}},"chunk-"+r,r)}},o.O.j=r=>0===e[r];var r=(r,t)=>{var i,a,n=t[0],l=t[1],d=t[2],c=0;if(n.some(r=>0!==e[r])){for(i in l)o.o(l,i)&&(o.m[i]=l[i]);if(d)var s=d(o)}for(r&&r(t);c<n.length;c++)a=n[c],o.o(e,a)&&e[a]&&e[a][0](),e[a]=0;return o.O(s)},t=self.webpackChunknextcloud=self.webpackChunknextcloud||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),o.nc=void 0;var n=o.O(void 0,[4208],()=>o(97986));n=o.O(n)})();
//# sourceMappingURL=files-search.js.map?v=9196d720220fe120311e

File diff suppressed because one or more lines are too long

2
dist/files-settings-admin.js vendored Normal file

File diff suppressed because one or more lines are too long

129
dist/files-settings-admin.js.license vendored Normal file
View file

@ -0,0 +1,129 @@
SPDX-License-Identifier: MIT
SPDX-License-Identifier: ISC
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-License-Identifier: BSD-3-Clause
SPDX-License-Identifier: AGPL-3.0-or-later
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
SPDX-FileCopyrightText: inherits developers
SPDX-FileCopyrightText: escape-html developers
SPDX-FileCopyrightText: Varun A P
SPDX-FileCopyrightText: Tobias Koppers @sokra
SPDX-FileCopyrightText: T. Jameson Little <t.jameson.little@gmail.com>
SPDX-FileCopyrightText: Roman Shtylman <shtylman@gmail.com>
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
SPDX-FileCopyrightText: Matt Zabriskie
SPDX-FileCopyrightText: Joyent
SPDX-FileCopyrightText: Guillaume Chau <guillaume.b.chau@gmail.com>
SPDX-FileCopyrightText: GitHub Inc.
SPDX-FileCopyrightText: Feross Aboukhadijeh
SPDX-FileCopyrightText: Evan You
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
SPDX-FileCopyrightText: David Clark
SPDX-FileCopyrightText: Christoph Wurst
SPDX-FileCopyrightText: Anthony Fu <https://github.com/antfu>
SPDX-FileCopyrightText: @nextcloud/dialogs developers
This file is generated from multiple sources. Included packages:
- @nextcloud/auth
- version: 2.5.2
- license: GPL-3.0-or-later
- @nextcloud/axios
- version: 2.5.1
- license: GPL-3.0-or-later
- @nextcloud/browser-storage
- version: 0.4.0
- license: GPL-3.0-or-later
- @nextcloud/dialogs
- version: 6.3.1
- license: AGPL-3.0-or-later
- semver
- version: 7.7.2
- license: ISC
- @nextcloud/event-bus
- version: 3.3.2
- license: GPL-3.0-or-later
- @nextcloud/initial-state
- version: 3.0.0
- license: GPL-3.0-or-later
- @nextcloud/l10n
- version: 3.4.0
- license: GPL-3.0-or-later
- @nextcloud/logger
- version: 3.0.2
- license: GPL-3.0-or-later
- @nextcloud/router
- version: 3.0.1
- license: GPL-3.0-or-later
- @nextcloud/initial-state
- version: 2.2.0
- license: GPL-3.0-or-later
- @nextcloud/vue
- version: 8.29.2
- license: AGPL-3.0-or-later
- @vueuse/core
- version: 11.3.0
- license: MIT
- @vueuse/shared
- version: 11.3.0
- license: MIT
- axios
- version: 1.11.0
- license: MIT
- base64-js
- version: 1.5.1
- license: MIT
- css-loader
- version: 7.1.2
- license: MIT
- dompurify
- version: 3.2.6
- license: (MPL-2.0 OR Apache-2.0)
- escape-html
- version: 1.0.3
- license: MIT
- floating-vue
- version: 1.0.0-beta.19
- license: MIT
- focus-trap
- version: 7.6.5
- license: MIT
- ieee754
- version: 1.2.1
- license: BSD-3-Clause
- buffer
- version: 6.0.3
- license: MIT
- inherits
- version: 2.0.3
- license: ISC
- util
- version: 0.10.4
- license: MIT
- path
- version: 0.12.7
- license: MIT
- process
- version: 0.11.10
- license: MIT
- style-loader
- version: 4.0.0
- license: MIT
- tabbable
- version: 6.2.0
- license: MIT
- toastify-js
- version: 1.12.0
- license: MIT
- vue-loader
- version: 15.11.1
- license: MIT
- vue
- version: 2.7.16
- license: MIT
- webpack
- version: 5.101.3
- license: MIT
- nextcloud
- version: 1.0.0
- license: AGPL-3.0-or-later

1
dist/files-settings-admin.js.map vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/files-settings-admin.js.map.license vendored Symbolic link
View file

@ -0,0 +1 @@
files-settings-admin.js.license

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
(()=>{"use strict";var e,t,r,o={3360:(e,t,r)=>{var o=r(85168),n=r(81222),a=r(53334);window.addEventListener("DOMContentLoaded",function(){const{updateLink:e,updateVersion:t}=(0,n.C)("updatenotification","updateState"),r=(0,a.t)("core","{version} is available. Get more information on how to update.",{version:t});(0,o.cf)(r,{onClick:()=>window.open(e,"_blank")})})}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(r.exports,r,r.exports,a),r.loaded=!0,r.exports}a.m=o,e=[],a.O=(t,r,o,n)=>{if(!r){var i=1/0;for(u=0;u<e.length;u++){r=e[u][0],o=e[u][1],n=e[u][2];for(var l=!0,c=0;c<r.length;c++)(!1&n||i>=n)&&Object.keys(a.O).every(e=>a.O[e](r[c]))?r.splice(c--,1):(l=!1,n<i&&(i=n));if(l){e.splice(u--,1);var d=o();void 0!==d&&(t=d)}}return t}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[r,o,n]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((t,r)=>(a.f[r](e,t),t),[])),a.u=e=>e+"-"+e+".js?v="+{640:"d3d98600d88fd55c7b27",5771:"d141d1ad8187d99738b9",5810:"fc51f8aa95a9854d22fd",7471:"6423b9b898ffefeb7d1d",8474:"d060bb2e97b1499bd6b0"}[e],a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",a.l=(e,o,n,i)=>{if(t[e])t[e].push(o);else{var l,c;if(void 0!==n)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==r+n){l=s;break}}l||(c=!0,(l=document.createElement("script")).charset="utf-8",l.timeout=120,a.nc&&l.setAttribute("nonce",a.nc),l.setAttribute("data-webpack",r+n),l.src=e),t[e]=[o];var p=(r,o)=>{l.onerror=l.onload=null,clearTimeout(f);var n=t[e];if(delete t[e],l.parentNode&&l.parentNode.removeChild(l),n&&n.forEach(e=>e(o)),r)return r(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=p.bind(null,l.onerror),l.onload=p.bind(null,l.onload),c&&document.head.appendChild(l)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=5169,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var t=a.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var o=r.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=r[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={5169:0};a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise((r,n)=>o=e[t]=[r,n]);r.push(o[2]=n);var i=a.p+a.u(t),l=new Error;a.l(i,r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;l.message="Loading chunk "+t+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,o[1](l)}},"chunk-"+t,t)}},a.O.j=t=>0===e[t];var t=(t,r)=>{var o,n,i=r[0],l=r[1],c=r[2],d=0;if(i.some(t=>0!==e[t])){for(o in l)a.o(l,o)&&(a.m[o]=l[o]);if(c)var u=c(a)}for(t&&t(r);d<i.length;d++)n=i[d],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),a.nc=void 0;var i=a.O(void 0,[4208],()=>a(3360));i=a.O(i)})();
//# sourceMappingURL=updatenotification-update-notification-legacy.js.map?v=17a1b95d9b8713605754
(()=>{"use strict";var e,t,r,o={3360:(e,t,r)=>{var o=r(85168),n=r(81222),a=r(53334);window.addEventListener("DOMContentLoaded",function(){const{updateLink:e,updateVersion:t}=(0,n.C)("updatenotification","updateState"),r=(0,a.t)("core","{version} is available. Get more information on how to update.",{version:t});(0,o.cf)(r,{onClick:()=>window.open(e,"_blank")})})}},n={};function a(e){var t=n[e];if(void 0!==t)return t.exports;var r=n[e]={id:e,loaded:!1,exports:{}};return o[e].call(r.exports,r,r.exports,a),r.loaded=!0,r.exports}a.m=o,e=[],a.O=(t,r,o,n)=>{if(!r){var i=1/0;for(u=0;u<e.length;u++){r=e[u][0],o=e[u][1],n=e[u][2];for(var c=!0,l=0;l<r.length;l++)(!1&n||i>=n)&&Object.keys(a.O).every(e=>a.O[e](r[l]))?r.splice(l--,1):(c=!1,n<i&&(i=n));if(c){e.splice(u--,1);var d=o();void 0!==d&&(t=d)}}return t}n=n||0;for(var u=e.length;u>0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[r,o,n]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.f={},a.e=e=>Promise.all(Object.keys(a.f).reduce((t,r)=>(a.f[r](e,t),t),[])),a.u=e=>e+"-"+e+".js?v="+{640:"d3d98600d88fd55c7b27",5771:"d141d1ad8187d99738b9",5810:"fc51f8aa95a9854d22fd",7471:"6423b9b898ffefeb7d1d",8474:"d060bb2e97b1499bd6b0"}[e],a.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),t={},r="nextcloud:",a.l=(e,o,n,i)=>{if(t[e])t[e].push(o);else{var c,l;if(void 0!==n)for(var d=document.getElementsByTagName("script"),u=0;u<d.length;u++){var s=d[u];if(s.getAttribute("src")==e||s.getAttribute("data-webpack")==r+n){c=s;break}}c||(l=!0,(c=document.createElement("script")).charset="utf-8",c.timeout=120,a.nc&&c.setAttribute("nonce",a.nc),c.setAttribute("data-webpack",r+n),c.src=e),t[e]=[o];var p=(r,o)=>{c.onerror=c.onload=null,clearTimeout(f);var n=t[e];if(delete t[e],c.parentNode&&c.parentNode.removeChild(c),n&&n.forEach(e=>e(o)),r)return r(o)},f=setTimeout(p.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=p.bind(null,c.onerror),c.onload=p.bind(null,c.onload),l&&document.head.appendChild(c)}},a.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),a.j=5169,(()=>{var e;a.g.importScripts&&(e=a.g.location+"");var t=a.g.document;if(!e&&t&&(t.currentScript&&"SCRIPT"===t.currentScript.tagName.toUpperCase()&&(e=t.currentScript.src),!e)){var r=t.getElementsByTagName("script");if(r.length)for(var o=r.length-1;o>-1&&(!e||!/^http(s?):/.test(e));)e=r[o--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),a.p=e})(),(()=>{a.b=document.baseURI||self.location.href;var e={5169:0};a.f.j=(t,r)=>{var o=a.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else{var n=new Promise((r,n)=>o=e[t]=[r,n]);r.push(o[2]=n);var i=a.p+a.u(t),c=new Error;a.l(i,r=>{if(a.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var n=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;c.message="Loading chunk "+t+" failed.\n("+n+": "+i+")",c.name="ChunkLoadError",c.type=n,c.request=i,o[1](c)}},"chunk-"+t,t)}},a.O.j=t=>0===e[t];var t=(t,r)=>{var o,n,i=r[0],c=r[1],l=r[2],d=0;if(i.some(t=>0!==e[t])){for(o in c)a.o(c,o)&&(a.m[o]=c[o]);if(l)var u=l(a)}for(t&&t(r);d<i.length;d++)n=i[d],a.o(e,n)&&e[n]&&e[n][0](),e[n]=0;return a.O(u)},r=self.webpackChunknextcloud=self.webpackChunknextcloud||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))})(),a.nc=void 0;var i=a.O(void 0,[4208],()=>a(3360));i=a.O(i)})();
//# sourceMappingURL=updatenotification-update-notification-legacy.js.map?v=1fa87738e0b6c7f3eacd

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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: {