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..4956295 --- /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.replaceAll(/{{\s*[\w]+\.\s*}}/, '') // Remove placeholders ending with a dot + + Handlebars.registerHelper('helperMissing', () => '') + + return Handlebars?.compile(sanitizedTemplate, { strict: false }) +}