Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59871af530 |
@@ -162,7 +162,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
|
||||
throw new BadRequest('Missing "name" attribute');
|
||||
}
|
||||
|
||||
$tagName = $data['name'];
|
||||
$tagName = Util::sanitizeWordsAndEmojis($data['name']);
|
||||
$userVisible = true;
|
||||
$userAssignable = true;
|
||||
|
||||
|
||||
@@ -277,9 +277,6 @@ OC.L10N.register(
|
||||
"Failed to convert files: {message}" : "Fehler beim Konvertieren der Dateien: {message}",
|
||||
"All files failed to be converted" : "Alle Dateien konnten nicht konvertiert werden",
|
||||
"One file could not be converted: {message}" : "Eine Datei konnte nicht konvertiert werden: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n Datei konnte nicht konvertiert werden","%n Dateien konnten nicht konvertiert werden"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n Datei konvertiert","%n Dateien konvertiert"],
|
||||
"Files converted" : "Dateien konvertiert",
|
||||
"Failed to convert files" : "Dateien konnten nicht konvertiert werden",
|
||||
"Converting file …" : "Datei wird konvertiert …",
|
||||
"File successfully converted" : "Datei konvertiert",
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
"Failed to convert files: {message}" : "Fehler beim Konvertieren der Dateien: {message}",
|
||||
"All files failed to be converted" : "Alle Dateien konnten nicht konvertiert werden",
|
||||
"One file could not be converted: {message}" : "Eine Datei konnte nicht konvertiert werden: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n Datei konnte nicht konvertiert werden","%n Dateien konnten nicht konvertiert werden"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n Datei konvertiert","%n Dateien konvertiert"],
|
||||
"Files converted" : "Dateien konvertiert",
|
||||
"Failed to convert files" : "Dateien konnten nicht konvertiert werden",
|
||||
"Converting file …" : "Datei wird konvertiert …",
|
||||
"File successfully converted" : "Datei konvertiert",
|
||||
|
||||
@@ -277,9 +277,6 @@ OC.L10N.register(
|
||||
"Failed to convert files: {message}" : "Fehler beim Konvertieren der Dateien: {message}",
|
||||
"All files failed to be converted" : "Alle Dateien konnten nicht konvertiert werden",
|
||||
"One file could not be converted: {message}" : "Eine Datei konnte nicht konvertiert werden: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n Datei konnte nicht konvertiert werden","%n Dateien konnten nicht konvertiert werden"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n Datei konvertiert","%n Dateien konvertiert"],
|
||||
"Files converted" : "Dateien konvertiert",
|
||||
"Failed to convert files" : "Dateien konnten nicht konvertiert werden",
|
||||
"Converting file …" : "Datei wird konvertiert …",
|
||||
"File successfully converted" : "Datei konvertiert",
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
"Failed to convert files: {message}" : "Fehler beim Konvertieren der Dateien: {message}",
|
||||
"All files failed to be converted" : "Alle Dateien konnten nicht konvertiert werden",
|
||||
"One file could not be converted: {message}" : "Eine Datei konnte nicht konvertiert werden: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n Datei konnte nicht konvertiert werden","%n Dateien konnten nicht konvertiert werden"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n Datei konvertiert","%n Dateien konvertiert"],
|
||||
"Files converted" : "Dateien konvertiert",
|
||||
"Failed to convert files" : "Dateien konnten nicht konvertiert werden",
|
||||
"Converting file …" : "Datei wird konvertiert …",
|
||||
"File successfully converted" : "Datei konvertiert",
|
||||
|
||||
@@ -277,9 +277,6 @@ OC.L10N.register(
|
||||
"Failed to convert files: {message}" : "Failide teisendamine ei õnnestunud: {message}",
|
||||
"All files failed to be converted" : "Kõiki faile ei õnnestunud teisendada",
|
||||
"One file could not be converted: {message}" : "Ühe faili teisendamine ei õnnestunud: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n faili teisendamine ei õnnestunud","%n faili teisendamine ei õnnestunud"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n fail on teisendatud","%n faili on teisendatud"],
|
||||
"Files converted" : "Failid on teisendatud",
|
||||
"Failed to convert files" : "Failide teisendamine ei õnnestunud",
|
||||
"Converting file …" : "Teisendan faile…",
|
||||
"File successfully converted" : "Faili teisendamine õnnestus",
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
"Failed to convert files: {message}" : "Failide teisendamine ei õnnestunud: {message}",
|
||||
"All files failed to be converted" : "Kõiki faile ei õnnestunud teisendada",
|
||||
"One file could not be converted: {message}" : "Ühe faili teisendamine ei õnnestunud: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n faili teisendamine ei õnnestunud","%n faili teisendamine ei õnnestunud"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n fail on teisendatud","%n faili on teisendatud"],
|
||||
"Files converted" : "Failid on teisendatud",
|
||||
"Failed to convert files" : "Failide teisendamine ei õnnestunud",
|
||||
"Converting file …" : "Teisendan faile…",
|
||||
"File successfully converted" : "Faili teisendamine õnnestus",
|
||||
|
||||
@@ -277,9 +277,6 @@ OC.L10N.register(
|
||||
"Failed to convert files: {message}" : "Impossible de convertir les fichiers : {message}",
|
||||
"All files failed to be converted" : "Aucun fichier n'a pu être converti",
|
||||
"One file could not be converted: {message}" : "Le fichier {message} n'a pas pu être converti",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["Un fichier n'a pas pu être converti","%n fichiers n'ont pas pu être convertis","%n fichiers n'ont pas pu être convertis"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n fichier converti","%n fichiers convertis","%n fichiers convertis"],
|
||||
"Files converted" : "Fichiers convertis",
|
||||
"Failed to convert files" : "Impossible de convertir les fichiers",
|
||||
"Converting file …" : "Conversion du fichier …",
|
||||
"File successfully converted" : "Fichier converti avec succès",
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
"Failed to convert files: {message}" : "Impossible de convertir les fichiers : {message}",
|
||||
"All files failed to be converted" : "Aucun fichier n'a pu être converti",
|
||||
"One file could not be converted: {message}" : "Le fichier {message} n'a pas pu être converti",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["Un fichier n'a pas pu être converti","%n fichiers n'ont pas pu être convertis","%n fichiers n'ont pas pu être convertis"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n fichier converti","%n fichiers convertis","%n fichiers convertis"],
|
||||
"Files converted" : "Fichiers convertis",
|
||||
"Failed to convert files" : "Impossible de convertir les fichiers",
|
||||
"Converting file …" : "Conversion du fichier …",
|
||||
"File successfully converted" : "Fichier converti avec succès",
|
||||
|
||||
@@ -277,9 +277,6 @@ OC.L10N.register(
|
||||
"Failed to convert files: {message}" : "Produciuse un fallo ao converter os ficheiros: {message}",
|
||||
"All files failed to be converted" : "Non foi posíbel converter ningún ficheiro",
|
||||
"One file could not be converted: {message}" : "Non foi posíbel converter un ficheiro: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["Non foi posíbel converter %n ficheiro","Non foi posíbel converter %n ficheiros"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n ficheiro convertido","%n ficheiros convertidos"],
|
||||
"Files converted" : "Ficheiros convertidos",
|
||||
"Failed to convert files" : "Produciuse un fallo ao converter os ficheiros",
|
||||
"Converting file …" : "Convertendo o ficheiro…",
|
||||
"File successfully converted" : "O ficheiro foi convertido correctamente",
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
"Failed to convert files: {message}" : "Produciuse un fallo ao converter os ficheiros: {message}",
|
||||
"All files failed to be converted" : "Non foi posíbel converter ningún ficheiro",
|
||||
"One file could not be converted: {message}" : "Non foi posíbel converter un ficheiro: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["Non foi posíbel converter %n ficheiro","Non foi posíbel converter %n ficheiros"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n ficheiro convertido","%n ficheiros convertidos"],
|
||||
"Files converted" : "Ficheiros convertidos",
|
||||
"Failed to convert files" : "Produciuse un fallo ao converter os ficheiros",
|
||||
"Converting file …" : "Convertendo o ficheiro…",
|
||||
"File successfully converted" : "O ficheiro foi convertido correctamente",
|
||||
|
||||
@@ -277,9 +277,6 @@ OC.L10N.register(
|
||||
"Failed to convert files: {message}" : "Неуспешно конвертирање датотеки: {message}",
|
||||
"All files failed to be converted" : "Сите датотеки неуспешно се конвертирани",
|
||||
"One file could not be converted: {message}" : "Една датотека не може да се конвертира: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n датотека не може да се конвертира","%n датотеки не можеа да се конвертираат"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n датотека е конвертирана","%n датотеки се конвертирани"],
|
||||
"Files converted" : "Конвертирани датотеки",
|
||||
"Failed to convert files" : "Неуспешно конвертирање датотеки",
|
||||
"Converting file …" : "Конвертирање датотека …",
|
||||
"File successfully converted" : "Датотеката е успешно конвертирана",
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
"Failed to convert files: {message}" : "Неуспешно конвертирање датотеки: {message}",
|
||||
"All files failed to be converted" : "Сите датотеки неуспешно се конвертирани",
|
||||
"One file could not be converted: {message}" : "Една датотека не може да се конвертира: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n датотека не може да се конвертира","%n датотеки не можеа да се конвертираат"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n датотека е конвертирана","%n датотеки се конвертирани"],
|
||||
"Files converted" : "Конвертирани датотеки",
|
||||
"Failed to convert files" : "Неуспешно конвертирање датотеки",
|
||||
"Converting file …" : "Конвертирање датотека …",
|
||||
"File successfully converted" : "Датотеката е успешно конвертирана",
|
||||
|
||||
@@ -277,9 +277,6 @@ OC.L10N.register(
|
||||
"Failed to convert files: {message}" : "Није успела конверзија фајлова: {message}",
|
||||
"All files failed to be converted" : "Ниједан фајл није могао да се конвертује",
|
||||
"One file could not be converted: {message}" : "Један фајл није могао да се конвертује: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : [" %n фајл није могао да се конвертује"," %n фајла нису могла да се конвертују"," %n фајлова није могло да се конвертује"],
|
||||
"_%n file converted_::_%n files converted_" : ["конветован је %n фајл","конветована су %n фајла","конветовано је %n фајлова"],
|
||||
"Files converted" : "Конвертовано фајлова",
|
||||
"Failed to convert files" : "Није успела конверзија фајлова",
|
||||
"Converting file …" : "Фајл се конвертује …",
|
||||
"File successfully converted" : "Фајл је успешно конвертован",
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
"Failed to convert files: {message}" : "Није успела конверзија фајлова: {message}",
|
||||
"All files failed to be converted" : "Ниједан фајл није могао да се конвертује",
|
||||
"One file could not be converted: {message}" : "Један фајл није могао да се конвертује: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : [" %n фајл није могао да се конвертује"," %n фајла нису могла да се конвертују"," %n фајлова није могло да се конвертује"],
|
||||
"_%n file converted_::_%n files converted_" : ["конветован је %n фајл","конветована су %n фајла","конветовано је %n фајлова"],
|
||||
"Files converted" : "Конвертовано фајлова",
|
||||
"Failed to convert files" : "Није успела конверзија фајлова",
|
||||
"Converting file …" : "Фајл се конвертује …",
|
||||
"File successfully converted" : "Фајл је успешно конвертован",
|
||||
|
||||
@@ -277,9 +277,6 @@ OC.L10N.register(
|
||||
"Failed to convert files: {message}" : "Kunde inte konvertera filer: {message}",
|
||||
"All files failed to be converted" : "Alla filer misslyckades med att konverteras",
|
||||
"One file could not be converted: {message}" : "En fil kunde inte konverteras: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n fil kunde inte konverteras","%n filer kunde inte konverteras"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n fil konverterad","%n filer konverterade"],
|
||||
"Files converted" : "Filer konverterade",
|
||||
"Failed to convert files" : "Det gick inte att konvertera filer",
|
||||
"Converting file …" : "Konverterar fil …",
|
||||
"File successfully converted" : "Filen har konverterats",
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
"Failed to convert files: {message}" : "Kunde inte konvertera filer: {message}",
|
||||
"All files failed to be converted" : "Alla filer misslyckades med att konverteras",
|
||||
"One file could not be converted: {message}" : "En fil kunde inte konverteras: {message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["%n fil kunde inte konverteras","%n filer kunde inte konverteras"],
|
||||
"_%n file converted_::_%n files converted_" : ["%n fil konverterad","%n filer konverterade"],
|
||||
"Files converted" : "Filer konverterade",
|
||||
"Failed to convert files" : "Det gick inte att konvertera filer",
|
||||
"Converting file …" : "Konverterar fil …",
|
||||
"File successfully converted" : "Filen har konverterats",
|
||||
|
||||
@@ -277,9 +277,6 @@ OC.L10N.register(
|
||||
"Failed to convert files: {message}" : "無法轉換檔案:{message}",
|
||||
"All files failed to be converted" : "所有檔案轉換失敗",
|
||||
"One file could not be converted: {message}" : "一個檔案無法轉換:{message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["無法轉換 %n 個檔案"],
|
||||
"_%n file converted_::_%n files converted_" : ["已轉換 %n 個檔案"],
|
||||
"Files converted" : "檔案已轉換",
|
||||
"Failed to convert files" : "轉換檔案失敗",
|
||||
"Converting file …" : "正在轉換檔案 ……",
|
||||
"File successfully converted" : "成功轉換檔案",
|
||||
|
||||
@@ -275,9 +275,6 @@
|
||||
"Failed to convert files: {message}" : "無法轉換檔案:{message}",
|
||||
"All files failed to be converted" : "所有檔案轉換失敗",
|
||||
"One file could not be converted: {message}" : "一個檔案無法轉換:{message}",
|
||||
"_%n file could not be converted_::_%n files could not be converted_" : ["無法轉換 %n 個檔案"],
|
||||
"_%n file converted_::_%n files converted_" : ["已轉換 %n 個檔案"],
|
||||
"Files converted" : "檔案已轉換",
|
||||
"Failed to convert files" : "轉換檔案失敗",
|
||||
"Converting file …" : "正在轉換檔案 ……",
|
||||
"File successfully converted" : "成功轉換檔案",
|
||||
|
||||
@@ -38,8 +38,7 @@ const registerQueue = new PQueue({ concurrency: 5, intervalCap: 5, interval: 200
|
||||
*/
|
||||
async function registerTreeChildren(path: string = '/') {
|
||||
await queue.add(async () => {
|
||||
// preload up to 2 depth levels for faster navigation
|
||||
const nodes = await getFolderTreeNodes(path, 2)
|
||||
const nodes = await getFolderTreeNodes(path)
|
||||
const promises = nodes.map((node) => registerQueue.add(() => registerNodeView(node)))
|
||||
await Promise.allSettled(promises)
|
||||
})
|
||||
|
||||
@@ -397,7 +397,6 @@ export default {
|
||||
this.sharedWithMe = {}
|
||||
this.shares = []
|
||||
this.linkShares = []
|
||||
this.externalShares = []
|
||||
this.showSharingDetailsView = false
|
||||
this.shareDetailsData = {}
|
||||
},
|
||||
|
||||
@@ -27,7 +27,6 @@ use OCP\Files\Node;
|
||||
use OCP\Files\Storage\IStorage;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserSession;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/** @template-implements IEventListener<Event> */
|
||||
class VersionStorageMoveListener implements IEventListener {
|
||||
@@ -37,7 +36,6 @@ class VersionStorageMoveListener implements IEventListener {
|
||||
public function __construct(
|
||||
private IVersionManager $versionManager,
|
||||
private IUserSession $userSession,
|
||||
private LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -100,21 +98,7 @@ class VersionStorageMoveListener implements IEventListener {
|
||||
$source = $this->movedNodes[$target->getId()];
|
||||
}
|
||||
|
||||
if ($source === null) {
|
||||
$this->logger->warning(
|
||||
'Failed to retrieve source file during version move/copy.',
|
||||
[
|
||||
'eventClass' => get_class($event),
|
||||
'targetPath' => $target->getPath(),
|
||||
'targetId' => $target->getId(),
|
||||
'movedNodesKeys' => array_keys($this->movedNodes),
|
||||
'sourceBackendClass' => get_class($sourceBackend),
|
||||
'targetBackendClass' => get_class($targetBackend),
|
||||
]
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var File $source */
|
||||
$this->handleMoveOrCopy($event, $user, $source, $target, $sourceBackend, $targetBackend);
|
||||
} elseif ($target instanceof Folder) {
|
||||
/** @var Folder $source */
|
||||
|
||||
@@ -130,6 +130,7 @@ return array(
|
||||
'OCA\\Settings\\SetupChecks\\PushService' => $baseDir . '/../lib/SetupChecks/PushService.php',
|
||||
'OCA\\Settings\\SetupChecks\\RandomnessSecure' => $baseDir . '/../lib/SetupChecks/RandomnessSecure.php',
|
||||
'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => $baseDir . '/../lib/SetupChecks/ReadOnlyConfig.php',
|
||||
'OCA\\Settings\\SetupChecks\\RepairSanitizeSystemTagsAvailable' => $baseDir . '/../lib/SetupChecks/RepairSanitizeSystemTagsAvailable.php',
|
||||
'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => $baseDir . '/../lib/SetupChecks/SchedulingTableSize.php',
|
||||
'OCA\\Settings\\SetupChecks\\SecurityHeaders' => $baseDir . '/../lib/SetupChecks/SecurityHeaders.php',
|
||||
'OCA\\Settings\\SetupChecks\\ServerIdConfig' => $baseDir . '/../lib/SetupChecks/ServerIdConfig.php',
|
||||
|
||||
@@ -145,6 +145,7 @@ class ComposerStaticInitSettings
|
||||
'OCA\\Settings\\SetupChecks\\PushService' => __DIR__ . '/..' . '/../lib/SetupChecks/PushService.php',
|
||||
'OCA\\Settings\\SetupChecks\\RandomnessSecure' => __DIR__ . '/..' . '/../lib/SetupChecks/RandomnessSecure.php',
|
||||
'OCA\\Settings\\SetupChecks\\ReadOnlyConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ReadOnlyConfig.php',
|
||||
'OCA\\Settings\\SetupChecks\\RepairSanitizeSystemTagsAvailable' => __DIR__ . '/..' . '/../lib/SetupChecks/RepairSanitizeSystemTagsAvailable.php',
|
||||
'OCA\\Settings\\SetupChecks\\SchedulingTableSize' => __DIR__ . '/..' . '/../lib/SetupChecks/SchedulingTableSize.php',
|
||||
'OCA\\Settings\\SetupChecks\\SecurityHeaders' => __DIR__ . '/..' . '/../lib/SetupChecks/SecurityHeaders.php',
|
||||
'OCA\\Settings\\SetupChecks\\ServerIdConfig' => __DIR__ . '/..' . '/../lib/SetupChecks/ServerIdConfig.php',
|
||||
|
||||
@@ -67,6 +67,7 @@ use OCA\Settings\SetupChecks\PhpOutputBuffering;
|
||||
use OCA\Settings\SetupChecks\PushService;
|
||||
use OCA\Settings\SetupChecks\RandomnessSecure;
|
||||
use OCA\Settings\SetupChecks\ReadOnlyConfig;
|
||||
use OCA\Settings\SetupChecks\RepairSanitizeSystemTagsAvailable;
|
||||
use OCA\Settings\SetupChecks\SchedulingTableSize;
|
||||
use OCA\Settings\SetupChecks\SecurityHeaders;
|
||||
use OCA\Settings\SetupChecks\ServerIdConfig;
|
||||
@@ -207,6 +208,7 @@ class Application extends App implements IBootstrap {
|
||||
$context->registerSetupCheck(PhpOutputBuffering::class);
|
||||
$context->registerSetupCheck(RandomnessSecure::class);
|
||||
$context->registerSetupCheck(ReadOnlyConfig::class);
|
||||
$context->registerSetupCheck(RepairSanitizeSystemTagsAvailable::class);
|
||||
$context->registerSetupCheck(SecurityHeaders::class);
|
||||
$context->registerSetupCheck(ServerIdConfig::class);
|
||||
$context->registerSetupCheck(SchedulingTableSize::class);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\Settings\SetupChecks;
|
||||
|
||||
use OC\Repair\RepairSanitizeSystemTags;
|
||||
use OCP\IL10N;
|
||||
use OCP\SetupCheck\ISetupCheck;
|
||||
use OCP\SetupCheck\SetupResult;
|
||||
|
||||
class RepairSanitizeSystemTagsAvailable implements ISetupCheck {
|
||||
|
||||
public function __construct(
|
||||
private RepairSanitizeSystemTags $repairSanitizeSystemTags,
|
||||
private IL10N $l10n,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getCategory(): string {
|
||||
return 'system';
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->l10n->t('Sanitize and merge duplicate system tags available');
|
||||
}
|
||||
|
||||
public function run(): SetupResult {
|
||||
if ($this->repairSanitizeSystemTags->migrationsAvailable()) {
|
||||
return SetupResult::warning(
|
||||
$this->l10n->t('One or more system tags need to be sanitized or merged. This can take a long time on larger instances so this is not done automatically during upgrades. Use the command `occ maintenance:repair --include-expensive` to perform the migrations.'),
|
||||
);
|
||||
} else {
|
||||
return SetupResult::success('None');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
</NcButton>
|
||||
</div>
|
||||
<div class="token-dialog__password">
|
||||
<NcPasswordField
|
||||
<NcTextField
|
||||
ref="appPassword"
|
||||
:label="t('settings', 'Password')"
|
||||
:value="appPassword"
|
||||
@@ -61,7 +61,6 @@ import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
|
||||
import logger from '../logger.ts'
|
||||
|
||||
export default defineComponent({
|
||||
@@ -71,7 +70,6 @@ export default defineComponent({
|
||||
NcDialog,
|
||||
NcIconSvgWrapper,
|
||||
NcTextField,
|
||||
NcPasswordField,
|
||||
QR,
|
||||
},
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ OC.L10N.register(
|
||||
"Failed to delete tag" : "Impossible de supprimer l'étiquette",
|
||||
"Create or edit tags" : "Créer ou modifier les étiquettes",
|
||||
"Search for a tag to edit" : "Rechercher une étiquette à modifier",
|
||||
"Collaborative tags …" : "Étiquettes collaboratives …",
|
||||
"No tags to select" : "Aucune étiquette à sélectionner",
|
||||
"Tag name" : "Nom de l'étiquette",
|
||||
"Tag level" : "Niveau de l'étiquette",
|
||||
@@ -87,7 +86,6 @@ OC.L10N.register(
|
||||
"Failed to load tags" : "Impossible de charger les étiquettes",
|
||||
"Failed to select tag" : "Impossible de sélectionner l'étiquette",
|
||||
"System admin disabled tag creation. You can only use existing ones." : "L'administrateur a désactivé la création d'étiquettes. Vous ne pouvez utiliser que les étiquettes existantes.",
|
||||
"Loading collaborative tags …" : "Chargement des étiquettes collaboratives ...",
|
||||
"Search or create collaborative tags" : "Rechercher ou créer des étiquettes collaboratives",
|
||||
"No tags to select, type to create a new tag" : "Aucune étiquette à sélectionner, entrez une nouvelle étiquette",
|
||||
"Unable to update setting" : "Impossible de mettre à jour les paramètres",
|
||||
@@ -100,7 +98,7 @@ OC.L10N.register(
|
||||
"Assigned collaborative tags" : "Étiquettes collaboratives attribuées",
|
||||
"Open in Files" : "Ouvrir dans Fichiers",
|
||||
"List of tags and their associated files and folders." : "Liste des étiquettes et leurs fichiers et dossiers associés.",
|
||||
"No tags found" : "Aucunes étiquettes trouvées",
|
||||
"No tags found" : "Aucune étiquette trouvée",
|
||||
"Tags you have created will show up here." : "Les étiquettes que vous avez créées apparaîtront ici.",
|
||||
"Failed to load tag" : "Échec du chargement de l'étiquette",
|
||||
"Failed to load last used tags" : "Impossible de charger les dernières étiquettes utilisées",
|
||||
|
||||
@@ -54,7 +54,6 @@
|
||||
"Failed to delete tag" : "Impossible de supprimer l'étiquette",
|
||||
"Create or edit tags" : "Créer ou modifier les étiquettes",
|
||||
"Search for a tag to edit" : "Rechercher une étiquette à modifier",
|
||||
"Collaborative tags …" : "Étiquettes collaboratives …",
|
||||
"No tags to select" : "Aucune étiquette à sélectionner",
|
||||
"Tag name" : "Nom de l'étiquette",
|
||||
"Tag level" : "Niveau de l'étiquette",
|
||||
@@ -85,7 +84,6 @@
|
||||
"Failed to load tags" : "Impossible de charger les étiquettes",
|
||||
"Failed to select tag" : "Impossible de sélectionner l'étiquette",
|
||||
"System admin disabled tag creation. You can only use existing ones." : "L'administrateur a désactivé la création d'étiquettes. Vous ne pouvez utiliser que les étiquettes existantes.",
|
||||
"Loading collaborative tags …" : "Chargement des étiquettes collaboratives ...",
|
||||
"Search or create collaborative tags" : "Rechercher ou créer des étiquettes collaboratives",
|
||||
"No tags to select, type to create a new tag" : "Aucune étiquette à sélectionner, entrez une nouvelle étiquette",
|
||||
"Unable to update setting" : "Impossible de mettre à jour les paramètres",
|
||||
@@ -98,7 +96,7 @@
|
||||
"Assigned collaborative tags" : "Étiquettes collaboratives attribuées",
|
||||
"Open in Files" : "Ouvrir dans Fichiers",
|
||||
"List of tags and their associated files and folders." : "Liste des étiquettes et leurs fichiers et dossiers associés.",
|
||||
"No tags found" : "Aucunes étiquettes trouvées",
|
||||
"No tags found" : "Aucune étiquette trouvée",
|
||||
"Tags you have created will show up here." : "Les étiquettes que vous avez créées apparaîtront ici.",
|
||||
"Failed to load tag" : "Échec du chargement de l'étiquette",
|
||||
"Failed to load last used tags" : "Impossible de charger les dernières étiquettes utilisées",
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Plugin\EventHandler\AfterExpressionAnalysisInterface;
|
||||
use Psalm\Plugin\EventHandler\Event\AfterExpressionAnalysisEvent;
|
||||
|
||||
class LogicalOperatorChecker implements AfterExpressionAnalysisInterface {
|
||||
public static function afterExpressionAnalysis(AfterExpressionAnalysisEvent $event): ?bool {
|
||||
$stmt = $event->getExpr();
|
||||
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
|
||||
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new \Psalm\Issue\UnrecognizedExpression(
|
||||
'Logical binary operators AND and OR are not allowed in the Nextcloud codebase',
|
||||
new CodeLocation($event->getStatementsSource()->getSource(), $stmt),
|
||||
),
|
||||
$event->getStatementsSource()->getSuppressedIssues(),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export default {
|
||||
|
||||
methods: {
|
||||
passwordResetFinished() {
|
||||
window.location.href = generateUrl('login') + '?direct=1'
|
||||
window.location.href = generateUrl('login')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Vendored
-2
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
@@ -1 +0,0 @@
|
||||
5660-5660.js.license
|
||||
Vendored
+2
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
6069-6069.js.license
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -2007,6 +2007,7 @@ return array(
|
||||
'OC\\Repair\\RepairInvalidShares' => $baseDir . '/lib/private/Repair/RepairInvalidShares.php',
|
||||
'OC\\Repair\\RepairLogoDimension' => $baseDir . '/lib/private/Repair/RepairLogoDimension.php',
|
||||
'OC\\Repair\\RepairMimeTypes' => $baseDir . '/lib/private/Repair/RepairMimeTypes.php',
|
||||
'OC\\Repair\\RepairSanitizeSystemTags' => $baseDir . '/lib/private/Repair/RepairSanitizeSystemTags.php',
|
||||
'OC\\RichObjectStrings\\RichTextFormatter' => $baseDir . '/lib/private/RichObjectStrings/RichTextFormatter.php',
|
||||
'OC\\RichObjectStrings\\Validator' => $baseDir . '/lib/private/RichObjectStrings/Validator.php',
|
||||
'OC\\Route\\CachingRouter' => $baseDir . '/lib/private/Route/CachingRouter.php',
|
||||
|
||||
@@ -2048,6 +2048,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
|
||||
'OC\\Repair\\RepairInvalidShares' => __DIR__ . '/../../..' . '/lib/private/Repair/RepairInvalidShares.php',
|
||||
'OC\\Repair\\RepairLogoDimension' => __DIR__ . '/../../..' . '/lib/private/Repair/RepairLogoDimension.php',
|
||||
'OC\\Repair\\RepairMimeTypes' => __DIR__ . '/../../..' . '/lib/private/Repair/RepairMimeTypes.php',
|
||||
'OC\\Repair\\RepairSanitizeSystemTags' => __DIR__ . '/../../..' . '/lib/private/Repair/RepairSanitizeSystemTags.php',
|
||||
'OC\\RichObjectStrings\\RichTextFormatter' => __DIR__ . '/../../..' . '/lib/private/RichObjectStrings/RichTextFormatter.php',
|
||||
'OC\\RichObjectStrings\\Validator' => __DIR__ . '/../../..' . '/lib/private/RichObjectStrings/Validator.php',
|
||||
'OC\\Route\\CachingRouter' => __DIR__ . '/../../..' . '/lib/private/Route/CachingRouter.php',
|
||||
|
||||
@@ -13,7 +13,6 @@ use OC\Core\Controller\TwoFactorChallengeController;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http\Attribute\PublicPage;
|
||||
use OCP\AppFramework\Middleware;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Authentication\TwoFactorAuth\ALoginSetupController;
|
||||
use OCP\ISession;
|
||||
use OCP\IUserSession;
|
||||
@@ -23,32 +22,19 @@ use ReflectionMethod;
|
||||
// Will close the session if the user session is ephemeral.
|
||||
// Happens when the user logs in via the login flow v2.
|
||||
class FlowV2EphemeralSessionsMiddleware extends Middleware {
|
||||
|
||||
private const EPHEMERAL_SESSION_TTL = 5 * 60; // 5 minutes
|
||||
|
||||
public function __construct(
|
||||
private ISession $session,
|
||||
private IUserSession $userSession,
|
||||
private ControllerMethodReflector $reflector,
|
||||
private LoggerInterface $logger,
|
||||
private ITimeFactory $timeFactory,
|
||||
) {
|
||||
}
|
||||
|
||||
public function beforeController(Controller $controller, string $methodName) {
|
||||
$sessionCreationTime = $this->session->get(ClientFlowLoginV2Controller::EPHEMERAL_NAME);
|
||||
|
||||
// Not an ephemeral session.
|
||||
if ($sessionCreationTime === null) {
|
||||
if (!$this->session->get(ClientFlowLoginV2Controller::EPHEMERAL_NAME)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Lax enforcement until TTL is reached.
|
||||
if ($this->timeFactory->getTime() < $sessionCreationTime + self::EPHEMERAL_SESSION_TTL) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow certain controllers/methods to proceed without logging out.
|
||||
if (
|
||||
$controller instanceof ClientFlowLoginV2Controller
|
||||
&& ($methodName === 'grantPage' || $methodName === 'generateAppPassword')
|
||||
|
||||
@@ -9,7 +9,6 @@ declare(strict_types=1);
|
||||
namespace OC\Authentication\Login;
|
||||
|
||||
use OC\Core\Controller\ClientFlowLoginV2Controller;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\ISession;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
@@ -17,14 +16,13 @@ class FlowV2EphemeralSessionsCommand extends ALoginCommand {
|
||||
public function __construct(
|
||||
private ISession $session,
|
||||
private IURLGenerator $urlGenerator,
|
||||
private ITimeFactory $timeFactory,
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(LoginData $loginData): LoginResult {
|
||||
$loginV2GrantRoute = $this->urlGenerator->linkToRoute('core.ClientFlowLoginV2.grantPage');
|
||||
if (str_starts_with($loginData->getRedirectUrl() ?? '', $loginV2GrantRoute)) {
|
||||
$this->session->set(ClientFlowLoginV2Controller::EPHEMERAL_NAME, $this->timeFactory->getTime());
|
||||
$this->session->set(ClientFlowLoginV2Controller::EPHEMERAL_NAME, true);
|
||||
}
|
||||
|
||||
return $this->processNextOrFinishSuccessfully($loginData);
|
||||
|
||||
@@ -18,7 +18,6 @@ use OCP\Http\Client\LocalServerException;
|
||||
use OCP\ICertificateManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\Security\IRemoteHostValidator;
|
||||
use OCP\ServerVersion;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function parse_url;
|
||||
|
||||
@@ -42,7 +41,6 @@ class Client implements IClient {
|
||||
GuzzleClient $client,
|
||||
IRemoteHostValidator $remoteHostValidator,
|
||||
protected LoggerInterface $logger,
|
||||
protected ServerVersion $serverVersion,
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->client = $client;
|
||||
@@ -83,8 +81,7 @@ class Client implements IClient {
|
||||
$options = array_merge($defaults, $options);
|
||||
|
||||
if (!isset($options[RequestOptions::HEADERS]['User-Agent'])) {
|
||||
$userAgent = 'Nextcloud-Server-Crawler/' . $this->serverVersion->getVersionString();
|
||||
$options[RequestOptions::HEADERS]['User-Agent'] = $userAgent;
|
||||
$options[RequestOptions::HEADERS]['User-Agent'] = 'Nextcloud Server Crawler';
|
||||
}
|
||||
|
||||
if (!isset($options[RequestOptions::HEADERS]['Accept-Encoding'])) {
|
||||
|
||||
@@ -18,7 +18,6 @@ use OCP\Http\Client\IClientService;
|
||||
use OCP\ICertificateManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\Security\IRemoteHostValidator;
|
||||
use OCP\ServerVersion;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
@@ -44,7 +43,6 @@ class ClientService implements IClientService {
|
||||
IRemoteHostValidator $remoteHostValidator,
|
||||
IEventLogger $eventLogger,
|
||||
protected LoggerInterface $logger,
|
||||
protected ServerVersion $serverVersion,
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->certificateManager = $certificateManager;
|
||||
@@ -76,7 +74,6 @@ class ClientService implements IClientService {
|
||||
$client,
|
||||
$this->remoteHostValidator,
|
||||
$this->logger,
|
||||
$this->serverVersion,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ use OC\Repair\RepairDavShares;
|
||||
use OC\Repair\RepairInvalidShares;
|
||||
use OC\Repair\RepairLogoDimension;
|
||||
use OC\Repair\RepairMimeTypes;
|
||||
use OC\Repair\RepairSanitizeSystemTags;
|
||||
use OC\Template\JSCombiner;
|
||||
use OCA\DAV\Migration\DeleteSchedulingObjects;
|
||||
use OCA\DAV\Migration\RemoveObjectProperties;
|
||||
@@ -221,6 +222,7 @@ class Repair implements IOutput {
|
||||
),
|
||||
\OCP\Server::get(DeleteSchedulingObjects::class),
|
||||
\OC::$server->get(RemoveObjectProperties::class),
|
||||
\OCP\Server::get(RepairSanitizeSystemTags::class),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OC\Repair;
|
||||
|
||||
use OC\Migration\NullOutput;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\IRepairStep;
|
||||
use OCP\Util;
|
||||
|
||||
class RepairSanitizeSystemTags implements IRepairStep {
|
||||
private bool $dryRun = false;
|
||||
private int $changeCount = 0;
|
||||
|
||||
public function __construct(
|
||||
protected IDBConnection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return 'Sanitize and merge duplicate system tags';
|
||||
}
|
||||
|
||||
public function migrationsAvailable(): bool {
|
||||
$this->dryRun = true;
|
||||
$this->sanitizeAndMergeTags(new NullOutput());
|
||||
$this->dryRun = false;
|
||||
return $this->changeCount > 0;
|
||||
}
|
||||
|
||||
public function run(IOutput $output): void {
|
||||
$this->dryRun = false;
|
||||
$this->sanitizeAndMergeTags($output);
|
||||
}
|
||||
|
||||
private function sanitizeAndMergeTags(IOutput $output): void {
|
||||
$output->info('Starting sanitization of system tags...');
|
||||
|
||||
$tags = $this->getAllTags();
|
||||
|
||||
// Group tags by sanitized name
|
||||
$sanitizedMap = [];
|
||||
$totalTags = 0;
|
||||
foreach ($tags as $tag) {
|
||||
$sanitizedMap[$tag['sanitizedName']][] = $tag;
|
||||
$totalTags++;
|
||||
}
|
||||
|
||||
$sanitizeCount = count($sanitizedMap);
|
||||
$output->info("Found $totalTags tags with $sanitizeCount unique sanitized names.");
|
||||
|
||||
// Get object counts for all tags in one query
|
||||
$objectCounts = [];
|
||||
foreach ($this->getAllTagObjectCounts() as $tagId => $count) {
|
||||
$objectCounts[$tagId] = $count;
|
||||
}
|
||||
|
||||
// Process each sanitized name group
|
||||
foreach ($sanitizedMap as $sanitizedName => $group) {
|
||||
// Single tag, no duplicates found
|
||||
if (count($group) === 1) {
|
||||
$tag = $group[0];
|
||||
if ($tag['originalName'] !== $sanitizedName) {
|
||||
if (!$this->dryRun) {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->update('systemtag')
|
||||
->set('name', $qb->createNamedParameter($sanitizedName))
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($tag['id'])))
|
||||
->executeStatement();
|
||||
}
|
||||
$this->changeCount++;
|
||||
$output->info("Sanitized tag ID {$tag['id']}: '{$tag['originalName']}' → '$sanitizedName'");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Multiple tags with same sanitized name - merge them
|
||||
$this->mergeTagGroup($group, $sanitizedName, $objectCounts, $output);
|
||||
}
|
||||
|
||||
$output->info('System tag sanitization and merge completed.');
|
||||
}
|
||||
|
||||
private function mergeTagGroup(array $group, string $sanitizedName, array $objectCounts, IOutput $output): void {
|
||||
// Validate that all tags in the group have the same visibility and editable settings
|
||||
$firstTag = $group[0];
|
||||
$visibility = $firstTag['visibility'];
|
||||
$editable = $firstTag['editable'];
|
||||
|
||||
foreach ($group as $tag) {
|
||||
if ($tag['visibility'] !== $visibility || $tag['editable'] !== $editable) {
|
||||
$output->warning(
|
||||
"Cannot merge tag group '$sanitizedName': tags have different visibility or editable settings. "
|
||||
. 'Manual verification required. Tag IDs: ' . implode(', ', array_column($group, 'id'))
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which tag to keep (most object mappings, then lowest ID as tiebreaker)
|
||||
$keepTag = null;
|
||||
$maxCount = -1;
|
||||
|
||||
foreach ($group as $tag) {
|
||||
$count = $objectCounts[$tag['id']] ?? 0;
|
||||
if ($count > $maxCount || ($count === $maxCount && ($keepTag === null || $tag['id'] < $keepTag['id']))) {
|
||||
$maxCount = $count;
|
||||
$keepTag = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
$keepId = $keepTag['id'];
|
||||
if ($keepTag === null) {
|
||||
$output->warning("Cannot merge tag group '$sanitizedName': unable to determine which tag to keep");
|
||||
return;
|
||||
}
|
||||
|
||||
$duplicateIds = array_filter(array_column($group, 'id'), fn ($id) => $id !== $keepId);
|
||||
if (empty($duplicateIds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->dryRun) {
|
||||
$this->connection->beginTransaction();
|
||||
try {
|
||||
// Step 1: Delete ALL mappings from duplicate tags that conflict with keepId
|
||||
// This must happen FIRST before any updates to avoid unique constraint violations
|
||||
$this->deleteConflictingMappings($duplicateIds, $keepId);
|
||||
|
||||
// Step 2: Update all remaining mappings from duplicates to keepId
|
||||
// These won't conflict because we just deleted the conflicts
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->update('systemtag_object_mapping')
|
||||
->set('systemtagid', $qb->createNamedParameter($keepId))
|
||||
->where($qb->expr()->in('systemtagid', $qb->createNamedParameter($duplicateIds, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
->executeStatement();
|
||||
|
||||
// Step 3: Delete duplicate tags in bulk (safe now that mappings are gone)
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->delete('systemtag')
|
||||
->where($qb->expr()->in('id', $qb->createNamedParameter($duplicateIds, IQueryBuilder::PARAM_INT_ARRAY)))
|
||||
->executeStatement();
|
||||
|
||||
// Step 4: Sanitize the kept tag name if needed
|
||||
// This is safe because we've already deleted all duplicates with the same sanitized name
|
||||
if ($keepTag['originalName'] !== $sanitizedName) {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->update('systemtag')
|
||||
->set('name', $qb->createNamedParameter($sanitizedName))
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($keepId)))
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
$this->connection->commit();
|
||||
} catch (\Exception $e) {
|
||||
$this->connection->rollBack();
|
||||
$output->warning("Failed to merge tag group '$sanitizedName': " . $e->getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->changeCount += count($duplicateIds);
|
||||
if ($keepTag['originalName'] !== $sanitizedName) {
|
||||
$this->changeCount++;
|
||||
}
|
||||
|
||||
$duplicateIdsList = implode(', ', $duplicateIds);
|
||||
$output->info("Merged tags [$duplicateIdsList] into ID $keepId (sanitized: '$sanitizedName')");
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete mappings from duplicate tags where the same object is already mapped to keepId
|
||||
* This prevents unique constraint violations when updating systemtagid
|
||||
*/
|
||||
private function deleteConflictingMappings(array $duplicateIds, int $keepId): void {
|
||||
$batchSize = 1000;
|
||||
$batch = [];
|
||||
|
||||
// Stream keepId mappings and process in batches
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->select('objectid', 'objecttype')
|
||||
->from('systemtag_object_mapping')
|
||||
->where($qb->expr()->eq('systemtagid', $qb->createNamedParameter($keepId)));
|
||||
|
||||
$result = $qb->executeQuery();
|
||||
|
||||
while ($mapping = $result->fetch()) {
|
||||
$batch[] = $mapping;
|
||||
|
||||
// When batch is full, delete conflicts for this batch
|
||||
if (count($batch) >= $batchSize) {
|
||||
$this->deleteBatchConflicts($batch, $duplicateIds);
|
||||
$batch = []; // Clear batch
|
||||
}
|
||||
}
|
||||
|
||||
$result->closeCursor();
|
||||
|
||||
// Process remaining mappings in the last batch
|
||||
if (!empty($batch)) {
|
||||
$this->deleteBatchConflicts($batch, $duplicateIds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete mappings in a batch that conflict with keepId mappings
|
||||
*/
|
||||
private function deleteBatchConflicts(array $batch, array $duplicateIds): void {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->delete('systemtag_object_mapping')
|
||||
->where($qb->expr()->in('systemtagid', $qb->createNamedParameter($duplicateIds, IQueryBuilder::PARAM_INT_ARRAY)));
|
||||
|
||||
$orX = $qb->expr()->orX();
|
||||
foreach ($batch as $mapping) {
|
||||
$orX->add($qb->expr()->andX(
|
||||
$qb->expr()->eq('objectid', $qb->createNamedParameter($mapping['objectid'])),
|
||||
$qb->expr()->eq('objecttype', $qb->createNamedParameter($mapping['objecttype']))
|
||||
));
|
||||
}
|
||||
$qb->andWhere($orX);
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a tag name already exists (excluding a specific tag ID)
|
||||
*/
|
||||
private function tagNameExists(string $name, int $excludeId): bool {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->select('id')
|
||||
->from('systemtag')
|
||||
->where($qb->expr()->eq('name', $qb->createNamedParameter($name)))
|
||||
->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($excludeId)))
|
||||
->setMaxResults(1);
|
||||
|
||||
$result = $qb->executeQuery();
|
||||
$exists = $result->fetch() !== false;
|
||||
$result->closeCursor();
|
||||
|
||||
return $exists;
|
||||
}
|
||||
|
||||
// Fetch all tags in batches to avoid memory issues
|
||||
private function getAllTags(int $offset = 0, ?int $limit = null): \Iterator {
|
||||
$maxBatchSize = 1000;
|
||||
|
||||
do {
|
||||
if ($limit !== null) {
|
||||
$batchSize = min($limit, $maxBatchSize);
|
||||
$limit -= $batchSize;
|
||||
} else {
|
||||
$batchSize = $maxBatchSize;
|
||||
}
|
||||
|
||||
$tags = $this->getTags($batchSize, $offset);
|
||||
$offset += $batchSize;
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
yield $tag;
|
||||
}
|
||||
|
||||
} while (count($tags) === $batchSize && $limit !== 0);
|
||||
}
|
||||
|
||||
// Fetch tags from the database
|
||||
private function getTags($limit = null, $offset = null): array {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->select('id', 'name', 'visibility', 'editable')
|
||||
->from('systemtag')
|
||||
->orderBy('name')
|
||||
->addOrderBy('id');
|
||||
|
||||
$tags = [];
|
||||
|
||||
// Apply limit and offset if provided
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if ($offset !== null) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
// Fetch and return tags
|
||||
$result = $qb->executeQuery();
|
||||
while ($row = $result->fetch()) {
|
||||
$tags[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'originalName' => $row['name'],
|
||||
'sanitizedName' => Util::sanitizeWordsAndEmojis($row['name']),
|
||||
'visibility' => (int)$row['visibility'],
|
||||
'editable' => (int)$row['editable'],
|
||||
];
|
||||
}
|
||||
$result->closeCursor();
|
||||
return $tags;
|
||||
}
|
||||
|
||||
private function getAllTagObjectCounts(int $offset = 0, ?int $limit = null): \Iterator {
|
||||
$maxBatchSize = 1000;
|
||||
|
||||
do {
|
||||
if ($limit !== null) {
|
||||
$batchSize = min($limit, $maxBatchSize);
|
||||
$limit -= $batchSize;
|
||||
} else {
|
||||
$batchSize = $maxBatchSize;
|
||||
}
|
||||
|
||||
$counts = $this->getTagObjectCounts($batchSize, $offset);
|
||||
$offset += $batchSize;
|
||||
|
||||
foreach ($counts as $tagId => $count) {
|
||||
yield $tagId => $count;
|
||||
}
|
||||
|
||||
} while (count($counts) === $batchSize && $limit !== 0);
|
||||
}
|
||||
|
||||
// Get object counts for all tags in one efficient query
|
||||
private function getTagObjectCounts($limit = null, $offset = null): array {
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->select('systemtagid')
|
||||
->selectAlias($qb->createFunction('COUNT(*)'), 'cnt')
|
||||
->from('systemtag_object_mapping')
|
||||
->groupBy('systemtagid');
|
||||
|
||||
$counts = [];
|
||||
|
||||
// Apply limit and offset if provided
|
||||
if ($limit !== null) {
|
||||
$qb->setMaxResults($limit);
|
||||
}
|
||||
if ($offset !== null) {
|
||||
$qb->setFirstResult($offset);
|
||||
}
|
||||
|
||||
// Fetch and return counts
|
||||
$result = $qb->executeQuery();
|
||||
while ($row = $result->fetch()) {
|
||||
$counts[(int)$row['systemtagid']] = (int)$row['cnt'];
|
||||
}
|
||||
$result->closeCursor();
|
||||
return $counts;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ use OCP\SystemTag\TagAlreadyExistsException;
|
||||
use OCP\SystemTag\TagCreationForbiddenException;
|
||||
use OCP\SystemTag\TagNotFoundException;
|
||||
use OCP\SystemTag\TagUpdateForbiddenException;
|
||||
use OCP\Util;
|
||||
|
||||
/**
|
||||
* Manager class for system tags
|
||||
@@ -156,6 +157,8 @@ class SystemTagManager implements ISystemTagManager {
|
||||
throw new TagCreationForbiddenException();
|
||||
}
|
||||
|
||||
$tagName = Util::sanitizeWordsAndEmojis($tagName);
|
||||
|
||||
// Check if tag already exists (case-insensitive)
|
||||
$existingTags = $this->getAllTags(null, $tagName);
|
||||
foreach ($existingTags as $existingTag) {
|
||||
@@ -225,8 +228,9 @@ class SystemTagManager implements ISystemTagManager {
|
||||
}
|
||||
|
||||
$beforeUpdate = array_shift($tags);
|
||||
$newName = Util::sanitizeWordsAndEmojis($newName);
|
||||
|
||||
// Length of name column is 64
|
||||
$newName = trim($newName);
|
||||
$truncatedNewName = substr($newName, 0, 64);
|
||||
$afterUpdate = new SystemTag(
|
||||
$tagId,
|
||||
@@ -451,5 +455,4 @@ class SystemTagManager implements ISystemTagManager {
|
||||
|
||||
return $groupIds;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -662,4 +662,25 @@ class Util {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a name by removing unwanted characters
|
||||
*
|
||||
* This function removes any character that is not a letter, space, or symbol (including emojis).
|
||||
* It also normalizes spaces by replacing multiple consecutive spaces with a single space and trimming
|
||||
* leading and trailing spaces.
|
||||
*
|
||||
* @param string $input The input string to sanitize
|
||||
* @return string The sanitized string
|
||||
* @since 33.0.0
|
||||
*/
|
||||
public static function sanitizeWordsAndEmojis(string $input): string {
|
||||
// Remove control characters and other invisible separators, but keep everything else
|
||||
$clean = preg_replace('/[\p{C}]+/u', '', $input);
|
||||
|
||||
// Normalize whitespace to single spaces
|
||||
$clean = preg_replace('/[[:space:]]+/u', ' ', trim($clean));
|
||||
|
||||
return $clean;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -89,7 +89,7 @@
|
||||
"wait-on": "^9.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.0.0",
|
||||
"npm": "^11.3.0"
|
||||
"node": "^22.0.0",
|
||||
"npm": "^10.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<plugins>
|
||||
<plugin filename="build/psalm/AppFrameworkTainter.php" />
|
||||
<plugin filename="build/psalm/AttributeNamedParameters.php" />
|
||||
<plugin filename="build/psalm/LogicalOperatorChecker.php" />
|
||||
</plugins>
|
||||
<projectFiles>
|
||||
<directory name="apps/admin_audit"/>
|
||||
|
||||
@@ -21,7 +21,6 @@ use OCP\Diagnostics\IEventLogger;
|
||||
use OCP\ICertificateManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\Security\IRemoteHostValidator;
|
||||
use OCP\ServerVersion;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
@@ -46,7 +45,6 @@ class ClientServiceTest extends \Test\TestCase {
|
||||
$remoteHostValidator = $this->createMock(IRemoteHostValidator::class);
|
||||
$eventLogger = $this->createMock(IEventLogger::class);
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$serverVersion = $this->createMock(ServerVersion::class);
|
||||
|
||||
$clientService = new ClientService(
|
||||
$config,
|
||||
@@ -55,7 +53,6 @@ class ClientServiceTest extends \Test\TestCase {
|
||||
$remoteHostValidator,
|
||||
$eventLogger,
|
||||
$logger,
|
||||
$serverVersion,
|
||||
);
|
||||
|
||||
$handler = new CurlHandler();
|
||||
@@ -75,7 +72,6 @@ class ClientServiceTest extends \Test\TestCase {
|
||||
$guzzleClient,
|
||||
$remoteHostValidator,
|
||||
$logger,
|
||||
$serverVersion,
|
||||
),
|
||||
$clientService->newClient()
|
||||
);
|
||||
@@ -98,7 +94,6 @@ class ClientServiceTest extends \Test\TestCase {
|
||||
$remoteHostValidator = $this->createMock(IRemoteHostValidator::class);
|
||||
$eventLogger = $this->createMock(IEventLogger::class);
|
||||
$logger = $this->createMock(LoggerInterface::class);
|
||||
$serverVersion = $this->createMock(ServerVersion::class);
|
||||
|
||||
$clientService = new ClientService(
|
||||
$config,
|
||||
@@ -107,7 +102,6 @@ class ClientServiceTest extends \Test\TestCase {
|
||||
$remoteHostValidator,
|
||||
$eventLogger,
|
||||
$logger,
|
||||
$serverVersion,
|
||||
);
|
||||
|
||||
$handler = new CurlHandler();
|
||||
@@ -126,7 +120,6 @@ class ClientServiceTest extends \Test\TestCase {
|
||||
$guzzleClient,
|
||||
$remoteHostValidator,
|
||||
$logger,
|
||||
$serverVersion,
|
||||
),
|
||||
$clientService->newClient()
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ use OCP\Http\Client\LocalServerException;
|
||||
use OCP\ICertificateManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\Security\IRemoteHostValidator;
|
||||
use OCP\ServerVersion;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use function parse_url;
|
||||
@@ -37,7 +36,6 @@ class ClientTest extends \Test\TestCase {
|
||||
/** @var IRemoteHostValidator|MockObject */
|
||||
private IRemoteHostValidator $remoteHostValidator;
|
||||
private LoggerInterface $logger;
|
||||
private ServerVersion $serverVersion;
|
||||
/** @var array */
|
||||
private $defaultRequestOptions;
|
||||
|
||||
@@ -48,15 +46,12 @@ class ClientTest extends \Test\TestCase {
|
||||
$this->certificateManager = $this->createMock(ICertificateManager::class);
|
||||
$this->remoteHostValidator = $this->createMock(IRemoteHostValidator::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->serverVersion = $this->createMock(ServerVersion::class);
|
||||
|
||||
$this->client = new Client(
|
||||
$this->config,
|
||||
$this->certificateManager,
|
||||
$this->guzzleClient,
|
||||
$this->remoteHostValidator,
|
||||
$this->logger,
|
||||
$this->serverVersion,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -281,9 +276,6 @@ class ClientTest extends \Test\TestCase {
|
||||
->with()
|
||||
->willReturn('/my/path.crt');
|
||||
|
||||
$this->serverVersion->method('getVersionString')
|
||||
->willReturn('123.45.6');
|
||||
|
||||
$this->defaultRequestOptions = [
|
||||
'verify' => '/my/path.crt',
|
||||
'proxy' => [
|
||||
@@ -291,7 +283,7 @@ class ClientTest extends \Test\TestCase {
|
||||
'https' => 'foo'
|
||||
],
|
||||
'headers' => [
|
||||
'User-Agent' => 'Nextcloud-Server-Crawler/123.45.6',
|
||||
'User-Agent' => 'Nextcloud Server Crawler',
|
||||
'Accept-Encoding' => 'gzip',
|
||||
],
|
||||
'timeout' => 30,
|
||||
@@ -474,13 +466,10 @@ class ClientTest extends \Test\TestCase {
|
||||
->expects($this->never())
|
||||
->method('listCertificates');
|
||||
|
||||
$this->serverVersion->method('getVersionString')
|
||||
->willReturn('123.45.6');
|
||||
|
||||
$this->assertEquals([
|
||||
'verify' => \OC::$SERVERROOT . '/resources/config/ca-bundle.crt',
|
||||
'headers' => [
|
||||
'User-Agent' => 'Nextcloud-Server-Crawler/123.45.6',
|
||||
'User-Agent' => 'Nextcloud Server Crawler',
|
||||
'Accept-Encoding' => 'gzip',
|
||||
],
|
||||
'timeout' => 30,
|
||||
@@ -524,9 +513,6 @@ class ClientTest extends \Test\TestCase {
|
||||
->with()
|
||||
->willReturn('/my/path.crt');
|
||||
|
||||
$this->serverVersion->method('getVersionString')
|
||||
->willReturn('123.45.6');
|
||||
|
||||
$this->assertEquals([
|
||||
'verify' => '/my/path.crt',
|
||||
'proxy' => [
|
||||
@@ -534,7 +520,7 @@ class ClientTest extends \Test\TestCase {
|
||||
'https' => 'foo'
|
||||
],
|
||||
'headers' => [
|
||||
'User-Agent' => 'Nextcloud-Server-Crawler/123.45.6',
|
||||
'User-Agent' => 'Nextcloud Server Crawler',
|
||||
'Accept-Encoding' => 'gzip',
|
||||
],
|
||||
'timeout' => 30,
|
||||
@@ -578,9 +564,6 @@ class ClientTest extends \Test\TestCase {
|
||||
->with()
|
||||
->willReturn('/my/path.crt');
|
||||
|
||||
$this->serverVersion->method('getVersionString')
|
||||
->willReturn('123.45.6');
|
||||
|
||||
$this->assertEquals([
|
||||
'verify' => '/my/path.crt',
|
||||
'proxy' => [
|
||||
@@ -589,7 +572,7 @@ class ClientTest extends \Test\TestCase {
|
||||
'no' => ['bar']
|
||||
],
|
||||
'headers' => [
|
||||
'User-Agent' => 'Nextcloud-Server-Crawler/123.45.6',
|
||||
'User-Agent' => 'Nextcloud Server Crawler',
|
||||
'Accept-Encoding' => 'gzip',
|
||||
],
|
||||
'timeout' => 30,
|
||||
|
||||
@@ -416,4 +416,57 @@ class UtilTest extends \Test\TestCase {
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
|
||||
public static function sanitizeProvider(): array {
|
||||
return [
|
||||
// Basic spaces and line controls
|
||||
['Hello World', 'Hello World'],
|
||||
[' Hello World ', 'Hello World'],
|
||||
["Hello\t World \nAgain", 'Hello World Again'],
|
||||
["Hello\rWorld", 'HelloWorld'],
|
||||
["Hello\r\nWorld", 'HelloWorld'],
|
||||
["Hello\u{200B}World", 'HelloWorld'], // zero-width space removed
|
||||
["Hello\t\n\r World", 'Hello World'],
|
||||
|
||||
// Unicode, emoji, and CJK
|
||||
['テスト 😃 💬', 'テスト 😃 💬'],
|
||||
['中文測試 ✅', '中文測試 ✅'],
|
||||
['Русский текст 😁', 'Русский текст 😁'],
|
||||
['Café crème ☕', 'Café crème ☕'],
|
||||
|
||||
// Punctuation and filename-like
|
||||
['Hello-World_123.', 'Hello-World_123.'],
|
||||
['File.name, with commas', 'File.name, with commas'],
|
||||
['Smile — dash', 'Smile — dash'],
|
||||
['Invalid:/\\?%*|<>name', 'Invalid:/\\?%*|<>name'], // kept as is
|
||||
['test@example.com', 'test@example.com'],
|
||||
|
||||
// Control and invisible chars
|
||||
["Bad\0Name", 'BadName'],
|
||||
["Hello\u{0007}World", 'HelloWorld'],
|
||||
["Line\r\nbreaks", 'Linebreaks'],
|
||||
["\x1F Hidden control", 'Hidden control'],
|
||||
|
||||
// Whitespace and normalization
|
||||
[" Multiple spaces\t and \nnewlines ", 'Multiple spaces and newlines'],
|
||||
["No-break\u{00A0}space", 'No-break space'], // NBSP normalized
|
||||
["Zero\u{2003}width\u{2009}spaces", 'Zero width spaces'], // various spaces
|
||||
|
||||
// Complex mixes
|
||||
['テスト 💬.png', 'テスト 💬.png'],
|
||||
[' Mix 😎 emojis 🎉 and 123 numbers ', 'Mix 😎 emojis 🎉 and 123 numbers'],
|
||||
["Hello \u{200B}\n World", 'Hello World'],
|
||||
['Path ../etc/passwd', 'Path ../etc/passwd'],
|
||||
['Symbols! @ # % ^ & * ( )', 'Symbols! @ # % ^ & * ( )'],
|
||||
['Special chars <script>', 'Special chars <script>'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider sanitizeProvider
|
||||
*/
|
||||
public function testSanitizeWordsAndEmojis(string $input, string $expected): void {
|
||||
$this->assertSame($expected, Util::sanitizeWordsAndEmojis($input));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user