mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-25 09:03:12 +03:00
🐛 Fixed newsletters not rendering with non-HTML safe chars (#15331)
Co-authored-by: Kevin Ansfield <kevin@lookingsideways.co.uk>
This commit is contained in:
parent
9355c6d8fa
commit
21e473ff78
@ -1,15 +1,43 @@
|
||||
/* eslint indent: warn, no-irregular-whitespace: warn */
|
||||
const iff = (cond, yes, no) => (cond ? yes : no);
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
|
||||
/**
|
||||
* @template {Object.<string, any>} Input
|
||||
* @param {Input} obj
|
||||
* @param {string[]} [keys]
|
||||
* @returns {Input}
|
||||
*/
|
||||
const sanitizeKeys = (obj, keys) => {
|
||||
const sanitized = Object.assign({}, obj);
|
||||
const keysToSanitize = keys || Object.keys(obj);
|
||||
|
||||
for (const key of keysToSanitize) {
|
||||
if (typeof sanitized[key] === 'string') {
|
||||
// @ts-ignore
|
||||
sanitized[key] = sanitizeHtml(sanitized[key], {
|
||||
allowedTags: false,
|
||||
allowedAttributes: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
module.exports = ({post, site, newsletter, templateSettings}) => {
|
||||
const date = new Date();
|
||||
const hasFeatureImageCaption = templateSettings.showFeatureImage && post.feature_image && post.feature_image_caption;
|
||||
const cleanPost = sanitizeKeys(post, ['title', 'excerpt', 'html', 'feature_image_alt', 'feature_image_caption']);
|
||||
const cleanSite = sanitizeKeys(site, ['title']);
|
||||
const cleanNewsletter = sanitizeKeys(newsletter, ['name']);
|
||||
return `<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>${post.title}</title>
|
||||
<title>${cleanPost.title}</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
@ -1137,7 +1165,7 @@ ${ templateSettings.showBadge ? `
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<span class="preheader">${ post.excerpt ? post.excerpt : `${post.title} – ` }</span>
|
||||
<span class="preheader">${ cleanPost.excerpt ? cleanPost.excerpt : `${cleanPost.title} – ` }</span>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" width="100%">
|
||||
|
||||
<!-- Outlook doesn't respect max-width so we need an extra centered table -->
|
||||
@ -1172,24 +1200,24 @@ ${ templateSettings.showBadge ? `
|
||||
<tr>
|
||||
<td class="${templateSettings.showHeaderTitle ? `site-info-bordered` : `site-info`}" width="100%" align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
${ templateSettings.showHeaderIcon && site.iconUrl ? `
|
||||
${ templateSettings.showHeaderIcon && cleanSite.iconUrl ? `
|
||||
<tr>
|
||||
<td class="site-icon"><a href="${site.url}"><img src="${site.iconUrl}" alt="${site.title}" border="0"></a></td>
|
||||
<td class="site-icon"><a href="${cleanSite.url}"><img src="${cleanSite.iconUrl}" alt="${cleanSite.title}" border="0"></a></td>
|
||||
</tr>
|
||||
` : ``}
|
||||
${ templateSettings.showHeaderTitle ? `
|
||||
<tr>
|
||||
<td class="site-url ${!templateSettings.showHeaderName ? 'site-url-bottom-padding' : ''}"><div style="width: 100% !important;"><a href="${site.url}" class="site-title">${site.title}</a></div></td>
|
||||
<td class="site-url ${!templateSettings.showHeaderName ? 'site-url-bottom-padding' : ''}"><div style="width: 100% !important;"><a href="${cleanSite.url}" class="site-title">${cleanSite.title}</a></div></td>
|
||||
</tr>
|
||||
` : ``}
|
||||
${ templateSettings.showHeaderName && templateSettings.showHeaderTitle ? `
|
||||
<tr>
|
||||
<td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${site.url}" class="site-subtitle">${newsletter.name}</a></div></td>
|
||||
<td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${cleanSite.url}" class="site-subtitle">${cleanNewsletter.name}</a></div></td>
|
||||
</tr>
|
||||
` : ``}
|
||||
${ templateSettings.showHeaderName && !templateSettings.showHeaderTitle ? `
|
||||
<tr>
|
||||
<td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${site.url}" class="site-title">${newsletter.name}</a></div></td>
|
||||
<td class="site-url site-url-bottom-padding"><div style="width: 100% !important;"><a href="${cleanSite.url}" class="site-title">${cleanNewsletter.name}</a></div></td>
|
||||
</tr>
|
||||
` : ``}
|
||||
|
||||
@ -1201,7 +1229,7 @@ ${ templateSettings.showBadge ? `
|
||||
|
||||
<tr>
|
||||
<td class="post-title ${templateSettings.titleFontCategory === 'serif' ? `post-title-serif` : `` } ${templateSettings.titleAlignment === 'left' ? `post-title-left` : ``}">
|
||||
<a href="${post.url}" class="post-title-link ${templateSettings.titleAlignment === 'left' ? `post-title-link-left` : ``}">${post.title}</a>
|
||||
<a href="${cleanPost.url}" class="post-title-link ${templateSettings.titleAlignment === 'left' ? `post-title-link-left` : ``}">${cleanPost.title}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@ -1209,28 +1237,28 @@ ${ templateSettings.showBadge ? `
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td class="post-meta ${templateSettings.titleAlignment === 'left' ? `post-meta-left` : ``}">
|
||||
By ${post.authors} –
|
||||
${post.published_at} –
|
||||
<a href="${post.url}" class="view-online-link">View online →</a>
|
||||
By ${cleanPost.authors} –
|
||||
${cleanPost.published_at} –
|
||||
<a href="${cleanPost.url}" class="view-online-link">View online →</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
${ templateSettings.showFeatureImage && post.feature_image ? `
|
||||
${ templateSettings.showFeatureImage && cleanPost.feature_image ? `
|
||||
<tr>
|
||||
<td class="feature-image ${hasFeatureImageCaption ? 'feature-image-with-caption' : ''}"><img src="${post.feature_image}"${post.feature_image_width ? ` width="${post.feature_image_width}"` : ''}${post.feature_image_alt ? ` alt="${post.feature_image_alt}"` : ''}></td>
|
||||
<td class="feature-image ${hasFeatureImageCaption ? 'feature-image-with-caption' : ''}"><img src="${cleanPost.feature_image}"${cleanPost.feature_image_width ? ` width="${cleanPost.feature_image_width}"` : ''}${cleanPost.feature_image_alt ? ` alt="${cleanPost.feature_image_alt}"` : ''}></td>
|
||||
</tr>
|
||||
` : ``}
|
||||
${ hasFeatureImageCaption ? `
|
||||
<tr>
|
||||
<td class="feature-image-caption" align="center">${post.feature_image_caption}</td>
|
||||
<td class="feature-image-caption" align="center">${cleanPost.feature_image_caption}</td>
|
||||
</tr>
|
||||
` : ``}
|
||||
<tr>
|
||||
<td class="${(templateSettings.bodyFontCategory === 'sans_serif') ? `post-content-sans-serif` : `post-content` }">
|
||||
<!-- POST CONTENT START -->
|
||||
${post.html}
|
||||
${cleanPost.html}
|
||||
<!-- POST CONTENT END -->
|
||||
</td>
|
||||
</tr>
|
||||
@ -1245,7 +1273,7 @@ ${ templateSettings.showBadge ? `
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-top: 40px; padding-bottom: 30px;">
|
||||
${iff(!!templateSettings.footerContent, `<tr><td class="footer">${templateSettings.footerContent}</td></tr>`, '')}
|
||||
<tr>
|
||||
<td class="footer">${site.title} © ${date.getFullYear()} – <a href="%recipient.unsubscribe_url%">Unsubscribe</a></td>
|
||||
<td class="footer">${cleanSite.title} © ${date.getFullYear()} – <a href="%recipient.unsubscribe_url%">Unsubscribe</a></td>
|
||||
</tr>
|
||||
|
||||
${ templateSettings.showBadge ? `
|
||||
|
@ -119,6 +119,86 @@ describe('Mega template', function () {
|
||||
should(footerPowered.find('a img').attr('alt')).eql('Powered by Ghost');
|
||||
});
|
||||
|
||||
it('Correctly escapes the contents', function () {
|
||||
const post = {
|
||||
title: 'I <3 Posts',
|
||||
html: '<div class="post-content-html">I am <100 years old</div>',
|
||||
feature_image: 'https://example.com/image.jpg',
|
||||
feature_image_alt: 'I <3 alt text',
|
||||
feature_image_caption: 'I <3 images'
|
||||
};
|
||||
const site = {
|
||||
iconUrl: 'site icon url',
|
||||
url: 'site url',
|
||||
title: 'Egg <3 eggs'
|
||||
};
|
||||
const templateSettings = {
|
||||
headerImage: 'header image',
|
||||
headerImageWidth: '600',
|
||||
showHeaderIcon: true,
|
||||
showHeaderTitle: true,
|
||||
showHeaderName: true,
|
||||
titleAlignment: 'left',
|
||||
titleFontCategory: 'serif',
|
||||
showFeatureImage: true,
|
||||
bodyFontCategory: 'sans_serif',
|
||||
footerContent: 'footer content',
|
||||
showBadge: true
|
||||
};
|
||||
const newsletter = {
|
||||
name: '<100 eggs to go'
|
||||
};
|
||||
|
||||
const html = render({post, site, templateSettings, newsletter});
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
should($('.site-title').text()).eql(site.title);
|
||||
should($('.site-title').html()).eql('Egg <3 eggs');
|
||||
should($('.post-content-html').length).eql(1);
|
||||
should($('.post-content-html').text()).eql('I am <100 years old');
|
||||
should($('.post-content-html').html()).eql('I am <100 years old');
|
||||
should($('.feature-image').html()).containEql('"I <3 alt text"');
|
||||
should($('.feature-image-caption').html()).eql('I <3 images');
|
||||
should($('.site-subtitle').html()).eql('<100 eggs to go');
|
||||
});
|
||||
|
||||
it('Doesn\'t strip class or style attributes when escaping content', function () {
|
||||
const post = {
|
||||
title: 'I <3 Posts',
|
||||
html: '<div class="post-content-html"><span class="custom" style="font-weight: 900; display: flex;">BOLD</span></div>'
|
||||
};
|
||||
const site = {
|
||||
iconUrl: 'site icon url',
|
||||
url: 'site url',
|
||||
title: 'Egg <3 eggs'
|
||||
};
|
||||
const templateSettings = {
|
||||
headerImage: 'header image',
|
||||
headerImageWidth: '600',
|
||||
showHeaderIcon: true,
|
||||
showHeaderTitle: true,
|
||||
showHeaderName: true,
|
||||
titleAlignment: 'left',
|
||||
titleFontCategory: 'serif',
|
||||
showFeatureImage: true,
|
||||
bodyFontCategory: 'sans_serif',
|
||||
footerContent: 'footer content',
|
||||
showBadge: true
|
||||
};
|
||||
const newsletter = {
|
||||
name: '<100 eggs to go'
|
||||
};
|
||||
|
||||
const html = render({post, site, templateSettings, newsletter});
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
should(html).containEql('class="custom"');
|
||||
// note that some part of rendering/sanitisation removes spaces from the style description
|
||||
should(html).containEql('style="font-weight:900;display:flex"');
|
||||
});
|
||||
|
||||
it('Uses the post title as a fallback for the excerpt', function () {
|
||||
const post = {
|
||||
title: 'My post title'
|
||||
|
Loading…
Reference in New Issue
Block a user