From 37ac0c7c318039676548064b705ebf74dd745fe2 Mon Sep 17 00:00:00 2001 From: arpandhakal Date: Mon, 3 Nov 2025 14:08:04 +0545 Subject: [PATCH 1/2] fix(OUT-2541): created safeCompile util wrapping handlebars to safely process templates with custom labels. - safeCompile.ts is a wrapper for handlebars designed to replace customFields ending with a . and register helperMissing with and empty string. - compiled templates with strict: false in custom handleBars compiler : safeCompile. - used safeCompile instead of directly using Handlebars?.compile in all places of the app. --- src/app/client-preview/page.tsx | 20 ++++++++++---------- src/app/components/EditorInterface.tsx | 3 ++- src/hooks/useAppData.tsx | 6 +++--- src/utils/safeCompile.ts | 14 ++++++++++++++ 4 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 src/utils/safeCompile.ts diff --git a/src/app/client-preview/page.tsx b/src/app/client-preview/page.tsx index 2c85907..3e4261e 100644 --- a/src/app/client-preview/page.tsx +++ b/src/app/client-preview/page.tsx @@ -1,17 +1,17 @@ -import Handlebars from 'handlebars' -import { IClient, ICustomField, ISettings } from '@/types/interfaces' -import ClientPreview from '../components/ClientPreview' import { apiUrl } from '@/config' +import { IClient, ICustomField, ISettings } from '@/types/interfaces' +import { defaultBannerImagePath, defaultBgColor } from '@/utils/constants' +import { CopilotAPI } from '@/utils/copilotApiUtils' +import { prepareCustomLabel } from '@/utils/customLabels' +import { getPreviewMode } from '@/utils/previewMode' +import { safeCompile } from '@/utils/safeCompile' +import { preprocessTemplate } from '@/utils/string' import Image from 'next/image' import { z } from 'zod' -import { CopilotAPI } from '@/utils/copilotApiUtils' -import InvalidToken from '../components/InvalidToken' import { defaultState } from '../../../defaultState' -import { defaultBannerImagePath, defaultBgColor } from '@/utils/constants' -import { getPreviewMode } from '@/utils/previewMode' +import ClientPreview from '../components/ClientPreview' +import InvalidToken from '../components/InvalidToken' import { NoPreviewSupport } from './NoPreviewSupport' -import { preprocessTemplate } from '@/utils/string' -import { prepareCustomLabel } from '@/utils/customLabels' export const revalidate = 0 @@ -128,7 +128,7 @@ export default async function ClientPreviewPage({ } } - const template = Handlebars?.compile( + const template = safeCompile( preprocessTemplate( prepareCustomLabel(settings?.content, workspace.labels, { isClientMode: true, diff --git a/src/app/components/EditorInterface.tsx b/src/app/components/EditorInterface.tsx index 138b2a5..ecc92d6 100644 --- a/src/app/components/EditorInterface.tsx +++ b/src/app/components/EditorInterface.tsx @@ -63,6 +63,7 @@ import { Box } from '@mui/material' import Image from 'next/image' import { defaultState } from '../../../defaultState' import { preprocessTemplate } from '@/utils/string' +import { safeCompile } from '@/utils/safeCompile' interface IEditorInterface { settings: ISettings | null @@ -185,7 +186,7 @@ const EditorInterface = ({ useEffect(() => { if (appState?.appState.readOnly) { - const template = Handlebars?.compile( + const template = safeCompile( preprocessTemplate( prepareCustomLabel( replaceCustomLabelsWithPlaceholders( diff --git a/src/hooks/useAppData.tsx b/src/hooks/useAppData.tsx index 0f5ab11..66f4a1e 100644 --- a/src/hooks/useAppData.tsx +++ b/src/hooks/useAppData.tsx @@ -1,8 +1,8 @@ import { createContext, PropsWithChildren, useContext, useMemo } from 'react' import { useAppState } from '@/hooks/useAppState' import { IClient, INotification } from '@/types/interfaces' -import Handlebars from 'handlebars' -import { CustomField, CustomFieldsSchema } from '@/types/common' +import { CustomField } from '@/types/common' +import { safeCompile } from '@/utils/safeCompile' const AppDataContext = createContext>({}) @@ -17,7 +17,7 @@ export const useAppData = (template: string) => { return template } - return Handlebars?.compile(template || '')(appData) + return safeCompile(template || '')(appData) } export const AppDataProvider = ({ children }: PropsWithChildren) => { diff --git a/src/utils/safeCompile.ts b/src/utils/safeCompile.ts new file mode 100644 index 0000000..7fee4a5 --- /dev/null +++ b/src/utils/safeCompile.ts @@ -0,0 +1,14 @@ +import Handlebars, { TemplateDelegate } from 'handlebars' + +/** + * Safely compiles a Handlebars template. + * Invalid/malformed placeholders like {{client.}} are removed. + * Missing properties resolve to empty string. + */ +export const safeCompile = (templateSource: string): TemplateDelegate => { + const sanitizedTemplate = templateSource.replace(/{{\s*[\w]+\.\s*}}/g, '') // Remove placeholders ending with a dot + + Handlebars.registerHelper('helperMissing', () => '') + + return Handlebars?.compile(sanitizedTemplate, { strict: false }) +} From e8ec43890cdc3c746ff3057525f79fd67f400dce Mon Sep 17 00:00:00 2001 From: arpandhakal Date: Mon, 3 Nov 2025 16:15:32 +0545 Subject: [PATCH 2/2] fix(OUT-2541): used replaceAll instead of replace removing the g flag --- src/utils/safeCompile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/safeCompile.ts b/src/utils/safeCompile.ts index 7fee4a5..4956295 100644 --- a/src/utils/safeCompile.ts +++ b/src/utils/safeCompile.ts @@ -6,7 +6,7 @@ import Handlebars, { TemplateDelegate } from 'handlebars' * Missing properties resolve to empty string. */ export const safeCompile = (templateSource: string): TemplateDelegate => { - const sanitizedTemplate = templateSource.replace(/{{\s*[\w]+\.\s*}}/g, '') // Remove placeholders ending with a dot + const sanitizedTemplate = templateSource.replaceAll(/{{\s*[\w]+\.\s*}}/, '') // Remove placeholders ending with a dot Handlebars.registerHelper('helperMissing', () => '')