mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-22 19:32:54 +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,6 +12,8 @@ const {EmailAddressParser} = require('@tryghost/email-addresses');
|
|||||||
const {registerHelpers} = require('./helpers/register-helpers');
|
const {registerHelpers} = require('./helpers/register-helpers');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
const DEFAULT_LOCALE = 'en-gb';
|
||||||
|
|
||||||
// Wrapper function so that i18next-parser can find these strings
|
// Wrapper function so that i18next-parser can find these strings
|
||||||
const t = (x) => {
|
const t = (x) => {
|
||||||
return x;
|
return x;
|
||||||
@ -38,10 +40,17 @@ function escapeHtml(unsafe) {
|
|||||||
.replace(/'/g, ''');
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateLong(date, timezone, locale = 'en-gb') {
|
function isValidLocale(locale) {
|
||||||
if (locale === 'en') {
|
try {
|
||||||
locale = 'en-gb';
|
// 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({
|
return DateTime.fromJSDate(date).setZone(timezone).setLocale(locale).toLocaleString({
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
@ -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) {
|
getFromAddress(post, newsletter) {
|
||||||
// Clean from address to ensure DMARC alignment
|
// Clean from address to ensure DMARC alignment
|
||||||
const addresses = this.#emailAddressService.getAddress({
|
const addresses = this.#emailAddressService.getAddress({
|
||||||
@ -562,7 +586,7 @@ class EmailRenderer {
|
|||||||
*/
|
*/
|
||||||
getMemberStatusText(member) {
|
getMemberStatusText(member) {
|
||||||
const t = this.#t;
|
const t = this.#t;
|
||||||
const locale = this.#settingsCache.get('locale');
|
const locale = this.#getValidLocale();
|
||||||
|
|
||||||
if (member.status === 'free') {
|
if (member.status === 'free') {
|
||||||
// Not really used, but as a backup
|
// Not really used, but as a backup
|
||||||
@ -625,7 +649,7 @@ class EmailRenderer {
|
|||||||
*/
|
*/
|
||||||
buildReplacementDefinitions({html, newsletterUuid}) {
|
buildReplacementDefinitions({html, newsletterUuid}) {
|
||||||
const t = this.#t; // es-lint-disable-line no-shadow
|
const t = this.#t; // es-lint-disable-line no-shadow
|
||||||
const locale = this.#settingsCache.get('locale');
|
const locale = this.#getValidLocale();
|
||||||
|
|
||||||
const baseDefinitions = [
|
const baseDefinitions = [
|
||||||
{
|
{
|
||||||
|
@ -399,6 +399,7 @@ describe('Email renderer', function () {
|
|||||||
assert.equal(replacements[0].id, 'created_at');
|
assert.equal(replacements[0].id, 'created_at');
|
||||||
assert.equal(replacements[0].getValue(member), '13 March 2023');
|
assert.equal(replacements[0].getValue(member), '13 March 2023');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles dates when the locale is fr', function () {
|
it('handles dates when the locale is fr', function () {
|
||||||
emailRenderer = new EmailRenderer({
|
emailRenderer = new EmailRenderer({
|
||||||
urlUtils: {
|
urlUtils: {
|
||||||
@ -427,6 +428,7 @@ describe('Email renderer', function () {
|
|||||||
assert.equal(replacements[0].id, 'created_at');
|
assert.equal(replacements[0].id, 'created_at');
|
||||||
assert.equal(replacements[0].getValue(member), '13 mars 2023');
|
assert.equal(replacements[0].getValue(member), '13 mars 2023');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('handles dates when the locale is en (US)', function () {
|
it('handles dates when the locale is en (US)', function () {
|
||||||
emailRenderer = new EmailRenderer({
|
emailRenderer = new EmailRenderer({
|
||||||
urlUtils: {
|
urlUtils: {
|
||||||
@ -455,6 +457,65 @@ describe('Email renderer', function () {
|
|||||||
assert.equal(replacements[0].id, 'created_at');
|
assert.equal(replacements[0].id, 'created_at');
|
||||||
assert.equal(replacements[0].getValue(member), '13 March 2023');
|
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 () {
|
describe('isMemberTrialing', function () {
|
||||||
let emailRenderer;
|
let emailRenderer;
|
||||||
|
Loading…
Reference in New Issue
Block a user