diff options
Diffstat (limited to 'apps/files/lib/Controller/ApiController.php')
-rw-r--r-- | apps/files/lib/Controller/ApiController.php | 486 |
1 files changed, 294 insertions, 192 deletions
diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index 0cf261af726..8bb024fb698 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -1,136 +1,124 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Felix Nüsse <Felix.nuesse@t-online.de> - * @author fnuesse <felix.nuesse@t-online.de> - * @author fnuesse <fnuesse@techfak.uni-bielefeld.de> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Max Kovalenko <mxss1998@yandex.ru> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Richard Steinmetz <richard@steinmetz.cloud> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Tobias Kaminsky <tobias@kaminsky.me> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ - namespace OCA\Files\Controller; use OC\Files\Node\Node; +use OCA\Files\Helper; +use OCA\Files\ResponseDefinitions; use OCA\Files\Service\TagService; +use OCA\Files\Service\UserConfig; +use OCA\Files\Service\ViewConfig; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\Attribute\StrictCookiesRequired; +use OCP\AppFramework\Http\ContentSecurityPolicy; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; +use OCP\AppFramework\Http\StreamResponse; use OCP\Files\File; use OCP\Files\Folder; +use OCP\Files\InvalidPathException; +use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\Files\Storage\ISharedStorage; +use OCP\Files\StorageNotAvailableException; use OCP\IConfig; +use OCP\IL10N; use OCP\IPreview; use OCP\IRequest; +use OCP\IUser; use OCP\IUserSession; +use OCP\PreConditionNotMetException; use OCP\Share\IManager; use OCP\Share\IShare; +use Psr\Log\LoggerInterface; +use Throwable; /** - * Class ApiController + * @psalm-import-type FilesFolderTree from ResponseDefinitions * * @package OCA\Files\Controller */ class ApiController extends Controller { - /** @var TagService */ - private $tagService; - /** @var IManager * */ - private $shareManager; - /** @var IPreview */ - private $previewManager; - /** IUserSession */ - private $userSession; - /** IConfig */ - private $config; - /** @var Folder */ - private $userFolder; - - /** - * @param string $appName - * @param IRequest $request - * @param IUserSession $userSession - * @param TagService $tagService - * @param IPreview $previewManager - * @param IManager $shareManager - * @param IConfig $config - * @param Folder $userFolder - */ - public function __construct($appName, - IRequest $request, - IUserSession $userSession, - TagService $tagService, - IPreview $previewManager, - IManager $shareManager, - IConfig $config, - Folder $userFolder) { + public function __construct( + string $appName, + IRequest $request, + private IUserSession $userSession, + private TagService $tagService, + private IPreview $previewManager, + private IManager $shareManager, + private IConfig $config, + private ?Folder $userFolder, + private UserConfig $userConfig, + private ViewConfig $viewConfig, + private IL10N $l10n, + private IRootFolder $rootFolder, + private LoggerInterface $logger, + ) { parent::__construct($appName, $request); - $this->userSession = $userSession; - $this->tagService = $tagService; - $this->previewManager = $previewManager; - $this->shareManager = $shareManager; - $this->config = $config; - $this->userFolder = $userFolder; } /** * Gets a thumbnail of the specified file * * @since API version 1.0 + * @deprecated 32.0.0 Use the preview endpoint provided by core instead * - * @NoAdminRequired - * @NoCSRFRequired - * @StrictCookieRequired - * - * @param int $x - * @param int $y + * @param int $x Width of the thumbnail + * @param int $y Height of the thumbnail * @param string $file URL-encoded filename - * @return DataResponse|FileDisplayResponse + * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message?: string}, array{}> + * + * 200: Thumbnail returned + * 400: Getting thumbnail is not possible + * 404: File not found */ + #[NoAdminRequired] + #[NoCSRFRequired] + #[StrictCookiesRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] public function getThumbnail($x, $y, $file) { if ($x < 1 || $y < 1) { return new DataResponse(['message' => 'Requested size must be numeric and a positive value.'], Http::STATUS_BAD_REQUEST); } try { - $file = $this->userFolder->get($file); - if ($file instanceof Folder) { + $file = $this->userFolder?->get($file); + if ($file === null + || !($file instanceof File) + || ($file->getId() <= 0) + ) { throw new NotFoundException(); } - /** @var File $file */ + // Validate the user is allowed to download the file (preview is some kind of download) + /** @var ISharedStorage $storage */ + $storage = $file->getStorage(); + if ($storage->instanceOfStorage(ISharedStorage::class)) { + /** @var IShare $share */ + $share = $storage->getShare(); + if (!$share->canSeeContent()) { + throw new NotFoundException(); + } + } + $preview = $this->previewManager->getPreview($file, $x, $y, true); return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]); - } catch (NotFoundException $e) { + } catch (NotFoundException|NotPermittedException|InvalidPathException) { return new DataResponse(['message' => 'File not found.'], Http::STATUS_NOT_FOUND); } catch (\Exception $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); @@ -142,23 +130,22 @@ class ApiController extends Controller { * The passed tags are absolute, which means they will * replace the actual tag selection. * - * @NoAdminRequired - * * @param string $path path * @param array|string $tags array of tags * @return DataResponse */ + #[NoAdminRequired] public function updateFileTags($path, $tags = null) { $result = []; // if tags specified or empty array, update tags if (!is_null($tags)) { try { $this->tagService->updateFileTags($path, $tags); - } catch (\OCP\Files\NotFoundException $e) { + } catch (NotFoundException $e) { return new DataResponse([ 'message' => $e->getMessage() ], Http::STATUS_NOT_FOUND); - } catch (\OCP\Files\StorageNotAvailableException $e) { + } catch (StorageNotAvailableException $e) { return new DataResponse([ 'message' => $e->getMessage() ], Http::STATUS_SERVICE_UNAVAILABLE); @@ -177,10 +164,10 @@ class ApiController extends Controller { * @return array */ private function formatNodes(array $nodes) { - return array_values(array_map(function (Node $node) { - /** @var \OC\Files\Node\Node $shareTypes */ - $shareTypes = $this->getShareTypes($node); - $file = \OCA\Files\Helper::formatFileInfo($node->getFileInfo()); + $shareTypesForNodes = $this->getShareTypesForNodes($nodes); + return array_values(array_map(function (Node $node) use ($shareTypesForNodes) { + $shareTypes = $shareTypesForNodes[$node->getId()] ?? []; + $file = Helper::formatFileInfo($node->getFileInfo()); $file['hasPreview'] = $this->previewManager->isAvailable($node); $parts = explode('/', dirname($node->getPath()), 4); if (isset($parts[3])) { @@ -196,12 +183,63 @@ class ApiController extends Controller { } /** - * Returns a list of recently modifed files. + * Get the share types for each node * - * @NoAdminRequired + * @param \OCP\Files\Node[] $nodes + * @return array<int, int[]> list of share types for each fileid + */ + private function getShareTypesForNodes(array $nodes): array { + $userId = $this->userSession->getUser()->getUID(); + $requestedShareTypes = [ + IShare::TYPE_USER, + IShare::TYPE_GROUP, + IShare::TYPE_LINK, + IShare::TYPE_REMOTE, + IShare::TYPE_EMAIL, + IShare::TYPE_ROOM, + IShare::TYPE_DECK, + IShare::TYPE_SCIENCEMESH, + ]; + $shareTypes = []; + + $nodeIds = array_map(function (Node $node) { + return $node->getId(); + }, $nodes); + + foreach ($requestedShareTypes as $shareType) { + $nodesLeft = array_combine($nodeIds, array_fill(0, count($nodeIds), true)); + $offset = 0; + + // fetch shares until we've either found shares for all nodes or there are no more shares left + while (count($nodesLeft) > 0) { + $shares = $this->shareManager->getSharesBy($userId, $shareType, null, false, 100, $offset); + foreach ($shares as $share) { + $fileId = $share->getNodeId(); + if (isset($nodesLeft[$fileId])) { + if (!isset($shareTypes[$fileId])) { + $shareTypes[$fileId] = []; + } + $shareTypes[$fileId][] = $shareType; + unset($nodesLeft[$fileId]); + } + } + + if (count($shares) < 100) { + break; + } else { + $offset += count($shares); + } + } + } + return $shareTypes; + } + + /** + * Returns a list of recently modified files. * * @return DataResponse */ + #[NoAdminRequired] public function getRecentFiles() { $nodes = $this->userFolder->getRecent(100); $files = $this->formatNodes($nodes); @@ -209,152 +247,216 @@ class ApiController extends Controller { } /** - * Return a list of share types for outgoing shares + * @param \OCP\Files\Node[] $nodes + * @param int $depth The depth to traverse into the contents of each node + */ + private function getChildren(array $nodes, int $depth = 1, int $currentDepth = 0): array { + if ($currentDepth >= $depth) { + return []; + } + + $children = []; + foreach ($nodes as $node) { + if (!($node instanceof Folder)) { + continue; + } + + $basename = basename($node->getPath()); + $entry = [ + 'id' => $node->getId(), + 'basename' => $basename, + 'children' => $this->getChildren($node->getDirectoryListing(), $depth, $currentDepth + 1), + ]; + $displayName = $node->getName(); + if ($basename !== $displayName) { + $entry['displayName'] = $displayName; + } + $children[] = $entry; + } + return $children; + } + + /** + * Returns the folder tree of the user * - * @param Node $node file node + * @param string $path The path relative to the user folder + * @param int $depth The depth of the tree * - * @return int[] array of share types + * @return JSONResponse<Http::STATUS_OK, FilesFolderTree, array{}>|JSONResponse<Http::STATUS_UNAUTHORIZED|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, array{message: string}, array{}> + * + * 200: Folder tree returned successfully + * 400: Invalid folder path + * 401: Unauthorized + * 404: Folder not found */ - private function getShareTypes(Node $node) { - $userId = $this->userSession->getUser()->getUID(); - $shareTypes = []; - $requestedShareTypes = [ - IShare::TYPE_USER, - IShare::TYPE_GROUP, - IShare::TYPE_LINK, - IShare::TYPE_REMOTE, - IShare::TYPE_EMAIL, - IShare::TYPE_ROOM, - IShare::TYPE_DECK, - ]; - foreach ($requestedShareTypes as $requestedShareType) { - // one of each type is enough to find out about the types - $shares = $this->shareManager->getSharesBy( - $userId, - $requestedShareType, - $node, - false, - 1 - ); - if (!empty($shares)) { - $shareTypes[] = $requestedShareType; + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/folder-tree')] + #[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] + public function getFolderTree(string $path = '/', int $depth = 1): JSONResponse { + $user = $this->userSession->getUser(); + if (!($user instanceof IUser)) { + return new JSONResponse([ + 'message' => $this->l10n->t('Failed to authorize'), + ], Http::STATUS_UNAUTHORIZED); + } + try { + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $userFolderPath = $userFolder->getPath(); + $fullPath = implode('/', [$userFolderPath, trim($path, '/')]); + $node = $this->rootFolder->get($fullPath); + if (!($node instanceof Folder)) { + return new JSONResponse([ + 'message' => $this->l10n->t('Invalid folder path'), + ], Http::STATUS_BAD_REQUEST); } + $nodes = $node->getDirectoryListing(); + $tree = $this->getChildren($nodes, $depth); + } catch (NotFoundException $e) { + return new JSONResponse([ + 'message' => $this->l10n->t('Folder not found'), + ], Http::STATUS_NOT_FOUND); + } catch (Throwable $th) { + $this->logger->error($th->getMessage(), ['exception' => $th]); + $tree = []; } - return $shareTypes; + return new JSONResponse($tree); } /** - * Change the default sort mode + * Returns the current logged-in user's storage stats. * - * @NoAdminRequired + * @param ?string $dir the directory to get the storage stats from + * @return JSONResponse + */ + #[NoAdminRequired] + public function getStorageStats($dir = '/'): JSONResponse { + $storageInfo = \OC_Helper::getStorageInfo($dir ?: '/'); + $response = new JSONResponse(['message' => 'ok', 'data' => $storageInfo]); + $response->cacheFor(5 * 60); + return $response; + } + + /** + * Set a user view config * - * @param string $mode - * @param string $direction - * @return Response - * @throws \OCP\PreConditionNotMetException + * @param string $view + * @param string $key + * @param string|bool $value + * @return JSONResponse */ - public function updateFileSorting($mode, $direction) { - $allowedMode = ['name', 'size', 'mtime']; - $allowedDirection = ['asc', 'desc']; - if (!in_array($mode, $allowedMode) || !in_array($direction, $allowedDirection)) { - $response = new Response(); - $response->setStatus(Http::STATUS_UNPROCESSABLE_ENTITY); - return $response; + #[NoAdminRequired] + public function setViewConfig(string $view, string $key, $value): JSONResponse { + try { + $this->viewConfig->setConfig($view, $key, (string)$value); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } - $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'file_sorting', $mode); - $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'file_sorting_direction', $direction); - return new Response(); + + return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfig($view)]); } + /** - * Toggle default for showing/hiding hidden files + * Get the user view config * - * @NoAdminRequired + * @return JSONResponse + */ + #[NoAdminRequired] + public function getViewConfigs(): JSONResponse { + return new JSONResponse(['message' => 'ok', 'data' => $this->viewConfig->getConfigs()]); + } + + /** + * Set a user config * - * @param bool $show + * @param string $key + * @param string|bool $value + * @return JSONResponse + */ + #[NoAdminRequired] + public function setConfig(string $key, $value): JSONResponse { + try { + $this->userConfig->setConfig($key, (string)$value); + } catch (\InvalidArgumentException $e) { + return new JSONResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } + + return new JSONResponse(['message' => 'ok', 'data' => ['key' => $key, 'value' => $value]]); + } + + + /** + * Get the user config + * + * @return JSONResponse + */ + #[NoAdminRequired] + public function getConfigs(): JSONResponse { + return new JSONResponse(['message' => 'ok', 'data' => $this->userConfig->getConfigs()]); + } + + /** + * Toggle default for showing/hiding hidden files + * + * @param bool $value * @return Response - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ - public function showHiddenFiles($show) { - $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', (int)$show); + #[NoAdminRequired] + public function showHiddenFiles(bool $value): Response { + $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_hidden', $value ? '1' : '0'); return new Response(); } /** * Toggle default for cropping preview images * - * @NoAdminRequired - * - * @param bool $crop + * @param bool $value * @return Response - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ - public function cropImagePreviews($crop) { - $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', (int)$crop); + #[NoAdminRequired] + public function cropImagePreviews(bool $value): Response { + $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'crop_image_previews', $value ? '1' : '0'); return new Response(); } /** * Toggle default for files grid view * - * @NoAdminRequired - * * @param bool $show * @return Response - * @throws \OCP\PreConditionNotMetException + * @throws PreConditionNotMetException */ - public function showGridView($show) { - $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', (int)$show); + #[NoAdminRequired] + public function showGridView(bool $show): Response { + $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', $show ? '1' : '0'); return new Response(); } /** * Get default settings for the grid view - * - * @NoAdminRequired */ + #[NoAdminRequired] public function getGridView() { $status = $this->config->getUserValue($this->userSession->getUser()->getUID(), 'files', 'show_grid', '0') === '1'; return new JSONResponse(['gridview' => $status]); } - /** - * Toggle default for showing/hiding xxx folder - * - * @NoAdminRequired - * - * @param int $show - * @param string $key the key of the folder - * - * @return Response - * @throws \OCP\PreConditionNotMetException - */ - public function toggleShowFolder(int $show, string $key) { - // ensure the edited key exists - $navItems = \OCA\Files\App::getNavigationManager()->getAll(); - foreach ($navItems as $item) { - // check if data is valid - if (($show === 0 || $show === 1) && isset($item['expandedState']) && $key === $item['expandedState']) { - $this->config->setUserValue($this->userSession->getUser()->getUID(), 'files', $key, $show); - return new Response(); - } - } - $response = new Response(); - $response->setStatus(Http::STATUS_FORBIDDEN); + #[PublicPage] + #[NoCSRFRequired] + #[OpenAPI(scope: OpenAPI::SCOPE_IGNORE)] + public function serviceWorker(): StreamResponse { + $response = new StreamResponse(__DIR__ . '/../../../../dist/preview-service-worker.js'); + $response->setHeaders([ + 'Content-Type' => 'application/javascript', + 'Service-Worker-Allowed' => '/' + ]); + $policy = new ContentSecurityPolicy(); + $policy->addAllowedWorkerSrcDomain("'self'"); + $policy->addAllowedScriptDomain("'self'"); + $policy->addAllowedConnectDomain("'self'"); + $response->setContentSecurityPolicy($policy); return $response; } - - /** - * Get sorting-order for custom sorting - * - * @NoAdminRequired - * - * @param string $folderpath - * @return string - * @throws \OCP\Files\NotFoundException - */ - public function getNodeType($folderpath) { - $node = $this->userFolder->get($folderpath); - return $node->getType(); - } } |