1
0

ShareController.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\Files_Sharing\Controller;
  8. use OC\Security\CSP\ContentSecurityPolicy;
  9. use OCA\DAV\Connector\Sabre\PublicAuth;
  10. use OCA\FederatedFileSharing\FederatedShareProvider;
  11. use OCA\Files_Sharing\Activity\Providers\Downloads;
  12. use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
  13. use OCA\Files_Sharing\Event\ShareLinkAccessedEvent;
  14. use OCP\Accounts\IAccountManager;
  15. use OCP\AppFramework\AuthPublicShareController;
  16. use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
  17. use OCP\AppFramework\Http\Attribute\OpenAPI;
  18. use OCP\AppFramework\Http\Attribute\PublicPage;
  19. use OCP\AppFramework\Http\DataResponse;
  20. use OCP\AppFramework\Http\NotFoundResponse;
  21. use OCP\AppFramework\Http\RedirectResponse;
  22. use OCP\AppFramework\Http\Response;
  23. use OCP\AppFramework\Http\TemplateResponse;
  24. use OCP\Constants;
  25. use OCP\Defaults;
  26. use OCP\EventDispatcher\IEventDispatcher;
  27. use OCP\Files\File;
  28. use OCP\Files\Folder;
  29. use OCP\Files\IRootFolder;
  30. use OCP\Files\Node;
  31. use OCP\Files\NotFoundException;
  32. use OCP\HintException;
  33. use OCP\IConfig;
  34. use OCP\IL10N;
  35. use OCP\IPreview;
  36. use OCP\IRequest;
  37. use OCP\ISession;
  38. use OCP\IURLGenerator;
  39. use OCP\IUserManager;
  40. use OCP\Security\Events\GenerateSecurePasswordEvent;
  41. use OCP\Security\ISecureRandom;
  42. use OCP\Security\PasswordContext;
  43. use OCP\Share;
  44. use OCP\Share\Exceptions\ShareNotFound;
  45. use OCP\Share\IManager as ShareManager;
  46. use OCP\Share\IPublicShareTemplateFactory;
  47. use OCP\Share\IShare;
  48. /**
  49. * @package OCA\Files_Sharing\Controllers
  50. */
  51. #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
  52. class ShareController extends AuthPublicShareController {
  53. protected ?IShare $share = null;
  54. public const SHARE_ACCESS = 'access';
  55. public const SHARE_AUTH = 'auth';
  56. public const SHARE_DOWNLOAD = 'download';
  57. public function __construct(
  58. string $appName,
  59. IRequest $request,
  60. protected IConfig $config,
  61. IURLGenerator $urlGenerator,
  62. protected IUserManager $userManager,
  63. protected \OCP\Activity\IManager $activityManager,
  64. protected ShareManager $shareManager,
  65. ISession $session,
  66. protected IPreview $previewManager,
  67. protected IRootFolder $rootFolder,
  68. protected FederatedShareProvider $federatedShareProvider,
  69. protected IAccountManager $accountManager,
  70. protected IEventDispatcher $eventDispatcher,
  71. protected IL10N $l10n,
  72. protected ISecureRandom $secureRandom,
  73. protected Defaults $defaults,
  74. private IPublicShareTemplateFactory $publicShareTemplateFactory,
  75. ) {
  76. parent::__construct($appName, $request, $session, $urlGenerator);
  77. }
  78. /**
  79. * Show the authentication page
  80. * The form has to submit to the authenticate method route
  81. */
  82. #[PublicPage]
  83. #[NoCSRFRequired]
  84. public function showAuthenticate(): TemplateResponse {
  85. $templateParameters = ['share' => $this->share];
  86. $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
  87. $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
  88. if ($this->share->getSendPasswordByTalk()) {
  89. $csp = new ContentSecurityPolicy();
  90. $csp->addAllowedConnectDomain('*');
  91. $csp->addAllowedMediaDomain('blob:');
  92. $response->setContentSecurityPolicy($csp);
  93. }
  94. return $response;
  95. }
  96. /**
  97. * The template to show when authentication failed
  98. */
  99. protected function showAuthFailed(): TemplateResponse {
  100. $templateParameters = ['share' => $this->share, 'wrongpw' => true];
  101. $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
  102. $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
  103. if ($this->share->getSendPasswordByTalk()) {
  104. $csp = new ContentSecurityPolicy();
  105. $csp->addAllowedConnectDomain('*');
  106. $csp->addAllowedMediaDomain('blob:');
  107. $response->setContentSecurityPolicy($csp);
  108. }
  109. return $response;
  110. }
  111. /**
  112. * The template to show after user identification
  113. */
  114. protected function showIdentificationResult(bool $success = false): TemplateResponse {
  115. $templateParameters = ['share' => $this->share, 'identityOk' => $success];
  116. $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
  117. $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
  118. if ($this->share->getSendPasswordByTalk()) {
  119. $csp = new ContentSecurityPolicy();
  120. $csp->addAllowedConnectDomain('*');
  121. $csp->addAllowedMediaDomain('blob:');
  122. $response->setContentSecurityPolicy($csp);
  123. }
  124. return $response;
  125. }
  126. /**
  127. * Validate the identity token of a public share
  128. *
  129. * @param ?string $identityToken
  130. * @return bool
  131. */
  132. protected function validateIdentity(?string $identityToken = null): bool {
  133. if ($this->share->getShareType() !== IShare::TYPE_EMAIL) {
  134. return false;
  135. }
  136. if ($identityToken === null || $this->share->getSharedWith() === null) {
  137. return false;
  138. }
  139. return $identityToken === $this->share->getSharedWith();
  140. }
  141. /**
  142. * Generates a password for the share, respecting any password policy defined
  143. */
  144. protected function generatePassword(): void {
  145. $event = new GenerateSecurePasswordEvent(PasswordContext::SHARING);
  146. $this->eventDispatcher->dispatchTyped($event);
  147. $password = $event->getPassword() ?? $this->secureRandom->generate(20);
  148. $this->share->setPassword($password);
  149. $this->shareManager->updateShare($this->share);
  150. }
  151. protected function verifyPassword(string $password): bool {
  152. return $this->shareManager->checkPassword($this->share, $password);
  153. }
  154. protected function getPasswordHash(): ?string {
  155. return $this->share->getPassword();
  156. }
  157. public function isValidToken(): bool {
  158. try {
  159. $this->share = $this->shareManager->getShareByToken($this->getToken());
  160. } catch (ShareNotFound $e) {
  161. return false;
  162. }
  163. return true;
  164. }
  165. protected function isPasswordProtected(): bool {
  166. return $this->share->getPassword() !== null;
  167. }
  168. protected function authSucceeded() {
  169. if ($this->share === null) {
  170. throw new NotFoundException();
  171. }
  172. // For share this was always set so it is still used in other apps
  173. $this->session->set(PublicAuth::DAV_AUTHENTICATED, $this->share->getId());
  174. }
  175. protected function authFailed() {
  176. $this->emitAccessShareHook($this->share, 403, 'Wrong password');
  177. $this->emitShareAccessEvent($this->share, self::SHARE_AUTH, 403, 'Wrong password');
  178. }
  179. /**
  180. * throws hooks when a share is attempted to be accessed
  181. *
  182. * @param IShare|string $share the Share instance if available,
  183. * otherwise token
  184. * @param int $errorCode
  185. * @param string $errorMessage
  186. *
  187. * @throws HintException
  188. * @throws \OC\ServerNotAvailableException
  189. *
  190. * @deprecated use OCP\Files_Sharing\Event\ShareLinkAccessedEvent
  191. */
  192. protected function emitAccessShareHook($share, int $errorCode = 200, string $errorMessage = '') {
  193. $itemType = $itemSource = $uidOwner = '';
  194. $token = $share;
  195. $exception = null;
  196. if ($share instanceof IShare) {
  197. try {
  198. $token = $share->getToken();
  199. $uidOwner = $share->getSharedBy();
  200. $itemType = $share->getNodeType();
  201. $itemSource = $share->getNodeId();
  202. } catch (\Exception $e) {
  203. // we log what we know and pass on the exception afterwards
  204. $exception = $e;
  205. }
  206. }
  207. \OC_Hook::emit(Share::class, 'share_link_access', [
  208. 'itemType' => $itemType,
  209. 'itemSource' => $itemSource,
  210. 'uidOwner' => $uidOwner,
  211. 'token' => $token,
  212. 'errorCode' => $errorCode,
  213. 'errorMessage' => $errorMessage
  214. ]);
  215. if (!is_null($exception)) {
  216. throw $exception;
  217. }
  218. }
  219. /**
  220. * Emit a ShareLinkAccessedEvent event when a share is accessed, downloaded, auth...
  221. */
  222. protected function emitShareAccessEvent(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = ''): void {
  223. if ($step !== self::SHARE_ACCESS &&
  224. $step !== self::SHARE_AUTH &&
  225. $step !== self::SHARE_DOWNLOAD) {
  226. return;
  227. }
  228. $this->eventDispatcher->dispatchTyped(new ShareLinkAccessedEvent($share, $step, $errorCode, $errorMessage));
  229. }
  230. /**
  231. * Validate the permissions of the share
  232. *
  233. * @param Share\IShare $share
  234. * @return bool
  235. */
  236. private function validateShare(IShare $share) {
  237. // If the owner is disabled no access to the link is granted
  238. $owner = $this->userManager->get($share->getShareOwner());
  239. if ($owner === null || !$owner->isEnabled()) {
  240. return false;
  241. }
  242. // If the initiator of the share is disabled no access is granted
  243. $initiator = $this->userManager->get($share->getSharedBy());
  244. if ($initiator === null || !$initiator->isEnabled()) {
  245. return false;
  246. }
  247. return $share->getNode()->isReadable() && $share->getNode()->isShareable();
  248. }
  249. /**
  250. * @param string $path
  251. * @return TemplateResponse
  252. * @throws NotFoundException
  253. * @throws \Exception
  254. */
  255. #[PublicPage]
  256. #[NoCSRFRequired]
  257. public function showShare($path = ''): TemplateResponse {
  258. \OC_User::setIncognitoMode(true);
  259. // Check whether share exists
  260. try {
  261. $share = $this->shareManager->getShareByToken($this->getToken());
  262. } catch (ShareNotFound $e) {
  263. // The share does not exists, we do not emit an ShareLinkAccessedEvent
  264. $this->emitAccessShareHook($this->getToken(), 404, 'Share not found');
  265. throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
  266. }
  267. if (!$this->validateShare($share)) {
  268. throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
  269. }
  270. $shareNode = $share->getNode();
  271. try {
  272. $templateProvider = $this->publicShareTemplateFactory->getProvider($share);
  273. $response = $templateProvider->renderPage($share, $this->getToken(), $path);
  274. } catch (NotFoundException $e) {
  275. $this->emitAccessShareHook($share, 404, 'Share not found');
  276. $this->emitShareAccessEvent($share, ShareController::SHARE_ACCESS, 404, 'Share not found');
  277. throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
  278. }
  279. // We can't get the path of a file share
  280. try {
  281. if ($shareNode instanceof File && $path !== '') {
  282. $this->emitAccessShareHook($share, 404, 'Share not found');
  283. $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
  284. throw new NotFoundException($this->l10n->t('This share does not exist or is no longer available'));
  285. }
  286. } catch (\Exception $e) {
  287. $this->emitAccessShareHook($share, 404, 'Share not found');
  288. $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
  289. throw $e;
  290. }
  291. $this->emitAccessShareHook($share);
  292. $this->emitShareAccessEvent($share, self::SHARE_ACCESS);
  293. return $response;
  294. }
  295. /**
  296. * @NoSameSiteCookieRequired
  297. *
  298. * @param string $token
  299. * @param string|null $files
  300. * @param string $path
  301. * @return void|Response
  302. * @throws NotFoundException
  303. * @deprecated 31.0.0 Users are encouraged to use the DAV endpoint
  304. */
  305. #[PublicPage]
  306. #[NoCSRFRequired]
  307. public function downloadShare($token, $files = null, $path = '') {
  308. \OC_User::setIncognitoMode(true);
  309. $share = $this->shareManager->getShareByToken($token);
  310. if (!($share->getPermissions() & Constants::PERMISSION_READ)) {
  311. return new DataResponse('Share has no read permission');
  312. }
  313. if (!$this->validateShare($share)) {
  314. throw new NotFoundException();
  315. }
  316. // Single file share
  317. if ($share->getNode() instanceof File) {
  318. // Single file download
  319. $this->singleFileDownloaded($share, $share->getNode());
  320. }
  321. // Directory share
  322. else {
  323. /** @var Folder $node */
  324. $node = $share->getNode();
  325. // Try to get the path
  326. if ($path !== '') {
  327. try {
  328. $node = $node->get($path);
  329. } catch (NotFoundException $e) {
  330. $this->emitAccessShareHook($share, 404, 'Share not found');
  331. $this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD, 404, 'Share not found');
  332. return new NotFoundResponse();
  333. }
  334. }
  335. if ($node instanceof Folder) {
  336. if ($files === null || $files === '') {
  337. // The folder is downloaded
  338. $this->singleFileDownloaded($share, $share->getNode());
  339. } else {
  340. $fileList = json_decode($files);
  341. // in case we get only a single file
  342. if (!is_array($fileList)) {
  343. $fileList = [$fileList];
  344. }
  345. foreach ($fileList as $file) {
  346. $subNode = $node->get($file);
  347. $this->singleFileDownloaded($share, $subNode);
  348. }
  349. }
  350. } else {
  351. // Single file download
  352. $this->singleFileDownloaded($share, $share->getNode());
  353. }
  354. }
  355. $this->emitAccessShareHook($share);
  356. $this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD);
  357. $davUrl = '/public.php/dav/files/' . $token . '/?accept=zip';
  358. if ($files !== null) {
  359. $davUrl .= '&files=' . $files;
  360. }
  361. return new RedirectResponse($this->urlGenerator->getAbsoluteURL($davUrl));
  362. }
  363. /**
  364. * create activity if a single file was downloaded from a link share
  365. *
  366. * @param Share\IShare $share
  367. * @throws NotFoundException when trying to download a folder of a "hide download" share
  368. */
  369. protected function singleFileDownloaded(IShare $share, Node $node) {
  370. if ($share->getHideDownload() && $node instanceof Folder) {
  371. throw new NotFoundException('Downloading a folder');
  372. }
  373. $fileId = $node->getId();
  374. $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
  375. $userNode = $userFolder->getFirstNodeById($fileId);
  376. $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
  377. $userPath = $userFolder->getRelativePath($userNode->getPath());
  378. $ownerPath = $ownerFolder->getRelativePath($node->getPath());
  379. $remoteAddress = $this->request->getRemoteAddress();
  380. $dateTime = new \DateTime();
  381. $dateTime = $dateTime->format('Y-m-d H');
  382. $remoteAddressHash = md5($dateTime . '-' . $remoteAddress);
  383. $parameters = [$userPath];
  384. if ($share->getShareType() === IShare::TYPE_EMAIL) {
  385. if ($node instanceof File) {
  386. $subject = Downloads::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED;
  387. } else {
  388. $subject = Downloads::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED;
  389. }
  390. $parameters[] = $share->getSharedWith();
  391. } else {
  392. if ($node instanceof File) {
  393. $subject = Downloads::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED;
  394. $parameters[] = $remoteAddressHash;
  395. } else {
  396. $subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED;
  397. $parameters[] = $remoteAddressHash;
  398. }
  399. }
  400. $this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath);
  401. if ($share->getShareOwner() !== $share->getSharedBy()) {
  402. $parameters[0] = $ownerPath;
  403. $this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath);
  404. }
  405. }
  406. /**
  407. * publish activity
  408. *
  409. * @param string $subject
  410. * @param array $parameters
  411. * @param string $affectedUser
  412. * @param int $fileId
  413. * @param string $filePath
  414. */
  415. protected function publishActivity($subject,
  416. array $parameters,
  417. $affectedUser,
  418. $fileId,
  419. $filePath) {
  420. $event = $this->activityManager->generateEvent();
  421. $event->setApp('files_sharing')
  422. ->setType('public_links')
  423. ->setSubject($subject, $parameters)
  424. ->setAffectedUser($affectedUser)
  425. ->setObject('files', $fileId, $filePath);
  426. $this->activityManager->publish($event);
  427. }
  428. }