summaryrefslogtreecommitdiffstats
path: root/lib/private/Repair/RepairUnmergedShares.php
diff options
context:
space:
mode:
Diffstat (limited to 'lib/private/Repair/RepairUnmergedShares.php')
-rw-r--r--lib/private/Repair/RepairUnmergedShares.php357
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();
- }
- }
-}