aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Repair/Owncloud
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Repair/Owncloud')
-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
8 files changed, 850 insertions, 0 deletions
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.');
+ }
+ }
+}