Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorVincent Petry <pvince81@owncloud.com>2016-07-20 17:48:54 +0300
committerThomas Müller <thomas.mueller@tmit.eu>2016-08-13 18:10:13 +0300
commitfd0f40383f4334ad5f9fb2ebc8893a7fdd653708 (patch)
tree513f265a21974101cf88ecadce13a077ae38faba /lib
parentc3e7cf5030f96fb733eeb3bac985a49d55f6995a (diff)
Add repair step for unmerged shares
Diffstat (limited to 'lib')
-rw-r--r--lib/private/repair.php7
-rw-r--r--lib/private/repair/repairunmergedshares.php326
2 files changed, 333 insertions, 0 deletions
diff --git a/lib/private/repair.php b/lib/private/repair.php
index 0cbb43293e8..0fa6f15a8aa 100644
--- a/lib/private/repair.php
+++ b/lib/private/repair.php
@@ -49,6 +49,7 @@ use OC\Repair\RepairMimeTypes;
use OC\Repair\SearchLuceneTables;
use OC\Repair\UpdateOutdatedOcsIds;
use OC\Repair\RepairInvalidShares;
+use OC\Repair\RepairUnmergedShares;
class Repair extends BasicEmitter {
/**
@@ -118,6 +119,12 @@ class Repair extends BasicEmitter {
new RemoveGetETagEntries(\OC::$server->getDatabaseConnection()),
new UpdateOutdatedOcsIds(\OC::$server->getConfig()),
new RepairInvalidShares(\OC::$server->getConfig(), \OC::$server->getDatabaseConnection()),
+ new RepairUnmergedShares(
+ \OC::$server->getConfig(),
+ \OC::$server->getDatabaseConnection(),
+ \OC::$server->getUserManager(),
+ \OC::$server->getGroupManager()
+ ),
new AvatarPermissions(\OC::$server->getDatabaseConnection()),
new BrokenUpdaterRepair(),
];
diff --git a/lib/private/repair/repairunmergedshares.php b/lib/private/repair/repairunmergedshares.php
new file mode 100644
index 00000000000..379e62ae65b
--- /dev/null
+++ b/lib/private/repair/repairunmergedshares.php
@@ -0,0 +1,326 @@
+<?php
+/**
+ * @author Vincent Petry <pvince81@owncloud.com>
+ *
+ * @copyright Copyright (c) 2016, ownCloud, Inc.
+ * @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 <http://www.gnu.org/licenses/>
+ *
+ */
+
+namespace OC\Repair;
+
+use OCP\Migration\IOutput;
+use OC\Hooks\BasicEmitter;
+use OC\Share\Constants;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IConfig;
+use OCP\IDBConnection;
+use OCP\IUserManager;
+use OCP\IUser;
+use OCP\IGroupManager;
+use OC\Share20\DefaultShareProvider;
+
+/**
+ * Repairs shares for which the received folder was not properly deduplicated.
+ *
+ * An unmerged share can for example happen when sharing a folder with the same
+ * user through multiple ways, like several groups and also directly, additionally
+ * to group shares. Since 9.0.0 these would create duplicate entries "folder (2)",
+ * one for every share. This repair step rearranges them so they only appear as a single
+ * folder.
+ */
+class RepairUnmergedShares extends BasicEmitter implements \OC\RepairStep {
+
+ /** @var \OCP\IConfig */
+ protected $config;
+
+ /** @var \OCP\IDBConnection */
+ protected $connection;
+
+ /** @var IUserManager */
+ protected $userManager;
+
+ /** @var IGroupManager */
+ protected $groupManager;
+
+ /** @var IQueryBuilder */
+ private $queryGetSharesWithUsers;
+
+ /** @var IQueryBuilder */
+ private $queryUpdateSharePermissionsAndTarget;
+
+ /** @var IQueryBuilder */
+ private $queryUpdateShareInBatch;
+
+ /**
+ * @param \OCP\IConfig $config
+ * @param \OCP\IDBConnection $connection
+ */
+ public function __construct(
+ IConfig $config,
+ IDBConnection $connection,
+ IUserManager $userManager,
+ IGroupManager $groupManager
+ ) {
+ $this->connection = $connection;
+ $this->config = $config;
+ $this->userManager = $userManager;
+ $this->groupManager = $groupManager;
+ }
+
+ public function getName() {
+ return 'Repair unmerged shares';
+ }
+
+ /**
+ * Builds prepared queries for reuse
+ */
+ private function buildPreparedQueries() {
+ /**
+ * Retrieve shares for a given user/group and share type
+ */
+ $query = $this->connection->getQueryBuilder();
+ $query
+ ->select('item_source', 'id', 'file_target', 'permissions', 'parent', 'share_type')
+ ->from('share')
+ ->where($query->expr()->eq('share_type', $query->createParameter('shareType')))
+ ->andWhere($query->expr()->in('share_with', $query->createParameter('shareWiths')))
+ ->andWhere($query->expr()->in('item_type', $query->createParameter('itemTypes')))
+ ->orderBy('item_source', 'ASC')
+ ->addOrderBy('stime', 'ASC');
+
+ $this->queryGetSharesWithUsers = $query;
+
+ /**
+ * Updates the file_target to the given value for all given share ids.
+ *
+ * This updates several shares in bulk which is faster than individually.
+ */
+ $query = $this->connection->getQueryBuilder();
+ $query->update('share')
+ ->set('file_target', $query->createParameter('file_target'))
+ ->where($query->expr()->in('id', $query->createParameter('ids')));
+
+ $this->queryUpdateShareInBatch = $query;
+
+ /**
+ * Updates the share permissions and target path of a single share.
+ */
+ $query = $this->connection->getQueryBuilder();
+ $query->update('share')
+ ->set('permissions', $query->createParameter('permissions'))
+ ->set('file_target', $query->createParameter('file_target'))
+ ->where($query->expr()->eq('id', $query->createParameter('shareid')));
+
+ $this->queryUpdateSharePermissionsAndTarget = $query;
+
+ }
+
+ private function getSharesWithUser($shareType, $shareWiths) {
+ $groupedShares = [];
+
+ $query = $this->queryGetSharesWithUsers;
+ $query->setParameter('shareWiths', $shareWiths, IQueryBuilder::PARAM_STR_ARRAY);
+ $query->setParameter('shareType', $shareType);
+ $query->setParameter('itemTypes', ['file', 'folder'], IQueryBuilder::PARAM_STR_ARRAY);
+
+ $shares = $query->execute()->fetchAll();
+
+ // group by item_source
+ foreach ($shares as $share) {
+ if (!isset($groupedShares[$share['item_source']])) {
+ $groupedShares[$share['item_source']] = [];
+ }
+ $groupedShares[$share['item_source']][] = $share;
+ }
+ return $groupedShares;
+ }
+
+ /**
+ * Fix the given received share represented by the set of group shares
+ * and matching sub shares
+ *
+ * @param array $groupShares group share entries
+ * @param array $subShares sub share entries
+ *
+ * @return boolean false if the share was not repaired, true if it was
+ */
+ private function fixThisShare($groupShares, $subShares) {
+ $groupSharesById = [];
+ foreach ($groupShares as $groupShare) {
+ $groupSharesById[$groupShare['id']] = $groupShare;
+ }
+
+ if ($this->isThisShareValid($groupSharesById, $subShares)) {
+ return false;
+ }
+
+ $targetPath = $groupShares[0]['file_target'];
+
+ // check whether the user opted out completely of all subshares
+ $optedOut = true;
+ foreach ($subShares as $subShare) {
+ if ((int)$subShare['permissions'] !== 0) {
+ $optedOut = false;
+ break;
+ }
+ }
+
+ $shareIds = [];
+ foreach ($subShares as $subShare) {
+ // only if the user deleted some subshares but not all, adjust the permissions of that subshare
+ if (!$optedOut && (int)$subShare['permissions'] === 0 && (int)$subShare['share_type'] === DefaultShareProvider::SHARE_TYPE_USERGROUP) {
+ // set permissions from parent group share
+ $permissions = $groupSharesById[$subShare['parent']]['permissions'];
+
+ // fix permissions and target directly
+ $query = $this->queryUpdateSharePermissionsAndTarget;
+ $query->setParameter('shareid', $subShare['id']);
+ $query->setParameter('file_target', $targetPath);
+ $query->setParameter('permissions', $permissions);
+ $query->execute();
+ } else {
+ // gather share ids for bulk target update
+ if ($subShare['file_target'] !== $targetPath) {
+ $shareIds[] = (int)$subShare['id'];
+ }
+ }
+ }
+
+ if (!empty($shareIds)) {
+ $query = $this->queryUpdateShareInBatch;
+ $query->setParameter('ids', $shareIds, IQueryBuilder::PARAM_INT_ARRAY);
+ $query->setParameter('file_target', $targetPath);
+ $query->execute();
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks whether the number of group shares is balanced with the child subshares.
+ * If all group shares have exactly one subshare, and the target of every subshare
+ * is the same, then the share is valid.
+ * If however there is a group share entry that has no matching subshare, it means
+ * we're in the bogus situation and the whole share must be repaired
+ *
+ * @param array $groupSharesById
+ * @param array $subShares
+ *
+ * @return true if the share is valid, false if it needs repair
+ */
+ private function isThisShareValid($groupSharesById, $subShares) {
+ $foundTargets = [];
+
+ // every group share needs to have exactly one matching subshare
+ foreach ($subShares as $subShare) {
+ $foundTargets[$subShare['file_target']] = true;
+ if (count($foundTargets) > 1) {
+ // not all the same target path value => invalid
+ return false;
+ }
+ if (isset($groupSharesById[$subShare['parent']])) {
+ // remove it from the list as we found it
+ unset($groupSharesById[$subShare['parent']]);
+ }
+ }
+
+ // if we found one subshare per group entry, the set will be empty.
+ // If not empty, it means that one of the group shares did not have
+ // a matching subshare entry.
+ return empty($groupSharesById);
+ }
+
+ /**
+ * Detect unmerged received shares and merge them properly
+ */
+ private function fixUnmergedShares(IUser $user) {
+ $groups = $this->groupManager->getUserGroupIds($user);
+ if (empty($groups)) {
+ // user is in no groups, so can't have received group shares
+ return;
+ }
+
+ $subSharesByItemSource = $this->getSharesWithUser(DefaultShareProvider::SHARE_TYPE_USERGROUP, [$user->getUID()]);
+ if (empty($subSharesByItemSource)) {
+ // nothing to repair for this user
+ return;
+ }
+
+ $groupSharesByItemSource = $this->getSharesWithUser(Constants::SHARE_TYPE_GROUP, $groups);
+ if (empty($groupSharesByItemSource)) {
+ // shouldn't happen, those invalid shares must be cleant already by RepairInvalidShares
+ return;
+ }
+
+ // because sometimes one wants to give the user more permissions than the group share
+ $userSharesByItemSource = $this->getSharesWithUser(Constants::SHARE_TYPE_USER, [$user->getUID()]);
+
+ $fixed = 0;
+ foreach ($groupSharesByItemSource as $itemSource => $groupShares) {
+ if (!isset($subSharesByItemSource[$itemSource])) {
+ // no subshares for this item source, skip it
+ continue;
+ }
+ $subShares = $subSharesByItemSource[$itemSource];
+
+ if (isset($userSharesByItemSource[$itemSource])) {
+ // add it to the subshares to get a similar treatment
+ $subShares = array_merge($subShares, $userSharesByItemSource[$itemSource]);
+ }
+
+ if ($this->fixThisShare($groupShares, $subShares)) {
+ $fixed++;
+ }
+ }
+
+ if ($fixed > 0) {
+ $this->emit('\OC\Repair', 'info', array('Fixed ' . $fixed . ' share(s) that were not merged properly'));
+ }
+ }
+
+ /**
+ * Count all the users
+ *
+ * @return int
+ */
+ private function countUsers() {
+ $allCount = $this->userManager->countUsers();
+
+ $totalCount = 0;
+ foreach ($allCount as $backend => $count) {
+ $totalCount += $count;
+ }
+
+ return $totalCount;
+ }
+
+ public function run() {
+ $ocVersionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0');
+ if (version_compare($ocVersionFromBeforeUpdate, '9.0.4.1', '<')) {
+ // this situation was only possible between 9.0.0 and 9.0.4 included
+
+ $function = function(IUser $user) {
+ $this->fixUnmergedShares($user);
+ };
+
+ $this->buildPreparedQueries();
+
+ $userCount = $this->countUsers();
+
+ $this->userManager->callForAllUsers($function);
+ }
+ }
+}