RemoveLinkShares.php 6.0 KB

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