0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-20 22:42:53 -05:00

🐛 Fixed members-only content incorrectly showing in plaintext email (#17137)

fixes https://github.com/TryGhost/Ghost/issues/16131

Members only content was incorrectly being shown in a plaintext email
due to the email `preheader` using the post model `plaintext` field
directly (which contained the members-only content). This changes this
behaviour so that the post html content is utilised for the `preheader`
but has all members-only content (post-preview content + segmented
content) removed
This commit is contained in:
Michael Barrett 2023-06-29 09:40:04 +01:00 committed by GitHub
parent 08e1bcd50c
commit 576fba0568
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 101 additions and 19 deletions

View file

@ -8,6 +8,7 @@ const {textColorForBackgroundColor, darkenToContrastThreshold} = require('@trygh
const {DateTime} = require('luxon');
const htmlToPlaintext = require('@tryghost/html-to-plaintext');
const tpl = require('@tryghost/tpl');
const cheerio = require('cheerio');
const messages = {
subscriptionStatus: {
@ -218,7 +219,6 @@ class EmailRenderer {
return allowedSegments;
}
const cheerio = require('cheerio');
const $ = cheerio.load(html);
let allSegments = $('[data-gh-segment]')
@ -281,11 +281,30 @@ class EmailRenderer {
}
}
let $ = cheerio.load(html);
// Remove parts of the HTML not applicable to the current segment - We do this
// before rendering the template as the preheader for the email may be generated
// using the HTML and we don't want to include content that should not be
// visible depending on the segment
$('[data-gh-segment]').get().forEach((node) => {
// TODO: replace with NQL interpretation
if (node.attribs['data-gh-segment'] !== segment) {
$(node).remove();
} else {
// Getting rid of the attribute for a cleaner html output
$(node).removeAttr('data-gh-segment');
}
});
html = $.html();
const templateData = await this.getTemplateData({
post,
newsletter,
html,
addPaywall
addPaywall,
segment
});
html = await this.renderTemplate(templateData);
@ -326,8 +345,7 @@ class EmailRenderer {
html = juice(html, {inlinePseudoElements: true, removeStyleTags: true});
// happens after inlining of CSS so we can change element types without worrying about styling
const cheerio = require('cheerio');
const $ = cheerio.load(html);
$ = cheerio.load(html);
// force all links to open in new tab
$('a').attr('target', '_blank');
@ -335,17 +353,6 @@ class EmailRenderer {
// convert figure and figcaption to div so that Outlook applies margins
$('figure, figcaption').each((i, elem) => !!(elem.tagName = 'div'));
// Remove/hide parts of the email based on segment data attributes
$('[data-gh-segment]').get().forEach((node) => {
// TODO: replace with NQL interpretation
if (node.attribs['data-gh-segment'] !== segment) {
$(node).remove();
} else {
// Getting rid of the attribute for a cleaner html output
$(node).removeAttr('data-gh-segment');
}
});
// Remove duplicate black/white images (CSS based solution not working in Outlook)
if (templateData.backgroundIsDark) {
$('img.is-light-background').each((i, elem) => {
@ -715,13 +722,19 @@ class EmailRenderer {
* @param {object} postModel
* @returns
*/
#getEmailPreheader(postModel) {
#getEmailPreheader(postModel, segment, html) {
let plaintext = postModel.get('plaintext');
let customExcerpt = postModel.get('custom_excerpt');
if (customExcerpt) {
return customExcerpt;
} else {
if (plaintext) {
// The plaintext field on the model may contain paid only content
// so we use the provided HTML to generate the plaintext as this
// should have already had the paid content removed
if (segment === 'status:free') {
plaintext = htmlToPlaintext.email(html);
}
return plaintext.substring(0, 500);
} else {
return `${postModel.get('title')} `;
@ -821,7 +834,7 @@ class EmailRenderer {
/**
* @private
*/
async getTemplateData({post, newsletter, html, addPaywall}) {
async getTemplateData({post, newsletter, html, addPaywall, segment}) {
let accentColor = this.#settingsCache.get('accent_color') || '#15212A';
let adjustedAccentColor;
let adjustedAccentContrastColor;
@ -931,7 +944,7 @@ class EmailRenderer {
image: this.#settingsCache.get('icon')
}, true) : null
},
preheader: this.#getEmailPreheader(post),
preheader: this.#getEmailPreheader(post, segment, html),
html,
post: {

View file

@ -855,7 +855,7 @@ describe('Email renderer', function () {
});
describe('renderBody', function () {
let renderedPost = '<p>Lexical Test</p><img class="is-light-background" src="test-dark" /><img class="is-dark-background" src="test-light" />';
let renderedPost;
let postUrl = 'http://example.com';
let customSettings = {};
let emailRenderer;
@ -864,6 +864,7 @@ describe('Email renderer', function () {
let labsEnabled;
beforeEach(function () {
renderedPost = '<p>Lexical Test</p><img class="is-light-background" src="test-dark" /><img class="is-dark-background" src="test-light" />';
labsEnabled = true;
basePost = {
lexical: '{}',
@ -1057,6 +1058,74 @@ describe('Email renderer', function () {
should($('.preheader').text()).eql('Custom excerpt');
});
it('does not include members-only content in preheader for non-members', async function () {
renderedPost = '<div> Lexical Test </div> some text for both <!--members-only--> finishing part only for members';
let post = {
related: sinon.stub(),
get: (key) => {
if (key === 'lexical') {
return '{}';
}
if (key === 'visibility') {
return 'paid';
}
if (key === 'plaintext') {
return 'foobarbaz';
}
},
getLazyRelation: sinon.stub()
};
let newsletter = {
get: sinon.stub()
};
let response = await emailRenderer.renderBody(
post,
newsletter,
'status:free',
{}
);
const $ = cheerio.load(response.html);
should($('.preheader').text()).eql('Lexical Test some text for both');
});
it('does not include paid segmented content in preheader for non-paying members', async function () {
renderedPost = '<div> Lexical Test </div> <div data-gh-segment="status:-free"> members only section</div> some text for both';
let post = {
related: sinon.stub(),
get: (key) => {
if (key === 'lexical') {
return '{}';
}
if (key === 'visibility') {
return 'public';
}
if (key === 'plaintext') {
return 'foobarbaz';
}
},
getLazyRelation: sinon.stub()
};
let newsletter = {
get: sinon.stub()
};
let response = await emailRenderer.renderBody(
post,
newsletter,
'status:free',
{}
);
const $ = cheerio.load(response.html);
should($('.preheader').text()).eql('Lexical Test some text for both');
});
it('only includes first author if more than 2', async function () {
const post = createModel({...basePost, authors: [
createModel({