You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1898 lines
58 KiB

3 years ago
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  6. * @author Bjoern Schiessle <bjoern@schiessle.org>
  7. * @author Björn Schießle <bjoern@schiessle.org>
  8. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  9. * @author Daniel Calviño Sánchez <danxuliu@gmail.com>
  10. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  11. * @author Jan-Christoph Borchardt <hey@jancborchardt.net>
  12. * @author Joas Schilling <coding@schilljs.com>
  13. * @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
  14. * @author Julius Härtl <jus@bitgrid.net>
  15. * @author Lukas Reschke <lukas@statuscode.ch>
  16. * @author Maxence Lange <maxence@artificial-owl.com>
  17. * @author Maxence Lange <maxence@nextcloud.com>
  18. * @author Morris Jobke <hey@morrisjobke.de>
  19. * @author Pauli Järvinen <pauli.jarvinen@gmail.com>
  20. * @author Robin Appelman <robin@icewind.nl>
  21. * @author Roeland Jago Douma <roeland@famdouma.nl>
  22. * @author Vincent Petry <pvince81@owncloud.com>
  23. *
  24. * @license AGPL-3.0
  25. *
  26. * This code is free software: you can redistribute it and/or modify
  27. * it under the terms of the GNU Affero General Public License, version 3,
  28. * as published by the Free Software Foundation.
  29. *
  30. * This program is distributed in the hope that it will be useful,
  31. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  32. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  33. * GNU Affero General Public License for more details.
  34. *
  35. * You should have received a copy of the GNU Affero General Public License, version 3,
  36. * along with this program. If not, see <http://www.gnu.org/licenses/>
  37. *
  38. */
  39. namespace OC\Share20;
  40. use OC\Cache\CappedMemoryCache;
  41. use OC\Files\Mount\MoveableMount;
  42. use OC\HintException;
  43. use OC\Share20\Exception\ProviderException;
  44. use OCA\Files_Sharing\ISharedStorage;
  45. use OCP\EventDispatcher\IEventDispatcher;
  46. use OCP\Files\File;
  47. use OCP\Files\Folder;
  48. use OCP\Files\IRootFolder;
  49. use OCP\Files\Mount\IMountManager;
  50. use OCP\Files\Node;
  51. use OCP\IConfig;
  52. use OCP\IGroupManager;
  53. use OCP\IL10N;
  54. use OCP\ILogger;
  55. use OCP\IURLGenerator;
  56. use OCP\IUser;
  57. use OCP\IUserManager;
  58. use OCP\L10N\IFactory;
  59. use OCP\Mail\IMailer;
  60. use OCP\Security\Events\ValidatePasswordPolicyEvent;
  61. use OCP\Security\IHasher;
  62. use OCP\Security\ISecureRandom;
  63. use OCP\Share;
  64. use OCP\Share\Exceptions\GenericShareException;
  65. use OCP\Share\Exceptions\ShareNotFound;
  66. use OCP\Share\IManager;
  67. use OCP\Share\IProviderFactory;
  68. use OCP\Share\IShare;
  69. use OCP\Share\IShareProvider;
  70. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  71. use Symfony\Component\EventDispatcher\GenericEvent;
  72. /**
  73. * This class is the communication hub for all sharing related operations.
  74. */
  75. class Manager implements IManager {
  76. /** @var IProviderFactory */
  77. private $factory;
  78. /** @var ILogger */
  79. private $logger;
  80. /** @var IConfig */
  81. private $config;
  82. /** @var ISecureRandom */
  83. private $secureRandom;
  84. /** @var IHasher */
  85. private $hasher;
  86. /** @var IMountManager */
  87. private $mountManager;
  88. /** @var IGroupManager */
  89. private $groupManager;
  90. /** @var IL10N */
  91. private $l;
  92. /** @var IFactory */
  93. private $l10nFactory;
  94. /** @var IUserManager */
  95. private $userManager;
  96. /** @var IRootFolder */
  97. private $rootFolder;
  98. /** @var CappedMemoryCache */
  99. private $sharingDisabledForUsersCache;
  100. /** @var EventDispatcherInterface */
  101. private $legacyDispatcher;
  102. /** @var LegacyHooks */
  103. private $legacyHooks;
  104. /** @var IMailer */
  105. private $mailer;
  106. /** @var IURLGenerator */
  107. private $urlGenerator;
  108. /** @var \OC_Defaults */
  109. private $defaults;
  110. /** @var IEventDispatcher */
  111. private $dispatcher;
  112. /**
  113. * Manager constructor.
  114. *
  115. * @param ILogger $logger
  116. * @param IConfig $config
  117. * @param ISecureRandom $secureRandom
  118. * @param IHasher $hasher
  119. * @param IMountManager $mountManager
  120. * @param IGroupManager $groupManager
  121. * @param IL10N $l
  122. * @param IFactory $l10nFactory
  123. * @param IProviderFactory $factory
  124. * @param IUserManager $userManager
  125. * @param IRootFolder $rootFolder
  126. * @param EventDispatcherInterface $eventDispatcher
  127. * @param IMailer $mailer
  128. * @param IURLGenerator $urlGenerator
  129. * @param \OC_Defaults $defaults
  130. */
  131. public function __construct(
  132. ILogger $logger,
  133. IConfig $config,
  134. ISecureRandom $secureRandom,
  135. IHasher $hasher,
  136. IMountManager $mountManager,
  137. IGroupManager $groupManager,
  138. IL10N $l,
  139. IFactory $l10nFactory,
  140. IProviderFactory $factory,
  141. IUserManager $userManager,
  142. IRootFolder $rootFolder,
  143. EventDispatcherInterface $legacyDispatcher,
  144. IMailer $mailer,
  145. IURLGenerator $urlGenerator,
  146. \OC_Defaults $defaults,
  147. IEventDispatcher $dispatcher
  148. ) {
  149. $this->logger = $logger;
  150. $this->config = $config;
  151. $this->secureRandom = $secureRandom;
  152. $this->hasher = $hasher;
  153. $this->mountManager = $mountManager;
  154. $this->groupManager = $groupManager;
  155. $this->l = $l;
  156. $this->l10nFactory = $l10nFactory;
  157. $this->factory = $factory;
  158. $this->userManager = $userManager;
  159. $this->rootFolder = $rootFolder;
  160. $this->legacyDispatcher = $legacyDispatcher;
  161. $this->sharingDisabledForUsersCache = new CappedMemoryCache();
  162. $this->legacyHooks = new LegacyHooks($this->legacyDispatcher);
  163. $this->mailer = $mailer;
  164. $this->urlGenerator = $urlGenerator;
  165. $this->defaults = $defaults;
  166. $this->dispatcher = $dispatcher;
  167. }
  168. /**
  169. * Convert from a full share id to a tuple (providerId, shareId)
  170. *
  171. * @param string $id
  172. * @return string[]
  173. */
  174. private function splitFullId($id) {
  175. return explode(':', $id, 2);
  176. }
  177. /**
  178. * Verify if a password meets all requirements
  179. *
  180. * @param string $password
  181. * @throws \Exception
  182. */
  183. protected function verifyPassword($password) {
  184. if ($password === null) {
  185. // No password is set, check if this is allowed.
  186. if ($this->shareApiLinkEnforcePassword()) {
  187. throw new \InvalidArgumentException('Passwords are enforced for link shares');
  188. }
  189. return;
  190. }
  191. // Let others verify the password
  192. try {
  193. $this->legacyDispatcher->dispatch(new ValidatePasswordPolicyEvent($password));
  194. } catch (HintException $e) {
  195. throw new \Exception($e->getHint());
  196. }
  197. }
  198. /**
  199. * Check for generic requirements before creating a share
  200. *
  201. * @param IShare $share
  202. * @throws \InvalidArgumentException
  203. * @throws GenericShareException
  204. *
  205. * @suppress PhanUndeclaredClassMethod
  206. */
  207. protected function generalCreateChecks(IShare $share) {
  208. if ($share->getShareType() === IShare::TYPE_USER) {
  209. // We expect a valid user as sharedWith for user shares
  210. if (!$this->userManager->userExists($share->getSharedWith())) {
  211. throw new \InvalidArgumentException('SharedWith is not a valid user');
  212. }
  213. } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
  214. // We expect a valid group as sharedWith for group shares
  215. if (!$this->groupManager->groupExists($share->getSharedWith())) {
  216. throw new \InvalidArgumentException('SharedWith is not a valid group');
  217. }
  218. } elseif ($share->getShareType() === IShare::TYPE_LINK) {
  219. if ($share->getSharedWith() !== null) {
  220. throw new \InvalidArgumentException('SharedWith should be empty');
  221. }
  222. } elseif ($share->getShareType() === IShare::TYPE_REMOTE) {
  223. if ($share->getSharedWith() === null) {
  224. throw new \InvalidArgumentException('SharedWith should not be empty');
  225. }
  226. } elseif ($share->getShareType() === IShare::TYPE_REMOTE_GROUP) {
  227. if ($share->getSharedWith() === null) {
  228. throw new \InvalidArgumentException('SharedWith should not be empty');
  229. }
  230. } elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
  231. if ($share->getSharedWith() === null) {
  232. throw new \InvalidArgumentException('SharedWith should not be empty');
  233. }
  234. } elseif ($share->getShareType() === IShare::TYPE_CIRCLE) {
  235. $circle = \OCA\Circles\Api\v1\Circles::detailsCircle($share->getSharedWith());
  236. if ($circle === null) {
  237. throw new \InvalidArgumentException('SharedWith is not a valid circle');
  238. }
  239. } elseif ($share->getShareType() === IShare::TYPE_ROOM) {
  240. } else {
  241. // We can't handle other types yet
  242. throw new \InvalidArgumentException('unknown share type');
  243. }
  244. // Verify the initiator of the share is set
  245. if ($share->getSharedBy() === null) {
  246. throw new \InvalidArgumentException('SharedBy should be set');
  247. }
  248. // Cannot share with yourself
  249. if ($share->getShareType() === IShare::TYPE_USER &&
  250. $share->getSharedWith() === $share->getSharedBy()) {
  251. throw new \InvalidArgumentException('Can’t share with yourself');
  252. }
  253. // The path should be set
  254. if ($share->getNode() === null) {
  255. throw new \InvalidArgumentException('Path should be set');
  256. }
  257. // And it should be a file or a folder
  258. if (!($share->getNode() instanceof \OCP\Files\File) &&
  259. !($share->getNode() instanceof \OCP\Files\Folder)) {
  260. throw new \InvalidArgumentException('Path should be either a file or a folder');
  261. }
  262. // And you can't share your rootfolder
  263. if ($this->userManager->userExists($share->getSharedBy())) {
  264. $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
  265. } else {
  266. $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
  267. }
  268. if ($userFolder->getId() === $share->getNode()->getId()) {
  269. throw new \InvalidArgumentException('You can’t share your root folder');
  270. }
  271. // Check if we actually have share permissions
  272. if (!$share->getNode()->isShareable()) {
  273. $path = $userFolder->getRelativePath($share->getNode()->getPath());
  274. $message_t = $this->l->t('You are not allowed to share %s', [$path]);
  275. throw new GenericShareException($message_t, $message_t, 404);
  276. }
  277. // Permissions should be set
  278. if ($share->getPermissions() === null) {
  279. throw new \InvalidArgumentException('A share requires permissions');
  280. }
  281. $isFederatedShare = $share->getNode()->getStorage()->instanceOfStorage('\OCA\Files_Sharing\External\Storage');
  282. $permissions = 0;
  283. if (!$isFederatedShare && $share->getNode()->getOwner() && $share->getNode()->getOwner()->getUID() !== $share->getSharedBy()) {
  284. $userMounts = array_filter($userFolder->getById($share->getNode()->getId()), function ($mount) {
  285. // We need to filter since there might be other mountpoints that contain the file
  286. // e.g. if the user has access to the same external storage that the file is originating from
  287. return $mount->getStorage()->instanceOfStorage(ISharedStorage::class);
  288. });
  289. $userMount = array_shift($userMounts);
  290. if ($userMount === null) {
  291. throw new GenericShareException('Could not get proper share mount for ' . $share->getNode()->getId() . '. Failing since else the next calls are called with null');
  292. }
  293. $mount = $userMount->getMountPoint();
  294. // When it's a reshare use the parent share permissions as maximum
  295. $userMountPointId = $mount->getStorageRootId();
  296. $userMountPoints = $userFolder->getById($userMountPointId);
  297. $userMountPoint = array_shift($userMountPoints);
  298. if ($userMountPoint === null) {
  299. throw new GenericShareException('Could not get proper user mount for ' . $userMountPointId . '. Failing since else the next calls are called with null');
  300. }
  301. /* Check if this is an incoming share */
  302. $incomingShares = $this->getSharedWith($share->getSharedBy(), IShare::TYPE_USER, $userMountPoint, -1, 0);
  303. $incomingShares = array_merge($incomingShares, $this->getSharedWith($share->getSharedBy(), IShare::TYPE_GROUP, $userMountPoint, -1, 0));
  304. $incomingShares = array_merge($incomingShares, $this->getSharedWith($share->getSharedBy(), IShare::TYPE_CIRCLE, $userMountPoint, -1, 0));
  305. $incomingShares = array_merge($incomingShares, $this->getSharedWith($share->getSharedBy(), IShare::TYPE_ROOM, $userMountPoint, -1, 0));
  306. /** @var IShare[] $incomingShares */
  307. if (!empty($incomingShares)) {
  308. foreach ($incomingShares as $incomingShare) {
  309. $permissions |= $incomingShare->getPermissions();
  310. }
  311. }
  312. } else {
  313. /*
  314. * Quick fix for #23536
  315. * Non moveable mount points do not have update and delete permissions
  316. * while we 'most likely' do have that on the storage.
  317. */
  318. $permissions = $share->getNode()->getPermissions();
  319. if (!($share->getNode()->getMountPoint() instanceof MoveableMount)) {
  320. $permissions |= \OCP\Constants::PERMISSION_DELETE | \OCP\Constants::PERMISSION_UPDATE;
  321. }
  322. }
  323. // Check that we do not share with more permissions than we have
  324. if ($share->getPermissions() & ~$permissions) {
  325. $path = $userFolder->getRelativePath($share->getNode()->getPath());
  326. $message_t = $this->l->t('Can’t increase permissions of %s', [$path]);
  327. throw new GenericShareException($message_t, $message_t, 404);
  328. }
  329. // Check that read permissions are always set
  330. // Link shares are allowed to have no read permissions to allow upload to hidden folders
  331. $noReadPermissionRequired = $share->getShareType() === IShare::TYPE_LINK
  332. || $share->getShareType() === IShare::TYPE_EMAIL;
  333. if (!$noReadPermissionRequired &&
  334. ($share->getPermissions() & \OCP\Constants::PERMISSION_READ) === 0) {
  335. throw new \InvalidArgumentException('Shares need at least read permissions');
  336. }
  337. if ($share->getNode() instanceof \OCP\Files\File) {
  338. if ($share->getPermissions() & \OCP\Constants::PERMISSION_DELETE) {
  339. $message_t = $this->l->t('Files can’t be shared with delete permissions');
  340. throw new GenericShareException($message_t);
  341. }
  342. if ($share->getPermissions() & \OCP\Constants::PERMISSION_CREATE) {
  343. $message_t = $this->l->t('Files can’t be shared with create permissions');
  344. throw new GenericShareException($message_t);
  345. }
  346. }
  347. }
  348. /**
  349. * Validate if the expiration date fits the system settings
  350. *
  351. * @param IShare $share The share to validate the expiration date of
  352. * @return IShare The modified share object
  353. * @throws GenericShareException
  354. * @throws \InvalidArgumentException
  355. * @throws \Exception
  356. */
  357. protected function validateExpirationDateInternal(IShare $share) {
  358. $expirationDate = $share->getExpirationDate();
  359. if ($expirationDate !== null) {
  360. //Make sure the expiration date is a date
  361. $expirationDate->setTime(0, 0, 0);
  362. $date = new \DateTime();
  363. $date->setTime(0, 0, 0);
  364. if ($date >= $expirationDate) {
  365. $message = $this->l->t('Expiration date is in the past');
  366. throw new GenericShareException($message, $message, 404);
  367. }
  368. }
  369. // If expiredate is empty set a default one if there is a default
  370. $fullId = null;
  371. try {
  372. $fullId = $share->getFullId();
  373. } catch (\UnexpectedValueException $e) {
  374. // This is a new share
  375. }
  376. if ($fullId === null && $expirationDate === null && $this->shareApiInternalDefaultExpireDate()) {
  377. $expirationDate = new \DateTime();
  378. $expirationDate->setTime(0,0,0);
  379. $days = (int)$this->config->getAppValue('core', 'internal_defaultExpDays', $this->shareApiLinkDefaultExpireDays());
  380. if ($days > $this->shareApiLinkDefaultExpireDays()) {
  381. $days = $this->shareApiLinkDefaultExpireDays();
  382. }
  383. $expirationDate->add(new \DateInterval('P'.$days.'D'));
  384. }
  385. // If we enforce the expiration date check that is does not exceed
  386. if ($this->shareApiInternalDefaultExpireDateEnforced()) {
  387. if ($expirationDate === null) {
  388. throw new \InvalidArgumentException('Expiration date is enforced');
  389. }
  390. $date = new \DateTime();
  391. $date->setTime(0, 0, 0);
  392. $date->add(new \DateInterval('P' . $this->shareApiInternalDefaultExpireDays() . 'D'));
  393. if ($date < $expirationDate) {
  394. $message = $this->l->t('Can’t set expiration date more than %s days in the future', [$this->shareApiInternalDefaultExpireDays()]);
  395. throw new GenericShareException($message, $message, 404);
  396. }
  397. }
  398. $accepted = true;
  399. $message = '';
  400. \OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
  401. 'expirationDate' => &$expirationDate,
  402. 'accepted' => &$accepted,
  403. 'message' => &$message,
  404. 'passwordSet' => $share->getPassword() !== null,
  405. ]);
  406. if (!$accepted) {
  407. throw new \Exception($message);
  408. }
  409. $share->setExpirationDate($expirationDate);
  410. return $share;
  411. }
  412. /**
  413. * Validate if the expiration date fits the system settings
  414. *
  415. * @param IShare $share The share to validate the expiration date of
  416. * @return IShare The modified share object
  417. * @throws GenericShareException
  418. * @throws \InvalidArgumentException
  419. * @throws \Exception
  420. */
  421. protected function validateExpirationDate(IShare $share) {
  422. $expirationDate = $share->getExpirationDate();
  423. if ($expirationDate !== null) {
  424. //Make sure the expiration date is a date
  425. $expirationDate->setTime(0, 0, 0);
  426. $date = new \DateTime();
  427. $date->setTime(0, 0, 0);
  428. if ($date >= $expirationDate) {
  429. $message = $this->l->t('Expiration date is in the past');
  430. throw new GenericShareException($message, $message, 404);
  431. }
  432. }
  433. // If expiredate is empty set a default one if there is a default
  434. $fullId = null;
  435. try {
  436. $fullId = $share->getFullId();
  437. } catch (\UnexpectedValueException $e) {
  438. // This is a new share
  439. }
  440. if ($fullId === null && $expirationDate === null && $this->shareApiLinkDefaultExpireDate()) {
  441. $expirationDate = new \DateTime();
  442. $expirationDate->setTime(0,0,0);
  443. $days = (int)$this->config->getAppValue('core', 'link_defaultExpDays', $this->shareApiLinkDefaultExpireDays());
  444. if ($days > $this->shareApiLinkDefaultExpireDays()) {
  445. $days = $this->shareApiLinkDefaultExpireDays();
  446. }
  447. $expirationDate->add(new \DateInterval('P'.$days.'D'));
  448. }
  449. // If we enforce the expiration date check that is does not exceed
  450. if ($this->shareApiLinkDefaultExpireDateEnforced()) {
  451. if ($expirationDate === null) {
  452. throw new \InvalidArgumentException('Expiration date is enforced');
  453. }
  454. $date = new \DateTime();
  455. $date->setTime(0, 0, 0);
  456. $date->add(new \DateInterval('P' . $this->shareApiLinkDefaultExpireDays() . 'D'));
  457. if ($date < $expirationDate) {
  458. $message = $this->l->t('Can’t set expiration date more than %s days in the future', [$this->shareApiLinkDefaultExpireDays()]);
  459. throw new GenericShareException($message, $message, 404);
  460. }
  461. }
  462. $accepted = true;
  463. $message = '';
  464. \OCP\Util::emitHook('\OC\Share', 'verifyExpirationDate', [
  465. 'expirationDate' => &$expirationDate,
  466. 'accepted' => &$accepted,
  467. 'message' => &$message,
  468. 'passwordSet' => $share->getPassword() !== null,
  469. ]);
  470. if (!$accepted) {
  471. throw new \Exception($message);
  472. }
  473. $share->setExpirationDate($expirationDate);
  474. return $share;
  475. }
  476. /**
  477. * Check for pre share requirements for user shares
  478. *
  479. * @param IShare $share
  480. * @throws \Exception
  481. */
  482. protected function userCreateChecks(IShare $share) {
  483. // Check if we can share with group members only
  484. if ($this->shareWithGroupMembersOnly()) {
  485. $sharedBy = $this->userManager->get($share->getSharedBy());
  486. $sharedWith = $this->userManager->get($share->getSharedWith());
  487. // Verify we can share with this user
  488. $groups = array_intersect(
  489. $this->groupManager->getUserGroupIds($sharedBy),
  490. $this->groupManager->getUserGroupIds($sharedWith)
  491. );
  492. if (empty($groups)) {
  493. throw new \Exception('Sharing is only allowed with group members');
  494. }
  495. }
  496. /*
  497. * TODO: Could be costly, fix
  498. *
  499. * Also this is not what we want in the future.. then we want to squash identical shares.
  500. */
  501. $provider = $this->factory->getProviderForType(IShare::TYPE_USER);
  502. $existingShares = $provider->getSharesByPath($share->getNode());
  503. foreach ($existingShares as $existingShare) {
  504. // Ignore if it is the same share
  505. try {
  506. if ($existingShare->getFullId() === $share->getFullId()) {
  507. continue;
  508. }
  509. } catch (\UnexpectedValueException $e) {
  510. //Shares are not identical
  511. }
  512. // Identical share already existst
  513. if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
  514. throw new \Exception('Path is already shared with this user');
  515. }
  516. // The share is already shared with this user via a group share
  517. if ($existingShare->getShareType() === IShare::TYPE_GROUP) {
  518. $group = $this->groupManager->get($existingShare->getSharedWith());
  519. if (!is_null($group)) {
  520. $user = $this->userManager->get($share->getSharedWith());
  521. if ($group->inGroup($user) && $existingShare->getShareOwner() !== $share->getShareOwner()) {
  522. throw new \Exception('Path is already shared with this user');
  523. }
  524. }
  525. }
  526. }
  527. }
  528. /**
  529. * Check for pre share requirements for group shares
  530. *
  531. * @param IShare $share
  532. * @throws \Exception
  533. */
  534. protected function groupCreateChecks(IShare $share) {
  535. // Verify group shares are allowed
  536. if (!$this->allowGroupSharing()) {
  537. throw new \Exception('Group sharing is now allowed');
  538. }
  539. // Verify if the user can share with this group
  540. if ($this->shareWithGroupMembersOnly()) {
  541. $sharedBy = $this->userManager->get($share->getSharedBy());
  542. $sharedWith = $this->groupManager->get($share->getSharedWith());
  543. if (is_null($sharedWith) || !$sharedWith->inGroup($sharedBy)) {
  544. throw new \Exception('Sharing is only allowed within your own groups');
  545. }
  546. }
  547. /*
  548. * TODO: Could be costly, fix
  549. *
  550. * Also this is not what we want in the future.. then we want to squash identical shares.
  551. */
  552. $provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
  553. $existingShares = $provider->getSharesByPath($share->getNode());
  554. foreach ($existingShares as $existingShare) {
  555. try {
  556. if ($existingShare->getFullId() === $share->getFullId()) {
  557. continue;
  558. }
  559. } catch (\UnexpectedValueException $e) {
  560. //It is a new share so just continue
  561. }
  562. if ($existingShare->getSharedWith() === $share->getSharedWith() && $existingShare->getShareType() === $share->getShareType()) {
  563. throw new \Exception('Path is already shared with this group');
  564. }
  565. }
  566. }
  567. /**
  568. * Check for pre share requirements for link shares
  569. *
  570. * @param IShare $share
  571. * @throws \Exception
  572. */
  573. protected function linkCreateChecks(IShare $share) {
  574. // Are link shares allowed?
  575. if (!$this->shareApiAllowLinks()) {
  576. throw new \Exception('Link sharing is not allowed');
  577. }
  578. // Check if public upload is allowed
  579. if (!$this->shareApiLinkAllowPublicUpload() &&
  580. ($share->getPermissions() & (\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE | \OCP\Constants::PERMISSION_DELETE))) {
  581. throw new \InvalidArgumentException('Public upload is not allowed');
  582. }
  583. }
  584. /**
  585. * To make sure we don't get invisible link shares we set the parent
  586. * of a link if it is a reshare. This is a quick word around
  587. * until we can properly display multiple link shares in the UI
  588. *
  589. * See: https://github.com/owncloud/core/issues/22295
  590. *
  591. * FIXME: Remove once multiple link shares can be properly displayed
  592. *
  593. * @param IShare $share
  594. */
  595. protected function setLinkParent(IShare $share) {
  596. // No sense in checking if the method is not there.
  597. if (method_exists($share, 'setParent')) {
  598. $storage = $share->getNode()->getStorage();
  599. if ($storage->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
  600. /** @var \OCA\Files_Sharing\SharedStorage $storage */
  601. $share->setParent($storage->getShareId());
  602. }
  603. }
  604. }
  605. /**
  606. * @param File|Folder $path
  607. */
  608. protected function pathCreateChecks($path) {
  609. // Make sure that we do not share a path that contains a shared mountpoint
  610. if ($path instanceof \OCP\Files\Folder) {
  611. $mounts = $this->mountManager->findIn($path->getPath());
  612. foreach ($mounts as $mount) {
  613. if ($mount->getStorage()->instanceOfStorage('\OCA\Files_Sharing\ISharedStorage')) {
  614. throw new \InvalidArgumentException('Path contains files shared with you');
  615. }
  616. }
  617. }
  618. }
  619. /**
  620. * Check if the user that is sharing can actually share
  621. *
  622. * @param IShare $share
  623. * @throws \Exception
  624. */
  625. protected function canShare(IShare $share) {
  626. if (!$this->shareApiEnabled()) {
  627. throw new \Exception('Sharing is disabled');
  628. }
  629. if ($this->sharingDisabledForUser($share->getSharedBy())) {
  630. throw new \Exception('Sharing is disabled for you');
  631. }
  632. }
  633. /**
  634. * Share a path
  635. *
  636. * @param IShare $share
  637. * @return IShare The share object
  638. * @throws \Exception
  639. *
  640. * TODO: handle link share permissions or check them
  641. */
  642. public function createShare(IShare $share) {
  643. $this->canShare($share);
  644. $this->generalCreateChecks($share);
  645. // Verify if there are any issues with the path
  646. $this->pathCreateChecks($share->getNode());
  647. /*
  648. * On creation of a share the owner is always the owner of the path
  649. * Except for mounted federated shares.
  650. */
  651. $storage = $share->getNode()->getStorage();
  652. if ($storage->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
  653. $parent = $share->getNode()->getParent();
  654. while ($parent->getStorage()->instanceOfStorage('OCA\Files_Sharing\External\Storage')) {
  655. $parent = $parent->getParent();
  656. }
  657. $share->setShareOwner($parent->getOwner()->getUID());
  658. } else {
  659. if ($share->getNode()->getOwner()) {
  660. $share->setShareOwner($share->getNode()->getOwner()->getUID());
  661. } else {
  662. $share->setShareOwner($share->getSharedBy());
  663. }
  664. }
  665. //Verify share type
  666. if ($share->getShareType() === IShare::TYPE_USER) {
  667. $this->userCreateChecks($share);
  668. //Verify the expiration date
  669. $share = $this->validateExpirationDateInternal($share);
  670. } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
  671. $this->groupCreateChecks($share);
  672. //Verify the expiration date
  673. $share = $this->validateExpirationDateInternal($share);
  674. } elseif ($share->getShareType() === IShare::TYPE_LINK) {
  675. $this->linkCreateChecks($share);
  676. $this->setLinkParent($share);
  677. /*
  678. * For now ignore a set token.
  679. */
  680. $share->setToken(
  681. $this->secureRandom->generate(
  682. \OC\Share\Constants::TOKEN_LENGTH,
  683. \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE
  684. )
  685. );
  686. //Verify the expiration date
  687. $share = $this->validateExpirationDate($share);
  688. //Verify the password
  689. $this->verifyPassword($share->getPassword());
  690. // If a password is set. Hash it!
  691. if ($share->getPassword() !== null) {
  692. $share->setPassword($this->hasher->hash($share->getPassword()));
  693. }
  694. } elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
  695. $share->setToken(
  696. $this->secureRandom->generate(
  697. \OC\Share\Constants::TOKEN_LENGTH,
  698. \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE
  699. )
  700. );
  701. }
  702. // Cannot share with the owner
  703. if ($share->getShareType() === IShare::TYPE_USER &&
  704. $share->getSharedWith() === $share->getShareOwner()) {
  705. throw new \InvalidArgumentException('Can’t share with the share owner');
  706. }
  707. // Generate the target
  708. $target = $this->config->getSystemValue('share_folder', '/') .'/'. $share->getNode()->getName();
  709. $target = \OC\Files\Filesystem::normalizePath($target);
  710. $share->setTarget($target);
  711. // Pre share event
  712. $event = new GenericEvent($share);
  713. $this->legacyDispatcher->dispatch('OCP\Share::preShare', $event);
  714. if ($event->isPropagationStopped() && $event->hasArgument('error')) {
  715. throw new \Exception($event->getArgument('error'));
  716. }
  717. $oldShare = $share;
  718. $provider = $this->factory->getProviderForType($share->getShareType());
  719. $share = $provider->create($share);
  720. //reuse the node we already have
  721. $share->setNode($oldShare->getNode());
  722. // Reset the target if it is null for the new share
  723. if ($share->getTarget() === '') {
  724. $share->setTarget($target);
  725. }
  726. // Post share event
  727. $event = new GenericEvent($share);
  728. $this->legacyDispatcher->dispatch('OCP\Share::postShare', $event);
  729. $this->dispatcher->dispatchTyped(new Share\Events\ShareCreatedEvent($share));
  730. if ($this->config->getSystemValueBool('sharing.enable_share_mail', true)
  731. && $share->getShareType() === IShare::TYPE_USER) {
  732. $mailSend = $share->getMailSend();
  733. if ($mailSend === true) {
  734. $user = $this->userManager->get($share->getSharedWith());
  735. if ($user !== null) {
  736. $emailAddress = $user->getEMailAddress();
  737. if ($emailAddress !== null && $emailAddress !== '') {
  738. $userLang = $this->l10nFactory->getUserLanguage($user);
  739. $l = $this->l10nFactory->get('lib', $userLang);
  740. $this->sendMailNotification(
  741. $l,
  742. $share->getNode()->getName(),
  743. $this->urlGenerator->linkToRouteAbsolute('files_sharing.Accept.accept', ['shareId' => $share->getFullId()]),
  744. $share->getSharedBy(),
  745. $emailAddress,
  746. $share->getExpirationDate()
  747. );
  748. $this->logger->debug('Sent share notification to ' . $emailAddress . ' for share with ID ' . $share->getId(), ['app' => 'share']);
  749. } else {
  750. $this->logger->debug('Share notification not sent to ' . $share->getSharedWith() . ' because email address is not set.', ['app' => 'share']);
  751. }
  752. } else {
  753. $this->logger->debug('Share notification not sent to ' . $share->getSharedWith() . ' because user could not be found.', ['app' => 'share']);
  754. }
  755. } else {
  756. $this->logger->debug('Share notification not sent because mailsend is false.', ['app' => 'share']);
  757. }
  758. }
  759. return $share;
  760. }
  761. /**
  762. * Send mail notifications
  763. *
  764. * This method will catch and log mail transmission errors
  765. *
  766. * @param IL10N $l Language of the recipient
  767. * @param string $filename file/folder name
  768. * @param string $link link to the file/folder
  769. * @param string $initiator user ID of share sender
  770. * @param string $shareWith email address of share receiver
  771. * @param \DateTime|null $expiration
  772. */
  773. protected function sendMailNotification(IL10N $l,
  774. $filename,
  775. $link,
  776. $initiator,
  777. $shareWith,
  778. \DateTime $expiration = null) {
  779. $initiatorUser = $this->userManager->get($initiator);
  780. $initiatorDisplayName = ($initiatorUser instanceof IUser) ? $initiatorUser->getDisplayName() : $initiator;
  781. $message = $this->mailer->createMessage();
  782. $emailTemplate = $this->mailer->createEMailTemplate('files_sharing.RecipientNotification', [
  783. 'filename' => $filename,
  784. 'link' => $link,
  785. 'initiator' => $initiatorDisplayName,
  786. 'expiration' => $expiration,
  787. 'shareWith' => $shareWith,
  788. ]);
  789. $emailTemplate->setSubject($l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]));
  790. $emailTemplate->addHeader();
  791. $emailTemplate->addHeading($l->t('%1$s shared »%2$s« with you', [$initiatorDisplayName, $filename]), false);
  792. $text = $l->t('%1$s shared »%2$s« with you.', [$initiatorDisplayName, $filename]);
  793. $emailTemplate->addBodyText(
  794. htmlspecialchars($text . ' ' . $l->t('Click the button below to open it.')),
  795. $text
  796. );
  797. $emailTemplate->addBodyButton(
  798. $l->t('Open »%s«', [$filename]),
  799. $link
  800. );
  801. $message->setTo([$shareWith]);
  802. // The "From" contains the sharers name
  803. $instanceName = $this->defaults->getName();
  804. $senderName = $l->t(
  805. '%1$s via %2$s',
  806. [
  807. $initiatorDisplayName,
  808. $instanceName
  809. ]
  810. );
  811. $message->setFrom([\OCP\Util::getDefaultEmailAddress($instanceName) => $senderName]);
  812. // The "Reply-To" is set to the sharer if an mail address is configured
  813. // also the default footer contains a "Do not reply" which needs to be adjusted.
  814. $initiatorEmail = $initiatorUser->getEMailAddress();
  815. if ($initiatorEmail !== null) {
  816. $message->setReplyTo([$initiatorEmail => $initiatorDisplayName]);
  817. $emailTemplate->addFooter($instanceName . ($this->defaults->getSlogan($l->getLanguageCode()) !== '' ? ' - ' . $this->defaults->getSlogan($l->getLanguageCode()) : ''));
  818. } else {
  819. $emailTemplate->addFooter('', $l->getLanguageCode());
  820. }
  821. $message->useTemplate($emailTemplate);
  822. try {
  823. $failedRecipients = $this->mailer->send($message);
  824. if (!empty($failedRecipients)) {
  825. $this->logger->error('Share notification mail could not be sent to: ' . implode(', ', $failedRecipients));
  826. return;
  827. }
  828. } catch (\Exception $e) {
  829. $this->logger->logException($e, ['message' => 'Share notification mail could not be sent']);
  830. }
  831. }
  832. /**
  833. * Update a share
  834. *
  835. * @param IShare $share
  836. * @return IShare The share object
  837. * @throws \InvalidArgumentException
  838. */
  839. public function updateShare(IShare $share) {
  840. $expirationDateUpdated = false;
  841. $this->canShare($share);
  842. try {
  843. $originalShare = $this->getShareById($share->getFullId());
  844. } catch (\UnexpectedValueException $e) {
  845. throw new \InvalidArgumentException('Share does not have a full id');
  846. }
  847. // We can't change the share type!
  848. if ($share->getShareType() !== $originalShare->getShareType()) {
  849. throw new \InvalidArgumentException('Can’t change share type');
  850. }
  851. // We can only change the recipient on user shares
  852. if ($share->getSharedWith() !== $originalShare->getSharedWith() &&
  853. $share->getShareType() !== IShare::TYPE_USER) {
  854. throw new \InvalidArgumentException('Can only update recipient on user shares');
  855. }
  856. // Cannot share with the owner
  857. if ($share->getShareType() === IShare::TYPE_USER &&
  858. $share->getSharedWith() === $share->getShareOwner()) {
  859. throw new \InvalidArgumentException('Can’t share with the share owner');
  860. }
  861. $this->generalCreateChecks($share);
  862. if ($share->getShareType() === IShare::TYPE_USER) {
  863. $this->userCreateChecks($share);
  864. if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
  865. //Verify the expiration date
  866. $this->validateExpirationDate($share);
  867. $expirationDateUpdated = true;
  868. }
  869. } elseif ($share->getShareType() === IShare::TYPE_GROUP) {
  870. $this->groupCreateChecks($share);
  871. if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
  872. //Verify the expiration date
  873. $this->validateExpirationDate($share);
  874. $expirationDateUpdated = true;
  875. }
  876. } elseif ($share->getShareType() === IShare::TYPE_LINK) {
  877. $this->linkCreateChecks($share);
  878. $plainTextPassword = $share->getPassword();
  879. $this->updateSharePasswordIfNeeded($share, $originalShare);
  880. if (empty($plainTextPassword) && $share->getSendPasswordByTalk()) {
  881. throw new \InvalidArgumentException('Can’t enable sending the password by Talk with an empty password');
  882. }
  883. if ($share->getExpirationDate() != $originalShare->getExpirationDate()) {
  884. //Verify the expiration date
  885. $this->validateExpirationDate($share);
  886. $expirationDateUpdated = true;
  887. }
  888. } elseif ($share->getShareType() === IShare::TYPE_EMAIL) {
  889. // The new password is not set again if it is the same as the old
  890. // one.
  891. $plainTextPassword = $share->getPassword();
  892. if (!empty($plainTextPassword) && !$this->updateSharePasswordIfNeeded($share, $originalShare)) {
  893. $plainTextPassword = null;
  894. }
  895. if (empty($plainTextPassword) && !$originalShare->getSendPasswordByTalk() && $share->getSendPasswordByTalk()) {
  896. // If the same password was already sent by mail the recipient
  897. // would already have access to the share without having to call
  898. // the sharer to verify her identity
  899. throw new \InvalidArgumentException('Can’t enable sending the password by Talk without setting a new password');
  900. } elseif (empty($plainTextPassword) && $originalShare->getSendPasswordByTalk() && !$share->getSendPasswordByTalk()) {
  901. throw new \InvalidArgumentException('Can’t disable sending the password by Talk without setting a new password');
  902. }
  903. }
  904. $this->pathCreateChecks($share->getNode());
  905. // Now update the share!
  906. $provider = $this->factory->getProviderForType($share->getShareType());
  907. if ($share->getShareType() === IShare::TYPE_EMAIL) {
  908. $share = $provider->update($share, $plainTextPassword);
  909. } else {
  910. $share = $provider->update($share);
  911. }
  912. if ($expirationDateUpdated === true) {
  913. \OC_Hook::emit(Share::class, 'post_set_expiration_date', [
  914. 'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
  915. 'itemSource' => $share->getNode()->getId(),
  916. 'date' => $share->getExpirationDate(),
  917. 'uidOwner' => $share->getSharedBy(),
  918. ]);
  919. }
  920. if ($share->getPassword() !== $originalShare->getPassword()) {
  921. \OC_Hook::emit(Share::class, 'post_update_password', [
  922. 'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
  923. 'itemSource' => $share->getNode()->getId(),
  924. 'uidOwner' => $share->getSharedBy(),
  925. 'token' => $share->getToken(),
  926. 'disabled' => is_null($share->getPassword()),
  927. ]);
  928. }
  929. if ($share->getPermissions() !== $originalShare->getPermissions()) {
  930. if ($this->userManager->userExists($share->getShareOwner())) {
  931. $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
  932. } else {
  933. $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
  934. }
  935. \OC_Hook::emit(Share::class, 'post_update_permissions', [
  936. 'itemType' => $share->getNode() instanceof \OCP\Files\File ? 'file' : 'folder',
  937. 'itemSource' => $share->getNode()->getId(),
  938. 'shareType' => $share->getShareType(),
  939. 'shareWith' => $share->getSharedWith(),
  940. 'uidOwner' => $share->getSharedBy(),
  941. 'permissions' => $share->getPermissions(),
  942. 'path' => $userFolder->getRelativePath($share->getNode()->getPath()),
  943. ]);
  944. }
  945. return $share;
  946. }
  947. /**
  948. * Accept a share.
  949. *
  950. * @param IShare $share
  951. * @param string $recipientId
  952. * @return IShare The share object
  953. * @throws \InvalidArgumentException
  954. * @since 9.0.0
  955. */
  956. public function acceptShare(IShare $share, string $recipientId): IShare {
  957. [$providerId, ] = $this->splitFullId($share->getFullId());
  958. $provider = $this->factory->getProvider($providerId);
  959. if (!method_exists($provider, 'acceptShare')) {
  960. // TODO FIX ME
  961. throw new \InvalidArgumentException('Share provider does not support accepting');
  962. }
  963. $provider->acceptShare($share, $recipientId);
  964. $event = new GenericEvent($share);
  965. $this->legacyDispatcher->dispatch('OCP\Share::postAcceptShare', $event);
  966. return $share;
  967. }
  968. /**
  969. * Updates the password of the given share if it is not the same as the
  970. * password of the original share.
  971. *
  972. * @param IShare $share the share to update its password.
  973. * @param IShare $originalShare the original share to compare its
  974. * password with.
  975. * @return boolean whether the password was updated or not.
  976. */
  977. private function updateSharePasswordIfNeeded(IShare $share, IShare $originalShare) {
  978. $passwordsAreDifferent = ($share->getPassword() !== $originalShare->getPassword()) &&
  979. (($share->getPassword() !== null && $originalShare->getPassword() === null) ||
  980. ($share->getPassword() === null && $originalShare->getPassword() !== null) ||
  981. ($share->getPassword() !== null && $originalShare->getPassword() !== null &&
  982. !$this->hasher->verify($share->getPassword(), $originalShare->getPassword())));
  983. // Password updated.
  984. if ($passwordsAreDifferent) {
  985. //Verify the password
  986. $this->verifyPassword($share->getPassword());
  987. // If a password is set. Hash it!
  988. if ($share->getPassword() !== null) {
  989. $share->setPassword($this->hasher->hash($share->getPassword()));
  990. return true;
  991. }
  992. } else {
  993. // Reset the password to the original one, as it is either the same
  994. // as the "new" password or a hashed version of it.
  995. $share->setPassword($originalShare->getPassword());
  996. }
  997. return false;
  998. }
  999. /**
  1000. * Delete all the children of this share
  1001. * FIXME: remove once https://github.com/owncloud/core/pull/21660 is in
  1002. *
  1003. * @param IShare $share
  1004. * @return IShare[] List of deleted shares
  1005. */
  1006. protected function deleteChildren(IShare $share) {
  1007. $deletedShares = [];
  1008. $provider = $this->factory->getProviderForType($share->getShareType());
  1009. foreach ($provider->getChildren($share) as $child) {
  1010. $deletedChildren = $this->deleteChildren($child);
  1011. $deletedShares = array_merge($deletedShares, $deletedChildren);
  1012. $provider->delete($child);
  1013. $deletedShares[] = $child;
  1014. }
  1015. return $deletedShares;
  1016. }
  1017. /**
  1018. * Delete a share
  1019. *
  1020. * @param IShare $share
  1021. * @throws ShareNotFound
  1022. * @throws \InvalidArgumentException
  1023. */
  1024. public function deleteShare(IShare $share) {
  1025. try {
  1026. $share->getFullId();
  1027. } catch (\UnexpectedValueException $e) {
  1028. throw new \InvalidArgumentException('Share does not have a full id');
  1029. }
  1030. $event = new GenericEvent($share);
  1031. $this->legacyDispatcher->dispatch('OCP\Share::preUnshare', $event);
  1032. // Get all children and delete them as well
  1033. $deletedShares = $this->deleteChildren($share);
  1034. // Do the actual delete
  1035. $provider = $this->factory->getProviderForType($share->getShareType());
  1036. $provider->delete($share);
  1037. // All the deleted shares caused by this delete
  1038. $deletedShares[] = $share;
  1039. // Emit post hook
  1040. $event->setArgument('deletedShares', $deletedShares);
  1041. $this->legacyDispatcher->dispatch('OCP\Share::postUnshare', $event);
  1042. }
  1043. /**
  1044. * Unshare a file as the recipient.
  1045. * This can be different from a regular delete for example when one of
  1046. * the users in a groups deletes that share. But the provider should
  1047. * handle this.
  1048. *
  1049. * @param IShare $share
  1050. * @param string $recipientId
  1051. */
  1052. public function deleteFromSelf(IShare $share, $recipientId) {
  1053. list($providerId, ) = $this->splitFullId($share->getFullId());
  1054. $provider = $this->factory->getProvider($providerId);
  1055. $provider->deleteFromSelf($share, $recipientId);
  1056. $event = new GenericEvent($share);
  1057. $this->legacyDispatcher->dispatch('OCP\Share::postUnshareFromSelf', $event);
  1058. }
  1059. public function restoreShare(IShare $share, string $recipientId): IShare {
  1060. list($providerId, ) = $this->splitFullId($share->getFullId());
  1061. $provider = $this->factory->getProvider($providerId);
  1062. return $provider->restore($share, $recipientId);
  1063. }
  1064. /**
  1065. * @inheritdoc
  1066. */
  1067. public function moveShare(IShare $share, $recipientId) {
  1068. if ($share->getShareType() === IShare::TYPE_LINK) {
  1069. throw new \InvalidArgumentException('Can’t change target of link share');
  1070. }
  1071. if ($share->getShareType() === IShare::TYPE_USER && $share->getSharedWith() !== $recipientId) {
  1072. throw new \InvalidArgumentException('Invalid recipient');
  1073. }
  1074. if ($share->getShareType() === IShare::TYPE_GROUP) {
  1075. $sharedWith = $this->groupManager->get($share->getSharedWith());
  1076. if (is_null($sharedWith)) {
  1077. throw new \InvalidArgumentException('Group "' . $share->getSharedWith() . '" does not exist');
  1078. }
  1079. $recipient = $this->userManager->get($recipientId);
  1080. if (!$sharedWith->inGroup($recipient)) {
  1081. throw new \InvalidArgumentException('Invalid recipient');
  1082. }
  1083. }
  1084. list($providerId, ) = $this->splitFullId($share->getFullId());
  1085. $provider = $this->factory->getProvider($providerId);
  1086. return $provider->move($share, $recipientId);
  1087. }
  1088. public function getSharesInFolder($userId, Folder $node, $reshares = false) {
  1089. $providers = $this->factory->getAllProviders();
  1090. return array_reduce($providers, function ($shares, IShareProvider $provider) use ($userId, $node, $reshares) {
  1091. $newShares = $provider->getSharesInFolder($userId, $node, $reshares);
  1092. foreach ($newShares as $fid => $data) {
  1093. if (!isset($shares[$fid])) {
  1094. $shares[$fid] = [];
  1095. }
  1096. $shares[$fid] = array_merge($shares[$fid], $data);
  1097. }
  1098. return $shares;
  1099. }, []);
  1100. }
  1101. /**
  1102. * @inheritdoc
  1103. */
  1104. public function getSharesBy($userId, $shareType, $path = null, $reshares = false, $limit = 50, $offset = 0) {
  1105. if ($path !== null &&
  1106. !($path instanceof \OCP\Files\File) &&
  1107. !($path instanceof \OCP\Files\Folder)) {
  1108. throw new \InvalidArgumentException('invalid path');
  1109. }
  1110. try {
  1111. $provider = $this->factory->getProviderForType($shareType);
  1112. } catch (ProviderException $e) {
  1113. return [];
  1114. }
  1115. $shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
  1116. /*
  1117. * Work around so we don't return expired shares but still follow
  1118. * proper pagination.
  1119. */
  1120. $shares2 = [];
  1121. while (true) {
  1122. $added = 0;
  1123. foreach ($shares as $share) {
  1124. try {
  1125. $this->checkExpireDate($share);
  1126. } catch (ShareNotFound $e) {
  1127. //Ignore since this basically means the share is deleted
  1128. continue;
  1129. }
  1130. $added++;
  1131. $shares2[] = $share;
  1132. if (count($shares2) === $limit) {
  1133. break;
  1134. }
  1135. }
  1136. // If we did not fetch more shares than the limit then there are no more shares
  1137. if (count($shares) < $limit) {
  1138. break;
  1139. }
  1140. if (count($shares2) === $limit) {
  1141. break;
  1142. }
  1143. // If there was no limit on the select we are done
  1144. if ($limit === -1) {
  1145. break;
  1146. }
  1147. $offset += $added;
  1148. // Fetch again $limit shares
  1149. $shares = $provider->getSharesBy($userId, $shareType, $path, $reshares, $limit, $offset);
  1150. // No more shares means we are done
  1151. if (empty($shares)) {
  1152. break;
  1153. }
  1154. }
  1155. $shares = $shares2;
  1156. return $shares;
  1157. }
  1158. /**
  1159. * @inheritdoc
  1160. */
  1161. public function getSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0) {
  1162. try {
  1163. $provider = $this->factory->getProviderForType($shareType);
  1164. } catch (ProviderException $e) {
  1165. return [];
  1166. }
  1167. $shares = $provider->getSharedWith($userId, $shareType, $node, $limit, $offset);
  1168. // remove all shares which are already expired
  1169. foreach ($shares as $key => $share) {
  1170. try {
  1171. $this->checkExpireDate($share);
  1172. } catch (ShareNotFound $e) {
  1173. unset($shares[$key]);
  1174. }
  1175. }
  1176. return $shares;
  1177. }
  1178. /**
  1179. * @inheritdoc
  1180. */
  1181. public function getDeletedSharedWith($userId, $shareType, $node = null, $limit = 50, $offset = 0) {
  1182. $shares = $this->getSharedWith($userId, $shareType, $node, $limit, $offset);
  1183. // Only get deleted shares
  1184. $shares = array_filter($shares, function (IShare $share) {
  1185. return $share->getPermissions() === 0;
  1186. });
  1187. // Only get shares where the owner still exists
  1188. $shares = array_filter($shares, function (IShare $share) {
  1189. return $this->userManager->userExists($share->getShareOwner());
  1190. });
  1191. return $shares;
  1192. }
  1193. /**
  1194. * @inheritdoc
  1195. */
  1196. public function getShareById($id, $recipient = null) {
  1197. if ($id === null) {
  1198. throw new ShareNotFound();
  1199. }
  1200. list($providerId, $id) = $this->splitFullId($id);
  1201. try {
  1202. $provider = $this->factory->getProvider($providerId);
  1203. } catch (ProviderException $e) {
  1204. throw new ShareNotFound();
  1205. }
  1206. $share = $provider->getShareById($id, $recipient);
  1207. $this->checkExpireDate($share);
  1208. return $share;
  1209. }
  1210. /**
  1211. * Get all the shares for a given path
  1212. *
  1213. * @param \OCP\Files\Node $path
  1214. * @param int $page
  1215. * @param int $perPage
  1216. *
  1217. * @return Share[]
  1218. */
  1219. public function getSharesByPath(\OCP\Files\Node $path, $page=0, $perPage=50) {
  1220. return [];
  1221. }
  1222. /**
  1223. * Get the share by token possible with password
  1224. *
  1225. * @param string $token
  1226. * @return IShare
  1227. *
  1228. * @throws ShareNotFound
  1229. */
  1230. public function getShareByToken($token) {
  1231. // tokens can't be valid local user names
  1232. if ($this->userManager->userExists($token)) {
  1233. throw new ShareNotFound();
  1234. }
  1235. $share = null;
  1236. try {
  1237. if ($this->shareApiAllowLinks()) {
  1238. $provider = $this->factory->getProviderForType(IShare::TYPE_LINK);
  1239. $share = $provider->getShareByToken($token);
  1240. }
  1241. } catch (ProviderException $e) {
  1242. } catch (ShareNotFound $e) {
  1243. }
  1244. // If it is not a link share try to fetch a federated share by token
  1245. if ($share === null) {
  1246. try {
  1247. $provider = $this->factory->getProviderForType(IShare::TYPE_REMOTE);
  1248. $share = $provider->getShareByToken($token);
  1249. } catch (ProviderException $e) {
  1250. } catch (ShareNotFound $e) {
  1251. }
  1252. }
  1253. // If it is not a link share try to fetch a mail share by token
  1254. if ($share === null && $this->shareProviderExists(IShare::TYPE_EMAIL)) {
  1255. try {
  1256. $provider = $this->factory->getProviderForType(IShare::TYPE_EMAIL);
  1257. $share = $provider->getShareByToken($token);
  1258. } catch (ProviderException $e) {
  1259. } catch (ShareNotFound $e) {
  1260. }
  1261. }
  1262. if ($share === null && $this->shareProviderExists(IShare::TYPE_CIRCLE)) {
  1263. try {
  1264. $provider = $this->factory->getProviderForType(IShare::TYPE_CIRCLE);
  1265. $share = $provider->getShareByToken($token);
  1266. } catch (ProviderException $e) {
  1267. } catch (ShareNotFound $e) {
  1268. }
  1269. }
  1270. if ($share === null && $this->shareProviderExists(IShare::TYPE_ROOM)) {
  1271. try {
  1272. $provider = $this->factory->getProviderForType(IShare::TYPE_ROOM);
  1273. $share = $provider->getShareByToken($token);
  1274. } catch (ProviderException $e) {
  1275. } catch (ShareNotFound $e) {
  1276. }
  1277. }
  1278. if ($share === null) {
  1279. throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
  1280. }
  1281. $this->checkExpireDate($share);
  1282. /*
  1283. * Reduce the permissions for link shares if public upload is not enabled
  1284. */
  1285. if ($share->getShareType() === IShare::TYPE_LINK &&
  1286. !$this->shareApiLinkAllowPublicUpload()) {
  1287. $share->setPermissions($share->getPermissions() & ~(\OCP\Constants::PERMISSION_CREATE | \OCP\Constants::PERMISSION_UPDATE));
  1288. }
  1289. return $share;
  1290. }
  1291. protected function checkExpireDate($share) {
  1292. if ($share->isExpired()) {
  1293. $this->deleteShare($share);
  1294. throw new ShareNotFound($this->l->t('The requested share does not exist anymore'));
  1295. }
  1296. }
  1297. /**
  1298. * Verify the password of a public share
  1299. *
  1300. * @param IShare $share
  1301. * @param string $password
  1302. * @return bool
  1303. */
  1304. public function checkPassword(IShare $share, $password) {
  1305. $passwordProtected = $share->getShareType() !== IShare::TYPE_LINK
  1306. || $share->getShareType() !== IShare::TYPE_EMAIL
  1307. || $share->getShareType() !== IShare::TYPE_CIRCLE;
  1308. if (!$passwordProtected) {
  1309. //TODO maybe exception?
  1310. return false;
  1311. }
  1312. if ($password === null || $share->getPassword() === null) {
  1313. return false;
  1314. }
  1315. $newHash = '';
  1316. if (!$this->hasher->verify($password, $share->getPassword(), $newHash)) {
  1317. return false;
  1318. }
  1319. if (!empty($newHash)) {
  1320. $share->setPassword($newHash);
  1321. $provider = $this->factory->getProviderForType($share->getShareType());
  1322. $provider->update($share);
  1323. }
  1324. return true;
  1325. }
  1326. /**
  1327. * @inheritdoc
  1328. */
  1329. public function userDeleted($uid) {
  1330. $types = [IShare::TYPE_USER, IShare::TYPE_GROUP, IShare::TYPE_LINK, IShare::TYPE_REMOTE, IShare::TYPE_EMAIL];
  1331. foreach ($types as $type) {
  1332. try {
  1333. $provider = $this->factory->getProviderForType($type);
  1334. } catch (ProviderException $e) {
  1335. continue;
  1336. }
  1337. $provider->userDeleted($uid, $type);
  1338. }
  1339. }
  1340. /**
  1341. * @inheritdoc
  1342. */
  1343. public function groupDeleted($gid) {
  1344. $provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
  1345. $provider->groupDeleted($gid);
  1346. $excludedGroups = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
  1347. if ($excludedGroups === '') {
  1348. return;
  1349. }
  1350. $excludedGroups = json_decode($excludedGroups, true);
  1351. if (json_last_error() !== JSON_ERROR_NONE) {
  1352. return;
  1353. }
  1354. $excludedGroups = array_diff($excludedGroups, [$gid]);
  1355. $this->config->setAppValue('core', 'shareapi_exclude_groups_list', json_encode($excludedGroups));
  1356. }
  1357. /**
  1358. * @inheritdoc
  1359. */
  1360. public function userDeletedFromGroup($uid, $gid) {
  1361. $provider = $this->factory->getProviderForType(IShare::TYPE_GROUP);
  1362. $provider->userDeletedFromGroup($uid, $gid);
  1363. }
  1364. /**
  1365. * Get access list to a path. This means
  1366. * all the users that can access a given path.
  1367. *
  1368. * Consider:
  1369. * -root
  1370. * |-folder1 (23)
  1371. * |-folder2 (32)
  1372. * |-fileA (42)
  1373. *
  1374. * fileA is shared with user1 and user1@server1
  1375. * folder2 is shared with group2 (user4 is a member of group2)
  1376. * folder1 is shared with user2 (renamed to "folder (1)") and user2@server2
  1377. *
  1378. * Then the access list to '/folder1/folder2/fileA' with $currentAccess is:
  1379. * [
  1380. * users => [
  1381. * 'user1' => ['node_id' => 42, 'node_path' => '/fileA'],
  1382. * 'user4' => ['node_id' => 32, 'node_path' => '/folder2'],
  1383. * 'user2' => ['node_id' => 23, 'node_path' => '/folder (1)'],
  1384. * ],
  1385. * remote => [
  1386. * 'user1@server1' => ['node_id' => 42, 'token' => 'SeCr3t'],
  1387. * 'user2@server2' => ['node_id' => 23, 'token' => 'FooBaR'],
  1388. * ],
  1389. * public => bool
  1390. * mail => bool
  1391. * ]
  1392. *
  1393. * The access list to '/folder1/folder2/fileA' **without** $currentAccess is:
  1394. * [
  1395. * users => ['user1', 'user2', 'user4'],
  1396. * remote => bool,
  1397. * public => bool
  1398. * mail => bool
  1399. * ]
  1400. *
  1401. * This is required for encryption/activity
  1402. *
  1403. * @param \OCP\Files\Node $path
  1404. * @param bool $recursive Should we check all parent folders as well
  1405. * @param bool $currentAccess Ensure the recipient has access to the file (e.g. did not unshare it)
  1406. * @return array
  1407. */
  1408. public function getAccessList(\OCP\Files\Node $path, $recursive = true, $currentAccess = false) {
  1409. $owner = $path->getOwner();
  1410. if ($owner === null) {
  1411. return [];
  1412. }
  1413. $owner = $owner->getUID();
  1414. if ($currentAccess) {
  1415. $al = ['users' => [], 'remote' => [], 'public' => false];
  1416. } else {
  1417. $al = ['users' => [], 'remote' => false, 'public' => false];
  1418. }
  1419. if (!$this->userManager->userExists($owner)) {
  1420. return $al;
  1421. }
  1422. //Get node for the owner and correct the owner in case of external storages
  1423. $userFolder = $this->rootFolder->getUserFolder($owner);
  1424. if ($path->getId() !== $userFolder->getId() && !$userFolder->isSubNode($path)) {
  1425. $nodes = $userFolder->getById($path->getId());
  1426. $path = array_shift($nodes);
  1427. if ($path->getOwner() === null) {
  1428. return [];
  1429. }
  1430. $owner = $path->getOwner()->getUID();
  1431. }
  1432. $providers = $this->factory->getAllProviders();
  1433. /** @var Node[] $nodes */
  1434. $nodes = [];
  1435. if ($currentAccess) {
  1436. $ownerPath = $path->getPath();
  1437. $ownerPath = explode('/', $ownerPath, 4);
  1438. if (count($ownerPath) < 4) {
  1439. $ownerPath = '';
  1440. } else {
  1441. $ownerPath = $ownerPath[3];
  1442. }
  1443. $al['users'][$owner] = [
  1444. 'node_id' => $path->getId(),
  1445. 'node_path' => '/' . $ownerPath,
  1446. ];
  1447. } else {
  1448. $al['users'][] = $owner;
  1449. }
  1450. // Collect all the shares
  1451. while ($path->getPath() !== $userFolder->getPath()) {
  1452. $nodes[] = $path;
  1453. if (!$recursive) {
  1454. break;
  1455. }
  1456. $path = $path->getParent();
  1457. }
  1458. foreach ($providers as $provider) {
  1459. $tmp = $provider->getAccessList($nodes, $currentAccess);
  1460. foreach ($tmp as $k => $v) {
  1461. if (isset($al[$k])) {
  1462. if (is_array($al[$k])) {
  1463. if ($currentAccess) {
  1464. $al[$k] += $v;
  1465. } else {
  1466. $al[$k] = array_merge($al[$k], $v);
  1467. $al[$k] = array_unique($al[$k]);
  1468. $al[$k] = array_values($al[$k]);
  1469. }
  1470. } else {
  1471. $al[$k] = $al[$k] || $v;
  1472. }
  1473. } else {
  1474. $al[$k] = $v;
  1475. }
  1476. }
  1477. }
  1478. return $al;
  1479. }
  1480. /**
  1481. * Create a new share
  1482. *
  1483. * @return IShare
  1484. */
  1485. public function newShare() {
  1486. return new \OC\Share20\Share($this->rootFolder, $this->userManager);
  1487. }
  1488. /**
  1489. * Is the share API enabled
  1490. *
  1491. * @return bool
  1492. */
  1493. public function shareApiEnabled() {
  1494. return $this->config->getAppValue('core', 'shareapi_enabled', 'yes') === 'yes';
  1495. }
  1496. /**
  1497. * Is public link sharing enabled
  1498. *
  1499. * @return bool
  1500. */
  1501. public function shareApiAllowLinks() {
  1502. return $this->config->getAppValue('core', 'shareapi_allow_links', 'yes') === 'yes';
  1503. }
  1504. /**
  1505. * Is password on public link requires
  1506. *
  1507. * @return bool
  1508. */
  1509. public function shareApiLinkEnforcePassword() {
  1510. return $this->config->getAppValue('core', 'shareapi_enforce_links_password', 'no') === 'yes';
  1511. }
  1512. /**
  1513. * Is default link expire date enabled
  1514. *
  1515. * @return bool
  1516. */
  1517. public function shareApiLinkDefaultExpireDate() {
  1518. return $this->config->getAppValue('core', 'shareapi_default_expire_date', 'no') === 'yes';
  1519. }
  1520. /**
  1521. * Is default link expire date enforced
  1522. *`
  1523. * @return bool
  1524. */
  1525. public function shareApiLinkDefaultExpireDateEnforced() {
  1526. return $this->shareApiLinkDefaultExpireDate() &&
  1527. $this->config->getAppValue('core', 'shareapi_enforce_expire_date', 'no') === 'yes';
  1528. }
  1529. /**
  1530. * Number of default link expire days
  1531. * @return int
  1532. */
  1533. public function shareApiLinkDefaultExpireDays() {
  1534. return (int)$this->config->getAppValue('core', 'shareapi_expire_after_n_days', '7');
  1535. }
  1536. /**
  1537. * Is default internal expire date enabled
  1538. *
  1539. * @return bool
  1540. */
  1541. public function shareApiInternalDefaultExpireDate(): bool {
  1542. return $this->config->getAppValue('core', 'shareapi_default_internal_expire_date', 'no') === 'yes';
  1543. }
  1544. /**
  1545. * Is default expire date enforced
  1546. *`
  1547. * @return bool
  1548. */
  1549. public function shareApiInternalDefaultExpireDateEnforced(): bool {
  1550. return $this->shareApiInternalDefaultExpireDate() &&
  1551. $this->config->getAppValue('core', 'shareapi_enforce_internal_expire_date', 'no') === 'yes';
  1552. }
  1553. /**
  1554. * Number of default expire days
  1555. * @return int
  1556. */
  1557. public function shareApiInternalDefaultExpireDays(): int {
  1558. return (int)$this->config->getAppValue('core', 'shareapi_internal_expire_after_n_days', '7');
  1559. }
  1560. /**
  1561. * Allow public upload on link shares
  1562. *
  1563. * @return bool
  1564. */
  1565. public function shareApiLinkAllowPublicUpload() {
  1566. return $this->config->getAppValue('core', 'shareapi_allow_public_upload', 'yes') === 'yes';
  1567. }
  1568. /**
  1569. * check if user can only share with group members
  1570. * @return bool
  1571. */
  1572. public function shareWithGroupMembersOnly() {
  1573. return $this->config->getAppValue('core', 'shareapi_only_share_with_group_members', 'no') === 'yes';
  1574. }
  1575. /**
  1576. * Check if users can share with groups
  1577. * @return bool
  1578. */
  1579. public function allowGroupSharing() {
  1580. return $this->config->getAppValue('core', 'shareapi_allow_group_sharing', 'yes') === 'yes';
  1581. }
  1582. public function allowEnumeration(): bool {
  1583. return $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
  1584. }
  1585. public function limitEnumerationToGroups(): bool {
  1586. return $this->allowEnumeration() &&
  1587. $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
  1588. }
  1589. /**
  1590. * Copied from \OC_Util::isSharingDisabledForUser
  1591. *
  1592. * TODO: Deprecate fuction from OC_Util
  1593. *
  1594. * @param string $userId
  1595. * @return bool
  1596. */
  1597. public function sharingDisabledForUser($userId) {
  1598. if ($userId === null) {
  1599. return false;
  1600. }
  1601. if (isset($this->sharingDisabledForUsersCache[$userId])) {
  1602. return $this->sharingDisabledForUsersCache[$userId];
  1603. }
  1604. if ($this->config->getAppValue('core', 'shareapi_exclude_groups', 'no') === 'yes') {
  1605. $groupsList = $this->config->getAppValue('core', 'shareapi_exclude_groups_list', '');
  1606. $excludedGroups = json_decode($groupsList);
  1607. if (is_null($excludedGroups)) {
  1608. $excludedGroups = explode(',', $groupsList);
  1609. $newValue = json_encode($excludedGroups);
  1610. $this->config->setAppValue('core', 'shareapi_exclude_groups_list', $newValue);
  1611. }
  1612. $user = $this->userManager->get($userId);
  1613. $usersGroups = $this->groupManager->getUserGroupIds($user);
  1614. if (!empty($usersGroups)) {
  1615. $remainingGroups = array_diff($usersGroups, $excludedGroups);
  1616. // if the user is only in groups which are disabled for sharing then
  1617. // sharing is also disabled for the user
  1618. if (empty($remainingGroups)) {
  1619. $this->sharingDisabledForUsersCache[$userId] = true;
  1620. return true;
  1621. }
  1622. }
  1623. }
  1624. $this->sharingDisabledForUsersCache[$userId] = false;
  1625. return false;
  1626. }
  1627. /**
  1628. * @inheritdoc
  1629. */
  1630. public function outgoingServer2ServerSharesAllowed() {
  1631. return $this->config->getAppValue('files_sharing', 'outgoing_server2server_share_enabled', 'yes') === 'yes';
  1632. }
  1633. /**
  1634. * @inheritdoc
  1635. */
  1636. public function outgoingServer2ServerGroupSharesAllowed() {
  1637. return $this->config->getAppValue('files_sharing', 'outgoing_server2server_group_share_enabled', 'no') === 'yes';
  1638. }
  1639. /**
  1640. * @inheritdoc
  1641. */
  1642. public function shareProviderExists($shareType) {
  1643. try {
  1644. $this->factory->getProviderForType($shareType);
  1645. } catch (ProviderException $e) {
  1646. return false;
  1647. }
  1648. return true;
  1649. }
  1650. public function getAllShares(): iterable {
  1651. $providers = $this->factory->getAllProviders();
  1652. foreach ($providers as $provider) {
  1653. yield from $provider->getAllShares();
  1654. }
  1655. }
  1656. }