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.

206 lines
6.9 KiB

  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
  5. * SPDX-License-Identifier: AGPL-3.0-or-later
  6. */
  7. namespace OC\Core\Controller;
  8. use OCP\AppFramework\Controller;
  9. use OCP\AppFramework\Http;
  10. use OCP\AppFramework\Http\Attribute\FrontpageRoute;
  11. use OCP\AppFramework\Http\Attribute\NoAdminRequired;
  12. use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
  13. use OCP\AppFramework\Http\Attribute\OpenAPI;
  14. use OCP\AppFramework\Http\Attribute\PublicPage;
  15. use OCP\AppFramework\Http\DataResponse;
  16. use OCP\AppFramework\Http\FileDisplayResponse;
  17. use OCP\AppFramework\Http\RedirectResponse;
  18. use OCP\AppFramework\Http\Response;
  19. use OCP\Files\File;
  20. use OCP\Files\IRootFolder;
  21. use OCP\Files\Node;
  22. use OCP\Files\NotFoundException;
  23. use OCP\Files\Storage\ISharedStorage;
  24. use OCP\IPreview;
  25. use OCP\IRequest;
  26. use OCP\Preview\IMimeIconProvider;
  27. class PreviewController extends Controller {
  28. public function __construct(
  29. string $appName,
  30. IRequest $request,
  31. private IPreview $preview,
  32. private IRootFolder $root,
  33. private ?string $userId,
  34. private IMimeIconProvider $mimeIconProvider,
  35. ) {
  36. parent::__construct($appName, $request);
  37. }
  38. /**
  39. * Get a preview by file path
  40. *
  41. * @param string $file Path of the file
  42. * @param int $x Width of the preview. A width of -1 will use the original image width.
  43. * @param int $y Height of the preview. A height of -1 will use the original image height.
  44. * @param bool $a Preserve the aspect ratio
  45. * @param bool $forceIcon Force returning an icon
  46. * @param 'fill'|'cover' $mode How to crop the image
  47. * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available
  48. * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
  49. *
  50. * 200: Preview returned
  51. * 303: Redirect to the mime icon url if mimeFallback is true
  52. * 400: Getting preview is not possible
  53. * 403: Getting preview is not allowed
  54. * 404: Preview not found
  55. */
  56. #[NoAdminRequired]
  57. #[NoCSRFRequired]
  58. #[FrontpageRoute(verb: 'GET', url: '/core/preview.png')]
  59. #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
  60. public function getPreview(
  61. string $file = '',
  62. int $x = 32,
  63. int $y = 32,
  64. bool $a = false,
  65. bool $forceIcon = true,
  66. string $mode = 'fill',
  67. bool $mimeFallback = false): Response {
  68. if ($file === '' || $x === 0 || $y === 0) {
  69. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  70. }
  71. try {
  72. $userFolder = $this->root->getUserFolder($this->userId);
  73. $node = $userFolder->get($file);
  74. } catch (NotFoundException $e) {
  75. return new DataResponse([], Http::STATUS_NOT_FOUND);
  76. }
  77. return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
  78. }
  79. /**
  80. * Get a preview by file ID
  81. *
  82. * @param int $fileId ID of the file
  83. * @param int $x Width of the preview. A width of -1 will use the original image width.
  84. * @param int $y Height of the preview. A height of -1 will use the original image height.
  85. * @param bool $a Preserve the aspect ratio
  86. * @param bool $forceIcon Force returning an icon
  87. * @param 'fill'|'cover' $mode How to crop the image
  88. * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available
  89. * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
  90. *
  91. * 200: Preview returned
  92. * 303: Redirect to the mime icon url if mimeFallback is true
  93. * 400: Getting preview is not possible
  94. * 403: Getting preview is not allowed
  95. * 404: Preview not found
  96. */
  97. #[NoAdminRequired]
  98. #[NoCSRFRequired]
  99. #[FrontpageRoute(verb: 'GET', url: '/core/preview')]
  100. #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
  101. public function getPreviewByFileId(
  102. int $fileId = -1,
  103. int $x = 32,
  104. int $y = 32,
  105. bool $a = false,
  106. bool $forceIcon = true,
  107. string $mode = 'fill',
  108. bool $mimeFallback = false) {
  109. if ($fileId === -1 || $x === 0 || $y === 0) {
  110. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  111. }
  112. $userFolder = $this->root->getUserFolder($this->userId);
  113. $node = $userFolder->getFirstNodeById($fileId);
  114. if (!$node) {
  115. return new DataResponse([], Http::STATUS_NOT_FOUND);
  116. }
  117. return $this->fetchPreview($node, $x, $y, $a, $forceIcon, $mode, $mimeFallback);
  118. }
  119. /**
  120. * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
  121. */
  122. private function fetchPreview(
  123. Node $node,
  124. int $x,
  125. int $y,
  126. bool $a,
  127. bool $forceIcon,
  128. string $mode,
  129. bool $mimeFallback = false) : Response {
  130. if (!($node instanceof File) || (!$forceIcon && !$this->preview->isAvailable($node))) {
  131. return new DataResponse([], Http::STATUS_NOT_FOUND);
  132. }
  133. if (!$node->isReadable()) {
  134. return new DataResponse([], Http::STATUS_FORBIDDEN);
  135. }
  136. if ($node->getId() <= 0) {
  137. return new DataResponse([], Http::STATUS_NOT_FOUND);
  138. }
  139. // Is this header is set it means our UI is doing a preview for no-download shares
  140. // we check a header so we at least prevent people from using the link directly (obfuscation)
  141. $isNextcloudPreview = $this->request->getHeader('x-nc-preview') === 'true';
  142. $storage = $node->getStorage();
  143. if ($isNextcloudPreview === false && $storage->instanceOfStorage(ISharedStorage::class)) {
  144. /** @var ISharedStorage $storage */
  145. $share = $storage->getShare();
  146. if (!$share->canSeeContent()) {
  147. return new DataResponse([], Http::STATUS_FORBIDDEN);
  148. }
  149. }
  150. try {
  151. $f = $this->preview->getPreview($node, $x, $y, !$a, $mode);
  152. $response = new FileDisplayResponse($f, Http::STATUS_OK, [
  153. 'Content-Type' => $f->getMimeType(),
  154. ]);
  155. $response->cacheFor(3600 * 24, false, true);
  156. return $response;
  157. } catch (NotFoundException $e) {
  158. // If we have no preview enabled, we can redirect to the mime icon if any
  159. if ($mimeFallback) {
  160. if ($url = $this->mimeIconProvider->getMimeIconUrl($node->getMimeType())) {
  161. return new RedirectResponse($url);
  162. }
  163. }
  164. return new DataResponse([], Http::STATUS_NOT_FOUND);
  165. } catch (\InvalidArgumentException $e) {
  166. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  167. }
  168. }
  169. /**
  170. * Get a preview by mime
  171. *
  172. * @param string $mime Mime type
  173. * @return RedirectResponse<Http::STATUS_SEE_OTHER, array{}>
  174. *
  175. * 303: The mime icon url
  176. */
  177. #[NoCSRFRequired]
  178. #[PublicPage]
  179. #[FrontpageRoute(verb: 'GET', url: '/core/mimeicon')]
  180. #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
  181. public function getMimeIconUrl(string $mime = 'application/octet-stream') {
  182. $url = $this->mimeIconProvider->getMimeIconUrl($mime);
  183. if ($url === null) {
  184. $url = $this->mimeIconProvider->getMimeIconUrl('application/octet-stream');
  185. }
  186. return new RedirectResponse($url);
  187. }
  188. }