Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Commit

Permalink
Add v3.13.4
Browse files Browse the repository at this point in the history
  • Loading branch information
YannickRe committed Apr 20, 2020
1 parent b22f205 commit 84fd9ba
Show file tree
Hide file tree
Showing 19 changed files with 335 additions and 167 deletions.

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions core/frontend/meta/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,7 @@ function schemaPublisherObject(metaDataVal) {
'@type': 'Organization',
name: escapeExpression(metaDataVal.site.title),
url: metaDataVal.site.url || null,
logo: {
'@type': 'ImageObject',
url: schemaImageObject(metaDataVal.site.logo) || null
}
logo: schemaImageObject(metaDataVal.site.logo) || null
};

return publisherObject;
Expand Down
2 changes: 1 addition & 1 deletion core/frontend/services/routing/config/canary.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ module.exports.QUERY = {
module.exports.TAXONOMIES = {
tag: {
filter: 'tags:\'%s\'+tags.visibility:public',
editRedirect: '#/settings/tags/:slug/',
editRedirect: '#/tags/:slug/',
resource: 'tags'
},
author: {
Expand Down
2 changes: 1 addition & 1 deletion core/frontend/services/routing/config/v2.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ module.exports.QUERY = {
module.exports.TAXONOMIES = {
tag: {
filter: 'tags:\'%s\'+tags.visibility:public',
editRedirect: '#/settings/tags/:slug/',
editRedirect: '#/tags/:slug/',
resource: 'tags'
},
author: {
Expand Down
2 changes: 1 addition & 1 deletion core/frontend/services/routing/config/v3.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ module.exports.QUERY = {
module.exports.TAXONOMIES = {
tag: {
filter: 'tags:\'%s\'+tags.visibility:public',
editRedirect: '#/settings/tags/:slug/',
editRedirect: '#/tags/:slug/',
resource: 'tags'
},
author: {
Expand Down
12 changes: 11 additions & 1 deletion core/server/api/canary/email-preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,17 @@ module.exports = {
});
}

return mega.postEmailSerializer.serialize(model, {isBrowserPreview: true});
return mega.postEmailSerializer.serialize(model, {isBrowserPreview: true}).then(({emailTmpl, replacements}) => {
// perform replacements using no member data
replacements.forEach((replacement) => {
emailTmpl[replacement.format] = emailTmpl[replacement.format].replace(
replacement.match,
replacement.fallback || ''
);
});

return emailTmpl;
});
});
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"mobiledoc": {
"type": ["string", "null"],
"format": "json-string",
"maxLength": 1000000000
},
"html": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"mobiledoc": {
"type": ["string", "null"],
"format": "json-string",
"maxLength": 1000000000
},
"html": {
Expand Down
12 changes: 11 additions & 1 deletion core/server/api/canary/utils/validators/utils/json-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ const common = require('../../../../../lib/common');

const ajv = new Ajv({
allErrors: true,
useDefaults: true
useDefaults: true,
formats: {
'json-string': (data) => {
try {
JSON.parse(data);
return true;
} catch (e) {
return false;
}
}
}
});

stripKeyword(ajv);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"mobiledoc": {
"type": ["string", "null"],
"format": "json-string",
"maxLength": 1000000000
},
"html": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"mobiledoc": {
"type": ["string", "null"],
"format": "json-string",
"maxLength": 1000000000
},
"html": {
Expand Down
12 changes: 11 additions & 1 deletion core/server/api/v2/utils/validators/utils/json-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@ const common = require('../../../../../lib/common');

const ajv = new Ajv({
allErrors: true,
useDefaults: true
useDefaults: true,
formats: {
'json-string': (data) => {
try {
JSON.parse(data);
return true;
} catch (e) {
return false;
}
}
}
});

stripKeyword(ajv);
Expand Down
70 changes: 52 additions & 18 deletions core/server/services/mega/mega.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,41 @@ const models = require('../../models');
const postEmailSerializer = require('./post-email-serializer');
const config = require('../../config');

const getEmailData = async (postModel, recipients = []) => {
const emailTmpl = await postEmailSerializer.serialize(postModel);
const getEmailData = async (postModel, members = []) => {
const {emailTmpl, replacements} = await postEmailSerializer.serialize(postModel);

emailTmpl.from = membersService.config.getEmailFromAddress();

const emails = recipients.map(recipient => recipient.email);
const emailData = recipients.reduce((emailData, recipient) => {
return Object.assign({
[recipient.email]: {
unique_id: recipient.uuid,
unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(recipient.uuid)
}
}, emailData);
}, {});
// update templates to use Mailgun variable syntax for replacements
replacements.forEach((replacement) => {
emailTmpl[replacement.format] = emailTmpl[replacement.format].replace(
replacement.match,
`%recipient.${replacement.id}%`
);
});

const emails = [];
const emailData = {};
members.forEach((member) => {
emails.push(member.email);

// first_name is a computed property only used here for now
// TODO: move into model computed property or output serializer?
member.first_name = (member.name || '').split(' ')[0];

// add static data to mailgun template variables
const data = {
unique_id: member.uuid,
unsubscribe_url: postEmailSerializer.createUnsubscribeUrl(member.uuid)
};

// add replacement data/requested fallback to mailgun template variables
replacements.forEach(({id, memberProp, fallback}) => {
data[id] = member[memberProp] || fallback || '';
});

emailData[member.email] = data;
});

return {emailTmpl, emails, emailData};
};
Expand All @@ -36,9 +58,10 @@ const sendEmail = async (postModel, members) => {
};

const sendTestEmail = async (postModel, toEmails) => {
const recipients = toEmails.map((email) => {
return {email};
});
const recipients = await Promise.all(toEmails.map(async (email) => {
const member = await membersService.api.members.get({email});
return member || {email};
}));
const {emailTmpl, emails, emailData} = await getEmailData(postModel, recipients);
emailTmpl.subject = `[Test] ${emailTmpl.subject}`;
return bulkEmailService.send(emailTmpl, emails, emailData);
Expand All @@ -60,21 +83,30 @@ const addEmail = async (postModel, options) => {
const membersToSendTo = members.filter((member) => {
return membersService.contentGating.checkPostAccess(postModel.toJSON(), member);
});
const {emailTmpl, emails} = await getEmailData(postModel, membersToSendTo);

// NOTE: don't create email object when there's nobody to send the email to
if (!emails.length) {
if (!membersToSendTo.length) {
return null;
}

const postId = postModel.get('id');
const existing = await models.Email.findOne({post_id: postId}, knexOptions);

if (!existing) {
// get email contents and perform replacements using no member data so
// we have a decent snapshot of email content for later display
const {emailTmpl, replacements} = await postEmailSerializer.serialize(postModel, {isBrowserPreview: true});
replacements.forEach((replacement) => {
emailTmpl[replacement.format] = emailTmpl[replacement.format].replace(
replacement.match,
replacement.fallback || ''
);
});

return models.Email.add({
post_id: postId,
status: 'pending',
email_count: emails.length,
email_count: membersToSendTo.length,
subject: emailTmpl.subject,
html: emailTmpl.html,
plaintext: emailTmpl.plaintext,
Expand Down Expand Up @@ -233,7 +265,9 @@ async function pendingEmailHandler(emailModel, options) {
}

const statusChangedHandler = (emailModel, options) => {
const emailRetried = emailModel.wasChanged() && (emailModel.get('status') === 'pending') && (emailModel.previous('status') === 'failed');
const emailRetried = emailModel.wasChanged()
&& emailModel.get('status') === 'pending'
&& emailModel.previous('status') === 'failed';

if (emailRetried) {
pendingEmailHandler(emailModel, options);
Expand Down
70 changes: 67 additions & 3 deletions core/server/services/mega/post-email-serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const moment = require('moment');
const cheerio = require('cheerio');
const api = require('../../api');
const {URL} = require('url');
const mobiledocLib = require('../../lib/mobiledoc');
const htmlToText = require('html-to-text');

const getSite = () => {
const publicSettings = settingsCache.getPublic();
Expand Down Expand Up @@ -39,7 +41,8 @@ const createUnsubscribeUrl = (uuid) => {
// NOTE: serialization is needed to make sure we are using current API and do post transformations
// such as image URL transformation from relative to absolute
const serializePostModel = async (model) => {
const frame = {options: {context: {user: true}, formats: 'html, plaintext'}};
// fetch mobiledoc rather than html and plaintext so we can render email-specific contents
const frame = {options: {context: {user: true}, formats: 'mobiledoc'}};
const apiVersion = model.get('api_version') || 'v3';
const docName = 'posts';

Expand All @@ -51,29 +54,90 @@ const serializePostModel = async (model) => {
return frame.response[docName][0];
};

// parses templates and extracts an array of replacements with desired fallbacks
// removes %% wrappers from unknown replacement strings (modifies emailTmpl in place)
const _parseReplacements = (emailTmpl) => {
const EMAIL_REPLACEMENT_REGEX = /%%(\{.*?\})%%/g;
// the " is necessary here because `juice` will convert "->" for email compatibility
const REPLACEMENT_STRING_REGEX = /\{(?<memberProp>\w*?)(?:,? *(?:"|&quot;)(?<fallback>.*?)(?:"|&quot;))?\}/;
const ALLOWED_REPLACEMENTS = ['first_name'];

const replacements = [];
['html', 'plaintext'].forEach((format) => {
emailTmpl[format] = emailTmpl[format].replace(EMAIL_REPLACEMENT_REGEX, (replacementMatch, replacementStr) => {
const match = replacementStr.match(REPLACEMENT_STRING_REGEX);

if (match) {
const {memberProp, fallback} = match.groups;

if (ALLOWED_REPLACEMENTS.includes(memberProp)) {
const id = `replacement_${replacements.length + 1}`;

replacements.push({
format,
id,
match: replacementMatch,
memberProp,
fallback
});

// keeps wrapping %% for later replacement with real data
return replacementMatch;
}
}

// removes %% so output matches user supplied content
return replacementStr;
});
});

return replacements;
};

const serialize = async (postModel, options = {isBrowserPreview: false}) => {
const post = await serializePostModel(postModel);

post.published_at = post.published_at ? moment(post.published_at).format('DD MMM YYYY') : moment().format('DD MMM YYYY');
post.authors = post.authors && post.authors.map(author => author.name).join(',');
post.html = post.html || '';
if (post.posts_meta) {
post.email_subject = post.posts_meta.email_subject;
}
post.html = mobiledocLib.mobiledocHtmlRenderer.render(JSON.parse(post.mobiledoc), {target: 'email'});
// same options as used in Post model for generating plaintext but without `wordwrap: 80`
// to avoid replacement strings being split across lines and for mail clients to handle
// word wrapping based on user preferences
post.plaintext = htmlToText.fromString(post.html, {
wordwrap: false,
ignoreImage: true,
hideLinkHrefIfSameAsText: true,
preserveNewlines: true,
returnDomByDefault: true,
uppercaseHeadings: false
});

let htmlTemplate = template({post, site: getSite()});
if (options.isBrowserPreview) {
const previewUnsubscribeUrl = createUnsubscribeUrl();
htmlTemplate = htmlTemplate.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
}

let juicedHtml = juice(htmlTemplate);

// Force all links to open in new tab
let _cheerio = cheerio.load(juicedHtml);
_cheerio('a').attr('target','_blank');
juicedHtml = _cheerio.html();
return {

const emailTmpl = {
subject: post.email_subject || post.title,
html: juicedHtml,
plaintext: post.plaintext
};

// Extract known replacements and clean up unknown replacement strings
const replacements = _parseReplacements(emailTmpl);

return {emailTmpl, replacements};
};

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion core/server/web/admin/views/default-prod.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
<div id="ember-basic-dropdown-wormhole"></div>


<script src="assets/vendor.min-d101f3d8eb38856c4ed168cee6232f37.js"></script>
<script src="assets/vendor.min-dd367fd71acbc2c70621e55ba07b7193.js"></script>
<script src="assets/ghost.min-7ef23448585a3e41b82d92e96136bdd0.js"></script>

</body>
Expand Down
2 changes: 1 addition & 1 deletion core/server/web/admin/views/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
<div id="ember-basic-dropdown-wormhole"></div>


<script src="assets/vendor.min-d101f3d8eb38856c4ed168cee6232f37.js"></script>
<script src="assets/vendor.min-dd367fd71acbc2c70621e55ba07b7193.js"></script>
<script src="assets/ghost.min-7ef23448585a3e41b82d92e96136bdd0.js"></script>

</body>
Expand Down
8 changes: 4 additions & 4 deletions core/server/web/shared/middlewares/api/spam-prevention.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const config = require('../../../../config');
const {logging,i18n} = require('../../../../lib/common');
const spam = config.get('spam') || {};

const spamPrivateBlog = spam.private_blog || {};
const spamPrivateBlock = spam.private_block || {};
const spamGlobalBlock = spam.global_block || {};
const spamGlobalReset = spam.global_reset || {};
const spamUserReset = spam.user_reset || {};
Expand Down Expand Up @@ -190,8 +190,8 @@ const privateBlog = () => {
logging.error(new errors.TooManyRequestsError({
message: i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.error',
{
rateSigninAttempts: spamPrivateBlog.freeRetries + 1 || 5,
rateSigninPeriod: spamPrivateBlog.lifetime || 60 * 60
rateSigninAttempts: spamPrivateBlock.freeRetries + 1 || 5,
rateSigninPeriod: spamPrivateBlock.lifetime || 60 * 60
}),
context: i18n.t('errors.middleware.spamprevention.tooManySigninAttempts.context')
}));
Expand All @@ -201,7 +201,7 @@ const privateBlog = () => {
}));
},
handleStoreError: handleStoreError
}, pick(spamPrivateBlog, spamConfigKeys))
}, pick(spamPrivateBlock, spamConfigKeys))
);

return privateBlogInstance;
Expand Down
Loading

0 comments on commit 84fd9ba

Please sign in to comment.