From 7c495d03ab6b385abc970ae343828ffdceba537b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Thu, 29 Oct 2026 02:11:28 +0100 Subject: [PATCH] fix: let files in a shared folder be renamed and deleted on Stalwart 1.0, which sends rights for folders only --- src/lib/__tests__/files-access.test.ts | 9 +++++++++ src/lib/files-access.ts | 21 +++++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/lib/__tests__/files-access.test.ts b/src/lib/__tests__/files-access.test.ts index 024b8e16..3cd9a456 100644 --- a/src/lib/__tests__/files-access.test.ts +++ b/src/lib/__tests__/files-access.test.ts @@ -32,6 +32,15 @@ describe('fileNodeAccess', () => { expect(fileNodeAccess(nodes.at(-1)!, nodes)).toEqual({ rename: true, delete: true, move: false, duplicate: true }); }); + it('takes the folder\'s rights for a file that names none (Stalwart 1.0)', () => { + const writable = shared(WRITE, WRITE); + const nodes = [...writable.slice(0, 2), { ...writable[2], myRights: undefined }]; + expect(fileNodeAccess(nodes.at(-1)!, nodes)).toEqual({ rename: true, delete: true, move: false, duplicate: true }); + const readOnly = shared(NONE, NONE); + const ro = [...readOnly.slice(0, 2), { ...readOnly[2], myRights: undefined }]; + expect(fileNodeAccess(ro.at(-1)!, ro)).toEqual({ rename: false, delete: false, move: false, duplicate: false }); + }); + it('offers nothing inside a folder shared read-only', () => { const nodes = shared(NONE, NONE); expect(fileNodeAccess(nodes.at(-1)!, nodes)).toEqual({ rename: false, delete: false, move: false, duplicate: false }); diff --git a/src/lib/files-access.ts b/src/lib/files-access.ts index 641ae2f2..25365cfc 100644 --- a/src/lib/files-access.ts +++ b/src/lib/files-access.ts @@ -27,24 +27,37 @@ function sharedParent(node: FileNode, nodes: FileNode[]): FileNode | undefined { return nodes.find((n) => n.id === node.parentId && !n.discoverableOnly); } +// Stalwart 1.0 sends `myRights` on folders only; a file has the rights of +// the folder it is in. Walk up to the nearest node that names its rights. +function effectiveRights(node: FileNode, nodes: FileNode[]): FileNode['myRights'] { + const seen = new Set(); + let current: FileNode | undefined = node; + while (current && !seen.has(current.id)) { + if (current.myRights) return current.myRights; + seen.add(current.id); + const parentId: string | null | undefined = current.parentId; + current = parentId == null ? undefined : nodes.find((n) => n.id === parentId); + } + return undefined; +} + export function fileNodeAccess(node: FileNode, nodes: FileNode[]): FileNodeAccess { if (!node.isShared) return ALL; const parent = sharedParent(node, nodes); if (!parent) return { rename: false, delete: false, move: false, duplicate: false }; - const rights = node.myRights; + const rights = effectiveRights(node, nodes); return { rename: !!rights?.mayRename, delete: !!rights?.mayDelete, move: false, - duplicate: !!parent.myRights?.mayAddChildren, + duplicate: !!effectiveRights(parent, nodes)?.mayAddChildren, }; } /** * Whether new folders and uploads may go into the folder `folderId` (null: * the user's own drive root). Inside a shared folder only with its - * `mayAddChildren`; Stalwart 0.16 reports a child of a shared folder with - * every right false, so there it is offered in the shared folder itself. + * `mayAddChildren`. */ export function mayCreateIn(folderId: string | null, nodes: FileNode[]): boolean { if (folderId === null) return true;