diff options
Diffstat (limited to 'lib/private/Repair')
38 files changed, 791 insertions, 350 deletions
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 index 8bd938b7e3a..e631a3303f1 100644 --- a/lib/private/Repair/AddCleanupUpdaterBackupsJob.php +++ b/lib/private/Repair/AddCleanupUpdaterBackupsJob.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/AddMetadataGenerationJob.php b/lib/private/Repair/AddMetadataGenerationJob.php index 4535fb0c9e0..76c60f303a7 100644 --- a/lib/private/Repair/AddMetadataGenerationJob.php +++ b/lib/private/Repair/AddMetadataGenerationJob.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/CleanTags.php b/lib/private/Repair/CleanTags.php index f2fc8156f29..ad8fa6235e6 100644 --- a/lib/private/Repair/CleanTags.php +++ b/lib/private/Repair/CleanTags.php @@ -19,11 +19,6 @@ use OCP\Migration\IRepairStep; * @package OC\Repair */ class CleanTags implements IRepairStep { - /** @var IDBConnection */ - protected $connection; - - /** @var IUserManager */ - protected $userManager; protected $deletedTags = 0; @@ -31,9 +26,10 @@ class CleanTags implements IRepairStep { * @param IDBConnection $connection * @param IUserManager $userManager */ - public function __construct(IDBConnection $connection, IUserManager $userManager) { - $this->connection = $connection; - $this->userManager = $userManager; + public function __construct( + protected IDBConnection $connection, + protected IUserManager $userManager, + ) { } /** @@ -73,7 +69,7 @@ class CleanTags implements IRepairStep { ->orderBy('uid') ->setMaxResults(50) ->setFirstResult($offset); - $result = $query->execute(); + $result = $query->executeQuery(); $users = []; $hadResults = false; @@ -94,7 +90,7 @@ class CleanTags implements IRepairStep { $query = $this->connection->getQueryBuilder(); $query->delete('vcategory') ->where($query->expr()->in('uid', $query->createNamedParameter($users, IQueryBuilder::PARAM_STR_ARRAY))); - $this->deletedTags += $query->execute(); + $this->deletedTags += $query->executeStatement(); } return true; } @@ -107,7 +103,7 @@ class CleanTags implements IRepairStep { $output, '%d tags for delete files have been removed.', 'vcategory_to_object', 'objid', - 'filecache', 'fileid', 'path_hash' + 'filecache', 'fileid', 'fileid' ); } @@ -147,8 +143,8 @@ class CleanTags implements IRepairStep { * @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 + * @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(); @@ -162,23 +158,24 @@ class CleanTags implements IRepairStep { ->andWhere( $qb->expr()->isNull('s.' . $sourceNullColumn) ); - $result = $qb->execute(); + $result = $qb->executeQuery(); $orphanItems = []; while ($row = $result->fetch()) { - $orphanItems[] = (int) $row[$deleteId]; + $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) { - $qb->delete($deleteTable) - ->where( - $qb->expr()->eq('type', $qb->expr()->literal('files')) - ) - ->andWhere($qb->expr()->in($deleteId, $qb->createParameter('ids'))); - $qb->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY); - $qb->execute(); + $deleteQuery->setParameter('ids', $items, IQueryBuilder::PARAM_INT_ARRAY); + $deleteQuery->executeStatement(); } } diff --git a/lib/private/Repair/ClearFrontendCaches.php b/lib/private/Repair/ClearFrontendCaches.php index 77a3df5598a..5c57a63379d 100644 --- a/lib/private/Repair/ClearFrontendCaches.php +++ b/lib/private/Repair/ClearFrontendCaches.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/ClearGeneratedAvatarCache.php b/lib/private/Repair/ClearGeneratedAvatarCache.php index 2dea4bd2d61..0f743afbb4c 100644 --- a/lib/private/Repair/ClearGeneratedAvatarCache.php +++ b/lib/private/Repair/ClearGeneratedAvatarCache.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/ClearGeneratedAvatarCacheJob.php b/lib/private/Repair/ClearGeneratedAvatarCacheJob.php index 38cf03b731a..524a470e62a 100644 --- a/lib/private/Repair/ClearGeneratedAvatarCacheJob.php +++ b/lib/private/Repair/ClearGeneratedAvatarCacheJob.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/Collation.php b/lib/private/Repair/Collation.php index 0affb3b1ca9..43229792217 100644 --- a/lib/private/Repair/Collation.php +++ b/lib/private/Repair/Collation.php @@ -8,7 +8,6 @@ namespace OC\Repair; use Doctrine\DBAL\Exception\DriverException; -use Doctrine\DBAL\Platforms\MySQLPlatform; use OCP\IConfig; use OCP\IDBConnection; use OCP\Migration\IOutput; @@ -16,7 +15,7 @@ use OCP\Migration\IRepairStep; use Psr\Log\LoggerInterface; class Collation implements IRepairStep { - /** @var IConfig */ + /** @var IConfig */ protected $config; protected LoggerInterface $logger; @@ -34,7 +33,7 @@ class Collation implements IRepairStep { IConfig $config, LoggerInterface $logger, IDBConnection $connection, - $ignoreFailures + $ignoreFailures, ) { $this->connection = $connection; $this->config = $config; @@ -50,7 +49,7 @@ class Collation implements IRepairStep { * Fix mime types */ public function run(IOutput $output) { - if (!$this->connection->getDatabasePlatform() instanceof MySQLPlatform) { + if ($this->connection->getDatabaseProvider() !== IDBConnection::PLATFORM_MYSQL) { $output->info('Not a mysql database -> nothing to do'); return; } @@ -93,16 +92,16 @@ class Collation implements IRepairStep { * @return string[] */ protected function getAllNonUTF8BinTables(IDBConnection $connection) { - $dbName = $this->config->getSystemValueString("dbname"); + $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*%'", + '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(); @@ -113,11 +112,11 @@ class Collation implements IRepairStep { // 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*%'", + '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(); 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 index c4be72ce530..476db9e4702 100644 --- a/lib/private/Repair/Events/RepairAdvanceEvent.php +++ b/lib/private/Repair/Events/RepairAdvanceEvent.php @@ -16,7 +16,7 @@ class RepairAdvanceEvent extends Event { public function __construct( int $increment, - string $description + string $description, ) { $this->increment = $increment; $this->description = $description; diff --git a/lib/private/Repair/Events/RepairErrorEvent.php b/lib/private/Repair/Events/RepairErrorEvent.php index 8cd5d41b1b4..e5be8a5a031 100644 --- a/lib/private/Repair/Events/RepairErrorEvent.php +++ b/lib/private/Repair/Events/RepairErrorEvent.php @@ -14,7 +14,7 @@ class RepairErrorEvent extends Event { private string $message; public function __construct( - string $message + string $message, ) { $this->message = $message; } diff --git a/lib/private/Repair/Events/RepairInfoEvent.php b/lib/private/Repair/Events/RepairInfoEvent.php index c48b295a9a9..ce8eb2f99e6 100644 --- a/lib/private/Repair/Events/RepairInfoEvent.php +++ b/lib/private/Repair/Events/RepairInfoEvent.php @@ -14,7 +14,7 @@ class RepairInfoEvent extends Event { private string $message; public function __construct( - string $message + string $message, ) { $this->message = $message; } diff --git a/lib/private/Repair/Events/RepairStartEvent.php b/lib/private/Repair/Events/RepairStartEvent.php index e154df5e6e1..47e713d57d9 100644 --- a/lib/private/Repair/Events/RepairStartEvent.php +++ b/lib/private/Repair/Events/RepairStartEvent.php @@ -16,7 +16,7 @@ class RepairStartEvent extends Event { public function __construct( int $max, - string $current + string $current, ) { $this->max = $max; $this->current = $current; diff --git a/lib/private/Repair/Events/RepairStepEvent.php b/lib/private/Repair/Events/RepairStepEvent.php index 7140d13687d..27e1efbdb08 100644 --- a/lib/private/Repair/Events/RepairStepEvent.php +++ b/lib/private/Repair/Events/RepairStepEvent.php @@ -14,7 +14,7 @@ class RepairStepEvent extends Event { private string $stepName; public function __construct( - string $stepName + string $stepName, ) { $this->stepName = $stepName; } diff --git a/lib/private/Repair/Events/RepairWarningEvent.php b/lib/private/Repair/Events/RepairWarningEvent.php index 403eec87158..6893a7212ec 100644 --- a/lib/private/Repair/Events/RepairWarningEvent.php +++ b/lib/private/Repair/Events/RepairWarningEvent.php @@ -14,7 +14,7 @@ class RepairWarningEvent extends Event { private string $message; public function __construct( - string $message + string $message, ) { $this->message = $message; } diff --git a/lib/private/Repair/MoveUpdaterStepFile.php b/lib/private/Repair/MoveUpdaterStepFile.php index c9b51b308c4..bb8f9d3acfc 100644 --- a/lib/private/Repair/MoveUpdaterStepFile.php +++ b/lib/private/Repair/MoveUpdaterStepFile.php @@ -1,10 +1,12 @@ <?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; @@ -40,7 +42,7 @@ class MoveUpdaterStepFile implements IRepairStep { // cleanup if (file_exists($previousStepFile)) { - if (\OC_Helper::rmdirr($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'); diff --git a/lib/private/Repair/NC11/FixMountStorages.php b/lib/private/Repair/NC11/FixMountStorages.php deleted file mode 100644 index b1663102d2f..00000000000 --- a/lib/private/Repair/NC11/FixMountStorages.php +++ /dev/null @@ -1,60 +0,0 @@ -<?php -/** - * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -namespace OC\Repair\NC11; - -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCP\IDBConnection; -use OCP\Migration\IOutput; -use OCP\Migration\IRepairStep; - -class FixMountStorages implements IRepairStep { - /** @var IDBConnection */ - private $db; - - /** - * @param IDBConnection $db - */ - public function __construct(IDBConnection $db) { - $this->db = $db; - } - - /** - * @return string - */ - public function getName() { - return 'Fix potential broken mount points'; - } - - public function run(IOutput $output) { - $query = $this->db->getQueryBuilder(); - $query->select('m.id', 'f.storage') - ->from('mounts', 'm') - ->leftJoin('m', 'filecache', 'f', $query->expr()->eq('m.root_id', 'f.fileid')) - ->where($query->expr()->neq('m.storage_id', 'f.storage')); - - $update = $this->db->getQueryBuilder(); - $update->update('mounts') - ->set('storage_id', $update->createParameter('storage')) - ->where($query->expr()->eq('id', $update->createParameter('mount'))); - - $result = $query->execute(); - $entriesUpdated = 0; - while ($row = $result->fetch()) { - $update->setParameter('storage', $row['storage'], IQueryBuilder::PARAM_INT) - ->setParameter('mount', $row['id'], IQueryBuilder::PARAM_INT); - $update->execute(); - $entriesUpdated++; - } - $result->closeCursor(); - - if ($entriesUpdated > 0) { - $output->info($entriesUpdated . ' mounts updated'); - return; - } - - $output->info('No mounts updated'); - } -} diff --git a/lib/private/Repair/NC13/AddLogRotateJob.php b/lib/private/Repair/NC13/AddLogRotateJob.php index 8fe68a42819..bd6c510785f 100644 --- a/lib/private/Repair/NC13/AddLogRotateJob.php +++ b/lib/private/Repair/NC13/AddLogRotateJob.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php b/lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php index a9cbbb4cbbf..646dd2c5e83 100644 --- a/lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php +++ b/lib/private/Repair/NC16/CleanupCardDAVPhotoCache.php @@ -6,9 +6,10 @@ 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\IAppData; +use OCP\Files\AppData\IAppDataFactory; use OCP\Files\NotFoundException; use OCP\Files\SimpleFS\ISimpleFolder; use OCP\IConfig; @@ -27,18 +28,11 @@ use RuntimeException; * photo could be returned for this vcard. These invalid files are removed by this migration step. */ class CleanupCardDAVPhotoCache implements IRepairStep { - /** @var IConfig */ - private $config; - - /** @var IAppData */ - private $appData; - - private LoggerInterface $logger; - - public function __construct(IConfig $config, IAppData $appData, LoggerInterface $logger) { - $this->config = $config; - $this->appData = $appData; - $this->logger = $logger; + public function __construct( + private IConfig $config, + private IAppDataFactory $appDataFactory, + private LoggerInterface $logger, + ) { } public function getName(): string { @@ -46,8 +40,10 @@ class CleanupCardDAVPhotoCache implements IRepairStep { } private function repair(IOutput $output): void { + $photoCacheAppData = $this->appDataFactory->get('dav-photocache'); + try { - $folders = $this->appData->getDirectoryListing(); + $folders = $photoCacheAppData->getDirectoryListing(); } catch (NotFoundException $e) { return; } catch (RuntimeException $e) { diff --git a/lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php b/lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php index 4f80b3809e8..5cee33b381c 100644 --- a/lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php +++ b/lib/private/Repair/NC21/AddCheckForUserCertificatesJob.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/NC21/ValidatePhoneNumber.php b/lib/private/Repair/NC21/ValidatePhoneNumber.php deleted file mode 100644 index 3a6ace37bd2..00000000000 --- a/lib/private/Repair/NC21/ValidatePhoneNumber.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -namespace OC\Repair\NC21; - -use OCP\Accounts\IAccountManager; -use OCP\IConfig; -use OCP\IUser; -use OCP\IUserManager; -use OCP\Migration\IOutput; -use OCP\Migration\IRepairStep; - -class ValidatePhoneNumber implements IRepairStep { - /** @var IConfig */ - protected $config; - /** @var IUserManager */ - protected $userManager; - /** @var IAccountManager */ - private $accountManager; - - public function __construct(IUserManager $userManager, - IAccountManager $accountManager, - IConfig $config) { - $this->config = $config; - $this->userManager = $userManager; - $this->accountManager = $accountManager; - } - - public function getName(): string { - return 'Validate the phone number and store it in a known format for search'; - } - - public function run(IOutput $output): void { - if ($this->config->getSystemValueString('default_phone_region', '') === '') { - $output->warning('Can not validate phone numbers without `default_phone_region` being set in the config file'); - return; - } - - $numUpdated = 0; - $numRemoved = 0; - - $this->userManager->callForSeenUsers(function (IUser $user) use (&$numUpdated, &$numRemoved) { - $account = $this->accountManager->getAccount($user); - $property = $account->getProperty(IAccountManager::PROPERTY_PHONE); - - if ($property->getValue() !== '') { - $this->accountManager->updateAccount($account); - $updatedAccount = $this->accountManager->getAccount($user); - $updatedProperty = $updatedAccount->getProperty(IAccountManager::PROPERTY_PHONE); - - if ($property->getValue() !== $updatedProperty->getValue()) { - if ($updatedProperty->getValue() === '') { - $numRemoved++; - } else { - $numUpdated++; - } - } - } - }); - - if ($numRemoved > 0 || $numUpdated > 0) { - $output->info('Updated ' . $numUpdated . ' entries and cleaned ' . $numRemoved . ' invalid phone numbers'); - } - } -} diff --git a/lib/private/Repair/NC25/AddMissingSecretJob.php b/lib/private/Repair/NC25/AddMissingSecretJob.php index b407ef2a2a9..46b89d5f6f7 100644 --- a/lib/private/Repair/NC25/AddMissingSecretJob.php +++ b/lib/private/Repair/NC25/AddMissingSecretJob.php @@ -33,7 +33,7 @@ class AddMissingSecretJob implements IRepairStep { 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."); + $output->warning('passwordsalt is missing from your config.php and your config.php is read only. Please fix it manually.'); } } @@ -42,7 +42,7 @@ class AddMissingSecretJob implements IRepairStep { 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."); + $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 index 54f2078395e..003c15cfb88 100644 --- a/lib/private/Repair/OldGroupMembershipShares.php +++ b/lib/private/Repair/OldGroupMembershipShares.php @@ -14,12 +14,6 @@ use OCP\Migration\IRepairStep; use OCP\Share\IShare; class OldGroupMembershipShares implements IRepairStep { - /** @var \OCP\IDBConnection */ - protected $connection; - - /** @var \OCP\IGroupManager */ - protected $groupManager; - /** * @var array [gid => [uid => (bool)]] */ @@ -29,9 +23,10 @@ class OldGroupMembershipShares implements IRepairStep { * @param IDBConnection $connection * @param IGroupManager $groupManager */ - public function __construct(IDBConnection $connection, IGroupManager $groupManager) { - $this->connection = $connection; - $this->groupManager = $groupManager; + public function __construct( + protected IDBConnection $connection, + protected IGroupManager $groupManager, + ) { } /** @@ -57,20 +52,20 @@ class OldGroupMembershipShares implements IRepairStep { ->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))) + ->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->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { if (!$this->isMember($row['group'], $row['user'])) { - $deletedEntries += $deleteQuery->setParameter('share', (int) $row['id']) - ->execute(); + $deletedEntries += $deleteQuery->setParameter('share', (int)$row['id']) + ->executeStatement(); } } $result->closeCursor(); diff --git a/lib/private/Repair/Owncloud/CleanPreviews.php b/lib/private/Repair/Owncloud/CleanPreviews.php index 86e173cf402..50ee965e087 100644 --- a/lib/private/Repair/Owncloud/CleanPreviews.php +++ b/lib/private/Repair/Owncloud/CleanPreviews.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/Owncloud/DropAccountTermsTable.php b/lib/private/Repair/Owncloud/DropAccountTermsTable.php index 18f169c9b49..534825c146a 100644 --- a/lib/private/Repair/Owncloud/DropAccountTermsTable.php +++ b/lib/private/Repair/Owncloud/DropAccountTermsTable.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/Owncloud/MigrateOauthTables.php b/lib/private/Repair/Owncloud/MigrateOauthTables.php index e8728cd2f66..de26a907e02 100644 --- a/lib/private/Repair/Owncloud/MigrateOauthTables.php +++ b/lib/private/Repair/Owncloud/MigrateOauthTables.php @@ -1,25 +1,37 @@ <?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 { - /** @var Connection */ - protected $db; - /** - * @param Connection $db - */ - public function __construct(Connection $db) { - $this->db = $db; + public function __construct( + protected Connection $db, + private AccessTokenMapper $accessTokenMapper, + private ITokenProvider $tokenProvider, + private ISecureRandom $random, + private ITimeFactory $timeFactory, + private ICrypto $crypto, + private IConfig $config, + ) { } /** @@ -32,19 +44,27 @@ class MigrateOauthTables implements IRepairStep { public function run(IOutput $output) { $schema = new SchemaWrapper($this->db); if (!$schema->hasTable('oauth2_clients')) { - $output->info("oauth2_clients table does not exist."); + $output->info('oauth2_clients table does not exist.'); return; } - $output->info("Update the oauth2_access_tokens table schema."); - $schema = new SchemaWrapper($this->db); + // 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, @@ -52,14 +72,33 @@ class MigrateOauthTables implements IRepairStep { ]); } 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."); - $schema = new SchemaWrapper($this->db); + $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 @@ -101,8 +140,9 @@ class MigrateOauthTables implements IRepairStep { $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."); @@ -114,7 +154,7 @@ class MigrateOauthTables implements IRepairStep { $result->closeCursor(); // 2. Insert them into the client_identifier column. - foreach ($identifiers as ["id" => $id, "identifier" => $clientIdentifier]) { + foreach ($identifiers as ['id' => $id, 'identifier' => $clientIdentifier]) { $insertQuery = $this->db->getQueryBuilder(); $insertQuery->update('oauth2_clients') ->set('client_identifier', $insertQuery->createNamedParameter($clientIdentifier, IQueryBuilder::PARAM_STR)) @@ -122,14 +162,21 @@ class MigrateOauthTables implements IRepairStep { ->executeStatement(); } - $output->info("Drop the identifier column."); - $schema = new SchemaWrapper($this->db); + $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); } - $output->info('Delete clients (and their related access tokens) with the redirect_uri starting with oc:// or ending with *'); + $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(); @@ -138,10 +185,12 @@ class MigrateOauthTables implements IRepairStep { ->from('oauth2_clients') ->where( $qbSelectClientId->expr()->iLike('redirect_uri', $qbDeleteAccessTokens->createNamedParameter('oc://%', IQueryBuilder::PARAM_STR)) - ) - ->orWhere( + ); + if (!$enableOcClients) { + $qbSelectClientId->orWhere( $qbSelectClientId->expr()->iLike('redirect_uri', $qbDeleteAccessTokens->createNamedParameter('%*', IQueryBuilder::PARAM_STR)) ); + } $qbDeleteAccessTokens->delete('oauth2_access_tokens') ->where( @@ -154,10 +203,57 @@ class MigrateOauthTables implements IRepairStep { $qbDeleteClients->delete('oauth2_clients') ->where( $qbDeleteClients->expr()->iLike('redirect_uri', $qbDeleteClients->createNamedParameter('oc://%', IQueryBuilder::PARAM_STR)) - ) - ->orWhere( + ); + 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 index 7fdabae7a66..9e3f4b89b13 100644 --- a/lib/private/Repair/Owncloud/MoveAvatars.php +++ b/lib/private/Repair/Owncloud/MoveAvatars.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Repair/Owncloud/SaveAccountsTableData.php b/lib/private/Repair/Owncloud/SaveAccountsTableData.php index 1b6da7c858f..ab1560ddb8d 100644 --- a/lib/private/Repair/Owncloud/SaveAccountsTableData.php +++ b/lib/private/Repair/Owncloud/SaveAccountsTableData.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -150,7 +151,7 @@ class SaveAccountsTableData implements IRepairStep { * @throws \UnexpectedValueException */ protected function migrateUserInfo(IQueryBuilder $update, $userdata) { - $state = (int) $userdata['state']; + $state = (int)$userdata['state']; if ($state === 3) { // Deleted user, ignore return; diff --git a/lib/private/Repair/Owncloud/UpdateLanguageCodes.php b/lib/private/Repair/Owncloud/UpdateLanguageCodes.php index e27ab06b2f3..8d9046ad49f 100644 --- a/lib/private/Repair/Owncloud/UpdateLanguageCodes.php +++ b/lib/private/Repair/Owncloud/UpdateLanguageCodes.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later 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 index f128b6f731b..a07ebdb72c3 100644 --- a/lib/private/Repair/RemoveLinkShares.php +++ b/lib/private/Repair/RemoveLinkShares.php @@ -19,31 +19,17 @@ use OCP\Migration\IRepairStep; use OCP\Notification\IManager; class RemoveLinkShares implements IRepairStep { - /** @var IDBConnection */ - private $connection; - /** @var IConfig */ - private $config; /** @var string[] */ private $userToNotify = []; - /** @var IGroupManager */ - private $groupManager; - /** @var IManager */ - private $notificationManager; - /** @var ITimeFactory */ - private $timeFactory; - - public function __construct(IDBConnection $connection, - IConfig $config, - IGroupManager $groupManager, - IManager $notificationManager, - ITimeFactory $timeFactory) { - $this->connection = $connection; - $this->config = $config; - $this->groupManager = $groupManager; - $this->notificationManager = $notificationManager; - $this->timeFactory = $timeFactory; - } + 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'; @@ -74,7 +60,7 @@ class RemoveLinkShares implements IRepairStep { $qb = $this->connection->getQueryBuilder(); $qb->delete('share') ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))); - $qb->execute(); + $qb->executeStatement(); } /** @@ -107,11 +93,11 @@ class RemoveLinkShares implements IRepairStep { ->from('share') ->where($query->expr()->in('id', $query->createFunction($subQuery->getSQL()))); - $result = $query->execute(); + $result = $query->executeQuery(); $data = $result->fetch(); $result->closeCursor(); - return (int) $data['total']; + return (int)$data['total']; } /** @@ -137,7 +123,7 @@ class RemoveLinkShares implements IRepairStep { )) ->andWhere($query->expr()->eq('s1.item_source', 's2.item_source')); /** @var IResult $result */ - $result = $query->execute(); + $result = $query->executeQuery(); return $result; } @@ -180,7 +166,7 @@ class RemoveLinkShares implements IRepairStep { $users = array_keys($this->userToNotify); foreach ($users as $user) { - $notification->setUser((string) $user); + $notification->setUser((string)$user); $this->notificationManager->notify($notification); } } diff --git a/lib/private/Repair/RepairDavShares.php b/lib/private/Repair/RepairDavShares.php index 792fdd4033e..557e2c080ca 100644 --- a/lib/private/Repair/RepairDavShares.php +++ b/lib/private/Repair/RepairDavShares.php @@ -23,27 +23,15 @@ use function urlencode; class RepairDavShares implements IRepairStep { protected const GROUP_PRINCIPAL_PREFIX = 'principals/groups/'; - /** @var IConfig */ - private $config; - /** @var IDBConnection */ - private $dbc; - /** @var IGroupManager */ - private $groupManager; - /** @var LoggerInterface */ - private $logger; /** @var bool */ private $hintInvalidShares = false; public function __construct( - IConfig $config, - IDBConnection $dbc, - IGroupManager $groupManager, - LoggerInterface $logger + private IConfig $config, + private IDBConnection $dbc, + private IGroupManager $groupManager, + private LoggerInterface $logger, ) { - $this->config = $config; - $this->dbc = $dbc; - $this->groupManager = $groupManager; - $this->logger = $logger; } /** diff --git a/lib/private/Repair/RepairInvalidShares.php b/lib/private/Repair/RepairInvalidShares.php index f28ae1c45fb..9553f25ee70 100644 --- a/lib/private/Repair/RepairInvalidShares.php +++ b/lib/private/Repair/RepairInvalidShares.php @@ -7,6 +7,8 @@ */ namespace OC\Repair; +use OCP\IConfig; +use OCP\IDBConnection; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; @@ -16,19 +18,10 @@ use OCP\Migration\IRepairStep; class RepairInvalidShares implements IRepairStep { public const CHUNK_SIZE = 200; - /** @var \OCP\IConfig */ - protected $config; - - /** @var \OCP\IDBConnection */ - protected $connection; - - /** - * @param \OCP\IConfig $config - * @param \OCP\IDBConnection $connection - */ - public function __construct($config, $connection) { - $this->connection = $connection; - $this->config = $config; + public function __construct( + protected IConfig $config, + protected IDBConnection $connection, + ) { } public function getName() { @@ -49,7 +42,7 @@ class RepairInvalidShares implements IRepairStep { ->where($builder->expr()->eq('item_type', $builder->expr()->literal('file'))) ->andWhere($builder->expr()->neq('permissions', $permsFunc)); - $updatedEntries = $builder->execute(); + $updatedEntries = $builder->executeStatement(); if ($updatedEntries > 0) { $out->info('Fixed file share permissions for ' . $updatedEntries . ' shares'); } @@ -65,7 +58,7 @@ class RepairInvalidShares implements IRepairStep { $query->select('s1.parent') ->from('share', 's1') ->where($query->expr()->isNotNull('s1.parent')) - ->andWhere($query->expr()->isNull('s2.id')) + ->andWhere($query->expr()->isNull('s2.id')) ->leftJoin('s1', 'share', 's2', $query->expr()->eq('s1.parent', 's2.id')) ->groupBy('s1.parent') ->setMaxResults(self::CHUNK_SIZE); @@ -77,11 +70,11 @@ class RepairInvalidShares implements IRepairStep { $deletedInLastChunk = self::CHUNK_SIZE; while ($deletedInLastChunk === self::CHUNK_SIZE) { $deletedInLastChunk = 0; - $result = $query->execute(); + $result = $query->executeQuery(); while ($row = $result->fetch()) { $deletedInLastChunk++; - $deletedEntries += $deleteQuery->setParameter('parent', (int) $row['parent']) - ->execute(); + $deletedEntries += $deleteQuery->setParameter('parent', (int)$row['parent']) + ->executeStatement(); } $result->closeCursor(); } 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 index 103ce9c13fc..3c9720b9e91 100644 --- a/lib/private/Repair/RepairMimeTypes.php +++ b/lib/private/Repair/RepairMimeTypes.php @@ -1,38 +1,48 @@ <?php /** - * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors - * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * 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 { - /** @var IConfig */ - protected $config; - /** @var IDBConnection */ - protected $connection; + private bool $dryRun = false; + private int $changeCount = 0; /** @var int */ - protected $folderMimeTypeId; + protected int $folderMimeTypeId; - public function __construct(IConfig $config, - IDBConnection $connection) { - $this->config = $config; - $this->connection = $connection; + public function __construct( + protected IConfig $config, + protected IAppConfig $appConfig, + protected IDBConnection $connection, + ) { } - public function getName() { + public function getName(): string { return 'Repair mime types'; } - private function updateMimetypes($updatedMimetypes) { + /** + * @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') @@ -50,6 +60,7 @@ class RepairMimeTypes implements IRepairStep { $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)) @@ -80,16 +91,11 @@ class RepairMimeTypes implements IRepairStep { return $count; } - private function introduceAsciidocType() { - $updatedMimetypes = [ - 'adoc' => 'text/asciidoc', - 'asciidoc' => 'text/asciidoc', - ]; - - return $this->updateMimetypes($updatedMimetypes); - } - - private function introduceImageTypes() { + /** + * @throws Exception + * @since 12.0.0.14 + */ + private function introduceImageTypes(): IResult|int|null { $updatedMimetypes = [ 'jp2' => 'image/jp2', 'webp' => 'image/webp', @@ -98,7 +104,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceWindowsProgramTypes() { + /** + * @throws Exception + * @since 12.0.0.13 + */ + private function introduceWindowsProgramTypes(): IResult|int|null { $updatedMimetypes = [ 'htaccess' => 'text/plain', 'bat' => 'application/x-msdos-program', @@ -108,7 +118,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceLocationTypes() { + /** + * @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', @@ -119,7 +133,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceInternetShortcutTypes() { + /** + * @throws Exception + * @since 13.0.0.3 + */ + private function introduceInternetShortcutTypes(): IResult|int|null { $updatedMimetypes = [ 'url' => 'application/internet-shortcut', 'webloc' => 'application/internet-shortcut' @@ -128,7 +146,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceStreamingTypes() { + /** + * @throws Exception + * @since 13.0.0.6 + */ + private function introduceStreamingTypes(): IResult|int|null { $updatedMimetypes = [ 'm3u' => 'audio/mpegurl', 'm3u8' => 'audio/mpegurl', @@ -138,7 +160,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceVisioTypes() { + /** + * @throws Exception + * @since 14.0.0.8 + */ + private function introduceVisioTypes(): IResult|int|null { $updatedMimetypes = [ 'vsdm' => 'application/vnd.visio', 'vsdx' => 'application/vnd.visio', @@ -151,7 +177,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceComicbookTypes() { + /** + * @throws Exception + * @since 14.0.0.10 + */ + private function introduceComicbookTypes(): IResult|int|null { $updatedMimetypes = [ 'cb7' => 'application/comicbook+7z', 'cba' => 'application/comicbook+ace', @@ -164,7 +194,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceOpenDocumentTemplates() { + /** + * @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', @@ -175,35 +209,64 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceFlatOpenDocumentType() { + /** + * @throws Exception + * @since 21.0.0.7 + */ + private function introduceOrgModeType(): 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", + 'org' => 'text/org' ]; return $this->updateMimetypes($updatedMimetypes); } - private function introduceOrgModeType() { + /** + * @throws Exception + * @since 23.0.0.2 + */ + private function introduceFlatOpenDocumentType(): IResult|int|null { $updatedMimetypes = [ - 'org' => 'text/org' + '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); } - private function introduceOnlyofficeFormType() { + /** + * @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", + 'oform' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.oform', + 'docxf' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxf', ]; return $this->updateMimetypes($updatedMimetypes); } - private function introduceEnhancedMetafileFormatType() { + /** + * @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', ]; @@ -211,7 +274,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceEmlAndMsgFormatType() { + /** + * @throws Exception + * @since 29.0.0.2 + */ + private function introduceEmlAndMsgFormatType(): IResult|int|null { $updatedMimetypes = [ 'eml' => 'message/rfc822', 'msg' => 'application/vnd.ms-outlook', @@ -220,7 +287,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceAacAudioType() { + /** + * @throws Exception + * @since 29.0.0.6 + */ + private function introduceAacAudioType(): IResult|int|null { $updatedMimetypes = [ 'aac' => 'audio/aac', ]; @@ -228,7 +299,11 @@ class RepairMimeTypes implements IRepairStep { return $this->updateMimetypes($updatedMimetypes); } - private function introduceReStructuredTextFormatType() { + /** + * @throws Exception + * @since 29.0.10 + */ + private function introduceReStructuredTextFormatType(): IResult|int|null { $updatedMimetypes = [ 'rst' => 'text/x-rst', ]; @@ -237,76 +312,162 @@ class RepairMimeTypes implements IRepairStep { } /** + * @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) { - $ocVersionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0'); + 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($ocVersionFromBeforeUpdate, '12.0.0.14', '<') && $this->introduceImageTypes()) { + if (version_compare($mimeTypeVersion, '12.0.0.14', '<') && $this->introduceImageTypes()) { $out->info('Fixed image mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '12.0.0.13', '<') && $this->introduceWindowsProgramTypes()) { + if (version_compare($mimeTypeVersion, '12.0.0.13', '<') && $this->introduceWindowsProgramTypes()) { $out->info('Fixed windows program mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '13.0.0.0', '<') && $this->introduceLocationTypes()) { + if (version_compare($mimeTypeVersion, '13.0.0.0', '<') && $this->introduceLocationTypes()) { $out->info('Fixed geospatial mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '13.0.0.3', '<') && $this->introduceInternetShortcutTypes()) { + if (version_compare($mimeTypeVersion, '13.0.0.3', '<') && $this->introduceInternetShortcutTypes()) { $out->info('Fixed internet-shortcut mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '13.0.0.6', '<') && $this->introduceStreamingTypes()) { + if (version_compare($mimeTypeVersion, '13.0.0.6', '<') && $this->introduceStreamingTypes()) { $out->info('Fixed streaming mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '14.0.0.8', '<') && $this->introduceVisioTypes()) { + if (version_compare($mimeTypeVersion, '14.0.0.8', '<') && $this->introduceVisioTypes()) { $out->info('Fixed visio mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '14.0.0.10', '<') && $this->introduceComicbookTypes()) { + if (version_compare($mimeTypeVersion, '14.0.0.10', '<') && $this->introduceComicbookTypes()) { $out->info('Fixed comicbook mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '20.0.0.5', '<') && $this->introduceOpenDocumentTemplates()) { + if (version_compare($mimeTypeVersion, '20.0.0.5', '<') && $this->introduceOpenDocumentTemplates()) { $out->info('Fixed OpenDocument template mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '21.0.0.7', '<') && $this->introduceOrgModeType()) { + if (version_compare($mimeTypeVersion, '21.0.0.7', '<') && $this->introduceOrgModeType()) { $out->info('Fixed orgmode mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '23.0.0.2', '<') && $this->introduceFlatOpenDocumentType()) { + if (version_compare($mimeTypeVersion, '23.0.0.2', '<') && $this->introduceFlatOpenDocumentType()) { $out->info('Fixed Flat OpenDocument mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '25.0.0.2', '<') && $this->introduceOnlyofficeFormType()) { + if (version_compare($mimeTypeVersion, '25.0.0.2', '<') && $this->introduceOnlyofficeFormType()) { $out->info('Fixed ONLYOFFICE Forms OpenXML mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '26.0.0.1', '<') && $this->introduceAsciidocType()) { + if (version_compare($mimeTypeVersion, '26.0.0.1', '<') && $this->introduceAsciidocType()) { $out->info('Fixed AsciiDoc mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '28.0.0.5', '<') && $this->introduceEnhancedMetafileFormatType()) { + if (version_compare($mimeTypeVersion, '28.0.0.5', '<') && $this->introduceEnhancedMetafileFormatType()) { $out->info('Fixed Enhanced Metafile Format mime types'); } - if (version_compare($ocVersionFromBeforeUpdate, '29.0.0.2', '<') && $this->introduceEmlAndMsgFormatType()) { + if (version_compare($mimeTypeVersion, '29.0.0.2', '<') && $this->introduceEmlAndMsgFormatType()) { $out->info('Fixed eml and msg mime type'); } - if (version_compare($ocVersionFromBeforeUpdate, '29.0.0.6', '<') && $this->introduceAacAudioType()) { + if (version_compare($mimeTypeVersion, '29.0.0.6', '<') && $this->introduceAacAudioType()) { $out->info('Fixed aac mime type'); } - if (version_compare($ocVersionFromBeforeUpdate, '29.0.0.10', '<') && $this->introduceReStructuredTextFormatType()) { + 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); + } } } |