mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-22 11:16:01 +03:00
🐛 Fixed newsletter not sending if locale is invalid (#21573)
ref https://github.com/moment/luxon/blob/master/docs/intl.md - We noticed the following error trace: RangeError: Incorrect locale information provided at BatchSendingService.retryDb (/home/ghost/node_modules/@tryghost/email-service/lib/BatchSendingService.js:639:32) at new DateTimeFormat (<anonymous>) at getCachedDTF (/home/ghost/node_modules/luxon/build/node/luxon.js:621:11) at new PolyDateFormatter (/home/ghost/node_modules/luxon/build/node/luxon.js:842:16) at Locale.dtFormatter (/home/ghost/node_modules/luxon/build/node/luxon.js:1066:12) at Formatter.dtFormatter (/home/ghost/node_modules/luxon/build/node/luxon.js:2274:21) at Formatter.formatDateTime (/home/ghost/node_modules/luxon/build/node/luxon.js:2280:17) at DateTime.toLocaleString (/home/ghost/node_modules/luxon/build/node/luxon.js:6893:78) at formatDateLong (/home/ghost/node_modules/@tryghost/email-service/lib/EmailRenderer.js:45:74) at Object.getValue (/home/ghost/node_modules/@tryghost/email-service/lib/EmailRenderer.js:683:47) at /home/ghost/node_modules/@tryghost/email-service/lib/SendingService.js:158:36 at Array.map (<anonymous>) at /home/ghost/node_modules/@tryghost/email-service/lib/SendingService.js:154:54 at Array.map (<anonymous>) at SendingService.buildRecipients (/home/ghost/node_modules/@tryghost/email-service/lib/SendingService.js:151:24) at SendingService.send (/home/ghost/node_modules/@tryghost/email-service/lib/SendingService.js:127:33) at response.retryDb (/home/ghost/node_modules/@tryghost/email-service/lib/BatchSendingService.js:451:51) - This is due to the locale being user-input - it can be set to any string. - In our email sending code we pass the string to luxon to format dates, which errors if the locale is not valid according it Intl. - This fix ensures that the locale is valid before passing it to luxon, falling back to en-gb if the locale is not valid
This commit is contained in:
parent
2eb1fdf7cd
commit
d3cda0d39d
@ -12,8 +12,10 @@ const {EmailAddressParser} = require('@tryghost/email-addresses');
|
||||
const {registerHelpers} = require('./helpers/register-helpers');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const DEFAULT_LOCALE = 'en-gb';
|
||||
|
||||
// Wrapper function so that i18next-parser can find these strings
|
||||
const t = (x) => {
|
||||
const t = (x) => {
|
||||
return x;
|
||||
};
|
||||
|
||||
@ -38,10 +40,17 @@ function escapeHtml(unsafe) {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatDateLong(date, timezone, locale = 'en-gb') {
|
||||
if (locale === 'en') {
|
||||
locale = 'en-gb';
|
||||
function isValidLocale(locale) {
|
||||
try {
|
||||
// Attempt to create a DateTimeFormat with the locale
|
||||
new Intl.DateTimeFormat(locale);
|
||||
return true; // No error means it's a valid locale
|
||||
} catch (e) {
|
||||
return false; // RangeError means invalid locale
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateLong(date, timezone, locale = DEFAULT_LOCALE) {
|
||||
return DateTime.fromJSDate(date).setZone(timezone).setLocale(locale).toLocaleString({
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
@ -185,7 +194,7 @@ class EmailRenderer {
|
||||
this.#models = models;
|
||||
this.#t = t;
|
||||
}
|
||||
|
||||
|
||||
getSubject(post, isTestEmail = false) {
|
||||
const subject = post.related('posts_meta')?.get('email_subject') || post.get('title');
|
||||
return isTestEmail ? `[TEST] ${subject}` : subject;
|
||||
@ -216,6 +225,21 @@ class EmailRenderer {
|
||||
};
|
||||
}
|
||||
|
||||
// Locale is user-input, so we need to ensure it's valid
|
||||
#getValidLocale() {
|
||||
let locale = this.#settingsCache.get('locale') || DEFAULT_LOCALE;
|
||||
|
||||
// Remove any trailing whitespace
|
||||
locale = locale.trim();
|
||||
|
||||
// If the locale is just "en", or is not valid, revert to default
|
||||
if (locale === 'en' || !isValidLocale(locale)) {
|
||||
locale = DEFAULT_LOCALE;
|
||||
}
|
||||
|
||||
return locale;
|
||||
}
|
||||
|
||||
getFromAddress(post, newsletter) {
|
||||
// Clean from address to ensure DMARC alignment
|
||||
const addresses = this.#emailAddressService.getAddress({
|
||||
@ -561,8 +585,8 @@ class EmailRenderer {
|
||||
* @returns {string}
|
||||
*/
|
||||
getMemberStatusText(member) {
|
||||
const t = this.#t;
|
||||
const locale = this.#settingsCache.get('locale');
|
||||
const t = this.#t;
|
||||
const locale = this.#getValidLocale();
|
||||
|
||||
if (member.status === 'free') {
|
||||
// Not really used, but as a backup
|
||||
@ -625,7 +649,7 @@ class EmailRenderer {
|
||||
*/
|
||||
buildReplacementDefinitions({html, newsletterUuid}) {
|
||||
const t = this.#t; // es-lint-disable-line no-shadow
|
||||
const locale = this.#settingsCache.get('locale');
|
||||
const locale = this.#getValidLocale();
|
||||
|
||||
const baseDefinitions = [
|
||||
{
|
||||
|
@ -130,7 +130,7 @@ describe('Email renderer', function () {
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'free'
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
it('returns the unsubscribe header replacement by default', function () {
|
||||
@ -388,7 +388,7 @@ describe('Email renderer', function () {
|
||||
email: 'test@example.com',
|
||||
createdAt: new Date(2023, 2, 13, 12, 0),
|
||||
status: 'free'
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
it('handles dates when the locale is en-gb (default)', function () {
|
||||
@ -399,6 +399,7 @@ describe('Email renderer', function () {
|
||||
assert.equal(replacements[0].id, 'created_at');
|
||||
assert.equal(replacements[0].getValue(member), '13 March 2023');
|
||||
});
|
||||
|
||||
it('handles dates when the locale is fr', function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
@ -427,6 +428,7 @@ describe('Email renderer', function () {
|
||||
assert.equal(replacements[0].id, 'created_at');
|
||||
assert.equal(replacements[0].getValue(member), '13 mars 2023');
|
||||
});
|
||||
|
||||
it('handles dates when the locale is en (US)', function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
@ -455,6 +457,65 @@ describe('Email renderer', function () {
|
||||
assert.equal(replacements[0].id, 'created_at');
|
||||
assert.equal(replacements[0].getValue(member), '13 March 2023');
|
||||
});
|
||||
|
||||
it('handles dates when the locale has whitespace like "en "', function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor: () => 'http://example.com/subdirectory/'
|
||||
},
|
||||
labs: {
|
||||
isSet: () => labsEnabled
|
||||
},
|
||||
settingsCache: {
|
||||
get: (key) => {
|
||||
if (key === 'timezone') {
|
||||
return 'UTC';
|
||||
}
|
||||
if (key === 'locale') {
|
||||
return 'en ';
|
||||
}
|
||||
}
|
||||
},
|
||||
settingsHelpers: {getMembersValidationKey,createUnsubscribeUrl},
|
||||
t: t
|
||||
});
|
||||
const html = '%%{created_at}%%';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
assert.equal(replacements.length, 2);
|
||||
assert.equal(replacements[0].token.toString(), '/%%\\{created_at\\}%%/g');
|
||||
assert.equal(replacements[0].id, 'created_at');
|
||||
assert.equal(replacements[0].getValue(member), '13 March 2023');
|
||||
});
|
||||
|
||||
it('handles dates when the locale is invalid like "(en)"', function () {
|
||||
emailRenderer = new EmailRenderer({
|
||||
urlUtils: {
|
||||
urlFor: () => 'http://example.com/subdirectory/'
|
||||
},
|
||||
labs: {
|
||||
isSet: () => labsEnabled
|
||||
},
|
||||
settingsCache: {
|
||||
get: (key) => {
|
||||
if (key === 'timezone') {
|
||||
return 'UTC';
|
||||
}
|
||||
if (key === 'locale') {
|
||||
return '(en)';
|
||||
}
|
||||
}
|
||||
},
|
||||
settingsHelpers: {getMembersValidationKey,createUnsubscribeUrl},
|
||||
t: t
|
||||
});
|
||||
|
||||
const html = '%%{created_at}%%';
|
||||
const replacements = emailRenderer.buildReplacementDefinitions({html, newsletterUuid: newsletter.get('uuid')});
|
||||
assert.equal(replacements.length, 2);
|
||||
assert.equal(replacements[0].token.toString(), '/%%\\{created_at\\}%%/g');
|
||||
assert.equal(replacements[0].id, 'created_at');
|
||||
assert.equal(replacements[0].getValue(member), '13 March 2023');
|
||||
});
|
||||
});
|
||||
describe('isMemberTrialing', function () {
|
||||
let emailRenderer;
|
||||
|
Loading…
Reference in New Issue
Block a user