ApiController.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  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\Controller;
  8. use OC\Files\Node\Node;
  9. use OCA\Files\Service\TagService;
  10. use OCA\Files\Service\UserConfig;
  11. use OCA\Files\Service\ViewConfig;
  12. use OCP\AppFramework\Controller;
  13. use OCP\AppFramework\Http;
  14. use OCP\AppFramework\Http\Attribute\OpenAPI;
  15. use OCP\AppFramework\Http\ContentSecurityPolicy;
  16. use OCP\AppFramework\Http\DataResponse;
  17. use OCP\AppFramework\Http\FileDisplayResponse;
  18. use OCP\AppFramework\Http\JSONResponse;
  19. use OCP\AppFramework\Http\Response;
  20. use OCP\AppFramework\Http\StreamResponse;
  21. use OCP\Files\File;
  22. use OCP\Files\Folder;
  23. use OCP\Files\NotFoundException;
  24. use OCP\IConfig;
  25. use OCP\IPreview;
  26. use OCP\IRequest;
  27. use OCP\IUserSession;
  28. use OCP\Share\IManager;
  29. use OCP\Share\IShare;
  30. /**
  31. * @package OCA\Files\Controller
  32. */
  33. class ApiController extends Controller {
  34. private TagService $tagService;
  35. private IManager $shareManager;
  36. private IPreview $previewManager;
  37. private IUserSession $userSession;
  38. private IConfig $config;
  39. private ?Folder $userFolder;
  40. private UserConfig $userConfig;
  41. private ViewConfig $viewConfig;
  42. public function __construct(string $appName,
  43. IRequest $request,
  44. IUserSession $userSession,
  45. TagService $tagService,
  46. IPreview $previewManager,
  47. IManager $shareManager,
  48. IConfig $config,
  49. ?Folder $userFolder,
  50. UserConfig $userConfig,
  51. ViewConfig $viewConfig) {
  52. parent::__construct($appName, $request);
  53. $this->userSession = $userSession;
  54. $this->tagService = $tagService;
  55. $this->previewManager = $previewManager;
  56. $this->shareManager = $shareManager;
  57. $this->config = $config;
  58. $this->userFolder = $userFolder;
  59. $this->userConfig = $userConfig;
  60. $this->viewConfig = $viewConfig;
  61. }
  62. /**
  63. * Gets a thumbnail of the specified file
  64. *
  65. * @since API version 1.0
  66. *
  67. * @NoAdminRequired
  68. * @NoCSRFRequired
  69. * @StrictCookieRequired
  70. *
  71. * @param int $x Width of the thumbnail
  72. * @param int $y Height of the thumbnail
  73. * @param string $file URL-encoded filename
  74. * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message?: string}, array{}>
  75. *
  76. * 200: Thumbnail returned
  77. * 400: Getting thumbnail is not possible
  78. * 404: File not found
  79. */
  80. public function getThumbnail($x, $y, $file) {
  81. if ($x < 1 || $y < 1) {
  82. return new DataResponse(['message' => 'Requested size must be numeric and a positive value.'], Http::STATUS_BAD_REQUEST);
  83. }
  84. try {
  85. $file = $this->userFolder->get($file);
  86. if ($file instanceof Folder) {
  87. throw new NotFoundException();
  88. }
  89. /** @var File $file */
  90. $preview = $this->previewManager->getPreview($file, $x, $y, true);
  91. return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]);
  92. } catch (NotFoundException $e) {
  93. return new DataResponse(['message' => 'File not found.'], Http::STATUS_NOT_FOUND);
  94. } catch (\Exception $e) {
  95. return new DataResponse([], Http::STATUS_BAD_REQUEST);
  96. }
  97. }
  98. /**
  99. * Updates the info of the specified file path
  100. * The passed tags are absolute, which means they will
  101. * replace the actual tag selection.
  102. *
  103. * @NoAdminRequired
  104. *
  105. * @param string $path path
  106. * @param array|string $tags array of tags
  107. * @return DataResponse
  108. */
  109. public function updateFileTags($path, $tags = null) {
  110. $result = [];
  111. // if tags specified or empty array, update tags
  112. if (!is_null($tags)) {
  113. try {
  114. $this->tagService->updateFileTags($path, $tags);
  115. } catch (\OCP\Files\NotFoundException $e) {
  116. return new DataResponse([
  117. 'message' => $e->getMessage()
  118. ], Http::STATUS_NOT_FOUND);
  119. } catch (\OCP\Files\StorageNotAvailableException $e) {
  120. return new DataResponse([
  121. 'message' => $e->getMessage()
  122. ], Http::STATUS_SERVICE_UNAVAILABLE);
  123. } catch (\Exception $e) {
  124. return new DataResponse([
  125. 'message' => $e->getMessage()
  126. ], Http::STATUS_NOT_FOUND);
  127. }
  128. $result['tags'] = $tags;
  129. }
  130. return new DataResponse($result);
  131. }
  132. /**
  133. * @param \OCP\Files\Node[] $nodes
  134. * @return array
  135. */
  136. private function formatNodes(array $nodes) {
  137. $shareTypesForNodes = $this->getShareTypesForNodes($nodes);
  138. return array_values(array_map(function (Node $node) use ($shareTypesForNodes) {
  139. $shareTypes = $shareTypesForNodes[$node->getId()] ?? [];
  140. $file = \OCA\Files\Helper::formatFileInfo($node->getFileInfo());
  141. $file['hasPreview'] = $this->previewManager->isAvailable($node);
  142. $parts = explode('/', dirname($node->getPath()), 4);
  143. if (isset($parts[3])) {
  144. $file['path'] = '/' . $parts[3];
  145. } else {
  146. $file['path'] = '/';
  147. }
  148. if (!empty($shareTypes)) {
  149. $file['shareTypes'] = $shareTypes;
  150. }
  151. return $file;
  152. }, $nodes));
  153. }
  154. /**
  155. * Get the share types for each node
  156. *
  157. * @param \OCP\Files\Node[] $nodes
  158. * @return array<int, int[]> list of share types for each fileid
  159. */
  160. private function getShareTypesForNodes(array $nodes): array {
  161. $userId = $this->userSession->getUser()->getUID();
  162. $requestedShareTypes = [
  163. IShare::TYPE_USER,
  164. IShare::TYPE_GROUP,
  165. IShare::TYPE_LINK,
  166. IShare::TYPE_REMOTE,
  167. IShare::TYPE_EMAIL,
  168. IShare::TYPE_ROOM,
  169. IShare::TYPE_DECK,
  170. IShare::TYPE_SCIENCEMESH,
  171. ];
  172. $shareTypes = [];
  173. $nodeIds = array_map(function (Node $node) {
  174. return $node->getId();
  175. }, $nodes);
  176. foreach ($requestedShareTypes as $shareType) {
  177. $nodesLeft = array_combine($nodeIds, array_fill(0, count($nodeIds), true));
  178. $offset = 0;
  179. // fetch shares until we've either found shares for all nodes or there are no more shares left
  180. while (count($nodesLeft) > 0) {
  181. $shares = $this->shareManager->getSharesBy($userId, $shareType, null, false, 100, $offset);
  182. foreach ($shares as $share) {
  183. $fileId = $share->getNodeId();
  184. if (isset($nodesLeft[$fileId])) {
  185. if (!isset($shareTypes[$fileId])) {
  186. $shareTypes[$fileId] = [];
  187. }
  188. $shareTypes[$fileId][] = $shareType;
  189. unset($nodesLeft[$fileId]);
  190. }
  191. }
  192. if (count($shares) < 100) {
  193. break;
  194. } else {
  195. $offset += count($shares);
  196. }
  197. }
  198. }
  199. return $shareTypes;
  200. }
  201. /**
  202. * Returns a list of recently modified files.
  203. *
  204. * @NoAdminRequired
  205. *
  206. * @return DataResponse
  207. */
  208. public function getRecentFiles() {
  209. $nodes = $this->userFolder->getRecent(100);
  210. $files = $this->formatNodes($nodes);
  211. return new DataResponse(['files' => $files]);
  212. }
  213. /**
  214. * Returns the current logged-in user's storage stats.
  215. *
  216. * @NoAdminRequired
  217. *
  218. * @param ?string $dir the directory to get the storage stats from
  219. * @return JSONResponse
  220. */
  221. public function getStorageStats($dir = '/'): JSONResponse {
  222. $storageInfo = \OC_Helper::getStorageInfo($dir ?: '/');
  223. return new JSONResponse(['message' => 'ok', 'data' => $storageInfo]);
  224. }
  225. /**
  226. * Set a user view config
  227. *
  228. * @NoAdminRequired
  229. *
  230. * @param string $view
  231. * @param string $key
  232. * @param string|bool $value
  233. * @return JSONResponse
  234. */
  235. public function setViewConfig(string $view, string $key, $value): JSONResponse {
  236. try {
  237. $this->viewConfig->setConfig($view, $key, (string)$value);
  238. } catch (\InvalidArgumentException $e) {
  239. return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
  240. }
  241. return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfig($view)]);
  242. }
  243. /**
  244. * Get the user view config
  245. *
  246. * @NoAdminRequired
  247. *
  248. * @return JSONResponse
  249. */
  250. public function getViewConfigs(): JSONResponse {
  251. return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfigs()]);
  252. }
  253. /**
  254. * Set a user config
  255. *
  256. * @NoAdminRequired
  257. *
  258. * @param string $key
  259. * @param string|bool $value
  260. * @return JSONResponse
  261. */
  262. public function setConfig(string $key, $value): JSONResponse {
  263. try {
  264. $this->userConfig->setConfig($key, (string)$value);
  265. } catch (\InvalidArgumentException $e) {
  266. return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
  267. }
  268. return new JSONResponse(['message' => 'ok', 'data' => ['key' => $key, 'value' => $value]]);
  269. }
  270. /**
  271. * Get the user config
  272. *
  273. * @NoAdminRequired
  274. *
  275. * @return JSONResponse
  276. */
  277. public function getConfigs(): JSONResponse {
  278. return new JSONResponse(['message' => 'ok', 'data' => $this->userConfig->getConfigs()]);
  279. }
  280. /**
  281. * Toggle default for showing/hiding hidden files
  282. *
  283. * @NoAdminRequired
  284. *
  285. * @param bool $value
  286. * @return Response
  287. * @throws \OCP\PreConditionNotMetException
  288. */
  289. public function showHiddenFiles(bool $value): Response {
  290. $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', $value ? '1' : '0');
  291. return new Response();
  292. }
  293. /**
  294. * Toggle default for cropping preview images
  295. *
  296. * @NoAdminRequired
  297. *
  298. * @param bool $value
  299. * @return Response
  300. * @throws \OCP\PreConditionNotMetException
  301. */
  302. public function cropImagePreviews(bool $value): Response {
  303. $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', $value ? '1' : '0');
  304. return new Response();
  305. }
  306. /**
  307. * Toggle default for files grid view
  308. *
  309. * @NoAdminRequired
  310. *
  311. * @param bool $show
  312. * @return Response
  313. * @throws \OCP\PreConditionNotMetException
  314. */
  315. public function showGridView(bool $show): Response {
  316. $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', $show ? '1' : '0');
  317. return new Response();
  318. }
  319. /**
  320. * Get default settings for the grid view
  321. *
  322. * @NoAdminRequired
  323. */
  324. public function getGridView() {
  325. $status = $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', '0') === '1';
  326. return new JSONResponse(['gridview' => $status]);
  327. }
  328. /**
  329. * @NoAdminRequired
  330. * @NoCSRFRequired
  331. * @PublicPage
  332. */
  333. #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)]
  334. public function serviceWorker(): StreamResponse {
  335. $response = new StreamResponse(__DIR__ . '/../../../../dist/preview-service-worker.js');
  336. $response->setHeaders([
  337. 'Content-Type' => 'application/javascript',
  338. 'Service-Worker-Allowed' => '/'
  339. ]);
  340. $policy = new ContentSecurityPolicy();
  341. $policy->addAllowedWorkerSrcDomain("'self'");
  342. $policy->addAllowedScriptDomain("'self'");
  343. $policy->addAllowedConnectDomain("'self'");
  344. $response->setContentSecurityPolicy($policy);
  345. return $response;
  346. }
  347. }