From ee176480da005aabcd45155d8a470e40d84a11db Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 21 Jul 2025 17:03:11 +0400 Subject: [PATCH] feat: Enhanced WhatsApp template support with security optimizations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive WhatsApp template parameter support for header/footer/buttons - Implement coupon code template support with 15-character validation - Support all WhatsApp template types (text, media, interactive) - Add advanced parameter types (currency, date/time, location) - Remove restrictions on DOCUMENT, IMAGE, VIDEO template formats Security improvements: - Fix XSS vulnerability by replacing v-html with v-dompurify-html - Add input sanitization and parameter validation - Implement URL validation with scheme and length checks - Add protection against DoS attacks with length limits Performance optimizations: - Replace recursive template fetching with iterative pagination - Add safety limits to prevent infinite loops and stack overflow - Optimize template processing for better memory usage Frontend enhancements: - Enhanced template picker with header/footer/button display - Component-based parameter input organized by type - Real-time validation for coupon codes and parameters - Improved template preview with sanitized HTML rendering - Fix Vue linting issues with proper computed properties Maintains full backward compatibility with existing template configurations. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../components/WhatsappTemplateParser.vue | 238 +++++++++++++++++- .../WhatsappTemplates/TemplatesPicker.vue | 97 ++++++- .../providers/whatsapp_cloud_service.rb | 37 ++- .../whatsapp/template_processor_service.rb | 132 ++++++++++ 4 files changed, 471 insertions(+), 33 deletions(-) diff --git a/app/javascript/dashboard/components-next/NewConversation/components/WhatsappTemplateParser.vue b/app/javascript/dashboard/components-next/NewConversation/components/WhatsappTemplateParser.vue index f3b8a26da..0dfd53d3a 100644 --- a/app/javascript/dashboard/components-next/NewConversation/components/WhatsappTemplateParser.vue +++ b/app/javascript/dashboard/components-next/NewConversation/components/WhatsappTemplateParser.vue @@ -24,10 +24,43 @@ const templateName = computed(() => { return props.template?.name || ''; }); +const templateComponents = computed(() => { + return props.template?.components || []; +}); + const templateString = computed(() => { - return props.template?.components?.find( - component => component.type === 'BODY' - ).text; + return ( + props.template?.components?.find(component => component.type === 'BODY') + ?.text || '' + ); +}); + +const headerComponent = computed(() => { + return templateComponents.value.find( + component => component.type === 'HEADER' + ); +}); + +const footerComponent = computed(() => { + return templateComponents.value.find( + component => component.type === 'FOOTER' + ); +}); + +const buttonComponents = computed(() => { + return templateComponents.value.filter( + component => component.type === 'BUTTONS' + ); +}); + +const legacyParams = computed(() => { + const params = {}; + Object.keys(processedParams.value).forEach(key => { + if (!['header', 'body', 'footer', 'buttons'].includes(key)) { + params[key] = processedParams.value[key]; + } + }); + return params; }); const processVariable = str => { @@ -72,14 +105,76 @@ const getFieldErrorType = key => { }; const generateVariables = () => { - const matchedVariables = templateString.value.match(/{{([^}]+)}}/g); - if (!matchedVariables) return; + const allVariables = {}; - const finalVars = matchedVariables.map(i => processVariable(i)); - processedParams.value = finalVars.reduce((acc, variable) => { - acc[variable] = ''; - return acc; - }, {}); + // Process body variables + const bodyVars = templateString.value.match(/{{([^}]+)}}/g) || []; + bodyVars.forEach(variable => { + const key = processVariable(variable); + if (!allVariables.body) allVariables.body = {}; + allVariables.body[key] = ''; + }); + + // Process header variables + if (headerComponent.value?.text) { + const headerVars = headerComponent.value.text.match(/{{([^}]+)}}/g) || []; + headerVars.forEach(variable => { + const key = processVariable(variable); + if (!allVariables.header) allVariables.header = {}; + allVariables.header[key] = ''; + }); + } + + // Process footer variables + if (footerComponent.value?.text) { + const footerVars = footerComponent.value.text.match(/{{([^}]+)}}/g) || []; + footerVars.forEach(variable => { + const key = processVariable(variable); + if (!allVariables.footer) allVariables.footer = {}; + allVariables.footer[key] = ''; + }); + } + + // Process button variables + buttonComponents.value.forEach(buttonComponent => { + if (buttonComponent.buttons) { + buttonComponent.buttons.forEach((button, index) => { + // Handle URL buttons with variables + if (button.url && button.url.includes('{{')) { + const buttonVars = button.url.match(/{{([^}]+)}}/g) || []; + buttonVars.forEach(() => { + if (!allVariables.buttons) allVariables.buttons = []; + if (!allVariables.buttons[index]) allVariables.buttons[index] = {}; + allVariables.buttons[index].type = 'url'; + allVariables.buttons[index].parameter = ''; + }); + } + + // Handle copy code buttons + if (button.type === 'COPY_CODE') { + if (!allVariables.buttons) allVariables.buttons = []; + if (!allVariables.buttons[index]) allVariables.buttons[index] = {}; + allVariables.buttons[index].type = 'copy_code'; + allVariables.buttons[index].parameter = ''; + } + }); + } + }); + + processedParams.value = allVariables; +}; + +const validateButtonParameter = (button, index) => { + const parameter = processedParams.value.buttons[index].parameter; + + if (button.type === 'copy_code') { + if (parameter && parameter.length > 15) { + processedParams.value.buttons[index].parameter = parameter.substring( + 0, + 15 + ); + } + } }; const sendMessage = async () => { @@ -117,8 +212,8 @@ onMounted(() => { }}

{ }} + +

+

+ {{ + t('WHATSAPP_TEMPLATES.PARSER.HEADER_PARAMETERS') || + 'Header Parameters' + }} +

+
+ + {{ key }} + + +
+
+ + +
+

+ {{ + t('WHATSAPP_TEMPLATES.PARSER.BODY_PARAMETERS') || 'Body Parameters' + }} +

+
+ + {{ key }} + + +
+
+ + +
+

+ {{ + t('WHATSAPP_TEMPLATES.PARSER.FOOTER_PARAMETERS') || + 'Footer Parameters' + }} +

+
+ + {{ key }} + + +
+
+ + +
+

+ {{ + t('WHATSAPP_TEMPLATES.PARSER.BUTTON_PARAMETERS') || + 'Button Parameters' + }} +

+
+ + {{ + button.type === 'copy_code' + ? t('WHATSAPP_TEMPLATES.PARSER.COUPON_CODE') || 'Coupon Code' + : `${t('WHATSAPP_TEMPLATES.PARSER.BUTTON') || 'Button'} ${index + 1}` + }} + + +
+
+ +
diff --git a/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue b/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue index dd8a95879..4ed02d50b 100644 --- a/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue +++ b/app/javascript/dashboard/components/widgets/conversation/WhatsappTemplates/TemplatesPicker.vue @@ -1,6 +1,6 @@