Skip to content

Commit 67694fc

Browse files
authored
Encode form name (#211)
1 parent 0c41c79 commit 67694fc

7 files changed

Lines changed: 80 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
### 1.50.0 - 2026-03-30
2+
- Introduce `Utils.encodeFormName` which is a client-side corollary for `PageFlowUtil.encodeFormName`.
3+
- Update form data binding to encode keys
4+
15
### 1.49.1 - 2026-03-23
26
- Fix Filter parseValue method incorrectly handling invalid JSON values
37
- GH Issue 948: Multi value text choice filters don't work for JSON values, crash the app

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/api",
3-
"version": "1.49.1",
3+
"version": "1.50.0",
44
"description": "JavaScript client API for LabKey Server",
55
"scripts": {
66
"build": "npm run build:dist && npm run build:docs",

src/labkey/Utils.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,34 @@ describe('caseInsensitiveEquals', () => {
4444
});
4545
});
4646

47+
describe('encodeFormName', () => {
48+
test('empty', () => {
49+
expect(Utils.encodeFormName(null)).toBeNull();
50+
expect(Utils.encodeFormName(undefined)).toBeUndefined();
51+
expect(Utils.encodeFormName('')).toBe('');
52+
expect(Utils.encodeFormName(' ')).toBe(' ');
53+
});
54+
55+
test('no relevant special character', () => {
56+
expect(Utils.encodeFormName('a')).toBe('a');
57+
expect(Utils.encodeFormName('$')).toBe('$');
58+
expect(Utils.encodeFormName('9')).toBe('9');
59+
expect(Utils.encodeFormName('[a]')).toBe('[a]');
60+
});
61+
62+
test('encoded', () => {
63+
expect(Utils.encodeFormName('"')).toBe('%_%22');
64+
expect(Utils.encodeFormName('%')).toBe('%_%25');
65+
expect(Utils.encodeFormName('%_beep')).toBe('%_%25_beep');
66+
expect(Utils.encodeFormName('""')).toBe('%_%22%22');
67+
expect(Utils.encodeFormName('"22')).toBe('%_%2222');
68+
expect(Utils.encodeFormName('"a"')).toBe('%_%22a%22');
69+
expect(Utils.encodeFormName('a%22')).toBe('%_a%2522');
70+
expect(Utils.encodeFormName('"a%22')).toBe('%_%22a%2522');
71+
expect(Utils.encodeFormName('"a%222')).toBe('%_%22a%25222');
72+
});
73+
});
74+
4775
describe('ensureRegionName', () => {
4876
it('should return default', () => {
4977
expect(Utils.ensureRegionName()).toEqual('query');

src/labkey/Utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,19 @@ export function encode(data: any): string {
335335
return JSON.stringify(data);
336336
}
337337

338+
/**
339+
* Encodes a form value name for submission to a LabKey Server.
340+
*
341+
* @param name The form value name to encode.
342+
* @return The encoded form value name.
343+
*/
344+
export function encodeFormName(name: string): string {
345+
// Issue 52925, Issue 52119, Issue 54218
346+
// Should be consistent with PageFlowUtil.encodeFormName() on the server
347+
if (!name || !/[\\"%]/.test(name)) return name;
348+
return '%_' + encodeURIComponent(name);
349+
}
350+
338351
/**
339352
* Encodes the html passed in and converts it to a String so that it will not be interpreted as HTML
340353
* by the browser. For example, if your input string was "<p>Hello</p>" the output would be

src/labkey/query/Rows.spec.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -351,35 +351,49 @@ describe('bindSaveRowsData', () => {
351351
const fileA = new File([], '');
352352
const fileB = new File([], '');
353353
const fileC = new File([], '');
354+
const fileD = new File([], '');
355+
const fileE = new File([], '');
354356
const form = bindSaveRowsData({
355357
commands: [
356358
{
357359
...baseCommand,
358360
rows: [
359361
{ myFile: fileA, rowId: 1 },
360362
{ myFile: fileB, rowId: 2 },
363+
{ 'file"Name': fileC, rowId: 3 },
364+
{ 'file\\Name': fileD, rowId: 4 },
361365
],
362366
},
363-
{ ...baseCommand, rows: [{ myFile: fileC, rowId: 3 }] },
367+
{ ...baseCommand, rows: [{ myFile: fileE, rowId: 5 }] },
364368
],
365369
});
370+
expect(Array.from(form.keys()).sort()).toEqual([
371+
'%_file%22Name::0::2',
372+
'%_file%5CName::0::3',
373+
'json',
374+
'myFile::0::0',
375+
'myFile::0::1',
376+
'myFile::1::0',
377+
]);
366378
expect(form.get('myFile::0::0')).toEqual(fileA);
367379
expect(form.get('myFile::0::1')).toEqual(fileB);
368-
expect(form.get('myFile::1::0')).toEqual(fileC);
380+
expect(form.get('%_file%22Name::0::2')).toEqual(fileC);
381+
expect(form.get('%_file%5CName::0::3')).toEqual(fileD);
382+
expect(form.get('myFile::1::0')).toEqual(fileE);
369383
expect(form.get('json')).toEqual(
370384
JSON.stringify({
371385
commands: [
372386
{
373387
schemaName: 'schema',
374388
queryName: 'query',
375389
command: 'update',
376-
rows: [{ rowId: 1 }, { rowId: 2 }],
390+
rows: [{ rowId: 1 }, { rowId: 2 }, { rowId: 3 }, { rowId: 4 }],
377391
},
378392
{
379393
schemaName: 'schema',
380394
queryName: 'query',
381395
command: 'update',
382-
rows: [{ rowId: 3 }],
396+
rows: [{ rowId: 5 }],
383397
},
384398
],
385399
})

src/labkey/query/Rows.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616
import { request, RequestOptions } from '../Ajax';
1717
import { buildURL } from '../ActionURL';
18-
import { getCallbackWrapper, getOnFailure, getOnSuccess, RequestCallbackOptions } from '../Utils';
18+
import { encodeFormName, getCallbackWrapper, getOnFailure, getOnSuccess, RequestCallbackOptions } from '../Utils';
1919
import { AuditBehaviorTypes } from '../constants';
2020

2121
export interface QueryRequestOptions extends RequestCallbackOptions {
@@ -273,18 +273,17 @@ export interface SaveRowsResponse {
273273
}
274274

275275
export interface SaveRowsOptions extends RequestCallbackOptions<SaveRowsResponse> {
276-
277-
/**
278-
* Optional audit details to record in the transaction audit log for this command.
279-
*/
280-
auditDetails?: Record<string, any>;
281276
/**
282277
* Version of the API. If this is 13.2 or higher, a request that fails
283278
* validation will be returned as a successful response. Use the 'errorCount' and 'committed' properties in the
284279
* response to tell if it committed or not. If this is 13.1 or lower (or unspecified), the failure callback
285280
* will be invoked instead in the event of a validation failure.
286281
*/
287-
apiVersion?: string | number;
282+
apiVersion?: number | string;
283+
/**
284+
* Optional audit details to record in the transaction audit log for this command.
285+
*/
286+
auditDetails?: Record<string, any>;
288287
/** An array of the update/insert/delete operations to be performed. */
289288
commands: Command[];
290289
/**
@@ -303,15 +302,14 @@ export interface SaveRowsOptions extends RequestCallbackOptions<SaveRowsResponse
303302
*/
304303
timeout?: number;
305304
/**
306-
* Whether all of the row changes for all of the tables
307-
* should be done in a single transaction, so they all succeed or all fail. Defaults to true.
305+
* Whether all the row changes for all the tables should be done in a single transaction,
306+
* so they all succeed or all fail. Defaults to true.
308307
*/
309308
transacted?: boolean;
310309
/**
311-
* Whether or not the server should attempt proceed through all of the
312-
* commands, but not actually commit them to the database. Useful for scenarios like giving incremental
313-
* validation feedback as a user fills out a UI form, but not actually save anything until they explicitly request
314-
* a save.
310+
* Whether the server should attempt to proceed through all the commands but not commit them to the database.
311+
* Useful for scenarios like giving incremental validation feedback as a user fills out a UI form but does not save
312+
* anything until they explicitly request a save.
315313
*/
316314
validateOnly?: boolean;
317315
}
@@ -336,7 +334,7 @@ function bindSaveRowsCommand(form: FormData, command: Command, commandIndex: num
336334

337335
Object.keys(updatedRow).forEach(key => {
338336
if (updatedRow[key] instanceof File) {
339-
form.append(`${key}::${commandIndex}::${rowIndex}`, updatedRow[key]);
337+
form.append(`${encodeFormName(key)}::${commandIndex}::${rowIndex}`, updatedRow[key]);
340338
delete updatedRow[key];
341339
}
342340
});
@@ -420,7 +418,7 @@ export function bindFormData(jsonData: { rows?: any[] }, options: SendRequestOpt
420418
form = new FormData();
421419

422420
// Process and extract File data with row offsets
423-
const rows: Array<Record<string, any>> = [];
421+
const rows: Record<string, any>[] = [];
424422

425423
jsonData.rows.forEach((row, i) => {
426424
if (row) {
@@ -429,7 +427,7 @@ export function bindFormData(jsonData: { rows?: any[] }, options: SendRequestOpt
429427
Object.keys(row).forEach(k => {
430428
// Extract File values from the row
431429
if (row[k] instanceof File) {
432-
form.append(`${k}::${i}`, row[k]);
430+
form.append(`${encodeFormName(k)}::${i}`, row[k]);
433431
} else {
434432
_row[k] = row[k];
435433
}

0 commit comments

Comments
 (0)