diff --git a/lib/client-api/src/args.ts b/lib/client-api/src/args.ts index f2816057fea..f65506cc421 100644 --- a/lib/client-api/src/args.ts +++ b/lib/client-api/src/args.ts @@ -7,6 +7,8 @@ type ValueType = { name: string; value?: ObjectValueType | ValueType }; type ObjectValueType = Record; const INCOMPATIBLE = Symbol('incompatible'); +const NUMBER_REGEXP = /^-?[0-9]+(\.[0-9]+)?$/; + const map = (arg: unknown, type: ValueType): any => { if (arg === undefined || arg === null || !type) return arg; switch (type.name) { @@ -26,6 +28,7 @@ const map = (arg: unknown, type: ValueType): any => { return acc; }, new Array(arg.length)); case 'object': + if (typeof arg === 'string') return NUMBER_REGEXP.test(arg) ? Number(arg) : arg; if (!type.value || typeof arg !== 'object') return INCOMPATIBLE; return Object.entries(arg).reduce((acc, [key, val]) => { const mapped = map(val, (type.value as ObjectValueType)[key]); diff --git a/lib/core-client/src/preview/parseArgsParam.test.ts b/lib/core-client/src/preview/parseArgsParam.test.ts index 4208791b713..9186c0217fb 100644 --- a/lib/core-client/src/preview/parseArgsParam.test.ts +++ b/lib/core-client/src/preview/parseArgsParam.test.ts @@ -225,6 +225,14 @@ describe('parseArgsParam', () => { expect(parseArgsParam('key:1')).toStrictEqual({ key: '1' }); }); + it('allows valid fractional numbers', () => { + expect(parseArgsParam('key:1.2')).toStrictEqual({ key: '1.2' }); + expect(parseArgsParam('key:-1.2')).toStrictEqual({ key: '-1.2' }); + expect(parseArgsParam('key:1.')).toStrictEqual({}); + expect(parseArgsParam('key:.2')).toStrictEqual({}); + expect(parseArgsParam('key:1.2.3')).toStrictEqual({}); + }); + it('also applies to nested object and array values', () => { expect(parseArgsParam('obj.key:a!b')).toStrictEqual({}); expect(parseArgsParam('obj[key]:a!b')).toStrictEqual({}); diff --git a/lib/core-client/src/preview/parseArgsParam.ts b/lib/core-client/src/preview/parseArgsParam.ts index b0e79f9aa05..1bdebabeb9f 100644 --- a/lib/core-client/src/preview/parseArgsParam.ts +++ b/lib/core-client/src/preview/parseArgsParam.ts @@ -6,6 +6,7 @@ import isPlainObject from 'lodash/isPlainObject'; // Keep this in sync with validateArgs in router/src/utils.ts const VALIDATION_REGEXP = /^[a-zA-Z0-9 _-]*$/; +const NUMBER_REGEXP = /^-?[0-9]+(\.[0-9]+)?$/; const HEX_REGEXP = /^#([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i; const COLOR_REGEXP = /^(rgba?|hsla?)\(([0-9]{1,3}),\s?([0-9]{1,3})%?,\s?([0-9]{1,3})%?,?\s?([0-9](\.[0-9]{1,2})?)?\)$/i; const validateArgs = (key = '', value: unknown): boolean => { @@ -14,8 +15,14 @@ const validateArgs = (key = '', value: unknown): boolean => { if (value === null || value === undefined) return true; // encoded as `!null` or `!undefined` if (value instanceof Date) return true; // encoded as modified ISO string if (typeof value === 'number' || typeof value === 'boolean') return true; - if (typeof value === 'string') - return VALIDATION_REGEXP.test(value) || HEX_REGEXP.test(value) || COLOR_REGEXP.test(value); + if (typeof value === 'string') { + return ( + VALIDATION_REGEXP.test(value) || + NUMBER_REGEXP.test(value) || + HEX_REGEXP.test(value) || + COLOR_REGEXP.test(value) + ); + } if (Array.isArray(value)) return value.every((v) => validateArgs(key, v)); if (isPlainObject(value)) return Object.entries(value).every(([k, v]) => validateArgs(k, v)); return false; diff --git a/lib/router/src/utils.ts b/lib/router/src/utils.ts index 20743e00e3a..0831485f8e0 100644 --- a/lib/router/src/utils.ts +++ b/lib/router/src/utils.ts @@ -63,6 +63,7 @@ export const deepDiff = (value: any, update: any): any => { // Keep this in sync with validateArgs in core-client/src/preview/parseArgsParam.ts const VALIDATION_REGEXP = /^[a-zA-Z0-9 _-]*$/; +const NUMBER_REGEXP = /^-?[0-9]+(\.[0-9]+)?$/; const HEX_REGEXP = /^#([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i; const COLOR_REGEXP = /^(rgba?|hsla?)\(([0-9]{1,3}),\s?([0-9]{1,3})%?,\s?([0-9]{1,3})%?,?\s?([0-9](\.[0-9]{1,2})?)?\)$/i; const validateArgs = (key = '', value: unknown): boolean => { @@ -71,8 +72,14 @@ const validateArgs = (key = '', value: unknown): boolean => { if (value === null || value === undefined) return true; // encoded as `!null` or `!undefined` if (value instanceof Date) return true; // encoded as modified ISO string if (typeof value === 'number' || typeof value === 'boolean') return true; - if (typeof value === 'string') - return VALIDATION_REGEXP.test(value) || HEX_REGEXP.test(value) || COLOR_REGEXP.test(value); + if (typeof value === 'string') { + return ( + VALIDATION_REGEXP.test(value) || + NUMBER_REGEXP.test(value) || + HEX_REGEXP.test(value) || + COLOR_REGEXP.test(value) + ); + } if (Array.isArray(value)) return value.every((v) => validateArgs(key, v)); if (isPlainObject(value)) return Object.entries(value).every(([k, v]) => validateArgs(k, v)); return false;