ApiController.php 13 KB

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