From 9fe94f282f26310adca844fc2b206add91e30d6c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Julius=20H=C3=A4rtl?= <jus@bitgrid.net>
Date: Mon, 7 Dec 2020 14:30:08 +0100
Subject: [PATCH] Readd repair steps that are relevant when migrating from
 ownCloud
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This reverts commit d9b1492e03ab9fe58bb87baaeba745790ca15c53.

Signed-off-by: Julius Härtl <jus@bitgrid.net>
---
 lib/composer/composer/autoload_classmap.php   |   7 +
 lib/composer/composer/autoload_static.php     |   7 +
 lib/private/Repair.php                        |  20 ++
 lib/private/Repair/Owncloud/CleanPreviews.php |  73 ++++++
 .../Owncloud/CleanPreviewsBackgroundJob.php   | 132 ++++++++++
 .../Repair/Owncloud/InstallCoreBundle.php     |  80 ++++++
 lib/private/Repair/Owncloud/MoveAvatars.php   |  73 ++++++
 .../Owncloud/MoveAvatarsBackgroundJob.php     | 147 +++++++++++
 .../Repair/Owncloud/UpdateLanguageCodes.php   |  90 +++++++
 .../CleanPreviewsBackgroundJobTest.php        | 245 ++++++++++++++++++
 .../Repair/Owncloud/CleanPreviewsTest.php     | 134 ++++++++++
 .../Repair/Owncloud/InstallCoreBundleTest.php | 142 ++++++++++
 .../Owncloud/UpdateLanguageCodesTest.php      | 175 +++++++++++++
 13 files changed, 1325 insertions(+)
 create mode 100644 lib/private/Repair/Owncloud/CleanPreviews.php
 create mode 100644 lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php
 create mode 100644 lib/private/Repair/Owncloud/InstallCoreBundle.php
 create mode 100644 lib/private/Repair/Owncloud/MoveAvatars.php
 create mode 100644 lib/private/Repair/Owncloud/MoveAvatarsBackgroundJob.php
 create mode 100644 lib/private/Repair/Owncloud/UpdateLanguageCodes.php
 create mode 100644 tests/Test/Repair/Owncloud/CleanPreviewsBackgroundJobTest.php
 create mode 100644 tests/Test/Repair/Owncloud/CleanPreviewsTest.php
 create mode 100644 tests/Test/Repair/Owncloud/InstallCoreBundleTest.php
 create mode 100644 tests/Test/Repair/Owncloud/UpdateLanguageCodesTest.php

diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index b7dbc6675d2..765af4f165e 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1256,7 +1256,14 @@ return array(
     'OC\\Repair\\ClearGeneratedAvatarCache' => $baseDir . '/lib/private/Repair/ClearGeneratedAvatarCache.php',
     'OC\\Repair\\Collation' => $baseDir . '/lib/private/Repair/Collation.php',
     'OC\\Repair\\MoveUpdaterStepFile' => $baseDir . '/lib/private/Repair/MoveUpdaterStepFile.php',
+    'OC\\Repair\\NC11\\CleanPreviews' => $baseDir . '/lib/private/Repair/NC11/CleanPreviews.php',
+    'OC\\Repair\\NC11\\CleanPreviewsBackgroundJob' => $baseDir . '/lib/private/Repair/NC11/CleanPreviewsBackgroundJob.php',
     'OC\\Repair\\NC11\\FixMountStorages' => $baseDir . '/lib/private/Repair/NC11/FixMountStorages.php',
+    'OC\\Repair\\NC11\\MoveAvatars' => $baseDir . '/lib/private/Repair/NC11/MoveAvatars.php',
+    'OC\\Repair\\NC11\\MoveAvatarsBackgroundJob' => $baseDir . '/lib/private/Repair/NC11/MoveAvatarsBackgroundJob.php',
+    'OC\\Repair\\NC12\\InstallCoreBundle' => $baseDir . '/lib/private/Repair/NC12/InstallCoreBundle.php',
+    'OC\\Repair\\NC12\\RepairIdentityProofKeyFolders' => $baseDir . '/lib/private/Repair/NC12/RepairIdentityProofKeyFolders.php',
+    'OC\\Repair\\NC12\\UpdateLanguageCodes' => $baseDir . '/lib/private/Repair/NC12/UpdateLanguageCodes.php',
     'OC\\Repair\\NC13\\AddLogRotateJob' => $baseDir . '/lib/private/Repair/NC13/AddLogRotateJob.php',
     'OC\\Repair\\NC14\\AddPreviewBackgroundCleanupJob' => $baseDir . '/lib/private/Repair/NC14/AddPreviewBackgroundCleanupJob.php',
     'OC\\Repair\\NC16\\AddClenupLoginFlowV2BackgroundJob' => $baseDir . '/lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index a8984b486f3..43b0ac9cb5a 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1285,7 +1285,14 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OC\\Repair\\ClearGeneratedAvatarCache' => __DIR__ . '/../../..' . '/lib/private/Repair/ClearGeneratedAvatarCache.php',
         'OC\\Repair\\Collation' => __DIR__ . '/../../..' . '/lib/private/Repair/Collation.php',
         'OC\\Repair\\MoveUpdaterStepFile' => __DIR__ . '/../../..' . '/lib/private/Repair/MoveUpdaterStepFile.php',
+        'OC\\Repair\\NC11\\CleanPreviews' => __DIR__ . '/../../..' . '/lib/private/Repair/NC11/CleanPreviews.php',
+        'OC\\Repair\\NC11\\CleanPreviewsBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC11/CleanPreviewsBackgroundJob.php',
         'OC\\Repair\\NC11\\FixMountStorages' => __DIR__ . '/../../..' . '/lib/private/Repair/NC11/FixMountStorages.php',
+        'OC\\Repair\\NC11\\MoveAvatars' => __DIR__ . '/../../..' . '/lib/private/Repair/NC11/MoveAvatars.php',
+        'OC\\Repair\\NC11\\MoveAvatarsBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC11/MoveAvatarsBackgroundJob.php',
+        'OC\\Repair\\NC12\\InstallCoreBundle' => __DIR__ . '/../../..' . '/lib/private/Repair/NC12/InstallCoreBundle.php',
+        'OC\\Repair\\NC12\\RepairIdentityProofKeyFolders' => __DIR__ . '/../../..' . '/lib/private/Repair/NC12/RepairIdentityProofKeyFolders.php',
+        'OC\\Repair\\NC12\\UpdateLanguageCodes' => __DIR__ . '/../../..' . '/lib/private/Repair/NC12/UpdateLanguageCodes.php',
         'OC\\Repair\\NC13\\AddLogRotateJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC13/AddLogRotateJob.php',
         'OC\\Repair\\NC14\\AddPreviewBackgroundCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC14/AddPreviewBackgroundCleanupJob.php',
         'OC\\Repair\\NC16\\AddClenupLoginFlowV2BackgroundJob' => __DIR__ . '/../../..' . '/lib/private/Repair/NC16/AddClenupLoginFlowV2BackgroundJob.php',
diff --git a/lib/private/Repair.php b/lib/private/Repair.php
index 847a41aeb25..4793485a384 100644
--- a/lib/private/Repair.php
+++ b/lib/private/Repair.php
@@ -34,6 +34,7 @@
 
 namespace OC;
 
+use OC\App\AppStore\Bundles\BundleFetcher;
 use OC\Avatar\AvatarManager;
 use OC\Repair\AddBruteForceCleanupJob;
 use OC\Repair\AddCleanupUpdaterBackupsJob;
@@ -42,7 +43,11 @@ use OC\Repair\ClearFrontendCaches;
 use OC\Repair\ClearGeneratedAvatarCache;
 use OC\Repair\Collation;
 use OC\Repair\MoveUpdaterStepFile;
+use OC\Repair\Owncloud\CleanPreviews;
 use OC\Repair\NC11\FixMountStorages;
+use OC\Repair\Owncloud\MoveAvatars;
+use OC\Repair\Owncloud\InstallCoreBundle;
+use OC\Repair\Owncloud\UpdateLanguageCodes;
 use OC\Repair\NC13\AddLogRotateJob;
 use OC\Repair\NC14\AddPreviewBackgroundCleanupJob;
 use OC\Repair\NC16\AddClenupLoginFlowV2BackgroundJob;
@@ -151,7 +156,22 @@ class Repair implements IOutput {
 			new CleanTags(\OC::$server->getDatabaseConnection(), \OC::$server->getUserManager()),
 			new RepairInvalidShares(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()),
 			new MoveUpdaterStepFile(\OC::$server->getConfig()),
+			new MoveAvatars(
+				\OC::$server->getJobList(),
+				\OC::$server->getConfig()
+			),
+			new CleanPreviews(
+				\OC::$server->getJobList(),
+				\OC::$server->getUserManager(),
+				\OC::$server->getConfig()
+			),
 			new FixMountStorages(\OC::$server->getDatabaseConnection()),
+			new UpdateLanguageCodes(\OC::$server->getDatabaseConnection(), \OC::$server->getConfig()),
+			new InstallCoreBundle(
+				\OC::$server->query(BundleFetcher::class),
+				\OC::$server->getConfig(),
+				\OC::$server->query(Installer::class)
+			),
 			new AddLogRotateJob(\OC::$server->getJobList()),
 			new ClearFrontendCaches(\OC::$server->getMemCacheFactory(), \OC::$server->query(SCSSCacher::class), \OC::$server->query(JSCombiner::class)),
 			new ClearGeneratedAvatarCache(\OC::$server->getConfig(), \OC::$server->query(AvatarManager::class)),
diff --git a/lib/private/Repair/Owncloud/CleanPreviews.php b/lib/private/Repair/Owncloud/CleanPreviews.php
new file mode 100644
index 00000000000..5c183451d67
--- /dev/null
+++ b/lib/private/Repair/Owncloud/CleanPreviews.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+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..e8d89c9c7a0
--- /dev/null
+++ b/lib/private/Repair/Owncloud/CleanPreviewsBackgroundJob.php
@@ -0,0 +1,132 @@
+<?php
+/**
+ * @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OC\Repair\Owncloud;
+
+use OC\BackgroundJob\QueuedJob;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\ILogger;
+use OCP\IUserManager;
+
+class CleanPreviewsBackgroundJob extends QueuedJob {
+	/** @var IRootFolder */
+	private $rootFolder;
+
+	/** @var ILogger */
+	private $logger;
+
+	/** @var IJobList */
+	private $jobList;
+
+	/** @var ITimeFactory */
+	private $timeFactory;
+
+	/** @var IUserManager */
+	private $userManager;
+
+	/**
+	 * CleanPreviewsBackgroundJob constructor.
+	 *
+	 * @param IRootFolder $rootFolder
+	 * @param ILogger $logger
+	 * @param IJobList $jobList
+	 * @param ITimeFactory $timeFactory
+	 * @param IUserManager $userManager
+	 */
+	public function __construct(IRootFolder $rootFolder,
+								ILogger $logger,
+								IJobList $jobList,
+								ITimeFactory $timeFactory,
+								IUserManager $userManager) {
+		$this->rootFolder = $rootFolder;
+		$this->logger = $logger;
+		$this->jobList = $jobList;
+		$this->timeFactory = $timeFactory;
+		$this->userManager = $userManager;
+	}
+
+	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 $uid
+	 * @return bool
+	 */
+	private function cleanupPreviews($uid) {
+		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->timeFactory->getTime();
+		foreach ($thumbnails as $thumbnail) {
+			try {
+				$thumbnail->delete();
+			} catch (NotPermittedException $e) {
+				// Ignore
+			}
+
+			if (($this->timeFactory->getTime() - $start) > 15) {
+				return false;
+			}
+		}
+
+		try {
+			$thumbnailFolder->delete();
+		} catch (NotPermittedException $e) {
+			// Ignore
+		}
+
+		return true;
+	}
+}
diff --git a/lib/private/Repair/Owncloud/InstallCoreBundle.php b/lib/private/Repair/Owncloud/InstallCoreBundle.php
new file mode 100644
index 00000000000..6d07ec9b962
--- /dev/null
+++ b/lib/private/Repair/Owncloud/InstallCoreBundle.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @author Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace OC\Repair\Owncloud;
+
+use OC\App\AppStore\Bundles\BundleFetcher;
+use OC\Installer;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class InstallCoreBundle implements IRepairStep {
+	/** @var BundleFetcher */
+	private $bundleFetcher;
+	/** @var IConfig */
+	private $config;
+	/** @var Installer */
+	private $installer;
+
+	/**
+	 * @param BundleFetcher $bundleFetcher
+	 * @param IConfig $config
+	 * @param Installer $installer
+	 */
+	public function __construct(BundleFetcher $bundleFetcher,
+								IConfig $config,
+								Installer $installer) {
+		$this->bundleFetcher = $bundleFetcher;
+		$this->config = $config;
+		$this->installer = $installer;
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function getName() {
+		return 'Install new core bundle components';
+	}
+
+	/**
+	 * {@inheritdoc}
+	 */
+	public function run(IOutput $output) {
+		$versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0');
+
+		if (version_compare($versionFromBeforeUpdate, '12.0.0.14', '>')) {
+			return;
+		}
+
+		$defaultBundle = $this->bundleFetcher->getDefaultInstallationBundle();
+		foreach ($defaultBundle as $bundle) {
+			try {
+				$this->installer->installAppBundle($bundle);
+				$output->info('Successfully installed core app bundle.');
+			} catch (\Exception $e) {
+				$output->warning('Could not install core app bundle: ' . $e->getMessage());
+			}
+		}
+	}
+}
diff --git a/lib/private/Repair/Owncloud/MoveAvatars.php b/lib/private/Repair/Owncloud/MoveAvatars.php
new file mode 100644
index 00000000000..53f3097aeec
--- /dev/null
+++ b/lib/private/Repair/Owncloud/MoveAvatars.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+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->getSystemValue('enable_avatars', true) === false) {
+			$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..ecc2d6eb3da
--- /dev/null
+++ b/lib/private/Repair/Owncloud/MoveAvatarsBackgroundJob.php
@@ -0,0 +1,147 @@
+<?php
+/**
+ * @copyright 2016 Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Joas Schilling <coding@schilljs.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OC\Repair\Owncloud;
+
+use OC\BackgroundJob\QueuedJob;
+use OCP\Files\File;
+use OCP\Files\Folder;
+use OCP\Files\IAppData;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\SimpleFS\ISimpleFolder;
+use OCP\ILogger;
+use OCP\IUser;
+use OCP\IUserManager;
+
+class MoveAvatarsBackgroundJob extends QueuedJob {
+
+	/** @var IUserManager */
+	private $userManager;
+
+	/** @var IRootFolder */
+	private $rootFolder;
+
+	/** @var IAppData */
+	private $appData;
+
+	/** @var ILogger */
+	private $logger;
+
+	/**
+	 * MoveAvatars constructor.
+	 */
+	public function __construct() {
+		$this->userManager = \OC::$server->getUserManager();
+		$this->rootFolder = \OC::$server->getRootFolder();
+		$this->logger = \OC::$server->getLogger();
+		$this->appData = \OC::$server->getAppDataDir('avatar');
+	}
+
+	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() {
+		try {
+			$ownCloudAvatars = $this->rootFolder->get('avatars');
+		} catch (NotFoundException $e) {
+			$this->logger->info('No legacy avatars available, skipping migration');
+			return;
+		}
+
+		$counter = 0;
+		$this->userManager->callForSeenUsers(function (IUser $user) use ($counter, $ownCloudAvatars) {
+			$uid = $user->getUID();
+
+			\OC\Files\Filesystem::initMountPoints($uid);
+			/** @var Folder $userFolder */
+			$userFolder = $this->rootFolder->get($uid);
+
+			try {
+				$userData = $this->appData->getFolder($uid);
+			} catch (NotFoundException $e) {
+				$userData = $this->appData->newFolder($uid);
+			}
+
+			$foundAvatars = $this->copyAvatarsFromFolder($userFolder, $userData);
+
+			// ownCloud migration?
+			if ($foundAvatars === 0 && $ownCloudAvatars instanceof Folder) {
+				$parts = $this->buildOwnCloudAvatarPath($uid);
+				$userOwnCloudAvatar = $ownCloudAvatars;
+				foreach ($parts as $part) {
+					try {
+						$userOwnCloudAvatar = $userOwnCloudAvatar->get($part);
+					} catch (NotFoundException $e) {
+						return;
+					}
+				}
+
+				$this->copyAvatarsFromFolder($userOwnCloudAvatar, $userData);
+			}
+
+			$counter++;
+			if ($counter % 100 === 0) {
+				$this->logger->info('{amount} avatars migrated', ['amount' => $counter]);
+			}
+		});
+	}
+
+	/**
+	 * @param Folder $source
+	 * @param ISimpleFolder $target
+	 * @return int
+	 * @throws \OCP\Files\NotPermittedException
+	 * @throws NotFoundException
+	 */
+	protected function copyAvatarsFromFolder(Folder $source, ISimpleFolder $target) {
+		$foundAvatars = 0;
+		$avatars = $source->getDirectoryListing();
+		$regex = '/^avatar\.([0-9]+\.)?(jpg|png)$/';
+
+		foreach ($avatars as $avatar) {
+			/** @var File $avatar */
+			if (preg_match($regex, $avatar->getName())) {
+				/*
+				 * This is not the most effective but it is the most abstract way
+				 * to handle this. Avatars should be small anyways.
+				 */
+				$newAvatar = $target->newFile($avatar->getName());
+				$newAvatar->putContent($avatar->getContent());
+				$avatar->delete();
+				$foundAvatars++;
+			}
+		}
+
+		return $foundAvatars;
+	}
+
+	protected function buildOwnCloudAvatarPath($userId) {
+		$avatar = substr_replace(substr_replace(md5($userId), '/', 4, 0), '/', 2, 0);
+		return explode('/', $avatar);
+	}
+}
diff --git a/lib/private/Repair/Owncloud/UpdateLanguageCodes.php b/lib/private/Repair/Owncloud/UpdateLanguageCodes.php
new file mode 100644
index 00000000000..b7da0b00528
--- /dev/null
+++ b/lib/private/Repair/Owncloud/UpdateLanguageCodes.php
@@ -0,0 +1,90 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @author Joas Schilling <coding@schilljs.com>
+ * @author Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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->getSystemValue('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/tests/Test/Repair/Owncloud/CleanPreviewsBackgroundJobTest.php b/tests/Test/Repair/Owncloud/CleanPreviewsBackgroundJobTest.php
new file mode 100644
index 00000000000..267f01ca43f
--- /dev/null
+++ b/tests/Test/Repair/Owncloud/CleanPreviewsBackgroundJobTest.php
@@ -0,0 +1,245 @@
+<?php
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace Test\Repair\Owncloud;
+
+use OC\Repair\Owncloud\CleanPreviewsBackgroundJob;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\IJobList;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\ILogger;
+use OCP\IUserManager;
+use Test\TestCase;
+
+class CleanPreviewsBackgroundJobTest extends TestCase {
+	/** @var IRootFolder|\PHPUnit_Framework_MockObject_MockObject */
+	private $rootFolder;
+
+	/** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */
+	private $logger;
+
+	/** @var IJobList|\PHPUnit_Framework_MockObject_MockObject */
+	private $jobList;
+
+	/** @var ITimeFactory|\PHPUnit_Framework_MockObject_MockObject */
+	private $timeFactory;
+
+	/** @var CleanPreviewsBackgroundJob */
+	private $job;
+
+	/** @var  IUserManager|\PHPUnit_Framework_MockObject_MockObject */
+	private $userManager;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->rootFolder = $this->createMock(IRootFolder::class);
+		$this->logger = $this->createMock(ILogger::class);
+		$this->jobList = $this->createMock(IJobList::class);
+		$this->timeFactory = $this->createMock(ITimeFactory::class);
+		$this->userManager = $this->createMock(IUserManager::class);
+
+		$this->userManager->expects($this->any())->method('userExists')->willReturn(true);
+
+		$this->job = new CleanPreviewsBackgroundJob(
+			$this->rootFolder,
+			$this->logger,
+			$this->jobList,
+			$this->timeFactory,
+			$this->userManager
+		);
+	}
+
+	public function testCleanupPreviewsUnfinished() {
+		$userFolder = $this->createMock(Folder::class);
+		$userRoot = $this->createMock(Folder::class);
+		$thumbnailFolder = $this->createMock(Folder::class);
+
+		$this->rootFolder->method('getUserFolder')
+			->with($this->equalTo('myuid'))
+			->willReturn($userFolder);
+
+		$userFolder->method('getParent')->willReturn($userRoot);
+
+		$userRoot->method('get')
+			->with($this->equalTo('thumbnails'))
+			->willReturn($thumbnailFolder);
+
+		$previewFolder1 = $this->createMock(Folder::class);
+
+		$previewFolder1->expects($this->once())
+			->method('delete');
+
+		$thumbnailFolder->method('getDirectoryListing')
+			->willReturn([$previewFolder1]);
+		$thumbnailFolder->expects($this->never())
+			->method('delete');
+
+		$this->timeFactory->method('getTime')
+			->will($this->onConsecutiveCalls(100, 200));
+
+		$this->jobList->expects($this->once())
+			->method('add')
+			->with(
+				$this->equalTo(CleanPreviewsBackgroundJob::class),
+				$this->equalTo(['uid' => 'myuid'])
+			);
+
+		$this->logger->expects($this->at(0))
+			->method('info')
+			->with($this->equalTo('Started preview cleanup for myuid'));
+		$this->logger->expects($this->at(1))
+			->method('info')
+			->with($this->equalTo('New preview cleanup scheduled for myuid'));
+
+		$this->job->run(['uid' => 'myuid']);
+	}
+
+	public function testCleanupPreviewsFinished() {
+		$userFolder = $this->createMock(Folder::class);
+		$userRoot = $this->createMock(Folder::class);
+		$thumbnailFolder = $this->createMock(Folder::class);
+
+		$this->rootFolder->method('getUserFolder')
+			->with($this->equalTo('myuid'))
+			->willReturn($userFolder);
+
+		$userFolder->method('getParent')->willReturn($userRoot);
+
+		$userRoot->method('get')
+			->with($this->equalTo('thumbnails'))
+			->willReturn($thumbnailFolder);
+
+		$previewFolder1 = $this->createMock(Folder::class);
+
+		$previewFolder1->expects($this->once())
+			->method('delete');
+
+		$thumbnailFolder->method('getDirectoryListing')
+			->willReturn([$previewFolder1]);
+
+		$this->timeFactory->method('getTime')
+			->will($this->onConsecutiveCalls(100, 101));
+
+		$this->jobList->expects($this->never())
+			->method('add');
+
+		$this->logger->expects($this->at(0))
+			->method('info')
+			->with($this->equalTo('Started preview cleanup for myuid'));
+		$this->logger->expects($this->at(1))
+			->method('info')
+			->with($this->equalTo('Preview cleanup done for myuid'));
+
+		$thumbnailFolder->expects($this->once())
+			->method('delete');
+
+		$this->job->run(['uid' => 'myuid']);
+	}
+
+
+	public function testNoUserFolder() {
+		$this->rootFolder->method('getUserFolder')
+			->with($this->equalTo('myuid'))
+			->willThrowException(new NotFoundException());
+
+		$this->logger->expects($this->at(0))
+			->method('info')
+			->with($this->equalTo('Started preview cleanup for myuid'));
+		$this->logger->expects($this->at(1))
+			->method('info')
+			->with($this->equalTo('Preview cleanup done for myuid'));
+
+		$this->job->run(['uid' => 'myuid']);
+	}
+
+	public function testNoThumbnailFolder() {
+		$userFolder = $this->createMock(Folder::class);
+		$userRoot = $this->createMock(Folder::class);
+
+		$this->rootFolder->method('getUserFolder')
+			->with($this->equalTo('myuid'))
+			->willReturn($userFolder);
+
+		$userFolder->method('getParent')->willReturn($userRoot);
+
+		$userRoot->method('get')
+			->with($this->equalTo('thumbnails'))
+			->willThrowException(new NotFoundException());
+
+		$this->logger->expects($this->at(0))
+			->method('info')
+			->with($this->equalTo('Started preview cleanup for myuid'));
+		$this->logger->expects($this->at(1))
+			->method('info')
+			->with($this->equalTo('Preview cleanup done for myuid'));
+
+		$this->job->run(['uid' => 'myuid']);
+	}
+
+	public function testNotPermittedToDelete() {
+		$userFolder = $this->createMock(Folder::class);
+		$userRoot = $this->createMock(Folder::class);
+		$thumbnailFolder = $this->createMock(Folder::class);
+
+		$this->rootFolder->method('getUserFolder')
+			->with($this->equalTo('myuid'))
+			->willReturn($userFolder);
+
+		$userFolder->method('getParent')->willReturn($userRoot);
+
+		$userRoot->method('get')
+			->with($this->equalTo('thumbnails'))
+			->willReturn($thumbnailFolder);
+
+		$previewFolder1 = $this->createMock(Folder::class);
+
+		$previewFolder1->expects($this->once())
+			->method('delete')
+			->willThrowException(new NotPermittedException());
+
+		$thumbnailFolder->method('getDirectoryListing')
+			->willReturn([$previewFolder1]);
+
+		$this->timeFactory->method('getTime')
+			->will($this->onConsecutiveCalls(100, 101));
+
+		$this->jobList->expects($this->never())
+			->method('add');
+
+		$this->logger->expects($this->at(0))
+			->method('info')
+			->with($this->equalTo('Started preview cleanup for myuid'));
+		$this->logger->expects($this->at(1))
+			->method('info')
+			->with($this->equalTo('Preview cleanup done for myuid'));
+
+		$thumbnailFolder->expects($this->once())
+			->method('delete')
+			->willThrowException(new NotPermittedException());
+
+		$this->job->run(['uid' => 'myuid']);
+	}
+}
diff --git a/tests/Test/Repair/Owncloud/CleanPreviewsTest.php b/tests/Test/Repair/Owncloud/CleanPreviewsTest.php
new file mode 100644
index 00000000000..131b80517fb
--- /dev/null
+++ b/tests/Test/Repair/Owncloud/CleanPreviewsTest.php
@@ -0,0 +1,134 @@
+<?php
+/**
+ * @copyright 2016, Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @author Roeland Jago Douma <roeland@famdouma.nl>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace Test\Repair\Owncloud;
+
+use OC\Repair\Owncloud\CleanPreviews;
+use OC\Repair\Owncloud\CleanPreviewsBackgroundJob;
+use OCP\BackgroundJob\IJobList;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Migration\IOutput;
+use Test\TestCase;
+
+class CleanPreviewsTest extends TestCase {
+
+
+	/** @var IJobList|\PHPUnit_Framework_MockObject_MockObject */
+	private $jobList;
+
+	/** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */
+	private $userManager;
+
+	/** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
+	private $config;
+
+	/** @var CleanPreviews */
+	private $repair;
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->jobList = $this->createMock(IJobList::class);
+		$this->userManager = $this->createMock(IUserManager::class);
+		$this->config = $this->createMock(IConfig::class);
+
+		$this->repair = new CleanPreviews(
+			$this->jobList,
+			$this->userManager,
+			$this->config
+		);
+	}
+
+	public function testGetName() {
+		$this->assertSame('Add preview cleanup background jobs', $this->repair->getName());
+	}
+
+	public function testRun() {
+		$user1 = $this->createMock(IUser::class);
+		$user1->method('getUID')
+			->willReturn('user1');
+		$user2 = $this->createMock(IUser::class);
+		$user2->method('getUID')
+			->willReturn('user2');
+
+		$this->userManager->expects($this->once())
+			->method('callForSeenUsers')
+			->will($this->returnCallback(function (\Closure $function) use ($user1, $user2) {
+				$function($user1);
+				$function($user2);
+			}));
+
+		$this->jobList->expects($this->at(0))
+			->method('add')
+			->with(
+				$this->equalTo(CleanPreviewsBackgroundJob::class),
+				$this->equalTo(['uid' => 'user1'])
+			);
+
+		$this->jobList->expects($this->at(1))
+			->method('add')
+			->with(
+				$this->equalTo(CleanPreviewsBackgroundJob::class),
+				$this->equalTo(['uid' => 'user2'])
+			);
+
+		$this->config->expects($this->once())
+			->method('getAppValue')
+			->with(
+				$this->equalTo('core'),
+				$this->equalTo('previewsCleanedUp'),
+				$this->equalTo(false)
+			)->willReturn(false);
+		$this->config->expects($this->once())
+			->method('setAppValue')
+			->with(
+				$this->equalTo('core'),
+				$this->equalTo('previewsCleanedUp'),
+				$this->equalTo(1)
+			);
+
+		$this->repair->run($this->createMock(IOutput::class));
+	}
+
+
+	public function testRunAlreadyDoone() {
+		$this->userManager->expects($this->never())
+			->method($this->anything());
+
+		$this->jobList->expects($this->never())
+			->method($this->anything());
+
+		$this->config->expects($this->once())
+			->method('getAppValue')
+			->with(
+				$this->equalTo('core'),
+				$this->equalTo('previewsCleanedUp'),
+				$this->equalTo(false)
+			)->willReturn('1');
+		$this->config->expects($this->never())
+			->method('setAppValue');
+
+		$this->repair->run($this->createMock(IOutput::class));
+	}
+}
diff --git a/tests/Test/Repair/Owncloud/InstallCoreBundleTest.php b/tests/Test/Repair/Owncloud/InstallCoreBundleTest.php
new file mode 100644
index 00000000000..37163aee961
--- /dev/null
+++ b/tests/Test/Repair/Owncloud/InstallCoreBundleTest.php
@@ -0,0 +1,142 @@
+<?php
+/**
+ * @copyright Copyright (c) 2017 Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @author Lukas Reschke <lukas@statuscode.ch>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Test\Repair\Owncloud;
+
+use OC\App\AppStore\Bundles\Bundle;
+use OC\App\AppStore\Bundles\BundleFetcher;
+use OC\Installer;
+use OC\Repair\Owncloud\InstallCoreBundle;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use Test\TestCase;
+
+class InstallCoreBundleTest extends TestCase {
+	/** @var BundleFetcher|\PHPUnit_Framework_MockObject_MockObject */
+	private $bundleFetcher;
+	/** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */
+	private $config;
+	/** @var Installer|\PHPUnit_Framework_MockObject_MockObject */
+	private $installer;
+	/** @var InstallCoreBundle */
+	private $installCoreBundle;
+
+	public function setUp() {
+		parent::setUp();
+		$this->bundleFetcher = $this->createMock(BundleFetcher::class);
+		$this->config = $this->createMock(IConfig::class);
+		$this->installer = $this->createMock(Installer::class);
+
+		$this->installCoreBundle = new InstallCoreBundle(
+			$this->bundleFetcher,
+			$this->config,
+			$this->installer
+		);
+	}
+
+	public function testGetName() {
+		$this->assertSame('Install new core bundle components', $this->installCoreBundle->getName());
+	}
+
+	public function testRunOlder() {
+		$this->config
+			->expects($this->once())
+			->method('getSystemValue')
+			->with('version', '0.0.0')
+			->willReturn('12.0.0.15');
+		$this->bundleFetcher
+			->expects($this->never())
+			->method('getDefaultInstallationBundle');
+		/** @var IOutput|\PHPUnit_Framework_MockObject_MockObject $output */
+		$output = $this->createMock(IOutput::class);
+		$output
+			->expects($this->never())
+			->method('info');
+		$output
+			->expects($this->never())
+			->method('warning');
+
+		$this->installCoreBundle->run($output);
+	}
+
+	public function testRunWithException() {
+		$this->config
+			->expects($this->once())
+			->method('getSystemValue')
+			->with('version', '0.0.0')
+			->willReturn('12.0.0.14');
+		$bundle = $this->createMock(Bundle::class);
+		$this->bundleFetcher
+			->expects($this->once())
+			->method('getDefaultInstallationBundle')
+			->willReturn([
+				$bundle,
+			]);
+		$this->installer
+			->expects($this->once())
+			->method('installAppBundle')
+			->with($bundle)
+			->willThrowException(new \Exception('ExceptionText'));
+		/** @var IOutput|\PHPUnit_Framework_MockObject_MockObject $output */
+		$output = $this->createMock(IOutput::class);
+		$output
+			->expects($this->never())
+			->method('info');
+		$output
+			->expects($this->once())
+			->method('warning')
+			->with('Could not install core app bundle: ExceptionText');
+
+		$this->installCoreBundle->run($output);
+	}
+
+	public function testRun() {
+		$this->config
+			->expects($this->once())
+			->method('getSystemValue')
+			->with('version', '0.0.0')
+			->willReturn('12.0.0.14');
+		$bundle = $this->createMock(Bundle::class);
+		$this->bundleFetcher
+			->expects($this->once())
+			->method('getDefaultInstallationBundle')
+			->willReturn([
+				$bundle,
+			]);
+		$this->installer
+			->expects($this->once())
+			->method('installAppBundle')
+			->with($bundle);
+		/** @var IOutput|\PHPUnit_Framework_MockObject_MockObject $output */
+		$output = $this->createMock(IOutput::class);
+		$output
+			->expects($this->once())
+			->method('info')
+			->with('Successfully installed core app bundle.');
+		$output
+			->expects($this->never())
+			->method('warning');
+
+		$this->installCoreBundle->run($output);
+	}
+}
diff --git a/tests/Test/Repair/Owncloud/UpdateLanguageCodesTest.php b/tests/Test/Repair/Owncloud/UpdateLanguageCodesTest.php
new file mode 100644
index 00000000000..171b7701194
--- /dev/null
+++ b/tests/Test/Repair/Owncloud/UpdateLanguageCodesTest.php
@@ -0,0 +1,175 @@
+<?php
+/**
+ * @copyright Copyright (c) 2016 Morris Jobke <hey@morrisjobke.de>
+ *
+ * @author Morris Jobke <hey@morrisjobke.de>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+namespace Test\Repair\Owncloud;
+
+use OC\Repair\Owncloud\UpdateLanguageCodes;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\Migration\IOutput;
+use Test\TestCase;
+
+/**
+ * Class UpdateLanguageCodesTest
+ *
+ * @group DB
+ *
+ * @package Test\Repair
+ */
+class UpdateLanguageCodesTest extends TestCase {
+	/** @var \OCP\IDBConnection */
+	protected $connection;
+
+	/** @var IConfig | \PHPUnit_Framework_MockObject_MockObject */
+	private $config;
+
+	protected function setUp() {
+		parent::setUp();
+
+		$this->connection = \OC::$server->getDatabaseConnection();
+		$this->config = $this->createMock(IConfig::class);
+	}
+
+	public function testRun() {
+		$users = [
+			['userid' => 'user1', 'configvalue' => 'fi_FI'],
+			['userid' => 'user2', 'configvalue' => 'de'],
+			['userid' => 'user3', 'configvalue' => 'fi'],
+			['userid' => 'user4', 'configvalue' => 'ja'],
+			['userid' => 'user5', 'configvalue' => 'bg_BG'],
+			['userid' => 'user6', 'configvalue' => 'ja'],
+			['userid' => 'user7', 'configvalue' => 'th_TH'],
+			['userid' => 'user8', 'configvalue' => 'th_TH'],
+		];
+
+		// insert test data
+		$qb = $this->connection->getQueryBuilder();
+		$qb->insert('preferences')
+				->values([
+					'userid' => $qb->createParameter('userid'),
+					'appid' => $qb->createParameter('appid'),
+					'configkey' => $qb->createParameter('configkey'),
+					'configvalue' => $qb->createParameter('configvalue'),
+				]);
+		foreach ($users as $user) {
+			$qb->setParameters([
+				'userid' => $user['userid'],
+				'appid' => 'core',
+				'configkey' => 'lang',
+				'configvalue' => $user['configvalue'],
+			])->execute();
+		}
+
+		// check if test data is written to DB
+		$qb = $this->connection->getQueryBuilder();
+		$result = $qb->select(['userid', 'configvalue'])
+			->from('preferences')
+			->where($qb->expr()->eq('appid', $qb->createNamedParameter('core')))
+			->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lang')))
+			->execute();
+
+		$rows = $result->fetchAll();
+		$result->closeCursor();
+
+		$this->assertSame($users, $rows, 'Asserts that the entries are the ones from the test data set');
+
+		/** @var IOutput|\PHPUnit_Framework_MockObject_MockObject $outputMock */
+		$outputMock = $this->createMock(IOutput::class);
+		$outputMock->expects($this->at(0))
+			->method('info')
+			->with('Changed 1 setting(s) from "bg_BG" to "bg" in preferences table.');
+		$outputMock->expects($this->at(1))
+			->method('info')
+			->with('Changed 0 setting(s) from "cs_CZ" to "cs" in preferences table.');
+		$outputMock->expects($this->at(2))
+			->method('info')
+			->with('Changed 1 setting(s) from "fi_FI" to "fi" in preferences table.');
+		$outputMock->expects($this->at(3))
+			->method('info')
+			->with('Changed 0 setting(s) from "hu_HU" to "hu" in preferences table.');
+		$outputMock->expects($this->at(4))
+			->method('info')
+			->with('Changed 0 setting(s) from "nb_NO" to "nb" in preferences table.');
+		$outputMock->expects($this->at(5))
+			->method('info')
+			->with('Changed 0 setting(s) from "sk_SK" to "sk" in preferences table.');
+		$outputMock->expects($this->at(6))
+			->method('info')
+			->with('Changed 2 setting(s) from "th_TH" to "th" in preferences table.');
+
+		$this->config->expects($this->once())
+			->method('getSystemValue')
+			->with('version', '0.0.0')
+			->willReturn('12.0.0.13');
+
+		// run repair step
+		$repair = new UpdateLanguageCodes($this->connection, $this->config);
+		$repair->run($outputMock);
+
+		// check if test data is correctly modified in DB
+		$qb = $this->connection->getQueryBuilder();
+		$result = $qb->select(['userid', 'configvalue'])
+			->from('preferences')
+			->where($qb->expr()->eq('appid', $qb->createNamedParameter('core')))
+			->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lang')))
+			->orderBy('userid')
+			->execute();
+
+		$rows = $result->fetchAll();
+		$result->closeCursor();
+
+		// value has changed for one user
+		$users[0]['configvalue'] = 'fi';
+		$users[4]['configvalue'] = 'bg';
+		$users[6]['configvalue'] = 'th';
+		$users[7]['configvalue'] = 'th';
+		$this->assertSame($users, $rows, 'Asserts that the entries are updated correctly.');
+
+		// remove test data
+		foreach ($users as $user) {
+			$qb = $this->connection->getQueryBuilder();
+			$qb->delete('preferences')
+				->where($qb->expr()->eq('userid', $qb->createNamedParameter($user['userid'])))
+				->andWhere($qb->expr()->eq('appid', $qb->createNamedParameter('core')))
+				->andWhere($qb->expr()->eq('configkey', $qb->createNamedParameter('lang')))
+				->andWhere($qb->expr()->eq('configvalue', $qb->createNamedParameter($user['configvalue']), IQueryBuilder::PARAM_STR))
+				->execute();
+		}
+	}
+
+	public function testSecondRun() {
+		/** @var IOutput|\PHPUnit_Framework_MockObject_MockObject $outputMock */
+		$outputMock = $this->createMock(IOutput::class);
+		$outputMock->expects($this->never())
+			->method('info');
+
+		$this->config->expects($this->once())
+			->method('getSystemValue')
+			->with('version', '0.0.0')
+			->willReturn('12.0.0.14');
+
+		// run repair step
+		$repair = new UpdateLanguageCodes($this->connection, $this->config);
+		$repair->run($outputMock);
+	}
+}
-- 
GitLab