ShareController.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  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 Georg Ehrke <oc.list@georgehrke.com>
  11. * @author j3l11234 <297259024@qq.com>
  12. * @author Joas Schilling <coding@schilljs.com>
  13. * @author John Molakvoæ <skjnldsv@protonmail.com>
  14. * @author Jonas Sulzer <jonas@violoncello.ch>
  15. * @author Julius Härtl <jus@bitgrid.net>
  16. * @author Lukas Reschke <lukas@statuscode.ch>
  17. * @author MartB <mart.b@outlook.de>
  18. * @author Maxence Lange <maxence@pontapreta.net>
  19. * @author Michael Weimann <mail@michael-weimann.eu>
  20. * @author Morris Jobke <hey@morrisjobke.de>
  21. * @author Piotr Filiciak <piotr@filiciak.pl>
  22. * @author Robin Appelman <robin@icewind.nl>
  23. * @author Roeland Jago Douma <roeland@famdouma.nl>
  24. * @author Sascha Sambale <mastixmc@gmail.com>
  25. * @author Thomas Müller <thomas.mueller@tmit.eu>
  26. * @author Vincent Petry <vincent@nextcloud.com>
  27. * @author Kate Döen <kate.doeen@nextcloud.com>
  28. *
  29. * @license AGPL-3.0
  30. *
  31. * This code is free software: you can redistribute it and/or modify
  32. * it under the terms of the GNU Affero General Public License, version 3,
  33. * as published by the Free Software Foundation.
  34. *
  35. * This program is distributed in the hope that it will be useful,
  36. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  37. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  38. * GNU Affero General Public License for more details.
  39. *
  40. * You should have received a copy of the GNU Affero General Public License, version 3,
  41. * along with this program. If not, see <http://www.gnu.org/licenses/>
  42. *
  43. */
  44. namespace OCA\Files_Sharing\Controller;
  45. use OC\Security\CSP\ContentSecurityPolicy;
  46. use OC_Files;
  47. use OC_Util;
  48. use OCA\DAV\Connector\Sabre\PublicAuth;
  49. use OCA\FederatedFileSharing\FederatedShareProvider;
  50. use OCA\Files_Sharing\Activity\Providers\Downloads;
  51. use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
  52. use OCA\Files_Sharing\Event\ShareLinkAccessedEvent;
  53. use OCP\Accounts\IAccountManager;
  54. use OCP\AppFramework\AuthPublicShareController;
  55. use OCP\AppFramework\Http\Attribute\OpenAPI;
  56. use OCP\AppFramework\Http\NotFoundResponse;
  57. use OCP\AppFramework\Http\TemplateResponse;
  58. use OCP\Defaults;
  59. use OCP\EventDispatcher\IEventDispatcher;
  60. use OCP\Files\Folder;
  61. use OCP\Files\IRootFolder;
  62. use OCP\Files\NotFoundException;
  63. use OCP\IConfig;
  64. use OCP\IL10N;
  65. use OCP\IPreview;
  66. use OCP\IRequest;
  67. use OCP\ISession;
  68. use OCP\IURLGenerator;
  69. use OCP\IUserManager;
  70. use OCP\Security\ISecureRandom;
  71. use OCP\Share;
  72. use OCP\Share\Exceptions\ShareNotFound;
  73. use OCP\Share\IManager as ShareManager;
  74. use OCP\Share\IPublicShareTemplateFactory;
  75. use OCP\Share\IShare;
  76. use OCP\Template;
  77. /**
  78. * @package OCA\Files_Sharing\Controllers
  79. */
  80. #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
  81. class ShareController extends AuthPublicShareController {
  82. protected ?Share\IShare $share = null;
  83. public const SHARE_ACCESS = 'access';
  84. public const SHARE_AUTH = 'auth';
  85. public const SHARE_DOWNLOAD = 'download';
  86. public function __construct(
  87. string $appName,
  88. IRequest $request,
  89. protected IConfig $config,
  90. IURLGenerator $urlGenerator,
  91. protected IUserManager $userManager,
  92. protected \OCP\Activity\IManager $activityManager,
  93. protected ShareManager $shareManager,
  94. ISession $session,
  95. protected IPreview $previewManager,
  96. protected IRootFolder $rootFolder,
  97. protected FederatedShareProvider $federatedShareProvider,
  98. protected IAccountManager $accountManager,
  99. protected IEventDispatcher $eventDispatcher,
  100. protected IL10N $l10n,
  101. protected ISecureRandom $secureRandom,
  102. protected Defaults $defaults,
  103. private IPublicShareTemplateFactory $publicShareTemplateFactory,
  104. ) {
  105. parent::__construct($appName, $request, $session, $urlGenerator);
  106. }
  107. /**
  108. * @PublicPage
  109. * @NoCSRFRequired
  110. *
  111. * Show the authentication page
  112. * The form has to submit to the authenticate method route
  113. */
  114. public function showAuthenticate(): TemplateResponse {
  115. $templateParameters = ['share' => $this->share];
  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. * The template to show when authentication failed
  128. */
  129. protected function showAuthFailed(): TemplateResponse {
  130. $templateParameters = ['share' => $this->share, 'wrongpw' => true];
  131. $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
  132. $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
  133. if ($this->share->getSendPasswordByTalk()) {
  134. $csp = new ContentSecurityPolicy();
  135. $csp->addAllowedConnectDomain('*');
  136. $csp->addAllowedMediaDomain('blob:');
  137. $response->setContentSecurityPolicy($csp);
  138. }
  139. return $response;
  140. }
  141. /**
  142. * The template to show after user identification
  143. */
  144. protected function showIdentificationResult(bool $success = false): TemplateResponse {
  145. $templateParameters = ['share' => $this->share, 'identityOk' => $success];
  146. $this->eventDispatcher->dispatchTyped(new BeforeTemplateRenderedEvent($this->share, BeforeTemplateRenderedEvent::SCOPE_PUBLIC_SHARE_AUTH));
  147. $response = new TemplateResponse('core', 'publicshareauth', $templateParameters, 'guest');
  148. if ($this->share->getSendPasswordByTalk()) {
  149. $csp = new ContentSecurityPolicy();
  150. $csp->addAllowedConnectDomain('*');
  151. $csp->addAllowedMediaDomain('blob:');
  152. $response->setContentSecurityPolicy($csp);
  153. }
  154. return $response;
  155. }
  156. /**
  157. * Validate the identity token of a public share
  158. *
  159. * @param ?string $identityToken
  160. * @return bool
  161. */
  162. protected function validateIdentity(?string $identityToken = null): bool {
  163. if ($this->share->getShareType() !== IShare::TYPE_EMAIL) {
  164. return false;
  165. }
  166. if ($identityToken === null || $this->share->getSharedWith() === null) {
  167. return false;
  168. }
  169. return $identityToken === $this->share->getSharedWith();
  170. }
  171. /**
  172. * Generates a password for the share, respecting any password policy defined
  173. */
  174. protected function generatePassword(): void {
  175. $event = new \OCP\Security\Events\GenerateSecurePasswordEvent();
  176. $this->eventDispatcher->dispatchTyped($event);
  177. $password = $event->getPassword() ?? $this->secureRandom->generate(20);
  178. $this->share->setPassword($password);
  179. $this->shareManager->updateShare($this->share);
  180. }
  181. protected function verifyPassword(string $password): bool {
  182. return $this->shareManager->checkPassword($this->share, $password);
  183. }
  184. protected function getPasswordHash(): ?string {
  185. return $this->share->getPassword();
  186. }
  187. public function isValidToken(): bool {
  188. try {
  189. $this->share = $this->shareManager->getShareByToken($this->getToken());
  190. } catch (ShareNotFound $e) {
  191. return false;
  192. }
  193. return true;
  194. }
  195. protected function isPasswordProtected(): bool {
  196. return $this->share->getPassword() !== null;
  197. }
  198. protected function authSucceeded() {
  199. if ($this->share === null) {
  200. throw new NotFoundException();
  201. }
  202. // For share this was always set so it is still used in other apps
  203. $this->session->set(PublicAuth::DAV_AUTHENTICATED, $this->share->getId());
  204. }
  205. protected function authFailed() {
  206. $this->emitAccessShareHook($this->share, 403, 'Wrong password');
  207. $this->emitShareAccessEvent($this->share, self::SHARE_AUTH, 403, 'Wrong password');
  208. }
  209. /**
  210. * throws hooks when a share is attempted to be accessed
  211. *
  212. * @param \OCP\Share\IShare|string $share the Share instance if available,
  213. * otherwise token
  214. * @param int $errorCode
  215. * @param string $errorMessage
  216. *
  217. * @throws \OCP\HintException
  218. * @throws \OC\ServerNotAvailableException
  219. *
  220. * @deprecated use OCP\Files_Sharing\Event\ShareLinkAccessedEvent
  221. */
  222. protected function emitAccessShareHook($share, int $errorCode = 200, string $errorMessage = '') {
  223. $itemType = $itemSource = $uidOwner = '';
  224. $token = $share;
  225. $exception = null;
  226. if ($share instanceof \OCP\Share\IShare) {
  227. try {
  228. $token = $share->getToken();
  229. $uidOwner = $share->getSharedBy();
  230. $itemType = $share->getNodeType();
  231. $itemSource = $share->getNodeId();
  232. } catch (\Exception $e) {
  233. // we log what we know and pass on the exception afterwards
  234. $exception = $e;
  235. }
  236. }
  237. \OC_Hook::emit(Share::class, 'share_link_access', [
  238. 'itemType' => $itemType,
  239. 'itemSource' => $itemSource,
  240. 'uidOwner' => $uidOwner,
  241. 'token' => $token,
  242. 'errorCode' => $errorCode,
  243. 'errorMessage' => $errorMessage
  244. ]);
  245. if (!is_null($exception)) {
  246. throw $exception;
  247. }
  248. }
  249. /**
  250. * Emit a ShareLinkAccessedEvent event when a share is accessed, downloaded, auth...
  251. */
  252. protected function emitShareAccessEvent(IShare $share, string $step = '', int $errorCode = 200, string $errorMessage = ''): void {
  253. if ($step !== self::SHARE_ACCESS &&
  254. $step !== self::SHARE_AUTH &&
  255. $step !== self::SHARE_DOWNLOAD) {
  256. return;
  257. }
  258. $this->eventDispatcher->dispatchTyped(new ShareLinkAccessedEvent($share, $step, $errorCode, $errorMessage));
  259. }
  260. /**
  261. * Validate the permissions of the share
  262. *
  263. * @param Share\IShare $share
  264. * @return bool
  265. */
  266. private function validateShare(\OCP\Share\IShare $share) {
  267. // If the owner is disabled no access to the link is granted
  268. $owner = $this->userManager->get($share->getShareOwner());
  269. if ($owner === null || !$owner->isEnabled()) {
  270. return false;
  271. }
  272. // If the initiator of the share is disabled no access is granted
  273. $initiator = $this->userManager->get($share->getSharedBy());
  274. if ($initiator === null || !$initiator->isEnabled()) {
  275. return false;
  276. }
  277. return $share->getNode()->isReadable() && $share->getNode()->isShareable();
  278. }
  279. /**
  280. * @PublicPage
  281. * @NoCSRFRequired
  282. *
  283. *
  284. * @param string $path
  285. * @return TemplateResponse
  286. * @throws NotFoundException
  287. * @throws \Exception
  288. */
  289. public function showShare($path = ''): TemplateResponse {
  290. \OC_User::setIncognitoMode(true);
  291. // Check whether share exists
  292. try {
  293. $share = $this->shareManager->getShareByToken($this->getToken());
  294. } catch (ShareNotFound $e) {
  295. // The share does not exists, we do not emit an ShareLinkAccessedEvent
  296. $this->emitAccessShareHook($this->getToken(), 404, 'Share not found');
  297. throw new NotFoundException();
  298. }
  299. if (!$this->validateShare($share)) {
  300. throw new NotFoundException();
  301. }
  302. $shareNode = $share->getNode();
  303. try {
  304. $templateProvider = $this->publicShareTemplateFactory->getProvider($share);
  305. $response = $templateProvider->renderPage($share, $this->getToken(), $path);
  306. } catch (NotFoundException $e) {
  307. $this->emitAccessShareHook($share, 404, 'Share not found');
  308. $this->emitShareAccessEvent($share, ShareController::SHARE_ACCESS, 404, 'Share not found');
  309. throw new NotFoundException();
  310. }
  311. // We can't get the path of a file share
  312. try {
  313. if ($shareNode instanceof \OCP\Files\File && $path !== '') {
  314. $this->emitAccessShareHook($share, 404, 'Share not found');
  315. $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
  316. throw new NotFoundException();
  317. }
  318. } catch (\Exception $e) {
  319. $this->emitAccessShareHook($share, 404, 'Share not found');
  320. $this->emitShareAccessEvent($share, self::SHARE_ACCESS, 404, 'Share not found');
  321. throw $e;
  322. }
  323. $this->emitAccessShareHook($share);
  324. $this->emitShareAccessEvent($share, self::SHARE_ACCESS);
  325. return $response;
  326. }
  327. /**
  328. * @PublicPage
  329. * @NoCSRFRequired
  330. * @NoSameSiteCookieRequired
  331. *
  332. * @param string $token
  333. * @param string $files
  334. * @param string $path
  335. * @param string $downloadStartSecret
  336. * @return void|\OCP\AppFramework\Http\Response
  337. * @throws NotFoundException
  338. */
  339. public function downloadShare($token, $files = null, $path = '', $downloadStartSecret = '') {
  340. \OC_User::setIncognitoMode(true);
  341. $share = $this->shareManager->getShareByToken($token);
  342. if (!($share->getPermissions() & \OCP\Constants::PERMISSION_READ)) {
  343. return new \OCP\AppFramework\Http\DataResponse('Share has no read permission');
  344. }
  345. $files_list = null;
  346. if (!is_null($files)) { // download selected files
  347. $files_list = json_decode($files);
  348. // in case we get only a single file
  349. if ($files_list === null) {
  350. $files_list = [$files];
  351. }
  352. // Just in case $files is a single int like '1234'
  353. if (!is_array($files_list)) {
  354. $files_list = [$files_list];
  355. }
  356. }
  357. if (!$this->validateShare($share)) {
  358. throw new NotFoundException();
  359. }
  360. $userFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
  361. $originalSharePath = $userFolder->getRelativePath($share->getNode()->getPath());
  362. // Single file share
  363. if ($share->getNode() instanceof \OCP\Files\File) {
  364. // Single file download
  365. $this->singleFileDownloaded($share, $share->getNode());
  366. }
  367. // Directory share
  368. else {
  369. /** @var \OCP\Files\Folder $node */
  370. $node = $share->getNode();
  371. // Try to get the path
  372. if ($path !== '') {
  373. try {
  374. $node = $node->get($path);
  375. } catch (NotFoundException $e) {
  376. $this->emitAccessShareHook($share, 404, 'Share not found');
  377. $this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD, 404, 'Share not found');
  378. return new NotFoundResponse();
  379. }
  380. }
  381. $originalSharePath = $userFolder->getRelativePath($node->getPath());
  382. if ($node instanceof \OCP\Files\File) {
  383. // Single file download
  384. $this->singleFileDownloaded($share, $share->getNode());
  385. } else {
  386. try {
  387. if (!empty($files_list)) {
  388. $this->fileListDownloaded($share, $files_list, $node);
  389. } else {
  390. // The folder is downloaded
  391. $this->singleFileDownloaded($share, $share->getNode());
  392. }
  393. } catch (NotFoundException $e) {
  394. return new NotFoundResponse();
  395. }
  396. }
  397. }
  398. /* FIXME: We should do this all nicely in OCP */
  399. OC_Util::tearDownFS();
  400. OC_Util::setupFS($share->getShareOwner());
  401. /**
  402. * this sets a cookie to be able to recognize the start of the download
  403. * the content must not be longer than 32 characters and must only contain
  404. * alphanumeric characters
  405. */
  406. if (!empty($downloadStartSecret)
  407. && !isset($downloadStartSecret[32])
  408. && preg_match('!^[a-zA-Z0-9]+$!', $downloadStartSecret) === 1) {
  409. // FIXME: set on the response once we use an actual app framework response
  410. setcookie('ocDownloadStarted', $downloadStartSecret, time() + 20, '/');
  411. }
  412. $this->emitAccessShareHook($share);
  413. $this->emitShareAccessEvent($share, self::SHARE_DOWNLOAD);
  414. $server_params = [ 'head' => $this->request->getMethod() === 'HEAD' ];
  415. /**
  416. * Http range requests support
  417. */
  418. if (isset($_SERVER['HTTP_RANGE'])) {
  419. $server_params['range'] = $this->request->getHeader('Range');
  420. }
  421. // download selected files
  422. if (!is_null($files) && $files !== '') {
  423. // FIXME: The exit is required here because otherwise the AppFramework is trying to add headers as well
  424. // after dispatching the request which results in a "Cannot modify header information" notice.
  425. OC_Files::get($originalSharePath, $files_list, $server_params);
  426. exit();
  427. } else {
  428. // FIXME: The exit is required here because otherwise the AppFramework is trying to add headers as well
  429. // after dispatching the request which results in a "Cannot modify header information" notice.
  430. OC_Files::get(dirname($originalSharePath), basename($originalSharePath), $server_params);
  431. exit();
  432. }
  433. }
  434. /**
  435. * create activity for every downloaded file
  436. *
  437. * @param Share\IShare $share
  438. * @param array $files_list
  439. * @param \OCP\Files\Folder $node
  440. * @throws NotFoundException when trying to download a folder or multiple files of a "hide download" share
  441. */
  442. protected function fileListDownloaded(Share\IShare $share, array $files_list, \OCP\Files\Folder $node) {
  443. if ($share->getHideDownload() && count($files_list) > 1) {
  444. throw new NotFoundException('Downloading more than 1 file');
  445. }
  446. foreach ($files_list as $file) {
  447. $subNode = $node->get($file);
  448. $this->singleFileDownloaded($share, $subNode);
  449. }
  450. }
  451. /**
  452. * create activity if a single file was downloaded from a link share
  453. *
  454. * @param Share\IShare $share
  455. * @throws NotFoundException when trying to download a folder of a "hide download" share
  456. */
  457. protected function singleFileDownloaded(Share\IShare $share, \OCP\Files\Node $node) {
  458. if ($share->getHideDownload() && $node instanceof Folder) {
  459. throw new NotFoundException('Downloading a folder');
  460. }
  461. $fileId = $node->getId();
  462. $userFolder = $this->rootFolder->getUserFolder($share->getSharedBy());
  463. $userNode = $userFolder->getFirstNodeById($fileId);
  464. $ownerFolder = $this->rootFolder->getUserFolder($share->getShareOwner());
  465. $userPath = $userFolder->getRelativePath($userNode->getPath());
  466. $ownerPath = $ownerFolder->getRelativePath($node->getPath());
  467. $remoteAddress = $this->request->getRemoteAddress();
  468. $dateTime = new \DateTime();
  469. $dateTime = $dateTime->format('Y-m-d H');
  470. $remoteAddressHash = md5($dateTime . '-' . $remoteAddress);
  471. $parameters = [$userPath];
  472. if ($share->getShareType() === IShare::TYPE_EMAIL) {
  473. if ($node instanceof \OCP\Files\File) {
  474. $subject = Downloads::SUBJECT_SHARED_FILE_BY_EMAIL_DOWNLOADED;
  475. } else {
  476. $subject = Downloads::SUBJECT_SHARED_FOLDER_BY_EMAIL_DOWNLOADED;
  477. }
  478. $parameters[] = $share->getSharedWith();
  479. } else {
  480. if ($node instanceof \OCP\Files\File) {
  481. $subject = Downloads::SUBJECT_PUBLIC_SHARED_FILE_DOWNLOADED;
  482. $parameters[] = $remoteAddressHash;
  483. } else {
  484. $subject = Downloads::SUBJECT_PUBLIC_SHARED_FOLDER_DOWNLOADED;
  485. $parameters[] = $remoteAddressHash;
  486. }
  487. }
  488. $this->publishActivity($subject, $parameters, $share->getSharedBy(), $fileId, $userPath);
  489. if ($share->getShareOwner() !== $share->getSharedBy()) {
  490. $parameters[0] = $ownerPath;
  491. $this->publishActivity($subject, $parameters, $share->getShareOwner(), $fileId, $ownerPath);
  492. }
  493. }
  494. /**
  495. * publish activity
  496. *
  497. * @param string $subject
  498. * @param array $parameters
  499. * @param string $affectedUser
  500. * @param int $fileId
  501. * @param string $filePath
  502. */
  503. protected function publishActivity($subject,
  504. array $parameters,
  505. $affectedUser,
  506. $fileId,
  507. $filePath) {
  508. $event = $this->activityManager->generateEvent();
  509. $event->setApp('files_sharing')
  510. ->setType('public_links')
  511. ->setSubject($subject, $parameters)
  512. ->setAffectedUser($affectedUser)
  513. ->setObject('files', $fileId, $filePath);
  514. $this->activityManager->publish($event);
  515. }
  516. }