aboutsummaryrefslogtreecommitdiffstats
path: root/apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php
stat options
Period:
Authors:

Commits per author per week (path 'apps/oauth2/tests/Controller/LoginRedirectorControllerTest.php')

AuthorW28 2025W29 2025W30 2025W31 2025Total
Total00000
>40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176
<?php

declare(strict_types=1);

/**
 * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
 * SPDX-License-Identifier: AGPL-3.0-or-later
 */

namespace OC\Core\Command\Maintenance;

use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;

class RepairShareOwnership extends Command {
	public function __construct(
		private IDBConnection $dbConnection,
		private IUserManager $userManager,
	) {
		parent::__construct();
	}

	protected function configure() {
		$this
			->setName('maintenance:repair-share-owner')
			->setDescription('repair invalid share-owner entries in the database')
			->addOption('no-confirm', 'y', InputOption::VALUE_NONE, "Don't ask for confirmation before repairing the shares")
			->addArgument('user', InputArgument::OPTIONAL, 'User to fix incoming shares for, if omitted all users will be fixed');
	}

	protected function execute(InputInterface $input, OutputInterface $output): int {
		$noConfirm = $input->getOption('no-confirm');
		$userId = $input->getArgument('user');
		if ($userId) {
			$user = $this->userManager->get($userId);
			if (!$user) {
				$output->writeln("<error>user $userId not found</error>");
				return 1;
			}
			$shares = $this->getWrongShareOwnershipForUser($user);
		} else {
			$shares = $this->getWrongShareOwnership();
		}

		if ($shares) {
			$output->writeln('');
			$output->writeln('Found ' . count($shares) . ' shares with invalid share owner');
			foreach ($shares as $share) {
				/** @var array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string} $share */
				$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']}\"");
			}
			$output->writeln('');

			if (!$noConfirm) {
				/** @var QuestionHelper $helper */
				$helper = $this->getHelper('question');
				$question = new ConfirmationQuestion('Repair these shares? [y/N]', false);

				if (!$helper->ask($input, $output, $question)) {
					return 0;
				}
			}
			$output->writeln('Repairing ' . count($shares) . ' shares');
			$this->repairShares($shares);
		} else {
			$output->writeln('Found no shares with invalid share owner');
		}

		return 0;
	}

	/**
	 * @return array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[]
	 * @throws \OCP\DB\Exception
	 */
	protected function getWrongShareOwnership(): array {
		$qb = $this->dbConnection->getQueryBuilder();
		$brokenShares = $qb
			->select('s.id', 'm.user_id', 's.uid_owner', 's.uid_initiator', 's.share_with', 's.file_target')
			->from('share', 's')
			->join('s', 'filecache', 'f', $qb->expr()->eq($qb->expr()->castColumn('s.item_source', IQueryBuilder::PARAM_INT), 'f.fileid'))
			->join('s', 'mounts', 'm', $qb->expr()->eq('f.storage', 'm.storage_id'))
			->where($qb->expr()->neq('m.user_id', 's.uid_owner'))
			->andWhere($qb->expr()->eq($qb->func()->concat($qb->expr()->literal('/'), 'm.user_id', $qb->expr()->literal('/')), 'm.mount_point'))
			->executeQuery()
			->fetchAll();

		$found = [];

		foreach ($brokenShares as $share) {
			$found[] = [
				'shareId' => (int)$share['id'],
				'fileTarget' => $share['file_target'],
				'initiator' => $share['uid_initiator'],
				'receiver' => $share['share_with'],
				'owner' => $share['uid_owner'],
				'mountOwner' => $share['user_id'],
			];
		}

		return $found;
	}

	/**
	 * @param IUser $user
	 * @return array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[]
	 * @throws \OCP\DB\Exception
	 */
	protected function getWrongShareOwnershipForUser(IUser $user): array {
		$qb = $this->dbConnection->getQueryBuilder();
		$brokenShares = $qb
			->select('s.id', 'm.user_id', 's.uid_owner', 's.uid_initiator', 's.share_with', 's.file_target')
			->from('share', 's')
			->join('s', 'filecache', 'f', $qb->expr()->eq('s.item_source', $qb->expr()->castColumn('f.fileid', IQueryBuilder::PARAM_STR)))
			->join('s', 'mounts', 'm', $qb->expr()->eq('f.storage', 'm.storage_id'))
			->where($qb->expr()->neq('m.user_id', 's.uid_owner'))
			->andWhere($qb->expr()->eq($qb->func()->concat($qb->expr()->literal('/'), 'm.user_id', $qb->expr()->literal('/')), 'm.mount_point'))
			->andWhere($qb->expr()->eq('s.share_with', $qb->createNamedParameter($user->getUID())))
			->executeQuery()
			->fetchAll();

		$found = [];

		foreach ($brokenShares as $share) {
			$found[] = [
				'shareId' => (int)$share['id'],
				'fileTarget' => $share['file_target'],
				'initiator' => $share['uid_initiator'],
				'receiver' => $share['share_with'],
				'owner' => $share['uid_owner'],
				'mountOwner' => $share['user_id'],
			];
		}

		return $found;
	}

	/**
	 * @param array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string}[] $shares
	 * @return void
	 */
	protected function repairShares(array $shares) {
		$this->dbConnection->beginTransaction();

		$update = $this->dbConnection->getQueryBuilder();
		$update->update('share')
			->set('uid_owner', $update->createParameter('share_owner'))
			->set('uid_initiator', $update->createParameter('share_initiator'))
			->where($update->expr()->eq('id', $update->createParameter('share_id')));

		foreach ($shares as $share) {
			/** @var array{shareId: int, fileTarget: string, initiator: string, receiver: string, owner: string, mountOwner: string} $share */
			$update->setParameter('share_id', $share['shareId'], IQueryBuilder::PARAM_INT);
			$update->setParameter('share_owner', $share['mountOwner']);

			// if the broken owner is also the initiator it's safe to update them both, otherwise we don't touch the initiator
			if ($share['initiator'] === $share['owner']) {
				$update->setParameter('share_initiator', $share['mountOwner']);
			} else {
				$update->setParameter('share_initiator', $share['initiator']);
			}
			$update->executeStatement();
		}

		$this->dbConnection->commit();
	}
}