mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-12 00:05:38 +00:00
fix: open read-only shared files instead of forwarding write_url=undefined (#3667)
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.
This commit is contained in:
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user