mirror of
https://github.com/nextcloud/server.git
synced 2026-04-26 16:48:59 -04:00
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:
commit
9c3acefe53
41 changed files with 906 additions and 105 deletions
|
|
@ -59,6 +59,7 @@
|
|||
</commands>
|
||||
|
||||
<settings>
|
||||
<admin>OCA\Files\Settings\AdminSettings</admin>
|
||||
<personal>OCA\Files\Settings\PersonalSettings</personal>
|
||||
</settings>
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
224
apps/files/lib/BackgroundJob/SanitizeFilenames.php
Normal file
224
apps/files/lib/BackgroundJob/SanitizeFilenames.php
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
102
apps/files/lib/Controller/FilenamesController.php
Normal file
102
apps/files/lib/Controller/FilenamesController.php
Normal 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([]);
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
48
apps/files/lib/Settings/AdminSettings.php
Normal file
48
apps/files/lib/Settings/AdminSettings.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
182
apps/files/src/components/Settings/SettingsSanitizeFilenames.vue
Normal file
182
apps/files/src/components/Settings/SettingsSanitizeFilenames.vue
Normal 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>
|
||||
17
apps/files/src/main-settings-admin.ts
Normal file
17
apps/files/src/main-settings-admin.ts
Normal 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')
|
||||
|
|
@ -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
|
||||
15
apps/files/src/models/SanitizeFilenameStatus.ts
Normal file
15
apps/files/src/models/SanitizeFilenameStatus.ts
Normal 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,
|
||||
}
|
||||
78
apps/files/src/views/SettingsAdmin.vue
Normal file
78
apps/files/src/views/SettingsAdmin.vue
Normal 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>
|
||||
8
apps/files/templates/settings-admin.php
Normal file
8
apps/files/templates/settings-admin.php
Normal 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>
|
||||
4
dist/comments-comments-app.js
vendored
4
dist/comments-comments-app.js
vendored
File diff suppressed because one or more lines are too long
2
dist/comments-comments-app.js.map
vendored
2
dist/comments-comments-app.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-search.js
vendored
4
dist/files-search.js
vendored
|
|
@ -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
|
||||
2
dist/files-search.js.map
vendored
2
dist/files-search.js.map
vendored
File diff suppressed because one or more lines are too long
2
dist/files-settings-admin.js
vendored
Normal file
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
129
dist/files-settings-admin.js.license
vendored
Normal 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
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
1
dist/files-settings-admin.js.map.license
vendored
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
files-settings-admin.js.license
|
||||
4
dist/files-settings-personal.js
vendored
4
dist/files-settings-personal.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-settings-personal.js.map
vendored
2
dist/files-settings-personal.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files_sharing-personal-settings.js
vendored
4
dist/files_sharing-personal-settings.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-personal-settings.js.map
vendored
2
dist/files_sharing-personal-settings.js.map
vendored
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
|
|
@ -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
4
dist/weather_status-weather-status.js
vendored
4
dist/weather_status-weather-status.js
vendored
File diff suppressed because one or more lines are too long
2
dist/weather_status-weather-status.js.map
vendored
2
dist/weather_status-weather-status.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue