|  | 
|  | 1 | +<?php | 
|  | 2 | + | 
|  | 3 | +declare(strict_types=1); | 
|  | 4 | + | 
|  | 5 | +/** | 
|  | 6 | + * @copyright Copyright (c) 2020 Arthur Schiwon <blizzz@arthur-schiwon.de> | 
|  | 7 | + * | 
|  | 8 | + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> | 
|  | 9 | + * | 
|  | 10 | + * @license GNU AGPL version 3 or any later version | 
|  | 11 | + * | 
|  | 12 | + * This program is free software: you can redistribute it and/or modify | 
|  | 13 | + * it under the terms of the GNU Affero General Public License as | 
|  | 14 | + * published by the Free Software Foundation, either version 3 of the | 
|  | 15 | + * License, or (at your option) any later version. | 
|  | 16 | + * | 
|  | 17 | + * This program is distributed in the hope that it will be useful, | 
|  | 18 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
|  | 19 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 
|  | 20 | + * GNU Affero General Public License for more details. | 
|  | 21 | + * | 
|  | 22 | + * You should have received a copy of the GNU Affero General Public License | 
|  | 23 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. | 
|  | 24 | + * | 
|  | 25 | + */ | 
|  | 26 | + | 
|  | 27 | +namespace OC\Core\Command\Maintenance; | 
|  | 28 | + | 
|  | 29 | +use Symfony\Component\Console\Command\Command; | 
|  | 30 | +use OCP\DB\QueryBuilder\IQueryBuilder; | 
|  | 31 | +use OCP\IDBConnection; | 
|  | 32 | +use OCP\IUser; | 
|  | 33 | +use OCP\IUserManager; | 
|  | 34 | +use Symfony\Component\Console\Input\InputArgument; | 
|  | 35 | +use Symfony\Component\Console\Input\InputInterface; | 
|  | 36 | +use Symfony\Component\Console\Input\InputOption; | 
|  | 37 | +use Symfony\Component\Console\Output\OutputInterface; | 
|  | 38 | +use Symfony\Component\Console\Question\ConfirmationQuestion; | 
|  | 39 | + | 
|  | 40 | +class RepairShareOwnership extends Command { | 
|  | 41 | +	/** @var IDBConnection $dbConnection */ | 
|  | 42 | +	private $dbConnection; | 
|  | 43 | +	/** @var IUserManager $userManager */ | 
|  | 44 | +	private $userManager; | 
|  | 45 | + | 
|  | 46 | +	public function __construct( | 
|  | 47 | +		IDBConnection $dbConnection, | 
|  | 48 | +		IUserManager $userManager | 
|  | 49 | +	) { | 
|  | 50 | +		$this->dbConnection = $dbConnection; | 
|  | 51 | +		$this->userManager = $userManager; | 
|  | 52 | +		parent::__construct(); | 
|  | 53 | +	} | 
|  | 54 | + | 
|  | 55 | +	protected function configure() { | 
|  | 56 | +		$this | 
|  | 57 | +			->setName('maintenance:repair-share-owner') | 
|  | 58 | +			->setDescription('repair invalid share-owner entries in the database') | 
|  | 59 | +			->addOption('no-confirm', 'y', InputOption::VALUE_NONE, "Don't ask for confirmation before repairing the shares") | 
|  | 60 | +			->addArgument('user', InputArgument::OPTIONAL, "User to fix incoming shares for, if omitted all users will be fixed"); | 
|  | 61 | +	} | 
|  | 62 | + | 
|  | 63 | +	protected function execute(InputInterface $input, OutputInterface $output): int { | 
|  | 64 | +		$noConfirm = $input->getOption('no-confirm'); | 
|  | 65 | +		$userId = $input->getArgument('user'); | 
|  | 66 | +		if ($userId) { | 
|  | 67 | +			$user = $this->userManager->get($userId); | 
|  | 68 | +			if (!$user) { | 
|  | 69 | +				$output->writeln("<error>user $userId not found</error>"); | 
|  | 70 | +				return 1; | 
|  | 71 | +			} | 
|  | 72 | +			$shares = $this->getWrongShareOwnershipForUser($user); | 
|  | 73 | +		} else { | 
|  | 74 | +			$shares = $this->getWrongShareOwnership(); | 
|  | 75 | +		} | 
|  | 76 | + | 
|  | 77 | +		if ($shares) { | 
|  | 78 | +			$output->writeln(""); | 
|  | 79 | +			$output->writeln("Found " . count($shares) . " shares with invalid share owner"); | 
|  | 80 | +			foreach ($shares as $share) { | 
|  | 81 | +				/** @var array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string} $share */ | 
|  | 82 | +				$output->writeln(" - share ${share['shareId']} from \"${share['initiator']}\" to \"${share['receiver']}\" at \"${share['fileTarget']}\", owned by \"${share['owner']}\", that should be owned by \"${share['mountOwner']}\""); | 
|  | 83 | +			} | 
|  | 84 | +			$output->writeln(""); | 
|  | 85 | + | 
|  | 86 | +			if (!$noConfirm) { | 
|  | 87 | +				$helper = $this->getHelper('question'); | 
|  | 88 | +				$question = new ConfirmationQuestion('Repair these shares? [y/N]', false); | 
|  | 89 | + | 
|  | 90 | +				if (!$helper->ask($input, $output, $question)) { | 
|  | 91 | +					return 0; | 
|  | 92 | +				} | 
|  | 93 | +			} | 
|  | 94 | +			$output->writeln("Repairing " . count($shares) . " shares"); | 
|  | 95 | +			$this->repairShares($shares); | 
|  | 96 | +		} else { | 
|  | 97 | +			$output->writeln("Found no shares with invalid share owner"); | 
|  | 98 | +		} | 
|  | 99 | + | 
|  | 100 | +		return 0; | 
|  | 101 | +	} | 
|  | 102 | + | 
|  | 103 | +	/** | 
|  | 104 | +	 * @return array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[] | 
|  | 105 | +	 * @throws \OCP\DB\Exception | 
|  | 106 | +	 */ | 
|  | 107 | +	protected function getWrongShareOwnership(): array { | 
|  | 108 | +		$qb = $this->dbConnection->getQueryBuilder(); | 
|  | 109 | +		$brokenShares = $qb | 
|  | 110 | +			->select('s.id', 'm.user_id', 's.uid_owner', 's.uid_initiator', 's.share_with', 's.file_target') | 
|  | 111 | +			->from('share', 's') | 
|  | 112 | +			->join('s', 'filecache', 'f', $qb->expr()->eq('s.item_source', $qb->expr()->castColumn('f.fileid', IQueryBuilder::PARAM_STR))) | 
|  | 113 | +			->join('s', 'mounts', 'm', $qb->expr()->eq('f.storage', 'm.storage_id')) | 
|  | 114 | +			->where($qb->expr()->neq('m.user_id', 's.uid_owner')) | 
|  | 115 | +			->andWhere($qb->expr()->eq($qb->func()->concat($qb->expr()->literal('/'), $qb->func()->concat('m.user_id', $qb->expr()->literal('/'))), 'm.mount_point')) | 
|  | 116 | +			->executeQuery() | 
|  | 117 | +			->fetchAll(); | 
|  | 118 | + | 
|  | 119 | +		$found = []; | 
|  | 120 | + | 
|  | 121 | +		foreach ($brokenShares as $share) { | 
|  | 122 | +			$found[] = [ | 
|  | 123 | +				'shareId' => (int) $share['id'], | 
|  | 124 | +				'fileTarget' => $share['file_target'], | 
|  | 125 | +				'initiator' => $share['uid_initiator'], | 
|  | 126 | +				'receiver' => $share['share_with'], | 
|  | 127 | +				'owner' => $share['uid_owner'], | 
|  | 128 | +				'mountOwner' => $share['user_id'], | 
|  | 129 | +			]; | 
|  | 130 | +		} | 
|  | 131 | + | 
|  | 132 | +		return $found; | 
|  | 133 | +	} | 
|  | 134 | + | 
|  | 135 | +	/** | 
|  | 136 | +	 * @param IUser $user | 
|  | 137 | +	 * @return array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[] | 
|  | 138 | +	 * @throws \OCP\DB\Exception | 
|  | 139 | +	 */ | 
|  | 140 | +	protected function getWrongShareOwnershipForUser(IUser $user): array { | 
|  | 141 | +		$qb = $this->dbConnection->getQueryBuilder(); | 
|  | 142 | +		$brokenShares = $qb | 
|  | 143 | +			->select('s.id', 'm.user_id', 's.uid_owner', 's.uid_initiator', 's.share_with', 's.file_target') | 
|  | 144 | +			->from('share', 's') | 
|  | 145 | +			->join('s', 'filecache', 'f', $qb->expr()->eq('s.item_source', $qb->expr()->castColumn('f.fileid', IQueryBuilder::PARAM_STR))) | 
|  | 146 | +			->join('s', 'mounts', 'm', $qb->expr()->eq('f.storage', 'm.storage_id')) | 
|  | 147 | +			->where($qb->expr()->neq('m.user_id', 's.uid_owner')) | 
|  | 148 | +			->andWhere($qb->expr()->eq($qb->func()->concat($qb->expr()->literal('/'), $qb->func()->concat('m.user_id', $qb->expr()->literal('/'))), 'm.mount_point')) | 
|  | 149 | +			->andWhere($qb->expr()->eq('s.share_with', $qb->createNamedParameter($user->getUID()))) | 
|  | 150 | +			->executeQuery() | 
|  | 151 | +			->fetchAll(); | 
|  | 152 | + | 
|  | 153 | +		$found = []; | 
|  | 154 | + | 
|  | 155 | +		foreach ($brokenShares as $share) { | 
|  | 156 | +			$found[] = [ | 
|  | 157 | +				'shareId' => (int) $share['id'], | 
|  | 158 | +				'fileTarget' => $share['file_target'], | 
|  | 159 | +				'initiator' => $share['uid_initiator'], | 
|  | 160 | +				'receiver' => $share['share_with'], | 
|  | 161 | +				'owner' => $share['uid_owner'], | 
|  | 162 | +				'mountOwner' => $share['user_id'], | 
|  | 163 | +			]; | 
|  | 164 | +		} | 
|  | 165 | + | 
|  | 166 | +		return $found; | 
|  | 167 | +	} | 
|  | 168 | + | 
|  | 169 | +	/** | 
|  | 170 | +	 * @param array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[] $shares | 
|  | 171 | +	 * @return void | 
|  | 172 | +	 */ | 
|  | 173 | +	protected function repairShares(array $shares) { | 
|  | 174 | +		$this->dbConnection->beginTransaction(); | 
|  | 175 | + | 
|  | 176 | +		$update = $this->dbConnection->getQueryBuilder(); | 
|  | 177 | +		$update->update('share') | 
|  | 178 | +			->set('uid_owner', $update->createParameter('share_owner')) | 
|  | 179 | +			->set('uid_initiator', $update->createParameter('share_initiator')) | 
|  | 180 | +			->where($update->expr()->eq('id', $update->createParameter('share_id'))); | 
|  | 181 | + | 
|  | 182 | +		foreach ($shares as $share) { | 
|  | 183 | +			/** @var array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string} $share */ | 
|  | 184 | +			$update->setParameter('share_id', $share['shareId'], IQueryBuilder::PARAM_INT); | 
|  | 185 | +			$update->setParameter('share_owner', $share['mountOwner']); | 
|  | 186 | + | 
|  | 187 | +			// if the broken owner is also the initiator it's safe to update them both, otherwise we don't touch the initiator | 
|  | 188 | +			if ($share['initiator'] === $share['owner']) { | 
|  | 189 | +				$update->setParameter('share_initiator', $share['mountOwner']); | 
|  | 190 | +			} else { | 
|  | 191 | +				$update->setParameter('share_initiator', $share['initiator']); | 
|  | 192 | +			} | 
|  | 193 | +			$update->executeStatement(); | 
|  | 194 | +		} | 
|  | 195 | + | 
|  | 196 | +		$this->dbConnection->commit(); | 
|  | 197 | +	} | 
|  | 198 | +} | 
0 commit comments