From 5e6976fc2b5142d9451a970379d1b9317b8fc93e Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Wed, 22 Mar 2023 11:24:15 -0700 Subject: [PATCH] refactor: split ApiBase Signed-off-by: Varun Patil --- lib/Controller/ApiBase.php | 381 +------------------------------ lib/Controller/ApiBaseFs.php | 291 +++++++++++++++++++++++ lib/Controller/ApiBaseParams.php | 59 +++++ lib/Controller/ApiBaseUtils.php | 134 +++++++++++ 4 files changed, 488 insertions(+), 377 deletions(-) create mode 100644 lib/Controller/ApiBaseFs.php create mode 100644 lib/Controller/ApiBaseParams.php create mode 100644 lib/Controller/ApiBaseUtils.php diff --git a/lib/Controller/ApiBase.php b/lib/Controller/ApiBase.php index 13f480b9..7a943abb 100644 --- a/lib/Controller/ApiBase.php +++ b/lib/Controller/ApiBase.php @@ -25,16 +25,8 @@ namespace OCA\Memories\Controller; use OCA\Memories\AppInfo\Application; use OCA\Memories\Db\TimelineQuery; -use OCA\Memories\Db\TimelineRoot; -use OCA\Memories\Errors; -use OCA\Memories\Exif; -use OCA\Memories\Util; use OCP\App\IAppManager; use OCP\AppFramework\Controller; -use OCP\AppFramework\Http; -use OCP\AppFramework\Http\DataDisplayResponse; -use OCP\Files\File; -use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\IConfig; use OCP\IDBConnection; @@ -44,6 +36,10 @@ use Psr\Log\LoggerInterface; class ApiBase extends Controller { + use ApiBaseFs; + use ApiBaseParams; + use ApiBaseUtils; + protected IConfig $config; protected IUserSession $userSession; protected IRootFolder $rootFolder; @@ -71,373 +67,4 @@ class ApiBase extends Controller $this->logger = $logger; $this->timelineQuery = new TimelineQuery($connection); } - - /** Get logged in user's UID or throw exception */ - protected function getUID(): string - { - $user = $this->userSession->getUser(); - if ($this->getShareToken()) { - $user = null; - } elseif (null === $user) { - throw new \Exception('User not logged in'); - } - - return $user ? $user->getUID() : ''; - } - - /** Get the TimelineRoot object relevant to the request */ - protected function getRequestRoot() - { - $user = $this->userSession->getUser(); - $root = new TimelineRoot(); - - // Albums have no folder - if ($this->albumsIsEnabled() && $this->request->getParam('album')) { - if (null !== $user) { - return $root; - } - if (($token = $this->getShareToken()) && $this->timelineQuery->getAlbumByLink($token)) { - return $root; - } - } - - // Public shared folder - if ($share = $this->getShareNode()) { // can throw - if (!$share instanceof Folder) { - throw new \Exception('Share is not a folder'); - } - - $root->addFolder($share); - - return $root; - } - - // Anything else needs a user - if (null === $user) { - throw new \Exception('User not logged in'); - } - $uid = $user->getUID(); - - $folder = null; - $folderPath = $this->request->getParam('folder'); - $userFolder = $this->rootFolder->getUserFolder($uid); - - try { - if (null !== $folderPath) { - $folder = $userFolder->get(Exif::removeExtraSlash($folderPath)); - $root->addFolder($folder); - } else { - $timelinePath = $this->request->getParam('timelinePath', Exif::getPhotosPath($this->config, $uid)); - $timelinePath = Exif::removeExtraSlash($timelinePath); - - // Multiple timeline path support - $paths = explode(';', $timelinePath); - foreach ($paths as &$path) { - $folder = $userFolder->get(trim($path)); - $root->addFolder($folder); - } - $root->addMountPoints(); - } - } catch (\OCP\Files\NotFoundException $e) { - $msg = $e->getMessage(); - - throw new \Exception("Folder not found: {$msg}"); - } - - return $root; - } - - /** - * Get a file with ID for the current user. - */ - protected function getUserFile(int $fileId): ?File - { - // Don't check self for share token - if ($this->getShareToken()) { - return $this->getShareFile($fileId); - } - - // Check both user folder and album - return $this->getUserFolderFile($fileId) ?? - $this->getAlbumFile($fileId); - } - - /** - * Get a file with ID from user's folder. - */ - protected function getUserFolderFile(int $id): ?File - { - $user = $this->userSession->getUser(); - if (null === $user) { - return null; - } - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - - // No need to force permissions when reading - // from the user's own folder. This includes shared - // folders and files from other users. - return $this->getOneFileFromFolder($userFolder, $id); - } - - /** - * Get a file with ID from an album. - */ - protected function getAlbumFile(int $id): ?File - { - $user = $this->userSession->getUser(); - if (null === $user) { - return null; - } - $uid = $user->getUID(); - - $owner = $this->timelineQuery->albumHasUserFile($uid, $id); - if (!$owner) { - return null; - } - - $folder = $this->rootFolder->getUserFolder($owner); - - // Album files are always read-only - // Note that albums have lowest priority, so it means the - // user doesn't have access to the file in their own folder. - return $this->getOneFileFromFolder($folder, $id, \OCP\Constants::PERMISSION_READ); - } - - /** - * Get a file with ID from a public share. - * - * @param int $fileId - */ - protected function getShareFile(int $id): ?File - { - try { - // Album share - if ($this->request->getParam('album')) { - $album = $this->timelineQuery->getAlbumByLink($this->getShareToken()); - if (null === $album) { - return null; - } - - $owner = $this->timelineQuery->albumHasFile((int) $album['album_id'], $id); - if (!$owner) { - return null; - } - - $folder = $this->rootFolder->getUserFolder($owner); - - // Public albums are always read-only - return $this->getOneFileFromFolder($folder, $id, \OCP\Constants::PERMISSION_READ); - } - - // Folder share - if ($share = $this->getShareNode()) { - // Public shares may allow editing - // Just use the same permissions as the share - if ($share instanceof File) { - return $share; - } - if ($share instanceof Folder) { - return $this->getOneFileFromFolder($share, $id, $share->getPermissions()); - } - - return null; - } - } catch (\Exception $e) { - } - - return null; - } - - protected function isRecursive() - { - return null === $this->request->getParam('folder') || $this->request->getParam('recursive'); - } - - protected function isArchive() - { - return null !== $this->request->getParam('archive'); - } - - protected function isMonthView() - { - return null !== $this->request->getParam('monthView'); - } - - protected function isReverse() - { - return null !== $this->request->getParam('reverse'); - } - - protected function getShareToken() - { - return $this->request->getParam('token'); - } - - protected function getShareObject() - { - // Get token from request - $token = $this->getShareToken(); - if (null === $token) { - return null; - } - - // Get share by token - $share = \OC::$server->get(\OCP\Share\IManager::class)->getShareByToken($token); - if (!PublicController::validateShare($share)) { - return null; - } - - // Check if share is password protected - if (($password = $share->getPassword()) !== null) { - $session = \OC::$server->get(\OCP\ISession::class); - - // https://github.com/nextcloud/server/blob/0447b53bda9fe95ea0cbed765aa332584605d652/lib/public/AppFramework/PublicShareController.php#L119 - if ( - $session->get('public_link_authenticated_token') !== $token - || $session->get('public_link_authenticated_password_hash') !== $password - ) { - throw new \Exception('Share is password protected and user is not authenticated'); - } - } - - return $share; - } - - protected function getShareNode() - { - $share = $this->getShareObject(); - if (null === $share) { - return null; - } - - // Get node from share - $node = $share->getNode(); // throws exception if not found - if (!$node->isReadable() || !$node->isShareable()) { - throw new \Exception('Share not found or invalid'); - } - - // Force permissions from the share onto the node - Util::forcePermissions($node, $share->getPermissions()); - - return $node; - } - - /** - * Given a list of file ids, return the first preview image possible. - */ - protected function getPreviewFromImageList(array $list, int $quality = 512): Http\Response - { - // Get preview manager - $previewManager = \OC::$server->get(\OCP\IPreview::class); - - // Try to get a preview - $userFolder = $this->rootFolder->getUserFolder($this->getUID()); - foreach ($list as &$img) { - // Get the file - $files = $userFolder->getById($img); - if (0 === \count($files)) { - continue; - } - - // Check read permission - if (!$files[0]->isReadable()) { - continue; - } - - // Get preview image - try { - $preview = $previewManager->getPreview($files[0], $quality, $quality, false); - $response = new DataDisplayResponse($preview->getContent(), Http::STATUS_OK, [ - 'Content-Type' => $preview->getMimeType(), - ]); - $response->cacheFor(3600 * 24, false, false); - - return $response; - } catch (\Exception $e) { - continue; - } - } - - return Errors::NotFound('preview from list'); - } - - /** - * Check if albums are enabled for this user. - */ - protected function albumsIsEnabled(): bool - { - return \OCA\Memories\Util::albumsIsEnabled($this->appManager); - } - - /** - * Check if tags is enabled for this user. - */ - protected function tagsIsEnabled(): bool - { - return \OCA\Memories\Util::tagsIsEnabled($this->appManager); - } - - /** - * Check if recognize is enabled for this user. - */ - protected function recognizeIsEnabled(): bool - { - return \OCA\Memories\Util::recognizeIsEnabled($this->appManager); - } - - // Check if facerecognition is installed and enabled for this user. - protected function facerecognitionIsInstalled(): bool - { - return \OCA\Memories\Util::facerecognitionIsInstalled($this->appManager); - } - - /** - * Check if facerecognition is enabled for this user. - */ - protected function facerecognitionIsEnabled(): bool - { - return \OCA\Memories\Util::facerecognitionIsEnabled($this->config, $this->getUID()); - } - - /** - * Check if geolocation is enabled for this user. - */ - protected function placesIsEnabled(): bool - { - return \OCA\Memories\Util::placesGISType() > 0; - } - - /** - * Helper to get one file or null from a fiolder. - * - * @param Folder $folder Folder to search in - * @param int $id Id of the file - * @param int $perm Permissions to force on the file - */ - private function getOneFileFromFolder(Folder $folder, int $id, int $perm = -1): ?File - { - // Check for permissions and get numeric Id - $file = $folder->getById($id); - if (0 === \count($file)) { - return null; - } - $file = $file[0]; - - // Check if node is a file - if (!$file instanceof File) { - return null; - } - - // Check read permission - if (!$file->isReadable()) { - return null; - } - - // Force file permissions if required - if ($perm >= 0) { - Util::forcePermissions($file, $perm); - } - - return $file; - } } diff --git a/lib/Controller/ApiBaseFs.php b/lib/Controller/ApiBaseFs.php new file mode 100644 index 00000000..f659e88d --- /dev/null +++ b/lib/Controller/ApiBaseFs.php @@ -0,0 +1,291 @@ + + * @author Varun Patil + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + */ + +namespace OCA\Memories\Controller; + +use OCA\Memories\Db\TimelineQuery; +use OCA\Memories\Db\TimelineRoot; +use OCA\Memories\Exif; +use OCA\Memories\Util; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\IRootFolder; +use OCP\IConfig; +use OCP\IUserSession; + +trait ApiBaseFs +{ + use ApiBaseParams; + use ApiBaseUtils; + + protected IConfig $config; + protected IUserSession $userSession; + protected IRootFolder $rootFolder; + protected TimelineQuery $timelineQuery; + + /** Get the TimelineRoot object relevant to the request */ + protected function getRequestRoot() + { + $user = $this->userSession->getUser(); + $root = new TimelineRoot(); + + // Albums have no folder + if ($this->albumsIsEnabled() && $this->request->getParam('album')) { + if (null !== $user) { + return $root; + } + if (($token = $this->getShareToken()) && $this->timelineQuery->getAlbumByLink($token)) { + return $root; + } + } + + // Public shared folder + if ($share = $this->getShareNode()) { // can throw + if (!$share instanceof Folder) { + throw new \Exception('Share is not a folder'); + } + + $root->addFolder($share); + + return $root; + } + + // Anything else needs a user + if (null === $user) { + throw new \Exception('User not logged in'); + } + $uid = $user->getUID(); + + $folder = null; + $folderPath = $this->request->getParam('folder'); + $userFolder = $this->rootFolder->getUserFolder($uid); + + try { + if (null !== $folderPath) { + $folder = $userFolder->get(Exif::removeExtraSlash($folderPath)); + $root->addFolder($folder); + } else { + $timelinePath = $this->request->getParam('timelinePath', Exif::getPhotosPath($this->config, $uid)); + $timelinePath = Exif::removeExtraSlash($timelinePath); + + // Multiple timeline path support + $paths = explode(';', $timelinePath); + foreach ($paths as &$path) { + $folder = $userFolder->get(trim($path)); + $root->addFolder($folder); + } + $root->addMountPoints(); + } + } catch (\OCP\Files\NotFoundException $e) { + $msg = $e->getMessage(); + + throw new \Exception("Folder not found: {$msg}"); + } + + return $root; + } + + /** + * Get a file with ID for the current user. + */ + protected function getUserFile(int $fileId): ?File + { + // Don't check self for share token + if ($this->getShareToken()) { + return $this->getShareFile($fileId); + } + + // Check both user folder and album + return $this->getUserFolderFile($fileId) ?? + $this->getAlbumFile($fileId); + } + + /** + * Get a file with ID from user's folder. + */ + protected function getUserFolderFile(int $id): ?File + { + $user = $this->userSession->getUser(); + if (null === $user) { + return null; + } + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + + // No need to force permissions when reading + // from the user's own folder. This includes shared + // folders and files from other users. + return $this->getOneFileFromFolder($userFolder, $id); + } + + /** + * Get a file with ID from an album. + */ + protected function getAlbumFile(int $id): ?File + { + $user = $this->userSession->getUser(); + if (null === $user) { + return null; + } + $uid = $user->getUID(); + + $owner = $this->timelineQuery->albumHasUserFile($uid, $id); + if (!$owner) { + return null; + } + + $folder = $this->rootFolder->getUserFolder($owner); + + // Album files are always read-only + // Note that albums have lowest priority, so it means the + // user doesn't have access to the file in their own folder. + return $this->getOneFileFromFolder($folder, $id, \OCP\Constants::PERMISSION_READ); + } + + /** + * Get a file with ID from a public share. + * + * @param int $fileId + */ + protected function getShareFile(int $id): ?File + { + try { + // Album share + if ($this->request->getParam('album')) { + $album = $this->timelineQuery->getAlbumByLink($this->getShareToken()); + if (null === $album) { + return null; + } + + $owner = $this->timelineQuery->albumHasFile((int) $album['album_id'], $id); + if (!$owner) { + return null; + } + + $folder = $this->rootFolder->getUserFolder($owner); + + // Public albums are always read-only + return $this->getOneFileFromFolder($folder, $id, \OCP\Constants::PERMISSION_READ); + } + + // Folder share + if ($share = $this->getShareNode()) { + // Public shares may allow editing + // Just use the same permissions as the share + if ($share instanceof File) { + return $share; + } + if ($share instanceof Folder) { + return $this->getOneFileFromFolder($share, $id, $share->getPermissions()); + } + + return null; + } + } catch (\Exception $e) { + } + + return null; + } + + protected function getShareObject() + { + // Get token from request + $token = $this->getShareToken(); + if (null === $token) { + return null; + } + + // Get share by token + $share = \OC::$server->get(\OCP\Share\IManager::class)->getShareByToken($token); + if (!PublicController::validateShare($share)) { + return null; + } + + // Check if share is password protected + if (($password = $share->getPassword()) !== null) { + $session = \OC::$server->get(\OCP\ISession::class); + + // https://github.com/nextcloud/server/blob/0447b53bda9fe95ea0cbed765aa332584605d652/lib/public/AppFramework/PublicShareController.php#L119 + if ( + $session->get('public_link_authenticated_token') !== $token + || $session->get('public_link_authenticated_password_hash') !== $password + ) { + throw new \Exception('Share is password protected and user is not authenticated'); + } + } + + return $share; + } + + protected function getShareNode() + { + $share = $this->getShareObject(); + if (null === $share) { + return null; + } + + // Get node from share + $node = $share->getNode(); // throws exception if not found + if (!$node->isReadable() || !$node->isShareable()) { + throw new \Exception('Share not found or invalid'); + } + + // Force permissions from the share onto the node + Util::forcePermissions($node, $share->getPermissions()); + + return $node; + } + + /** + * Helper to get one file or null from a fiolder. + * + * @param Folder $folder Folder to search in + * @param int $id Id of the file + * @param int $perm Permissions to force on the file + */ + private function getOneFileFromFolder(Folder $folder, int $id, int $perm = -1): ?File + { + // Check for permissions and get numeric Id + $file = $folder->getById($id); + if (0 === \count($file)) { + return null; + } + $file = $file[0]; + + // Check if node is a file + if (!$file instanceof File) { + return null; + } + + // Check read permission + if (!$file->isReadable()) { + return null; + } + + // Force file permissions if required + if ($perm >= 0) { + Util::forcePermissions($file, $perm); + } + + return $file; + } +} diff --git a/lib/Controller/ApiBaseParams.php b/lib/Controller/ApiBaseParams.php new file mode 100644 index 00000000..2210ec12 --- /dev/null +++ b/lib/Controller/ApiBaseParams.php @@ -0,0 +1,59 @@ + + * @author Varun Patil + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + */ + +namespace OCA\Memories\Controller; + +trait ApiBaseParams +{ + /** + * current request. + * + * @var \OCP\IRequest + */ + protected $request; + + protected function isRecursive() + { + return null === $this->request->getParam('folder') || $this->request->getParam('recursive'); + } + + protected function isArchive() + { + return null !== $this->request->getParam('archive'); + } + + protected function isMonthView() + { + return null !== $this->request->getParam('monthView'); + } + + protected function isReverse() + { + return null !== $this->request->getParam('reverse'); + } + + protected function getShareToken() + { + return $this->request->getParam('token'); + } +} diff --git a/lib/Controller/ApiBaseUtils.php b/lib/Controller/ApiBaseUtils.php new file mode 100644 index 00000000..1fd9103b --- /dev/null +++ b/lib/Controller/ApiBaseUtils.php @@ -0,0 +1,134 @@ + + * @author Varun Patil + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + */ + +namespace OCA\Memories\Controller; + +use OCA\Memories\Errors; +use OCP\App\IAppManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataDisplayResponse; +use OCP\IConfig; + +trait ApiBaseUtils +{ + protected IAppManager $appManager; + protected IConfig $config; + + /** Get logged in user's UID or throw exception */ + protected function getUID(): string + { + $user = $this->userSession->getUser(); + if ($this->getShareToken()) { + $user = null; + } elseif (null === $user) { + throw new \Exception('User not logged in'); + } + + return $user ? $user->getUID() : ''; + } + + /** + * Check if albums are enabled for this user. + */ + protected function albumsIsEnabled(): bool + { + return \OCA\Memories\Util::albumsIsEnabled($this->appManager); + } + + /** + * Check if tags is enabled for this user. + */ + protected function tagsIsEnabled(): bool + { + return \OCA\Memories\Util::tagsIsEnabled($this->appManager); + } + + /** + * Check if recognize is enabled for this user. + */ + protected function recognizeIsEnabled(): bool + { + return \OCA\Memories\Util::recognizeIsEnabled($this->appManager); + } + + // Check if facerecognition is installed and enabled for this user. + protected function facerecognitionIsInstalled(): bool + { + return \OCA\Memories\Util::facerecognitionIsInstalled($this->appManager); + } + + /** + * Check if facerecognition is enabled for this user. + */ + protected function facerecognitionIsEnabled(): bool + { + return \OCA\Memories\Util::facerecognitionIsEnabled($this->config, $this->getUID()); + } + + /** + * Check if geolocation is enabled for this user. + */ + protected function placesIsEnabled(): bool + { + return \OCA\Memories\Util::placesGISType() > 0; + } + + /** + * Given a list of file ids, return the first preview image possible. + */ + protected function getPreviewFromImageList(array $list, int $quality = 512): Http\Response + { + // Get preview manager + $previewManager = \OC::$server->get(\OCP\IPreview::class); + + // Try to get a preview + $userFolder = $this->rootFolder->getUserFolder($this->getUID()); + foreach ($list as &$img) { + // Get the file + $files = $userFolder->getById($img); + if (0 === \count($files)) { + continue; + } + + // Check read permission + if (!$files[0]->isReadable()) { + continue; + } + + // Get preview image + try { + $preview = $previewManager->getPreview($files[0], $quality, $quality, false); + $response = new DataDisplayResponse($preview->getContent(), Http::STATUS_OK, [ + 'Content-Type' => $preview->getMimeType(), + ]); + $response->cacheFor(3600 * 24, false, false); + + return $response; + } catch (\Exception $e) { + continue; + } + } + + return Errors::NotFound('preview from list'); + } +}