mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 18:52:14 +03:00
Merge branch 'mega'
This commit is contained in:
commit
f9f2d36f53
27
core/frontend/services/routing/UnsubscribeRouter.js
Normal file
27
core/frontend/services/routing/UnsubscribeRouter.js
Normal file
@ -0,0 +1,27 @@
|
||||
const ParentRouter = require('./ParentRouter');
|
||||
const controllers = require('./controllers');
|
||||
|
||||
/**
|
||||
* @description Unsubscribe Router.
|
||||
*
|
||||
* "/unsubscribe/" -> Unsubscribe Router
|
||||
*/
|
||||
class UnsubscribeRouter extends ParentRouter {
|
||||
constructor() {
|
||||
super('UnsubscribeRouter');
|
||||
|
||||
// @NOTE: hardcoded, not configurable
|
||||
this.route = {value: '/unsubscribe/'};
|
||||
this._registerRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Register all routes of this router.
|
||||
* @private
|
||||
*/
|
||||
_registerRoutes() {
|
||||
this.mountRoute(this.route.value, controllers.unsubscribe);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UnsubscribeRouter;
|
8
core/frontend/services/routing/bootstrap.js
vendored
8
core/frontend/services/routing/bootstrap.js
vendored
@ -8,6 +8,7 @@ const CollectionRouter = require('./CollectionRouter');
|
||||
const TaxonomyRouter = require('./TaxonomyRouter');
|
||||
const PreviewRouter = require('./PreviewRouter');
|
||||
const ParentRouter = require('./ParentRouter');
|
||||
const UnsubscribeRouter = require('./UnsubscribeRouter');
|
||||
|
||||
const defaultApiVersion = 'v3';
|
||||
|
||||
@ -52,7 +53,7 @@ module.exports.init = (options = {start: false}) => {
|
||||
* The routers are created in a specific order. This order defines who can get a resource first or
|
||||
* who can dominant other routers.
|
||||
*
|
||||
* 1. Preview Router: Is the strongest and is an inbuilt feature, which you can never override.
|
||||
* 1. Preview + Unsubscribe Routers: Strongest inbuilt features, which you can never override.
|
||||
* 2. Static Routes: Very strong, because you can override any urls and redirect to a static route.
|
||||
* 3. Taxonomies: Stronger than collections, because it's an inbuilt feature.
|
||||
* 4. Collections
|
||||
@ -62,8 +63,11 @@ module.exports.init = (options = {start: false}) => {
|
||||
module.exports.start = (apiVersion) => {
|
||||
const RESOURCE_CONFIG = require(`./config/${apiVersion}`);
|
||||
|
||||
const previewRouter = new PreviewRouter(RESOURCE_CONFIG);
|
||||
const unsubscribeRouter = new UnsubscribeRouter();
|
||||
siteRouter.mountRouter(unsubscribeRouter.router());
|
||||
registry.setRouter('unsubscribeRouter', unsubscribeRouter);
|
||||
|
||||
const previewRouter = new PreviewRouter(RESOURCE_CONFIG);
|
||||
siteRouter.mountRouter(previewRouter.router());
|
||||
registry.setRouter('previewRouter', previewRouter);
|
||||
|
||||
|
@ -21,5 +21,9 @@ module.exports = {
|
||||
|
||||
get static() {
|
||||
return require('./static');
|
||||
},
|
||||
|
||||
get unsubscribe() {
|
||||
return require('./unsubscribe');
|
||||
}
|
||||
};
|
||||
|
31
core/frontend/services/routing/controllers/unsubscribe.js
Normal file
31
core/frontend/services/routing/controllers/unsubscribe.js
Normal file
@ -0,0 +1,31 @@
|
||||
const debug = require('ghost-ignition').debug('services:routing:controllers:unsubscribe');
|
||||
const path = require('path');
|
||||
const megaService = require('../../../../server/services/mega');
|
||||
const labsService = require('../../../../server/services/labs');
|
||||
const helpers = require('../../../services/routing/helpers');
|
||||
|
||||
module.exports = async function unsubscribeController(req, res, next) {
|
||||
debug('unsubscribeController');
|
||||
|
||||
if (!labsService.isSet('members')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
data.member = await megaService.mega.handleUnsubscribeRequest(req);
|
||||
} catch (err) {
|
||||
data.error = err.message;
|
||||
}
|
||||
|
||||
const templateName = 'unsubscribe';
|
||||
|
||||
res.routerOptions = {
|
||||
type: 'custom',
|
||||
templates: templateName,
|
||||
defaultTemplate: path.resolve(__dirname, '../../../views/', templateName)
|
||||
};
|
||||
|
||||
return helpers.renderer(req, res, data);
|
||||
};
|
53
core/frontend/views/unsubscribe.hbs
Normal file
53
core/frontend/views/unsubscribe.hbs
Normal file
@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>
|
||||
{{#if member}}Successfully Unsubscribed{{/if}}
|
||||
{{#if error}}Unsubscribe Failed{{/if}}
|
||||
</title>
|
||||
<link rel="stylesheet" href="{{asset "public/ghost.css" hasMinFile="true"}}" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="gh-app">
|
||||
<div class="gh-viewport">
|
||||
<main class="gh-main" role="main">
|
||||
<div class="gh-flow">
|
||||
<header class="gh-flow-head gh-flow-head-unsubscribe">
|
||||
<nav class="gh-flow-nav">
|
||||
<a href="{{@site.url}}" class="gh-flow-back-plain">
|
||||
<!--?xml version="1.0" encoding="UTF-8" standalone="no"?-->
|
||||
<svg width="17px" height="27px" viewBox="0 0 17 27" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<g id="Stroke-42" transform="translate(2.000000, 2.000000)" stroke-width="3"
|
||||
stroke="#7d878a">
|
||||
<polyline points="13.5401 0.4256 0.3971 11.9256 13.5401 23.4256"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
Back to {{@site.title}}
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div class="gh-flow-content-wrap">
|
||||
<section class="gh-flow-content gh-flow-content-unsubscribe">
|
||||
<p>
|
||||
{{#if error}}
|
||||
{{error}}
|
||||
{{else}}
|
||||
{{#if member}}<span class="gh-flow-em">{{member.email}}</span> has been successfully unsubscribed{{/if}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
69
core/server/api/canary/email-preview.js
Normal file
69
core/server/api/canary/email-preview.js
Normal file
@ -0,0 +1,69 @@
|
||||
const models = require('../../models');
|
||||
const common = require('../../lib/common');
|
||||
const mega = require('../../services/mega');
|
||||
|
||||
module.exports = {
|
||||
docName: 'email_preview',
|
||||
|
||||
read: {
|
||||
options: [
|
||||
'fields'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
fields: ['html', 'plaintext', 'subject']
|
||||
}
|
||||
},
|
||||
data: [
|
||||
'id',
|
||||
'status'
|
||||
],
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
const options = Object.assign(frame.options, {formats: 'html,plaintext', withRelated: ['authors', 'posts_meta']});
|
||||
const data = Object.assign(frame.data, {status: 'all'});
|
||||
return models.Post.findOne(data, options)
|
||||
.then((model) => {
|
||||
if (!model) {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.posts.postNotFound')
|
||||
});
|
||||
}
|
||||
|
||||
return mega.postEmailSerializer.serialize(model, {isBrowserPreview: true});
|
||||
});
|
||||
}
|
||||
},
|
||||
sendTestEmail: {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
options: [
|
||||
'id'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
id: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
async query(frame) {
|
||||
const options = Object.assign(frame.options, {status: 'all'});
|
||||
let model = await models.Post.findOne(options, {withRelated: ['authors']});
|
||||
if (!model) {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.api.posts.postNotFound')
|
||||
});
|
||||
}
|
||||
const {emails = []} = frame.data;
|
||||
const response = await mega.mega.sendTestEmail(model, emails);
|
||||
if (response && response[0] && response[0].error) {
|
||||
throw new common.errors.EmailError({
|
||||
message: response[0].error.message
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
}
|
||||
};
|
59
core/server/api/canary/email.js
Normal file
59
core/server/api/canary/email.js
Normal file
@ -0,0 +1,59 @@
|
||||
const models = require('../../models');
|
||||
const common = require('../../lib/common');
|
||||
const megaService = require('../../services/mega');
|
||||
|
||||
module.exports = {
|
||||
docName: 'emails',
|
||||
|
||||
read: {
|
||||
options: [
|
||||
'fields'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
fields: ['html', 'plaintext', 'subject']
|
||||
}
|
||||
},
|
||||
data: [
|
||||
'id'
|
||||
],
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
return models.Email.findOne(frame.data, frame.options)
|
||||
.then((model) => {
|
||||
if (!model) {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.models.email.emailNotFound')
|
||||
});
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
retry: {
|
||||
data: [
|
||||
'id'
|
||||
],
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
return models.Email.findOne(frame.data, frame.options)
|
||||
.then(async (model) => {
|
||||
if (!model) {
|
||||
throw new common.errors.NotFoundError({
|
||||
message: common.i18n.t('errors.models.email.emailNotFound')
|
||||
});
|
||||
}
|
||||
|
||||
if (model.get('status') !== 'failed') {
|
||||
throw new common.errors.IncorrectUsageError({
|
||||
message: common.i18n.t('errors.models.email.retryNotAllowed')
|
||||
});
|
||||
}
|
||||
|
||||
return await megaService.mega.retryFailedEmail(model);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
@ -107,6 +107,14 @@ module.exports = {
|
||||
return shared.pipeline(require('./actions'), localUtils);
|
||||
},
|
||||
|
||||
get email_preview() {
|
||||
return shared.pipeline(require('./email-preview'), localUtils);
|
||||
},
|
||||
|
||||
get emails() {
|
||||
return shared.pipeline(require('./email'), localUtils);
|
||||
},
|
||||
|
||||
get site() {
|
||||
return shared.pipeline(require('./site'), localUtils);
|
||||
},
|
||||
|
@ -1,7 +1,8 @@
|
||||
const models = require('../../models');
|
||||
const common = require('../../lib/common');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
const allowedIncludes = ['tags', 'authors', 'authors.roles'];
|
||||
const {mega} = require('../../services/mega');
|
||||
const allowedIncludes = ['tags', 'authors', 'authors.roles', 'email'];
|
||||
const unsafeAttrs = ['status', 'authors', 'visibility'];
|
||||
|
||||
module.exports = {
|
||||
@ -84,7 +85,8 @@ module.exports = {
|
||||
headers: {},
|
||||
options: [
|
||||
'include',
|
||||
'source'
|
||||
'source',
|
||||
'send_email_when_published'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
@ -119,6 +121,7 @@ module.exports = {
|
||||
'include',
|
||||
'id',
|
||||
'source',
|
||||
'send_email_when_published',
|
||||
// NOTE: only for internal context
|
||||
'forUpdate',
|
||||
'transacting'
|
||||
@ -141,6 +144,27 @@ module.exports = {
|
||||
},
|
||||
query(frame) {
|
||||
return models.Post.edit(frame.data.posts[0], frame.options)
|
||||
.then(async (model) => {
|
||||
if (!model.get('send_email_when_published')) {
|
||||
return model;
|
||||
}
|
||||
|
||||
const postPublished = model.wasChanged() && (model.get('status') === 'published') && (model.previous('status') !== 'published');
|
||||
|
||||
if (postPublished) {
|
||||
let postEmail = model.relations.email;
|
||||
|
||||
if (!postEmail) {
|
||||
const email = await mega.addEmail(model, frame.options);
|
||||
model.set('email', email);
|
||||
} else if (postEmail && postEmail.get('status') === 'failed') {
|
||||
const email = await mega.retryFailedEmail(postEmail);
|
||||
model.set('email', email);
|
||||
}
|
||||
}
|
||||
|
||||
return model;
|
||||
})
|
||||
.then((model) => {
|
||||
if (
|
||||
model.get('status') === 'published' && model.wasChanged() ||
|
||||
|
@ -37,7 +37,7 @@ function defaultRelations(frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
frame.options.withRelated = ['tags', 'authors', 'authors.roles'];
|
||||
frame.options.withRelated = ['tags', 'authors', 'authors.roles', 'email'];
|
||||
}
|
||||
|
||||
function setDefaultOrder(frame) {
|
||||
|
@ -49,6 +49,12 @@ module.exports = {
|
||||
if (['cover_image', 'icon', 'logo'].includes(setting.key)) {
|
||||
setting = url.forSetting(setting);
|
||||
}
|
||||
|
||||
//CASE: Ensure we don't store calculated fields `isEnabled/Config` in bulk email settings
|
||||
if (setting.key === 'bulk_email_settings') {
|
||||
const {apiKey = '', domain = '', baseUrl = '', provider = 'mailgun'} = setting.value ? JSON.parse(setting.value) : {};
|
||||
setting.value = JSON.stringify({apiKey, domain, baseUrl, provider});
|
||||
}
|
||||
});
|
||||
|
||||
// CASE: deprecated, won't accept
|
||||
|
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
read(emailPreview, apiConfig, frame) {
|
||||
frame.response = {
|
||||
email_previews: [emailPreview]
|
||||
};
|
||||
}
|
||||
};
|
11
core/server/api/canary/utils/serializers/output/emails.js
Normal file
11
core/server/api/canary/utils/serializers/output/emails.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
read(email, apiConfig, frame) {
|
||||
frame.response = {
|
||||
emails: [email.toJSON(frame.options)]
|
||||
};
|
||||
},
|
||||
|
||||
get retry() {
|
||||
return this.read;
|
||||
}
|
||||
};
|
@ -101,5 +101,13 @@ module.exports = {
|
||||
|
||||
get site() {
|
||||
return require('./site');
|
||||
},
|
||||
|
||||
get email_preview() {
|
||||
return require('./email-preview');
|
||||
},
|
||||
|
||||
get emails() {
|
||||
return require('./emails');
|
||||
}
|
||||
};
|
||||
|
@ -12,7 +12,7 @@ module.exports = {
|
||||
|
||||
if (models.meta) {
|
||||
frame.response = {
|
||||
pages: models.data.map(model => mapper.mapPost(model, frame)),
|
||||
pages: models.data.map(model => mapper.mapPage(model, frame)),
|
||||
meta: models.meta
|
||||
};
|
||||
|
||||
@ -20,7 +20,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
frame.response = {
|
||||
pages: [mapper.mapPost(models, frame)]
|
||||
pages: [mapper.mapPage(models, frame)]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
const localUtils = require('../../../index');
|
||||
const config = require('../../../../../../config');
|
||||
|
||||
const tag = (attrs, frame) => {
|
||||
if (localUtils.isContentAPI(frame)) {
|
||||
@ -138,7 +139,28 @@ const action = (attrs) => {
|
||||
}
|
||||
};
|
||||
|
||||
const settings = (attrs) => {
|
||||
if (_.isArray(attrs)) {
|
||||
attrs.forEach((attr) => {
|
||||
if (attr.key === 'bulk_email_settings') {
|
||||
const {provider, apiKey, domain, baseUrl} = attr.value ? JSON.parse(attr.value) : {};
|
||||
|
||||
const bulkEmailConfig = config.get('bulkEmail');
|
||||
const hasMailgunConfig = !!(bulkEmailConfig && bulkEmailConfig.mailgun);
|
||||
const hasMailgunSetting = !!(apiKey && baseUrl && domain);
|
||||
|
||||
attr.value = JSON.stringify({
|
||||
provider, apiKey, domain, baseUrl,
|
||||
isEnabled: (hasMailgunConfig || hasMailgunSetting),
|
||||
isConfig: hasMailgunConfig
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.post = post;
|
||||
module.exports.tag = tag;
|
||||
module.exports.author = author;
|
||||
module.exports.action = action;
|
||||
module.exports.settings = settings;
|
||||
|
@ -60,6 +60,10 @@ const mapPost = (model, frame) => {
|
||||
if (relation === 'authors' && jsonModel.authors) {
|
||||
jsonModel.authors = jsonModel.authors.map(author => mapUser(author, frame));
|
||||
}
|
||||
|
||||
if (relation === 'email' && _.isEmpty(jsonModel.email)) {
|
||||
jsonModel.email = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -75,9 +79,19 @@ const mapPost = (model, frame) => {
|
||||
return jsonModel;
|
||||
};
|
||||
|
||||
const mapPage = (model, frame) => {
|
||||
const jsonModel = mapPost(model, frame);
|
||||
|
||||
delete jsonModel.email_subject;
|
||||
delete jsonModel.send_email_when_published;
|
||||
|
||||
return jsonModel;
|
||||
};
|
||||
|
||||
const mapSettings = (attrs, frame) => {
|
||||
url.forSettings(attrs);
|
||||
extraAttrs.forSettings(attrs, frame);
|
||||
clean.settings(attrs, frame);
|
||||
|
||||
// NOTE: The cleanup of deprecated ghost_head/ghost_foot has to happen here
|
||||
// because codeinjection_head/codeinjection_foot are assigned on a previous
|
||||
@ -125,6 +139,7 @@ const mapMember = (model, frame) => {
|
||||
};
|
||||
|
||||
module.exports.mapPost = mapPost;
|
||||
module.exports.mapPage = mapPage;
|
||||
module.exports.mapUser = mapUser;
|
||||
module.exports.mapTag = mapTag;
|
||||
module.exports.mapIntegration = mapIntegration;
|
||||
|
@ -1,37 +1,11 @@
|
||||
const _ = require('lodash');
|
||||
const membersService = require('../../../../../../services/members');
|
||||
const labs = require('../../../../../../services/labs');
|
||||
|
||||
// Checks if request should hide members only content
|
||||
function hideMembersOnlyContent({visibility}, frame) {
|
||||
const PERMIT_CONTENT = false;
|
||||
const BLOCK_CONTENT = true;
|
||||
|
||||
if (visibility === 'public') {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
const requestFromMember = frame.original.context.member;
|
||||
|
||||
if (!requestFromMember) {
|
||||
return BLOCK_CONTENT;
|
||||
} else if (visibility === 'members') {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
const memberHasPlan = !!(_.get(frame, 'original.context.member.stripe.subscriptions', [])).length;
|
||||
|
||||
if (visibility === 'paid' && memberHasPlan) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
return BLOCK_CONTENT;
|
||||
}
|
||||
|
||||
const forPost = (attrs, frame) => {
|
||||
if (labs.isSet('members')) {
|
||||
const hideFormatsData = hideMembersOnlyContent(attrs, frame);
|
||||
const memberHasAccess = membersService.contentGating.checkPostAccess(attrs, frame.original.context.member);
|
||||
|
||||
if (hideFormatsData) {
|
||||
if (!memberHasAccess) {
|
||||
['plaintext', 'html'].forEach((field) => {
|
||||
attrs[field] = '';
|
||||
});
|
||||
|
@ -25,6 +25,9 @@
|
||||
"minLength": 0,
|
||||
"maxLength": 2000
|
||||
},
|
||||
"subscribed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"strip": true
|
||||
},
|
||||
|
@ -99,6 +99,10 @@
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 500
|
||||
},
|
||||
"email_subject": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 300
|
||||
},
|
||||
"custom_template": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 100
|
||||
@ -152,6 +156,12 @@
|
||||
},
|
||||
"plaintext": {
|
||||
"strip": true
|
||||
},
|
||||
"email": {
|
||||
"strip": true
|
||||
},
|
||||
"send_email_when_published": {
|
||||
"strip": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,37 +1,11 @@
|
||||
const _ = require('lodash');
|
||||
const membersService = require('../../../../../../services/members');
|
||||
const labs = require('../../../../../../services/labs');
|
||||
|
||||
// Checks if request should hide members only content
|
||||
function hideMembersOnlyContent({visibility}, frame) {
|
||||
const PERMIT_CONTENT = false;
|
||||
const BLOCK_CONTENT = true;
|
||||
|
||||
if (visibility === 'public') {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
const requestFromMember = frame.original.context.member;
|
||||
|
||||
if (!requestFromMember) {
|
||||
return BLOCK_CONTENT;
|
||||
} else if (visibility === 'members') {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
const memberHasPlan = !!(_.get(frame, 'original.context.member.stripe.subscriptions', [])).length;
|
||||
|
||||
if (visibility === 'paid' && memberHasPlan) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
return BLOCK_CONTENT;
|
||||
}
|
||||
|
||||
const forPost = (attrs, frame) => {
|
||||
if (labs.isSet('members')) {
|
||||
const hideFormatsData = hideMembersOnlyContent(attrs, frame);
|
||||
const memberHasAccess = membersService.contentGating.checkPostAccess(attrs, frame.original.context.member);
|
||||
|
||||
if (hideFormatsData) {
|
||||
if (!memberHasAccess) {
|
||||
['plaintext', 'html'].forEach((field) => {
|
||||
attrs[field] = '';
|
||||
});
|
||||
|
@ -0,0 +1,25 @@
|
||||
const commands = require('../../../schema').commands;
|
||||
|
||||
module.exports.up = commands.createColumnMigration({
|
||||
table: 'posts',
|
||||
column: 'send_email_when_published',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === true;
|
||||
},
|
||||
operation: commands.addColumn,
|
||||
operationVerb: 'Adding'
|
||||
});
|
||||
|
||||
module.exports.down = commands.createColumnMigration({
|
||||
table: 'posts',
|
||||
column: 'send_email_when_published',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === false;
|
||||
},
|
||||
operation: commands.dropColumn,
|
||||
operationVerb: 'Removing'
|
||||
});
|
||||
|
||||
module.exports.config = {
|
||||
transaction: true
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
const commands = require('../../../schema').commands;
|
||||
|
||||
module.exports.up = commands.createColumnMigration({
|
||||
table: 'posts_meta',
|
||||
column: 'email_subject',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === true;
|
||||
},
|
||||
operation: commands.addColumn,
|
||||
operationVerb: 'Adding'
|
||||
});
|
||||
|
||||
module.exports.down = commands.createColumnMigration({
|
||||
table: 'posts_meta',
|
||||
column: 'email_subject',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === false;
|
||||
},
|
||||
operation: commands.dropColumn,
|
||||
operationVerb: 'Removing'
|
||||
});
|
||||
|
||||
module.exports.config = {
|
||||
transaction: true
|
||||
};
|
@ -0,0 +1,51 @@
|
||||
const _ = require('lodash');
|
||||
const utils = require('../../../schema/fixtures/utils');
|
||||
const permissions = require('../../../../services/permissions');
|
||||
const logging = require('../../../../lib/common/logging');
|
||||
|
||||
const resources = ['email_preview'];
|
||||
const _private = {};
|
||||
|
||||
_private.getPermissions = function getPermissions(resource) {
|
||||
return utils.findModelFixtures('Permission', {object_type: resource});
|
||||
};
|
||||
|
||||
_private.printResult = function printResult(result, message) {
|
||||
if (result.done === result.expected) {
|
||||
logging.info(message);
|
||||
} else {
|
||||
logging.warn(`(${result.done}/${result.expected}) ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.config = {
|
||||
transaction: true
|
||||
};
|
||||
|
||||
module.exports.up = (options) => {
|
||||
const localOptions = _.merge({
|
||||
context: {internal: true}
|
||||
}, options);
|
||||
|
||||
return Promise.map(resources, (resource) => {
|
||||
const modelToAdd = _private.getPermissions(resource);
|
||||
|
||||
return utils.addFixturesForModel(modelToAdd, localOptions)
|
||||
.then(result => _private.printResult(result, `Adding permissions fixtures for ${resource}s`))
|
||||
.then(() => permissions.init(localOptions));
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.down = (options) => {
|
||||
const localOptions = _.merge({
|
||||
context: {internal: true}
|
||||
}, options);
|
||||
|
||||
return Promise.map(resources, (resource) => {
|
||||
const modelToRemove = _private.getPermissions(resource);
|
||||
|
||||
// permission model automatically cleans up permissions_roles on .destroy()
|
||||
return utils.removeFixturesForModel(modelToRemove, localOptions)
|
||||
.then(result => _private.printResult(result, `Removing permissions fixtures for ${resource}s`));
|
||||
});
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
const common = require('../../../../lib/common');
|
||||
const commands = require('../../../schema').commands;
|
||||
|
||||
const createLog = type => msg => common.logging[type](msg);
|
||||
|
||||
function createColumnMigration({table, column, dbIsInCorrectState, operation, operationVerb}) {
|
||||
return function columnMigrations({transacting}) {
|
||||
return transacting.schema.hasColumn(table, column)
|
||||
.then(dbIsInCorrectState)
|
||||
.then((isInCorrectState) => {
|
||||
const log = createLog(isInCorrectState ? 'warn' : 'info');
|
||||
|
||||
log(`${operationVerb} ${table}.${column}`);
|
||||
|
||||
if (!isInCorrectState) {
|
||||
return operation(table, column, transacting);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports.up = createColumnMigration({
|
||||
table: 'members',
|
||||
column: 'subscribed',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === true;
|
||||
},
|
||||
operation: commands.addColumn,
|
||||
operationVerb: 'Adding'
|
||||
});
|
||||
|
||||
module.exports.down = createColumnMigration({
|
||||
table: 'members',
|
||||
column: 'subscribed',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === false;
|
||||
},
|
||||
operation: commands.dropColumn,
|
||||
operationVerb: 'Removing'
|
||||
});
|
||||
|
||||
module.exports.config = {
|
||||
transaction: true
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
const common = require('../../../../lib/common');
|
||||
const commands = require('../../../schema').commands;
|
||||
const table = 'emails';
|
||||
const message1 = 'Adding table: ' + table;
|
||||
const message2 = 'Dropping table: ' + table;
|
||||
|
||||
module.exports.up = (options) => {
|
||||
const connection = options.connection;
|
||||
|
||||
return connection.schema.hasTable(table)
|
||||
.then(function (exists) {
|
||||
if (exists) {
|
||||
common.logging.warn(message1);
|
||||
return;
|
||||
}
|
||||
|
||||
common.logging.info(message1);
|
||||
return commands.createTable(table, connection);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.down = (options) => {
|
||||
const connection = options.connection;
|
||||
|
||||
return connection.schema.hasTable(table)
|
||||
.then(function (exists) {
|
||||
if (!exists) {
|
||||
common.logging.warn(message2);
|
||||
return;
|
||||
}
|
||||
|
||||
common.logging.info(message2);
|
||||
return commands.deleteTable(table, connection);
|
||||
});
|
||||
};
|
@ -0,0 +1,51 @@
|
||||
const _ = require('lodash');
|
||||
const utils = require('../../../schema/fixtures/utils');
|
||||
const permissions = require('../../../../services/permissions');
|
||||
const logging = require('../../../../lib/common/logging');
|
||||
|
||||
const resources = ['email'];
|
||||
const _private = {};
|
||||
|
||||
_private.getPermissions = function getPermissions(resource) {
|
||||
return utils.findModelFixtures('Permission', {object_type: resource});
|
||||
};
|
||||
|
||||
_private.printResult = function printResult(result, message) {
|
||||
if (result.done === result.expected) {
|
||||
logging.info(message);
|
||||
} else {
|
||||
logging.warn(`(${result.done}/${result.expected}) ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.config = {
|
||||
transaction: true
|
||||
};
|
||||
|
||||
module.exports.up = (options) => {
|
||||
const localOptions = _.merge({
|
||||
context: {internal: true}
|
||||
}, options);
|
||||
|
||||
return Promise.map(resources, (resource) => {
|
||||
const modelToAdd = _private.getPermissions(resource);
|
||||
|
||||
return utils.addFixturesForModel(modelToAdd, localOptions)
|
||||
.then(result => _private.printResult(result, `Adding permissions fixtures for ${resource}`))
|
||||
.then(() => permissions.init(localOptions));
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.down = (options) => {
|
||||
const localOptions = _.merge({
|
||||
context: {internal: true}
|
||||
}, options);
|
||||
|
||||
return Promise.map(resources, (resource) => {
|
||||
const modelToRemove = _private.getPermissions(resource);
|
||||
|
||||
// permission model automatically cleans up permissions_roles on .destroy()
|
||||
return utils.removeFixturesForModel(modelToRemove, localOptions)
|
||||
.then(result => _private.printResult(result, `Removing permissions fixtures for ${resource}s`));
|
||||
});
|
||||
};
|
@ -0,0 +1,44 @@
|
||||
const common = require('../../../../lib/common');
|
||||
const commands = require('../../../schema').commands;
|
||||
|
||||
const createLog = type => msg => common.logging[type](msg);
|
||||
|
||||
function createColumnMigration({table, column, dbIsInCorrectState, operation, operationVerb}) {
|
||||
return function columnMigrations({transacting}) {
|
||||
return transacting.schema.hasColumn(table, column)
|
||||
.then(dbIsInCorrectState)
|
||||
.then((isInCorrectState) => {
|
||||
const log = createLog(isInCorrectState ? 'warn' : 'info');
|
||||
|
||||
log(`${operationVerb} ${table}.${column}`);
|
||||
|
||||
if (!isInCorrectState) {
|
||||
return operation(table, column, transacting);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports.up = createColumnMigration({
|
||||
table: 'members',
|
||||
column: 'uuid',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === true;
|
||||
},
|
||||
operation: commands.addColumn,
|
||||
operationVerb: 'Adding'
|
||||
});
|
||||
|
||||
module.exports.down = createColumnMigration({
|
||||
table: 'members',
|
||||
column: 'uuid',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === false;
|
||||
},
|
||||
operation: commands.dropColumn,
|
||||
operationVerb: 'Removing'
|
||||
});
|
||||
|
||||
module.exports.config = {
|
||||
transaction: true
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
const common = require('../../../../lib/common');
|
||||
const uuid = require('uuid');
|
||||
|
||||
module.exports = {
|
||||
config: {
|
||||
transaction: true
|
||||
},
|
||||
async up(options) {
|
||||
const conn = options.connection || options.transacting;
|
||||
|
||||
const membersWithoutUUID = await conn.select('id').from('members').whereNull('uuid');
|
||||
|
||||
common.logging.info(`Adding uuid field value to ${membersWithoutUUID.length} members.`);
|
||||
|
||||
for (const member of membersWithoutUUID) {
|
||||
await conn('members').update('uuid', uuid.v4()).where('id', member.id);
|
||||
}
|
||||
},
|
||||
async down() {
|
||||
// noop
|
||||
}
|
||||
};
|
@ -0,0 +1,51 @@
|
||||
const _ = require('lodash');
|
||||
const utils = require('../../../schema/fixtures/utils');
|
||||
const permissions = require('../../../../services/permissions');
|
||||
const logging = require('../../../../lib/common/logging');
|
||||
|
||||
const resources = ['email'];
|
||||
const _private = {};
|
||||
|
||||
_private.getPermissions = function getPermissions(resource) {
|
||||
return utils.findModelFixtures('Permission', {object_type: resource});
|
||||
};
|
||||
|
||||
_private.printResult = function printResult(result, message) {
|
||||
if (result.done === result.expected) {
|
||||
logging.info(message);
|
||||
} else {
|
||||
logging.warn(`(${result.done}/${result.expected}) ${message}`);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.config = {
|
||||
transaction: true
|
||||
};
|
||||
|
||||
module.exports.up = (options) => {
|
||||
const localOptions = _.merge({
|
||||
context: {internal: true}
|
||||
}, options);
|
||||
|
||||
return Promise.map(resources, (resource) => {
|
||||
const modelToAdd = _private.getPermissions(resource);
|
||||
|
||||
return utils.addFixturesForModel(modelToAdd, localOptions)
|
||||
.then(result => _private.printResult(result, `Adding permissions fixtures for ${resource}`))
|
||||
.then(() => permissions.init(localOptions));
|
||||
});
|
||||
};
|
||||
|
||||
module.exports.down = (options) => {
|
||||
const localOptions = _.merge({
|
||||
context: {internal: true}
|
||||
}, options);
|
||||
|
||||
return Promise.map(resources, (resource) => {
|
||||
const modelToRemove = _private.getPermissions(resource);
|
||||
|
||||
// permission model automatically cleans up permissions_roles on .destroy()
|
||||
return utils.removeFixturesForModel(modelToRemove, localOptions)
|
||||
.then(result => _private.printResult(result, `Removing permissions fixtures for ${resource}s`));
|
||||
});
|
||||
};
|
@ -0,0 +1,25 @@
|
||||
const commands = require('../../../schema').commands;
|
||||
|
||||
module.exports.up = commands.createColumnMigration({
|
||||
table: 'emails',
|
||||
column: 'error_data',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === true;
|
||||
},
|
||||
operation: commands.addColumn,
|
||||
operationVerb: 'Adding'
|
||||
});
|
||||
|
||||
module.exports.down = commands.createColumnMigration({
|
||||
table: 'emails',
|
||||
column: 'error_data',
|
||||
dbIsInCorrectState(columnExists) {
|
||||
return columnExists === false;
|
||||
},
|
||||
operation: commands.dropColumn,
|
||||
operationVerb: 'Removing'
|
||||
});
|
||||
|
||||
module.exports.config = {
|
||||
transaction: true
|
||||
};
|
@ -203,5 +203,10 @@
|
||||
"members_subscription_settings": {
|
||||
"defaultValue": "{\"isPaid\":false,\"fromAddress\":\"noreply\",\"allowSelfSignup\":true,\"paymentProcessors\":[{\"adapter\":\"stripe\",\"config\":{\"secret_token\":\"\",\"public_token\":\"\",\"product\":{\"name\":\"Ghost Subscription\"},\"plans\":[{\"name\":\"Monthly\",\"currency\":\"usd\",\"interval\":\"month\",\"amount\":\"\"},{\"name\":\"Yearly\",\"currency\":\"usd\",\"interval\":\"year\",\"amount\":\"\"}]}}]}"
|
||||
}
|
||||
},
|
||||
"bulk_email": {
|
||||
"bulk_email_settings": {
|
||||
"defaultValue": "{\"provider\":\"mailgun\", \"apiKey\": \"\", \"domain\": \"\", \"baseUrl\": \"\"}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -362,6 +362,31 @@
|
||||
"name": "Backup database",
|
||||
"action_type": "backupContent",
|
||||
"object_type": "db"
|
||||
},
|
||||
{
|
||||
"name": "Email preview",
|
||||
"action_type": "read",
|
||||
"object_type": "email_preview"
|
||||
},
|
||||
{
|
||||
"name": "Send test email",
|
||||
"action_type": "sendTestEmail",
|
||||
"object_type": "email_preview"
|
||||
},
|
||||
{
|
||||
"name": "Browse emails",
|
||||
"action_type": "browse",
|
||||
"object_type": "email"
|
||||
},
|
||||
{
|
||||
"name": "Read emails",
|
||||
"action_type": "read",
|
||||
"object_type": "email"
|
||||
},
|
||||
{
|
||||
"name": "Retry emails",
|
||||
"action_type": "retry",
|
||||
"object_type": "email"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -556,7 +581,9 @@
|
||||
"integration": "all",
|
||||
"api_key": "all",
|
||||
"action": "all",
|
||||
"member": "all"
|
||||
"member": "all",
|
||||
"email_preview": "all",
|
||||
"email": "all"
|
||||
},
|
||||
"DB Backup Integration": {
|
||||
"db": "all"
|
||||
@ -578,7 +605,9 @@
|
||||
"redirect": "all",
|
||||
"webhook": "all",
|
||||
"action": "all",
|
||||
"member": "all"
|
||||
"member": "all",
|
||||
"email_preview": "all",
|
||||
"email": "all"
|
||||
},
|
||||
"Editor": {
|
||||
"notification": "all",
|
||||
@ -589,7 +618,9 @@
|
||||
"user": "all",
|
||||
"role": "all",
|
||||
"invite": "all",
|
||||
"theme": ["browse"]
|
||||
"theme": ["browse"],
|
||||
"email_preview": "all",
|
||||
"email": "all"
|
||||
},
|
||||
"Author": {
|
||||
"post": ["browse", "read", "add"],
|
||||
@ -598,7 +629,9 @@
|
||||
"tag": ["browse", "read", "add"],
|
||||
"user": ["browse", "read"],
|
||||
"role": ["browse"],
|
||||
"theme": ["browse"]
|
||||
"theme": ["browse"],
|
||||
"email_preview": "read",
|
||||
"email": "read"
|
||||
},
|
||||
"Contributor": {
|
||||
"post": ["browse", "read", "add"],
|
||||
@ -607,7 +640,9 @@
|
||||
"tag": ["browse", "read"],
|
||||
"user": ["browse", "read"],
|
||||
"role": ["browse"],
|
||||
"theme": ["browse"]
|
||||
"theme": ["browse"],
|
||||
"email_preview": "read",
|
||||
"email": "read"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -29,6 +29,7 @@ module.exports = {
|
||||
defaultTo: 'public',
|
||||
validations: {isIn: [['public', 'members', 'paid']]}
|
||||
},
|
||||
send_email_when_published: {type: 'bool', nullable: true, defaultTo: false},
|
||||
/**
|
||||
* @deprecated: `author_id`, might be removed in Ghost 3.0
|
||||
* If we keep it, then only, because you can easier query post.author_id than posts_authors[*].sort_order.
|
||||
@ -61,7 +62,8 @@ module.exports = {
|
||||
twitter_title: {type: 'string', maxlength: 300, nullable: true},
|
||||
twitter_description: {type: 'string', maxlength: 500, nullable: true},
|
||||
meta_title: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 300}}},
|
||||
meta_description: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 500}}}
|
||||
meta_description: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 500}}},
|
||||
email_subject: {type: 'string', maxlength: 300, nullable: true}
|
||||
},
|
||||
users: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
@ -150,7 +152,7 @@ module.exports = {
|
||||
maxlength: 50,
|
||||
nullable: false,
|
||||
defaultTo: 'core',
|
||||
validations: {isIn: [['core', 'blog', 'theme', 'app', 'plugin', 'private', 'members']]}
|
||||
validations: {isIn: [['core', 'blog', 'theme', 'app', 'plugin', 'private', 'members', 'bulk_email']]}
|
||||
},
|
||||
created_at: {type: 'dateTime', nullable: false},
|
||||
created_by: {type: 'string', maxlength: 24, nullable: false},
|
||||
@ -321,9 +323,11 @@ module.exports = {
|
||||
},
|
||||
members: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: true, unique: true, validations: {isUUID: true}},
|
||||
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
|
||||
name: {type: 'string', maxlength: 191, nullable: true},
|
||||
note: {type: 'string', maxlength: 2000, nullable: true},
|
||||
subscribed: {type: 'bool', nullable: true, defaultTo: true},
|
||||
created_at: {type: 'dateTime', nullable: false},
|
||||
created_by: {type: 'string', maxlength: 24, nullable: false},
|
||||
updated_at: {type: 'dateTime', nullable: true},
|
||||
@ -372,5 +376,30 @@ module.exports = {
|
||||
// @NOTE: The context object can be used to store information about an action e.g. diffs, meta
|
||||
context: {type: 'text', maxlength: 1000000000, nullable: true},
|
||||
created_at: {type: 'dateTime', nullable: false}
|
||||
},
|
||||
emails: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
post_id: {type: 'string', maxlength: 24, nullable: false, index: true, unique: true},
|
||||
uuid: {type: 'string', maxlength: 36, nullable: false, validations: {isUUID: true}},
|
||||
status: {
|
||||
type: 'string',
|
||||
maxlength: 50,
|
||||
nullable: false,
|
||||
defaultTo: 'pending',
|
||||
validations: {isIn: [['pending', 'submitting', 'submitted', 'failed']]}
|
||||
},
|
||||
error: {type: 'string', maxlength: 2000, nullable: true},
|
||||
error_data: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
|
||||
meta: {type: 'text', maxlength: 65535, nullable: true},
|
||||
stats: {type: 'text', maxlength: 65535, nullable: true},
|
||||
email_count: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0},
|
||||
subject: {type: 'string', maxlength: 300, nullable: true},
|
||||
html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
|
||||
plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
|
||||
submitted_at: {type: 'dateTime', nullable: false},
|
||||
created_at: {type: 'dateTime', nullable: false},
|
||||
created_by: {type: 'string', maxlength: 24, nullable: false},
|
||||
updated_at: {type: 'dateTime', nullable: true},
|
||||
updated_by: {type: 'string', maxlength: 24, nullable: true}
|
||||
}
|
||||
};
|
||||
|
@ -33,6 +33,7 @@ function initialiseServices() {
|
||||
apps = require('./services/apps'),
|
||||
xmlrpc = require('./services/xmlrpc'),
|
||||
slack = require('./services/slack'),
|
||||
{mega} = require('./services/mega'),
|
||||
webhooks = require('./services/webhooks'),
|
||||
scheduling = require('./adapters/scheduling');
|
||||
|
||||
@ -43,6 +44,7 @@ function initialiseServices() {
|
||||
permissions.init(),
|
||||
xmlrpc.listen(),
|
||||
slack.listen(),
|
||||
mega.listen(),
|
||||
webhooks.listen(),
|
||||
apps.init(),
|
||||
scheduling.init({
|
||||
@ -50,12 +52,12 @@ function initialiseServices() {
|
||||
active: config.get('scheduling').active,
|
||||
// NOTE: When changing API version need to consider how to migrate custom scheduling adapters
|
||||
// that rely on URL to lookup persisted scheduled records (jobs, etc.). Ref: https://github.com/TryGhost/Ghost/pull/10726#issuecomment-489557162
|
||||
apiUrl: urlUtils.urlFor('api', {version: 'v2', versionType: 'admin'}, true),
|
||||
apiUrl: urlUtils.urlFor('api', {version: 'v3', versionType: 'admin'}, true),
|
||||
internalPath: config.get('paths').internalSchedulingPath,
|
||||
contentPath: config.getContentPath('scheduling')
|
||||
})
|
||||
).then(function () {
|
||||
debug('XMLRPC, Slack, Webhooks, Apps, Scheduling, Permissions done');
|
||||
debug('XMLRPC, Slack, MEGA, Webhooks, Apps, Scheduling, Permissions done');
|
||||
|
||||
// Initialise analytics events
|
||||
if (config.get('segment:key')) {
|
||||
|
57
core/server/models/email.js
Normal file
57
core/server/models/email.js
Normal file
@ -0,0 +1,57 @@
|
||||
const uuid = require('uuid');
|
||||
const ghostBookshelf = require('./base');
|
||||
|
||||
const Email = ghostBookshelf.Model.extend({
|
||||
tableName: 'emails',
|
||||
|
||||
defaults: function defaults() {
|
||||
return {
|
||||
uuid: uuid.v4(),
|
||||
status: 'pending',
|
||||
stats: JSON.stringify({
|
||||
delivered: 0,
|
||||
failed: 0,
|
||||
opened: 0,
|
||||
clicked: 0,
|
||||
unsubscribed: 0,
|
||||
complaints: 0
|
||||
})
|
||||
};
|
||||
},
|
||||
|
||||
emitChange: function emitChange(event, options) {
|
||||
const eventToTrigger = 'email' + '.' + event;
|
||||
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
|
||||
},
|
||||
|
||||
onCreated: function onCreated(model, attrs, options) {
|
||||
ghostBookshelf.Model.prototype.onCreated.apply(this, arguments);
|
||||
|
||||
model.emitChange('added', options);
|
||||
},
|
||||
|
||||
onUpdated: function onUpdated(model, attrs, options) {
|
||||
ghostBookshelf.Model.prototype.onUpdated.apply(this, arguments);
|
||||
|
||||
model.emitChange('edited', options);
|
||||
},
|
||||
|
||||
onDestroyed: function onDestroyed(model, options) {
|
||||
ghostBookshelf.Model.prototype.onDestroyed.apply(this, arguments);
|
||||
|
||||
model.emitChange('deleted', options);
|
||||
}
|
||||
}, {
|
||||
post() {
|
||||
return this.belongsTo('Post');
|
||||
}
|
||||
});
|
||||
|
||||
const Emails = ghostBookshelf.Collection.extend({
|
||||
model: Email
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
Email: ghostBookshelf.model('Email', Email),
|
||||
Emails: ghostBookshelf.collection('Emails', Emails)
|
||||
};
|
@ -36,7 +36,8 @@ models = [
|
||||
'action',
|
||||
'posts-meta',
|
||||
'member-stripe-customer',
|
||||
'stripe-customer-subscription'
|
||||
'stripe-customer-subscription',
|
||||
'email'
|
||||
];
|
||||
|
||||
function init() {
|
||||
|
@ -1,8 +1,16 @@
|
||||
const ghostBookshelf = require('./base');
|
||||
const uuid = require('uuid');
|
||||
|
||||
const Member = ghostBookshelf.Model.extend({
|
||||
tableName: 'members',
|
||||
|
||||
defaults() {
|
||||
return {
|
||||
subscribed: true,
|
||||
uuid: uuid.v4()
|
||||
};
|
||||
},
|
||||
|
||||
emitChange: function emitChange(event, options) {
|
||||
const eventToTrigger = 'member' + '.' + event;
|
||||
ghostBookshelf.Model.prototype.emitChange.bind(this)(this, eventToTrigger, options);
|
||||
|
@ -144,11 +144,18 @@ pagination = function pagination(bookshelf) {
|
||||
// Get the table name and idAttribute for this model
|
||||
var tableName = _.result(this.constructor.prototype, 'tableName'),
|
||||
idAttribute = _.result(this.constructor.prototype, 'idAttribute'),
|
||||
self = this,
|
||||
self = this;
|
||||
|
||||
let countPromise;
|
||||
if (options.transacting) {
|
||||
countPromise = this.query().clone().transacting(options.transacting).select(
|
||||
bookshelf.knex.raw('count(distinct ' + tableName + '.' + idAttribute + ') as aggregate')
|
||||
);
|
||||
} else {
|
||||
countPromise = this.query().clone().select(
|
||||
bookshelf.knex.raw('count(distinct ' + tableName + '.' + idAttribute + ') as aggregate')
|
||||
);
|
||||
|
||||
}
|
||||
// #### Pre count clauses
|
||||
// Add any where or join clauses which need to be included with the aggregate query
|
||||
|
||||
|
@ -49,6 +49,7 @@ Post = ghostBookshelf.Model.extend({
|
||||
}
|
||||
|
||||
return {
|
||||
send_email_when_published: false,
|
||||
uuid: uuid.v4(),
|
||||
status: 'draft',
|
||||
featured: false,
|
||||
@ -456,6 +457,24 @@ Post = ghostBookshelf.Model.extend({
|
||||
}
|
||||
}
|
||||
|
||||
// send_email_when_published is read-only and should only be set using a query param when publishing/scheduling
|
||||
if (options.send_email_when_published && this.hasChanged('status') && (newStatus === 'published' || newStatus === 'scheduled')) {
|
||||
this.set('send_email_when_published', true);
|
||||
}
|
||||
|
||||
// ensure draft posts have the send_email_when_published reset unless an email has already been sent
|
||||
if (newStatus === 'draft' && this.hasChanged('status')) {
|
||||
ops.push(function ensureSendEmailWhenPublishedIsUnchanged() {
|
||||
return self.related('email').fetch({transacting: options.transacting}).then((email) => {
|
||||
if (email) {
|
||||
self.set('send_email_when_published', true);
|
||||
} else {
|
||||
self.set('send_email_when_published', false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If a title is set, not the same as the old title, a draft post, and has never been published
|
||||
if (prevTitle !== undefined && newTitle !== prevTitle && newStatus === 'draft' && !publishedAt) {
|
||||
ops.push(function updateSlug() {
|
||||
@ -575,6 +594,10 @@ Post = ghostBookshelf.Model.extend({
|
||||
return this.hasOne('PostsMeta', 'post_id');
|
||||
},
|
||||
|
||||
email: function email() {
|
||||
return this.hasOne('Email', 'post_id');
|
||||
},
|
||||
|
||||
/**
|
||||
* @NOTE:
|
||||
* If you are requesting models with `columns`, you try to only receive some fields of the model/s.
|
||||
@ -755,7 +778,7 @@ Post = ghostBookshelf.Model.extend({
|
||||
findPage: ['status'],
|
||||
findAll: ['columns', 'filter'],
|
||||
destroy: ['destroyAll', 'destroyBy'],
|
||||
edit: ['filter']
|
||||
edit: ['filter', 'send_email_when_published']
|
||||
};
|
||||
|
||||
// The post model additionally supports having a formats option
|
||||
|
@ -691,6 +691,33 @@ h2 {
|
||||
.gh-flow-back:hover {
|
||||
border: #dae1e3 1px solid;
|
||||
}
|
||||
.gh-flow-back-plain {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
margin: 0 0 0 3%;
|
||||
padding: 2px 9px 2px 5px;
|
||||
color: #7d878a;
|
||||
font-weight: 300;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
.gh-flow-back-plain svg {
|
||||
margin-right: 4px;
|
||||
height: 12px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.gh-flow-back-plain svg path {
|
||||
stroke: #7d878a;
|
||||
stroke-width: 1.2px;
|
||||
}
|
||||
.gh-flow-back-plain:hover {
|
||||
color: #15212A;
|
||||
}
|
||||
.gh-flow-nav {
|
||||
position: relative;
|
||||
-ms-flex: 1;
|
||||
@ -714,10 +741,21 @@ h2 {
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
.gh-flow-content-unsubscribe {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@media (max-width: 500px) {
|
||||
.gh-flow-head-unsubscribe {
|
||||
padding-top: 2.8vh;
|
||||
}
|
||||
.gh-flow-content {
|
||||
font-size: 4vw;
|
||||
}
|
||||
.gh-flow-content-unsubscribe {
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
}
|
||||
.gh-flow-content header {
|
||||
margin: 0 auto;
|
||||
@ -747,6 +785,9 @@ h2 {
|
||||
line-height: 1.4em;
|
||||
font-weight: 100;
|
||||
}
|
||||
.gh-flow-em {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Sign in
|
||||
/* ---------------------------------------------------------- */
|
||||
|
118
core/server/services/bulk-email/index.js
Normal file
118
core/server/services/bulk-email/index.js
Normal file
@ -0,0 +1,118 @@
|
||||
const _ = require('lodash');
|
||||
const common = require('../../lib/common');
|
||||
const mailgunProvider = require('./mailgun');
|
||||
const configService = require('../../config');
|
||||
const settingsCache = require('../settings/cache');
|
||||
|
||||
/**
|
||||
* An object representing batch request result
|
||||
* @typedef { Object } BatchResultBase
|
||||
* @property { string } data - data that is returned from Mailgun or one which Mailgun was called with
|
||||
*/
|
||||
class BatchResultBase {
|
||||
}
|
||||
|
||||
class SuccessfulBatch extends BatchResultBase {
|
||||
constructor(data) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
class FailedBatch extends BatchResultBase {
|
||||
constructor(error, data) {
|
||||
super();
|
||||
|
||||
// give a better error message for the invalid credentials state
|
||||
if (error.message === 'Forbidden') {
|
||||
error.message = 'Invalid Mailgun credentials';
|
||||
}
|
||||
|
||||
this.error = error;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An email address
|
||||
* @typedef { string } EmailAddress
|
||||
*/
|
||||
|
||||
/**
|
||||
* An object representing an email to send
|
||||
* @typedef { Object } Email
|
||||
* @property { string } html - The html content of the email
|
||||
* @property { string } subject - The subject of the email
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
SuccessfulBatch,
|
||||
FailedBatch,
|
||||
/**
|
||||
* @param {Email} message - The message to send
|
||||
* @param {[EmailAddress]} recipients - the recipients to send the email to
|
||||
* @param {[object]} recipientData - list of data keyed by email to inject into the email
|
||||
* @returns {Promise<Array<BatchResultBase>>} An array of promises representing the success of the batch email sending
|
||||
*/
|
||||
async send(message, recipients, recipientData = {}) {
|
||||
let BATCH_SIZE = 1000;
|
||||
const mailgunInstance = mailgunProvider.getInstance();
|
||||
if (!mailgunInstance) {
|
||||
return;
|
||||
}
|
||||
let fromAddress = message.from;
|
||||
if (/@localhost$/.test(message.from) || /@ghost.local$/.test(message.from)) {
|
||||
fromAddress = 'localhost@example.com';
|
||||
common.logging.warn(`Rewriting bulk email from address ${message.from} to ${fromAddress}`);
|
||||
|
||||
BATCH_SIZE = 2;
|
||||
}
|
||||
|
||||
const blogTitle = settingsCache.get('title');
|
||||
fromAddress = blogTitle ? `${blogTitle}<${fromAddress}>` : fromAddress;
|
||||
|
||||
const chunkedRecipients = _.chunk(recipients, BATCH_SIZE);
|
||||
|
||||
return Promise.mapSeries(chunkedRecipients, (toAddresses) => {
|
||||
const recipientVariables = {};
|
||||
toAddresses.forEach((email) => {
|
||||
recipientVariables[email] = recipientData[email];
|
||||
});
|
||||
|
||||
const batchData = {
|
||||
to: toAddresses,
|
||||
from: fromAddress,
|
||||
'recipient-variables': recipientVariables
|
||||
};
|
||||
|
||||
const bulkEmailConfig = configService.get('bulkEmail');
|
||||
|
||||
if (bulkEmailConfig && bulkEmailConfig.mailgun && bulkEmailConfig.mailgun.tag) {
|
||||
Object.assign(batchData, {
|
||||
'o:tag': [bulkEmailConfig.mailgun.tag, 'bulk-email']
|
||||
});
|
||||
}
|
||||
|
||||
const messageData = Object.assign({}, message, batchData);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
mailgunInstance.messages().send(messageData, (error, body) => {
|
||||
if (error) {
|
||||
// NOTE: logging an error here only but actual handling should happen in more sophisticated batch retry handler
|
||||
// REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
|
||||
common.logging.warn(new common.errors.GhostError({
|
||||
err: error,
|
||||
context: common.i18n.t('errors.services.mega.requestFailed.error')
|
||||
}));
|
||||
|
||||
// NOTE: these are generated variables, so can be regenerated when retry is done
|
||||
const data = _.omit(batchData, ['recipient-variables']);
|
||||
resolve(new FailedBatch(error, data));
|
||||
} else {
|
||||
resolve(new SuccessfulBatch(body));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
41
core/server/services/bulk-email/mailgun.js
Normal file
41
core/server/services/bulk-email/mailgun.js
Normal file
@ -0,0 +1,41 @@
|
||||
const {URL} = require('url');
|
||||
const mailgun = require('mailgun-js');
|
||||
const common = require('../../lib/common');
|
||||
const configService = require('../../config');
|
||||
const settingsCache = require('../settings/cache');
|
||||
|
||||
function createMailgun(config) {
|
||||
const baseUrl = new URL(config.baseUrl);
|
||||
|
||||
return mailgun({
|
||||
apiKey: config.apiKey,
|
||||
domain: config.domain,
|
||||
protocol: baseUrl.protocol,
|
||||
host: baseUrl.host,
|
||||
port: baseUrl.port,
|
||||
endpoint: baseUrl.pathname,
|
||||
retry: 5
|
||||
});
|
||||
}
|
||||
|
||||
function getInstance() {
|
||||
const bulkEmailConfig = configService.get('bulkEmail');
|
||||
const bulkEmailSetting = settingsCache.get('bulk_email_settings');
|
||||
const hasMailgunConfig = !!(bulkEmailConfig && bulkEmailConfig.mailgun);
|
||||
const hasMailgunSetting = !!(bulkEmailSetting && bulkEmailSetting.apiKey && bulkEmailSetting.baseUrl && bulkEmailSetting.domain);
|
||||
if (!hasMailgunConfig && !hasMailgunSetting) {
|
||||
common.logging.warn(`Bulk email service is not configured`);
|
||||
} else {
|
||||
try {
|
||||
let mailgunConfig = hasMailgunConfig ? bulkEmailConfig.mailgun : bulkEmailSetting;
|
||||
return createMailgun(mailgunConfig);
|
||||
} catch (err) {
|
||||
common.logging.warn(`Bulk email service is not configured`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getInstance: getInstance
|
||||
};
|
10
core/server/services/mega/index.js
Normal file
10
core/server/services/mega/index.js
Normal file
@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
get mega() {
|
||||
return require('./mega');
|
||||
},
|
||||
|
||||
get postEmailSerializer() {
|
||||
return require('./post-email-serializer');
|
||||
}
|
||||
};
|
||||
|
231
core/server/services/mega/mega.js
Normal file
231
core/server/services/mega/mega.js
Normal file
@ -0,0 +1,231 @@
|
||||
const _ = require('lodash');
|
||||
const url = require('url');
|
||||
const moment = require('moment');
|
||||
const common = require('../../lib/common');
|
||||
const membersService = require('../members');
|
||||
const bulkEmailService = require('../bulk-email');
|
||||
const models = require('../../models');
|
||||
const postEmailSerializer = require('./post-email-serializer');
|
||||
|
||||
const getEmailData = async (postModel, recipients = []) => {
|
||||
const emailTmpl = 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);
|
||||
}, {});
|
||||
|
||||
return {emailTmpl, emails, emailData};
|
||||
};
|
||||
|
||||
const sendEmail = async (postModel, members) => {
|
||||
const membersToSendTo = members.filter((member) => {
|
||||
return membersService.contentGating.checkPostAccess(postModel.toJSON(), member);
|
||||
});
|
||||
|
||||
const {emailTmpl, emails, emailData} = await getEmailData(postModel, membersToSendTo);
|
||||
|
||||
return bulkEmailService.send(emailTmpl, emails, emailData);
|
||||
};
|
||||
|
||||
const sendTestEmail = async (postModel, toEmails) => {
|
||||
const recipients = toEmails.map((email) => {
|
||||
return {email};
|
||||
});
|
||||
const {emailTmpl, emails, emailData} = await getEmailData(postModel, recipients);
|
||||
emailTmpl.subject = `${emailTmpl.subject} [Test]`;
|
||||
return bulkEmailService.send(emailTmpl, emails, emailData);
|
||||
};
|
||||
|
||||
/**
|
||||
* addEmail
|
||||
*
|
||||
* Accepts a post model and creates an email record based on it. Only creates one
|
||||
* record per post
|
||||
*
|
||||
* @param {object} postModel Post Model Object
|
||||
*/
|
||||
|
||||
const addEmail = async (postModel, options) => {
|
||||
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
||||
|
||||
const {members} = await membersService.api.members.list(Object.assign(knexOptions, {filter: 'subscribed:true'}, {limit: 'all'}));
|
||||
const {emailTmpl, emails} = await getEmailData(postModel, members);
|
||||
|
||||
// NOTE: don't create email object when there's nobody to send the email to
|
||||
if (!emails.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const postId = postModel.get('id');
|
||||
const existing = await models.Email.findOne({post_id: postId}, knexOptions);
|
||||
|
||||
if (!existing) {
|
||||
return models.Email.add({
|
||||
post_id: postId,
|
||||
status: 'pending',
|
||||
email_count: emails.length,
|
||||
subject: emailTmpl.subject,
|
||||
html: emailTmpl.html,
|
||||
plaintext: emailTmpl.plaintext,
|
||||
submitted_at: moment().toDate()
|
||||
}, knexOptions);
|
||||
} else {
|
||||
return existing;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* retryFailedEmail
|
||||
*
|
||||
* Accepts an Email model and resets it's fields to trigger retry listeners
|
||||
*
|
||||
* @param {object} model Email model
|
||||
*/
|
||||
const retryFailedEmail = async (model) => {
|
||||
return await models.Email.edit({
|
||||
status: 'pending'
|
||||
}, {
|
||||
id: model.get('id')
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* handleUnsubscribeRequest
|
||||
*
|
||||
* Takes a request/response pair and reads the `unsubscribe` query parameter,
|
||||
* using the content to update the members service to set the `subscribed` flag
|
||||
* to false on the member
|
||||
*
|
||||
* If any operation fails, or the request is invalid the function will error - so using
|
||||
* as middleware should consider wrapping with `try/catch`
|
||||
*
|
||||
* @param {Request} req
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function handleUnsubscribeRequest(req) {
|
||||
if (!req.url) {
|
||||
throw new common.errors.BadRequestError({
|
||||
message: 'Unsubscribe failed! Could not find member'
|
||||
});
|
||||
}
|
||||
|
||||
const {query} = url.parse(req.url, true);
|
||||
if (!query || !query.uuid) {
|
||||
throw new common.errors.BadRequestError({
|
||||
message: (query.preview ? 'Unsubscribe preview' : 'Unsubscribe failed! Could not find member')
|
||||
});
|
||||
}
|
||||
|
||||
const member = await membersService.api.members.get({
|
||||
uuid: query.uuid
|
||||
});
|
||||
|
||||
if (!member) {
|
||||
throw new common.errors.BadRequestError({
|
||||
message: 'Unsubscribe failed! Could not find member'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return await membersService.api.members.update({subscribed: false}, {id: member.id});
|
||||
} catch (err) {
|
||||
throw new common.errors.InternalServerError({
|
||||
message: 'Failed to unsubscribe member'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function pendingEmailHandler(emailModel, options) {
|
||||
// CASE: do not send email if we import a database
|
||||
// TODO: refactor post.published events to never fire on importing
|
||||
if (options && options.importing) {
|
||||
return;
|
||||
}
|
||||
const postModel = await models.Post.findOne({id: emailModel.get('post_id')}, {withRelated: ['authors']});
|
||||
|
||||
if (emailModel.get('status') !== 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
const {members} = await membersService.api.members.list(Object.assign({filter: 'subscribed:true'}, {limit: 'all'}));
|
||||
|
||||
if (!members.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await models.Email.edit({
|
||||
status: 'submitting'
|
||||
}, {
|
||||
id: emailModel.id
|
||||
});
|
||||
|
||||
let meta = [];
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
// NOTE: meta can contains an array which can be a mix of successful and error responses
|
||||
// needs filtering and saving objects of {error, batchData} form to separate property
|
||||
meta = await sendEmail(postModel, members);
|
||||
} catch (err) {
|
||||
common.logging.error(new common.errors.GhostError({
|
||||
err: err,
|
||||
context: common.i18n.t('errors.services.mega.requestFailed.error')
|
||||
}));
|
||||
error = err.message;
|
||||
}
|
||||
|
||||
const successes = meta.filter(response => (response instanceof bulkEmailService.SuccessfulBatch));
|
||||
const failures = meta.filter(response => (response instanceof bulkEmailService.FailedBatch));
|
||||
const batchStatus = successes.length ? 'submitted' : 'failed';
|
||||
|
||||
if (!error && failures.length) {
|
||||
error = failures[0].error.message;
|
||||
}
|
||||
|
||||
if (error && error.length > 2000) {
|
||||
error = error.substring(0, 2000);
|
||||
}
|
||||
|
||||
try {
|
||||
// CASE: the batch partially succeeded
|
||||
await models.Email.edit({
|
||||
status: batchStatus,
|
||||
meta: JSON.stringify(successes),
|
||||
error: error,
|
||||
error_data: JSON.stringify(failures) // NOTE:need to discuss how we store this
|
||||
}, {
|
||||
id: emailModel.id
|
||||
});
|
||||
} catch (err) {
|
||||
common.logging.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
const statusChangedHandler = (emailModel, options) => {
|
||||
const emailRetried = emailModel.wasChanged() && (emailModel.get('status') === 'pending') && (emailModel.previous('status') === 'failed');
|
||||
|
||||
if (emailRetried) {
|
||||
pendingEmailHandler(emailModel, options);
|
||||
}
|
||||
};
|
||||
|
||||
function listen() {
|
||||
common.events.on('email.added', pendingEmailHandler);
|
||||
common.events.on('email.edited', statusChangedHandler);
|
||||
}
|
||||
|
||||
// Public API
|
||||
module.exports = {
|
||||
listen,
|
||||
addEmail,
|
||||
retryFailedEmail,
|
||||
sendTestEmail,
|
||||
handleUnsubscribeRequest
|
||||
};
|
80
core/server/services/mega/post-email-serializer.js
Normal file
80
core/server/services/mega/post-email-serializer.js
Normal file
@ -0,0 +1,80 @@
|
||||
const juice = require('juice');
|
||||
const template = require('./template');
|
||||
const settingsCache = require('../../services/settings/cache');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
const moment = require('moment');
|
||||
const cheerio = require('cheerio');
|
||||
const api = require('../../api');
|
||||
const {URL} = require('url');
|
||||
|
||||
const getSite = () => {
|
||||
return Object.assign({}, settingsCache.getPublic(), {
|
||||
url: urlUtils.urlFor('home', true)
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* createUnsubscribeUrl
|
||||
*
|
||||
* Takes a member uuid and returns the url that should be used to unsubscribe
|
||||
* In case of no member uuid, generates the preview unsubscribe url - `?preview=1`
|
||||
*
|
||||
* @param {string} uuid
|
||||
*/
|
||||
const createUnsubscribeUrl = (uuid) => {
|
||||
const siteUrl = urlUtils.getSiteUrl();
|
||||
const unsubscribeUrl = new URL(siteUrl);
|
||||
unsubscribeUrl.pathname = `${unsubscribeUrl.pathname}/unsubscribe/`.replace('//', '/');
|
||||
if (uuid) {
|
||||
unsubscribeUrl.searchParams.set('uuid', uuid);
|
||||
} else {
|
||||
unsubscribeUrl.searchParams.set('preview', '1');
|
||||
}
|
||||
|
||||
return unsubscribeUrl.href;
|
||||
};
|
||||
|
||||
// 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'}};
|
||||
const apiVersion = model.get('api_version') || 'v3';
|
||||
const docName = 'posts';
|
||||
|
||||
await api.shared
|
||||
.serializers
|
||||
.handle
|
||||
.output(model, {docName: docName, method: 'read'}, api[apiVersion].serializers.output, frame);
|
||||
|
||||
return frame.response[docName][0];
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
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 {
|
||||
subject: post.email_subject || post.title,
|
||||
html: juicedHtml,
|
||||
plaintext: post.plaintext
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
serialize,
|
||||
createUnsubscribeUrl
|
||||
};
|
876
core/server/services/mega/template.js
Normal file
876
core/server/services/mega/template.js
Normal file
@ -0,0 +1,876 @@
|
||||
/* eslint indent: warn, no-irregular-whitespace: warn */
|
||||
module.exports = ({post, site}) => {
|
||||
const date = new Date();
|
||||
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>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
|
||||
/*All the styling goes here*/
|
||||
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #fff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
color: #15212A;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table td {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
font-size: 18px;
|
||||
vertical-align: top;
|
||||
color: #15212A;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
.body {
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 600px;
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
POST CONTENT
|
||||
------------------------------------- */
|
||||
hr {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 3em 0;
|
||||
padding: 0;
|
||||
height: 1px;
|
||||
border: 0;
|
||||
border-top: 1px solid #e5eff5;
|
||||
}
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
dl,
|
||||
blockquote {
|
||||
margin: 0 0 1.5em 0;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 1.3em;
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ul ol,
|
||||
ol ul {
|
||||
margin: 0.5em 0 1em;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0.5em 0;
|
||||
padding-left: 0.3em;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
dt {
|
||||
float: left;
|
||||
margin: 0 20px 0 0;
|
||||
width: 120px;
|
||||
color: #15212A;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0 0 5px 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 2em 0;
|
||||
padding: 0 25px 0 25px;
|
||||
border-left: #15212A 2px solid;
|
||||
font-style: italic;
|
||||
font-size: 20px;
|
||||
line-height: 1.75em;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
margin: 0.8em 0;
|
||||
font-size: 1.2em;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
blockquote small {
|
||||
display: inline-block;
|
||||
margin: 0.8em 0 0.8em 1.5em;
|
||||
font-size: 0.9em;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
blockquote cite {
|
||||
font-weight: bold;
|
||||
}
|
||||
blockquote cite a {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #15212A;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
line-height: 1.15em;
|
||||
font-weight: 600;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 1.5em 0 0.5em 0;
|
||||
font-size: 42px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 1.5em 0 0.5em 0;
|
||||
font-size: 32px;
|
||||
line-height: 1.22em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 1.5em 0 0.5em 0;
|
||||
font-size: 26px;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1.8em 0 0.5em 0;
|
||||
font-size: 21px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 2em 0 0.5em 0;
|
||||
font-size: 19px;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
margin: 2em 0 0.5em 0;
|
||||
font-size: 19px;
|
||||
line-height: 1.4em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1.5em;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
figcaption {
|
||||
text-align: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
font-size: 14px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
background: #15212A;
|
||||
padding: 15px;
|
||||
border-radius: 3px;
|
||||
line-height: 1.2em;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
p code {
|
||||
background: #F2F7FA;
|
||||
word-break: break-all;
|
||||
padding: 1px 7px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
figure blockquote p {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.site-icon {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.site-icon img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.site-info {
|
||||
padding-top: 50px;
|
||||
border-bottom: 1px solid #e5eff5;
|
||||
}
|
||||
|
||||
.site-url {
|
||||
color: #15212A;
|
||||
font-size: 16px;
|
||||
letter-spacing: -0.1px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
padding-bottom: 10px;
|
||||
font-size: 42px;
|
||||
line-height: 1.1em;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.post-title-link {
|
||||
color: #15212A;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.post-meta,
|
||||
.view-online {
|
||||
padding-bottom: 50px;
|
||||
white-space: nowrap;
|
||||
color: #738a94;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.2px;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.view-online {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.view-online-link {
|
||||
word-wrap: none;
|
||||
white-space: nowrap;
|
||||
color: #15212A;
|
||||
}
|
||||
|
||||
.feature-image {
|
||||
padding-bottom: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
max-width: 600px !important;
|
||||
font-family: Georgia, serif;
|
||||
font-size: 18px;
|
||||
line-height: 1.5em;
|
||||
color: #23323D;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e5eff5;
|
||||
}
|
||||
|
||||
.post-content a {
|
||||
color: #08121A;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.kg-bookmark-card {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.kg-bookmark-card a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.kg-card + .kg-bookmark-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.kg-bookmark-container {
|
||||
display: flex;
|
||||
min-height: 148px;
|
||||
color: #15212A;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
text-decoration: none;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #e5eff5;
|
||||
}
|
||||
|
||||
.kg-bookmark-content {
|
||||
flex-grow: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.kg-bookmark-title {
|
||||
color: #15212A;
|
||||
font-size: 15px;
|
||||
line-height: 1.5em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.kg-bookmark-description {
|
||||
display: -webkit-box;
|
||||
overflow-y: hidden;
|
||||
margin-top: 12px;
|
||||
max-height: 40px;
|
||||
color: #738a94;
|
||||
font-size: 13px;
|
||||
line-height: 1.5em;
|
||||
font-weight: 400;
|
||||
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.kg-bookmark-thumbnail {
|
||||
position: relative;
|
||||
min-width: 140px;
|
||||
max-width: 180px;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.kg-bookmark-thumbnail img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0 3px 3px 0;
|
||||
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.kg-bookmark-metadata {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-top: 14px;
|
||||
color: #15212A;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.kg-bookmark-icon {
|
||||
margin-right: 8px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.kg-bookmark-author {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.kg-bookmark-author:after {
|
||||
content: "•";
|
||||
margin: 0 6px;
|
||||
}
|
||||
|
||||
.kg-bookmark-publisher {
|
||||
overflow: hidden;
|
||||
max-width: 240px;
|
||||
line-height: 1.5em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kg-gallery-container {
|
||||
margin-top: -20px;
|
||||
}
|
||||
|
||||
.kg-gallery-image img {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #ffffff;
|
||||
border-radius: 3px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
color: #738a94;
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
padding-bottom: 40px;
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn>tbody>tr>td {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #3498db;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
padding: 12px 25px;
|
||||
text-decoration: none;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #3498db;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #3498db;
|
||||
border-color: #3498db;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
|
||||
table[class=body] {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
table[class=body] pre {
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-word !important;
|
||||
}
|
||||
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 0 10px !important;
|
||||
}
|
||||
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
table[class=body] .site-icon img {
|
||||
width: 40px !important;
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
table[class=body] .site-url a {
|
||||
font-size: 14px !important;
|
||||
padding-bottom: 15px !important;
|
||||
}
|
||||
|
||||
table[class=body] .post-meta {
|
||||
white-space: normal !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
table[class=body] .view-online-link,
|
||||
table[class=body] .footer,
|
||||
table[class=body] .footer a {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
table[class=body] .post-title a {
|
||||
font-size: 32px !important;
|
||||
line-height: 1.15em !important;
|
||||
}
|
||||
|
||||
table[class=body] .kg-bookmark-card {
|
||||
width: 90vw !important;
|
||||
}
|
||||
|
||||
table[class=body] .kg-bookmark-thumbnail {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
table[class=body] .kg-bookmark-metadata span {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
table[class=body] .kg-embed-card {
|
||||
max-width: 90vw !important;
|
||||
}
|
||||
|
||||
table[class=body] h1 {
|
||||
font-size: 32px !important;
|
||||
line-height: 1.3em !important;
|
||||
}
|
||||
|
||||
table[class=body] h2 {
|
||||
font-size: 26px !important;
|
||||
line-height: 1.22em !important;
|
||||
}
|
||||
|
||||
table[class=body] h3 {
|
||||
font-size: 21px !important;
|
||||
line-height: 1.25em !important;
|
||||
}
|
||||
|
||||
table[class=body] h4 {
|
||||
font-size: 19px !important;
|
||||
line-height: 1.3em !important;
|
||||
}
|
||||
|
||||
table[class=body] h5 {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4em !important;
|
||||
}
|
||||
|
||||
table[class=body] h6 {
|
||||
font-size: 16px !important;
|
||||
line-height: 1.4em !important;
|
||||
}
|
||||
|
||||
table[class=body] blockquote {
|
||||
font-size: 17px !important;
|
||||
line-height: 1.6em !important;
|
||||
margin-bottom: 0 !important;
|
||||
padding-left: 15px !important;
|
||||
}
|
||||
|
||||
table[class=body] blockquote + * {
|
||||
margin-top: 1.5em !important;
|
||||
}
|
||||
|
||||
table[class=body] hr {
|
||||
margin: 2em 0 !important;
|
||||
}
|
||||
|
||||
table[class=body] figcaption,
|
||||
table[class=body] figcaption a {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
#MessageViewBody a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="">
|
||||
<span class="preheader">${ post.excerpt ? post.excerpt : `${post.title} – ` }</span>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" width="100%">
|
||||
<tr>
|
||||
<td> </td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="main" width="100%">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td class="site-info" width="100%" align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
${ site.icon ? `
|
||||
<tr>
|
||||
<td class="site-icon"><a href="${site.url}"><img src="${site.url}${site.icon}" border="0"></a></td>
|
||||
</tr>
|
||||
` : ``}
|
||||
<tr>
|
||||
<td class="site-url"><div style="width: 100% !important;"><a href="${site.url}">${site.title}</a></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="post-title"><a href="${post.url}" class="post-title-link">${post.title}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td class="post-meta">
|
||||
By ${post.authors} –
|
||||
${post.published_at} –
|
||||
<a href="${post.url}" class="view-online-link">View online →</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
${
|
||||
post.feature_image ? `
|
||||
<tr>
|
||||
<td class="feature-image"><img src="${post.feature_image}"></td>
|
||||
</tr>
|
||||
` : ``
|
||||
}
|
||||
<tr>
|
||||
<td class="post-content">
|
||||
|
||||
<!-- POST CONTENT START -->
|
||||
${post.html}
|
||||
<!-- POST CONTENT END -->
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
|
||||
<tr>
|
||||
<td class="wrapper" align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td class="footer">${site.title} © ${date.getFullYear()} – <a href="%recipient.unsubscribe_url%">Unsubscribe</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
};
|
@ -1,15 +1,12 @@
|
||||
const crypto = require('crypto');
|
||||
const {URL} = require('url');
|
||||
const settingsCache = require('../settings/cache');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
const MembersApi = require('@tryghost/members-api');
|
||||
const common = require('../../lib/common');
|
||||
const ghostVersion = require('../../lib/ghost-version');
|
||||
const mail = require('../mail');
|
||||
const models = require('../../models');
|
||||
const signinEmail = require('./emails/signin');
|
||||
const signupEmail = require('./emails/signup');
|
||||
const subscribeEmail = require('./emails/subscribe');
|
||||
const config = require('./config');
|
||||
|
||||
async function createMember({email, name, note}, options = {}) {
|
||||
const model = await models.Member.add({
|
||||
@ -22,7 +19,7 @@ async function createMember({email, name, note}, options = {}) {
|
||||
}
|
||||
|
||||
async function getMember(data, options = {}) {
|
||||
if (!data.email && !data.id) {
|
||||
if (!data.email && !data.id && !data.uuid) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const model = await models.Member.findOne(data, options);
|
||||
@ -75,11 +72,17 @@ async function getMetadata(module, member) {
|
||||
};
|
||||
}
|
||||
|
||||
async function updateMember({name, note}, options = {}) {
|
||||
const model = await models.Member.edit({
|
||||
async function updateMember({name, note, subscribed}, options = {}) {
|
||||
const attrs = {
|
||||
name: name || null,
|
||||
note: note || null
|
||||
}, options);
|
||||
};
|
||||
|
||||
if (subscribed !== undefined) {
|
||||
attrs.subscribed = subscribed;
|
||||
}
|
||||
|
||||
const model = await models.Member.edit(attrs, options);
|
||||
|
||||
const member = model.toJSON(options);
|
||||
return member;
|
||||
@ -105,102 +108,17 @@ function listMembers(options) {
|
||||
});
|
||||
}
|
||||
|
||||
const getApiUrl = ({version, type}) => {
|
||||
const {href} = new URL(
|
||||
urlUtils.getApiPath({version, type}),
|
||||
urlUtils.urlFor('admin', true)
|
||||
);
|
||||
return href;
|
||||
};
|
||||
|
||||
const siteUrl = urlUtils.getSiteUrl();
|
||||
const membersApiUrl = getApiUrl({version: 'v3', type: 'members'});
|
||||
|
||||
const ghostMailer = new mail.GhostMailer();
|
||||
|
||||
function getStripePaymentConfig() {
|
||||
const subscriptionSettings = settingsCache.get('members_subscription_settings');
|
||||
|
||||
const stripePaymentProcessor = subscriptionSettings.paymentProcessors.find(
|
||||
paymentProcessor => paymentProcessor.adapter === 'stripe'
|
||||
);
|
||||
|
||||
if (!stripePaymentProcessor || !stripePaymentProcessor.config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!stripePaymentProcessor.config.public_token || !stripePaymentProcessor.config.secret_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const webhookHandlerUrl = new URL('/members/webhooks/stripe', siteUrl);
|
||||
|
||||
const checkoutSuccessUrl = new URL(siteUrl);
|
||||
checkoutSuccessUrl.searchParams.set('stripe', 'success');
|
||||
const checkoutCancelUrl = new URL(siteUrl);
|
||||
checkoutCancelUrl.searchParams.set('stripe', 'cancel');
|
||||
|
||||
return {
|
||||
publicKey: stripePaymentProcessor.config.public_token,
|
||||
secretKey: stripePaymentProcessor.config.secret_token,
|
||||
checkoutSuccessUrl: checkoutSuccessUrl.href,
|
||||
checkoutCancelUrl: checkoutCancelUrl.href,
|
||||
webhookHandlerUrl: webhookHandlerUrl.href,
|
||||
product: stripePaymentProcessor.config.product,
|
||||
plans: stripePaymentProcessor.config.plans,
|
||||
appInfo: {
|
||||
name: 'Ghost',
|
||||
partner_id: 'pp_partner_DKmRVtTs4j9pwZ',
|
||||
version: ghostVersion.original,
|
||||
url: 'https://ghost.org/'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getAuthSecret() {
|
||||
const hexSecret = settingsCache.get('members_email_auth_secret');
|
||||
if (!hexSecret) {
|
||||
common.logging.warn('Could not find members_email_auth_secret, using dynamically generated secret');
|
||||
return crypto.randomBytes(64);
|
||||
}
|
||||
const secret = Buffer.from(hexSecret, 'hex');
|
||||
if (secret.length < 64) {
|
||||
common.logging.warn('members_email_auth_secret not large enough (64 bytes), using dynamically generated secret');
|
||||
return crypto.randomBytes(64);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
function getAllowSelfSignup() {
|
||||
const subscriptionSettings = settingsCache.get('members_subscription_settings');
|
||||
return subscriptionSettings.allowSelfSignup;
|
||||
}
|
||||
|
||||
// NOTE: the function is an exact duplicate of one in GhostMailer should be extracted
|
||||
// into a common lib once it needs to be reused anywhere else again
|
||||
function getDomain() {
|
||||
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
||||
return domain && domain[1];
|
||||
}
|
||||
|
||||
module.exports = createApiInstance;
|
||||
|
||||
function createApiInstance() {
|
||||
const membersApiInstance = MembersApi({
|
||||
tokenConfig: {
|
||||
issuer: membersApiUrl,
|
||||
publicKey: settingsCache.get('members_public_key'),
|
||||
privateKey: settingsCache.get('members_private_key')
|
||||
},
|
||||
tokenConfig: config.getTokenConfig(),
|
||||
auth: {
|
||||
getSigninURL(token, type) {
|
||||
const signinURL = new URL(siteUrl);
|
||||
signinURL.searchParams.set('token', token);
|
||||
signinURL.searchParams.set('action', type);
|
||||
return signinURL.href;
|
||||
},
|
||||
allowSelfSignup: getAllowSelfSignup(),
|
||||
secret: getAuthSecret()
|
||||
getSigninURL: config.getSigninURL,
|
||||
allowSelfSignup: config.getAllowSelfSignup(),
|
||||
secret: config.getAuthSecret()
|
||||
},
|
||||
mail: {
|
||||
transporter: {
|
||||
@ -209,15 +127,10 @@ function createApiInstance() {
|
||||
common.logging.warn(message.text);
|
||||
}
|
||||
let msg = Object.assign({
|
||||
from: config.getEmailFromAddress(),
|
||||
subject: 'Signin',
|
||||
forceTextContent: true
|
||||
}, message);
|
||||
const subscriptionSettings = settingsCache.get('members_subscription_settings');
|
||||
|
||||
if (subscriptionSettings && subscriptionSettings.fromAddress) {
|
||||
let from = `${subscriptionSettings.fromAddress}@${getDomain()}`;
|
||||
msg = Object.assign({from: from}, msg);
|
||||
}
|
||||
|
||||
return ghostMailer.send(msg);
|
||||
}
|
||||
@ -308,7 +221,7 @@ function createApiInstance() {
|
||||
}
|
||||
},
|
||||
paymentConfig: {
|
||||
stripe: getStripePaymentConfig()
|
||||
stripe: config.getStripePaymentConfig()
|
||||
},
|
||||
setMetadata,
|
||||
getMetadata,
|
||||
|
112
core/server/services/members/config.js
Normal file
112
core/server/services/members/config.js
Normal file
@ -0,0 +1,112 @@
|
||||
const {URL} = require('url');
|
||||
const settingsCache = require('../settings/cache');
|
||||
const ghostVersion = require('../../lib/ghost-version');
|
||||
const crypto = require('crypto');
|
||||
const common = require('../../lib/common');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
|
||||
// NOTE: the function is an exact duplicate of one in GhostMailer should be extracted
|
||||
// into a common lib once it needs to be reused anywhere else again
|
||||
function getDomain() {
|
||||
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
||||
return domain && domain[1];
|
||||
}
|
||||
|
||||
function getEmailFromAddress() {
|
||||
const subscriptionSettings = settingsCache.get('members_subscription_settings') || {};
|
||||
|
||||
return `${subscriptionSettings.fromAddress || 'noreply'}@${getDomain()}`;
|
||||
}
|
||||
|
||||
const getApiUrl = ({version, type}) => {
|
||||
const {href} = new URL(
|
||||
urlUtils.getApiPath({version, type}),
|
||||
urlUtils.urlFor('admin', true)
|
||||
);
|
||||
return href;
|
||||
};
|
||||
|
||||
const siteUrl = urlUtils.getSiteUrl();
|
||||
const membersApiUrl = getApiUrl({version: 'v3', type: 'members'});
|
||||
|
||||
function getStripePaymentConfig() {
|
||||
const subscriptionSettings = settingsCache.get('members_subscription_settings');
|
||||
|
||||
const stripePaymentProcessor = subscriptionSettings.paymentProcessors.find(
|
||||
paymentProcessor => paymentProcessor.adapter === 'stripe'
|
||||
);
|
||||
|
||||
if (!stripePaymentProcessor || !stripePaymentProcessor.config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!stripePaymentProcessor.config.public_token || !stripePaymentProcessor.config.secret_token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const webhookHandlerUrl = new URL('/members/webhooks/stripe', siteUrl);
|
||||
|
||||
const checkoutSuccessUrl = new URL(siteUrl);
|
||||
checkoutSuccessUrl.searchParams.set('stripe', 'success');
|
||||
const checkoutCancelUrl = new URL(siteUrl);
|
||||
checkoutCancelUrl.searchParams.set('stripe', 'cancel');
|
||||
|
||||
return {
|
||||
publicKey: stripePaymentProcessor.config.public_token,
|
||||
secretKey: stripePaymentProcessor.config.secret_token,
|
||||
checkoutSuccessUrl: checkoutSuccessUrl.href,
|
||||
checkoutCancelUrl: checkoutCancelUrl.href,
|
||||
webhookHandlerUrl: webhookHandlerUrl.href,
|
||||
product: stripePaymentProcessor.config.product,
|
||||
plans: stripePaymentProcessor.config.plans,
|
||||
appInfo: {
|
||||
name: 'Ghost',
|
||||
partner_id: 'pp_partner_DKmRVtTs4j9pwZ',
|
||||
version: ghostVersion.original,
|
||||
url: 'https://ghost.org/'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getAuthSecret() {
|
||||
const hexSecret = settingsCache.get('members_email_auth_secret');
|
||||
if (!hexSecret) {
|
||||
common.logging.warn('Could not find members_email_auth_secret, using dynamically generated secret');
|
||||
return crypto.randomBytes(64);
|
||||
}
|
||||
const secret = Buffer.from(hexSecret, 'hex');
|
||||
if (secret.length < 64) {
|
||||
common.logging.warn('members_email_auth_secret not large enough (64 bytes), using dynamically generated secret');
|
||||
return crypto.randomBytes(64);
|
||||
}
|
||||
return secret;
|
||||
}
|
||||
|
||||
function getAllowSelfSignup() {
|
||||
const subscriptionSettings = settingsCache.get('members_subscription_settings');
|
||||
return subscriptionSettings.allowSelfSignup;
|
||||
}
|
||||
|
||||
function getTokenConfig() {
|
||||
return {
|
||||
issuer: membersApiUrl,
|
||||
publicKey: settingsCache.get('members_public_key'),
|
||||
privateKey: settingsCache.get('members_private_key')
|
||||
};
|
||||
}
|
||||
|
||||
function getSigninURL(token, type) {
|
||||
const signinURL = new URL(siteUrl);
|
||||
signinURL.searchParams.set('token', token);
|
||||
signinURL.searchParams.set('action', type);
|
||||
return signinURL.href;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getEmailFromAddress,
|
||||
getStripePaymentConfig,
|
||||
getAllowSelfSignup,
|
||||
getAuthSecret,
|
||||
getTokenConfig,
|
||||
getSigninURL
|
||||
};
|
39
core/server/services/members/content-gating.js
Normal file
39
core/server/services/members/content-gating.js
Normal file
@ -0,0 +1,39 @@
|
||||
// @ts-check
|
||||
/** @typedef { boolean } AccessFlag */
|
||||
|
||||
const PERMIT_ACCESS = true;
|
||||
const BLOCK_ACCESS = false;
|
||||
|
||||
/**
|
||||
* @param {object} post - A post object to check access to
|
||||
* @param {object} member - The member whos access should be checked
|
||||
*
|
||||
* @returns {AccessFlag}
|
||||
*/
|
||||
function checkPostAccess(post, member) {
|
||||
if (post.visibility === 'public') {
|
||||
return PERMIT_ACCESS;
|
||||
}
|
||||
|
||||
if (!member) {
|
||||
return BLOCK_ACCESS;
|
||||
}
|
||||
|
||||
if (post.visibility === 'members') {
|
||||
return PERMIT_ACCESS;
|
||||
}
|
||||
|
||||
const memberHasPlan = member.stripe && member.stripe.subscriptions && member.stripe.subscriptions.length;
|
||||
|
||||
if (post.visibility === 'paid' && memberHasPlan) {
|
||||
return PERMIT_ACCESS;
|
||||
}
|
||||
|
||||
return BLOCK_ACCESS;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkPostAccess,
|
||||
PERMIT_ACCESS,
|
||||
BLOCK_ACCESS
|
||||
};
|
@ -28,6 +28,10 @@ const membersService = {
|
||||
return !!settings && settings.isPaid && settings.paymentProcessors.length !== 0;
|
||||
},
|
||||
|
||||
contentGating: require('./content-gating'),
|
||||
|
||||
config: require('./config'),
|
||||
|
||||
get api() {
|
||||
if (!membersApi) {
|
||||
membersApi = createMembersApiInstance();
|
||||
|
@ -255,6 +255,10 @@
|
||||
"api_key": {
|
||||
"apiKeyNotFound": "API Key not found"
|
||||
},
|
||||
"email": {
|
||||
"emailNotFound": "Email not found.",
|
||||
"retryNotAllowed": "Only failed emails can be retried"
|
||||
},
|
||||
"base": {
|
||||
"index": {
|
||||
"missingContext": "missing context"
|
||||
@ -516,6 +520,11 @@
|
||||
},
|
||||
"loader": "Error trying to load YAML setting for {setting} from '{path}'.",
|
||||
"ensureSettings": "Error trying to access settings files in {path}."
|
||||
},
|
||||
"mega": {
|
||||
"requestFailed": {
|
||||
"error" : "The email service was unable to send an email batch."
|
||||
}
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
|
@ -215,5 +215,13 @@ module.exports = function apiRoutes() {
|
||||
// ## Actions
|
||||
router.get('/actions', mw.authAdminApi, http(apiCanary.actions.browse));
|
||||
|
||||
// ## Email Preview
|
||||
router.get('/email_preview/posts/:id', mw.authAdminApi, http(apiCanary.email_preview.read));
|
||||
router.post('/email_preview/posts/:id', mw.authAdminApi, http(apiCanary.email_preview.sendTestEmail));
|
||||
|
||||
// ## Emails
|
||||
router.get('/emails/:id', mw.authAdminApi, http(apiCanary.emails.read));
|
||||
router.put('/emails/:id/retry', mw.authAdminApi, http(apiCanary.emails.retry));
|
||||
|
||||
return router;
|
||||
};
|
||||
|
@ -55,7 +55,7 @@ describe('DB API', function () {
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse.db);
|
||||
jsonResponse.db.should.have.length(1);
|
||||
Object.keys(jsonResponse.db[0].data).length.should.eql(27);
|
||||
Object.keys(jsonResponse.db[0].data).length.should.eql(28);
|
||||
});
|
||||
});
|
||||
|
||||
|
72
core/test/acceptance/admin/email_preview_spec.js
Normal file
72
core/test/acceptance/admin/email_preview_spec.js
Normal file
@ -0,0 +1,72 @@
|
||||
const should = require('should');
|
||||
const supertest = require('supertest');
|
||||
const ObjectId = require('bson-objectid');
|
||||
const testUtils = require('../../utils');
|
||||
const localUtils = require('./utils');
|
||||
const config = require('../../../server/config');
|
||||
|
||||
const ghost = testUtils.startGhost;
|
||||
|
||||
describe('Email Preview API', function () {
|
||||
let request;
|
||||
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function (_ghostServer) {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request, 'users:extra', 'posts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Read', function () {
|
||||
it('can\'t retrieve for non existent post', function (done) {
|
||||
request.get(localUtils.API.getApiQuery(`posts/${ObjectId.generate()}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
var jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.errors);
|
||||
testUtils.API.checkResponseValue(jsonResponse.errors[0], [
|
||||
'message',
|
||||
'context',
|
||||
'type',
|
||||
'details',
|
||||
'property',
|
||||
'help',
|
||||
'code',
|
||||
'id'
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('can read post email preview with fields', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`email_preview/posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.email_previews);
|
||||
|
||||
localUtils.API.checkResponse(jsonResponse.email_previews[0], 'email_preview', null, null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
47
core/test/acceptance/admin/emails_spec.js
Normal file
47
core/test/acceptance/admin/emails_spec.js
Normal file
@ -0,0 +1,47 @@
|
||||
const should = require('should');
|
||||
const supertest = require('supertest');
|
||||
const testUtils = require('../../utils');
|
||||
const config = require('../../../server/config');
|
||||
const localUtils = require('./utils');
|
||||
|
||||
const ghost = testUtils.startGhost;
|
||||
|
||||
describe('Email API', function () {
|
||||
let request;
|
||||
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function () {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request, 'posts', 'emails');
|
||||
});
|
||||
});
|
||||
|
||||
it('Can read an email', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`emails/${testUtils.DataGenerator.Content.emails[0].id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.emails);
|
||||
jsonResponse.emails.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.emails[0], 'email');
|
||||
|
||||
const stats = JSON.parse(jsonResponse.emails[0].stats);
|
||||
|
||||
should.exist(stats.delivered);
|
||||
should.exist(stats.failed);
|
||||
should.exist(stats.opened);
|
||||
should.exist(stats.clicked);
|
||||
should.exist(stats.unsubscribed);
|
||||
should.exist(stats.complaints);
|
||||
});
|
||||
});
|
||||
});
|
@ -22,7 +22,7 @@ describe('Posts API', function () {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request, 'users:extra', 'posts');
|
||||
return localUtils.doAuth(request, 'users:extra', 'posts', 'emails');
|
||||
})
|
||||
.then(function (cookie) {
|
||||
ownerCookie = cookie;
|
||||
@ -95,7 +95,7 @@ describe('Posts API', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('Can includes single relation', function (done) {
|
||||
it('Can include single relation', function (done) {
|
||||
request.get(localUtils.API.getApiQuery('posts/?include=tags'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
@ -115,7 +115,7 @@ describe('Posts API', function () {
|
||||
jsonResponse.posts[0],
|
||||
'post',
|
||||
null,
|
||||
['authors', 'primary_author']
|
||||
['authors', 'primary_author', 'email']
|
||||
);
|
||||
|
||||
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
||||
@ -232,7 +232,7 @@ describe('Posts API', function () {
|
||||
|
||||
it('Can include relations for a single post', function (done) {
|
||||
request
|
||||
.get(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/?include=authors,tags'))
|
||||
.get(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/?include=authors,tags,email'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
@ -254,6 +254,9 @@ describe('Posts API', function () {
|
||||
|
||||
jsonResponse.posts[0].tags[0].should.be.an.Object();
|
||||
localUtils.API.checkResponse(jsonResponse.posts[0].tags[0], 'tag', ['url']);
|
||||
|
||||
jsonResponse.posts[0].email.should.be.an.Object();
|
||||
localUtils.API.checkResponse(jsonResponse.posts[0].email, 'email');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
@ -37,7 +37,7 @@ const expectedProperties = {
|
||||
// always returns computed properties
|
||||
.concat('url', 'primary_tag', 'primary_author', 'excerpt')
|
||||
// returned by default
|
||||
.concat('tags', 'authors')
|
||||
.concat('tags', 'authors', 'email')
|
||||
// returns meta fields from `posts_meta` schema
|
||||
.concat(
|
||||
..._(schema.posts_meta).keys().without('post_id', 'id')
|
||||
@ -54,13 +54,18 @@ const expectedProperties = {
|
||||
.without('type')
|
||||
// deprecated
|
||||
.without('author_id', 'author')
|
||||
// pages are not sent as emails
|
||||
.without('send_email_when_published')
|
||||
// always returns computed properties
|
||||
.concat('url', 'primary_tag', 'primary_author', 'excerpt')
|
||||
// returned by default
|
||||
.concat('tags', 'authors')
|
||||
// returns meta fields from `posts_meta` schema
|
||||
.concat(
|
||||
..._(schema.posts_meta).keys().without('post_id', 'id')
|
||||
..._(schema.posts_meta).keys()
|
||||
.without('post_id', 'id')
|
||||
// pages are not sent as emails
|
||||
.without('email_subject')
|
||||
)
|
||||
,
|
||||
|
||||
@ -96,6 +101,10 @@ const expectedProperties = {
|
||||
,
|
||||
webhook: _(schema.webhooks)
|
||||
.keys()
|
||||
,
|
||||
email: _(schema.emails)
|
||||
.keys(),
|
||||
email_preview: ['html', 'subject', 'plaintext']
|
||||
};
|
||||
|
||||
_.each(expectedProperties, (value, key) => {
|
||||
|
@ -20,7 +20,7 @@ describe('Posts API', function () {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request, 'users:extra', 'posts');
|
||||
return localUtils.doAuth(request, 'users:extra', 'posts', 'emails');
|
||||
})
|
||||
.then(function (cookie) {
|
||||
ownerCookie = cookie;
|
||||
@ -60,7 +60,7 @@ describe('Posts API', function () {
|
||||
});
|
||||
|
||||
it('combined fields, formats, include and non existing', function (done) {
|
||||
request.get(localUtils.API.getApiQuery('posts/?formats=mobiledoc,html,plaintext&fields=id,title,primary_tag,doesnotexist&include=authors,tags'))
|
||||
request.get(localUtils.API.getApiQuery('posts/?formats=mobiledoc,html,plaintext&fields=id,title,primary_tag,doesnotexist&include=authors,tags,email'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
@ -81,7 +81,7 @@ describe('Posts API', function () {
|
||||
'post',
|
||||
null,
|
||||
null,
|
||||
['mobiledoc', 'plaintext', 'id', 'title', 'html', 'authors', 'tags', 'primary_tag']
|
||||
['mobiledoc', 'plaintext', 'id', 'title', 'html', 'authors', 'tags', 'primary_tag', 'email']
|
||||
);
|
||||
|
||||
localUtils.API.checkResponse(jsonResponse.meta.pagination, 'pagination');
|
||||
|
@ -70,6 +70,8 @@ const expectedProperties = {
|
||||
,
|
||||
webhook: _(schema.webhooks)
|
||||
.keys()
|
||||
,
|
||||
email_preview: ['html', 'subject', 'plaintext']
|
||||
};
|
||||
|
||||
_.each(expectedProperties, (value, key) => {
|
||||
|
@ -246,7 +246,7 @@ describe('Database Migration (special functions)', function () {
|
||||
result.roles.at(7).get('name').should.eql('Scheduler Integration');
|
||||
|
||||
// Permissions
|
||||
result.permissions.length.should.eql(62);
|
||||
result.permissions.length.should.eql(65);
|
||||
result.permissions.toJSON().should.be.CompletePermissions();
|
||||
});
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ describe('Unit: canary/utils/serializers/output/pages', function () {
|
||||
return Object.assign(data, {toJSON: sinon.stub().returns(data)});
|
||||
};
|
||||
|
||||
sinon.stub(mapper, 'mapPost').returns({});
|
||||
sinon.stub(mapper, 'mapPage').returns({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -46,7 +46,7 @@ describe('Unit: canary/utils/serializers/output/pages', function () {
|
||||
|
||||
serializers.output.pages.all(ctrlResponse, apiConfig, frame);
|
||||
|
||||
mapper.mapPost.callCount.should.equal(2);
|
||||
mapper.mapPost.getCall(0).args.should.eql([ctrlResponse.data[0], frame]);
|
||||
mapper.mapPage.callCount.should.equal(2);
|
||||
mapper.mapPage.getCall(0).args.should.eql([ctrlResponse.data[0], frame]);
|
||||
});
|
||||
});
|
||||
|
@ -38,7 +38,7 @@ describe('Unit: canary/utils/serializers/output/posts', function () {
|
||||
meta: {}
|
||||
};
|
||||
|
||||
serializers.output.pages.all(ctrlResponse, apiConfig, frame);
|
||||
serializers.output.posts.all(ctrlResponse, apiConfig, frame);
|
||||
|
||||
mapper.mapPost.callCount.should.equal(2);
|
||||
mapper.mapPost.getCall(0).args.should.eql([ctrlResponse.data[0], frame]);
|
||||
|
@ -12,7 +12,7 @@ describe('Unit: v3/utils/serializers/output/pages', function () {
|
||||
return Object.assign(data, {toJSON: sinon.stub().returns(data)});
|
||||
};
|
||||
|
||||
sinon.stub(mapper, 'mapPost').returns({});
|
||||
sinon.stub(mapper, 'mapPage').returns({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -34,11 +34,11 @@ describe('Unit: v3/utils/serializers/output/pages', function () {
|
||||
data: [
|
||||
pageModel(testUtils.DataGenerator.forKnex.createPost({
|
||||
id: 'id1',
|
||||
page: true
|
||||
type: 'page'
|
||||
})),
|
||||
pageModel(testUtils.DataGenerator.forKnex.createPost({
|
||||
id: 'id2',
|
||||
page: true
|
||||
type: 'page'
|
||||
}))
|
||||
],
|
||||
meta: {}
|
||||
@ -46,7 +46,7 @@ describe('Unit: v3/utils/serializers/output/pages', function () {
|
||||
|
||||
serializers.output.pages.all(ctrlResponse, apiConfig, frame);
|
||||
|
||||
mapper.mapPost.callCount.should.equal(2);
|
||||
mapper.mapPost.getCall(0).args.should.eql([ctrlResponse.data[0], frame]);
|
||||
mapper.mapPage.callCount.should.equal(2);
|
||||
mapper.mapPage.getCall(0).args.should.eql([ctrlResponse.data[0], frame]);
|
||||
});
|
||||
});
|
||||
|
@ -38,7 +38,7 @@ describe('Unit: v3/utils/serializers/output/posts', function () {
|
||||
meta: {}
|
||||
};
|
||||
|
||||
serializers.output.pages.all(ctrlResponse, apiConfig, frame);
|
||||
serializers.output.posts.all(ctrlResponse, apiConfig, frame);
|
||||
|
||||
mapper.mapPost.callCount.should.equal(2);
|
||||
mapper.mapPost.getCall(0).args.should.eql([ctrlResponse.data[0], frame]);
|
||||
|
@ -150,19 +150,19 @@ describe('Migration Fixture Utils', function () {
|
||||
fixtureUtils.addFixturesForRelation(fixtures.relations[0]).then(function (result) {
|
||||
should.exist(result);
|
||||
result.should.be.an.Object();
|
||||
result.should.have.property('expected', 56);
|
||||
result.should.have.property('done', 56);
|
||||
result.should.have.property('expected', 66);
|
||||
result.should.have.property('done', 66);
|
||||
|
||||
// Permissions & Roles
|
||||
permsAllStub.calledOnce.should.be.true();
|
||||
rolesAllStub.calledOnce.should.be.true();
|
||||
dataMethodStub.filter.callCount.should.eql(56);
|
||||
dataMethodStub.filter.callCount.should.eql(66);
|
||||
dataMethodStub.find.callCount.should.eql(7);
|
||||
baseUtilAttachStub.callCount.should.eql(56);
|
||||
baseUtilAttachStub.callCount.should.eql(66);
|
||||
|
||||
fromItem.related.callCount.should.eql(56);
|
||||
fromItem.findWhere.callCount.should.eql(56);
|
||||
toItem[0].get.callCount.should.eql(112);
|
||||
fromItem.related.callCount.should.eql(66);
|
||||
fromItem.findWhere.callCount.should.eql(66);
|
||||
toItem[0].get.callCount.should.eql(132);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
|
@ -19,8 +19,8 @@ var should = require('should'),
|
||||
*/
|
||||
describe('DB version integrity', function () {
|
||||
// Only these variables should need updating
|
||||
const currentSchemaHash = '09ab28b8a18dc7f7f4fb5dbd5ccc10ac';
|
||||
const currentFixturesHash = '59714063fa30b2f3fbfc929551b3c1e5';
|
||||
const currentSchemaHash = '773f8f6cd4267f50aec6af8c8b1edbd2';
|
||||
const currentFixturesHash = '1a0f96fa1d8b976d663eb06719be031c';
|
||||
|
||||
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
|
||||
// and the values above will need updating as confirmation
|
||||
|
@ -113,7 +113,7 @@ describe('Unit: models/settings', function () {
|
||||
|
||||
return models.Settings.populateDefaults()
|
||||
.then(() => {
|
||||
eventSpy.callCount.should.equal(82);
|
||||
eventSpy.callCount.should.equal(84);
|
||||
const eventsEmitted = eventSpy.args.map(args => args[0]);
|
||||
const checkEventEmitted = event => should.ok(eventsEmitted.includes(event), `${event} event should be emitted`);
|
||||
|
||||
@ -135,7 +135,7 @@ describe('Unit: models/settings', function () {
|
||||
|
||||
return models.Settings.populateDefaults()
|
||||
.then(() => {
|
||||
eventSpy.callCount.should.equal(80);
|
||||
eventSpy.callCount.should.equal(82);
|
||||
|
||||
eventSpy.args[13][0].should.equal('settings.logo.added');
|
||||
});
|
||||
|
@ -418,6 +418,31 @@ DataGenerator.Content = {
|
||||
type: 'admin',
|
||||
integration_id: undefined // "internal"
|
||||
}
|
||||
],
|
||||
|
||||
emails: [
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
uuid: '6b6afda6-4b5e-4893-bff6-f16859e8349a',
|
||||
status: 'submitted',
|
||||
email_count: 2,
|
||||
subject: 'You got mailed!',
|
||||
html: '<p>Look! I\'m an email</p>',
|
||||
plaintext: 'Waba-daba-dab-da',
|
||||
submitted_at: moment().toDate()
|
||||
},
|
||||
{
|
||||
id: ObjectId.generate(),
|
||||
uuid: '365daa11-4bf0-4614-ad43-6346387ffa00',
|
||||
status: 'failed',
|
||||
error: 'Everything went south',
|
||||
stats: '',
|
||||
email_count: 3,
|
||||
subject: 'You got mailed! Again!',
|
||||
html: '<p>What\'s that? Another email!</p>',
|
||||
plaintext: 'yes this is an email',
|
||||
submitted_at: moment().toDate()
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@ -425,6 +450,8 @@ DataGenerator.Content = {
|
||||
DataGenerator.Content.subscribers[0].post_id = DataGenerator.Content.posts[0].id;
|
||||
DataGenerator.Content.api_keys[0].integration_id = DataGenerator.Content.integrations[0].id;
|
||||
DataGenerator.Content.api_keys[1].integration_id = DataGenerator.Content.integrations[0].id;
|
||||
DataGenerator.Content.emails[0].post_id = DataGenerator.Content.posts[0].id;
|
||||
DataGenerator.Content.emails[1].post_id = DataGenerator.Content.posts[1].id;
|
||||
|
||||
DataGenerator.forKnex = (function () {
|
||||
function createBasic(overrides) {
|
||||
@ -904,6 +931,11 @@ DataGenerator.forKnex = (function () {
|
||||
createBasic(DataGenerator.Content.api_keys[2])
|
||||
];
|
||||
|
||||
const emails = [
|
||||
createBasic(DataGenerator.Content.emails[0]),
|
||||
createBasic(DataGenerator.Content.emails[1])
|
||||
];
|
||||
|
||||
return {
|
||||
createPost: createPost,
|
||||
createGenericPost: createGenericPost,
|
||||
@ -940,7 +972,8 @@ DataGenerator.forKnex = (function () {
|
||||
roles_users: roles_users,
|
||||
webhooks: webhooks,
|
||||
integrations: integrations,
|
||||
api_keys: api_keys
|
||||
api_keys: api_keys,
|
||||
emails: emails
|
||||
};
|
||||
}());
|
||||
|
||||
|
@ -464,6 +464,12 @@ fixtures = {
|
||||
return Promise.map(DataGenerator.forKnex.api_keys, function (api_key) {
|
||||
return models.ApiKey.add(api_key, module.exports.context.internal);
|
||||
});
|
||||
},
|
||||
|
||||
insertEmails: function insertEmails() {
|
||||
return Promise.map(DataGenerator.forKnex.emails, function (email) {
|
||||
return models.Email.add(email, module.exports.context.internal);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -613,6 +619,9 @@ toDoList = {
|
||||
},
|
||||
api_keys: function insertApiKeys() {
|
||||
return fixtures.insertApiKeys();
|
||||
},
|
||||
emails: function insertEmails() {
|
||||
return fixtures.insertEmails();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -41,7 +41,7 @@
|
||||
"dependencies": {
|
||||
"@nexes/nql": "0.3.0",
|
||||
"@tryghost/helpers": "1.1.19",
|
||||
"@tryghost/members-api": "0.8.3",
|
||||
"@tryghost/members-api": "0.9.0",
|
||||
"@tryghost/members-ssr": "0.7.3",
|
||||
"@tryghost/social-urls": "0.1.4",
|
||||
"@tryghost/string": "^0.1.3",
|
||||
@ -88,10 +88,12 @@
|
||||
"js-yaml": "3.13.1",
|
||||
"jsonpath": "1.0.2",
|
||||
"jsonwebtoken": "8.5.1",
|
||||
"juice": "5.2.0",
|
||||
"keypair": "1.0.1",
|
||||
"knex": "0.19.5",
|
||||
"knex-migrator": "3.4.0",
|
||||
"lodash": "4.17.15",
|
||||
"mailgun-js": "0.22.0",
|
||||
"markdown-it": "10.0.0",
|
||||
"markdown-it-footnote": "3.0.2",
|
||||
"markdown-it-lazy-headers": "0.1.3",
|
||||
|
354
yarn.lock
354
yarn.lock
@ -227,22 +227,22 @@
|
||||
dependencies:
|
||||
"@tryghost/kg-clean-basic-html" "^0.1.4"
|
||||
|
||||
"@tryghost/magic-link@^0.3.1":
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-0.3.1.tgz#a06fafb6a6e526a8658cef64c194df5f57ce4436"
|
||||
integrity sha512-jwTVp79ElFKhZm5T5Yn8fzXSWMwzCnMaW2FMEPKgLZnRKDLqHBMqcDkhJDoZ0ilgK7B4wNf1qDRxoMGa+boHoA==
|
||||
"@tryghost/magic-link@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/magic-link/-/magic-link-0.3.2.tgz#dba8b1854d88d86b287be1e91be49d15a27e60b6"
|
||||
integrity sha512-r5UTsHco7cQ17JvUdcqS1afs04D6ZxdUMSSiKcg2rRqx/POA/sFwsahm5Neek93goB03pPQoAsS7vQLPifsvdw==
|
||||
dependencies:
|
||||
bluebird "^3.5.5"
|
||||
ghost-ignition "^3.1.0"
|
||||
jsonwebtoken "^8.5.1"
|
||||
lodash "^4.17.15"
|
||||
|
||||
"@tryghost/members-api@0.8.3":
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-0.8.3.tgz#83b829a7567827f5dc00dc2f93c6e412c405ef8e"
|
||||
integrity sha512-RDIw12MxQn/MUFnJ6OOjxHgerxVHkCNTEtLtj8SaLrVJ+lSw88NVUvDCzKFsC80Pm0bLJjwj81bspH9uftud5A==
|
||||
"@tryghost/members-api@0.9.0":
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@tryghost/members-api/-/members-api-0.9.0.tgz#8fc227b9b6f536093f67a68d47687830de2c1748"
|
||||
integrity sha512-8nfaI2CAi+B3DToLrxUwSYVaBX/gX+mLnBJW3cIml/sBXokno4Sr08HX7OpTnTNXLroLHa/+QYivhtMRAOxjQQ==
|
||||
dependencies:
|
||||
"@tryghost/magic-link" "^0.3.1"
|
||||
"@tryghost/magic-link" "^0.3.2"
|
||||
bluebird "^3.5.4"
|
||||
body-parser "^1.19.0"
|
||||
cookies "^0.8.0"
|
||||
@ -419,6 +419,20 @@ addressparser@~0.3.2:
|
||||
resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-0.3.2.tgz#59873f35e8fcf6c7361c10239261d76e15348bb2"
|
||||
integrity sha1-WYc/Nej89sc2HBAjkmHXbhU0i7I=
|
||||
|
||||
agent-base@4, agent-base@^4.2.0, agent-base@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
|
||||
integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==
|
||||
dependencies:
|
||||
es6-promisify "^5.0.0"
|
||||
|
||||
agent-base@~4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
|
||||
integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
|
||||
dependencies:
|
||||
es6-promisify "^5.0.0"
|
||||
|
||||
ajv-keywords@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.1.tgz#617997fc5f60576894c435f940d819e135b80762"
|
||||
@ -703,6 +717,11 @@ assign-symbols@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
|
||||
integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=
|
||||
|
||||
ast-types@0.x.x:
|
||||
version "0.13.2"
|
||||
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48"
|
||||
integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA==
|
||||
|
||||
astral-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
|
||||
@ -725,7 +744,7 @@ async@^2.0.0, async@^2.1.2, async@^2.6.0, async@^2.6.1, async@^2.6.3:
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
async@^3.0.1:
|
||||
async@^3.0.1, async@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.1.0.tgz#42b3b12ae1b74927b5217d8c0016baaf62463772"
|
||||
integrity sha512-4vx/aaY6j/j3Lw3fbCHNWP0pPaTCew3F6F3hYyl/tHs/ndmV1q7NW9T5yuJ2XAGwdQrP+6Wu20x06U4APo/iQQ==
|
||||
@ -1281,7 +1300,7 @@ cheerio-advanced-selectors@~2.0.1:
|
||||
resolved "https://registry.yarnpkg.com/cheerio-advanced-selectors/-/cheerio-advanced-selectors-2.0.1.tgz#fb5ec70a4599e8cec1cf669c6d9b90a3fa969c48"
|
||||
integrity sha512-5wHR8bpiD5pdUtaS81A6hnJezzoDzL1TLWfK6bxnLkIgEKPV26BlOdMCcvuj3fTE7JSalsTUeNU7AOD/u6bYhw==
|
||||
|
||||
cheerio@0.22.0:
|
||||
cheerio@0.22.0, cheerio@^0.22.0:
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
|
||||
integrity sha1-qbqoYKP5tZWmuBsahocxIe06Jp4=
|
||||
@ -1539,6 +1558,11 @@ commander@2.20.0:
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422"
|
||||
integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==
|
||||
|
||||
commander@^2.15.1:
|
||||
version "2.20.3"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
||||
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
|
||||
|
||||
commander@^2.19.0, commander@^2.20.0, commander@^2.9.0:
|
||||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.1.tgz#3863ce3ca92d0831dcf2a102f5fb4b5926afd0f9"
|
||||
@ -1937,6 +1961,11 @@ dashdash@^1.12.0:
|
||||
dependencies:
|
||||
assert-plus "^1.0.0"
|
||||
|
||||
data-uri-to-buffer@1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835"
|
||||
integrity sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==
|
||||
|
||||
data-urls@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"
|
||||
@ -1946,6 +1975,14 @@ data-urls@^1.1.0:
|
||||
whatwg-mimetype "^2.2.0"
|
||||
whatwg-url "^7.0.0"
|
||||
|
||||
datauri@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/datauri/-/datauri-2.0.0.tgz#ff0ee23729935a6bcc81f301621bed3e692bf3c7"
|
||||
integrity sha512-zS2HSf9pI5XPlNZgIqJg/wCJpecgU/HA6E/uv2EfaWnW1EiTGLfy/EexTIsC9c99yoCOTXlqeeWk4FkCSuO3/g==
|
||||
dependencies:
|
||||
image-size "^0.7.3"
|
||||
mimer "^1.0.0"
|
||||
|
||||
dateformat@~1.0.12:
|
||||
version "1.0.12"
|
||||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9"
|
||||
@ -1975,7 +2012,7 @@ debug@3.2.6, debug@^3.0.0, debug@^3.1.0, debug@^3.2.6:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@4.1.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
|
||||
debug@4, debug@4.1.1, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
|
||||
integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
|
||||
@ -2075,6 +2112,15 @@ defined@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
|
||||
integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=
|
||||
|
||||
degenerator@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095"
|
||||
integrity sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU=
|
||||
dependencies:
|
||||
ast-types "0.x.x"
|
||||
escodegen "1.x.x"
|
||||
esprima "3.x.x"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
@ -2373,11 +2419,18 @@ es-to-primitive@^1.2.0:
|
||||
is-date-object "^1.0.1"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
es6-promise@^4.2.6:
|
||||
es6-promise@^4.0.3, es6-promise@^4.2.6:
|
||||
version "4.2.8"
|
||||
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
|
||||
integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==
|
||||
|
||||
es6-promisify@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
|
||||
integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
|
||||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
@ -2388,7 +2441,7 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
escodegen@^1.11.0, escodegen@^1.8.1:
|
||||
escodegen@1.x.x, escodegen@^1.11.0, escodegen@^1.8.1:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541"
|
||||
integrity sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==
|
||||
@ -2628,16 +2681,16 @@ esprima@1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b"
|
||||
integrity sha1-dqD9Zvz+FU/SkmZ9wmQBl1CxZXs=
|
||||
|
||||
esprima@3.x.x, esprima@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
|
||||
integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
|
||||
|
||||
esprima@^2.6.0:
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
|
||||
integrity sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=
|
||||
|
||||
esprima@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633"
|
||||
integrity sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=
|
||||
|
||||
esprima@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
@ -2977,6 +3030,11 @@ file-sync-cmp@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz#a5e7a8ffbfa493b43b923bbd4ca89a53b63b612b"
|
||||
integrity sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=
|
||||
|
||||
file-uri-to-path@1:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==
|
||||
|
||||
fill-keys@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/fill-keys/-/fill-keys-1.0.2.tgz#9a8fa36f4e8ad634e3bf6b4f3c8882551452eb20"
|
||||
@ -3145,7 +3203,7 @@ forever-agent@~0.6.1:
|
||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
||||
|
||||
form-data@^2.3.1:
|
||||
form-data@^2.3.1, form-data@^2.3.3:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4"
|
||||
integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==
|
||||
@ -3227,6 +3285,14 @@ fs.realpath@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
|
||||
|
||||
ftp@~0.3.10:
|
||||
version "0.3.10"
|
||||
resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d"
|
||||
integrity sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0=
|
||||
dependencies:
|
||||
readable-stream "1.1.x"
|
||||
xregexp "2.0.0"
|
||||
|
||||
function-bind@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
|
||||
@ -3311,6 +3377,18 @@ get-stream@^5.1.0:
|
||||
dependencies:
|
||||
pump "^3.0.0"
|
||||
|
||||
get-uri@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.4.tgz#d4937ab819e218d4cb5ae18e4f5962bef169cc6a"
|
||||
integrity sha512-v7LT/s8kVjs+Tx0ykk1I+H/rbpzkHvuIq87LmeXptcf5sNWm9uQiwjNAt94SJPA1zOlCntmnOlJvVWKmzsxG8Q==
|
||||
dependencies:
|
||||
data-uri-to-buffer "1"
|
||||
debug "2"
|
||||
extend "~3.0.2"
|
||||
file-uri-to-path "1"
|
||||
ftp "~0.3.10"
|
||||
readable-stream "2"
|
||||
|
||||
get-value@^2.0.3, get-value@^2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
|
||||
@ -3984,15 +4062,7 @@ http-errors@1.7.2:
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
|
||||
http-errors@~1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.4.0.tgz#6c0242dea6b3df7afda153c71089b31c6e82aabf"
|
||||
integrity sha1-bAJC3qaz33r9oVPHEImzHG6Cqr8=
|
||||
dependencies:
|
||||
inherits "2.0.1"
|
||||
statuses ">= 1.2.1 < 2"
|
||||
|
||||
http-errors@~1.7.2:
|
||||
http-errors@1.7.3, http-errors@~1.7.2:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06"
|
||||
integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
|
||||
@ -4003,11 +4073,27 @@ http-errors@~1.7.2:
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.0"
|
||||
|
||||
http-errors@~1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.4.0.tgz#6c0242dea6b3df7afda153c71089b31c6e82aabf"
|
||||
integrity sha1-bAJC3qaz33r9oVPHEImzHG6Cqr8=
|
||||
dependencies:
|
||||
inherits "2.0.1"
|
||||
statuses ">= 1.2.1 < 2"
|
||||
|
||||
"http-parser-js@>=0.4.0 <0.4.11":
|
||||
version "0.4.10"
|
||||
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4"
|
||||
integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=
|
||||
|
||||
http-proxy-agent@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405"
|
||||
integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==
|
||||
dependencies:
|
||||
agent-base "4"
|
||||
debug "3.1.0"
|
||||
|
||||
http-signature@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||
@ -4017,6 +4103,14 @@ http-signature@~1.2.0:
|
||||
jsprim "^1.2.2"
|
||||
sshpk "^1.7.0"
|
||||
|
||||
https-proxy-agent@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz#b8c286433e87602311b01c8ea34413d856a4af81"
|
||||
integrity sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==
|
||||
dependencies:
|
||||
agent-base "^4.3.0"
|
||||
debug "^3.1.0"
|
||||
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.17, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
@ -4069,7 +4163,7 @@ image-size@0.8.3:
|
||||
dependencies:
|
||||
queue "6.0.1"
|
||||
|
||||
image-size@^0.7.4:
|
||||
image-size@^0.7.3, image-size@^0.7.4:
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.7.5.tgz#269f357cf5797cb44683dfa99790e54c705ead04"
|
||||
integrity sha512-Hiyv+mXHfFEP7LzUL/llg9RwFxxY+o9N3JVLIeG5E7iFIFAalxvRU9UZthBdYDEVnzHMgjnKJPPpay5BWf1g9g==
|
||||
@ -4104,11 +4198,16 @@ indexes-of@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
|
||||
integrity sha1-8w9xbI4r00bHtn0985FVZqfAVgc=
|
||||
|
||||
inflection@^1.5.1:
|
||||
inflection@^1.5.1, inflection@~1.12.0:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.12.0.tgz#a200935656d6f5f6bc4dc7502e1aecb703228416"
|
||||
integrity sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=
|
||||
|
||||
inflection@~1.3.0:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/inflection/-/inflection-1.3.8.tgz#cbd160da9f75b14c3cc63578d4f396784bf3014e"
|
||||
integrity sha1-y9Fg2p91sUw8xjV41POWeEvzAU4=
|
||||
|
||||
inflight@^1.0.4:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
|
||||
@ -4219,6 +4318,11 @@ ip-regex@^4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.1.0.tgz#5ad62f685a14edb421abebc2fff8db94df67b455"
|
||||
integrity sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==
|
||||
|
||||
ip@1.1.5, ip@^1.1.5:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
|
||||
integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
|
||||
|
||||
ipaddr.js@1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
|
||||
@ -4744,6 +4848,19 @@ jsprim@^1.2.2:
|
||||
json-schema "0.2.3"
|
||||
verror "1.10.0"
|
||||
|
||||
juice@5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/juice/-/juice-5.2.0.tgz#a40ea144bde2845fe2aade46a81f493f8ea677a0"
|
||||
integrity sha512-0l6GZmT3efexyaaay3SchKT5kG311N59TEFP5lfvEy0nz9SNqjx311plJ3b4jze7arsmDsiHQLh/xnAuk0HFTQ==
|
||||
dependencies:
|
||||
cheerio "^0.22.0"
|
||||
commander "^2.15.1"
|
||||
cross-spawn "^6.0.5"
|
||||
deep-extend "^0.6.0"
|
||||
mensch "^0.3.3"
|
||||
slick "^1.12.2"
|
||||
web-resource-inliner "^4.3.1"
|
||||
|
||||
just-extend@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc"
|
||||
@ -5134,7 +5251,7 @@ lodash.sortby@^4.7.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||
|
||||
lodash.unescape@^4.0.0:
|
||||
lodash.unescape@^4.0.0, lodash.unescape@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
|
||||
integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
|
||||
@ -5224,6 +5341,13 @@ lru-cache@^4.0.0, lru-cache@^4.0.1, lru-cache@^4.1.5:
|
||||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
|
||||
lru-cache@^5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
|
||||
integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
mailcomposer@~0.2.10:
|
||||
version "0.2.12"
|
||||
resolved "https://registry.yarnpkg.com/mailcomposer/-/mailcomposer-0.2.12.tgz#4d02a604616adcb45fb36d37513f4c1bd0b75681"
|
||||
@ -5235,6 +5359,21 @@ mailcomposer@~0.2.10:
|
||||
mime "~1.2.11"
|
||||
mimelib "~0.2.15"
|
||||
|
||||
mailgun-js@0.22.0:
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/mailgun-js/-/mailgun-js-0.22.0.tgz#128942b5e47a364a470791608852bf68c96b3a05"
|
||||
integrity sha512-a2alg5nuTZA9Psa1pSEIEsbxr1Zrmqx4VkgGCQ30xVh0kIH7Bu57AYILo+0v8QLSdXtCyLaS+KVmdCrQo0uWFA==
|
||||
dependencies:
|
||||
async "^2.6.1"
|
||||
debug "^4.1.0"
|
||||
form-data "^2.3.3"
|
||||
inflection "~1.12.0"
|
||||
is-stream "^1.1.0"
|
||||
path-proxy "~1.0.0"
|
||||
promisify-call "^2.0.2"
|
||||
proxy-agent "^3.0.3"
|
||||
tsscmp "^1.0.6"
|
||||
|
||||
make-dir@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
|
||||
@ -5369,6 +5508,11 @@ memoize-one@~5.1.1:
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
|
||||
integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
|
||||
|
||||
mensch@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/mensch/-/mensch-0.3.3.tgz#e200ff4dd823717f8e0563b32e3f5481fca262b2"
|
||||
integrity sha1-4gD/TdgjcX+OBWOzLj9UgfyiYrI=
|
||||
|
||||
meow@^3.3.0:
|
||||
version "3.7.0"
|
||||
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
|
||||
@ -5561,6 +5705,11 @@ mimelib@~0.2.15:
|
||||
addressparser "~0.3.2"
|
||||
encoding "~0.1.7"
|
||||
|
||||
mimer@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mimer/-/mimer-1.0.0.tgz#32251bef4dc7a63184db3a1082ed9be3abe0f3db"
|
||||
integrity sha512-4ZJvCzfcwsBgPbkKXUzGoVZMWjv8IDIygkGzVc7uUYhgnK0t2LmGxxjdgH1i+pn0/KQfB5F/VKUJlfyTSOFQjg==
|
||||
|
||||
mimic-fn@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
|
||||
@ -5905,6 +6054,11 @@ netjet@1.3.0:
|
||||
lru-cache "^4.0.0"
|
||||
posthtml "^0.9.0"
|
||||
|
||||
netmask@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35"
|
||||
integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU=
|
||||
|
||||
next-tick@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
|
||||
@ -6349,6 +6503,31 @@ p-try@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
|
||||
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
|
||||
|
||||
pac-proxy-agent@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-3.0.1.tgz#115b1e58f92576cac2eba718593ca7b0e37de2ad"
|
||||
integrity sha512-44DUg21G/liUZ48dJpUSjZnFfZro/0K5JTyFYLBcmh9+T6Ooi4/i4efwUiEy0+4oQusCBqWdhv16XohIj1GqnQ==
|
||||
dependencies:
|
||||
agent-base "^4.2.0"
|
||||
debug "^4.1.1"
|
||||
get-uri "^2.0.0"
|
||||
http-proxy-agent "^2.1.0"
|
||||
https-proxy-agent "^3.0.0"
|
||||
pac-resolver "^3.0.0"
|
||||
raw-body "^2.2.0"
|
||||
socks-proxy-agent "^4.0.1"
|
||||
|
||||
pac-resolver@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-3.0.0.tgz#6aea30787db0a891704deb7800a722a7615a6f26"
|
||||
integrity sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA==
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
degenerator "^1.0.4"
|
||||
ip "^1.1.5"
|
||||
netmask "^1.0.6"
|
||||
thunkify "^2.1.2"
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
@ -6469,6 +6648,13 @@ path-parse@^1.0.6:
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
|
||||
integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
|
||||
|
||||
path-proxy@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-proxy/-/path-proxy-1.0.0.tgz#18e8a36859fc9d2f1a53b48dee138543c020de5e"
|
||||
integrity sha1-GOijaFn8nS8aU7SN7hOFQ8Ag3l4=
|
||||
dependencies:
|
||||
inflection "~1.3.0"
|
||||
|
||||
path-root-regex@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d"
|
||||
@ -6960,6 +7146,13 @@ progress@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
|
||||
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
|
||||
|
||||
promisify-call@^2.0.2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/promisify-call/-/promisify-call-2.0.4.tgz#d48c2d45652ccccd52801ddecbd533a6d4bd5fba"
|
||||
integrity sha1-1IwtRWUszM1SgB3ey9UzptS9X7o=
|
||||
dependencies:
|
||||
with-callback "^1.0.2"
|
||||
|
||||
propagate@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/propagate/-/propagate-2.0.1.tgz#40cdedab18085c792334e64f0ac17256d38f9a45"
|
||||
@ -6978,6 +7171,25 @@ proxy-addr@~2.0.5:
|
||||
forwarded "~0.1.2"
|
||||
ipaddr.js "1.9.0"
|
||||
|
||||
proxy-agent@^3.0.3:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-3.1.1.tgz#7e04e06bf36afa624a1540be247b47c970bd3014"
|
||||
integrity sha512-WudaR0eTsDx33O3EJE16PjBRZWcX8GqCEeERw1W3hZJgH/F2a46g7jty6UGty6NeJ4CKQy8ds2CJPMiyeqaTvw==
|
||||
dependencies:
|
||||
agent-base "^4.2.0"
|
||||
debug "4"
|
||||
http-proxy-agent "^2.1.0"
|
||||
https-proxy-agent "^3.0.0"
|
||||
lru-cache "^5.1.1"
|
||||
pac-proxy-agent "^3.0.1"
|
||||
proxy-from-env "^1.0.0"
|
||||
socks-proxy-agent "^4.0.1"
|
||||
|
||||
proxy-from-env@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
|
||||
integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
|
||||
|
||||
proxyquire@2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/proxyquire/-/proxyquire-2.1.3.tgz#2049a7eefa10a9a953346a18e54aab2b4268df39"
|
||||
@ -7100,6 +7312,16 @@ raw-body@2.4.0:
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
raw-body@^2.2.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c"
|
||||
integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA==
|
||||
dependencies:
|
||||
bytes "3.1.0"
|
||||
http-errors "1.7.3"
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
raw-body@~1.1.0:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425"
|
||||
@ -7162,7 +7384,7 @@ readable-stream@1.1.x, readable-stream@~1.1.9:
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@2.3.6, readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
|
||||
readable-stream@2, readable-stream@2.3.6, readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
|
||||
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
|
||||
@ -7373,7 +7595,7 @@ request-promise@^4.2.4:
|
||||
stealthy-require "^1.1.1"
|
||||
tough-cookie "^2.3.3"
|
||||
|
||||
"request@>=2.76.0 <3.0.0", request@^2.83.0, request@^2.87.0, request@^2.88.0:
|
||||
"request@>=2.76.0 <3.0.0", request@^2.78.0, request@^2.83.0, request@^2.87.0, request@^2.88.0:
|
||||
version "2.88.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
|
||||
integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
|
||||
@ -7566,7 +7788,7 @@ safe-regex@^1.1.0:
|
||||
dependencies:
|
||||
ret "~0.1.10"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@^2.1.2, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@ -7834,6 +8056,16 @@ slice-ansi@^2.1.0:
|
||||
astral-regex "^1.0.0"
|
||||
is-fullwidth-code-point "^2.0.0"
|
||||
|
||||
slick@^1.12.2:
|
||||
version "1.12.2"
|
||||
resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7"
|
||||
integrity sha1-vQSN23TefRymkV+qSldXCzVQwtc=
|
||||
|
||||
smart-buffer@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba"
|
||||
integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw==
|
||||
|
||||
smartquotes@~2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/smartquotes/-/smartquotes-2.3.1.tgz#01ebb595d6c7a9e24d90e8cb95c17d0e1af49407"
|
||||
@ -7876,6 +8108,22 @@ snapdragon@^0.8.1:
|
||||
source-map-resolve "^0.5.0"
|
||||
use "^3.1.0"
|
||||
|
||||
socks-proxy-agent@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386"
|
||||
integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg==
|
||||
dependencies:
|
||||
agent-base "~4.2.1"
|
||||
socks "~2.3.2"
|
||||
|
||||
socks@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3"
|
||||
integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA==
|
||||
dependencies:
|
||||
ip "1.1.5"
|
||||
smart-buffer "^4.1.0"
|
||||
|
||||
sort-keys@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
|
||||
@ -8400,6 +8648,11 @@ through@^2.3.6:
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
|
||||
|
||||
thunkify@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d"
|
||||
integrity sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0=
|
||||
|
||||
tildify@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a"
|
||||
@ -8549,7 +8802,7 @@ tslib@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||
|
||||
tsscmp@1.0.6:
|
||||
tsscmp@1.0.6, tsscmp@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
|
||||
integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
|
||||
@ -8830,6 +9083,11 @@ v8flags@^3.1.2, v8flags@^3.1.3, v8flags@~3.1.1:
|
||||
dependencies:
|
||||
homedir-polyfill "^1.0.1"
|
||||
|
||||
valid-data-url@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/valid-data-url/-/valid-data-url-2.0.0.tgz#2220fa9f8d4e761ebd3f3bb02770f1212b810537"
|
||||
integrity sha512-dyCZnv3aCey7yfTgIqdZanKl7xWAEEKCbgmR7SKqyK6QT/Z07ROactrgD1eA37C69ODRj7rNOjzKWVPh0EUjBA==
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
|
||||
@ -8917,6 +9175,21 @@ walkdir@^0.0.11:
|
||||
resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.0.11.tgz#a16d025eb931bd03b52f308caed0f40fcebe9532"
|
||||
integrity sha1-oW0CXrkxvQO1LzCMrtD0D86+lTI=
|
||||
|
||||
web-resource-inliner@^4.3.1:
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/web-resource-inliner/-/web-resource-inliner-4.3.3.tgz#a5446b02bc11beb4cb5e764e928d9c1e4ef47f41"
|
||||
integrity sha512-Qk19pohqZs3SoFUW4ZlOHlM8hsOnXhTpCrQ16b1qtJtKzJgO7NZLGP+/lcb2g3hWDQD39/LE/JYOn1Sjy7tn1A==
|
||||
dependencies:
|
||||
async "^3.1.0"
|
||||
chalk "^2.4.2"
|
||||
datauri "^2.0.0"
|
||||
htmlparser2 "^3.9.2"
|
||||
lodash.unescape "^4.0.1"
|
||||
request "^2.78.0"
|
||||
safer-buffer "^2.1.2"
|
||||
valid-data-url "^2.0.0"
|
||||
xtend "^4.0.2"
|
||||
|
||||
webidl-conversions@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||
@ -8999,6 +9272,11 @@ window-size@^0.1.4:
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
|
||||
integrity sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY=
|
||||
|
||||
with-callback@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/with-callback/-/with-callback-1.0.2.tgz#a09629b9a920028d721404fb435bdcff5c91bc21"
|
||||
integrity sha1-oJYpuakgAo1yFAT7Q1vc/1yRvCE=
|
||||
|
||||
wordwrap@~0.0.2:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
|
||||
@ -9097,7 +9375,7 @@ xss@~1.0.6:
|
||||
commander "^2.9.0"
|
||||
cssfilter "0.0.10"
|
||||
|
||||
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
|
||||
xtend@^4.0.0, xtend@^4.0.1, xtend@^4.0.2, xtend@~4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
@ -9117,7 +9395,7 @@ yallist@^2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
|
||||
integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
|
||||
|
||||
yallist@^3.0.0, yallist@^3.0.3:
|
||||
yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
||||
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
|
||||
|
Loading…
Reference in New Issue
Block a user