e8052adf1d
Bundle of email-renderer and email-caller fixes triggered by a
reproducer on picpeak.nothaft.cloud (gallery_created mail showing
literal `{{#if welcome_message}}` markers and `Passwort: (set at
creation)`). The audit that followed surfaced six more user-visible
defects in the same surface; all are fixed here so customer-facing
mail renders cleanly.
Renderer (`backend/src/services/emailProcessor.js`)
- `safeTemplateReplace` now resolves `{{#if VAR}}…{{/if}}` blocks
before flat `{{var}}` substitution. The shipped templates have used
Handlebars-style conditionals since migration 026; the renderer
ignored them, so the markers leaked verbatim into every mail with
an empty welcome_message. Lifted to module scope and exported so
the conditional contract is unit-testable. Single-pass, non-nested
(commented).
- Added `passwordSetAtCreationI18n` next to the existing two i18n
password sentinels so `(set at creation)` (sent by the publish-
from-draft flow when only the bcrypt hash remains) is localised
to "Das bei der Erstellung der Galerie gesetzte Passwort" /
equivalent in EN/DE/NL/PT/RU instead of the raw English string.
- Added an opt-in `{ escapeHtml: true }` mode to `safeTemplateReplace`
so admin-supplied free text (`event_name`, `host_name`, …) is
HTML-escaped on substitution into the HTML body. Allowlist of
passthrough keys (`welcome_message` already-HTML, server-generated
URLs `gallery_link` / `client_link`). Subject and text body keep
the legacy unescaped behaviour. `formatWelcomeMessage` now escapes
before nl2br so the welcome_message allowlist is safe.
- New `htmlToText()` strips `<style>` and `<script>` blocks (and
their content) before tag-stripping, decodes common entities, and
collapses whitespace. Used by the textBody fallback in
`sendTemplateEmail` — without this, every template missing a
`body_text` produced a "plain-text" mail starting with the 100+
lines of CSS embedded by `wrapEmailHtml()`.
- The client-access section (#172) now mirrors its HTML block into
`textBody` using the same per-language strings, so plain-text
recipients see the link / PIN / warning. `pinLabel = 'PIN'` moved
into `clientAccessI18n` (RU uses ПИН-код).
- Added `getSupportEmail()` exported helper that reads
`branding_support_email` from `app_settings` (JSON-decoded), with
the SMTP from-address as fallback. Used by the gallery_expired and
archive_complete callers below.
- Removed dead `require('handlebars')` (unused since the regex
renderer landed; pre-existing lint error in this file).
Callers (data the templates already reference)
- `expirationChecker.js queueExpirationWarning`: send `expiry_date`
(templates use this, the old code sent `expiration_date` —
typo'd key, never read), drop the hard-coded `.de`/`en` sniff
(the processor formats with the recipient's resolved language),
add the `{{password_security_message}}` sentinel for
`gallery_password` (plaintext is gone by warning time, so
customers used to see literal `{{gallery_password}}` in the mail).
- `expirationChecker.js handleExpiredEvent`: both queueEmail calls
now supply `host_name`, `event_date`, `expiry_date`,
`support_email` so the EN/DE/NL/PT/RU `gallery_expired` template
doesn't render literal `{{host_name}}, your gallery expired on
{{expiry_date}}`. Skip the duplicate admin send when
admin_email == customer_email.
- `archiveService.js`: `archive_complete` queue now supplies
`host_name`, `photo_count` (from `photoEntries.length`),
`archive_date`, `support_email` — the previous payload had only
`event_name` and `archive_size`, so most of the mail was
unfilled placeholders.
Tests
- `__tests__/services/emailProcessor.safeTemplateReplace.test.js`:
16 cases — flat substitution, conditional truthy/falsy/missing/
multi-line/sibling/numeric-0, plus 5 cases for the new
`escapeHtml` option (default off, escape on, allowlist
passthrough for welcome_message and gallery_link).
- `__tests__/services/emailProcessor.htmlToText.test.js`: 7 cases —
the regression scenario (full wrapped body with embedded `<style>`
block), tag-stripping, entity decoding, paragraph spacing.
- `__tests__/utils/formatters.test.js`: 12 cases for `escapeHtml`,
`nl2br`, and the now-escaping `formatWelcomeMessage`.
35 cases total, all green. Lint clean on every touched file
(also fixes a pre-existing `no-prototype-builtins` warning in the
process). Pre-existing failures in
`__tests__/services/backupService.enhanced.test.js` are unrelated
and pre-date this branch.
67 lines
2.0 KiB
JavaScript
67 lines
2.0 KiB
JavaScript
/**
|
|
* Unit tests for formatters.js — focused on the HTML-escape behaviour added
|
|
* so admin-supplied welcome messages can't inject markup into customer mail.
|
|
*/
|
|
|
|
const { escapeHtml, nl2br, formatWelcomeMessage } = require('../../src/utils/formatters');
|
|
|
|
describe('escapeHtml', () => {
|
|
it('escapes the five HTML metacharacters', () => {
|
|
expect(escapeHtml('& < > " \'')).toBe('& < > " '');
|
|
});
|
|
|
|
it('returns empty string for null/undefined', () => {
|
|
expect(escapeHtml(null)).toBe('');
|
|
expect(escapeHtml(undefined)).toBe('');
|
|
});
|
|
|
|
it('coerces non-string values', () => {
|
|
expect(escapeHtml(42)).toBe('42');
|
|
});
|
|
|
|
it('escapes & before introducing new entities', () => {
|
|
expect(escapeHtml('<&>')).toBe('<&>');
|
|
});
|
|
});
|
|
|
|
describe('nl2br', () => {
|
|
it('joins non-empty lines with <br />', () => {
|
|
expect(nl2br('a\nb\nc')).toBe('a<br />b<br />c');
|
|
});
|
|
|
|
it('normalises CRLF and CR', () => {
|
|
expect(nl2br('a\r\nb\rc')).toBe('a<br />b<br />c');
|
|
});
|
|
|
|
it('drops empty lines', () => {
|
|
expect(nl2br('a\n\n\nb')).toBe('a<br />b');
|
|
});
|
|
|
|
it('returns empty for empty input', () => {
|
|
expect(nl2br('')).toBe('');
|
|
expect(nl2br(null)).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('formatWelcomeMessage', () => {
|
|
it('returns empty string for empty input', () => {
|
|
expect(formatWelcomeMessage('')).toBe('');
|
|
expect(formatWelcomeMessage(' ')).toBe('');
|
|
});
|
|
|
|
it('escapes HTML metacharacters before nl2br', () => {
|
|
expect(formatWelcomeMessage('Hello <b>world</b>'))
|
|
.toBe('Hello <b>world</b>');
|
|
});
|
|
|
|
it('renders newlines as <br /> while keeping content escaped', () => {
|
|
expect(formatWelcomeMessage('line 1\n<script>x</script>\nline 3'))
|
|
.toBe('line 1<br /><script>x</script><br />line 3');
|
|
});
|
|
|
|
it('escapes ampersands and quotes that would otherwise break HTML', () => {
|
|
expect(formatWelcomeMessage('Tom & Jerry\'s "show"'))
|
|
.toBe('Tom & Jerry's "show"');
|
|
});
|
|
});
|