RemoveLinkShares.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Repair;
  8. use OCP\AppFramework\Utility\ITimeFactory;
  9. use OCP\DB\IResult;
  10. use OCP\DB\QueryBuilder\IQueryBuilder;
  11. use OCP\IConfig;
  12. use OCP\IDBConnection;
  13. use OCP\IGroupManager;
  14. use OCP\Migration\IOutput;
  15. use OCP\Migration\IRepairStep;
  16. use OCP\Notification\IManager;
  17. class RemoveLinkShares implements IRepairStep {
  18. /** @var string[] */
  19. private $userToNotify = [];
  20. public function __construct(
  21. private IDBConnection $connection,
  22. private IConfig $config,
  23. private IGroupManager $groupManager,
  24. private IManager $notificationManager,
  25. private ITimeFactory $timeFactory,
  26. ) {
  27. }
  28. public function getName(): string {
  29. return 'Remove potentially over exposing share links';
  30. }
  31. private function shouldRun(): bool {
  32. $versionFromBeforeUpdate = $this->config->getSystemValueString('version', '0.0.0');
  33. if (version_compare($versionFromBeforeUpdate, '14.0.11', '<')) {
  34. return true;
  35. }
  36. if (version_compare($versionFromBeforeUpdate, '15.0.8', '<')) {
  37. return true;
  38. }
  39. if (version_compare($versionFromBeforeUpdate, '16.0.0', '<=')) {
  40. return true;
  41. }
  42. return false;
  43. }
  44. /**
  45. * Delete the share
  46. *
  47. * @param int $id
  48. */
  49. private function deleteShare(int $id): void {
  50. $qb = $this->connection->getQueryBuilder();
  51. $qb->delete('share')
  52. ->where($qb->expr()->eq('id', $qb->createNamedParameter($id)));
  53. $qb->executeStatement();
  54. }
  55. /**
  56. * Get the total of affected shares
  57. *
  58. * @return int
  59. */
  60. private function getTotal(): int {
  61. $subSubQuery = $this->connection->getQueryBuilder();
  62. $subSubQuery->select('*')
  63. ->from('share')
  64. ->where($subSubQuery->expr()->isNotNull('parent'))
  65. ->andWhere($subSubQuery->expr()->eq('share_type', $subSubQuery->expr()->literal(3, IQueryBuilder::PARAM_INT)));
  66. $subQuery = $this->connection->getQueryBuilder();
  67. $subQuery->select('s1.id')
  68. ->from($subQuery->createFunction('(' . $subSubQuery->getSQL() . ')'), 's1')
  69. ->join(
  70. 's1', 'share', 's2',
  71. $subQuery->expr()->eq('s1.parent', 's2.id')
  72. )
  73. ->where($subQuery->expr()->orX(
  74. $subQuery->expr()->eq('s2.share_type', $subQuery->expr()->literal(1, IQueryBuilder::PARAM_INT)),
  75. $subQuery->expr()->eq('s2.share_type', $subQuery->expr()->literal(2, IQueryBuilder::PARAM_INT))
  76. ))
  77. ->andWhere($subQuery->expr()->eq('s1.item_source', 's2.item_source'));
  78. $query = $this->connection->getQueryBuilder();
  79. $query->select($query->func()->count('*', 'total'))
  80. ->from('share')
  81. ->where($query->expr()->in('id', $query->createFunction($subQuery->getSQL())));
  82. $result = $query->executeQuery();
  83. $data = $result->fetch();
  84. $result->closeCursor();
  85. return (int)$data['total'];
  86. }
  87. /**
  88. * Get the cursor to fetch all the shares
  89. */
  90. private function getShares(): IResult {
  91. $subQuery = $this->connection->getQueryBuilder();
  92. $subQuery->select('*')
  93. ->from('share')
  94. ->where($subQuery->expr()->isNotNull('parent'))
  95. ->andWhere($subQuery->expr()->eq('share_type', $subQuery->expr()->literal(3, IQueryBuilder::PARAM_INT)));
  96. $query = $this->connection->getQueryBuilder();
  97. $query->select('s1.id', 's1.uid_owner', 's1.uid_initiator')
  98. ->from($query->createFunction('(' . $subQuery->getSQL() . ')'), 's1')
  99. ->join(
  100. 's1', 'share', 's2',
  101. $query->expr()->eq('s1.parent', 's2.id')
  102. )
  103. ->where($query->expr()->orX(
  104. $query->expr()->eq('s2.share_type', $query->expr()->literal(1, IQueryBuilder::PARAM_INT)),
  105. $query->expr()->eq('s2.share_type', $query->expr()->literal(2, IQueryBuilder::PARAM_INT))
  106. ))
  107. ->andWhere($query->expr()->eq('s1.item_source', 's2.item_source'));
  108. /** @var IResult $result */
  109. $result = $query->executeQuery();
  110. return $result;
  111. }
  112. /**
  113. * Process a single share
  114. *
  115. * @param array $data
  116. */
  117. private function processShare(array $data): void {
  118. $id = $data['id'];
  119. $this->addToNotify($data['uid_owner']);
  120. $this->addToNotify($data['uid_initiator']);
  121. $this->deleteShare((int)$id);
  122. }
  123. /**
  124. * Update list of users to notify
  125. *
  126. * @param string $uid
  127. */
  128. private function addToNotify(string $uid): void {
  129. if (!isset($this->userToNotify[$uid])) {
  130. $this->userToNotify[$uid] = true;
  131. }
  132. }
  133. /**
  134. * Send all notifications
  135. */
  136. private function sendNotification(): void {
  137. $time = $this->timeFactory->getDateTime();
  138. $notification = $this->notificationManager->createNotification();
  139. $notification->setApp('core')
  140. ->setDateTime($time)
  141. ->setObject('repair', 'exposing_links')
  142. ->setSubject('repair_exposing_links');
  143. $users = array_keys($this->userToNotify);
  144. foreach ($users as $user) {
  145. $notification->setUser((string)$user);
  146. $this->notificationManager->notify($notification);
  147. }
  148. }
  149. private function repair(IOutput $output, int $total): void {
  150. $output->startProgress($total);
  151. $shareResult = $this->getShares();
  152. while ($data = $shareResult->fetch()) {
  153. $this->processShare($data);
  154. $output->advance();
  155. }
  156. $output->finishProgress();
  157. $shareResult->closeCursor();
  158. // Notify all admins
  159. $adminGroup = $this->groupManager->get('admin');
  160. $adminUsers = $adminGroup->getUsers();
  161. foreach ($adminUsers as $user) {
  162. $this->addToNotify($user->getUID());
  163. }
  164. $output->info('Sending notifications to admins and affected users');
  165. $this->sendNotification();
  166. }
  167. public function run(IOutput $output): void {
  168. if ($this->shouldRun() === false || ($total = $this->getTotal()) === 0) {
  169. $output->info('No need to remove link shares.');
  170. return;
  171. }
  172. $output->info('Removing potentially over exposing link shares');
  173. $this->repair($output, $total);
  174. $output->info('Removed potentially over exposing link shares');
  175. }
  176. }