RepairShareOwnership.php 7.0 KB

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