diff options
Diffstat (limited to 'lib/private/Repair/RepairUnmergedShares.php')
-rw-r--r-- | lib/private/Repair/RepairUnmergedShares.php | 357 |
1 files changed, 0 insertions, 357 deletions
diff --git a/lib/private/Repair/RepairUnmergedShares.php b/lib/private/Repair/RepairUnmergedShares.php deleted file mode 100644 index 56d935c74f5..00000000000 --- a/lib/private/Repair/RepairUnmergedShares.php +++ /dev/null @@ -1,357 +0,0 @@ -<?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 OCP\Migration\IRepairStep; -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 implements IRepairStep { - - /** @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', 'stime') - ->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; - } - - private function isPotentialDuplicateName($name) { - return (preg_match('/\(\d+\)(\.[^\.]+)?$/', $name) === 1); - } - - /** - * Decide on the best target name based on all group shares and subshares, - * goal is to increase the likeliness that the chosen name matches what - * the user is expecting. - * - * For this, we discard the entries with parenthesis "(2)". - * In case the user also renamed the duplicates to a legitimate name, this logic - * will still pick the most recent one as it's the one the user is most likely to - * remember renaming. - * - * If no suitable subshare is found, use the least recent group share instead. - * - * @param array $groupShares group share entries - * @param array $subShares sub share entries - * - * @return string chosen target name - */ - private function findBestTargetName($groupShares, $subShares) { - $pickedShare = null; - // sort by stime, this also properly sorts the direct user share if any - @usort($subShares, function($a, $b) { - return ((int)$a['stime'] - (int)$b['stime']); - }); - - foreach ($subShares as $subShare) { - // skip entries that have parenthesis with numbers - if ($this->isPotentialDuplicateName($subShare['file_target'])) { - continue; - } - // pick any share found that would match, the last being the most recent - $pickedShare = $subShare; - } - - // no suitable subshare found - if ($pickedShare === null) { - // use least recent group share target instead - $pickedShare = $groupShares[0]; - } - - return $pickedShare['file_target']; - } - - /** - * 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) { - if (empty($subShares)) { - return false; - } - - $groupSharesById = []; - foreach ($groupShares as $groupShare) { - $groupSharesById[$groupShare['id']] = $groupShare; - } - - if ($this->isThisShareValid($groupSharesById, $subShares)) { - return false; - } - - $targetPath = $this->findBestTargetName($groupShares, $subShares); - - // 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(IOutput $out, IUser $user) { - $groups = $this->groupManager->getUserGroupIds($user); - if (empty($groups)) { - // user is in no groups, so can't have received group shares - return; - } - - // get all subshares grouped by item source - $subSharesByItemSource = $this->getSharesWithUser(DefaultShareProvider::SHARE_TYPE_USERGROUP, [$user->getUID()]); - - // because sometimes one wants to give the user more permissions than the group share - $userSharesByItemSource = $this->getSharesWithUser(Constants::SHARE_TYPE_USER, [$user->getUID()]); - - if (empty($subSharesByItemSource) && empty($userSharesByItemSource)) { - // nothing to repair for this user, no need to do extra queries - return; - } - - $groupSharesByItemSource = $this->getSharesWithUser(Constants::SHARE_TYPE_GROUP, $groups); - if (empty($groupSharesByItemSource) && empty($userSharesByItemSource)) { - // nothing to repair for this user - return; - } - - foreach ($groupSharesByItemSource as $itemSource => $groupShares) { - $subShares = []; - if (isset($subSharesByItemSource[$itemSource])) { - $subShares = $subSharesByItemSource[$itemSource]; - } - - if (isset($userSharesByItemSource[$itemSource])) { - // add it to the subshares to get a similar treatment - $subShares = array_merge($subShares, $userSharesByItemSource[$itemSource]); - } - - $this->fixThisShare($groupShares, $subShares); - } - } - - public function run(IOutput $output) { - $ocVersionFromBeforeUpdate = $this->config->getSystemValue('version', '0.0.0'); - if (version_compare($ocVersionFromBeforeUpdate, '9.1.0.16', '<')) { - // this situation was only possible between 9.0.0 and 9.0.3 included - - $function = function(IUser $user) use ($output) { - $this->fixUnmergedShares($output, $user); - $output->advance(); - }; - - $this->buildPreparedQueries(); - - $output->startProgress($this->userManager->countUsers()); - - $this->userManager->callForAllUsers($function); - - $output->finishProgress(); - } - } -} |