Compare commits

..

1 Commits

Author SHA1 Message Date
skjnldsv 59871af530 fix(systemtags): remove duplicates, prevent and sanitize existing tags
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
2025-11-06 08:12:23 +01:00
59 changed files with 509 additions and 186 deletions
+1 -1
View File
@@ -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;
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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" : "Датотеката е успешно конвертирана",
-3
View File
@@ -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" : "Датотеката е успешно конвертирана",
-3
View File
@@ -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" : "Фајл је успешно конвертован",
-3
View File
@@ -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" : "Фајл је успешно конвертован",
-3
View File
@@ -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",
-3
View File
@@ -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",
-3
View File
@@ -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" : "成功轉換檔案",
-3
View File
@@ -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" : "成功轉換檔案",
+1 -2
View File
@@ -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,
},
+1 -3
View File
@@ -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",
+1 -3
View File
@@ -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",
-30
View File
@@ -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;
}
}
+1 -1
View File
@@ -163,7 +163,7 @@ export default {
methods: {
passwordResetFinished() {
window.location.href = generateUrl('login') + '?direct=1'
window.location.href = generateUrl('login')
},
},
}
-2
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
5660-5660.js.license
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
6069-6069.js.license
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
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
@@ -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);
+1 -4
View File
@@ -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,
);
}
}
+2
View File
@@ -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;
}
}
+5 -2
View File
@@ -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;
}
}
+21
View File
@@ -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
View File
@@ -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"
}
}
-1
View File
@@ -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()
);
+4 -21
View File
@@ -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,
+53
View File
@@ -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));
}
}