diff options
author | Vincent Petry <pvince81@owncloud.com> | 2016-07-20 17:48:54 +0300 |
---|---|---|
committer | Thomas Müller <thomas.mueller@tmit.eu> | 2016-08-13 18:10:13 +0300 |
commit | fd0f40383f4334ad5f9fb2ebc8893a7fdd653708 (patch) | |
tree | 513f265a21974101cf88ecadce13a077ae38faba /lib | |
parent | c3e7cf5030f96fb733eeb3bac985a49d55f6995a (diff) |
Add repair step for unmerged shares
Diffstat (limited to 'lib')
-rw-r--r-- | lib/private/repair.php | 7 | ||||
-rw-r--r-- | lib/private/repair/repairunmergedshares.php | 326 |
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); + } + } +} |