aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorskjnldsv <skjnldsv@protonmail.com>2025-07-31 08:53:35 +0200
committerskjnldsv <skjnldsv@protonmail.com>2025-08-06 09:00:50 +0200
commit612e06466adb971d11f90396cf40fa1479b73954 (patch)
tree1efe564ae8b14e108b3fd4efe878414c787d875c
parentc3cd5bb439dde62ba9e1a812b076ba3c2cd7897e (diff)
downloadnextcloud-server-backport/54269/stable31.tar.gz
nextcloud-server-backport/54269/stable31.zip
fix(core): ensure unique vcategorybackport/54269/stable31
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
-rw-r--r--core/Application.php6
-rw-r--r--core/Migrations/Version13000Date20170718121200.php1
-rw-r--r--core/Migrations/Version31000Date20250731062008.php106
-rw-r--r--lib/composer/composer/autoload_classmap.php1
-rw-r--r--lib/composer/composer/autoload_static.php1
-rw-r--r--lib/private/Tags.php1
-rw-r--r--version.php2
7 files changed, 116 insertions, 2 deletions
diff --git a/core/Application.php b/core/Application.php
index 9a759da544c..cfca7b3f4ad 100644
--- a/core/Application.php
+++ b/core/Application.php
@@ -237,6 +237,12 @@ class Application extends App {
'systag_objecttype',
['objecttype']
);
+
+ $event->addMissingUniqueIndex(
+ 'vcategory',
+ 'unique_category_per_user',
+ ['uid', 'type', 'category']
+ );
});
$eventDispatcher->addListener(AddMissingPrimaryKeyEvent::class, function (AddMissingPrimaryKeyEvent $event) {
diff --git a/core/Migrations/Version13000Date20170718121200.php b/core/Migrations/Version13000Date20170718121200.php
index 1adbf2f0ea2..4a0bc8b7eee 100644
--- a/core/Migrations/Version13000Date20170718121200.php
+++ b/core/Migrations/Version13000Date20170718121200.php
@@ -657,6 +657,7 @@ class Version13000Date20170718121200 extends SimpleMigrationStep {
$table->addIndex(['uid'], 'uid_index');
$table->addIndex(['type'], 'type_index');
$table->addIndex(['category'], 'category_index');
+ $table->addUniqueIndex(['uid', 'type', 'category'], 'unique_category_per_user');
}
if (!$schema->hasTable('vcategory_to_object')) {
diff --git a/core/Migrations/Version31000Date20250731062008.php b/core/Migrations/Version31000Date20250731062008.php
new file mode 100644
index 00000000000..6730a64e896
--- /dev/null
+++ b/core/Migrations/Version31000Date20250731062008.php
@@ -0,0 +1,106 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Core\Migrations;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+use Override;
+
+/**
+ * Make sure vcategory entries are unique per user and type
+ * This migration will clean up existing duplicates
+ * and add a unique constraint to prevent future duplicates.
+ */
+class Version31000Date20250731062008 extends SimpleMigrationStep {
+ public function __construct(
+ private IDBConnection $connection,
+ ) {
+ }
+
+ /**
+ * @param IOutput $output
+ * @param Closure(): ISchemaWrapper $schemaClosure
+ * @param array $options
+ */
+ #[Override]
+ public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
+ // Clean up duplicate categories before adding unique constraint
+ $this->cleanupDuplicateCategories($output);
+ }
+
+ /**
+ * Clean up duplicate categories
+ */
+ private function cleanupDuplicateCategories(IOutput $output) {
+ $output->info('Starting cleanup of duplicate vcategory records...');
+
+ // Find all categories, ordered to identify duplicates
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('id', 'uid', 'type', 'category')
+ ->from('vcategory')
+ ->orderBy('uid')
+ ->addOrderBy('type')
+ ->addOrderBy('category')
+ ->addOrderBy('id');
+
+ $result = $qb->executeQuery();
+
+ $seen = [];
+ $duplicateCount = 0;
+
+ while ($category = $result->fetch()) {
+ $key = $category['uid'] . '|' . $category['type'] . '|' . $category['category'];
+ $categoryId = (int)$category['id'];
+
+ if (!isset($seen[$key])) {
+ // First occurrence - keep this one
+ $seen[$key] = $categoryId;
+ continue;
+ }
+
+ // Duplicate found
+ $keepId = $seen[$key];
+ $duplicateCount++;
+
+ $output->info("Found duplicate: keeping ID $keepId, removing ID $categoryId");
+
+ // Update object references
+ $updateQb = $this->connection->getQueryBuilder();
+ $updateQb->update('vcategory_to_object')
+ ->set('categoryid', $updateQb->createNamedParameter($keepId))
+ ->where($updateQb->expr()->eq('categoryid', $updateQb->createNamedParameter($categoryId)));
+
+ $affectedRows = $updateQb->executeStatement();
+ if ($affectedRows > 0) {
+ $output->info(" - Updated $affectedRows object references from category $categoryId to $keepId");
+ }
+
+ // Remove duplicate category record
+ $deleteQb = $this->connection->getQueryBuilder();
+ $deleteQb->delete('vcategory')
+ ->where($deleteQb->expr()->eq('id', $deleteQb->createNamedParameter($categoryId)));
+
+ $deleteQb->executeStatement();
+ $output->info(" - Deleted duplicate category record ID $categoryId");
+
+ }
+
+ $result->closeCursor();
+
+ if ($duplicateCount === 0) {
+ $output->info('No duplicate categories found');
+ } else {
+ $output->info("Duplicate cleanup completed - processed $duplicateCount duplicates");
+ }
+ }
+}
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 5b7821a7438..2c1872e23d9 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1461,6 +1461,7 @@ return array(
'OC\\Core\\Migrations\\Version31000Date20240101084401' => $baseDir . '/core/Migrations/Version31000Date20240101084401.php',
'OC\\Core\\Migrations\\Version31000Date20240814184402' => $baseDir . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20250213102442' => $baseDir . '/core/Migrations/Version31000Date20250213102442.php',
+ 'OC\\Core\\Migrations\\Version31000Date20250731062008' => $baseDir . '/core/Migrations/Version31000Date20250731062008.php',
'OC\\Core\\Migrations\\Version32000Date20250620081925' => $baseDir . '/core/Migrations/Version32000Date20250620081925.php',
'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 1c22c0957f2..9e843c976d5 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1510,6 +1510,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OC\\Core\\Migrations\\Version31000Date20240101084401' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240101084401.php',
'OC\\Core\\Migrations\\Version31000Date20240814184402' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20240814184402.php',
'OC\\Core\\Migrations\\Version31000Date20250213102442' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20250213102442.php',
+ 'OC\\Core\\Migrations\\Version31000Date20250731062008' => __DIR__ . '/../../..' . '/core/Migrations/Version31000Date20250731062008.php',
'OC\\Core\\Migrations\\Version32000Date20250620081925' => __DIR__ . '/../../..' . '/core/Migrations/Version32000Date20250620081925.php',
'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php',
'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php',
diff --git a/lib/private/Tags.php b/lib/private/Tags.php
index 0a37f4c9f4e..fe4a4137e10 100644
--- a/lib/private/Tags.php
+++ b/lib/private/Tags.php
@@ -273,7 +273,6 @@ class Tags implements ITags {
return false;
}
if ($this->userHasTag($name, $this->user)) {
- // TODO use unique db properties instead of an additional check
$this->logger->debug(__METHOD__ . ' Tag with name already exists', ['app' => 'core']);
return false;
}
diff --git a/version.php b/version.php
index 3d66ce636dc..0181f04155d 100644
--- a/version.php
+++ b/version.php
@@ -9,7 +9,7 @@
// between betas, final and RCs. This is _not_ the public version number. Reset minor/patch level
// when updating major/minor version number.
-$OC_Version = [31, 0, 7, 1];
+$OC_Version = [31, 0, 7, 2];
// The human-readable string
$OC_VersionString = '31.0.7';