From c17613669c3f4a9ee31a34e0e117dd04033a69e3 Mon Sep 17 00:00:00 2001 From: Juan Fernando Castro Date: Sat, 29 Aug 2026 04:42:46 -0400 Subject: [PATCH] fix: open read-only shared files instead of forwarding write_url=undefined (#3667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening a file you hold read-but-not-write access on (a read-only share) failed silently. For such a file the backend correctly omits write_url from the /open_item signature, but launchApp appended it to the app iframe URL unconditionally. URLSearchParams coerces undefined to the string \"undefined\", so the app received puter.item.write_url=\"undefined\" — truthy, so the editor believed the file was writable, and an invalid URL, so it broke on open. Only read-only shares hit this; files you can write carry a real write_url. Extract the puter.item.* param building into append_signed_item_params and guard the write_url append so it is only added when present. launchApp.js is too coupled to UIWindow/jQuery/window globals to unit-test directly, so the pure helper carries the logic and its own test, matching the helpers/ pattern. Regression test pins both directions: a read-only signature omits write_url entirely (no \"undefined\"), and a writable signature still forwards it. --- src/gui/src/helpers/appendSignedItemParams.js | 41 ++++++++++++ .../helpers/appendSignedItemParams.test.js | 65 +++++++++++++++++++ src/gui/src/helpers/launchApp.js | 16 ++--- 3 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/gui/src/helpers/appendSignedItemParams.js create mode 100644 src/gui/src/helpers/appendSignedItemParams.test.js diff --git a/src/gui/src/helpers/appendSignedItemParams.js b/src/gui/src/helpers/appendSignedItemParams.js new file mode 100644 index 000000000..0c03b9176 --- /dev/null +++ b/src/gui/src/helpers/appendSignedItemParams.js @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter 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 . + */ + +/** + * Append a signed file's `puter.item.*` params onto the launch iframe URL. + * + * @param {URLSearchParams} searchParams the iframe URL's search params + * @param {object} file_signature the /sign or /open_item signature object + * @param {string} itemPath the (privacy-aware) path to advertise to the app + */ +export const append_signed_item_params = (searchParams, file_signature, itemPath) => { + searchParams.append('puter.item.uid', file_signature.uid); + searchParams.append('puter.item.path', itemPath); + searchParams.append('puter.item.name', file_signature.fsentry_name); + searchParams.append('puter.item.read_url', file_signature.read_url); + // Read-only shares carry no write_url; appending undefined stringifies it to "undefined", a truthy invalid URL that stops the editor opening the file. + if ( file_signature.write_url ) { + searchParams.append('puter.item.write_url', file_signature.write_url); + } + searchParams.append('puter.item.metadata_url', file_signature.metadata_url); + searchParams.append('puter.item.size', file_signature.fsentry_size); + searchParams.append('puter.item.accessed', file_signature.fsentry_accessed); + searchParams.append('puter.item.modified', file_signature.fsentry_modified); + searchParams.append('puter.item.created', file_signature.fsentry_created); +}; diff --git a/src/gui/src/helpers/appendSignedItemParams.test.js b/src/gui/src/helpers/appendSignedItemParams.test.js new file mode 100644 index 000000000..290dfe680 --- /dev/null +++ b/src/gui/src/helpers/appendSignedItemParams.test.js @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024-present Puter Technologies Inc. + * + * This file is part of Puter. + * + * Puter 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 . + */ + +import { describe, it, expect } from 'vitest'; +import { append_signed_item_params } from './appendSignedItemParams.js'; + +const readOnlySignature = { + uid: 'uid-1', + fsentry_name: 'shared.txt', + read_url: 'https://api.puter.test/file?uid=uid-1&signature=read', + // write_url intentionally absent — the backend strips it for a read-only share. + metadata_url: 'https://api.puter.test/itemMetadata?uid=uid-1&signature=read', + fsentry_size: 12, + fsentry_accessed: 1, + fsentry_modified: 2, + fsentry_created: 3, + path: '/owner/shared.txt', +}; + +describe('append_signed_item_params', () => { + // Appending an absent write_url stringifies it to a truthy, invalid "undefined" that stops the editor opening; it must be omitted. + it('omits write_url for a read-only share instead of forwarding "undefined"', () => { + const params = new URLSearchParams(); + append_signed_item_params(params, readOnlySignature, '/owner/shared.txt'); + + expect(params.has('puter.item.write_url')).toBe(false); + expect(params.get('puter.item.write_url')).toBeNull(); + expect(params.get('puter.item.read_url')).toBe(readOnlySignature.read_url); + }); + + it('forwards write_url when the share grants write', () => { + const params = new URLSearchParams(); + append_signed_item_params( + params, + { ...readOnlySignature, write_url: 'https://api.puter.test/writeFile?uid=uid-1&signature=write' }, + '/owner/shared.txt', + ); + + expect(params.get('puter.item.write_url')).toBe( + 'https://api.puter.test/writeFile?uid=uid-1&signature=write', + ); + }); + + it('advertises the passed path rather than the signature path', () => { + const params = new URLSearchParams(); + append_signed_item_params(params, readOnlySignature, '~/shared.txt'); + expect(params.get('puter.item.path')).toBe('~/shared.txt'); + }); +}); diff --git a/src/gui/src/helpers/launchApp.js b/src/gui/src/helpers/launchApp.js index c51083fa3..4d3b3ff48 100644 --- a/src/gui/src/helpers/launchApp.js +++ b/src/gui/src/helpers/launchApp.js @@ -21,6 +21,7 @@ import path from '../lib/path.js'; import { PROCESS_IPC_ATTACHED, PROCESS_RUNNING, PortalProcess, PseudoProcess } from '../definitions.js'; import UIWindow from '../UI/UIWindow.js'; import { starts_hidden } from './startsHidden.js'; +import { append_signed_item_params } from './appendSignedItemParams.js'; const normalizePrivateAccessDecision = (privateAccess) => { if ( !privateAccess || typeof privateAccess !== 'object' ) { @@ -463,16 +464,11 @@ const launch_app = async (options) => { } if ( file_signature ) { - iframe_url.searchParams.append('puter.item.uid', file_signature.uid); - iframe_url.searchParams.append('puter.item.path', options.file_path ? privacy_aware_path(options.file_path) : file_signature.path); - iframe_url.searchParams.append('puter.item.name', file_signature.fsentry_name); - iframe_url.searchParams.append('puter.item.read_url', file_signature.read_url); - iframe_url.searchParams.append('puter.item.write_url', file_signature.write_url); - iframe_url.searchParams.append('puter.item.metadata_url', file_signature.metadata_url); - iframe_url.searchParams.append('puter.item.size', file_signature.fsentry_size); - iframe_url.searchParams.append('puter.item.accessed', file_signature.fsentry_accessed); - iframe_url.searchParams.append('puter.item.modified', file_signature.fsentry_modified); - iframe_url.searchParams.append('puter.item.created', file_signature.fsentry_created); + append_signed_item_params( + iframe_url.searchParams, + file_signature, + options.file_path ? privacy_aware_path(options.file_path) : file_signature.path, + ); } else if ( options.readURL ) { iframe_url.searchParams.append('puter.item.name', options.filename);