diff --git a/lib/private/Repair.php b/lib/private/Repair.php
index 0e876d61f077..b79f42713df3 100644
--- a/lib/private/Repair.php
+++ b/lib/private/Repair.php
@@ -37,6 +37,7 @@
use OC\Repair\OldGroupMembershipShares;
use OC\Repair\RemoveGetETagEntries;
use OC\Repair\RemoveRootShares;
+use OC\Repair\RepairSubShares;
use OC\Repair\SharePropagation;
use OC\Repair\SqliteAutoincrement;
use OC\Repair\DropOldTables;
@@ -157,6 +158,9 @@ public static function getRepairSteps() {
\OC::$server->getConfig(),
\OC::$server->getAppConfig()
),
+ new RepairSubShares(
+ \OC::$server->getDatabaseConnection()
+ ),
];
}
diff --git a/lib/private/Repair/RepairSubShares.php b/lib/private/Repair/RepairSubShares.php
new file mode 100644
index 000000000000..8d77290f83ff
--- /dev/null
+++ b/lib/private/Repair/RepairSubShares.php
@@ -0,0 +1,105 @@
+
+ *
+ * @copyright Copyright (c) 2018, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace OC\Repair;
+
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+
+class RepairSubShares implements IRepairStep {
+
+ /** @var IDBConnection */
+ private $connection;
+
+ /** @var IQueryBuilder */
+ private $getDuplicateRows;
+
+ /** @var IQueryBuilder */
+ private $deleteShareId;
+
+ public function __construct(
+ IDBConnection $connection) {
+ $this->connection = $connection;
+ }
+
+ public function getName() {
+ return "Repair sub shares";
+ }
+
+ /**
+ * Set query to remove duplicate rows.
+ * i.e, except id all columns are same for oc_share
+ * Also set query to select rows which have duplicate rows of share.
+ */
+ private function setRemoveAndSelectQuery() {
+ /**
+ * Retrieves the duplicate rows with different id's
+ * of oc_share
+ */
+ $builder = $this->connection->getQueryBuilder();
+ $builder
+ ->select('id', 'parent', $builder->createFunction('count(*)'))
+ ->from('share')
+ ->where($builder->expr()->eq('share_type', $builder->createNamedParameter(2)))
+ ->groupBy('parent')
+ ->addGroupBy('id')
+ ->addGroupBy('share_with')
+ ->having('count(*) > 1')->setMaxResults(1000);
+
+ $this->getDuplicateRows = $builder;
+
+ $builder = $this->connection->getQueryBuilder();
+ $builder
+ ->delete('share')
+ ->where($builder->expr()->eq('id', $builder->createParameter('shareId')));
+
+ $this->deleteShareId = $builder;
+ }
+
+ public function run(IOutput $output) {
+ $deletedEntries = 0;
+ $this->setRemoveAndSelectQuery();
+
+ /**
+ * Going for pagination because if there are 1 million rows
+ * it wont be easy to scale the data
+ */
+ do {
+ $results = $this->getDuplicateRows->execute();
+ $rows = $results->fetchAll();
+ $results->closeCursor();
+ $lastResultCount = 0;
+
+ foreach ($rows as $row) {
+ $deletedEntries += $this->deleteShareId->setParameter('shareId', (int) $row['id'])
+ ->execute();
+ $lastResultCount++;
+ }
+ } while($lastResultCount > 0);
+
+ if ($deletedEntries > 0) {
+ $output->info('Removed ' . $deletedEntries . ' shares where duplicate rows where found');
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/lib/Repair/RepairSubSharesTest.php b/tests/lib/Repair/RepairSubSharesTest.php
new file mode 100644
index 000000000000..222589fc0a82
--- /dev/null
+++ b/tests/lib/Repair/RepairSubSharesTest.php
@@ -0,0 +1,200 @@
+
+ *
+ * @copyright Copyright (c) 2018, ownCloud GmbH
+ * @license AGPL-3.0
+ *
+ * This code is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * 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, version 3,
+ * along with this program. If not, see
+ *
+ */
+
+namespace Test\Repair;
+
+use OC\Repair\RepairSubShares;
+use OCP\Migration\IOutput;
+use OCP\Migration\IRepairStep;
+use Test\TestCase;
+use Test\Traits\UserTrait;
+
+/**
+ * Test for repairing invalid sub shares
+ *
+ * @group DB
+ *
+ * @see \OC\Repair\RepairSubShares
+ * @package Test\Repair
+ */
+class RepairSubSharesTest extends TestCase {
+ use UserTrait;
+
+ /** @var \OCP\IDBConnection */
+ private $connection;
+
+ /** @var IRepairStep */
+ private $repair;
+ protected function setUp() {
+ parent::setUp();
+
+ $this->connection = \OC::$server->getDatabaseConnection();
+ $this->repair = new RepairSubShares($this->connection);
+ $this->createUser('admin');
+ }
+
+ protected function tearDown() {
+ $this->deleteAllUsersAndGroups();
+ $this->deleteAllShares();
+ parent::tearDown();
+ }
+
+ public function deleteAllUsersAndGroups() {
+ $this->tearDownUserTrait();
+ $qb = $this->connection->getQueryBuilder();
+ $qb->delete('groups')->execute();
+ $qb->delete('group_user')->execute();
+ }
+
+ public function deleteAllShares() {
+ $qb = $this->connection->getQueryBuilder();
+ $qb->delete('share')->execute();
+ }
+
+ /**
+ * This is a very basic test
+ * This test would populate DB with data
+ * and later, remove the duplicates to test
+ * if the step is working properly
+ */
+ public function testPopulateDBAndRemoveDuplicates() {
+
+ $qb = $this->connection->getQueryBuilder();
+ //Create 10 users and 3 groups.
+ //add 3 users to each group
+ $userName = "user";
+ $groupName = "group";
+ $folderName = "/test";
+ $time = time();
+ $groupCount = 1;
+ $totalGroups = 3;
+ $parent = 1;
+ $multipleOf = 2;
+ for($userCount = 1; $userCount <= 10; $userCount++) {
+ $user = $this->createUser($userName.$userCount);
+ if (\OC::$server->getGroupManager()->groupExists($groupName.$groupCount) === false) {
+ \OC::$server->getGroupManager()->createGroup($groupName.$groupCount);
+ }
+ \OC::$server->getGroupManager()->get($groupName.$groupCount)->addUser($user);
+
+ //Create a group share
+ $qb->insert('share')
+ ->values([
+ 'share_type' => $qb->expr()->literal('2'),
+ 'share_with' => $qb->expr()->literal($userName.$groupCount),
+ 'uid_owner' => $qb->expr()->literal('admin'),
+ 'uid_initiator' => $qb->expr()->literal('admin'),
+ 'parent' => $qb->expr()->literal($parent),
+ 'item_type' => $qb->expr()->literal('folder'),
+ 'item_source' => $qb->expr()->literal(24),
+ 'file_source' => $qb->expr()->literal(24),
+ 'file_target' => $qb->expr()->literal($folderName.$groupCount),
+ 'permissions' => $qb->expr()->literal(31),
+ 'stime' => $qb->expr()->literal($time),
+ ])
+ ->execute();
+
+ /**
+ * Group count incremented once value of userCount reaches multiple of 3
+ */
+ if (($userCount % $totalGroups) === 0) {
+ $groupCount++;
+ $time = time();
+ }
+
+ /**
+ * Increment parent
+ */
+ if (($userCount % $multipleOf) === 0) {
+ $parent++;
+ }
+ }
+
+ $outputMock = $this->createMock(IOutput::class);
+ $this->repair->run($outputMock);
+
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('id', 'parent', $qb->createFunction('count(*)'))
+ ->from('share')
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(2)))
+ ->groupBy('parent')
+ ->addGroupBy('id')
+ ->addGroupBy('share_with')
+ ->having('count(*) > 1')->setMaxResults(1000);
+
+ $results = $qb->execute()->fetchAll();
+ $this->assertCount(0, $results);
+ }
+
+ /**
+ * This is to test large rows i.e, greater than 2000
+ * with duplicates
+ */
+ public function testLargeDuplicateShareRows() {
+ $qb = $this->connection->getQueryBuilder();
+ $userName = "user";
+ $time = time();
+ $groupCount = 0;
+ $folderName = "/test";
+ $maxUsersPerGroup = 1000;
+ $parent = $groupCount + 1;
+ for ($userCount = 0; $userCount < 5500; $userCount++) {
+ /**
+ * groupCount is incremented once userCount reaches
+ * multiple of maxUsersPerGroup.
+ */
+ if (($userCount % $maxUsersPerGroup) === 0) {
+ $groupCount++;
+ $parent = $groupCount;
+ }
+ $qb->insert('share')
+ ->values([
+ 'share_type' => $qb->expr()->literal('2'),
+ 'share_with' => $qb->expr()->literal($userName.$groupCount),
+ 'uid_owner' => $qb->expr()->literal('admin'),
+ 'uid_initiator' => $qb->expr()->literal('admin'),
+ 'parent' => $qb->expr()->literal($parent),
+ 'item_type' => $qb->expr()->literal('folder'),
+ 'item_source' => $qb->expr()->literal(24),
+ 'file_source' => $qb->expr()->literal(24),
+ 'file_target' => $qb->expr()->literal($folderName.$groupCount),
+ 'permissions' => $qb->expr()->literal(31),
+ 'stime' => $qb->expr()->literal($time),
+ ])
+ ->execute();
+ }
+
+ $outputMock = $this->createMock(IOutput::class);
+ $this->repair->run($outputMock);
+
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('id', 'parent', $qb->createFunction('count(*)'))
+ ->from('share')
+ ->where($qb->expr()->eq('share_type', $qb->createNamedParameter(2)))
+ ->groupBy('parent')
+ ->addGroupBy('id')
+ ->addGroupBy('share_with')
+ ->having('count(*) > 1')->setMaxResults(1000);
+
+ $results = $qb->execute()->fetchAll();
+ $this->assertCount(0, $results);
+ }
+}