diff --git a/lib/Controller/Implementation/ConfigImplementation.php b/lib/Controller/Implementation/ConfigImplementation.php index 49a0a2f63..4b0b777e1 100644 --- a/lib/Controller/Implementation/ConfigImplementation.php +++ b/lib/Controller/Implementation/ConfigImplementation.php @@ -2,7 +2,9 @@ namespace OCA\Cookbook\Controller\Implementation; +use OCA\Cookbook\Helper\MyRecipesFolderHelper; use OCA\Cookbook\Helper\RestParameterParser; +use OCA\Cookbook\Helper\SharedRecipesFolderHelper; use OCA\Cookbook\Helper\UserFolderHelper; use OCA\Cookbook\Service\DbCacheService; use OCA\Cookbook\Service\RecipeService; @@ -18,17 +20,25 @@ class ConfigImplementation { private $restParser; /** @var UserFolderHelper */ private $userFolder; + /** @var MyRecipesFolderHelper */ + private $myRecipesFolder; + /** @var SharedRecipesFolderHelper */ + private $sharedRecipesFolder; public function __construct( RecipeService $recipeService, DbCacheService $dbCacheService, RestParameterParser $restParser, UserFolderHelper $userFolder, + MyRecipesFolderHelper $myRecipesFolder, + SharedRecipesFolderHelper $sharedRecipesFolder, ) { $this->service = $recipeService; $this->dbCacheService = $dbCacheService; $this->restParser = $restParser; $this->userFolder = $userFolder; + $this->myRecipesFolder = $myRecipesFolder; + $this->sharedRecipesFolder = $sharedRecipesFolder; } protected const KEY_VISIBLE_INFO_BLOCKS = 'visibleInfoBlocks'; @@ -43,6 +53,8 @@ public function list() { return new JSONResponse([ 'folder' => $this->userFolder->getPath(), + 'my_recipes_folder' => $this->myRecipesFolder->getPath(), + 'shared_recipes_folder' => $this->sharedRecipesFolder->getPath(), 'update_interval' => $this->dbCacheService->getSearchIndexUpdateInterval(), 'print_image' => $this->service->getPrintImage(), self::KEY_VISIBLE_INFO_BLOCKS => $this->service->getVisibleInfoBlocks(), @@ -67,6 +79,16 @@ public function config() { $this->dbCacheService->updateCache(); } + if (isset($data['my_recipes_folder'])) { + $this->myRecipesFolder->setPath($data['my_recipes_folder']); + $this->dbCacheService->updateCache(); + } + + if (isset($data['shared_recipes_folder'])) { + $this->sharedRecipesFolder->setPath($data['shared_recipes_folder']); + $this->dbCacheService->updateCache(); + } + if (isset($data['update_interval'])) { $this->service->setSearchIndexUpdateInterval($data['update_interval']); } diff --git a/lib/Controller/MainController.php b/lib/Controller/MainController.php index a2090acf6..9188925cf 100755 --- a/lib/Controller/MainController.php +++ b/lib/Controller/MainController.php @@ -2,8 +2,10 @@ namespace OCA\Cookbook\Controller; -use OCA\Cookbook\Exception\UserFolderNotWritableException; +use OCA\Cookbook\Exception\FolderNotWritableException; use OCA\Cookbook\Exception\UserNotLoggedInException; +use OCA\Cookbook\Helper\MyRecipesFolderHelper; +use OCA\Cookbook\Helper\SharedRecipesFolderHelper; use OCA\Cookbook\Helper\UserFolderHelper; use OCA\Cookbook\Service\DbCacheService; use OCP\AppFramework\Controller; @@ -20,18 +22,26 @@ class MainController extends Controller { private $dbCacheService; /** @var UserFolderHelper */ private $userFolder; + /** @var MyRecipesFolderHelper */ + private $myRecipesFolder; + /** @var SharedRecipesFolderHelper */ + private $sharedRecipesFolder; public function __construct( string $AppName, IRequest $request, DbCacheService $dbCacheService, UserFolderHelper $userFolder, + MyRecipesFolderHelper $myRecipesFolder, + SharedRecipesFolderHelper $sharedRecipesFolder, ) { parent::__construct($AppName, $request); $this->appName = $AppName; $this->dbCacheService = $dbCacheService; $this->userFolder = $userFolder; + $this->myRecipesFolder = $myRecipesFolder; + $this->sharedRecipesFolder = $sharedRecipesFolder; } /** @@ -43,9 +53,11 @@ public function __construct( #[NoCSRFRequired] public function index(): TemplateResponse { try { - // Check if the user folder can be accessed + // Check if the user folders can be accessed $this->userFolder->getFolder(); - } catch (UserFolderNotWritableException $ex) { + $this->myRecipesFolder->getFolder(); + $this->sharedRecipesFolder->getFolder(); + } catch (FolderNotWritableException $ex) { Util::addScript('cookbook', 'cookbook-guest'); return new TemplateResponse($this->appName, 'invalid_guest'); } diff --git a/lib/Exception/UserFolderNotValidException.php b/lib/Exception/FolderNotValidException.php similarity index 75% rename from lib/Exception/UserFolderNotValidException.php rename to lib/Exception/FolderNotValidException.php index 73a87fe2a..600cee02f 100644 --- a/lib/Exception/UserFolderNotValidException.php +++ b/lib/Exception/FolderNotValidException.php @@ -2,7 +2,7 @@ namespace OCA\Cookbook\Exception; -class UserFolderNotValidException extends \Exception { +class FolderNotValidException extends \Exception { public function __construct($message = '', $code = 0, $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/lib/Exception/UserFolderNotWritableException.php b/lib/Exception/FolderNotWritableException.php similarity index 74% rename from lib/Exception/UserFolderNotWritableException.php rename to lib/Exception/FolderNotWritableException.php index bce73d6c1..570c963ad 100644 --- a/lib/Exception/UserFolderNotWritableException.php +++ b/lib/Exception/FolderNotWritableException.php @@ -2,7 +2,7 @@ namespace OCA\Cookbook\Exception; -class UserFolderNotWritableException extends \Exception { +class FolderNotWritableException extends \Exception { public function __construct($message = '', $code = 0, $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/lib/Helper/FolderHelper.php b/lib/Helper/FolderHelper.php new file mode 100644 index 000000000..af1de0cf6 --- /dev/null +++ b/lib/Helper/FolderHelper.php @@ -0,0 +1,145 @@ +userId = $UserId; + $this->root = $root; + $this->l = $l; + $this->config = $configHelper; + + $this->cache = null; + } + + /** + * Set the path relative to the user's root folder + * + * @param string The relative path name + * @throws NotImplementedException If the function is not implemented in the child class + */ + #[\Override] + public function setPath(string $path) { + throw new NotImplementedException(); + } + + /** + * Get the path relative to the user's root folder + * + * @return string The relative path name + * @throws NotImplementedException If the function is not implemented in the child class + */ + #[\Override] + public function getPath(): string { + throw new NotImplementedException(); + } + + /** + * Get the current folder + * + * @return Folder The folder + * @throws FolderNotValidException If the folder is a file or could not be generated + * @throws UserNotLoggedInException If there is no logged-in user at that time + */ + public function getFolder(): Folder { + // Ensure the cache is built. + if (is_null($this->cache)) { + $path = $this->getPath(); + + // Correct path to be relative to nc root + $path = '/' . $this->userId . '/files/' . $path; + $path = str_replace('//', '/', $path); + + + $this->cache = $this->getOrCreateFolder($path); + } + + return $this->cache; + } + + /** + * Get or Create the folder at path + * + * @param string The folder's relative path name + * @return Folder The folder at path + * @throws FolderNotValidException If the folder is a file or could not be generated + * @throws FolderNotWritableException If the folder cannot be written + */ + protected function getOrCreateFolder(string $path): Folder { + try { + $node = $this->root->get($path); + } catch (NotFoundException $ex) { + try { + $node = $this->root->newFolder($path); + } catch (NotPermittedException $ex1) { + throw new FolderNotValidException( + $this->l->t('The folder cannot be created due to missing permissions.'), + 0, + $ex1 + ); + } + } + + if ($node->getType() !== FileInfo::TYPE_FOLDER) { + throw new FolderNotValidException( + $this->l->t('The configured folder is a file.') + ); + } + + if (!$node->isCreatable()) { + throw new FolderNotWritableException( + $this->l->t('User cannot create folder') + ); + } + + return $node; + } +} diff --git a/lib/Helper/MyRecipesFolderHelper.php b/lib/Helper/MyRecipesFolderHelper.php new file mode 100644 index 000000000..c01479e0c --- /dev/null +++ b/lib/Helper/MyRecipesFolderHelper.php @@ -0,0 +1,39 @@ +config->setMyRecipesFolderName($path); + $this->cache = null; + } + + /** + * Get the path of the user's recipes relative to the user's root folder + * + * @return string The relative path name + * @throws UserNotLoggedInException If there is currently no logged in user + */ + #[\Override] + public function getPath(): string { + $path = $this->config->getMyRecipesFolderName(); + + // TODO This was in the original code. Is it still needed? + // $path = str_replace('//', '/', $path); + + return $path; + } +} diff --git a/lib/Helper/SharedRecipesFolderHelper.php b/lib/Helper/SharedRecipesFolderHelper.php new file mode 100644 index 000000000..e8556eb59 --- /dev/null +++ b/lib/Helper/SharedRecipesFolderHelper.php @@ -0,0 +1,39 @@ +config->setSharedRecipesFolderName($path); + $this->cache = null; + } + + /** + * Get the path of the shared recipes relative to the user's root folder + * + * @return string The relative path name + * @throws UserNotLoggedInException If there is currently no logged in user + */ + #[\Override] + public function getPath(): string { + $path = $this->config->getSharedRecipesFolderName(); + + // TODO This was in the original code. Is it still needed? + // $path = str_replace('//', '/', $path); + + return $path; + } +} diff --git a/lib/Helper/UserConfigHelper.php b/lib/Helper/UserConfigHelper.php index a4b2efe51..166a100d5 100644 --- a/lib/Helper/UserConfigHelper.php +++ b/lib/Helper/UserConfigHelper.php @@ -41,6 +41,8 @@ public function __construct( protected const KEY_PRINT_IMAGE = 'print_image'; protected const KEY_VISIBLE_INFO_BLOCKS = 'visible_info_blocks'; protected const KEY_FOLDER = 'folder'; + protected const KEY_MY_RECIPES_FOLDER = 'my_recipes_folder'; + protected const KEY_SHARED_RECIPES_FOLDER = 'shared_recipes_folder'; /** * Checks if the user is logged in and the configuration can be obtained at all @@ -197,7 +199,7 @@ public function setVisibleInfoBlocks(array $visibleInfoBlocks): void { * Do not use this method directly. * Instead use the methods of the UserFolderHelper class * - * @return string The name of the folder within the users files + * @return string The name of the folder within the user's files * @throws UserNotLoggedInException if no user is logged in */ public function getFolderName(): string { @@ -226,4 +228,82 @@ public function getFolderName(): string { public function setFolderName(string $value): void { $this->setRawValue(self::KEY_FOLDER, $value); } + + /** + * Get the name of the user's recipe folder. + * + * If no folder is stored in the config yet, a default setting will be generated and saved. + * + * **Note:** + * Do not use this method directly. + * Instead use the methods of the MyRecipesFolderHelper class + * + * @return string The name of the folder within the user's files + * @throws UserNotLoggedInException if no user is logged in + */ + public function getMyRecipesFolderName(): string { + $rawValue = $this->getRawValue(self::KEY_MY_RECIPES_FOLDER); + + if ($rawValue === '') { + $path = $this->getFolderName() . '/' . $this->l->t('My Recipes'); + $this->setFolderName($path); + + return $path; + } + + return $rawValue; + } + + /** + * Set the folder for the user's recipe folder. + * + * **Note:** + * Do not use this method directly. + * Instead use the methods of the MyRecipesFolderHelper class + * + * @param string $value The name of the folder within the user's files + * @throws UserNotLoggedInException if no user is logged in + */ + public function setMyRecipesFolderName(string $value): void { + $this->setRawValue(self::KEY_MY_RECIPES_FOLDER, $value); + } + + /** + * Get the name of the shared recipes folder. + * + * If no folder is stored in the config yet, a default setting will be generated and saved. + * + * **Note:** + * Do not use this method directly. + * Instead use the methods of the SharedRecipesFolderHelper class + * + * @return string The name of the folder within the user's files + * @throws UserNotLoggedInException if no user is logged in + */ + public function getSharedRecipesFolderName(): string { + $rawValue = $this->getRawValue(self::KEY_SHARED_RECIPES_FOLDER); + + if ($rawValue === '') { + $path = $this->getFolderName() . '/' . $this->l->t('Shared Recipes'); + $this->setFolderName($path); + + return $path; + } + + return $rawValue; + } + + /** + * Set the folder for the shared recipes folder. + * + * **Note:** + * Do not use this method directly. + * Instead use the methods of the SharedRecipesFolderHelper class + * + * @param string $value The name of the folder within the user's files + * @throws UserNotLoggedInException if no user is logged in + */ + public function setSharedRecipesFolderName(string $value): void { + $this->setRawValue(self::KEY_SHARED_RECIPES_FOLDER, $value); + } } diff --git a/lib/Helper/UserFolderHelper.php b/lib/Helper/UserFolderHelper.php index 4d31229a9..a4c7e0466 100644 --- a/lib/Helper/UserFolderHelper.php +++ b/lib/Helper/UserFolderHelper.php @@ -2,80 +2,32 @@ namespace OCA\Cookbook\Helper; -use OCA\Cookbook\Exception\UserFolderNotValidException; -use OCA\Cookbook\Exception\UserFolderNotWritableException; use OCA\Cookbook\Exception\UserNotLoggedInException; -use OCP\Files\FileInfo; -use OCP\Files\Folder; -use OCP\Files\IRootFolder; -use OCP\Files\Node; -use OCP\Files\NotFoundException; -use OCP\Files\NotPermittedException; -use OCP\IL10N; /** * This class caches the access to the user folder throughout the app. * * The user folder is the path, were all recipes are stored. */ -class UserFolderHelper { - /** - * @var UserConfigHelper - */ - private $config; - - /** - * @var IL10N - */ - private $l; - - /** - * @var ?string - */ - private $userId; - - /** - * @var IRootFolder - */ - private $root; - - /** - * The folder with all recipes or null if this is not yet cached - * - * @var ?Node - */ - private $cache; - - public function __construct( - ?string $UserId, - IRootFolder $root, - IL10N $l, - UserConfigHelper $configHelper, - ) { - $this->userId = $UserId; - $this->root = $root; - $this->l = $l; - $this->config = $configHelper; - - $this->cache = null; - } - +class UserFolderHelper extends FolderHelper { /** * Set the current path in the settings relative to the user's root folder * * @param string $path The name of the path to be used for the recipes */ + #[\Override] public function setPath(string $path) { $this->config->setFolderName($path); $this->cache = null; } /** - * Get the path of the recipes relative to the user's root folder + * Get the path of the user app folder relative to the user's root folder * * @return string The relative path name * @throws UserNotLoggedInException If there is currently no logged in user */ + #[\Override] public function getPath(): string { $path = $this->config->getFolderName(); @@ -84,57 +36,4 @@ public function getPath(): string { return $path; } - - /** - * Get the current folder where all recipes are stored of the user - * - * @return Folder The folder containing all recipes - * @throws UserFolderNotValidException If the saved user folder is a file or could not be generated - * @throws UserNotLoggedInException If there is no logged-in user at that time - */ - public function getFolder(): Folder { - // Ensure the cache is built. - if (is_null($this->cache)) { - $path = $this->getPath(); - - // Correct path to be relative to nc root - $path = '/' . $this->userId . '/files/' . $path; - $path = str_replace('//', '/', $path); - - - $this->cache = $this->getOrCreateFolder($path); - } - - return $this->cache; - } - - private function getOrCreateFolder(string $path): Folder { - try { - $node = $this->root->get($path); - } catch (NotFoundException $ex) { - try { - $node = $this->root->newFolder($path); - } catch (NotPermittedException $ex1) { - throw new UserFolderNotValidException( - $this->l->t('The user folder cannot be created due to missing permissions.'), - 0, - $ex1 - ); - } - } - - if ($node->getType() !== FileInfo::TYPE_FOLDER) { - throw new UserFolderNotValidException( - $this->l->t('The configured user folder is a file.') - ); - } - - if (!$node->isCreatable()) { - throw new UserFolderNotWritableException( - $this->l->t('User cannot create recipe folder') - ); - } - - return $node; - } } diff --git a/lib/Service/RecipeService.php b/lib/Service/RecipeService.php index 8abcbbdad..c69e3d379 100755 --- a/lib/Service/RecipeService.php +++ b/lib/Service/RecipeService.php @@ -4,15 +4,18 @@ use Exception; use OCA\Cookbook\Db\RecipeDb; +use OCA\Cookbook\Exception\FolderNotValidException; +use OCA\Cookbook\Exception\FolderNotWritableException; use OCA\Cookbook\Exception\HtmlParsingException; use OCA\Cookbook\Exception\ImportException; use OCA\Cookbook\Exception\NoRecipeNameGivenException; use OCA\Cookbook\Exception\RecipeExistsException; -use OCA\Cookbook\Exception\UserFolderNotWritableException; use OCA\Cookbook\Helper\DownloadHelper; use OCA\Cookbook\Helper\FileSystem\RecipeNameHelper; use OCA\Cookbook\Helper\Filter\JSON\JSONFilter; use OCA\Cookbook\Helper\ImageService\ImageSize; +use OCA\Cookbook\Helper\MyRecipesFolderHelper; +use OCA\Cookbook\Helper\SharedRecipesFolderHelper; use OCA\Cookbook\Helper\UserConfigHelper; use OCA\Cookbook\Helper\UserFolderHelper; use OCP\AppFramework\Db\DoesNotExistException; @@ -39,6 +42,14 @@ class RecipeService { * @var UserFolderHelper */ private $userFolder; + /** + * @var MyRecipesFolderHelper + */ + private $myRecipesFolder; + /** + * @var SharedRecipesFolderHelper + */ + private $sharedRecipesFolder; private $logger; /** @@ -75,6 +86,8 @@ public function __construct( RecipeDb $db, UserConfigHelper $userConfigHelper, UserFolderHelper $userFolder, + MyRecipesFolderHelper $myRecipesFolder, + SharedRecipesFolderHelper $sharedRecipesFolder, ImageService $imageService, RecipeNameHelper $recipeNameHelper, IL10N $il10n, @@ -216,14 +229,14 @@ public function addRecipe($json, $importedHtml = null) { $json['dateModified'] = $now; // Create/move recipe folder - $user_folder = $this->userFolder->getFolder(); + $my_recipes_folder = $this->myRecipesFolder->getFolder(); $recipe_folder = null; $recipeFolderName = $this->recipeNameHelper->getFolderName($json['name']); if (isset($json['id']) && $json['id']) { // Recipe already has an id, update it - $recipe_folder = $user_folder->getById($json['id'])[0]; + $recipe_folder = $my_recipes_folder->getById($json['id'])[0]; if (!($recipe_folder instanceof Folder)) { throw new \RuntimeException($this->il10n->t('Unexpected node received for recipe folder.')); } @@ -233,7 +246,7 @@ public function addRecipe($json, $importedHtml = null) { // The recipe is being renamed, move the folder if ($old_path !== $new_path) { - if ($user_folder->nodeExists($recipeFolderName)) { + if ($my_recipes_folder->nodeExists($recipeFolderName)) { throw new RecipeExistsException($this->il10n->t('Another recipe with that name already exists')); } @@ -244,11 +257,11 @@ public function addRecipe($json, $importedHtml = null) { // This is a new recipe, create it $json['dateCreated'] = $now; - if ($user_folder->nodeExists($recipeFolderName)) { + if ($my_recipes_folder->nodeExists($recipeFolderName)) { throw new RecipeExistsException($this->il10n->t('Another recipe with that name already exists')); } - $recipe_folder = $user_folder->newFolder($recipeFolderName); + $recipe_folder = $my_recipes_folder->newFolder($recipeFolderName); } // Write JSON file to disk @@ -367,8 +380,16 @@ public function downloadRecipe(string $url): File { * @return array */ public function getRecipeFiles() { - $user_folder = $this->userFolder->getFolder(); - $recipe_folders = $user_folder->getDirectoryListing(); + $my_recipes_folder = $this->myRecipesFolder->getFolder(); + $recipe_folders = $my_recipes_folder->getDirectoryListing(); + + $shared_recipes_folder = $this->sharedRecipesFolder->getFolder(); + $shared_recipes_folders = $shared_recipes_folder->getDirectoryListing(); + + foreach ($shared_recipes_folders as $shared_recipe_folder) { + $recipe_folders = [...$recipe_folders, ...$shared_recipe_folder->getDirectoryListing()]; + } + $recipe_files = []; foreach ($recipe_folders as $recipe_folder) { @@ -391,7 +412,7 @@ public function getRecipeFiles() { public function updateSearchIndex() { try { $this->migrateFolderStructure(); - } catch (UserFolderNotWritableException $ex) { + } catch (FolderNotWritableException $ex) { // Ignore migration if not permitted. $this->logger->warning('Cannot migrate cookbook file structure as not permitted.'); throw $ex; @@ -408,25 +429,54 @@ private function migrateFolderStructure() { // Restructure files if needed $user_folder = $this->userFolder->getFolder(); + $my_recipes_folder = $this->myRecipesFolder->getFolder(); + $shared_recipes_folder = $this->sharedRecipesFolder->getFolder(); + + if (!$user_folder->isSubNode($my_recipes_folder) || !$user_folder->isSubNode($shared_recipes_folder)) { + $this->logger->warning('Cannot migrate cookbook file structure as Cookbook subfolders are not within the main Cookbook folder.'); + throw new FolderNotValidException('Cannot migrate cookbook file structure as Cookbook subfolders are not within the main Cookbook folder.'); + } + + if ($user_folder->getOwner()->getUID() !== $this->user_id) { + // moving user folder to a shared folder since it's not owned by the current user + $user_folder->move($shared_recipes_folder->getPath() . '/' . $user_folder->getOwner()->getUID()); + $user_folder = $this->userFolder->getFolder(); + } - foreach ($user_folder->getDirectoryListing() as $node) { + $recipe_folders = [...$user_folder->getDirectoryListing(), ...$my_recipes_folder->getDirectoryListing()]; + + foreach ($recipe_folders as $node) { // Move JSON files from the user directory into its own folder if ($this->isRecipeFile($node)) { $recipe_name = str_replace('.json', '', $node->getName()); $node->move($node->getPath() . '_tmp'); - $recipe_folder = $user_folder->newFolder($recipe_name); + $recipe_folder = $my_recipes_folder->newFolder($recipe_name); $node->move($recipe_folder->getPath() . '/recipe.json'); - } elseif ($node instanceof Folder && strpos($node->getName(), '.json')) { - // Rename folders with .json extensions (this was likely caused by a migration bug) - $node->move(str_replace('.json', '', $node->getPath())); + } elseif ($node instanceof Folder) { + if (strpos($node->getName(), '.json')) { + // Rename folders with .json extensions (this was likely caused by a migration bug) + $node->move($my_recipes_folder->getPath() . '/' . str_replace('.json', '', $node->getName())); + } elseif ($this->getRecipeFileByFolderId($node->getId())) { + if (($node->getOwner()->getUID() == $this->user_id) && ($node->getParent() != $my_recipes_folder)) { + $node->move($my_recipes_folder->getPath() . '/' . $node->getName()); + } elseif (($node->getOwner()->getUID() !== $this->user_id) && ($node->getParent()->getParent() != $shared_recipes_folder)) { + $other_user_folder = $shared_recipes_folder->newFolder($node->getOwner()->GetUID()); + if ($other_user_folder->GetOwner()->getUID() == $this->user_id) { + $node->move($other_user_folder->getPath() . '/' . $node->getName()); + } elseif (($other_user_folder->GetOwner()->getUID() !== $this->user_id) && ($other_user_folder->nodeExists($node->getName()))) { + $node->delete(); + } else { + $this->logger->warning('Could not determine what to do about recipe "' . $node->getName() . '". Please fix it manually (currently located at "' . $node->getPath() . '")'); + } + } + } } } } - /** * Gets all keywords from the index * diff --git a/src/components/Modals/SettingsDialog.vue b/src/components/Modals/SettingsDialog.vue index 7d96821d0..a5cb7ab6f 100644 --- a/src/components/Modals/SettingsDialog.vue +++ b/src/components/Modals/SettingsDialog.vue @@ -32,6 +32,28 @@ @click="pickRecipeFolder" /> +