Skip to content

fix: preserve Liquid variables in email template link hrefs - #1343

Merged
daveearley merged 5 commits into
HiEventsDev:developfrom
b-pm:fix/1183-liquid-vars-in-email-links
Sep 15, 2026
Merged

daveearley merged 5 commits into
HiEventsDev:developfrom
b-pm:fix/1183-liquid-vars-in-email-links

Conversation

@b-pm

@b-pm b-pm commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

What changes I've made

  • Added HtmlPurifierService::purifyPreservingLiquid() that temporarily replaces Liquid {{ }} / {% %} tokens with per-call random placeholders before HTMLPurifier runs, then restores them
  • Wired create/update email template handlers to use that method for the body
  • Added unit tests covering href preservation (and that default purify() still encodes braces)

Fixes #1183

Why I've made these changes

Saving an email template ran the body through HTMLPurifier, which URI-encodes { / } in hrefs ({{ order.number }}%7B%7B%20order.number%20%7D%7D). Preview looked fine before save because purification only runs server-side on create/update. That matches the bug report on #1183 (claimed before opening this PR).

How I've tested these changes

  • Added HtmlPurifierServiceTest asserting Liquid tokens survive in hrefs / tag tokens, and that default purify still percent-encodes braces
  • Could not run the full PHPUnit suite in this environment (no local PHP app stack); relying on CI + the new unit tests

Suggested manual check:

  1. Edit an email template, add a link whose URL contains {{ order.number }}
  2. Save and reopen — href should still contain the Liquid token (not %7B%7B...)
  3. Preview / place an order — link resolves the variable

Checklist

  • I have read the contributing guidelines.
  • My code follows the coding standards of the project.
  • I have tested my changes, and they work as expected.
  • I understand that this PR will be closed if I do not follow the contributor guidelines and if this PR template is left unedited.

HTMLPurifier URI encoding was turning {{ order.number }} into
%7B%7B%20order.number%20%7D%7D on save. Protect Liquid tokens around
purification for email template bodies.

Fixes HiEventsDev#1183
Copilot AI lite review requested due to automatic review settings September 9, 2026 11:44
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new Liquid-preserving purifier can bypass URL-scheme enforcement and can throw on preg_replace_callback() null results, which needs to be addressed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR fixes a regression where HTMLPurifier percent-encodes Liquid tokens inside href attributes when saving email templates (e.g. {{ order.number }}%7B%7B...%7D%7D), by adding a purification path that preserves Liquid tokens during sanitization and using it for email template create/update.

Changes:

  • Added HtmlPurifierService::purifyPreservingLiquid() to protect/restore {{ }} and {% %} tokens around HTMLPurifier.
  • Updated email template create/update handlers to use the new purification method for template body.
  • Added unit tests asserting Liquid tokens remain intact in href attributes after the new purification flow.
File summaries
File Description
backend/app/Services/Infrastructure/HtmlPurifier/HtmlPurifierService.php Adds Liquid-preserving HTML purification method used by email template persistence.
backend/app/Services/Application/Handlers/EmailTemplate/CreateEmailTemplateHandler.php Switches template body purification to preserve Liquid tokens on create.
backend/app/Services/Application/Handlers/EmailTemplate/UpdateEmailTemplateHandler.php Switches template body purification to preserve Liquid tokens on update.
backend/tests/Unit/Services/Infrastructure/HtmlPurifier/HtmlPurifierServiceTest.php Adds coverage for Liquid token preservation vs default purification behavior.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +43 to +56
$tokens = [];
$protected = preg_replace_callback(
'/\{\{[\s\S]*?\}\}|\{%[\s\S]*?%\}/',
static function (array $matches) use (&$tokens): string {
$placeholder = 'LIQUIDTOKEN'.count($tokens).'X';
$tokens[$placeholder] = $matches[0];

return $placeholder;
},
$html,
);

$purified = $this->htmlPurifier->purify($protected, $this->config);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted — per-call random placeholder prefix and null-safe fallback to normal purify(). Falling back when an href starts with Liquid would break the reported use case (query params with {{ }}); tokens are restored after purification so scheme checks still run on the placeholder URI.

Comment on lines +32 to +36
/**
* Purify HTML while preserving Liquid template tokens such as
* `{{ order.number }}` and `{% if ... %}` so URI encoding does not turn
* them into `%7B%7B...%7D%7D` inside hrefs and other attributes.
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accepted — removed the explanatory docblock.

Use a per-call random placeholder prefix, fall back to normal purify when
preg_replace_callback fails, and drop the explanatory docblock per project
conventions.
@b-pm

b-pm commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@b-pm

b-pm commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

recheck

@b-pm

b-pm commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

recheck

# Conflicts:
#	backend/tests/Unit/Services/Infrastructure/HtmlPurifier/HtmlPurifierServiceTest.php
@daveearley

Copy link
Copy Markdown
Contributor

Thank you for this change! I made a slight adjustment as your code allowed some XSS to slip through.

@daveearley
daveearley merged commit 662c993 into HiEventsDev:develop Sep 15, 2026
6 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 15, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛Email template - variables in links break on save

3 participants