From 60683361ac228e4010497fc933ca634c563d8db9 Mon Sep 17 00:00:00 2001 From: Caleb CGates Date: Tue, 12 May 2026 14:14:35 -0400 Subject: [PATCH] fix(amis-core): guard tpl formatDate and date filter against unparseable input --- packages/amis-core/src/utils/filter.ts | 14 ++++- packages/amis-core/src/utils/tpl-lodash.ts | 22 ++++++- packages/amis/__tests__/utils/tpl.test.ts | 67 ++++++++++++++++++++++ 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/packages/amis-core/src/utils/filter.ts b/packages/amis-core/src/utils/filter.ts index dae6a47ba22..d6d972701a2 100644 --- a/packages/amis-core/src/utils/filter.ts +++ b/packages/amis-core/src/utils/filter.ts @@ -145,8 +145,18 @@ extendsFilters({ [modifier === 'add' ? 'add' : 'subtract'](parseInt(amount, 10) || 0, unit) .toDate(); }, - date: (input, format = 'LLL', inputFormat = 'X') => - moment(input, inputFormat).format(format), + date: (input, format = 'LLL', inputFormat = 'X') => { + // 与 tpl-lodash.ts formatDate 一致的守卫: + // input 为空 / 解析失败时不要静默渲染 "Invalid date" 字符串。 + if (input == null || input === '') { + return ''; + } + const m = moment(input, inputFormat); + if (!m.isValid()) { + return String(input); + } + return m.format(format); + }, number: input => { let parts = String(input).split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); diff --git a/packages/amis-core/src/utils/tpl-lodash.ts b/packages/amis-core/src/utils/tpl-lodash.ts index 809f30ac0d1..705bf8beaf0 100644 --- a/packages/amis-core/src/utils/tpl-lodash.ts +++ b/packages/amis-core/src/utils/tpl-lodash.ts @@ -21,8 +21,26 @@ const imports = { return Math.ceil((date.getTime() - now) / (1000 * 60 * 60 * 24)) + '天'; }, - formatDate: (value: any, format: string = 'LLL', inputFormat: string = '') => - moment(value, inputFormat).format(format) + formatDate: ( + value: any, + format: string = 'LLL', + inputFormat: string = '' + ) => { + // 当模板里 value 是用户配置 / 接口返回的脏数据时: + // 1) 字符串解析失败时 moment(value, inputFormat) 返回 invalid moment, + // .format() 静默返回字符串 "Invalid date" 直接渲染到页面里。 + // 2) value 为 null/undefined 时 moment 默认会回退到 "现在"(now), + // 会让 typo 字段(如 data.creatAt)静默渲染成今天的日期。 + // 这里两种情况都做守卫:null/undefined 回显空串,无法解析回显原值。 + if (value == null || value === '') { + return ''; + } + const m = moment(value, inputFormat); + if (!m.isValid()) { + return String(value); + } + return m.format(format); + } }; // 缓存一下提升性能 diff --git a/packages/amis/__tests__/utils/tpl.test.ts b/packages/amis/__tests__/utils/tpl.test.ts index 8da34267907..019e8ad0afc 100644 --- a/packages/amis/__tests__/utils/tpl.test.ts +++ b/packages/amis/__tests__/utils/tpl.test.ts @@ -17,3 +17,70 @@ test('filter', () => { }) ).toEqual('xxx_a=1&b=2'); }); + +test('filter:formatDate echoes unparseable input instead of rendering "Invalid date"', () => { + // Before the fix this rendered the literal string "Invalid date" into the page. + // After the fix the bad input is echoed back so the upstream caller can + // decide how to display it (or it's empty for null/undefined). + expect( + filter('<%= formatDate(data.d, "YYYY-MM-DD") %>', {d: 'not-a-real-date'}) + ).toEqual('not-a-real-date'); + + expect(filter('<%= formatDate(data.d, "YYYY-MM-DD") %>', {d: null})).toEqual( + '' + ); + + expect( + filter('<%= formatDate(data.d, "YYYY-MM-DD") %>', {d: undefined}) + ).toEqual(''); + + // Critical: must NOT render the silent "Invalid date" string anywhere. + expect( + filter('<%= formatDate(data.d, "YYYY-MM-DD") %>', {d: 'not-a-real-date'}) + ).not.toMatch(/Invalid date/); +}); + +test('filter:formatDate still formats valid input correctly', () => { + // Round-trip a known ISO date through the inputFormat overload. + expect( + filter('<%= formatDate(data.d, "YYYY-MM-DD", "YYYY-MM-DD") %>', { + d: '2024-01-15' + }) + ).toEqual('2024-01-15'); + + // Default (no inputFormat) path still works on standard ISO timestamps. + expect( + filter('<%= formatDate(data.d, "YYYY-MM-DD") %>', { + d: '2024-01-15T00:00:00Z' + }) + ).toEqual('2024-01-15'); +}); + +// `date` filter (from amis-core/src/utils/filter.ts) — also exposed in lodash +// templates as `formatTimeStamp: filters.date` (see tpl-lodash.ts:50). Same +// one-liner shape as `formatDate` before the guards landed; same failure mode. +test('filter:date guards against null/undefined/empty input', () => { + // formatTimeStamp is filters.date with inputFormat defaulting to "X" (Unix s). + expect( + filter('<%= formatTimeStamp(data.d, "YYYY-MM-DD") %>', {d: null}) + ).toEqual(''); + expect( + filter('<%= formatTimeStamp(data.d, "YYYY-MM-DD") %>', {d: undefined}) + ).toEqual(''); + expect(filter('<%= formatTimeStamp(data.d, "YYYY-MM-DD") %>', {d: ''})).toEqual( + '' + ); + + // Unparseable input echoes back the original — must NOT surface "Invalid date". + expect( + filter('<%= formatTimeStamp(data.d, "YYYY-MM-DD") %>', {d: 'not-a-real-date'}) + ).toEqual('not-a-real-date'); + expect( + filter('<%= formatTimeStamp(data.d, "YYYY-MM-DD") %>', {d: 'not-a-real-date'}) + ).not.toMatch(/Invalid date/); + + // Valid Unix-timestamp-seconds input still formats correctly. + expect( + filter('<%= formatTimeStamp(data.d, "YYYY-MM-DD") %>', {d: '1705276800'}) + ).toEqual('2024-01-15'); +});