fix(email-template): sécurise et fiabilise le rendu des mails HTML
- emailUrl(): whitelist de scheme (http/https/mailto/tel, data:image) et échappement des guillemets sur les URLs de liens et d'images — bloque l'injection d'attribut et les schemes javascript:/data:text/html - échappe aussi les guillemets dans l'attribut alt des images - bloc de code: passage en <table> largeur au contenu (rendu robuste sous Outlook, ne prend plus toute la largeur), coins arrondis et padding accrus - banner: coins plus arrondis et padding accru Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
805f0fe00b
commit
977b9e817e
1 changed files with 13 additions and 4 deletions
|
|
@ -1537,6 +1537,15 @@ const PG_EXAMPLE_HTML_TPL = {
|
||||||
Fonction partagée à l'identique avec la page password-generator. Les
|
Fonction partagée à l'identique avec la page password-generator. Les
|
||||||
{{ placeholders }} sont substitués en dernier (valeurs échappées). ---- */
|
{{ placeholders }} sont substitués en dernier (valeurs échappées). ---- */
|
||||||
function emailEsc(s){ return String(s == null ? '' : s).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); }
|
function emailEsc(s){ return String(s == null ? '' : s).replace(/[&<>]/g, c => ({'&':'&','<':'<','>':'>'}[c])); }
|
||||||
|
/* URL sûre en attribut : whitelist de scheme + échappe les guillemets.
|
||||||
|
Neutralise javascript:, data:text/html, vbscript:… → « # » si scheme non autorisé.
|
||||||
|
(emailEsc a déjà neutralisé & < > ; il reste les " qui casseraient l'attribut.) */
|
||||||
|
function emailUrl(u){
|
||||||
|
const s = String(u == null ? '' : u).trim();
|
||||||
|
const sch = /^([a-z][a-z0-9+.\-]*):/i.exec(s);
|
||||||
|
const ok = !sch || /^(https?|mailto|tel)$/i.test(sch[1]) || /^data:image\//i.test(s);
|
||||||
|
return (ok ? s : '#').replace(/"/g, '"');
|
||||||
|
}
|
||||||
function emailFillVars(html, ctx){
|
function emailFillVars(html, ctx){
|
||||||
return html.replace(/\{\{\s*(\w+)\s*\}\}/g, (m, k) => {
|
return html.replace(/\{\{\s*(\w+)\s*\}\}/g, (m, k) => {
|
||||||
if(!(k in ctx)) return m;
|
if(!(k in ctx)) return m;
|
||||||
|
|
@ -1551,7 +1560,7 @@ function renderEmailHtml(src, ctx){
|
||||||
const inline = t => t
|
const inline = t => t
|
||||||
.replace(/`([^`]+)`/g, '<code style="font-family:ui-monospace,Consolas,monospace;background:#f2f2f2;color:#222;padding:1px 5px;border-radius:4px">$1</code>')
|
.replace(/`([^`]+)`/g, '<code style="font-family:ui-monospace,Consolas,monospace;background:#f2f2f2;color:#222;padding:1px 5px;border-radius:4px">$1</code>')
|
||||||
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||||
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, '<a href="$2" style="color:#2f6fd8;text-decoration:underline">$1</a>');
|
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (mm, txt, url) => '<a href="' + emailUrl(url) + '" style="color:#2f6fd8;text-decoration:underline">' + txt + '</a>');
|
||||||
let html = '', i = 0, m;
|
let html = '', i = 0, m;
|
||||||
while(i < lines.length){
|
while(i < lines.length){
|
||||||
const line = lines[i];
|
const line = lines[i];
|
||||||
|
|
@ -1559,18 +1568,18 @@ function renderEmailHtml(src, ctx){
|
||||||
const color = m[1] || '#5b9cf5', buf = [];
|
const color = m[1] || '#5b9cf5', buf = [];
|
||||||
for(i++; i < lines.length && !/^:::\s*$/.test(lines[i]); i++) buf.push(lines[i]);
|
for(i++; i < lines.length && !/^:::\s*$/.test(lines[i]); i++) buf.push(lines[i]);
|
||||||
i++;
|
i++;
|
||||||
html += '<div style="background:' + color + ';color:#fff;padding:22px 24px;border-radius:8px;font-size:20px;font-weight:700;line-height:1.35;margin:0 0 18px">' + inline(buf.join('<br>')) + '</div>';
|
html += '<div style="background:' + color + ';color:#fff;padding:24px 28px;border-radius:14px;font-size:20px;font-weight:700;line-height:1.35;margin:0 0 18px">' + inline(buf.join('<br>')) + '</div>';
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(/^```/.test(line)){
|
if(/^```/.test(line)){
|
||||||
const buf = [];
|
const buf = [];
|
||||||
for(i++; i < lines.length && !/^```/.test(lines[i]); i++) buf.push(lines[i]);
|
for(i++; i < lines.length && !/^```/.test(lines[i]); i++) buf.push(lines[i]);
|
||||||
i++;
|
i++;
|
||||||
html += '<pre style="background:#1d2026;color:#e6e6e6;padding:14px 16px;border-radius:8px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:13px;line-height:1.55;margin:0 0 16px"><code>' + buf.join('\n') + '</code></pre>';
|
html += '<table role="presentation" cellpadding="0" cellspacing="0" style="margin:0 0 16px;border-collapse:separate"><tr><td style="background:#1d2026;color:#e6e6e6;padding:16px 18px;border-radius:12px;font-family:ui-monospace,Consolas,monospace;font-size:13px;line-height:1.55"><pre style="margin:0;overflow-x:auto;font-family:inherit"><code>' + buf.join('\n') + '</code></pre></td></tr></table>';
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if(m = line.match(/^!\[([^\]]*)\]\(([^)]+)\)\s*$/)){
|
if(m = line.match(/^!\[([^\]]*)\]\(([^)]+)\)\s*$/)){
|
||||||
html += '<div style="margin:0 0 16px"><img src="' + m[2] + '" alt="' + m[1] + '" style="max-width:100%;height:auto;border-radius:8px;display:block"></div>';
|
html += '<div style="margin:0 0 16px"><img src="' + emailUrl(m[2]) + '" alt="' + m[1].replace(/"/g, '"') + '" style="max-width:100%;height:auto;border-radius:8px;display:block"></div>';
|
||||||
i++; continue;
|
i++; continue;
|
||||||
}
|
}
|
||||||
if(/^---+\s*$/.test(line)){ html += '<hr style="border:0;border-top:1px solid #e2e2e2;margin:20px 0">'; i++; continue; }
|
if(/^---+\s*$/.test(line)){ html += '<hr style="border:0;border-top:1px solid #e2e2e2;margin:20px 0">'; i++; continue; }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue