From 78ebdbbbd857c30bff09a4573183a3496618659b Mon Sep 17 00:00:00 2001 From: Aguinaldo Tupy <44652991+aguinaldotupy@users.noreply.github.com> Date: Wed, 8 Oct 2025 07:03:06 -0300 Subject: [PATCH] fix: Normalize URLs with spaces in WhatsApp template parameters (#12594) This PR fixes URL parsing errors when WhatsApp template parameters contain URLs with spaces or special characters. The solution adds proper URL normalization using Addressable::URI before validation, which automatically handles space encoding and special character normalization. Related with https://github.com/chatwoot/chatwoot/pull/12462 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../populate_template_parameters_service.rb | 16 ++++- ...pulate_template_parameters_service_spec.rb | 70 +++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 spec/services/whatsapp/populate_template_parameters_service_spec.rb diff --git a/app/services/whatsapp/populate_template_parameters_service.rb b/app/services/whatsapp/populate_template_parameters_service.rb index 3f9f64b91..6ea3e6e05 100644 --- a/app/services/whatsapp/populate_template_parameters_service.rb +++ b/app/services/whatsapp/populate_template_parameters_service.rb @@ -34,8 +34,9 @@ class Whatsapp::PopulateTemplateParametersService return nil if url.blank? sanitized_url = sanitize_parameter(url) - validate_url(sanitized_url) - build_media_type_parameter(sanitized_url, media_type.downcase, media_name) + normalized_url = normalize_url(sanitized_url) + validate_url(normalized_url) + build_media_type_parameter(normalized_url, media_type.downcase, media_name) end def build_named_parameter(parameter_name, value) @@ -138,9 +139,20 @@ class Whatsapp::PopulateTemplateParametersService sanitized[0...1000] # Limit length to prevent DoS end + def normalize_url(url) + # Use Addressable::URI for better URL normalization + # It handles spaces, special characters, and encoding automatically + Addressable::URI.parse(url).normalize.to_s + rescue Addressable::URI::InvalidURIError + # Fallback: simple space encoding if Addressable fails + url.gsub(' ', '%20') + end + def validate_url(url) return if url.blank? + # url is already normalized by the caller + uri = URI.parse(url) raise ArgumentError, "Invalid URL scheme: #{uri.scheme}. Only http and https are allowed" unless %w[http https].include?(uri.scheme) raise ArgumentError, 'URL too long (max 2000 characters)' if url.length > 2000 diff --git a/spec/services/whatsapp/populate_template_parameters_service_spec.rb b/spec/services/whatsapp/populate_template_parameters_service_spec.rb new file mode 100644 index 000000000..05390bd90 --- /dev/null +++ b/spec/services/whatsapp/populate_template_parameters_service_spec.rb @@ -0,0 +1,70 @@ +require 'rails_helper' + +describe Whatsapp::PopulateTemplateParametersService do + let(:service) { described_class.new } + + describe '#normalize_url' do + it 'normalizes URLs with spaces' do + url_with_spaces = 'https://example.com/path with spaces' + normalized = service.send(:normalize_url, url_with_spaces) + + expect(normalized).to eq('https://example.com/path%20with%20spaces') + end + + it 'handles URLs with special characters' do + url = 'https://example.com/path?query=test value' + normalized = service.send(:normalize_url, url) + + expect(normalized).to include('https://example.com/path') + expect(normalized).not_to include(' ') + end + + it 'returns valid URLs unchanged' do + url = 'https://example.com/valid-path' + normalized = service.send(:normalize_url, url) + + expect(normalized).to eq(url) + end + end + + describe '#build_media_parameter' do + context 'when URL contains spaces' do + it 'normalizes the URL before building media parameter' do + url_with_spaces = 'https://example.com/image with spaces.jpg' + result = service.build_media_parameter(url_with_spaces, 'IMAGE') + + expect(result[:type]).to eq('image') + expect(result[:image][:link]).to eq('https://example.com/image%20with%20spaces.jpg') + end + end + + context 'when URL contains special characters in query string' do + it 'normalizes the URL correctly' do + url = 'https://example.com/video.mp4?title=My Video' + result = service.build_media_parameter(url, 'VIDEO', 'test_video') + + expect(result[:type]).to eq('video') + expect(result[:video][:link]).not_to include(' ') + end + end + + context 'when URL is already valid' do + it 'builds media parameter without changing URL' do + url = 'https://example.com/document.pdf' + result = service.build_media_parameter(url, 'DOCUMENT', 'test.pdf') + + expect(result[:type]).to eq('document') + expect(result[:document][:link]).to eq(url) + expect(result[:document][:filename]).to eq('test.pdf') + end + end + + context 'when URL is blank' do + it 'returns nil' do + result = service.build_media_parameter('', 'IMAGE') + + expect(result).to be_nil + end + end + end +end