aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Repair
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Repair')
-rw-r--r--lib/private/Repair/AddAppConfigLazyMigration.php45
-rw-r--r--lib/private/Repair/AddBruteForceCleanupJob.php31
-rw-r--r--lib/private/Repair/AddCleanupDeletedUsersBackgroundJob.php30
-rw-r--r--lib/private/Repair/AddCleanupUpdaterBackupsJob.php29
-rw-r--r--lib/private/Repair/AddMetadataGenerationJob.php27
-rw-r--r--lib/private/Repair/AddRemoveOldTasksBackgroundJob.php34
-rw-r--r--lib/private/Repair/CleanTags.php186
-rw-r--r--lib/private/Repair/CleanUpAbandonedApps.php34
-rw-r--r--lib/private/Repair/ClearFrontendCaches.php43
-rw-r--r--lib/private/Repair/ClearGeneratedAvatarCache.php51
-rw-r--r--lib/private/Repair/ClearGeneratedAvatarCacheJob.php24
-rw-r--r--lib/private/Repair/Collation.php129
-rw-r--r--lib/private/Repair/ConfigKeyMigration.php29
-rw-r--r--lib/private/Repair/Events/RepairAdvanceEvent.php32
-rw-r--r--lib/private/Repair/Events/RepairErrorEvent.php25
-rw-r--r--lib/private/Repair/Events/RepairFinishEvent.php14
-rw-r--r--lib/private/Repair/Events/RepairInfoEvent.php25
-rw-r--r--lib/private/Repair/Events/RepairStartEvent.php32
-rw-r--r--lib/private/Repair/Events/RepairStepEvent.php25
-rw-r--r--lib/private/Repair/Events/RepairWarningEvent.php25
-rw-r--r--lib/private/Repair/MoveUpdaterStepFile.php61
-rw-r--r--lib/private/Repair/NC13/AddLogRotateJob.php29
-rw-r--r--lib/private/Repair/NC14/AddPreviewBackgroundCleanupJob.php31
-rw-r--r--lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php31
-rw-r--r--lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php88
-rw-r--r--lib/private/Repair/NC16/ClearCollectionsAccessCache.php43
-rw-r--r--lib/private/Repair/NC18/ResetGeneratedAvatarFlag.php45
-rw-r--r--lib/private/Repair/NC20/EncryptionLegacyCipher.php49
-rw-r--r--lib/private/Repair/NC20/EncryptionMigration.php49
-rw-r--r--lib/private/Repair/NC20/ShippedDashboardEnable.php34
-rw-r--r--lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php43
-rw-r--r--lib/private/Repair/NC22/LookupServerSendCheck.php33
-rw-r--r--lib/private/Repair/NC24/AddTokenCleanupJob.php30
-rw-r--r--lib/private/Repair/NC25/AddMissingSecretJob.php49
-rw-r--r--lib/private/Repair/NC29/SanitizeAccountProperties.php30
-rw-r--r--lib/private/Repair/NC29/SanitizeAccountPropertiesJob.php75
-rw-r--r--lib/private/Repair/NC30/RemoveLegacyDatadirFile.php32
-rw-r--r--lib/private/Repair/OldGroupMembershipShares.php96
-rw-r--r--lib/private/Repair/Owncloud/CleanPreviews.php56
-rw-r--r--lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php91
-rw-r--r--lib/private/Repair/Owncloud/DropAccountTermsTable.php41
-rw-r--r--lib/private/Repair/Owncloud/MigrateOauthTables.php259
-rw-r--r--lib/private/Repair/Owncloud/MoveAvatars.php55
-rw-r--r--lib/private/Repair/Owncloud/MoveAvatarsBackgroundJob.php95
-rw-r--r--lib/private/Repair/Owncloud/SaveAccountsTableData.php181
-rw-r--r--lib/private/Repair/Owncloud/UpdateLanguageCodes.php72
-rw-r--r--lib/private/Repair/RemoveBrokenProperties.php68
-rw-r--r--lib/private/Repair/RemoveLinkShares.php206
-rw-r--r--lib/private/Repair/RepairDavShares.php109
-rw-r--r--lib/private/Repair/RepairInvalidShares.php95
-rw-r--r--lib/private/Repair/RepairLogoDimension.php82
-rw-r--r--lib/private/Repair/RepairMimeTypes.php473
52 files changed, 3601 insertions, 0 deletions
diff --git a/lib/private/Repair/AddAppConfigLazyMigration.php b/lib/private/Repair/AddAppConfigLazyMigration.php
new file mode 100644
index 00000000000..7ae83e87669
--- /dev/null
+++ b/lib/private/Repair/AddAppConfigLazyMigration.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OCP\IAppConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Psr\Log\LoggerInterface;
+
+class AddAppConfigLazyMigration implements IRepairStep {
+ /**
+ * Just add config values that needs to be migrated to lazy loading
+ */
+ private static array $lazyAppConfig = [
+ 'core' => [
+ 'oc.integritycheck.checker',
+ ],
+ ];
+
+ public function __construct(
+ private IAppConfig $appConfig,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ public function getName() {
+ return 'migrate lazy config values';
+ }
+
+ public function run(IOutput $output) {
+ $c = 0;
+ foreach (self::$lazyAppConfig as $appId => $configKeys) {
+ foreach ($configKeys as $configKey) {
+ $c += (int)$this->appConfig->updateLazy($appId, $configKey, true);
+ }
+ }
+
+ $this->logger->notice('core/BackgroundJobs/AppConfigLazyMigration: ' . $c . ' config values updated');
+ }
+}
diff --git a/lib/private/Repair/AddBruteForceCleanupJob.php b/lib/private/Repair/AddBruteForceCleanupJob.php
new file mode 100644
index 00000000000..dd08e36a597
--- /dev/null
+++ b/lib/private/Repair/AddBruteForceCleanupJob.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OC\Security\Bruteforce\CleanupJob;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddBruteForceCleanupJob implements IRepairStep {
+ /** @var IJobList */
+ protected $jobList;
+
+ public function __construct(IJobList $jobList) {
+ $this->jobList = $jobList;
+ }
+
+ public function getName() {
+ return 'Add job to cleanup the bruteforce entries';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(CleanupJob::class);
+ }
+}
diff --git a/lib/private/Repair/AddCleanupDeletedUsersBackgroundJob.php b/lib/private/Repair/AddCleanupDeletedUsersBackgroundJob.php
new file mode 100644
index 00000000000..9713d8595e7
--- /dev/null
+++ b/lib/private/Repair/AddCleanupDeletedUsersBackgroundJob.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OC\User\BackgroundJobs\CleanupDeletedUsers;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddCleanupDeletedUsersBackgroundJob implements IRepairStep {
+ private IJobList $jobList;
+
+ public function __construct(IJobList $jobList) {
+ $this->jobList = $jobList;
+ }
+
+ public function getName(): string {
+ return 'Add cleanup-deleted-users background job';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(CleanupDeletedUsers::class);
+ }
+}
diff --git a/lib/private/Repair/AddCleanupUpdaterBackupsJob.php b/lib/private/Repair/AddCleanupUpdaterBackupsJob.php
new file mode 100644
index 00000000000..e631a3303f1
--- /dev/null
+++ b/lib/private/Repair/AddCleanupUpdaterBackupsJob.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OC\Core\BackgroundJobs\BackgroundCleanupUpdaterBackupsJob;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddCleanupUpdaterBackupsJob implements IRepairStep {
+ /** @var IJobList */
+ protected $jobList;
+
+ public function __construct(IJobList $jobList) {
+ $this->jobList = $jobList;
+ }
+
+ public function getName() {
+ return 'Queue a one-time job to cleanup old backups of the updater';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(BackgroundCleanupUpdaterBackupsJob::class);
+ }
+}
diff --git a/lib/private/Repair/AddMetadataGenerationJob.php b/lib/private/Repair/AddMetadataGenerationJob.php
new file mode 100644
index 00000000000..76c60f303a7
--- /dev/null
+++ b/lib/private/Repair/AddMetadataGenerationJob.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OC\Core\BackgroundJobs\GenerateMetadataJob;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddMetadataGenerationJob implements IRepairStep {
+ public function __construct(
+ private IJobList $jobList,
+ ) {
+ }
+
+ public function getName() {
+ return 'Queue a job to generate metadata';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(GenerateMetadataJob::class);
+ }
+}
diff --git a/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php b/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php
new file mode 100644
index 00000000000..4ad320a0311
--- /dev/null
+++ b/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OC\TaskProcessing\RemoveOldTasksBackgroundJob;
+use OC\TextProcessing\RemoveOldTasksBackgroundJob as RemoveOldTextProcessingTasksBackgroundJob;
+use OC\TextToImage\RemoveOldTasksBackgroundJob as RemoveOldTextToImageTasksBackgroundJob;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddRemoveOldTasksBackgroundJob implements IRepairStep {
+ private IJobList $jobList;
+
+ public function __construct(IJobList $jobList) {
+ $this->jobList = $jobList;
+ }
+
+ public function getName(): string {
+ return 'Add AI tasks cleanup jobs';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(RemoveOldTextProcessingTasksBackgroundJob::class);
+ $this->jobList->add(RemoveOldTextToImageTasksBackgroundJob::class);
+ $this->jobList->add(RemoveOldTasksBackgroundJob::class);
+ }
+}
diff --git a/lib/private/Repair/CleanTags.php b/lib/private/Repair/CleanTags.php
new file mode 100644
index 00000000000..ad8fa6235e6
--- /dev/null
+++ b/lib/private/Repair/CleanTags.php
@@ -0,0 +1,186 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\IUserManager;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+/**
+ * Class RepairConfig
+ *
+ * @package OC\Repair
+ */
+class CleanTags implements IRepairStep {
+
+ protected $deletedTags = 0;
+
+ /**
+ * @param IDBConnection $connection
+ * @param IUserManager $userManager
+ */
+ public function __construct(
+ protected IDBConnection $connection,
+ protected IUserManager $userManager,
+ ) {
+ }
+
+ /**
+ * @return string
+ */
+ public function getName() {
+ return 'Clean tags and favorites';
+ }
+
+ /**
+ * Updates the configuration after running an update
+ */
+ public function run(IOutput $output) {
+ $this->deleteOrphanTags($output);
+ $this->deleteOrphanFileEntries($output);
+ $this->deleteOrphanTagEntries($output);
+ $this->deleteOrphanCategoryEntries($output);
+ }
+
+ /**
+ * Delete tags for deleted users
+ */
+ protected function deleteOrphanTags(IOutput $output) {
+ $offset = 0;
+ while ($this->checkTags($offset)) {
+ $offset += 50;
+ }
+
+ $output->info(sprintf('%d tags of deleted users have been removed.', $this->deletedTags));
+ }
+
+ protected function checkTags($offset) {
+ $query = $this->connection->getQueryBuilder();
+ $query->select('uid')
+ ->from('vcategory')
+ ->groupBy('uid')
+ ->orderBy('uid')
+ ->setMaxResults(50)
+ ->setFirstResult($offset);
+ $result = $query->executeQuery();
+
+ $users = [];
+ $hadResults = false;
+ while ($row = $result->fetch()) {
+ $hadResults = true;
+ if (!$this->userManager->userExists($row['uid'])) {
+ $users[] = $row['uid'];
+ }
+ }
+ $result->closeCursor();
+
+ if (!$hadResults) {
+ // No more tags, stop looping
+ return false;
+ }
+
+ if (!empty($users)) {
+ $query = $this->connection->getQueryBuilder();
+ $query->delete('vcategory')
+ ->where($query->expr()->in('uid', $query->createNamedParameter($users, IQueryBuilder::PARAM_STR_ARRAY)));
+ $this->deletedTags += $query->executeStatement();
+ }
+ return true;
+ }
+
+ /**
+ * Delete tag entries for deleted files
+ */
+ protected function deleteOrphanFileEntries(IOutput $output) {
+ $this->deleteOrphanEntries(
+ $output,
+ '%d tags for delete files have been removed.',
+ 'vcategory_to_object', 'objid',
+ 'filecache', 'fileid', 'fileid'
+ );
+ }
+
+ /**
+ * Delete tag entries for deleted tags
+ */
+ protected function deleteOrphanTagEntries(IOutput $output) {
+ $this->deleteOrphanEntries(
+ $output,
+ '%d tag entries for deleted tags have been removed.',
+ 'vcategory_to_object', 'categoryid',
+ 'vcategory', 'id', 'uid'
+ );
+ }
+
+ /**
+ * Delete tags that have no entries
+ */
+ protected function deleteOrphanCategoryEntries(IOutput $output) {
+ $this->deleteOrphanEntries(
+ $output,
+ '%d tags with no entries have been removed.',
+ 'vcategory', 'id',
+ 'vcategory_to_object', 'categoryid', 'type'
+ );
+ }
+
+ /**
+ * Deletes all entries from $deleteTable that do not have a matching entry in $sourceTable
+ *
+ * A query joins $deleteTable.$deleteId = $sourceTable.$sourceId and checks
+ * whether $sourceNullColumn is null. If it is null, the entry in $deleteTable
+ * is being deleted.
+ *
+ * @param string $repairInfo
+ * @param string $deleteTable
+ * @param string $deleteId
+ * @param string $sourceTable
+ * @param string $sourceId
+ * @param string $sourceNullColumn If this column is null in the source table,
+ * the entry is deleted in the $deleteTable
+ */
+ protected function deleteOrphanEntries(IOutput $output, $repairInfo, $deleteTable, $deleteId, $sourceTable, $sourceId, $sourceNullColumn) {
+ $qb = $this->connection->getQueryBuilder();
+
+ $qb->select('d.' . $deleteId)
+ ->from($deleteTable, 'd')
+ ->leftJoin('d', $sourceTable, 's', $qb->expr()->eq('d.' . $deleteId, 's.' . $sourceId))
+ ->where(
+ $qb->expr()->eq('d.type', $qb->expr()->literal('files'))
+ )
+ ->andWhere(
+ $qb->expr()->isNull('s.' . $sourceNullColumn)
+ );
+ $result = $qb->executeQuery();
+
+ $orphanItems = [];
+ while ($row = $result->fetch()) {
+ $orphanItems[] = (int)$row[$deleteId];
+ }
+
+ $deleteQuery = $this->connection->getQueryBuilder();
+ $deleteQuery->delete($deleteTable)
+ ->where(
+ $deleteQuery->expr()->eq('type', $deleteQuery->expr()->literal('files'))
+ )
+ ->andWhere($deleteQuery->expr()->in($deleteId, $deleteQuery->createParameter('ids')));
+ if (!empty($orphanItems)) {
+ $orphanItemsBatch = array_chunk($orphanItems, 200);
+ foreach ($orphanItemsBatch as $items) {
+ $deleteQuery->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY);
+ $deleteQuery->executeStatement();
+ }
+ }
+
+ if ($repairInfo) {
+ $output->info(sprintf($repairInfo, count($orphanItems)));
+ }
+ }
+}
diff --git a/lib/private/Repair/CleanUpAbandonedApps.php b/lib/private/Repair/CleanUpAbandonedApps.php
new file mode 100644
index 00000000000..718f625be86
--- /dev/null
+++ b/lib/private/Repair/CleanUpAbandonedApps.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class CleanUpAbandonedApps implements IRepairStep {
+ protected const ABANDONED_APPS = ['accessibility', 'files_videoplayer'];
+ private IConfig $config;
+
+ public function __construct(IConfig $config) {
+ $this->config = $config;
+ }
+
+ public function getName(): string {
+ return 'Clean up abandoned apps';
+ }
+
+ public function run(IOutput $output): void {
+ foreach (self::ABANDONED_APPS as $app) {
+ // only remove global app values
+ // user prefs of accessibility are dealt with in Theming migration
+ // videoplayer did not have user prefs
+ $this->config->deleteAppValues($app);
+ }
+ }
+}
diff --git a/lib/private/Repair/ClearFrontendCaches.php b/lib/private/Repair/ClearFrontendCaches.php
new file mode 100644
index 00000000000..5c57a63379d
--- /dev/null
+++ b/lib/private/Repair/ClearFrontendCaches.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OC\Template\JSCombiner;
+use OCP\ICacheFactory;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class ClearFrontendCaches implements IRepairStep {
+ /** @var ICacheFactory */
+ protected $cacheFactory;
+
+ /** @var JSCombiner */
+ protected $jsCombiner;
+
+ public function __construct(ICacheFactory $cacheFactory,
+ JSCombiner $JSCombiner) {
+ $this->cacheFactory = $cacheFactory;
+ $this->jsCombiner = $JSCombiner;
+ }
+
+ public function getName() {
+ return 'Clear frontend caches';
+ }
+
+ public function run(IOutput $output) {
+ try {
+ $c = $this->cacheFactory->createDistributed('imagePath');
+ $c->clear();
+ $output->info('Image cache cleared');
+
+ $this->jsCombiner->resetCache();
+ $output->info('JS cache cleared');
+ } catch (\Exception $e) {
+ $output->warning('Unable to clear the frontend cache');
+ }
+ }
+}
diff --git a/lib/private/Repair/ClearGeneratedAvatarCache.php b/lib/private/Repair/ClearGeneratedAvatarCache.php
new file mode 100644
index 00000000000..0f743afbb4c
--- /dev/null
+++ b/lib/private/Repair/ClearGeneratedAvatarCache.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OC\Avatar\AvatarManager;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class ClearGeneratedAvatarCache implements IRepairStep {
+ protected AvatarManager $avatarManager;
+ private IConfig $config;
+ private IJobList $jobList;
+
+ public function __construct(IConfig $config, AvatarManager $avatarManager, IJobList $jobList) {
+ $this->config = $config;
+ $this->avatarManager = $avatarManager;
+ $this->jobList = $jobList;
+ }
+
+ public function getName(): string {
+ return 'Clear every generated avatar';
+ }
+
+ /**
+ * Check if this repair step should run
+ */
+ private function shouldRun(): bool {
+ $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0.0');
+
+ // This job only runs if the server was on a version lower than or equal to 27.0.0 before the upgrade.
+ // To clear the avatar cache again, bump the version to the currently released version (and change the operator to <= if it's not the master branch) and wait for the next release.
+ return version_compare($versionFromBeforeUpdate, '27.0.0', '<');
+ }
+
+ public function run(IOutput $output): void {
+ if ($this->shouldRun()) {
+ try {
+ $this->jobList->add(ClearGeneratedAvatarCacheJob::class, []);
+ $output->info('Avatar cache clearing job added');
+ } catch (\Exception $e) {
+ $output->warning('Unable to clear the avatar cache');
+ }
+ }
+ }
+}
diff --git a/lib/private/Repair/ClearGeneratedAvatarCacheJob.php b/lib/private/Repair/ClearGeneratedAvatarCacheJob.php
new file mode 100644
index 00000000000..524a470e62a
--- /dev/null
+++ b/lib/private/Repair/ClearGeneratedAvatarCacheJob.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OC\Avatar\AvatarManager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\QueuedJob;
+
+class ClearGeneratedAvatarCacheJob extends QueuedJob {
+ protected AvatarManager $avatarManager;
+
+ public function __construct(ITimeFactory $timeFactory, AvatarManager $avatarManager) {
+ parent::__construct($timeFactory);
+ $this->avatarManager = $avatarManager;
+ }
+
+ public function run($argument) {
+ $this->avatarManager->clearCachedAvatars();
+ }
+}
diff --git a/lib/private/Repair/Collation.php b/lib/private/Repair/Collation.php
new file mode 100644
index 00000000000..43229792217
--- /dev/null
+++ b/lib/private/Repair/Collation.php
@@ -0,0 +1,129 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair;
+
+use Doctrine\DBAL\Exception\DriverException;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Psr\Log\LoggerInterface;
+
+class Collation implements IRepairStep {
+ /** @var IConfig */
+ protected $config;
+
+ protected LoggerInterface $logger;
+
+ /** @var IDBConnection */
+ protected $connection;
+
+ /** @var bool */
+ protected $ignoreFailures;
+
+ /**
+ * @param bool $ignoreFailures
+ */
+ public function __construct(
+ IConfig $config,
+ LoggerInterface $logger,
+ IDBConnection $connection,
+ $ignoreFailures,
+ ) {
+ $this->connection = $connection;
+ $this->config = $config;
+ $this->logger = $logger;
+ $this->ignoreFailures = $ignoreFailures;
+ }
+
+ public function getName() {
+ return 'Repair MySQL collation';
+ }
+
+ /**
+ * Fix mime types
+ */
+ public function run(IOutput $output) {
+ if ($this->connection->getDatabaseProvider() !== IDBConnection::PLATFORM_MYSQL) {
+ $output->info('Not a mysql database -> nothing to do');
+ return;
+ }
+
+ $characterSet = $this->config->getSystemValueBool('mysql.utf8mb4', false) ? 'utf8mb4' : 'utf8';
+
+ $tables = $this->getAllNonUTF8BinTables($this->connection);
+ foreach ($tables as $table) {
+ $output->info("Change row format for $table ...");
+ $query = $this->connection->prepare('ALTER TABLE `' . $table . '` ROW_FORMAT = DYNAMIC;');
+ try {
+ $query->execute();
+ } catch (DriverException $e) {
+ // Just log this
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ if (!$this->ignoreFailures) {
+ throw $e;
+ }
+ }
+
+ $output->info("Change collation for $table ...");
+ $query = $this->connection->prepare('ALTER TABLE `' . $table . '` CONVERT TO CHARACTER SET ' . $characterSet . ' COLLATE ' . $characterSet . '_bin;');
+ try {
+ $query->execute();
+ } catch (DriverException $e) {
+ // Just log this
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ if (!$this->ignoreFailures) {
+ throw $e;
+ }
+ }
+ }
+ if (empty($tables)) {
+ $output->info('All tables already have the correct collation -> nothing to do');
+ }
+ }
+
+ /**
+ * @param IDBConnection $connection
+ * @return string[]
+ */
+ protected function getAllNonUTF8BinTables(IDBConnection $connection) {
+ $dbName = $this->config->getSystemValueString('dbname');
+ $characterSet = $this->config->getSystemValueBool('mysql.utf8mb4', false) ? 'utf8mb4' : 'utf8';
+
+ // fetch tables by columns
+ $statement = $connection->executeQuery(
+ 'SELECT DISTINCT(TABLE_NAME) AS `table`'
+ . ' FROM INFORMATION_SCHEMA . COLUMNS'
+ . ' WHERE TABLE_SCHEMA = ?'
+ . " AND (COLLATION_NAME <> '" . $characterSet . "_bin' OR CHARACTER_SET_NAME <> '" . $characterSet . "')"
+ . " AND TABLE_NAME LIKE '*PREFIX*%'",
+ [$dbName]
+ );
+ $rows = $statement->fetchAll();
+ $result = [];
+ foreach ($rows as $row) {
+ $result[$row['table']] = true;
+ }
+
+ // fetch tables by collation
+ $statement = $connection->executeQuery(
+ 'SELECT DISTINCT(TABLE_NAME) AS `table`'
+ . ' FROM INFORMATION_SCHEMA . TABLES'
+ . ' WHERE TABLE_SCHEMA = ?'
+ . " AND TABLE_COLLATION <> '" . $characterSet . "_bin'"
+ . " AND TABLE_NAME LIKE '*PREFIX*%'",
+ [$dbName]
+ );
+ $rows = $statement->fetchAll();
+ foreach ($rows as $row) {
+ $result[$row['table']] = true;
+ }
+
+ return array_keys($result);
+ }
+}
diff --git a/lib/private/Repair/ConfigKeyMigration.php b/lib/private/Repair/ConfigKeyMigration.php
new file mode 100644
index 00000000000..da4aa153dc5
--- /dev/null
+++ b/lib/private/Repair/ConfigKeyMigration.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Repair;
+
+use OC\Config\ConfigManager;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class ConfigKeyMigration implements IRepairStep {
+ public function __construct(
+ private ConfigManager $configManager,
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Migrate config keys';
+ }
+
+ public function run(IOutput $output) {
+ $this->configManager->migrateConfigLexiconKeys();
+ }
+}
diff --git a/lib/private/Repair/Events/RepairAdvanceEvent.php b/lib/private/Repair/Events/RepairAdvanceEvent.php
new file mode 100644
index 00000000000..476db9e4702
--- /dev/null
+++ b/lib/private/Repair/Events/RepairAdvanceEvent.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair\Events;
+
+use OCP\EventDispatcher\Event;
+
+class RepairAdvanceEvent extends Event {
+ private int $increment;
+ private string $description;
+
+ public function __construct(
+ int $increment,
+ string $description,
+ ) {
+ $this->increment = $increment;
+ $this->description = $description;
+ }
+
+ public function getIncrement(): int {
+ return $this->increment;
+ }
+
+ public function getDescription(): string {
+ return $this->description;
+ }
+}
diff --git a/lib/private/Repair/Events/RepairErrorEvent.php b/lib/private/Repair/Events/RepairErrorEvent.php
new file mode 100644
index 00000000000..e5be8a5a031
--- /dev/null
+++ b/lib/private/Repair/Events/RepairErrorEvent.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair\Events;
+
+use OCP\EventDispatcher\Event;
+
+class RepairErrorEvent extends Event {
+ private string $message;
+
+ public function __construct(
+ string $message,
+ ) {
+ $this->message = $message;
+ }
+
+ public function getMessage(): string {
+ return $this->message;
+ }
+}
diff --git a/lib/private/Repair/Events/RepairFinishEvent.php b/lib/private/Repair/Events/RepairFinishEvent.php
new file mode 100644
index 00000000000..767a7506e6f
--- /dev/null
+++ b/lib/private/Repair/Events/RepairFinishEvent.php
@@ -0,0 +1,14 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair\Events;
+
+use OCP\EventDispatcher\Event;
+
+class RepairFinishEvent extends Event {
+}
diff --git a/lib/private/Repair/Events/RepairInfoEvent.php b/lib/private/Repair/Events/RepairInfoEvent.php
new file mode 100644
index 00000000000..ce8eb2f99e6
--- /dev/null
+++ b/lib/private/Repair/Events/RepairInfoEvent.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair\Events;
+
+use OCP\EventDispatcher\Event;
+
+class RepairInfoEvent extends Event {
+ private string $message;
+
+ public function __construct(
+ string $message,
+ ) {
+ $this->message = $message;
+ }
+
+ public function getMessage(): string {
+ return $this->message;
+ }
+}
diff --git a/lib/private/Repair/Events/RepairStartEvent.php b/lib/private/Repair/Events/RepairStartEvent.php
new file mode 100644
index 00000000000..47e713d57d9
--- /dev/null
+++ b/lib/private/Repair/Events/RepairStartEvent.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair\Events;
+
+use OCP\EventDispatcher\Event;
+
+class RepairStartEvent extends Event {
+ private int $max;
+ private string $current;
+
+ public function __construct(
+ int $max,
+ string $current,
+ ) {
+ $this->max = $max;
+ $this->current = $current;
+ }
+
+ public function getMaxStep(): int {
+ return $this->max;
+ }
+
+ public function getCurrentStepName(): string {
+ return $this->current;
+ }
+}
diff --git a/lib/private/Repair/Events/RepairStepEvent.php b/lib/private/Repair/Events/RepairStepEvent.php
new file mode 100644
index 00000000000..27e1efbdb08
--- /dev/null
+++ b/lib/private/Repair/Events/RepairStepEvent.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair\Events;
+
+use OCP\EventDispatcher\Event;
+
+class RepairStepEvent extends Event {
+ private string $stepName;
+
+ public function __construct(
+ string $stepName,
+ ) {
+ $this->stepName = $stepName;
+ }
+
+ public function getStepName(): string {
+ return $this->stepName;
+ }
+}
diff --git a/lib/private/Repair/Events/RepairWarningEvent.php b/lib/private/Repair/Events/RepairWarningEvent.php
new file mode 100644
index 00000000000..6893a7212ec
--- /dev/null
+++ b/lib/private/Repair/Events/RepairWarningEvent.php
@@ -0,0 +1,25 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair\Events;
+
+use OCP\EventDispatcher\Event;
+
+class RepairWarningEvent extends Event {
+ private string $message;
+
+ public function __construct(
+ string $message,
+ ) {
+ $this->message = $message;
+ }
+
+ public function getMessage(): string {
+ return $this->message;
+ }
+}
diff --git a/lib/private/Repair/MoveUpdaterStepFile.php b/lib/private/Repair/MoveUpdaterStepFile.php
new file mode 100644
index 00000000000..bb8f9d3acfc
--- /dev/null
+++ b/lib/private/Repair/MoveUpdaterStepFile.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OCP\Files;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class MoveUpdaterStepFile implements IRepairStep {
+ /** @var \OCP\IConfig */
+ protected $config;
+
+ /**
+ * @param \OCP\IConfig $config
+ */
+ public function __construct($config) {
+ $this->config = $config;
+ }
+
+ public function getName() {
+ return 'Move .step file of updater to backup location';
+ }
+
+ public function run(IOutput $output) {
+ $updateDir = $this->config->getSystemValue('updatedirectory', null) ?? $this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data');
+ $instanceId = $this->config->getSystemValueString('instanceid');
+
+ if (empty($instanceId)) {
+ return;
+ }
+
+ $updaterFolderPath = $updateDir . '/updater-' . $instanceId;
+ $stepFile = $updaterFolderPath . '/.step';
+ if (file_exists($stepFile)) {
+ $output->info('.step file exists');
+
+ $previousStepFile = $updaterFolderPath . '/.step-previous-update';
+
+ // cleanup
+ if (file_exists($previousStepFile)) {
+ if (Files::rmdirr($previousStepFile)) {
+ $output->info('.step-previous-update removed');
+ } else {
+ $output->info('.step-previous-update can\'t be removed - abort move of .step file');
+ return;
+ }
+ }
+
+ // move step file
+ if (rename($stepFile, $previousStepFile)) {
+ $output->info('.step file moved to .step-previous-update');
+ } else {
+ $output->warning('.step file can\'t be moved');
+ }
+ }
+ }
+}
diff --git a/lib/private/Repair/NC13/AddLogRotateJob.php b/lib/private/Repair/NC13/AddLogRotateJob.php
new file mode 100644
index 00000000000..bd6c510785f
--- /dev/null
+++ b/lib/private/Repair/NC13/AddLogRotateJob.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC13;
+
+use OC\Log\Rotate;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddLogRotateJob implements IRepairStep {
+ /** @var IJobList */
+ private $jobList;
+
+ public function __construct(IJobList $jobList) {
+ $this->jobList = $jobList;
+ }
+
+ public function getName() {
+ return 'Add log rotate job';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(Rotate::class);
+ }
+}
diff --git a/lib/private/Repair/NC14/AddPreviewBackgroundCleanupJob.php b/lib/private/Repair/NC14/AddPreviewBackgroundCleanupJob.php
new file mode 100644
index 00000000000..417bc5e6adc
--- /dev/null
+++ b/lib/private/Repair/NC14/AddPreviewBackgroundCleanupJob.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC14;
+
+use OC\Preview\BackgroundCleanupJob;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddPreviewBackgroundCleanupJob implements IRepairStep {
+ /** @var IJobList */
+ private $jobList;
+
+ public function __construct(IJobList $jobList) {
+ $this->jobList = $jobList;
+ }
+
+ public function getName(): string {
+ return 'Add preview background cleanup job';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(BackgroundCleanupJob::class);
+ }
+}
diff --git a/lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php b/lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php
new file mode 100644
index 00000000000..ab5f93415fc
--- /dev/null
+++ b/lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php
@@ -0,0 +1,31 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC16;
+
+use OC\Core\BackgroundJobs\CleanupLoginFlowV2;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddClenupLoginFlowV2BackgroundJob implements IRepairStep {
+ /** @var IJobList */
+ private $jobList;
+
+ public function __construct(IJobList $jobList) {
+ $this->jobList = $jobList;
+ }
+
+ public function getName(): string {
+ return 'Add background job to cleanup login flow v2 tokens';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(CleanupLoginFlowV2::class);
+ }
+}
diff --git a/lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php b/lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php
new file mode 100644
index 00000000000..646dd2c5e83
--- /dev/null
+++ b/lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php
@@ -0,0 +1,88 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Repair\NC16;
+
+use OCP\Files\AppData\IAppDataFactory;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+
+/**
+ * Class CleanupCardDAVPhotoCache
+ *
+ * This repair step removes "photo." files created by photocache
+ *
+ * Before https://github.com/nextcloud/server/pull/13843 a "photo." file could be created
+ * for unsupported image formats by photocache. Because a file is present but not jpg, png or gif no
+ * photo could be returned for this vcard. These invalid files are removed by this migration step.
+ */
+class CleanupCardDAVPhotoCache implements IRepairStep {
+ public function __construct(
+ private IConfig $config,
+ private IAppDataFactory $appDataFactory,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Cleanup invalid photocache files for carddav';
+ }
+
+ private function repair(IOutput $output): void {
+ $photoCacheAppData = $this->appDataFactory->get('dav-photocache');
+
+ try {
+ $folders = $photoCacheAppData->getDirectoryListing();
+ } catch (NotFoundException $e) {
+ return;
+ } catch (RuntimeException $e) {
+ $this->logger->error('Failed to fetch directory listing in CleanupCardDAVPhotoCache', ['exception' => $e]);
+ return;
+ }
+
+ $folders = array_filter($folders, function (ISimpleFolder $folder) {
+ return $folder->fileExists('photo.');
+ });
+
+ if (empty($folders)) {
+ return;
+ }
+
+ $output->info('Delete ' . count($folders) . ' "photo." files');
+
+ foreach ($folders as $folder) {
+ try {
+ /** @var ISimpleFolder $folder */
+ $folder->getFile('photo.')->delete();
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ $output->warning('Could not delete file "dav-photocache/' . $folder->getName() . '/photo."');
+ }
+ }
+ }
+
+ private function shouldRun(): bool {
+ return version_compare(
+ $this->config->getSystemValueString('version', '0.0.0.0'),
+ '16.0.0.0',
+ '<='
+ );
+ }
+
+ public function run(IOutput $output): void {
+ if ($this->shouldRun()) {
+ $this->repair($output);
+ }
+ }
+}
diff --git a/lib/private/Repair/NC16/ClearCollectionsAccessCache.php b/lib/private/Repair/NC16/ClearCollectionsAccessCache.php
new file mode 100644
index 00000000000..1627ed40b98
--- /dev/null
+++ b/lib/private/Repair/NC16/ClearCollectionsAccessCache.php
@@ -0,0 +1,43 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC16;
+
+use OC\Collaboration\Resources\Manager;
+use OCP\Collaboration\Resources\IManager;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class ClearCollectionsAccessCache implements IRepairStep {
+ /** @var IConfig */
+ private $config;
+
+ /** @var Manager */
+ private $manager;
+
+ public function __construct(IConfig $config, IManager $manager) {
+ $this->config = $config;
+ $this->manager = $manager;
+ }
+
+ public function getName(): string {
+ return 'Clear access cache of projects';
+ }
+
+ private function shouldRun(): bool {
+ $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0.0');
+ return version_compare($versionFromBeforeUpdate, '17.0.0.3', '<=');
+ }
+
+ public function run(IOutput $output): void {
+ if ($this->shouldRun()) {
+ $this->manager->invalidateAccessCacheForAllCollections();
+ }
+ }
+}
diff --git a/lib/private/Repair/NC18/ResetGeneratedAvatarFlag.php b/lib/private/Repair/NC18/ResetGeneratedAvatarFlag.php
new file mode 100644
index 00000000000..b0dfec295e7
--- /dev/null
+++ b/lib/private/Repair/NC18/ResetGeneratedAvatarFlag.php
@@ -0,0 +1,45 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC18;
+
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class ResetGeneratedAvatarFlag implements IRepairStep {
+ /** @var IConfig */
+ private $config;
+ /** @var IDBConnection */
+ private $connection;
+
+ public function __construct(IConfig $config,
+ IDBConnection $connection) {
+ $this->config = $config;
+ $this->connection = $connection;
+ }
+
+ public function getName(): string {
+ return 'Reset generated avatar flag';
+ }
+
+ private function shouldRun(): bool {
+ $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0.0');
+ return version_compare($versionFromBeforeUpdate, '18.0.0.5', '<=');
+ }
+
+ public function run(IOutput $output): void {
+ if ($this->shouldRun()) {
+ $query = $this->connection->getQueryBuilder();
+ $query->delete('preferences')
+ ->where($query->expr()->eq('appid', $query->createNamedParameter('avatar')))
+ ->andWhere($query->expr()->eq('configkey', $query->createNamedParameter('generated')));
+ }
+ }
+}
diff --git a/lib/private/Repair/NC20/EncryptionLegacyCipher.php b/lib/private/Repair/NC20/EncryptionLegacyCipher.php
new file mode 100644
index 00000000000..89473ffd5e8
--- /dev/null
+++ b/lib/private/Repair/NC20/EncryptionLegacyCipher.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC20;
+
+use OCP\Encryption\IManager;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class EncryptionLegacyCipher implements IRepairStep {
+ /** @var IConfig */
+ private $config;
+ /** @var IManager */
+ private $manager;
+
+ public function __construct(IConfig $config,
+ IManager $manager) {
+ $this->config = $config;
+ $this->manager = $manager;
+ }
+
+ public function getName(): string {
+ return 'Keep legacy encryption enabled';
+ }
+
+ private function shouldRun(): bool {
+ $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0.0');
+ return version_compare($versionFromBeforeUpdate, '20.0.0.0', '<=');
+ }
+
+ public function run(IOutput $output): void {
+ if (!$this->shouldRun()) {
+ return;
+ }
+
+ $masterKeyId = $this->config->getAppValue('encryption', 'masterKeyId');
+ if ($this->manager->isEnabled() || !empty($masterKeyId)) {
+ if ($this->config->getSystemValue('encryption.legacy_format_support', '') === '') {
+ $this->config->setSystemValue('encryption.legacy_format_support', true);
+ }
+ }
+ }
+}
diff --git a/lib/private/Repair/NC20/EncryptionMigration.php b/lib/private/Repair/NC20/EncryptionMigration.php
new file mode 100644
index 00000000000..6de143747b3
--- /dev/null
+++ b/lib/private/Repair/NC20/EncryptionMigration.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC20;
+
+use OCP\Encryption\IManager;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class EncryptionMigration implements IRepairStep {
+ /** @var IConfig */
+ private $config;
+ /** @var IManager */
+ private $manager;
+
+ public function __construct(IConfig $config,
+ IManager $manager) {
+ $this->config = $config;
+ $this->manager = $manager;
+ }
+
+ public function getName(): string {
+ return 'Check encryption key format';
+ }
+
+ private function shouldRun(): bool {
+ $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0.0');
+ return version_compare($versionFromBeforeUpdate, '20.0.0.1', '<=');
+ }
+
+ public function run(IOutput $output): void {
+ if (!$this->shouldRun()) {
+ return;
+ }
+
+ $masterKeyId = $this->config->getAppValue('encryption', 'masterKeyId');
+ if ($this->manager->isEnabled() || !empty($masterKeyId)) {
+ if ($this->config->getSystemValue('encryption.key_storage_migrated', '') === '') {
+ $this->config->setSystemValue('encryption.key_storage_migrated', false);
+ }
+ }
+ }
+}
diff --git a/lib/private/Repair/NC20/ShippedDashboardEnable.php b/lib/private/Repair/NC20/ShippedDashboardEnable.php
new file mode 100644
index 00000000000..955011a8c84
--- /dev/null
+++ b/lib/private/Repair/NC20/ShippedDashboardEnable.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC20;
+
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class ShippedDashboardEnable implements IRepairStep {
+ /** @var IConfig */
+ private $config;
+
+ public function __construct(IConfig $config) {
+ $this->config = $config;
+ }
+
+ public function getName() {
+ return 'Remove old dashboard app config data';
+ }
+
+ public function run(IOutput $output) {
+ $version = $this->config->getAppValue('dashboard', 'version', '7.0.0');
+ if (version_compare($version, '7.0.0', '<')) {
+ $this->config->deleteAppValues('dashboard');
+ $output->info('Removed old dashboard app config');
+ }
+ }
+}
diff --git a/lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php b/lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php
new file mode 100644
index 00000000000..5cee33b381c
--- /dev/null
+++ b/lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC21;
+
+use OC\Core\BackgroundJobs\CheckForUserCertificates;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddCheckForUserCertificatesJob implements IRepairStep {
+ /** @var IJobList */
+ protected $jobList;
+ /** @var IConfig */
+ private $config;
+
+ public function __construct(IConfig $config, IJobList $jobList) {
+ $this->jobList = $jobList;
+ $this->config = $config;
+ }
+
+ public function getName() {
+ return 'Queue a one-time job to check for user uploaded certificates';
+ }
+
+ private function shouldRun() {
+ $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0.0');
+
+ // was added to 21.0.0.2
+ return version_compare($versionFromBeforeUpdate, '21.0.0.2', '<');
+ }
+
+ public function run(IOutput $output) {
+ if ($this->shouldRun()) {
+ $this->config->setAppValue('files_external', 'user_certificate_scan', 'not-run-yet');
+ $this->jobList->add(CheckForUserCertificates::class);
+ }
+ }
+}
diff --git a/lib/private/Repair/NC22/LookupServerSendCheck.php b/lib/private/Repair/NC22/LookupServerSendCheck.php
new file mode 100644
index 00000000000..540dc2a730d
--- /dev/null
+++ b/lib/private/Repair/NC22/LookupServerSendCheck.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC22;
+
+use OC\Core\BackgroundJobs\LookupServerSendCheckBackgroundJob;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class LookupServerSendCheck implements IRepairStep {
+ private IJobList $jobList;
+ private IConfig $config;
+
+ public function __construct(IJobList $jobList, IConfig $config) {
+ $this->jobList = $jobList;
+ $this->config = $config;
+ }
+
+ public function getName(): string {
+ return 'Add background job to set the lookup server share state for users';
+ }
+
+ public function run(IOutput $output): void {
+ $this->jobList->add(LookupServerSendCheckBackgroundJob::class);
+ }
+}
diff --git a/lib/private/Repair/NC24/AddTokenCleanupJob.php b/lib/private/Repair/NC24/AddTokenCleanupJob.php
new file mode 100644
index 00000000000..f1dac2d4e12
--- /dev/null
+++ b/lib/private/Repair/NC24/AddTokenCleanupJob.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC24;
+
+use OC\Authentication\Token\TokenCleanupJob;
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class AddTokenCleanupJob implements IRepairStep {
+ private IJobList $jobList;
+
+ public function __construct(IJobList $jobList) {
+ $this->jobList = $jobList;
+ }
+
+ public function getName(): string {
+ return 'Add token cleanup job';
+ }
+
+ public function run(IOutput $output) {
+ $this->jobList->add(TokenCleanupJob::class);
+ }
+}
diff --git a/lib/private/Repair/NC25/AddMissingSecretJob.php b/lib/private/Repair/NC25/AddMissingSecretJob.php
new file mode 100644
index 00000000000..46b89d5f6f7
--- /dev/null
+++ b/lib/private/Repair/NC25/AddMissingSecretJob.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC25;
+
+use OCP\HintException;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use OCP\Security\ISecureRandom;
+
+class AddMissingSecretJob implements IRepairStep {
+ private IConfig $config;
+ private ISecureRandom $random;
+
+ public function __construct(IConfig $config, ISecureRandom $random) {
+ $this->config = $config;
+ $this->random = $random;
+ }
+
+ public function getName(): string {
+ return 'Add possibly missing system config';
+ }
+
+ public function run(IOutput $output): void {
+ $passwordSalt = $this->config->getSystemValueString('passwordsalt', '');
+ if ($passwordSalt === '') {
+ try {
+ $this->config->setSystemValue('passwordsalt', $this->random->generate(30));
+ } catch (HintException $e) {
+ $output->warning('passwordsalt is missing from your config.php and your config.php is read only. Please fix it manually.');
+ }
+ }
+
+ $secret = $this->config->getSystemValueString('secret', '');
+ if ($secret === '') {
+ try {
+ $this->config->setSystemValue('secret', $this->random->generate(48));
+ } catch (HintException $e) {
+ $output->warning('secret is missing from your config.php and your config.php is read only. Please fix it manually.');
+ }
+ }
+ }
+}
diff --git a/lib/private/Repair/NC29/SanitizeAccountProperties.php b/lib/private/Repair/NC29/SanitizeAccountProperties.php
new file mode 100644
index 00000000000..412570ba71d
--- /dev/null
+++ b/lib/private/Repair/NC29/SanitizeAccountProperties.php
@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC29;
+
+use OCP\BackgroundJob\IJobList;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class SanitizeAccountProperties implements IRepairStep {
+
+ public function __construct(
+ private IJobList $jobList,
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Validate account properties and store phone numbers in a known format for search';
+ }
+
+ public function run(IOutput $output): void {
+ $this->jobList->add(SanitizeAccountPropertiesJob::class, null);
+ $output->info('Queued background to validate account properties.');
+ }
+}
diff --git a/lib/private/Repair/NC29/SanitizeAccountPropertiesJob.php b/lib/private/Repair/NC29/SanitizeAccountPropertiesJob.php
new file mode 100644
index 00000000000..55ec445e9da
--- /dev/null
+++ b/lib/private/Repair/NC29/SanitizeAccountPropertiesJob.php
@@ -0,0 +1,75 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC29;
+
+use InvalidArgumentException;
+use OCP\Accounts\IAccountManager;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\QueuedJob;
+use OCP\IUser;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+
+class SanitizeAccountPropertiesJob extends QueuedJob {
+
+ private const PROPERTIES_TO_CHECK = [
+ IAccountManager::PROPERTY_PHONE,
+ IAccountManager::PROPERTY_WEBSITE,
+ IAccountManager::PROPERTY_TWITTER,
+ IAccountManager::PROPERTY_FEDIVERSE,
+ ];
+
+ public function __construct(
+ ITimeFactory $timeFactory,
+ private IUserManager $userManager,
+ private IAccountManager $accountManager,
+ private LoggerInterface $logger,
+ ) {
+ parent::__construct($timeFactory);
+ $this->setAllowParallelRuns(false);
+ }
+
+ protected function run(mixed $argument): void {
+ $numRemoved = 0;
+
+ $this->userManager->callForSeenUsers(function (IUser $user) use (&$numRemoved) {
+ $account = $this->accountManager->getAccount($user);
+ $properties = array_keys($account->jsonSerialize());
+
+ // Check if there are some properties we can sanitize - reduces number of db queries
+ if (empty(array_intersect($properties, self::PROPERTIES_TO_CHECK))) {
+ return;
+ }
+
+ // Limit the loop to the properties we check to ensure there are no infinite loops
+ // we add one additional loop (+ 1) as we need 1 loop for checking + 1 for update.
+ $iteration = count(self::PROPERTIES_TO_CHECK) + 1;
+ while ($iteration-- > 0) {
+ try {
+ $this->accountManager->updateAccount($account);
+ return;
+ } catch (InvalidArgumentException $e) {
+ if (in_array($e->getMessage(), IAccountManager::ALLOWED_PROPERTIES)) {
+ $numRemoved++;
+ $property = $account->getProperty($e->getMessage());
+ $account->setProperty($property->getName(), '', $property->getScope(), IAccountManager::NOT_VERIFIED);
+ } else {
+ $this->logger->error('Error while sanitizing account property', ['exception' => $e, 'user' => $user->getUID()]);
+ return;
+ }
+ }
+ }
+ $this->logger->error('Iteration limit exceeded while cleaning account properties', ['user' => $user->getUID()]);
+ });
+
+ if ($numRemoved > 0) {
+ $this->logger->info('Cleaned ' . $numRemoved . ' invalid account property entries');
+ }
+ }
+}
diff --git a/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php b/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php
new file mode 100644
index 00000000000..623163927bd
--- /dev/null
+++ b/lib/private/Repair/NC30/RemoveLegacyDatadirFile.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\NC30;
+
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class RemoveLegacyDatadirFile implements IRepairStep {
+
+ public function __construct(
+ private IConfig $config,
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Remove legacy ".ocdata" file';
+ }
+
+ public function run(IOutput $output): void {
+ $ocdata = $this->config->getSystemValueString('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata';
+ if (file_exists($ocdata)) {
+ unlink($ocdata);
+ }
+ }
+}
diff --git a/lib/private/Repair/OldGroupMembershipShares.php b/lib/private/Repair/OldGroupMembershipShares.php
new file mode 100644
index 00000000000..003c15cfb88
--- /dev/null
+++ b/lib/private/Repair/OldGroupMembershipShares.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair;
+
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use OCP\Share\IShare;
+
+class OldGroupMembershipShares implements IRepairStep {
+ /**
+ * @var array [gid => [uid => (bool)]]
+ */
+ protected $memberships;
+
+ /**
+ * @param IDBConnection $connection
+ * @param IGroupManager $groupManager
+ */
+ public function __construct(
+ protected IDBConnection $connection,
+ protected IGroupManager $groupManager,
+ ) {
+ }
+
+ /**
+ * Returns the step's name
+ *
+ * @return string
+ */
+ public function getName() {
+ return 'Remove shares of old group memberships';
+ }
+
+ /**
+ * Run repair step.
+ * Must throw exception on error.
+ *
+ * @throws \Exception in case of failure
+ */
+ public function run(IOutput $output) {
+ $deletedEntries = 0;
+
+ $query = $this->connection->getQueryBuilder();
+ $query->select('s1.id')->selectAlias('s1.share_with', 'user')->selectAlias('s2.share_with', 'group')
+ ->from('share', 's1')
+ ->where($query->expr()->isNotNull('s1.parent'))
+ // \OC\Share\Constant::$shareTypeGroupUserUnique === 2
+ ->andWhere($query->expr()->eq('s1.share_type', $query->expr()->literal(2)))
+ ->andWhere($query->expr()->isNotNull('s2.id'))
+ ->andWhere($query->expr()->eq('s2.share_type', $query->expr()->literal(IShare::TYPE_GROUP)))
+ ->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id'));
+
+ $deleteQuery = $this->connection->getQueryBuilder();
+ $deleteQuery->delete('share')
+ ->where($query->expr()->eq('id', $deleteQuery->createParameter('share')));
+
+ $result = $query->executeQuery();
+ while ($row = $result->fetch()) {
+ if (!$this->isMember($row['group'], $row['user'])) {
+ $deletedEntries += $deleteQuery->setParameter('share', (int)$row['id'])
+ ->executeStatement();
+ }
+ }
+ $result->closeCursor();
+
+ if ($deletedEntries) {
+ $output->info('Removed ' . $deletedEntries . ' shares where user is not a member of the group anymore');
+ }
+ }
+
+ /**
+ * @param string $gid
+ * @param string $uid
+ * @return bool
+ */
+ protected function isMember($gid, $uid) {
+ if (isset($this->memberships[$gid][$uid])) {
+ return $this->memberships[$gid][$uid];
+ }
+
+ $isMember = $this->groupManager->isInGroup($uid, $gid);
+ if (!isset($this->memberships[$gid])) {
+ $this->memberships[$gid] = [];
+ }
+ $this->memberships[$gid][$uid] = $isMember;
+
+ return $isMember;
+ }
+}
diff --git a/lib/private/Repair/Owncloud/CleanPreviews.php b/lib/private/Repair/Owncloud/CleanPreviews.php
new file mode 100644
index 00000000000..50ee965e087
--- /dev/null
+++ b/lib/private/Repair/Owncloud/CleanPreviews.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\Owncloud;
+
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class CleanPreviews implements IRepairStep {
+ /** @var IJobList */
+ private $jobList;
+
+ /** @var IUserManager */
+ private $userManager;
+
+ /** @var IConfig */
+ private $config;
+
+ /**
+ * MoveAvatars constructor.
+ *
+ * @param IJobList $jobList
+ * @param IUserManager $userManager
+ * @param IConfig $config
+ */
+ public function __construct(IJobList $jobList,
+ IUserManager $userManager,
+ IConfig $config) {
+ $this->jobList = $jobList;
+ $this->userManager = $userManager;
+ $this->config = $config;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName() {
+ return 'Add preview cleanup background jobs';
+ }
+
+ public function run(IOutput $output) {
+ if (!$this->config->getAppValue('core', 'previewsCleanedUp', false)) {
+ $this->userManager->callForSeenUsers(function (IUser $user) {
+ $this->jobList->add(CleanPreviewsBackgroundJob::class, ['uid' => $user->getUID()]);
+ });
+ $this->config->setAppValue('core', 'previewsCleanedUp', '1');
+ }
+ }
+}
diff --git a/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php b/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php
new file mode 100644
index 00000000000..6c606453bb9
--- /dev/null
+++ b/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php
@@ -0,0 +1,91 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\Owncloud;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\BackgroundJob\QueuedJob;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+
+class CleanPreviewsBackgroundJob extends QueuedJob {
+ public function __construct(
+ private IRootFolder $rootFolder,
+ private LoggerInterface $logger,
+ private IJobList $jobList,
+ ITimeFactory $timeFactory,
+ private IUserManager $userManager,
+ ) {
+ parent::__construct($timeFactory);
+ }
+
+ public function run($arguments) {
+ $uid = $arguments['uid'];
+ if (!$this->userManager->userExists($uid)) {
+ $this->logger->info('User no longer exists, skip user ' . $uid);
+ return;
+ }
+ $this->logger->info('Started preview cleanup for ' . $uid);
+ $empty = $this->cleanupPreviews($uid);
+
+ if (!$empty) {
+ $this->jobList->add(self::class, ['uid' => $uid]);
+ $this->logger->info('New preview cleanup scheduled for ' . $uid);
+ } else {
+ $this->logger->info('Preview cleanup done for ' . $uid);
+ }
+ }
+
+ /**
+ * @param string $uid
+ */
+ private function cleanupPreviews($uid): bool {
+ try {
+ $userFolder = $this->rootFolder->getUserFolder($uid);
+ } catch (NotFoundException $e) {
+ return true;
+ }
+
+ $userRoot = $userFolder->getParent();
+
+ try {
+ /** @var Folder $thumbnailFolder */
+ $thumbnailFolder = $userRoot->get('thumbnails');
+ } catch (NotFoundException $e) {
+ return true;
+ }
+
+ $thumbnails = $thumbnailFolder->getDirectoryListing();
+
+ $start = $this->time->getTime();
+ foreach ($thumbnails as $thumbnail) {
+ try {
+ $thumbnail->delete();
+ } catch (NotPermittedException $e) {
+ // Ignore
+ }
+
+ if (($this->time->getTime() - $start) > 15) {
+ return false;
+ }
+ }
+
+ try {
+ $thumbnailFolder->delete();
+ } catch (NotPermittedException $e) {
+ // Ignore
+ }
+
+ return true;
+ }
+}
diff --git a/lib/private/Repair/Owncloud/DropAccountTermsTable.php b/lib/private/Repair/Owncloud/DropAccountTermsTable.php
new file mode 100644
index 00000000000..534825c146a
--- /dev/null
+++ b/lib/private/Repair/Owncloud/DropAccountTermsTable.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\Owncloud;
+
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class DropAccountTermsTable implements IRepairStep {
+ /** @var IDBConnection */
+ protected $db;
+
+ /**
+ * @param IDBConnection $db
+ */
+ public function __construct(IDBConnection $db) {
+ $this->db = $db;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName() {
+ return 'Drop account terms table when migrating from ownCloud';
+ }
+
+ /**
+ * @param IOutput $output
+ */
+ public function run(IOutput $output) {
+ if (!$this->db->tableExists('account_terms')) {
+ return;
+ }
+
+ $this->db->dropTable('account_terms');
+ }
+}
diff --git a/lib/private/Repair/Owncloud/MigrateOauthTables.php b/lib/private/Repair/Owncloud/MigrateOauthTables.php
new file mode 100644
index 00000000000..de26a907e02
--- /dev/null
+++ b/lib/private/Repair/Owncloud/MigrateOauthTables.php
@@ -0,0 +1,259 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OC\Repair\Owncloud;
+
+use OC\Authentication\Token\IProvider as ITokenProvider;
+use OC\DB\Connection;
+use OC\DB\SchemaWrapper;
+use OCA\OAuth2\Db\AccessToken;
+use OCA\OAuth2\Db\AccessTokenMapper;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\Authentication\Token\IToken;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use OCP\Security\ICrypto;
+use OCP\Security\ISecureRandom;
+
+class MigrateOauthTables implements IRepairStep {
+
+ public function __construct(
+ protected Connection $db,
+ private AccessTokenMapper $accessTokenMapper,
+ private ITokenProvider $tokenProvider,
+ private ISecureRandom $random,
+ private ITimeFactory $timeFactory,
+ private ICrypto $crypto,
+ private IConfig $config,
+ ) {
+ }
+
+ /**
+ * @return string
+ */
+ public function getName() {
+ return 'Migrate oauth2_clients table to nextcloud schema';
+ }
+
+ public function run(IOutput $output) {
+ $schema = new SchemaWrapper($this->db);
+ if (!$schema->hasTable('oauth2_clients')) {
+ $output->info('oauth2_clients table does not exist.');
+ return;
+ }
+
+ // Create column and then migrate before handling unique index.
+ // So that we can distinguish between legacy (from oc) and new rows (from nc).
+ $table = $schema->getTable('oauth2_access_tokens');
+ if (!$table->hasColumn('hashed_code')) {
+ $output->info('Prepare the oauth2_access_tokens table schema.');
+ $table->addColumn('hashed_code', 'string', [
+ 'notnull' => true,
+ 'length' => 128,
+ ]);
+
+ // Regenerate schema after migrating to it
+ $this->db->migrateToSchema($schema->getWrappedSchema());
+ $schema = new SchemaWrapper($this->db);
+ }
+
+ $output->info('Update the oauth2_access_tokens table schema.');
+ $table = $schema->getTable('oauth2_access_tokens');
+ if (!$table->hasColumn('encrypted_token')) {
+ $table->addColumn('encrypted_token', 'string', [
+ 'notnull' => true,
+ 'length' => 786,
+ ]);
+ }
+ if (!$table->hasIndex('oauth2_access_hash_idx')) {
+ // Drop legacy access codes first to prevent integrity constraint violations
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete('oauth2_access_tokens')
+ ->where($qb->expr()->eq('hashed_code', $qb->createNamedParameter('')));
+ $qb->executeStatement();
+
+ $table->addUniqueIndex(['hashed_code'], 'oauth2_access_hash_idx');
+ }
+ if (!$table->hasIndex('oauth2_access_client_id_idx')) {
+ $table->addIndex(['client_id'], 'oauth2_access_client_id_idx');
+ }
+ if (!$table->hasColumn('token_id')) {
+ $table->addColumn('token_id', 'integer', [
+ 'notnull' => true,
+ ]);
+ }
+ if ($table->hasColumn('expires')) {
+ $table->dropColumn('expires');
+ }
+ if ($table->hasColumn('user_id')) {
+ $table->dropColumn('user_id');
+ }
+ if ($table->hasColumn('token')) {
+ $table->dropColumn('token');
+ }
+
+ $output->info('Update the oauth2_clients table schema.');
+ $table = $schema->getTable('oauth2_clients');
+ if ($table->getColumn('name')->getLength() !== 64) {
+ // shorten existing values before resizing the column
+ $qb = $this->db->getQueryBuilder();
+ $qb->update('oauth2_clients')
+ ->set('name', $qb->createParameter('shortenedName'))
+ ->where($qb->expr()->eq('id', $qb->createParameter('theId')));
+
+ $qbSelect = $this->db->getQueryBuilder();
+ $qbSelect->select('id', 'name')
+ ->from('oauth2_clients');
+
+ $result = $qbSelect->executeQuery();
+ while ($row = $result->fetch()) {
+ $id = $row['id'];
+ $shortenedName = mb_substr($row['name'], 0, 64);
+ $qb->setParameter('theId', $id, IQueryBuilder::PARAM_INT);
+ $qb->setParameter('shortenedName', $shortenedName, IQueryBuilder::PARAM_STR);
+ $qb->executeStatement();
+ }
+ $result->closeCursor();
+
+ // safely set the new column length
+ $table->getColumn('name')->setLength(64);
+ }
+ if ($table->hasColumn('allow_subdomains')) {
+ $table->dropColumn('allow_subdomains');
+ }
+ if ($table->hasColumn('trusted')) {
+ $table->dropColumn('trusted');
+ }
+
+ if (!$schema->getTable('oauth2_clients')->hasColumn('client_identifier')) {
+ $table->addColumn('client_identifier', 'string', [
+ 'notnull' => true,
+ 'length' => 64,
+ 'default' => ''
+ ]);
+ $table->addIndex(['client_identifier'], 'oauth2_client_id_idx');
+ }
+
+ // Regenerate schema after migrating to it
+ $this->db->migrateToSchema($schema->getWrappedSchema());
+ $schema = new SchemaWrapper($this->db);
+
+ if ($schema->getTable('oauth2_clients')->hasColumn('identifier')) {
+ $output->info("Move identifier column's data to the new client_identifier column.");
+ // 1. Fetch all [id, identifier] couple.
+ $selectQuery = $this->db->getQueryBuilder();
+ $selectQuery->select('id', 'identifier')->from('oauth2_clients');
+ $result = $selectQuery->executeQuery();
+ $identifiers = $result->fetchAll();
+ $result->closeCursor();
+
+ // 2. Insert them into the client_identifier column.
+ foreach ($identifiers as ['id' => $id, 'identifier' => $clientIdentifier]) {
+ $insertQuery = $this->db->getQueryBuilder();
+ $insertQuery->update('oauth2_clients')
+ ->set('client_identifier', $insertQuery->createNamedParameter($clientIdentifier, IQueryBuilder::PARAM_STR))
+ ->where($insertQuery->expr()->eq('id', $insertQuery->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
+ ->executeStatement();
+ }
+
+ $output->info('Drop the identifier column.');
+ $table = $schema->getTable('oauth2_clients');
+ $table->dropColumn('identifier');
+
+ // Regenerate schema after migrating to it
+ $this->db->migrateToSchema($schema->getWrappedSchema());
+ $schema = new SchemaWrapper($this->db);
+ }
+
+ $enableOcClients = $this->config->getSystemValueBool('oauth2.enable_oc_clients', false);
+ if ($enableOcClients) {
+ $output->info('Delete clients (and their related access tokens) with the redirect_uri starting with oc://');
+ } else {
+ $output->info('Delete clients (and their related access tokens) with the redirect_uri starting with oc:// or ending with *');
+ }
+ // delete the access tokens
+ $qbDeleteAccessTokens = $this->db->getQueryBuilder();
+
+ $qbSelectClientId = $this->db->getQueryBuilder();
+ $qbSelectClientId->select('id')
+ ->from('oauth2_clients')
+ ->where(
+ $qbSelectClientId->expr()->iLike('redirect_uri', $qbDeleteAccessTokens->createNamedParameter('oc://%', IQueryBuilder::PARAM_STR))
+ );
+ if (!$enableOcClients) {
+ $qbSelectClientId->orWhere(
+ $qbSelectClientId->expr()->iLike('redirect_uri', $qbDeleteAccessTokens->createNamedParameter('%*', IQueryBuilder::PARAM_STR))
+ );
+ }
+
+ $qbDeleteAccessTokens->delete('oauth2_access_tokens')
+ ->where(
+ $qbSelectClientId->expr()->in('client_id', $qbDeleteAccessTokens->createFunction($qbSelectClientId->getSQL()), IQueryBuilder::PARAM_STR_ARRAY)
+ );
+ $qbDeleteAccessTokens->executeStatement();
+
+ // delete the clients
+ $qbDeleteClients = $this->db->getQueryBuilder();
+ $qbDeleteClients->delete('oauth2_clients')
+ ->where(
+ $qbDeleteClients->expr()->iLike('redirect_uri', $qbDeleteClients->createNamedParameter('oc://%', IQueryBuilder::PARAM_STR))
+ );
+ if (!$enableOcClients) {
+ $qbDeleteClients->orWhere(
+ $qbDeleteClients->expr()->iLike('redirect_uri', $qbDeleteClients->createNamedParameter('%*', IQueryBuilder::PARAM_STR))
+ );
+ }
+ $qbDeleteClients->executeStatement();
+
+ // Migrate legacy refresh tokens from oc
+ if ($schema->hasTable('oauth2_refresh_tokens')) {
+ $output->info('Migrate legacy oauth2 refresh tokens.');
+
+ $qbSelect = $this->db->getQueryBuilder();
+ $qbSelect->select('*')
+ ->from('oauth2_refresh_tokens');
+ $result = $qbSelect->executeQuery();
+ $now = $this->timeFactory->now()->getTimestamp();
+ $index = 0;
+ while ($row = $result->fetch()) {
+ $clientId = $row['client_id'];
+ $refreshToken = $row['token'];
+
+ // Insert expired token so that it can be rotated on the next refresh
+ $accessToken = $this->random->generate(72, ISecureRandom::CHAR_UPPER . ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
+ $authToken = $this->tokenProvider->generateToken(
+ $accessToken,
+ $row['user_id'],
+ $row['user_id'],
+ null,
+ "oc_migrated_client{$clientId}_t{$now}_i$index",
+ IToken::PERMANENT_TOKEN,
+ IToken::DO_NOT_REMEMBER,
+ );
+ $authToken->setExpires($now - 3600);
+ $this->tokenProvider->updateToken($authToken);
+
+ $accessTokenEntity = new AccessToken();
+ $accessTokenEntity->setTokenId($authToken->getId());
+ $accessTokenEntity->setClientId($clientId);
+ $accessTokenEntity->setHashedCode(hash('sha512', $refreshToken));
+ $accessTokenEntity->setEncryptedToken($this->crypto->encrypt($accessToken, $refreshToken));
+ $accessTokenEntity->setCodeCreatedAt($now);
+ $accessTokenEntity->setTokenCount(1);
+ $this->accessTokenMapper->insert($accessTokenEntity);
+
+ $index++;
+ }
+ $result->closeCursor();
+
+ $schema->dropTable('oauth2_refresh_tokens');
+ $schema->performDropTableCalls();
+ }
+ }
+}
diff --git a/lib/private/Repair/Owncloud/MoveAvatars.php b/lib/private/Repair/Owncloud/MoveAvatars.php
new file mode 100644
index 00000000000..9e3f4b89b13
--- /dev/null
+++ b/lib/private/Repair/Owncloud/MoveAvatars.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\Owncloud;
+
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class MoveAvatars implements IRepairStep {
+ /** @var IJobList */
+ private $jobList;
+
+ /** @var IConfig */
+ private $config;
+
+ /**
+ * MoveAvatars constructor.
+ *
+ * @param IJobList $jobList
+ * @param IConfig $config
+ */
+ public function __construct(IJobList $jobList,
+ IConfig $config) {
+ $this->jobList = $jobList;
+ $this->config = $config;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName() {
+ return 'Add move avatar background job';
+ }
+
+ public function run(IOutput $output) {
+ // only run once
+ if ($this->config->getAppValue('core', 'moveavatarsdone') === 'yes') {
+ $output->info('Repair step already executed');
+ return;
+ }
+ if (!$this->config->getSystemValueBool('enable_avatars', true)) {
+ $output->info('Avatars are disabled');
+ } else {
+ $output->info('Add background job');
+ $this->jobList->add(MoveAvatarsBackgroundJob::class);
+ // if all were done, no need to redo the repair during next upgrade
+ $this->config->setAppValue('core', 'moveavatarsdone', 'yes');
+ }
+ }
+}
diff --git a/lib/private/Repair/Owncloud/MoveAvatarsBackgroundJob.php b/lib/private/Repair/Owncloud/MoveAvatarsBackgroundJob.php
new file mode 100644
index 00000000000..e145fb71863
--- /dev/null
+++ b/lib/private/Repair/Owncloud/MoveAvatarsBackgroundJob.php
@@ -0,0 +1,95 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\Owncloud;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\QueuedJob;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\Storage\IStorage;
+use OCP\IAvatarManager;
+use OCP\IUser;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+use function is_resource;
+
+class MoveAvatarsBackgroundJob extends QueuedJob {
+ private ?IStorage $owncloudAvatarStorage = null;
+
+ public function __construct(
+ private IUserManager $userManager,
+ private LoggerInterface $logger,
+ private IAvatarManager $avatarManager,
+ private IRootFolder $rootFolder,
+ ITimeFactory $time,
+ ) {
+ parent::__construct($time);
+ try {
+ $this->owncloudAvatarStorage = $rootFolder->get('avatars')->getStorage();
+ } catch (\Exception $e) {
+ }
+ }
+
+ public function run($arguments) {
+ $this->logger->info('Started migrating avatars to AppData folder');
+ $this->moveAvatars();
+ $this->logger->info('All avatars migrated to AppData folder');
+ }
+
+ private function moveAvatars(): void {
+ if (!$this->owncloudAvatarStorage) {
+ $this->logger->info('No legacy avatars available, skipping migration');
+ return;
+ }
+
+ $counter = 0;
+ $this->userManager->callForSeenUsers(function (IUser $user) use (&$counter) {
+ $uid = $user->getUID();
+
+ $path = 'avatars/' . $this->buildOwnCloudAvatarPath($uid);
+ $avatar = $this->avatarManager->getAvatar($uid);
+ try {
+ $avatarPath = $path . '/avatar.' . $this->getExtension($path);
+ $resource = $this->owncloudAvatarStorage->fopen($avatarPath, 'r');
+ if (is_resource($resource)) {
+ $avatar->set($resource);
+ fclose($resource);
+ } else {
+ throw new \Exception('Failed to open old avatar file for reading');
+ }
+ } catch (NotFoundException $e) {
+ // In case there is no avatar we can just skip
+ } catch (\Throwable $e) {
+ $this->logger->error('Failed to migrate avatar for user ' . $uid, ['exception' => $e]);
+ }
+
+ $counter++;
+ if ($counter % 100 === 0) {
+ $this->logger->info('{amount} avatars migrated', ['amount' => $counter]);
+ }
+ });
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ private function getExtension(string $path): string {
+ if ($this->owncloudAvatarStorage->file_exists("{$path}/avatar.jpg")) {
+ return 'jpg';
+ }
+ if ($this->owncloudAvatarStorage->file_exists("{$path}/avatar.png")) {
+ return 'png';
+ }
+ throw new NotFoundException("{$path}/avatar.jpg|png");
+ }
+
+ protected function buildOwnCloudAvatarPath(string $userId): string {
+ return substr_replace(substr_replace(md5($userId), '/', 4, 0), '/', 2, 0);
+ }
+}
diff --git a/lib/private/Repair/Owncloud/SaveAccountsTableData.php b/lib/private/Repair/Owncloud/SaveAccountsTableData.php
new file mode 100644
index 00000000000..ab1560ddb8d
--- /dev/null
+++ b/lib/private/Repair/Owncloud/SaveAccountsTableData.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\Owncloud;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use OCP\PreConditionNotMetException;
+
+/**
+ * Copies the email address from the accounts table to the preference table,
+ * before the data structure is changed and the information is gone
+ */
+class SaveAccountsTableData implements IRepairStep {
+ public const BATCH_SIZE = 75;
+
+ /** @var IDBConnection */
+ protected $db;
+
+ /** @var IConfig */
+ protected $config;
+
+ protected $hasForeignKeyOnPersistentLocks = false;
+
+ /**
+ * @param IDBConnection $db
+ * @param IConfig $config
+ */
+ public function __construct(IDBConnection $db, IConfig $config) {
+ $this->db = $db;
+ $this->config = $config;
+ }
+
+ /**
+ * @return string
+ */
+ public function getName() {
+ return 'Copy data from accounts table when migrating from ownCloud';
+ }
+
+ /**
+ * @param IOutput $output
+ */
+ public function run(IOutput $output) {
+ if (!$this->shouldRun()) {
+ return;
+ }
+
+ $offset = 0;
+ $numUsers = $this->runStep($offset);
+
+ while ($numUsers === self::BATCH_SIZE) {
+ $offset += $numUsers;
+ $numUsers = $this->runStep($offset);
+ }
+
+ // oc_persistent_locks will be removed later on anyways so we can just drop and ignore any foreign key constraints here
+ $tableName = $this->config->getSystemValueString('dbtableprefix', 'oc_') . 'persistent_locks';
+ $schema = $this->db->createSchema();
+ $table = $schema->getTable($tableName);
+ foreach ($table->getForeignKeys() as $foreignKey) {
+ $table->removeForeignKey($foreignKey->getName());
+ }
+ $this->db->migrateToSchema($schema);
+
+ // Remove the table
+ if ($this->hasForeignKeyOnPersistentLocks) {
+ $this->db->dropTable('persistent_locks');
+ }
+ $this->db->dropTable('accounts');
+ }
+
+ /**
+ * @return bool
+ */
+ protected function shouldRun() {
+ $schema = $this->db->createSchema();
+ $prefix = $this->config->getSystemValueString('dbtableprefix', 'oc_');
+
+ $tableName = $prefix . 'accounts';
+ if (!$schema->hasTable($tableName)) {
+ return false;
+ }
+
+ $table = $schema->getTable($tableName);
+ if (!$table->hasColumn('user_id')) {
+ return false;
+ }
+
+ if ($schema->hasTable($prefix . 'persistent_locks')) {
+ $locksTable = $schema->getTable($prefix . 'persistent_locks');
+ $foreignKeys = $locksTable->getForeignKeys();
+ foreach ($foreignKeys as $foreignKey) {
+ if ($tableName === $foreignKey->getForeignTableName()) {
+ $this->hasForeignKeyOnPersistentLocks = true;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @param int $offset
+ * @return int Number of copied users
+ */
+ protected function runStep($offset) {
+ $query = $this->db->getQueryBuilder();
+ $query->select('*')
+ ->from('accounts')
+ ->orderBy('id')
+ ->setMaxResults(self::BATCH_SIZE);
+
+ if ($offset > 0) {
+ $query->setFirstResult($offset);
+ }
+
+ $result = $query->execute();
+
+ $update = $this->db->getQueryBuilder();
+ $update->update('users')
+ ->set('displayname', $update->createParameter('displayname'))
+ ->where($update->expr()->eq('uid', $update->createParameter('userid')));
+
+ $updatedUsers = 0;
+ while ($row = $result->fetch()) {
+ try {
+ $this->migrateUserInfo($update, $row);
+ } catch (PreConditionNotMetException $e) {
+ // Ignore and continue
+ } catch (\UnexpectedValueException $e) {
+ // Ignore and continue
+ }
+ $updatedUsers++;
+ }
+ $result->closeCursor();
+
+ return $updatedUsers;
+ }
+
+ /**
+ * @param IQueryBuilder $update
+ * @param array $userdata
+ * @throws PreConditionNotMetException
+ * @throws \UnexpectedValueException
+ */
+ protected function migrateUserInfo(IQueryBuilder $update, $userdata) {
+ $state = (int)$userdata['state'];
+ if ($state === 3) {
+ // Deleted user, ignore
+ return;
+ }
+
+ if ($userdata['email'] !== null) {
+ $this->config->setUserValue($userdata['user_id'], 'settings', 'email', $userdata['email']);
+ }
+ if ($userdata['quota'] !== null) {
+ $this->config->setUserValue($userdata['user_id'], 'files', 'quota', $userdata['quota']);
+ }
+ if ($userdata['last_login'] !== null) {
+ $this->config->setUserValue($userdata['user_id'], 'login', 'lastLogin', $userdata['last_login']);
+ }
+ if ($state === 1) {
+ $this->config->setUserValue($userdata['user_id'], 'core', 'enabled', 'true');
+ } elseif ($state === 2) {
+ $this->config->setUserValue($userdata['user_id'], 'core', 'enabled', 'false');
+ }
+
+ if ($userdata['display_name'] !== null) {
+ $update->setParameter('displayname', $userdata['display_name'])
+ ->setParameter('userid', $userdata['user_id']);
+ $update->execute();
+ }
+ }
+}
diff --git a/lib/private/Repair/Owncloud/UpdateLanguageCodes.php b/lib/private/Repair/Owncloud/UpdateLanguageCodes.php
new file mode 100644
index 00000000000..8d9046ad49f
--- /dev/null
+++ b/lib/private/Repair/Owncloud/UpdateLanguageCodes.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair\Owncloud;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class UpdateLanguageCodes implements IRepairStep {
+ /** @var IDBConnection */
+ private $connection;
+
+ /** @var IConfig */
+ private $config;
+
+ /**
+ * @param IDBConnection $connection
+ * @param IConfig $config
+ */
+ public function __construct(IDBConnection $connection,
+ IConfig $config) {
+ $this->connection = $connection;
+ $this->config = $config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getName() {
+ return 'Repair language codes';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function run(IOutput $output) {
+ $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0');
+
+ if (version_compare($versionFromBeforeUpdate, '12.0.0.13', '>')) {
+ return;
+ }
+
+ $languages = [
+ 'bg_BG' => 'bg',
+ 'cs_CZ' => 'cs',
+ 'fi_FI' => 'fi',
+ 'hu_HU' => 'hu',
+ 'nb_NO' => 'nb',
+ 'sk_SK' => 'sk',
+ 'th_TH' => 'th',
+ ];
+
+ foreach ($languages as $oldCode => $newCode) {
+ $qb = $this->connection->getQueryBuilder();
+
+ $affectedRows = $qb->update('preferences')
+ ->set('configvalue', $qb->createNamedParameter($newCode))
+ ->where($qb->expr()->eq('appid', $qb->createNamedParameter('core')))
+ ->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lang')))
+ ->andWhere($qb->expr()->eq('configvalue', $qb->createNamedParameter($oldCode), IQueryBuilder::PARAM_STR))
+ ->execute();
+
+ $output->info('Changed ' . $affectedRows . ' setting(s) from "' . $oldCode . '" to "' . $newCode . '" in preferences table.');
+ }
+ }
+}
diff --git a/lib/private/Repair/RemoveBrokenProperties.php b/lib/private/Repair/RemoveBrokenProperties.php
new file mode 100644
index 00000000000..85939b39e5e
--- /dev/null
+++ b/lib/private/Repair/RemoveBrokenProperties.php
@@ -0,0 +1,68 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class RemoveBrokenProperties implements IRepairStep {
+ /**
+ * RemoveBrokenProperties constructor.
+ *
+ * @param IDBConnection $db
+ */
+ public function __construct(
+ private IDBConnection $db,
+ ) {
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function getName() {
+ return 'Remove broken DAV object properties';
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function run(IOutput $output) {
+ // retrieve all object properties
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('id', 'propertyvalue')
+ ->from('properties')
+ ->where($qb->expr()->eq('valuetype', $qb->createNamedParameter('3', IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT));
+ $result = $qb->executeQuery();
+ // find broken object properties
+ $brokenIds = [];
+ while ($entry = $result->fetch()) {
+ if (!empty($entry['propertyvalue'])) {
+ $object = @unserialize(str_replace('\x00', chr(0), $entry['propertyvalue']));
+ if ($object === false) {
+ $brokenIds[] = $entry['id'];
+ }
+ } else {
+ $brokenIds[] = $entry['id'];
+ }
+ }
+ $result->closeCursor();
+ // delete broken object properties
+ $qb = $this->db->getQueryBuilder();
+ $qb->delete('properties')
+ ->where($qb->expr()->in('id', $qb->createParameter('ids'), IQueryBuilder::PARAM_STR_ARRAY));
+ foreach (array_chunk($brokenIds, 1000) as $chunkIds) {
+ $qb->setParameter('ids', $chunkIds, IQueryBuilder::PARAM_STR_ARRAY);
+ $qb->executeStatement();
+ }
+ $total = count($brokenIds);
+ $output->info("$total broken object properties removed");
+ }
+}
diff --git a/lib/private/Repair/RemoveLinkShares.php b/lib/private/Repair/RemoveLinkShares.php
new file mode 100644
index 00000000000..a07ebdb72c3
--- /dev/null
+++ b/lib/private/Repair/RemoveLinkShares.php
@@ -0,0 +1,206 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use OCP\Notification\IManager;
+
+class RemoveLinkShares implements IRepairStep {
+ /** @var string[] */
+ private $userToNotify = [];
+
+ public function __construct(
+ private IDBConnection $connection,
+ private IConfig $config,
+ private IGroupManager $groupManager,
+ private IManager $notificationManager,
+ private ITimeFactory $timeFactory,
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Remove potentially over exposing share links';
+ }
+
+ private function shouldRun(): bool {
+ $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0');
+
+ if (version_compare($versionFromBeforeUpdate, '14.0.11', '<')) {
+ return true;
+ }
+ if (version_compare($versionFromBeforeUpdate, '15.0.8', '<')) {
+ return true;
+ }
+ if (version_compare($versionFromBeforeUpdate, '16.0.0', '<=')) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Delete the share
+ *
+ * @param int $id
+ */
+ private function deleteShare(int $id): void {
+ $qb = $this->connection->getQueryBuilder();
+ $qb->delete('share')
+ ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
+ $qb->executeStatement();
+ }
+
+ /**
+ * Get the total of affected shares
+ *
+ * @return int
+ */
+ private function getTotal(): int {
+ $subSubQuery = $this->connection->getQueryBuilder();
+ $subSubQuery->select('*')
+ ->from('share')
+ ->where($subSubQuery->expr()->isNotNull('parent'))
+ ->andWhere($subSubQuery->expr()->eq('share_type', $subSubQuery->expr()->literal(3, IQueryBuilder::PARAM_INT)));
+
+ $subQuery = $this->connection->getQueryBuilder();
+ $subQuery->select('s1.id')
+ ->from($subQuery->createFunction('(' . $subSubQuery->getSQL() . ')'), 's1')
+ ->join(
+ 's1', 'share', 's2',
+ $subQuery->expr()->eq('s1.parent', 's2.id')
+ )
+ ->where($subQuery->expr()->orX(
+ $subQuery->expr()->eq('s2.share_type', $subQuery->expr()->literal(1, IQueryBuilder::PARAM_INT)),
+ $subQuery->expr()->eq('s2.share_type', $subQuery->expr()->literal(2, IQueryBuilder::PARAM_INT))
+ ))
+ ->andWhere($subQuery->expr()->eq('s1.item_source', 's2.item_source'));
+
+ $query = $this->connection->getQueryBuilder();
+ $query->select($query->func()->count('*', 'total'))
+ ->from('share')
+ ->where($query->expr()->in('id', $query->createFunction($subQuery->getSQL())));
+
+ $result = $query->executeQuery();
+ $data = $result->fetch();
+ $result->closeCursor();
+
+ return (int)$data['total'];
+ }
+
+ /**
+ * Get the cursor to fetch all the shares
+ */
+ private function getShares(): IResult {
+ $subQuery = $this->connection->getQueryBuilder();
+ $subQuery->select('*')
+ ->from('share')
+ ->where($subQuery->expr()->isNotNull('parent'))
+ ->andWhere($subQuery->expr()->eq('share_type', $subQuery->expr()->literal(3, IQueryBuilder::PARAM_INT)));
+
+ $query = $this->connection->getQueryBuilder();
+ $query->select('s1.id', 's1.uid_owner', 's1.uid_initiator')
+ ->from($query->createFunction('(' . $subQuery->getSQL() . ')'), 's1')
+ ->join(
+ 's1', 'share', 's2',
+ $query->expr()->eq('s1.parent', 's2.id')
+ )
+ ->where($query->expr()->orX(
+ $query->expr()->eq('s2.share_type', $query->expr()->literal(1, IQueryBuilder::PARAM_INT)),
+ $query->expr()->eq('s2.share_type', $query->expr()->literal(2, IQueryBuilder::PARAM_INT))
+ ))
+ ->andWhere($query->expr()->eq('s1.item_source', 's2.item_source'));
+ /** @var IResult $result */
+ $result = $query->executeQuery();
+ return $result;
+ }
+
+ /**
+ * Process a single share
+ *
+ * @param array $data
+ */
+ private function processShare(array $data): void {
+ $id = $data['id'];
+
+ $this->addToNotify($data['uid_owner']);
+ $this->addToNotify($data['uid_initiator']);
+
+ $this->deleteShare((int)$id);
+ }
+
+ /**
+ * Update list of users to notify
+ *
+ * @param string $uid
+ */
+ private function addToNotify(string $uid): void {
+ if (!isset($this->userToNotify[$uid])) {
+ $this->userToNotify[$uid] = true;
+ }
+ }
+
+ /**
+ * Send all notifications
+ */
+ private function sendNotification(): void {
+ $time = $this->timeFactory->getDateTime();
+
+ $notification = $this->notificationManager->createNotification();
+ $notification->setApp('core')
+ ->setDateTime($time)
+ ->setObject('repair', 'exposing_links')
+ ->setSubject('repair_exposing_links');
+
+ $users = array_keys($this->userToNotify);
+ foreach ($users as $user) {
+ $notification->setUser((string)$user);
+ $this->notificationManager->notify($notification);
+ }
+ }
+
+ private function repair(IOutput $output, int $total): void {
+ $output->startProgress($total);
+
+ $shareResult = $this->getShares();
+ while ($data = $shareResult->fetch()) {
+ $this->processShare($data);
+ $output->advance();
+ }
+ $output->finishProgress();
+ $shareResult->closeCursor();
+
+ // Notify all admins
+ $adminGroup = $this->groupManager->get('admin');
+ $adminUsers = $adminGroup->getUsers();
+ foreach ($adminUsers as $user) {
+ $this->addToNotify($user->getUID());
+ }
+
+ $output->info('Sending notifications to admins and affected users');
+ $this->sendNotification();
+ }
+
+ public function run(IOutput $output): void {
+ if ($this->shouldRun() === false || ($total = $this->getTotal()) === 0) {
+ $output->info('No need to remove link shares.');
+ return;
+ }
+
+ $output->info('Removing potentially over exposing link shares');
+ $this->repair($output, $total);
+ $output->info('Removed potentially over exposing link shares');
+ }
+}
diff --git a/lib/private/Repair/RepairDavShares.php b/lib/private/Repair/RepairDavShares.php
new file mode 100644
index 00000000000..557e2c080ca
--- /dev/null
+++ b/lib/private/Repair/RepairDavShares.php
@@ -0,0 +1,109 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OCP\DB\Exception;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Psr\Log\LoggerInterface;
+use function strlen;
+use function substr;
+use function urldecode;
+use function urlencode;
+
+class RepairDavShares implements IRepairStep {
+ protected const GROUP_PRINCIPAL_PREFIX = 'principals/groups/';
+
+ /** @var bool */
+ private $hintInvalidShares = false;
+
+ public function __construct(
+ private IConfig $config,
+ private IDBConnection $dbc,
+ private IGroupManager $groupManager,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getName() {
+ return 'Repair DAV shares';
+ }
+
+ protected function repairUnencodedGroupShares() {
+ $qb = $this->dbc->getQueryBuilder();
+ $qb->select(['id', 'principaluri'])
+ ->from('dav_shares')
+ ->where($qb->expr()->like('principaluri', $qb->createNamedParameter(self::GROUP_PRINCIPAL_PREFIX . '%')));
+
+ $updateQuery = $this->dbc->getQueryBuilder();
+ $updateQuery->update('dav_shares')
+ ->set('principaluri', $updateQuery->createParameter('updatedPrincipalUri'))
+ ->where($updateQuery->expr()->eq('id', $updateQuery->createParameter('shareId')));
+
+ $statement = $qb->execute();
+ while ($share = $statement->fetch()) {
+ $gid = substr($share['principaluri'], strlen(self::GROUP_PRINCIPAL_PREFIX));
+ $decodedGid = urldecode($gid);
+ $encodedGid = urlencode($gid);
+ if ($gid === $encodedGid
+ || !$this->groupManager->groupExists($gid)
+ || ($gid !== $decodedGid && $this->groupManager->groupExists($decodedGid))
+ ) {
+ $this->hintInvalidShares = $this->hintInvalidShares || $gid !== $encodedGid;
+ continue;
+ }
+
+ // Repair when
+ // + the group name needs encoding
+ // + AND it is not encoded yet
+ // + AND there are no ambivalent groups
+
+ try {
+ $fixedPrincipal = self::GROUP_PRINCIPAL_PREFIX . $encodedGid;
+ $logParameters = [
+ 'app' => 'core',
+ 'id' => $share['id'],
+ 'old' => $share['principaluri'],
+ 'new' => $fixedPrincipal,
+ ];
+ $updateQuery
+ ->setParameter('updatedPrincipalUri', $fixedPrincipal)
+ ->setParameter('shareId', $share['id'])
+ ->execute();
+ $this->logger->info('Repaired principal for dav share {id} from {old} to {new}', $logParameters);
+ } catch (Exception $e) {
+ $logParameters['message'] = $e->getMessage();
+ $logParameters['exception'] = $e;
+ $this->logger->info('Could not repair principal for dav share {id} from {old} to {new}: {message}', $logParameters);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function run(IOutput $output) {
+ $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0');
+ if (version_compare($versionFromBeforeUpdate, '20.0.8', '<')
+ && $this->repairUnencodedGroupShares()
+ ) {
+ $output->info('Repaired DAV group shares');
+ if ($this->hintInvalidShares) {
+ $output->info('Invalid shares might be left in the database, running "occ dav:remove-invalid-shares" can remove them.');
+ }
+ }
+ }
+}
diff --git a/lib/private/Repair/RepairInvalidShares.php b/lib/private/Repair/RepairInvalidShares.php
new file mode 100644
index 00000000000..9553f25ee70
--- /dev/null
+++ b/lib/private/Repair/RepairInvalidShares.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair;
+
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+/**
+ * Repairs shares with invalid data
+ */
+class RepairInvalidShares implements IRepairStep {
+ public const CHUNK_SIZE = 200;
+
+ public function __construct(
+ protected IConfig $config,
+ protected IDBConnection $connection,
+ ) {
+ }
+
+ public function getName() {
+ return 'Repair invalid shares';
+ }
+
+ /**
+ * Adjust file share permissions
+ */
+ private function adjustFileSharePermissions(IOutput $out) {
+ $mask = \OCP\Constants::PERMISSION_READ | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_SHARE;
+ $builder = $this->connection->getQueryBuilder();
+
+ $permsFunc = $builder->expr()->bitwiseAnd('permissions', $mask);
+ $builder
+ ->update('share')
+ ->set('permissions', $permsFunc)
+ ->where($builder->expr()->eq('item_type', $builder->expr()->literal('file')))
+ ->andWhere($builder->expr()->neq('permissions', $permsFunc));
+
+ $updatedEntries = $builder->executeStatement();
+ if ($updatedEntries > 0) {
+ $out->info('Fixed file share permissions for ' . $updatedEntries . ' shares');
+ }
+ }
+
+ /**
+ * Remove shares where the parent share does not exist anymore
+ */
+ private function removeSharesNonExistingParent(IOutput $out) {
+ $deletedEntries = 0;
+
+ $query = $this->connection->getQueryBuilder();
+ $query->select('s1.parent')
+ ->from('share', 's1')
+ ->where($query->expr()->isNotNull('s1.parent'))
+ ->andWhere($query->expr()->isNull('s2.id'))
+ ->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id'))
+ ->groupBy('s1.parent')
+ ->setMaxResults(self::CHUNK_SIZE);
+
+ $deleteQuery = $this->connection->getQueryBuilder();
+ $deleteQuery->delete('share')
+ ->where($deleteQuery->expr()->eq('parent', $deleteQuery->createParameter('parent')));
+
+ $deletedInLastChunk = self::CHUNK_SIZE;
+ while ($deletedInLastChunk === self::CHUNK_SIZE) {
+ $deletedInLastChunk = 0;
+ $result = $query->executeQuery();
+ while ($row = $result->fetch()) {
+ $deletedInLastChunk++;
+ $deletedEntries += $deleteQuery->setParameter('parent', (int)$row['parent'])
+ ->executeStatement();
+ }
+ $result->closeCursor();
+ }
+
+ if ($deletedEntries) {
+ $out->info('Removed ' . $deletedEntries . ' shares where the parent did not exist');
+ }
+ }
+
+ public function run(IOutput $out) {
+ $ocVersionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0');
+ if (version_compare($ocVersionFromBeforeUpdate, '12.0.0.11', '<')) {
+ $this->adjustFileSharePermissions($out);
+ }
+
+ $this->removeSharesNonExistingParent($out);
+ }
+}
diff --git a/lib/private/Repair/RepairLogoDimension.php b/lib/private/Repair/RepairLogoDimension.php
new file mode 100644
index 00000000000..854aeb3ab07
--- /dev/null
+++ b/lib/private/Repair/RepairLogoDimension.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OC\Repair;
+
+use OCA\Theming\ImageManager;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use OCP\Server;
+
+class RepairLogoDimension implements IRepairStep {
+ public function __construct(
+ protected IConfig $config,
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Cache logo dimension to fix size in emails on Outlook';
+ }
+
+ public function run(IOutput $output): void {
+ $logoDimensions = $this->config->getAppValue('theming', 'logoDimensions');
+ if (preg_match('/^\d+x\d+$/', $logoDimensions)) {
+ $output->info('Logo dimensions are already known');
+ return;
+ }
+
+ try {
+ /** @var ImageManager $imageManager */
+ $imageManager = Server::get(ImageManager::class);
+ } catch (\Throwable) {
+ $output->info('Theming is disabled');
+ return;
+ }
+
+ if (!$imageManager->hasImage('logo')) {
+ $output->info('Theming is not used to provide a logo');
+ return;
+ }
+
+ try {
+ try {
+ $simpleFile = $imageManager->getImage('logo', false);
+ $image = @imagecreatefromstring($simpleFile->getContent());
+ } catch (NotFoundException|NotPermittedException) {
+ $simpleFile = $imageManager->getImage('logo');
+ $image = false;
+ }
+ } catch (NotFoundException|NotPermittedException) {
+ $output->info('Theming is not used to provide a logo');
+ return;
+ }
+
+ $dimensions = '';
+ if ($image !== false) {
+ $dimensions = imagesx($image) . 'x' . imagesy($image);
+ } elseif (str_starts_with($simpleFile->getMimeType(), 'image/svg')) {
+ $matched = preg_match('/viewbox=["\']\d* \d* (\d*\.?\d*) (\d*\.?\d*)["\']/i', $simpleFile->getContent(), $matches);
+ if ($matched) {
+ $dimensions = $matches[1] . 'x' . $matches[2];
+ }
+ }
+
+ if (!$dimensions) {
+ $output->warning('Failed to read dimensions from logo');
+ $this->config->deleteAppValue('theming', 'logoDimensions');
+ return;
+ }
+
+ $dimensions = imagesx($image) . 'x' . imagesy($image);
+ $this->config->setAppValue('theming', 'logoDimensions', $dimensions);
+ $output->info('Updated logo dimensions: ' . $dimensions);
+ }
+}
diff --git a/lib/private/Repair/RepairMimeTypes.php b/lib/private/Repair/RepairMimeTypes.php
new file mode 100644
index 00000000000..3c9720b9e91
--- /dev/null
+++ b/lib/private/Repair/RepairMimeTypes.php
@@ -0,0 +1,473 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OC\Repair;
+
+use OC\Migration\NullOutput;
+use OCP\DB\Exception;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IAppConfig;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class RepairMimeTypes implements IRepairStep {
+ private bool $dryRun = false;
+ private int $changeCount = 0;
+
+ /** @var int */
+ protected int $folderMimeTypeId;
+
+ public function __construct(
+ protected IConfig $config,
+ protected IAppConfig $appConfig,
+ protected IDBConnection $connection,
+ ) {
+ }
+
+ public function getName(): string {
+ return 'Repair mime types';
+ }
+
+ /**
+ * @throws Exception
+ */
+ private function updateMimetypes($updatedMimetypes): IResult|int|null {
+ if ($this->dryRun) {
+ $this->changeCount += count($updatedMimetypes);
+ return null;
+ }
+
+ $query = $this->connection->getQueryBuilder();
+ $query->select('id')
+ ->from('mimetypes')
+ ->where($query->expr()->eq('mimetype', $query->createParameter('mimetype'), IQueryBuilder::PARAM_INT));
+ $insert = $this->connection->getQueryBuilder();
+ $insert->insert('mimetypes')
+ ->setValue('mimetype', $insert->createParameter('mimetype'));
+
+ if (empty($this->folderMimeTypeId)) {
+ $query->setParameter('mimetype', 'httpd/unix-directory');
+ $result = $query->execute();
+ $this->folderMimeTypeId = (int)$result->fetchOne();
+ $result->closeCursor();
+ }
+
+ $update = $this->connection->getQueryBuilder();
+ $update->update('filecache')
+ ->runAcrossAllShards()
+ ->set('mimetype', $update->createParameter('mimetype'))
+ ->where($update->expr()->neq('mimetype', $update->createParameter('mimetype'), IQueryBuilder::PARAM_INT))
+ ->andWhere($update->expr()->neq('mimetype', $update->createParameter('folder'), IQueryBuilder::PARAM_INT))
+ ->andWhere($update->expr()->iLike('name', $update->createParameter('name')))
+ ->setParameter('folder', $this->folderMimeTypeId);
+
+ $count = 0;
+ foreach ($updatedMimetypes as $extension => $mimetype) {
+ // get target mimetype id
+ $query->setParameter('mimetype', $mimetype);
+ $result = $query->execute();
+ $mimetypeId = (int)$result->fetchOne();
+ $result->closeCursor();
+
+ if (!$mimetypeId) {
+ // insert mimetype
+ $insert->setParameter('mimetype', $mimetype);
+ $insert->execute();
+ $mimetypeId = $insert->getLastInsertId();
+ }
+
+ // change mimetype for files with x extension
+ $update->setParameter('mimetype', $mimetypeId)
+ ->setParameter('name', '%' . $this->connection->escapeLikeParameter('.' . $extension));
+ $count += $update->execute();
+ }
+
+ return $count;
+ }
+
+ /**
+ * @throws Exception
+ * @since 12.0.0.14
+ */
+ private function introduceImageTypes(): IResult|int|null {
+ $updatedMimetypes = [
+ 'jp2' => 'image/jp2',
+ 'webp' => 'image/webp',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 12.0.0.13
+ */
+ private function introduceWindowsProgramTypes(): IResult|int|null {
+ $updatedMimetypes = [
+ 'htaccess' => 'text/plain',
+ 'bat' => 'application/x-msdos-program',
+ 'cmd' => 'application/cmd',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 13.0.0.0
+ */
+ private function introduceLocationTypes(): IResult|int|null {
+ $updatedMimetypes = [
+ 'gpx' => 'application/gpx+xml',
+ 'kml' => 'application/vnd.google-earth.kml+xml',
+ 'kmz' => 'application/vnd.google-earth.kmz',
+ 'tcx' => 'application/vnd.garmin.tcx+xml',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 13.0.0.3
+ */
+ private function introduceInternetShortcutTypes(): IResult|int|null {
+ $updatedMimetypes = [
+ 'url' => 'application/internet-shortcut',
+ 'webloc' => 'application/internet-shortcut'
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 13.0.0.6
+ */
+ private function introduceStreamingTypes(): IResult|int|null {
+ $updatedMimetypes = [
+ 'm3u' => 'audio/mpegurl',
+ 'm3u8' => 'audio/mpegurl',
+ 'pls' => 'audio/x-scpls'
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 14.0.0.8
+ */
+ private function introduceVisioTypes(): IResult|int|null {
+ $updatedMimetypes = [
+ 'vsdm' => 'application/vnd.visio',
+ 'vsdx' => 'application/vnd.visio',
+ 'vssm' => 'application/vnd.visio',
+ 'vssx' => 'application/vnd.visio',
+ 'vstm' => 'application/vnd.visio',
+ 'vstx' => 'application/vnd.visio',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 14.0.0.10
+ */
+ private function introduceComicbookTypes(): IResult|int|null {
+ $updatedMimetypes = [
+ 'cb7' => 'application/comicbook+7z',
+ 'cba' => 'application/comicbook+ace',
+ 'cbr' => 'application/comicbook+rar',
+ 'cbt' => 'application/comicbook+tar',
+ 'cbtc' => 'application/comicbook+truecrypt',
+ 'cbz' => 'application/comicbook+zip',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 20.0.0.5
+ */
+ private function introduceOpenDocumentTemplates(): IResult|int|null {
+ $updatedMimetypes = [
+ 'ott' => 'application/vnd.oasis.opendocument.text-template',
+ 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template',
+ 'otp' => 'application/vnd.oasis.opendocument.presentation-template',
+ 'otg' => 'application/vnd.oasis.opendocument.graphics-template',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 21.0.0.7
+ */
+ private function introduceOrgModeType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'org' => 'text/org'
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 23.0.0.2
+ */
+ private function introduceFlatOpenDocumentType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'fodt' => 'application/vnd.oasis.opendocument.text-flat-xml',
+ 'fods' => 'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
+ 'fodg' => 'application/vnd.oasis.opendocument.graphics-flat-xml',
+ 'fodp' => 'application/vnd.oasis.opendocument.presentation-flat-xml',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 25.0.0.2
+ */
+ private function introduceOnlyofficeFormType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'oform' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform',
+ 'docxf' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 26.0.0.1
+ */
+ private function introduceAsciidocType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'adoc' => 'text/asciidoc',
+ 'asciidoc' => 'text/asciidoc',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 28.0.0.5
+ */
+ private function introduceEnhancedMetafileFormatType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'emf' => 'image/emf',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 29.0.0.2
+ */
+ private function introduceEmlAndMsgFormatType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'eml' => 'message/rfc822',
+ 'msg' => 'application/vnd.ms-outlook',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 29.0.0.6
+ */
+ private function introduceAacAudioType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'aac' => 'audio/aac',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 29.0.10
+ */
+ private function introduceReStructuredTextFormatType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'rst' => 'text/x-rst',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 30.0.0
+ */
+ private function introduceExcalidrawType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'excalidraw' => 'application/vnd.excalidraw+json',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+
+ /**
+ * @throws Exception
+ * @since 31.0.0
+ */
+ private function introduceZstType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'zst' => 'application/zstd',
+ 'nfo' => 'text/x-nfo',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+ /**
+ * @throws Exception
+ * @since 32.0.0
+ */
+ private function introduceMusicxmlType(): IResult|int|null {
+ $updatedMimetypes = [
+ 'mxl' => 'application/vnd.recordare.musicxml',
+ 'musicxml' => 'application/vnd.recordare.musicxml+xml',
+ ];
+
+ return $this->updateMimetypes($updatedMimetypes);
+ }
+
+
+
+ /**
+ * Check if there are any migrations available
+ *
+ * @throws Exception
+ */
+ public function migrationsAvailable(): bool {
+ $this->dryRun = true;
+ $this->run(new NullOutput());
+ $this->dryRun = false;
+ return $this->changeCount > 0;
+ }
+
+ /**
+ * Get the current mimetype version
+ */
+ private function getMimeTypeVersion(): string {
+ $serverVersion = $this->config->getSystemValueString('version', '0.0.0');
+ // 29.0.0.10 is the last version with a mimetype migration before it was moved to a separate version number
+ if (version_compare($serverVersion, '29.0.0.10', '>')) {
+ return $this->appConfig->getValueString('files', 'mimetype_version', '29.0.0.10');
+ }
+
+ return $serverVersion;
+ }
+
+ /**
+ * Fix mime types
+ *
+ * @throws Exception
+ */
+ public function run(IOutput $out): void {
+ $serverVersion = $this->config->getSystemValueString('version', '0.0.0');
+ $mimeTypeVersion = $this->getMimeTypeVersion();
+
+ // NOTE TO DEVELOPERS: when adding new mime types, please make sure to
+ // add a version comparison to avoid doing it every time
+ // PLEASE ALSO KEEP THE LIST SORTED BY VERSION NUMBER
+
+ if (version_compare($mimeTypeVersion, '12.0.0.14', '<') && $this->introduceImageTypes()) {
+ $out->info('Fixed image mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '12.0.0.13', '<') && $this->introduceWindowsProgramTypes()) {
+ $out->info('Fixed windows program mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '13.0.0.0', '<') && $this->introduceLocationTypes()) {
+ $out->info('Fixed geospatial mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '13.0.0.3', '<') && $this->introduceInternetShortcutTypes()) {
+ $out->info('Fixed internet-shortcut mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '13.0.0.6', '<') && $this->introduceStreamingTypes()) {
+ $out->info('Fixed streaming mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '14.0.0.8', '<') && $this->introduceVisioTypes()) {
+ $out->info('Fixed visio mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '14.0.0.10', '<') && $this->introduceComicbookTypes()) {
+ $out->info('Fixed comicbook mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '20.0.0.5', '<') && $this->introduceOpenDocumentTemplates()) {
+ $out->info('Fixed OpenDocument template mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '21.0.0.7', '<') && $this->introduceOrgModeType()) {
+ $out->info('Fixed orgmode mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '23.0.0.2', '<') && $this->introduceFlatOpenDocumentType()) {
+ $out->info('Fixed Flat OpenDocument mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '25.0.0.2', '<') && $this->introduceOnlyofficeFormType()) {
+ $out->info('Fixed ONLYOFFICE Forms OpenXML mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '26.0.0.1', '<') && $this->introduceAsciidocType()) {
+ $out->info('Fixed AsciiDoc mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '28.0.0.5', '<') && $this->introduceEnhancedMetafileFormatType()) {
+ $out->info('Fixed Enhanced Metafile Format mime types');
+ }
+
+ if (version_compare($mimeTypeVersion, '29.0.0.2', '<') && $this->introduceEmlAndMsgFormatType()) {
+ $out->info('Fixed eml and msg mime type');
+ }
+
+ if (version_compare($mimeTypeVersion, '29.0.0.6', '<') && $this->introduceAacAudioType()) {
+ $out->info('Fixed aac mime type');
+ }
+
+ if (version_compare($mimeTypeVersion, '29.0.0.10', '<') && $this->introduceReStructuredTextFormatType()) {
+ $out->info('Fixed ReStructured Text mime type');
+ }
+
+ if (version_compare($mimeTypeVersion, '30.0.0.0', '<') && $this->introduceExcalidrawType()) {
+ $out->info('Fixed Excalidraw mime type');
+ }
+
+ if (version_compare($mimeTypeVersion, '31.0.0.0', '<') && $this->introduceZstType()) {
+ $out->info('Fixed zst mime type');
+ }
+
+ if (version_compare($mimeTypeVersion, '32.0.0.0', '<') && $this->introduceMusicxmlType()) {
+ $out->info('Fixed musicxml mime type');
+ }
+
+ if (!$this->dryRun) {
+ $this->appConfig->setValueString('files', 'mimetype_version', $serverVersion);
+ }
+ }
+}