nextcloud / nextcloud/cookbook

Bug: Renaming the recipe folder causes a silent empty-folder recreation loop (with solutions)

Open
#3,236 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
HTML
Stars
641
Forks
113
Avg merge
21h 31m
Merged PRs (30d)
26

Description

Bug: Renaming the recipe folder causes a silent empty-folder recreation loop

App: Cookbook 0.11.7
Platform: Nextcloud 34.0.0 · MariaDB · Docker · Group folder


Summary

When a user renames their configured recipe folder via Nextcloud Files, Cookbook's stored path in oc_preferences becomes stale. On every subsequent page load, UserFolderHelper::getOrCreateFolder() silently creates a new empty folder at the old path rather than surfacing an error. This makes all recipes invisible with no diagnostic message, and the empty folder reappears immediately after the user deletes it — creating an inescapable loop until the database is corrected manually via occ.


Steps to Reproduce
  1. Configure Cookbook to use a folder, e.g. /Family Drive/Family_Recipes.
  2. Outside of Cookbook — via Nextcloud Files or a desktop client — rename that folder to Family Recipes (with a space).
  3. Return to Cookbook. Recipes are gone. No error is displayed.
  4. Open Nextcloud Files: a new, empty Family_Recipes folder has been created by Cookbook.
  5. Delete the empty folder. Reload Cookbook. The empty folder immediately reappears.
  6. The loop cannot be broken from the UI; it requires a manual occ user:setting correction.

Current vs Expected Behaviour

Current: When the configured folder is not found, getOrCreateFolder() catches NotFoundException and silently calls $this->root->newFolder($path), creating an empty folder at the stale path. Cookbook then operates against this empty folder, returning zero recipes. No message is shown to the user.

Expected: When the configured folder does not exist, Cookbook should surface a clear, actionable error — e.g. "Recipe folder '/Family Drive/Family_Recipes' was not found. Please update it in Settings." — and not silently mutate the filesystem.


Root Cause

The path stored in oc_preferences (appid = 'cookbook', configkey = 'folder') is never updated when the folder is renamed outside of Cookbook. Nextcloud has no mechanism to notify Cookbook of filesystem renames unless Cookbook explicitly listens for them.

The secondary failure is that the app treats a missing configured folder as a first-run condition and recreates it — appropriate at initial setup, destructive when the configuration is merely stale.

lib/Helper/UserFolderHelper.php — current problematic code:

private function getOrCreateFolder(string $path): Folder {
    try {
        $node = $this->root->get($path);
    } catch (NotFoundException $ex) {
        try {
            $node = $this->root->newFolder($path);  // ← silently creates empty folder
        } catch (NotPermittedException $ex1) {
            throw new UserFolderNotValidException(
                $this->l->t('The user folder cannot be created due to missing permissions.'),
                0, $ex1
            );
        }
    }
    // ...
}

Proposed Fix — Option 1 (Minimal): Throw instead of recreate

Remove the silent folder creation and throw UserFolderNotValidException when the configured path is missing. This stops the recreation loop and surfaces an actionable error to the user. The user still needs to re-select their folder in Settings.

Note on first-run: If getOrCreateFolder() is relied upon to create the initial /Recipes folder on first use, that creation should be moved to a dedicated setup step triggered only when no folder has ever been configured, rather than running on every page load.

lib/Helper/UserFolderHelper.php:

 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
-            );
-        }
+        throw new UserFolderNotValidException(
+            $this->l->t(
+                'Recipe folder "%s" was not found. Please update your cookbook folder in Settings.',
+                [$path]
+            ),
+            0,
+            $ex
+        );
     }

     if ($node->getType() !== FileInfo::TYPE_FOLDER) {
         throw new UserFolderNotValidException(
             $this->l->t('The configured user folder is a file.')
         );
     }
     // ...
 }

Proposed Fix — Option 2 (Complete): Auto-update path on rename via Nextcloud event

Register a listener for NodeRenamedEvent. When a node is renamed and its old path matches (or is a parent of) the user's configured recipe folder, the stored path is updated automatically. The user renames their folder in Files and Cookbook follows — no Settings visit required.

This fix includes Option 1: the throw-not-create change is still required as a safety net for cases where the folder is deleted entirely rather than renamed.

Step 1 — Create lib/Listener/RecipeFolderRenamedListener.php (new file):

<?php

namespace OCA\Cookbook\Listener;

use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Files\Events\Node\NodeRenamedEvent;
use OCP\IConfig;
use OCP\IUserSession;

/**
 * When the user renames the configured recipe folder, keep
 * the stored oc_preferences path in sync automatically.
 */
class RecipeFolderRenamedListener implements IEventListener {
    private const APP_ID     = 'cookbook';
    private const CONFIG_KEY = 'folder';

    public function __construct(
        private IConfig $config,
        private IUserSession $userSession,
    ) {}

    public function handle(Event $event): void {
        if (!($event instanceof NodeRenamedEvent)) {
            return;
        }

        $user = $this->userSession->getUser();
        if ($user === null) {
            return;
        }

        $userId     = $user->getUID();
        $sourcePath = $event->getSource()->getPath();  // /userId/files/old/path
        $targetPath = $event->getTarget()->getPath();  // /userId/files/new/path

        // Strip the /userId/files prefix to get the user-relative path
        $filesRoot = '/' . $userId . '/files';
        if (!str_starts_with($sourcePath, $filesRoot)) {
            return;
        }

        $sourceRelative = substr($sourcePath, strlen($filesRoot));  // /old/path
        $storedFolder   = $this->config->getUserValue($userId, self::APP_ID, self::CONFIG_KEY);

        // Match if the stored folder IS the renamed node, or is inside it
        if ($storedFolder !== $sourceRelative
            && !str_starts_with($storedFolder, $sourceRelative . '/')
        ) {
            return;
        }

        $targetRelative = substr($targetPath, strlen($filesRoot));  // /new/path
        $newFolder      = $targetRelative . substr($storedFolder, strlen($sourceRelative));

        $this->config->setUserValue($userId, self::APP_ID, self::CONFIG_KEY, $newFolder);
    }
}

Step 2 — Register the listener in lib/AppInfo/Application.php:

 use OCP\AppFramework\Bootstrap\IBootContext;
+use OCP\Files\Events\Node\NodeRenamedEvent;
+use OCA\Cookbook\Listener\RecipeFolderRenamedListener;

 public function boot(IBootContext $context): void {
     $context->injectFn(function (
         /* existing injections ... */
+        IEventDispatcher $dispatcher,
     ) {
+        $dispatcher->addServiceListener(
+            NodeRenamedEvent::class,
+            RecipeFolderRenamedListener::class,
+        );
     });
 }

Edge cases handled:

  • Parent folder renamed — if /Family Drive itself is renamed, str_starts_with($storedFolder, $sourceRelative . '/') catches it and the sub-path is preserved in the updated value.
  • Unrelated renames — the guard returns early, so the listener is a no-op for all other filesystem operations.
  • Folder moved (not just renamed) — Nextcloud models moves as renames; NodeRenamedEvent fires for both.
  • Folder deleted entirely — not covered by the event listener; Option 1's throw-not-create is the safety net for this case.

Additional Code Defects (lower severity)
Inverted condition in RestParameterParser::getEncoding()

substr_compare() returns 0 (falsy) on a match, so the condition below is true when the string does not begin with charset= — the opposite of the comment. Charset extraction never triggers. No observable impact today since the UTF-8 fallback default is always correct, but the logic is wrong.

lib/Helper/RestParameterParser.php:

-if (substr_compare(trim($parts[$i]), self::CHARSET, 0, strlen(self::CHARSET))) {
+if (substr_compare(trim($parts[$i]), self::CHARSET, 0, strlen(self::CHARSET)) === 0) {
     $enc = substr(trim($parts[$i]), strlen(self::CHARSET) + 1);
     break;
 }
Inverted condition in RestParameterParser::parseUrlEncoded()

The same inversion affects []-suffixed array key detection. The body runs when the key does not end in [], stripping two characters from ordinary keys instead. Not triggered by the current JS client (which sends JSON), but would affect any future non-POST, URL-encoded caller.

lib/Helper/RestParameterParser.php:

-if (substr_compare($key, '[]', -2, 2)) {   // true when key does NOT end in []
+if (substr_compare($key, '[]', -2, 2) === 0) {  // true when key ends in []
     $key = substr($key, 0, -2);
     // ...
 }

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with lib/Helper/UserFolderHelper.php and trace how missing folders are handled, then inspect lib/AppInfo/Application.php for event registration. If implementing rename tracking, add the listener described for NodeRenamedEvent and verify both renamed and missing folders behave safely. Review lib/Helper/RestParameterParser.php separately for the two inverted conditions; done means stale folders are surfaced rather than recreated and the specified parser cases work correctly.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.