From e5b59531a309bafd215da719afc23d5dca538e4d Mon Sep 17 00:00:00 2001
From: Arthur Schiwon <blizzz@arthur-schiwon.de>
Date: Fri, 29 Jan 2021 17:00:18 +0100
Subject: [PATCH] add repair job for unencoded calendars

Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
---
 lib/composer/composer/autoload_classmap.php |   1 +
 lib/composer/composer/autoload_static.php   |   1 +
 lib/private/Repair.php                      |   4 +-
 lib/private/Repair/RepairDavShares.php      | 132 +++++++++++++
 tests/lib/Repair/RepairDavSharesTest.php    | 197 ++++++++++++++++++++
 5 files changed, 334 insertions(+), 1 deletion(-)
 create mode 100644 lib/private/Repair/RepairDavShares.php
 create mode 100644 tests/lib/Repair/RepairDavSharesTest.php

diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index f51274171f0..49b138714a7 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -1305,6 +1305,7 @@ return array(
     'OC\\Repair\\Owncloud\\SaveAccountsTableData' => $baseDir . '/lib/private/Repair/Owncloud/SaveAccountsTableData.php',
     'OC\\Repair\\Owncloud\\UpdateLanguageCodes' => $baseDir . '/lib/private/Repair/Owncloud/UpdateLanguageCodes.php',
     'OC\\Repair\\RemoveLinkShares' => $baseDir . '/lib/private/Repair/RemoveLinkShares.php',
+    'OC\\Repair\\RepairDavShares' => $baseDir . '/lib/private/Repair/RepairDavShares.php',
     'OC\\Repair\\RepairInvalidShares' => $baseDir . '/lib/private/Repair/RepairInvalidShares.php',
     'OC\\Repair\\RepairMimeTypes' => $baseDir . '/lib/private/Repair/RepairMimeTypes.php',
     'OC\\Repair\\SqliteAutoincrement' => $baseDir . '/lib/private/Repair/SqliteAutoincrement.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index 3f1983a11c0..373db9144bd 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -1334,6 +1334,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OC\\Repair\\Owncloud\\SaveAccountsTableData' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/SaveAccountsTableData.php',
         'OC\\Repair\\Owncloud\\UpdateLanguageCodes' => __DIR__ . '/../../..' . '/lib/private/Repair/Owncloud/UpdateLanguageCodes.php',
         'OC\\Repair\\RemoveLinkShares' => __DIR__ . '/../../..' . '/lib/private/Repair/RemoveLinkShares.php',
+        'OC\\Repair\\RepairDavShares' => __DIR__ . '/../../..' . '/lib/private/Repair/RepairDavShares.php',
         'OC\\Repair\\RepairInvalidShares' => __DIR__ . '/../../..' . '/lib/private/Repair/RepairInvalidShares.php',
         'OC\\Repair\\RepairMimeTypes' => __DIR__ . '/../../..' . '/lib/private/Repair/RepairMimeTypes.php',
         'OC\\Repair\\SqliteAutoincrement' => __DIR__ . '/../../..' . '/lib/private/Repair/SqliteAutoincrement.php',
diff --git a/lib/private/Repair.php b/lib/private/Repair.php
index e4f75b43fdc..e7c6f729a8f 100644
--- a/lib/private/Repair.php
+++ b/lib/private/Repair.php
@@ -65,6 +65,7 @@ use OC\Repair\OldGroupMembershipShares;
 use OC\Repair\Owncloud\DropAccountTermsTable;
 use OC\Repair\Owncloud\SaveAccountsTableData;
 use OC\Repair\RemoveLinkShares;
+use OC\Repair\RepairDavShares;
 use OC\Repair\RepairInvalidShares;
 use OC\Repair\RepairMimeTypes;
 use OC\Repair\SqliteAutoincrement;
@@ -189,6 +190,7 @@ class Repair implements IOutput {
 			\OC::$server->get(ShippedDashboardEnable::class),
 			\OC::$server->get(AddBruteForceCleanupJob::class),
 			\OC::$server->get(AddCheckForUserCertificatesJob::class),
+			\OC::$server->get(RepairDavShares::class)
 		];
 	}
 
@@ -221,7 +223,7 @@ class Repair implements IOutput {
 			new Collation(\OC::$server->getConfig(), \OC::$server->getLogger(), $connectionAdapter, true),
 			new SqliteAutoincrement($connection),
 			new SaveAccountsTableData($connectionAdapter, $config),
-			new DropAccountTermsTable($connectionAdapter)
+			new DropAccountTermsTable($connectionAdapter),
 		];
 
 		return $steps;
diff --git a/lib/private/Repair/RepairDavShares.php b/lib/private/Repair/RepairDavShares.php
new file mode 100644
index 00000000000..ff4c51484eb
--- /dev/null
+++ b/lib/private/Repair/RepairDavShares.php
@@ -0,0 +1,132 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de>
+ *
+ * @author Arthur Schiwon <blizzz@arthur-schiwon.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;
+
+use OCP\DB\Exception;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Psr\Log\LoggerInterface;
+use function strlen;
+use function substr;
+use function urldecode;
+use function urlencode;
+
+class RepairDavShares implements IRepairStep {
+	protected const GROUP_PRINCIPAL_PREFIX = 'principals/groups/';
+
+	/** @var IConfig */
+	private $config;
+	/** @var IDBConnection */
+	private $dbc;
+	/** @var IGroupManager */
+	private $groupManager;
+	/** @var LoggerInterface */
+	private $logger;
+
+	public function __construct(
+		IConfig $config,
+		IDBConnection $dbc,
+		IGroupManager $groupManager,
+		LoggerInterface $logger
+	) {
+		$this->config = $config;
+		$this->dbc = $dbc;
+		$this->groupManager = $groupManager;
+		$this->logger = $logger;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function getName() {
+		return 'Repair DAV shares';
+	}
+
+	protected function repairUnencodedGroupShares() {
+		$qb = $this->dbc->getQueryBuilder();
+		$qb->select(['id', 'principaluri'])
+			->from('dav_shares')
+			->where($qb->expr()->like('principaluri', $qb->createNamedParameter(self::GROUP_PRINCIPAL_PREFIX . '%')));
+
+		$updateQuery = $this->dbc->getQueryBuilder();
+		$updateQuery->update('dav_shares')
+			->set('principaluri', $updateQuery->createParameter('updatedPrincipalUri'))
+			->where($updateQuery->expr()->eq('id', $updateQuery->createParameter('shareId')));
+
+		$statement = $qb->execute();
+		while ($share = $statement->fetch()) {
+			$gid = substr($share['principaluri'], strlen(self::GROUP_PRINCIPAL_PREFIX));
+			$decodedGid = urldecode($gid);
+			$encodedGid = urlencode($gid);
+			if ($gid === $encodedGid
+				|| !$this->groupManager->groupExists($gid)
+				|| ($gid !== $decodedGid && $this->groupManager->groupExists($decodedGid))
+			) {
+				continue;
+			}
+
+			// Repair when
+			// + the group name needs encoding
+			// + AND it is not encoded yet
+			// + AND there are no ambivalent groups
+
+			try {
+				$fixedPrincipal = self::GROUP_PRINCIPAL_PREFIX . $encodedGid;
+				$logParameters = [
+					'app' => 'core',
+					'id' => $share['id'],
+					'old' => $share['principaluri'],
+					'new' => $fixedPrincipal,
+				];
+				$updateQuery
+					->setParameter('updatedPrincipalUri', $fixedPrincipal)
+					->setParameter('shareId', $share['id'])
+					->execute();
+				$this->logger->info('Repaired principal for dav share {id} from {old} to {new}', $logParameters);
+			} catch (Exception $e) {
+				$logParameters['message'] = $e->getMessage();
+				$logParameters['exception'] = $e;
+				$this->logger->info('Could not repair principal for dav share {id} from {old} to {new}: {message}', $logParameters);
+			}
+		}
+		return true;
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	public function run(IOutput $output) {
+		$versionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0');
+		if (version_compare($versionFromBeforeUpdate, '20.0.7', '<')
+			&& $this->repairUnencodedGroupShares()
+		) {
+			$output->info('Repaired DAV group shares');
+		}
+	}
+}
diff --git a/tests/lib/Repair/RepairDavSharesTest.php b/tests/lib/Repair/RepairDavSharesTest.php
new file mode 100644
index 00000000000..32f09f55538
--- /dev/null
+++ b/tests/lib/Repair/RepairDavSharesTest.php
@@ -0,0 +1,197 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2021 Arthur Schiwon <blizzz@arthur-schiwon.de>
+ *
+ * @author Arthur Schiwon <blizzz@arthur-schiwon.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;
+
+use OC\Repair\RepairDavShares;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IExpressionBuilder;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IGroupManager;
+use Psr\Log\LoggerInterface;
+use Test\TestCase;
+use OCP\Migration\IOutput;
+use function in_array;
+
+class RepairDavSharesTest extends TestCase {
+
+	/** @var IOutput|\PHPUnit\Framework\MockObject\MockObject */
+	protected $output;
+	/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
+	protected $config;
+	/** @var IDBConnection|\PHPUnit\Framework\MockObject\MockObject */
+	protected $dbc;
+	/** @var IGroupManager|\PHPUnit\Framework\MockObject\MockObject */
+	protected $groupManager;
+	/** @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface */
+	protected $logger;
+	/** @var RepairDavSharesTest */
+	protected $repair;
+
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->output = $this->createMock(IOutput::class);
+
+		$this->config = $this->createMock(IConfig::class);
+		$this->dbc = $this->createMock(IDBConnection::class);
+		$this->groupManager = $this->createMock(IGroupManager::class);
+		$this->logger = $this->createMock(LoggerInterface::class);
+
+		$this->repair = new RepairDavShares(
+			$this->config,
+			$this->dbc,
+			$this->groupManager,
+			$this->logger
+		);
+	}
+
+	public function testRun() {
+		$this->config->expects($this->any())
+			->method('getSystemValue')
+			->with('version', '0.0.0')
+			->willReturn('20.0.2');
+
+		$this->output->expects($this->once())
+			->method('info')
+			->with('Repaired DAV group shares');
+
+		$existingGroups = [
+			'Innocent',
+			'Wants Repair',
+			'Well förmed',
+			'family+friends',
+			'family friends',
+		];
+
+		$shareResultData = [
+			[
+				// No update, nothing to escape
+				'id' => 0,
+				'principaluri' => 'principals/groups/Innocent',
+			],
+			[
+				// Update
+				'id' => 1,
+				'principaluri' => 'principals/groups/Wants Repair',
+			],
+			[
+				// No update, already proper
+				'id' => 2,
+				'principaluri' => 'principals/groups/Well+f%C3%B6rmed',
+			],
+			[
+				// No update, unknown group
+				'id' => 3,
+				'principaluri' => 'principals/groups/Not known',
+			],
+			[
+				// No update, unknown group
+				'id' => 4,
+				'principaluri' => 'principals/groups/Also%2F%2FNot%23Known',
+			],
+			[
+				// No update, group exists in both forms
+				'id' => 5,
+				'principaluri' => 'principals/groups/family+friends',
+			],
+			[
+				// No update, already proper
+				'id' => 6,
+				'principaluri' => 'principals/groups/family%2Bfriends',
+			],
+			[
+				// Update
+				'id' => 7,
+				'principaluri' => 'principals/groups/family friends',
+			],
+		];
+
+		$shareResults = $this->createMock(IResult::class);
+		$shareResults->expects($this->any())
+			->method('fetch')
+			->willReturnCallback(function () use (&$shareResultData) {
+				return array_pop($shareResultData);
+			});
+
+		$expressionBuilder = $this->createMock(IExpressionBuilder::class);
+
+		$selectMock = $this->createMock(IQueryBuilder::class);
+		$selectMock->expects($this->any())
+			->method('expr')
+			->willReturn($expressionBuilder);
+		$selectMock->expects($this->once())
+			->method('select')
+			->willReturnSelf();
+		$selectMock->expects($this->once())
+			->method('from')
+			->willReturnSelf();
+		$selectMock->expects($this->once())
+			->method('where')
+			->willReturnSelf();
+		$selectMock->expects($this->once())
+			->method('execute')
+			->willReturn($shareResults);
+
+		$updateMock = $this->createMock(IQueryBuilder::class);
+		$updateMock->expects($this->any())
+			->method('expr')
+			->willReturn($expressionBuilder);
+		$updateMock->expects($this->once())
+			->method('update')
+			->willReturnSelf();
+		$updateMock->expects($this->any())
+			->method('set')
+			->willReturnSelf();
+		$updateMock->expects($this->once())
+			->method('where')
+			->willReturnSelf();
+		$updateMock->expects($this->exactly(4))
+			->method('setParameter')
+			->withConsecutive(
+				['updatedPrincipalUri', 'principals/groups/' . urlencode('family friends')],
+				['shareId', 7],
+				['updatedPrincipalUri', 'principals/groups/' . urlencode('Wants Repair')],
+				['shareId', 1],
+			)
+			->willReturnSelf();
+		$updateMock->expects($this->exactly(2))
+			->method('execute');
+
+		$this->dbc->expects($this->atLeast(2))
+			->method('getQueryBuilder')
+			->willReturnOnConsecutiveCalls($selectMock, $updateMock);
+
+		$this->groupManager->expects($this->any())
+			->method('groupExists')
+			->willReturnCallback(function (string $gid) use ($existingGroups) {
+				return in_array($gid, $existingGroups);
+			});
+
+		$this->repair->run($this->output);
+	}
+}
-- 
GitLab