mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 10:42:45 +03:00
Merge branch 'master' into v3
This commit is contained in:
commit
6028fde666
@ -34,6 +34,7 @@ script: |
|
||||
fi
|
||||
notifications:
|
||||
slack:
|
||||
if: type = cron
|
||||
rooms:
|
||||
- secure: KzvGpf6RFHgQ3BkppKM4OejFjJYxH60KkDDShet0a0v+j/03HF5Nx0S0Vl9O/F2dPt2qWvblKb3j9EKgB7RsoFSdZuOk7ijA9Duvn+p9RrLom0C9JxOA3ob065WAGyE0OGRm1P5H98loX/L+6oOHC4/qqGKXgz6gUdBtZBn/yo8=
|
||||
on_success: change
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 8729af090f8fcd872e6ff0c10f46fb0375527692
|
||||
Subproject commit 215d837848b88354bb94a375338abf1a71ece023
|
@ -1 +1 @@
|
||||
Subproject commit a8ea51b3ad0c099aa02ef790b80c6a26056acbef
|
||||
Subproject commit ebe7c1475f6be9ec3e6471be7b58e76dcb4ae9c6
|
@ -124,7 +124,7 @@ function getAmperizeHTML(html, post) {
|
||||
amperize = amperize || new Amperize();
|
||||
|
||||
// make relative URLs abolute
|
||||
html = urlUtils.htmlRelativeToAbsolute(html, urlUtils.urlFor('home', true), post.url);
|
||||
html = urlUtils.htmlRelativeToAbsolute(html, post.url);
|
||||
|
||||
if (!amperizeCache[post.id] || moment(new Date(amperizeCache[post.id].updated_at)).diff(new Date(post.updated_at)) < 0) {
|
||||
return new Promise((resolve) => {
|
||||
|
@ -21,24 +21,23 @@ generateTags = function generateTags(data) {
|
||||
return [];
|
||||
};
|
||||
|
||||
generateItem = function generateItem(post, siteUrl, secure) {
|
||||
var itemUrl = urlService.getUrlByResourceId(post.id, {secure: secure, absolute: true}),
|
||||
htmlContent = cheerio.load(urlUtils.htmlRelativeToAbsolute(post.html, siteUrl, itemUrl), {decodeEntities: false}),
|
||||
item = {
|
||||
title: post.title,
|
||||
// @TODO: DRY this up with data/meta/index & other excerpt code
|
||||
description: post.custom_excerpt || post.meta_description || downsize(htmlContent.html(), {words: 50}),
|
||||
guid: post.id,
|
||||
url: itemUrl,
|
||||
date: post.published_at,
|
||||
categories: generateTags(post),
|
||||
author: post.primary_author ? post.primary_author.name : null,
|
||||
custom_elements: []
|
||||
},
|
||||
imageUrl;
|
||||
generateItem = function generateItem(post, secure) {
|
||||
const itemUrl = urlService.getUrlByResourceId(post.id, {secure, absolute: true});
|
||||
const htmlContent = cheerio.load(urlUtils.htmlRelativeToAbsolute(post.html, itemUrl, {secure}), {decodeEntities: false});
|
||||
const item = {
|
||||
title: post.title,
|
||||
// @TODO: DRY this up with data/meta/index & other excerpt code
|
||||
description: post.custom_excerpt || post.meta_description || downsize(htmlContent.html(), {words: 50}),
|
||||
guid: post.id,
|
||||
url: itemUrl,
|
||||
date: post.published_at,
|
||||
categories: generateTags(post),
|
||||
author: post.primary_author ? post.primary_author.name : null,
|
||||
custom_elements: []
|
||||
};
|
||||
|
||||
if (post.feature_image) {
|
||||
imageUrl = urlUtils.urlFor('image', {image: post.feature_image, secure: secure}, true);
|
||||
const imageUrl = urlUtils.urlFor('image', {image: post.feature_image, secure}, true);
|
||||
|
||||
// Add a media content tag
|
||||
item.custom_elements.push({
|
||||
@ -73,13 +72,14 @@ generateItem = function generateItem(post, siteUrl, secure) {
|
||||
* @param {{title, description, safeVersion, secure, posts}} data
|
||||
*/
|
||||
generateFeed = function generateFeed(baseUrl, data) {
|
||||
const siteUrl = urlUtils.urlFor('home', {secure: data.secure}, true);
|
||||
const {secure} = data;
|
||||
|
||||
const feed = new RSS({
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
generator: 'Ghost ' + data.safeVersion,
|
||||
feed_url: urlUtils.urlFor({relativeUrl: baseUrl, secure: data.secure}, true),
|
||||
site_url: siteUrl,
|
||||
feed_url: urlUtils.urlFor({relativeUrl: baseUrl, secure}, true),
|
||||
site_url: urlUtils.urlFor('home', {secure}, true),
|
||||
image_url: urlUtils.urlFor({relativeUrl: 'favicon.png'}, true),
|
||||
ttl: '60',
|
||||
custom_namespaces: {
|
||||
@ -90,7 +90,7 @@ generateFeed = function generateFeed(baseUrl, data) {
|
||||
|
||||
return data.posts.reduce((feedPromise, post) => {
|
||||
return feedPromise.then(() => {
|
||||
const item = generateItem(post, siteUrl, data.secure);
|
||||
const item = generateItem(post, secure);
|
||||
return feed.item(item);
|
||||
});
|
||||
}, Promise.resolve()).then(() => {
|
||||
|
@ -80,7 +80,9 @@ function updateLocalTemplateOptions(req, res, next) {
|
||||
|
||||
const member = req.member ? {
|
||||
email: req.member.email,
|
||||
subscribed: req.member.plans.length !== 0
|
||||
name: req.member.name,
|
||||
subscriptions: req.member.stripe.subscriptions,
|
||||
paid: req.member.stripe.subscriptions.length !== 0
|
||||
} : null;
|
||||
|
||||
hbs.updateLocalTemplateOptions(res.locals, _.merge({}, localTemplateOptions, {
|
||||
|
@ -1,7 +1,9 @@
|
||||
// NOTE: We must not cache references to membersService.api
|
||||
// as it is a getter and may change during runtime.
|
||||
const Promise = require('bluebird');
|
||||
const membersService = require('../../services/members');
|
||||
const common = require('../../lib/common');
|
||||
const fsLib = require('../../lib/fs');
|
||||
|
||||
const members = {
|
||||
docName: 'members',
|
||||
@ -40,6 +42,71 @@ const members = {
|
||||
}
|
||||
},
|
||||
|
||||
add: {
|
||||
statusCode: 201,
|
||||
headers: {},
|
||||
options: [
|
||||
'send_email',
|
||||
'email_type'
|
||||
],
|
||||
validation: {
|
||||
data: {
|
||||
email: {required: true}
|
||||
},
|
||||
options: {
|
||||
email_type: {
|
||||
values: ['signin', 'signup', 'subscribe']
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
// NOTE: Promise.resolve() is here for a reason! Method has to return an instance
|
||||
// of a Bluebird promise to allow reflection. If decided to be replaced
|
||||
// with something else, e.g: async/await, CSV export function
|
||||
// would need a deep rewrite (see failing tests if this line is removed)
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return membersService.api.members.create(frame.data.members[0], {
|
||||
sendEmail: frame.options.send_email,
|
||||
emailType: frame.options.email_type
|
||||
});
|
||||
})
|
||||
.then((member) => {
|
||||
if (member) {
|
||||
return Promise.resolve(member);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.members.memberAlreadyExists')}));
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
edit: {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
options: [
|
||||
'id'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
id: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
async query(frame) {
|
||||
const member = await membersService.api.members.update(frame.data.members[0], frame.options);
|
||||
return member;
|
||||
}
|
||||
},
|
||||
|
||||
destroy: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
@ -54,9 +121,87 @@ const members = {
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
async query(frame) {
|
||||
frame.options.require = true;
|
||||
return membersService.api.members.destroy(frame.options).return(null);
|
||||
await membersService.api.members.destroy(frame.options);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
exportCSV: {
|
||||
headers: {
|
||||
disposition: {
|
||||
type: 'csv',
|
||||
value() {
|
||||
const datetime = (new Date()).toJSON().substring(0, 10);
|
||||
return `members.${datetime}.csv`;
|
||||
}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
format: 'plain'
|
||||
},
|
||||
permissions: {
|
||||
method: 'browse'
|
||||
},
|
||||
validation: {},
|
||||
query(frame) {
|
||||
return membersService.api.members.list(frame.options);
|
||||
}
|
||||
},
|
||||
|
||||
importCSV: {
|
||||
statusCode: 201,
|
||||
permissions: {
|
||||
method: 'add'
|
||||
},
|
||||
async query(frame) {
|
||||
let filePath = frame.file.path,
|
||||
fulfilled = 0,
|
||||
invalid = 0,
|
||||
duplicates = 0;
|
||||
|
||||
return fsLib.readCSV({
|
||||
path: filePath,
|
||||
columnsToExtract: [{name: 'email', lookup: /email/i}, {name: 'name', lookup: /name/i}]
|
||||
}).then((result) => {
|
||||
return Promise.all(result.map((entry) => {
|
||||
const api = require('./index');
|
||||
|
||||
return api.members.add.query({
|
||||
data: {
|
||||
members: [{
|
||||
email: entry.email,
|
||||
name: entry.name
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
context: frame.options.context,
|
||||
options: {send_email: false}
|
||||
}
|
||||
}).reflect();
|
||||
})).each((inspection) => {
|
||||
if (inspection.isFulfilled()) {
|
||||
fulfilled = fulfilled + 1;
|
||||
} else {
|
||||
if (inspection.reason() instanceof common.errors.ValidationError) {
|
||||
duplicates = duplicates + 1;
|
||||
} else {
|
||||
invalid = invalid + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
return {
|
||||
meta: {
|
||||
stats: {
|
||||
imported: fulfilled,
|
||||
duplicates: duplicates,
|
||||
invalid: invalid
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -3,7 +3,6 @@ const debug = require('ghost-ignition').debug('api:canary:utils:serializers:inpu
|
||||
const mapNQLKeyValues = require('../../../../../../shared/nql-map-key-values');
|
||||
const url = require('./utils/url');
|
||||
const localUtils = require('../../index');
|
||||
const labs = require('../../../../../services/labs');
|
||||
const converters = require('../../../../../lib/mobiledoc/converters');
|
||||
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
|
||||
|
||||
@ -29,14 +28,6 @@ function removeMobiledocFormat(frame) {
|
||||
}
|
||||
}
|
||||
|
||||
function includeTags(frame) {
|
||||
if (!frame.options.withRelated) {
|
||||
frame.options.withRelated = ['tags'];
|
||||
} else if (!frame.options.withRelated.includes('tags')) {
|
||||
frame.options.withRelated.push('tags');
|
||||
}
|
||||
}
|
||||
|
||||
function defaultRelations(frame) {
|
||||
if (frame.options.withRelated) {
|
||||
return;
|
||||
@ -116,11 +107,6 @@ module.exports = {
|
||||
// CASE: the content api endpoint for posts should not return mobiledoc
|
||||
removeMobiledocFormat(frame);
|
||||
|
||||
// CASE: Members needs to have the tags to check if its allowed access
|
||||
if (labs.isSet('members')) {
|
||||
includeTags(frame);
|
||||
}
|
||||
|
||||
setDefaultOrder(frame);
|
||||
}
|
||||
|
||||
@ -150,11 +136,6 @@ module.exports = {
|
||||
// CASE: the content api endpoint for posts should not return mobiledoc
|
||||
removeMobiledocFormat(frame);
|
||||
|
||||
if (labs.isSet('members')) {
|
||||
// CASE: Members needs to have the tags to check if its allowed access
|
||||
includeTags(frame);
|
||||
}
|
||||
|
||||
setDefaultOrder(frame);
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const url = require('url');
|
||||
const urlUtils = require('../../../../../../lib/url-utils');
|
||||
|
||||
const handleCanonicalUrl = (canonicalUrl) => {
|
||||
const blogURl = urlUtils.getSiteUrl();
|
||||
const isSameProtocol = url.parse(canonicalUrl).protocol === url.parse(blogURl).protocol;
|
||||
const blogDomain = blogURl.replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const absolute = canonicalUrl.replace(/^http(s?):\/\//, '');
|
||||
|
||||
// We only want to transform to a relative URL when the canonical URL matches the current
|
||||
// Blog URL incl. the same protocol. This allows users to keep e.g. Facebook comments after
|
||||
// a http -> https switch
|
||||
if (absolute.startsWith(blogDomain) && isSameProtocol) {
|
||||
return urlUtils.absoluteToRelative(canonicalUrl);
|
||||
}
|
||||
|
||||
return canonicalUrl;
|
||||
};
|
||||
|
||||
const handleImageUrl = (imageUrl) => {
|
||||
const blogDomain = urlUtils.getSiteUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const imageUrlAbsolute = imageUrl.replace(/^http(s?):\/\//, '');
|
||||
@ -30,44 +12,7 @@ const handleImageUrl = (imageUrl) => {
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
const handleContentUrls = (content) => {
|
||||
const blogDomain = urlUtils.getSiteUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const imagePathRe = new RegExp(`(http(s?)://)?${blogDomain}/${urlUtils.STATIC_IMAGE_URL_PREFIX}`, 'g');
|
||||
|
||||
const matches = _.uniq(content.match(imagePathRe));
|
||||
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
const relative = urlUtils.absoluteToRelative(match);
|
||||
content = content.replace(new RegExp(match, 'g'), relative);
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const forPost = (attrs, options) => {
|
||||
// make all content image URLs relative, ref: https://github.com/TryGhost/Ghost/issues/10477
|
||||
if (attrs.mobiledoc) {
|
||||
attrs.mobiledoc = handleContentUrls(attrs.mobiledoc);
|
||||
}
|
||||
|
||||
if (attrs.feature_image) {
|
||||
attrs.feature_image = handleImageUrl(attrs.feature_image);
|
||||
}
|
||||
|
||||
if (attrs.og_image) {
|
||||
attrs.og_image = handleImageUrl(attrs.og_image);
|
||||
}
|
||||
|
||||
if (attrs.twitter_image) {
|
||||
attrs.twitter_image = handleImageUrl(attrs.twitter_image);
|
||||
}
|
||||
|
||||
if (attrs.canonical_url) {
|
||||
attrs.canonical_url = handleCanonicalUrl(attrs.canonical_url);
|
||||
}
|
||||
|
||||
if (options && options.withRelated) {
|
||||
options.withRelated.forEach((relation) => {
|
||||
if (relation === 'tags' && attrs.tags) {
|
||||
|
@ -8,6 +8,22 @@ module.exports = {
|
||||
frame.response = data;
|
||||
},
|
||||
|
||||
add(data, apiConfig, frame) {
|
||||
debug('add');
|
||||
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
},
|
||||
|
||||
edit(data, apiConfig, frame) {
|
||||
debug('edit');
|
||||
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
},
|
||||
|
||||
read(data, apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
@ -20,5 +36,42 @@ module.exports = {
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
},
|
||||
|
||||
exportCSV(models, apiConfig, frame) {
|
||||
debug('exportCSV');
|
||||
|
||||
const fields = ['id', 'email', 'name', 'created_at', 'deleted_at'];
|
||||
|
||||
function formatCSV(data) {
|
||||
let csv = `${fields.join(',')}\r\n`,
|
||||
entry,
|
||||
field,
|
||||
j,
|
||||
i;
|
||||
|
||||
for (j = 0; j < data.length; j = j + 1) {
|
||||
entry = data[j];
|
||||
|
||||
for (i = 0; i < fields.length; i = i + 1) {
|
||||
field = fields[i];
|
||||
csv += entry[field] !== null ? entry[field] : '';
|
||||
if (i !== fields.length - 1) {
|
||||
csv += ',';
|
||||
}
|
||||
}
|
||||
csv += '\r\n';
|
||||
}
|
||||
|
||||
return csv;
|
||||
}
|
||||
|
||||
frame.response = formatCSV(models.members);
|
||||
},
|
||||
|
||||
importCSV(data, apiConfig, frame) {
|
||||
debug('importCSV');
|
||||
|
||||
frame.response = data;
|
||||
}
|
||||
};
|
||||
|
@ -94,6 +94,8 @@ const post = (attrs, frame) => {
|
||||
if (attrs.og_description === '') {
|
||||
attrs.og_description = null;
|
||||
}
|
||||
|
||||
delete attrs.visibility;
|
||||
} else {
|
||||
delete attrs.page;
|
||||
}
|
||||
@ -107,7 +109,6 @@ const post = (attrs, frame) => {
|
||||
}
|
||||
|
||||
delete attrs.locale;
|
||||
delete attrs.visibility;
|
||||
delete attrs.author;
|
||||
delete attrs.type;
|
||||
|
||||
|
@ -1,52 +1,40 @@
|
||||
const _ = require('lodash');
|
||||
const labs = require('../../../../../../services/labs');
|
||||
const membersService = require('../../../../../../services/members');
|
||||
const MEMBER_TAG = '#members';
|
||||
const PERMIT_CONTENT = false;
|
||||
const BLOCK_CONTENT = true;
|
||||
|
||||
// Checks if request should hide memnbers only content
|
||||
function hideMembersOnlyContent(attrs, frame) {
|
||||
const membersEnabled = labs.isSet('members');
|
||||
if (!membersEnabled) {
|
||||
// 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 postHasMemberTag = attrs.tags && attrs.tags.find((tag) => {
|
||||
return (tag.name === MEMBER_TAG);
|
||||
});
|
||||
const requestFromMember = frame.original.context.member;
|
||||
if (!postHasMemberTag) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
if (!requestFromMember) {
|
||||
return BLOCK_CONTENT;
|
||||
} else if (visibility === 'members') {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
const memberHasPlan = !!(frame.original.context.member.plans || []).length;
|
||||
if (!membersService.isPaymentConfigured()) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
if (memberHasPlan) {
|
||||
const memberHasPlan = !!(_.get(frame, 'original.context.member.stripe.subscriptions', [])).length;
|
||||
|
||||
if (visibility === 'paid' && memberHasPlan) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
return BLOCK_CONTENT;
|
||||
}
|
||||
|
||||
const forPost = (attrs, frame) => {
|
||||
const hideFormatsData = hideMembersOnlyContent(attrs, frame);
|
||||
if (hideFormatsData) {
|
||||
['plaintext', 'html'].forEach((field) => {
|
||||
attrs[field] = '';
|
||||
});
|
||||
}
|
||||
if (labs.isSet('members')) {
|
||||
// CASE: Members always adds tags, remove if the user didn't originally ask for them
|
||||
const origQueryOrOptions = frame.original.query || frame.original.options || {};
|
||||
const origInclude = origQueryOrOptions.include;
|
||||
const hideFormatsData = hideMembersOnlyContent(attrs, frame);
|
||||
|
||||
if (!origInclude || !origInclude.includes('tags')) {
|
||||
delete attrs.tags;
|
||||
attrs.primary_tag = null;
|
||||
if (hideFormatsData) {
|
||||
['plaintext', 'html'].forEach((field) => {
|
||||
attrs[field] = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,39 +30,28 @@ const forPost = (id, attrs, frame) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.feature_image) {
|
||||
attrs.feature_image = urlUtils.urlFor('image', {image: attrs.feature_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.og_image) {
|
||||
attrs.og_image = urlUtils.urlFor('image', {image: attrs.og_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.twitter_image) {
|
||||
attrs.twitter_image = urlUtils.urlFor('image', {image: attrs.twitter_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.canonical_url) {
|
||||
attrs.canonical_url = urlUtils.relativeToAbsolute(attrs.canonical_url);
|
||||
}
|
||||
|
||||
if (attrs.html) {
|
||||
const urlOptions = {
|
||||
assetsOnly: true
|
||||
};
|
||||
|
||||
if (frame.options.absolute_urls) {
|
||||
urlOptions.assetsOnly = false;
|
||||
}
|
||||
|
||||
attrs.html = urlUtils.htmlRelativeToAbsolute(
|
||||
attrs.html,
|
||||
urlUtils.urlFor('home', true),
|
||||
attrs.url,
|
||||
urlOptions
|
||||
if (attrs.mobiledoc) {
|
||||
attrs.mobiledoc = urlUtils.mobiledocRelativeToAbsolute(
|
||||
attrs.mobiledoc,
|
||||
attrs.url
|
||||
);
|
||||
}
|
||||
|
||||
['html', 'codeinjection_head', 'codeinjection_foot'].forEach((attr) => {
|
||||
if (attrs[attr]) {
|
||||
attrs[attr] = urlUtils.htmlRelativeToAbsolute(
|
||||
attrs[attr],
|
||||
attrs.url
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
['feature_image', 'og_image', 'twitter_image', 'canonical_url'].forEach((attr) => {
|
||||
if (attrs[attr]) {
|
||||
attrs[attr] = urlUtils.relativeToAbsolute(attrs[attr]);
|
||||
}
|
||||
});
|
||||
|
||||
if (frame.options.columns && !frame.options.columns.includes('url')) {
|
||||
delete attrs.url;
|
||||
}
|
||||
|
@ -23,6 +23,10 @@ module.exports = {
|
||||
return require('./invitations');
|
||||
},
|
||||
|
||||
get members() {
|
||||
return require('./members');
|
||||
},
|
||||
|
||||
get settings() {
|
||||
return require('./settings');
|
||||
},
|
||||
|
15
core/server/api/canary/utils/validators/input/members.js
Normal file
15
core/server/api/canary/utils/validators/input/members.js
Normal file
@ -0,0 +1,15 @@
|
||||
const jsonSchema = require('../utils/json-schema');
|
||||
|
||||
module.exports = {
|
||||
add(apiConfig, frame) {
|
||||
const schema = require('./schemas/members-add');
|
||||
const definitions = require('./schemas/members');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
},
|
||||
|
||||
edit(apiConfig, frame) {
|
||||
const schema = require('./schemas/members-edit');
|
||||
const definitions = require('./schemas/members');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
}
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "members.add",
|
||||
"title": "members.add",
|
||||
"description": "Schema for members.add",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"members": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "members#/definitions/member"}],
|
||||
"required": ["email"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["members"]
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "members.edit",
|
||||
"title": "members.edit",
|
||||
"description": "Schema for members.edit",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"members": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "members#/definitions/member"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["members"]
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "members",
|
||||
"title": "members",
|
||||
"description": "Base members definitions",
|
||||
"definitions": {
|
||||
"member": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 191,
|
||||
"pattern": "^([^,]|$)"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 191,
|
||||
"pattern": "^([^,]|$)"
|
||||
},
|
||||
"id": {
|
||||
"strip": true
|
||||
},
|
||||
"created_at": {
|
||||
"strip": true
|
||||
},
|
||||
"created_by": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_at": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_by": {
|
||||
"strip": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -43,7 +43,7 @@
|
||||
},
|
||||
"visibility": {
|
||||
"type": ["string", "null"],
|
||||
"enum": ["public"]
|
||||
"enum": ["public", "members", "paid"]
|
||||
},
|
||||
"meta_title": {
|
||||
"type": ["string", "null"],
|
||||
|
@ -43,7 +43,7 @@
|
||||
},
|
||||
"visibility": {
|
||||
"type": ["string", "null"],
|
||||
"enum": ["public"]
|
||||
"enum": ["public", "members", "paid"]
|
||||
},
|
||||
"meta_title": {
|
||||
"type": ["string", "null"],
|
||||
|
@ -1,7 +1,9 @@
|
||||
// NOTE: We must not cache references to membersService.api
|
||||
// as it is a getter and may change during runtime.
|
||||
const Promise = require('bluebird');
|
||||
const membersService = require('../../services/members');
|
||||
const common = require('../../lib/common');
|
||||
const fsLib = require('../../lib/fs');
|
||||
|
||||
const members = {
|
||||
docName: 'members',
|
||||
@ -40,6 +42,71 @@ const members = {
|
||||
}
|
||||
},
|
||||
|
||||
add: {
|
||||
statusCode: 201,
|
||||
headers: {},
|
||||
options: [
|
||||
'send_email',
|
||||
'email_type'
|
||||
],
|
||||
validation: {
|
||||
data: {
|
||||
email: {required: true}
|
||||
},
|
||||
options: {
|
||||
email_type: {
|
||||
values: ['signin', 'signup', 'subscribe']
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
// NOTE: Promise.resolve() is here for a reason! Method has to return an instance
|
||||
// of a Bluebird promise to allow reflection. If decided to be replaced
|
||||
// with something else, e.g: async/await, CSV export function
|
||||
// would need a deep rewrite (see failing tests if this line is removed)
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
return membersService.api.members.create(frame.data.members[0], {
|
||||
sendEmail: frame.options.send_email,
|
||||
emailType: frame.options.email_type
|
||||
});
|
||||
})
|
||||
.then((member) => {
|
||||
if (member) {
|
||||
return Promise.resolve(member);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
|
||||
return Promise.reject(new common.errors.ValidationError({message: common.i18n.t('errors.api.members.memberAlreadyExists')}));
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
edit: {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
options: [
|
||||
'id'
|
||||
],
|
||||
validation: {
|
||||
options: {
|
||||
id: {
|
||||
required: true
|
||||
}
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
async query(frame) {
|
||||
const member = await membersService.api.members.update(frame.data.members[0], frame.options);
|
||||
return member;
|
||||
}
|
||||
},
|
||||
|
||||
destroy: {
|
||||
statusCode: 204,
|
||||
headers: {},
|
||||
@ -54,9 +121,87 @@ const members = {
|
||||
}
|
||||
},
|
||||
permissions: true,
|
||||
query(frame) {
|
||||
async query(frame) {
|
||||
frame.options.require = true;
|
||||
return membersService.api.members.destroy(frame.options).return(null);
|
||||
await membersService.api.members.destroy(frame.options);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
exportCSV: {
|
||||
headers: {
|
||||
disposition: {
|
||||
type: 'csv',
|
||||
value() {
|
||||
const datetime = (new Date()).toJSON().substring(0, 10);
|
||||
return `members.${datetime}.csv`;
|
||||
}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
format: 'plain'
|
||||
},
|
||||
permissions: {
|
||||
method: 'browse'
|
||||
},
|
||||
validation: {},
|
||||
query(frame) {
|
||||
return membersService.api.members.list(frame.options);
|
||||
}
|
||||
},
|
||||
|
||||
importCSV: {
|
||||
statusCode: 201,
|
||||
permissions: {
|
||||
method: 'add'
|
||||
},
|
||||
async query(frame) {
|
||||
let filePath = frame.file.path,
|
||||
fulfilled = 0,
|
||||
invalid = 0,
|
||||
duplicates = 0;
|
||||
|
||||
return fsLib.readCSV({
|
||||
path: filePath,
|
||||
columnsToExtract: [{name: 'email', lookup: /email/i}, {name: 'name', lookup: /name/i}]
|
||||
}).then((result) => {
|
||||
return Promise.all(result.map((entry) => {
|
||||
const api = require('./index');
|
||||
|
||||
return api.members.add.query({
|
||||
data: {
|
||||
members: [{
|
||||
email: entry.email,
|
||||
name: entry.name
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
context: frame.options.context,
|
||||
options: {send_email: false}
|
||||
}
|
||||
}).reflect();
|
||||
})).each((inspection) => {
|
||||
if (inspection.isFulfilled()) {
|
||||
fulfilled = fulfilled + 1;
|
||||
} else {
|
||||
if (inspection.reason() instanceof common.errors.ValidationError) {
|
||||
duplicates = duplicates + 1;
|
||||
} else {
|
||||
invalid = invalid + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
return {
|
||||
meta: {
|
||||
stats: {
|
||||
imported: fulfilled,
|
||||
duplicates: duplicates,
|
||||
invalid: invalid
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -3,7 +3,6 @@ const mapNQLKeyValues = require('../../../../../../shared/nql-map-key-values');
|
||||
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:input:posts');
|
||||
const url = require('./utils/url');
|
||||
const localUtils = require('../../index');
|
||||
const labs = require('../../../../../services/labs');
|
||||
const converters = require('../../../../../lib/mobiledoc/converters');
|
||||
const postsMetaSchema = require('../../../../../data/schema').tables.posts_meta;
|
||||
|
||||
@ -29,14 +28,6 @@ function removeMobiledocFormat(frame) {
|
||||
}
|
||||
}
|
||||
|
||||
function includeTags(frame) {
|
||||
if (!frame.options.withRelated) {
|
||||
frame.options.withRelated = ['tags'];
|
||||
} else if (!frame.options.withRelated.includes('tags')) {
|
||||
frame.options.withRelated.push('tags');
|
||||
}
|
||||
}
|
||||
|
||||
function defaultRelations(frame) {
|
||||
if (frame.options.withRelated) {
|
||||
return;
|
||||
@ -116,11 +107,6 @@ module.exports = {
|
||||
// CASE: the content api endpoint for posts should not return mobiledoc
|
||||
removeMobiledocFormat(frame);
|
||||
|
||||
// CASE: Members needs to have the tags to check if its allowed access
|
||||
if (labs.isSet('members')) {
|
||||
includeTags(frame);
|
||||
}
|
||||
|
||||
setDefaultOrder(frame);
|
||||
}
|
||||
|
||||
@ -150,11 +136,6 @@ module.exports = {
|
||||
// CASE: the content api endpoint for posts should not return mobiledoc
|
||||
removeMobiledocFormat(frame);
|
||||
|
||||
if (labs.isSet('members')) {
|
||||
// CASE: Members needs to have the tags to check if its allowed access
|
||||
includeTags(frame);
|
||||
}
|
||||
|
||||
setDefaultOrder(frame);
|
||||
}
|
||||
|
||||
|
@ -1,23 +1,5 @@
|
||||
const _ = require('lodash');
|
||||
const url = require('url');
|
||||
const urlUtils = require('../../../../../../lib/url-utils');
|
||||
|
||||
const handleCanonicalUrl = (canonicalUrl) => {
|
||||
const siteUrl = urlUtils.getSiteUrl();
|
||||
const isSameProtocol = url.parse(canonicalUrl).protocol === url.parse(siteUrl).protocol;
|
||||
const siteDomain = siteUrl.replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const absolute = canonicalUrl.replace(/^http(s?):\/\//, '');
|
||||
|
||||
// We only want to transform to a relative URL when the canonical URL matches the current
|
||||
// Site URL incl. the same protocol. This allows users to keep e.g. Facebook comments after
|
||||
// a http -> https switch
|
||||
if (absolute.startsWith(siteDomain) && isSameProtocol) {
|
||||
return urlUtils.absoluteToRelative(canonicalUrl);
|
||||
}
|
||||
|
||||
return canonicalUrl;
|
||||
};
|
||||
|
||||
const handleImageUrl = (imageUrl) => {
|
||||
const siteDomain = urlUtils.getSiteUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const imageUrlAbsolute = imageUrl.replace(/^http(s?):\/\//, '');
|
||||
@ -30,44 +12,7 @@ const handleImageUrl = (imageUrl) => {
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
const handleContentUrls = (content) => {
|
||||
const siteDomain = urlUtils.getSiteUrl().replace(/^http(s?):\/\//, '').replace(/\/$/, '');
|
||||
const imagePathRe = new RegExp(`(http(s?)://)?${siteDomain}/${urlUtils.STATIC_IMAGE_URL_PREFIX}`, 'g');
|
||||
|
||||
const matches = _.uniq(content.match(imagePathRe));
|
||||
|
||||
if (matches) {
|
||||
matches.forEach((match) => {
|
||||
const relative = urlUtils.absoluteToRelative(match);
|
||||
content = content.replace(new RegExp(match, 'g'), relative);
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
const forPost = (attrs, options) => {
|
||||
// make all content image URLs relative, ref: https://github.com/TryGhost/Ghost/issues/10477
|
||||
if (attrs.mobiledoc) {
|
||||
attrs.mobiledoc = handleContentUrls(attrs.mobiledoc);
|
||||
}
|
||||
|
||||
if (attrs.feature_image) {
|
||||
attrs.feature_image = handleImageUrl(attrs.feature_image);
|
||||
}
|
||||
|
||||
if (attrs.og_image) {
|
||||
attrs.og_image = handleImageUrl(attrs.og_image);
|
||||
}
|
||||
|
||||
if (attrs.twitter_image) {
|
||||
attrs.twitter_image = handleImageUrl(attrs.twitter_image);
|
||||
}
|
||||
|
||||
if (attrs.canonical_url) {
|
||||
attrs.canonical_url = handleCanonicalUrl(attrs.canonical_url);
|
||||
}
|
||||
|
||||
if (options && options.withRelated) {
|
||||
options.withRelated.forEach((relation) => {
|
||||
if (relation === 'tags' && attrs.tags) {
|
||||
|
@ -8,6 +8,22 @@ module.exports = {
|
||||
frame.response = data;
|
||||
},
|
||||
|
||||
add(data, apiConfig, frame) {
|
||||
debug('add');
|
||||
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
},
|
||||
|
||||
edit(data, apiConfig, frame) {
|
||||
debug('edit');
|
||||
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
},
|
||||
|
||||
read(data, apiConfig, frame) {
|
||||
debug('read');
|
||||
|
||||
@ -20,5 +36,42 @@ module.exports = {
|
||||
frame.response = {
|
||||
members: [data]
|
||||
};
|
||||
},
|
||||
|
||||
exportCSV(models, apiConfig, frame) {
|
||||
debug('exportCSV');
|
||||
|
||||
const fields = ['id', 'email', 'name', 'created_at', 'deleted_at'];
|
||||
|
||||
function formatCSV(data) {
|
||||
let csv = `${fields.join(',')}\r\n`,
|
||||
entry,
|
||||
field,
|
||||
j,
|
||||
i;
|
||||
|
||||
for (j = 0; j < data.length; j = j + 1) {
|
||||
entry = data[j];
|
||||
|
||||
for (i = 0; i < fields.length; i = i + 1) {
|
||||
field = fields[i];
|
||||
csv += entry[field] !== null ? entry[field] : '';
|
||||
if (i !== fields.length - 1) {
|
||||
csv += ',';
|
||||
}
|
||||
}
|
||||
csv += '\r\n';
|
||||
}
|
||||
|
||||
return csv;
|
||||
}
|
||||
|
||||
frame.response = formatCSV(models.members);
|
||||
},
|
||||
|
||||
importCSV(data, apiConfig, frame) {
|
||||
debug('importCSV');
|
||||
|
||||
frame.response = data;
|
||||
}
|
||||
};
|
||||
|
@ -94,6 +94,8 @@ const post = (attrs, frame) => {
|
||||
if (attrs.og_description === '') {
|
||||
attrs.og_description = null;
|
||||
}
|
||||
|
||||
delete attrs.visibility;
|
||||
} else {
|
||||
delete attrs.page;
|
||||
|
||||
@ -107,7 +109,6 @@ const post = (attrs, frame) => {
|
||||
}
|
||||
|
||||
delete attrs.locale;
|
||||
delete attrs.visibility;
|
||||
delete attrs.author;
|
||||
delete attrs.type;
|
||||
|
||||
|
@ -1,52 +1,40 @@
|
||||
const _ = require('lodash');
|
||||
const labs = require('../../../../../../services/labs');
|
||||
const membersService = require('../../../../../../services/members');
|
||||
const MEMBER_TAG = '#members';
|
||||
const PERMIT_CONTENT = false;
|
||||
const BLOCK_CONTENT = true;
|
||||
|
||||
// Checks if request should hide memnbers only content
|
||||
function hideMembersOnlyContent(attrs, frame) {
|
||||
const membersEnabled = labs.isSet('members');
|
||||
if (!membersEnabled) {
|
||||
// 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 postHasMemberTag = attrs.tags && attrs.tags.find((tag) => {
|
||||
return (tag.name === MEMBER_TAG);
|
||||
});
|
||||
const requestFromMember = frame.original.context.member;
|
||||
if (!postHasMemberTag) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
if (!requestFromMember) {
|
||||
return BLOCK_CONTENT;
|
||||
} else if (visibility === 'members') {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
const memberHasPlan = !!(frame.original.context.member.plans || []).length;
|
||||
if (!membersService.isPaymentConfigured()) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
if (memberHasPlan) {
|
||||
const memberHasPlan = !!(_.get(frame, 'original.context.member.stripe.subscriptions', [])).length;
|
||||
|
||||
if (visibility === 'paid' && memberHasPlan) {
|
||||
return PERMIT_CONTENT;
|
||||
}
|
||||
|
||||
return BLOCK_CONTENT;
|
||||
}
|
||||
|
||||
const forPost = (attrs, frame) => {
|
||||
const hideFormatsData = hideMembersOnlyContent(attrs, frame);
|
||||
if (hideFormatsData) {
|
||||
['plaintext', 'html'].forEach((field) => {
|
||||
attrs[field] = '';
|
||||
});
|
||||
}
|
||||
if (labs.isSet('members')) {
|
||||
// CASE: Members always adds tags, remove if the user didn't originally ask for them
|
||||
const origQueryOrOptions = frame.original.query || frame.original.options || {};
|
||||
const origInclude = origQueryOrOptions.include;
|
||||
const hideFormatsData = hideMembersOnlyContent(attrs, frame);
|
||||
|
||||
if (!origInclude || !origInclude.includes('tags')) {
|
||||
delete attrs.tags;
|
||||
attrs.primary_tag = null;
|
||||
if (hideFormatsData) {
|
||||
['plaintext', 'html'].forEach((field) => {
|
||||
attrs[field] = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,39 +30,38 @@ const forPost = (id, attrs, frame) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs.feature_image) {
|
||||
attrs.feature_image = urlUtils.urlFor('image', {image: attrs.feature_image}, true);
|
||||
const urlOptions = {};
|
||||
|
||||
// v2 only transforms asset URLS, v3 will transform all urls so that
|
||||
// input/output transformations are balanced and all URLs are absolute
|
||||
if (!frame.options.absolute_urls) {
|
||||
urlOptions.assetsOnly = true;
|
||||
}
|
||||
|
||||
if (attrs.og_image) {
|
||||
attrs.og_image = urlUtils.urlFor('image', {image: attrs.og_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.twitter_image) {
|
||||
attrs.twitter_image = urlUtils.urlFor('image', {image: attrs.twitter_image}, true);
|
||||
}
|
||||
|
||||
if (attrs.canonical_url) {
|
||||
attrs.canonical_url = urlUtils.relativeToAbsolute(attrs.canonical_url);
|
||||
}
|
||||
|
||||
if (attrs.html) {
|
||||
const urlOptions = {
|
||||
assetsOnly: true
|
||||
};
|
||||
|
||||
if (frame.options.absolute_urls) {
|
||||
urlOptions.assetsOnly = false;
|
||||
}
|
||||
|
||||
attrs.html = urlUtils.htmlRelativeToAbsolute(
|
||||
attrs.html,
|
||||
urlUtils.urlFor('home', true),
|
||||
if (attrs.mobiledoc) {
|
||||
attrs.mobiledoc = urlUtils.mobiledocRelativeToAbsolute(
|
||||
attrs.mobiledoc,
|
||||
attrs.url,
|
||||
urlOptions
|
||||
);
|
||||
}
|
||||
|
||||
['html', 'codeinjection_head', 'codeinjection_foot'].forEach((attr) => {
|
||||
if (attrs[attr]) {
|
||||
attrs[attr] = urlUtils.htmlRelativeToAbsolute(
|
||||
attrs[attr],
|
||||
attrs.url,
|
||||
urlOptions
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
['feature_image', 'og_image', 'twitter_image', 'canonical_url'].forEach((attr) => {
|
||||
if (attrs[attr]) {
|
||||
attrs[attr] = urlUtils.relativeToAbsolute(attrs[attr], attrs.url, urlOptions);
|
||||
}
|
||||
});
|
||||
|
||||
if (frame.options.columns && !frame.options.columns.includes('url')) {
|
||||
delete attrs.url;
|
||||
}
|
||||
|
@ -23,6 +23,10 @@ module.exports = {
|
||||
return require('./invitations');
|
||||
},
|
||||
|
||||
get members() {
|
||||
return require('./members');
|
||||
},
|
||||
|
||||
get settings() {
|
||||
return require('./settings');
|
||||
},
|
||||
|
15
core/server/api/v2/utils/validators/input/members.js
Normal file
15
core/server/api/v2/utils/validators/input/members.js
Normal file
@ -0,0 +1,15 @@
|
||||
const jsonSchema = require('../utils/json-schema');
|
||||
|
||||
module.exports = {
|
||||
add(apiConfig, frame) {
|
||||
const schema = require('./schemas/members-add');
|
||||
const definitions = require('./schemas/members');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
},
|
||||
|
||||
edit(apiConfig, frame) {
|
||||
const schema = require('./schemas/members-edit');
|
||||
const definitions = require('./schemas/members');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
}
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "members.add",
|
||||
"title": "members.add",
|
||||
"description": "Schema for members.add",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"members": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "members#/definitions/member"}],
|
||||
"required": ["email"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["members"]
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "members.edit",
|
||||
"title": "members.edit",
|
||||
"description": "Schema for members.edit",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"members": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"maxItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"allOf": [{"$ref": "members#/definitions/member"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["members"]
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "members",
|
||||
"title": "members",
|
||||
"description": "Base members definitions",
|
||||
"definitions": {
|
||||
"member": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 191,
|
||||
"pattern": "^([^,]|$)"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 191,
|
||||
"pattern": "^([^,]|$)"
|
||||
},
|
||||
"id": {
|
||||
"strip": true
|
||||
},
|
||||
"created_at": {
|
||||
"strip": true
|
||||
},
|
||||
"created_by": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_at": {
|
||||
"strip": true
|
||||
},
|
||||
"updated_by": {
|
||||
"strip": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -43,7 +43,7 @@
|
||||
},
|
||||
"visibility": {
|
||||
"type": ["string", "null"],
|
||||
"enum": ["public"]
|
||||
"enum": ["public", "members", "paid"]
|
||||
},
|
||||
"meta_title": {
|
||||
"type": ["string", "null"],
|
||||
|
@ -43,7 +43,7 @@
|
||||
},
|
||||
"visibility": {
|
||||
"type": ["string", "null"],
|
||||
"enum": ["public"]
|
||||
"enum": ["public", "members", "paid"]
|
||||
},
|
||||
"meta_title": {
|
||||
"type": ["string", "null"],
|
||||
|
@ -32,6 +32,10 @@
|
||||
"extensions": [".csv"],
|
||||
"contentTypes": ["text/csv", "application/csv", "application/octet-stream"]
|
||||
},
|
||||
"members": {
|
||||
"extensions": [".csv"],
|
||||
"contentTypes": ["text/csv", "application/csv", "application/octet-stream"]
|
||||
},
|
||||
"images": {
|
||||
"extensions": [".jpg", ".jpeg", ".gif", ".png", ".svg", ".svgz", ".ico"],
|
||||
"contentTypes": ["image/jpeg", "image/png", "image/gif", "image/svg+xml", "image/x-icon", "image/vnd.microsoft.icon"]
|
||||
|
@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
async up(){},
|
||||
async down(){}
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
const commands = require('../../../schema').commands;
|
||||
|
||||
module.exports = {
|
||||
|
||||
up: commands.createColumnMigration({
|
||||
table: 'members',
|
||||
column: 'name',
|
||||
dbIsInCorrectState(hasColumn) {
|
||||
return hasColumn === true;
|
||||
},
|
||||
operation: commands.addColumn,
|
||||
operationVerb: 'Adding'
|
||||
}),
|
||||
|
||||
down: commands.createColumnMigration({
|
||||
table: 'members',
|
||||
column: 'name',
|
||||
dbIsInCorrectState(hasColumn) {
|
||||
return hasColumn === false;
|
||||
},
|
||||
operation: commands.dropColumn,
|
||||
operationVerb: 'Dropping'
|
||||
}),
|
||||
|
||||
config: {
|
||||
transaction: true
|
||||
}
|
||||
};
|
@ -0,0 +1,37 @@
|
||||
const common = require('../../../../lib/common');
|
||||
const commands = require('../../../schema/commands');
|
||||
|
||||
module.exports = {
|
||||
config: {
|
||||
transaction: true
|
||||
},
|
||||
|
||||
async up(options){
|
||||
const conn = options.transacting || options.connection;
|
||||
const hasTable = await conn.schema.hasTable('members_stripe_customers');
|
||||
|
||||
if (hasTable) {
|
||||
common.logging.info('Dropping table: members_stripe_customers');
|
||||
await commands.deleteTable('members_stripe_customers', conn);
|
||||
} else {
|
||||
common.logging.warn('Dropping table: members_stripe_customers');
|
||||
}
|
||||
|
||||
common.logging.info('Adding table: members_stripe_customers');
|
||||
return commands.createTable('members_stripe_customers', conn);
|
||||
},
|
||||
|
||||
async down(options){
|
||||
const conn = options.transacting || options.connection;
|
||||
const hasTable = await conn.schema.hasTable('members_stripe_customers');
|
||||
|
||||
if (!hasTable) {
|
||||
common.logging.warn('Dropping table: members_stripe_customers');
|
||||
return;
|
||||
}
|
||||
|
||||
common.logging.info('Dropping table: members_stripe_customers');
|
||||
return commands.deleteTable('members_stripe_customers', conn);
|
||||
}
|
||||
};
|
||||
|
@ -194,6 +194,9 @@
|
||||
"members_session_secret": {
|
||||
"defaultValue": null
|
||||
},
|
||||
"default_content_visibility": {
|
||||
"defaultValue": "public"
|
||||
},
|
||||
"members_subscription_settings": {
|
||||
"defaultValue": "{\"isPaid\":false,\"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\":\"\"}]}}]}"
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ module.exports = {
|
||||
maxlength: 50,
|
||||
nullable: false,
|
||||
defaultTo: 'public',
|
||||
validations: {isIn: [['public']]}
|
||||
validations: {isIn: [['public', 'members', 'paid']]}
|
||||
},
|
||||
/**
|
||||
* @deprecated: `author_id`, might be removed in Ghost 3.0
|
||||
@ -343,6 +343,17 @@ module.exports = {
|
||||
members: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
|
||||
name: {type: 'string', maxlength: 191, nullable: true},
|
||||
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}
|
||||
},
|
||||
members_stripe_customers: {
|
||||
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
||||
member_id: {type: 'string', maxlength: 24, nullable: false, unique: false},
|
||||
// customer_id is unique: false because mysql with innodb utf8mb4 cannot have unqiue columns larger than 191 chars
|
||||
customer_id: {type: 'string', maxlength: 255, nullable: false, unique: false},
|
||||
created_at: {type: 'dateTime', nullable: false},
|
||||
created_by: {type: 'string', maxlength: 24, nullable: false},
|
||||
updated_at: {type: 'dateTime', nullable: true},
|
||||
|
@ -37,11 +37,12 @@ module.exports = createCard({
|
||||
name: 'bookmark',
|
||||
type: 'dom',
|
||||
render(opts) {
|
||||
if (!opts.payload.metadata) {
|
||||
let {payload, env: {dom}} = opts;
|
||||
|
||||
if (!payload.metadata || !payload.metadata.url || !payload.metadata.title || !payload.metadata.description) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let {payload, env: {dom}} = opts;
|
||||
let figure = createElement(dom, 'figure', 'kg-card kg-bookmark-card');
|
||||
let linkTag = createElement(dom, 'a', 'kg-bookmark-container', [{
|
||||
key: 'href',
|
||||
@ -89,5 +90,21 @@ module.exports = createCard({
|
||||
}
|
||||
|
||||
return figure;
|
||||
},
|
||||
|
||||
absoluteToRelative(urlUtils, payload, options) {
|
||||
if (payload.metadata) {
|
||||
payload.metadata.url = payload.metadata.url && urlUtils.absoluteToRelative(payload.metadata.url, options);
|
||||
}
|
||||
payload.caption = payload.caption && urlUtils.htmlAbsoluteToRelative(payload.caption, options);
|
||||
return payload;
|
||||
},
|
||||
|
||||
relativeToAbsolute(urlUtils, payload, options) {
|
||||
if (payload.metadata) {
|
||||
payload.metadata.url = payload.metadata.url && urlUtils.relativeToAbsolute(payload.metadata.url, options);
|
||||
}
|
||||
payload.caption = payload.caption && urlUtils.htmlRelativeToAbsolute(payload.caption, options);
|
||||
return payload;
|
||||
}
|
||||
});
|
||||
|
@ -34,5 +34,15 @@ module.exports = createCard({
|
||||
} else {
|
||||
return pre;
|
||||
}
|
||||
},
|
||||
|
||||
absoluteToRelative(urlUtils, payload, options) {
|
||||
payload.caption = payload.caption && urlUtils.htmlAbsoluteToRelative(payload.caption, options);
|
||||
return payload;
|
||||
},
|
||||
|
||||
relativeToAbsolute(urlUtils, payload, options) {
|
||||
payload.caption = payload.caption && urlUtils.htmlRelativeToAbsolute(payload.caption, options);
|
||||
return payload;
|
||||
}
|
||||
});
|
||||
|
@ -24,5 +24,15 @@ module.exports = createCard({
|
||||
}
|
||||
|
||||
return figure;
|
||||
},
|
||||
|
||||
absoluteToRelative(urlUtils, payload, options) {
|
||||
payload.caption = payload.caption && urlUtils.htmlAbsoluteToRelative(payload.caption, options);
|
||||
return payload;
|
||||
},
|
||||
|
||||
relativeToAbsolute(urlUtils, payload, options) {
|
||||
payload.caption = payload.caption && urlUtils.htmlRelativeToAbsolute(payload.caption, options);
|
||||
return payload;
|
||||
}
|
||||
});
|
||||
|
@ -108,5 +108,31 @@ module.exports = createCard({
|
||||
}
|
||||
|
||||
return figure;
|
||||
},
|
||||
|
||||
absoluteToRelative(urlUtils, payload, options) {
|
||||
if (payload.images) {
|
||||
payload.images.forEach((image) => {
|
||||
image.src = image.src && urlUtils.absoluteToRelative(image.src, options);
|
||||
image.caption = image.caption && urlUtils.htmlAbsoluteToRelative(image.caption, options);
|
||||
});
|
||||
}
|
||||
|
||||
payload.caption = payload.caption && urlUtils.htmlAbsoluteToRelative(payload.caption, options);
|
||||
|
||||
return payload;
|
||||
},
|
||||
|
||||
relativeToAbsolute(urlUtils, payload, options) {
|
||||
if (payload.images) {
|
||||
payload.images.forEach((image) => {
|
||||
image.src = image.src && urlUtils.relativeToAbsolute(image.src, options);
|
||||
image.caption = image.caption && urlUtils.htmlRelativeToAbsolute(image.caption, options);
|
||||
});
|
||||
}
|
||||
|
||||
payload.caption = payload.caption && urlUtils.htmlRelativeToAbsolute(payload.caption, options);
|
||||
|
||||
return payload;
|
||||
}
|
||||
});
|
||||
|
@ -14,5 +14,15 @@ module.exports = createCard({
|
||||
// use the SimpleDOM document to create a raw HTML section.
|
||||
// avoids parsing/rendering of potentially broken or unsupported HTML
|
||||
return opts.env.dom.createRawHTMLSection(opts.payload.html);
|
||||
},
|
||||
|
||||
absoluteToRelative(urlUtils, payload, options) {
|
||||
payload.html = payload.html && urlUtils.htmlAbsoluteToRelative(payload.html, options);
|
||||
return payload;
|
||||
},
|
||||
|
||||
relativeToAbsolute(urlUtils, payload, options) {
|
||||
payload.html = payload.html && urlUtils.htmlRelativeToAbsolute(payload.html, options);
|
||||
return payload;
|
||||
}
|
||||
});
|
||||
|
@ -39,5 +39,17 @@ module.exports = createCard({
|
||||
}
|
||||
|
||||
return figure;
|
||||
},
|
||||
|
||||
absoluteToRelative(urlUtils, payload, options) {
|
||||
payload.src = payload.src && urlUtils.absoluteToRelative(payload.src, options);
|
||||
payload.caption = payload.caption && urlUtils.htmlAbsoluteToRelative(payload.caption, options);
|
||||
return payload;
|
||||
},
|
||||
|
||||
relativeToAbsolute(urlUtils, payload, options) {
|
||||
payload.src = payload.src && urlUtils.relativeToAbsolute(payload.src, options);
|
||||
payload.caption = payload.caption && urlUtils.htmlRelativeToAbsolute(payload.caption, options);
|
||||
return payload;
|
||||
}
|
||||
});
|
||||
|
@ -19,5 +19,15 @@ module.exports = createCard({
|
||||
// use the SimpleDOM document to create a raw HTML section.
|
||||
// avoids parsing/rendering of potentially broken or unsupported HTML
|
||||
return opts.env.dom.createRawHTMLSection(html);
|
||||
},
|
||||
|
||||
absoluteToRelative(urlUtils, payload, options) {
|
||||
payload.markdown = payload.markdown && urlUtils.markdownAbsoluteToRelative(payload.markdown, options);
|
||||
return payload;
|
||||
},
|
||||
|
||||
relativeToAbsolute(urlUtils, payload, options) {
|
||||
payload.markdown = payload.markdown && urlUtils.markdownRelativeToAbsolute(payload.markdown, options);
|
||||
return payload;
|
||||
}
|
||||
});
|
||||
|
@ -1,9 +1,22 @@
|
||||
let urlUtils;
|
||||
|
||||
module.exports = function createCard(card) {
|
||||
const {name, type, config = {}} = card;
|
||||
const defaultTransformer = function (payload) {
|
||||
return payload;
|
||||
};
|
||||
|
||||
const {
|
||||
name,
|
||||
type,
|
||||
config = {},
|
||||
absoluteToRelative = defaultTransformer,
|
||||
relativeToAbsolute = defaultTransformer
|
||||
} = card;
|
||||
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
|
||||
render({env, payload, options}) {
|
||||
const {dom} = env;
|
||||
const cleanName = name.replace(/^card-/, '');
|
||||
@ -27,6 +40,26 @@ module.exports = function createCard(card) {
|
||||
}
|
||||
|
||||
return cardOutput;
|
||||
},
|
||||
|
||||
absoluteToRelative() {
|
||||
// it's necessary to wait until the method is called to require
|
||||
// urlUtils to ensure the class has actually been instantiated
|
||||
// as cards are passed in as an arg to the class instantiation
|
||||
if (!urlUtils) {
|
||||
urlUtils = require('../url-utils');
|
||||
}
|
||||
return absoluteToRelative(urlUtils, ...arguments);
|
||||
},
|
||||
|
||||
relativeToAbsolute() {
|
||||
// it's necessary to wait until the method is called to require
|
||||
// urlUtils to ensure the class has actually been instantiated
|
||||
// as cards are passed in as an arg to the class instantiation
|
||||
if (!urlUtils) {
|
||||
urlUtils = require('../url-utils');
|
||||
}
|
||||
return relativeToAbsolute(urlUtils, ...arguments);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
const UrlUtils = require('@tryghost/url-utils');
|
||||
const config = require('../../config');
|
||||
const cards = require('../mobiledoc/cards');
|
||||
|
||||
const urlUtils = new UrlUtils({
|
||||
url: config.get('url'),
|
||||
@ -8,7 +9,8 @@ const urlUtils = new UrlUtils({
|
||||
defaultApiVersion: 'v3',
|
||||
slugs: config.get('slugs').protected,
|
||||
redirectCacheMaxAge: config.get('caching:301:maxAge'),
|
||||
baseApiPath: '/ghost/api'
|
||||
baseApiPath: '/ghost/api',
|
||||
cardTransformers: cards
|
||||
});
|
||||
|
||||
module.exports = urlUtils;
|
||||
|
@ -35,7 +35,8 @@ models = [
|
||||
'mobiledoc-revision',
|
||||
'member',
|
||||
'action',
|
||||
'posts-meta'
|
||||
'posts-meta',
|
||||
'member-stripe-customer'
|
||||
];
|
||||
|
||||
function init() {
|
||||
|
9
core/server/models/member-stripe-customer.js
Normal file
9
core/server/models/member-stripe-customer.js
Normal file
@ -0,0 +1,9 @@
|
||||
const ghostBookshelf = require('./base');
|
||||
|
||||
const MemberStripeCustomer = ghostBookshelf.Model.extend({
|
||||
tableName: 'members_stripe_customers'
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
MemberStripeCustomer: ghostBookshelf.model('MemberStripeCustomer', MemberStripeCustomer)
|
||||
};
|
@ -1,32 +1,23 @@
|
||||
const ghostBookshelf = require('./base');
|
||||
const security = require('../lib/security');
|
||||
|
||||
const Member = ghostBookshelf.Model.extend({
|
||||
tableName: 'members',
|
||||
|
||||
onSaving() {
|
||||
ghostBookshelf.Model.prototype.onSaving.apply(this, arguments);
|
||||
|
||||
if (this.hasChanged('password')) {
|
||||
return security.password.hash(String(this.get('password')))
|
||||
.then((hash) => {
|
||||
this.set('password', hash);
|
||||
});
|
||||
}
|
||||
relationships: ['stripe_customers'],
|
||||
relationshipBelongsTo: {
|
||||
stripe_customers: 'members_stripe_customers'
|
||||
},
|
||||
|
||||
comparePassword(rawPassword) {
|
||||
return security.password.compare(rawPassword, this.get('password'));
|
||||
permittedAttributes(...args) {
|
||||
return ghostBookshelf.Model.prototype.permittedAttributes.apply(this, args).concat(this.relationships);
|
||||
},
|
||||
|
||||
toJSON(unfilteredOptions) {
|
||||
var options = Member.filterOptions(unfilteredOptions, 'toJSON'),
|
||||
attrs = ghostBookshelf.Model.prototype.toJSON.call(this, options);
|
||||
|
||||
// remove password hash and tokens for security reasons
|
||||
delete attrs.password;
|
||||
|
||||
return attrs;
|
||||
stripe_customers() {
|
||||
return this.hasMany('MemberStripeCustomer', 'member_id');
|
||||
}
|
||||
}, {
|
||||
permittedOptions(...args) {
|
||||
return ghostBookshelf.Model.permittedOptions.apply(this, args).concat(['withRelated']);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -8,8 +8,10 @@ const common = require('../lib/common');
|
||||
const htmlToText = require('html-to-text');
|
||||
const ghostBookshelf = require('./base');
|
||||
const config = require('../config');
|
||||
const settingsCache = require('../services/settings/cache');
|
||||
const converters = require('../lib/mobiledoc/converters');
|
||||
const relations = require('./relations');
|
||||
const urlUtils = require('../lib/url-utils');
|
||||
const MOBILEDOC_REVISIONS_COUNT = 10;
|
||||
const ALL_STATUSES = ['published', 'draft', 'scheduled'];
|
||||
|
||||
@ -40,12 +42,18 @@ Post = ghostBookshelf.Model.extend({
|
||||
* 2. model events e.g. "post.published" are using the inserted resource, not the fetched resource
|
||||
*/
|
||||
defaults: function defaults() {
|
||||
let visibility = 'public';
|
||||
|
||||
if (settingsCache.get('labs') && (settingsCache.get('labs').members === true) && settingsCache.get('default_content_visibility')) {
|
||||
visibility = settingsCache.get('default_content_visibility');
|
||||
}
|
||||
|
||||
return {
|
||||
uuid: uuid.v4(),
|
||||
status: 'draft',
|
||||
featured: false,
|
||||
type: 'post',
|
||||
visibility: 'public'
|
||||
visibility: visibility
|
||||
};
|
||||
},
|
||||
|
||||
@ -359,6 +367,39 @@ Post = ghostBookshelf.Model.extend({
|
||||
this.set('mobiledoc', JSON.stringify(converters.mobiledocConverter.blankStructure()));
|
||||
}
|
||||
|
||||
// ensure all URLs are stored as relative
|
||||
// note: html is not necessary to change because it's a generated later from mobiledoc
|
||||
const urlTransformMap = {
|
||||
mobiledoc: 'mobiledocAbsoluteToRelative',
|
||||
custom_excerpt: 'htmlAbsoluteToRelative',
|
||||
codeinjection_head: 'htmlAbsoluteToRelative',
|
||||
codeinjection_foot: 'htmlAbsoluteToRelative',
|
||||
feature_image: 'absoluteToRelative',
|
||||
og_image: 'absoluteToRelative',
|
||||
twitter_image: 'absoluteToRelative',
|
||||
canonical_url: {
|
||||
method: 'absoluteToRelative',
|
||||
options: {
|
||||
ignoreProtocol: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(urlTransformMap).forEach(([attr, transform]) => {
|
||||
let method = transform;
|
||||
let options = {};
|
||||
|
||||
if (typeof transform === 'object') {
|
||||
method = transform.method;
|
||||
options = transform.options || {};
|
||||
}
|
||||
|
||||
if (this.hasChanged(attr) && this.get(attr)) {
|
||||
const transformedValue = urlUtils[method](this.get(attr), options);
|
||||
this.set(attr, transformedValue);
|
||||
}
|
||||
});
|
||||
|
||||
// CASE: mobiledoc has changed, generate html
|
||||
// CASE: html is null, but mobiledoc exists (only important for migrations & importing)
|
||||
if (this.hasChanged('mobiledoc') || (!this.get('html') && (options.migrating || options.importing))) {
|
||||
|
@ -9,6 +9,11 @@ Array.prototype.forEach.call(document.querySelectorAll('form[data-members-form]'
|
||||
form.classList.remove('success', 'invalid', 'error');
|
||||
var input = event.target.querySelector('input[data-members-email]');
|
||||
var email = input.value;
|
||||
var emailType = undefined;
|
||||
|
||||
if (form.dataset.membersForm) {
|
||||
emailType = form.dataset.membersForm;
|
||||
}
|
||||
|
||||
if (!email.includes('@')) {
|
||||
form.classList.add('invalid')
|
||||
@ -23,7 +28,8 @@ Array.prototype.forEach.call(document.querySelectorAll('form[data-members-form]'
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email
|
||||
email: email,
|
||||
emailType: emailType
|
||||
})
|
||||
}).then(function (res) {
|
||||
form.addEventListener('submit', submitHandler);
|
||||
@ -48,6 +54,18 @@ Array.prototype.forEach.call(document.querySelectorAll('[data-members-plan]'), f
|
||||
event.preventDefault();
|
||||
|
||||
var plan = el.dataset.membersPlan;
|
||||
var successUrl = el.dataset.membersSuccess;
|
||||
var cancelUrl = el.dataset.membersCancel;
|
||||
var checkoutSuccessUrl;
|
||||
var checkoutCancelUrl;
|
||||
|
||||
if (successUrl) {
|
||||
checkoutSuccessUrl = (new URL(successUrl, window.location.href)).href;
|
||||
}
|
||||
|
||||
if (cancelUrl) {
|
||||
checkoutCancelUrl = (new URL(cancelUrl, window.location.href)).href;
|
||||
}
|
||||
|
||||
if (errorEl) {
|
||||
errorEl.innerText = '';
|
||||
@ -57,7 +75,7 @@ Array.prototype.forEach.call(document.querySelectorAll('[data-members-plan]'), f
|
||||
credentials: 'same-origin'
|
||||
}).then(function (res) {
|
||||
if (!res.ok) {
|
||||
throw new Error('Could not get identity token');
|
||||
return null;
|
||||
}
|
||||
return res.text();
|
||||
}).then(function (identity) {
|
||||
@ -68,7 +86,9 @@ Array.prototype.forEach.call(document.querySelectorAll('[data-members-plan]'), f
|
||||
},
|
||||
body: JSON.stringify({
|
||||
plan: plan,
|
||||
identity: identity
|
||||
identity: identity,
|
||||
successUrl: checkoutSuccessUrl,
|
||||
cancelUrl: checkoutCancelUrl
|
||||
})
|
||||
}).then(function (res) {
|
||||
if (!res.ok) {
|
||||
@ -118,19 +138,3 @@ Array.prototype.forEach.call(document.querySelectorAll('[data-members-signout]')
|
||||
}
|
||||
el.addEventListener('click', clickHandler);
|
||||
});
|
||||
|
||||
var magicLinkRegEx = /token=([a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+\.[a-zA-Z0-9_\-]+)/;
|
||||
var match = location.search.match(magicLinkRegEx);
|
||||
var isMagicLink = !!match
|
||||
var token = match && match[1];
|
||||
|
||||
if (isMagicLink) {
|
||||
fetch('{{blog-url}}/members/ssr', {
|
||||
method: 'POST',
|
||||
body: token
|
||||
}).then(function (res) {
|
||||
if (res.ok) {
|
||||
window.location.search = window.location.search.replace(magicLinkRegEx, '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1,117 +1,127 @@
|
||||
// # Mail
|
||||
// Handles sending email for Ghost
|
||||
var _ = require('lodash'),
|
||||
Promise = require('bluebird'),
|
||||
validator = require('validator'),
|
||||
config = require('../../config'),
|
||||
common = require('../../lib/common'),
|
||||
settingsCache = require('../settings/cache'),
|
||||
urlUtils = require('../../lib/url-utils');
|
||||
const _ = require('lodash');
|
||||
const Promise = require('bluebird');
|
||||
const validator = require('validator');
|
||||
const config = require('../../config');
|
||||
const common = require('../../lib/common');
|
||||
const settingsCache = require('../settings/cache');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
|
||||
function GhostMailer() {
|
||||
var nodemailer = require('nodemailer'),
|
||||
transport = config.get('mail') && config.get('mail').transport || 'direct',
|
||||
options = config.get('mail') && _.clone(config.get('mail').options) || {};
|
||||
const helpMessage = common.i18n.t('errors.api.authentication.checkEmailConfigInstructions', {url: 'https://ghost.org/docs/concepts/config/#mail'});
|
||||
const defaultErrorMessage = common.i18n.t('errors.mail.failedSendingEmail.error');
|
||||
|
||||
this.state = {};
|
||||
this.transport = nodemailer.createTransport(transport, options);
|
||||
this.state.usingDirect = transport === 'direct';
|
||||
function getDomain() {
|
||||
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
||||
return domain && domain[1];
|
||||
}
|
||||
|
||||
GhostMailer.prototype.from = function () {
|
||||
var from = config.get('mail') && config.get('mail').from,
|
||||
defaultBlogTitle;
|
||||
function getFromAddress(requestedFromAddress) {
|
||||
const configAddress = config.get('mail') && config.get('mail').from;
|
||||
|
||||
const address = requestedFromAddress || configAddress;
|
||||
// If we don't have a from address at all
|
||||
if (!from) {
|
||||
// Default to ghost@[blog.url]
|
||||
from = 'ghost@' + this.getDomain();
|
||||
if (!address) {
|
||||
// Default to noreply@[blog.url]
|
||||
return getFromAddress(`noreply@${getDomain()}`);
|
||||
}
|
||||
|
||||
// If we do have a from address, and it's just an email
|
||||
if (validator.isEmail(from)) {
|
||||
defaultBlogTitle = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : common.i18n.t('common.mail.title', {domain: this.getDomain()});
|
||||
from = '"' + defaultBlogTitle + '" <' + from + '>';
|
||||
if (validator.isEmail(address)) {
|
||||
const defaultBlogTitle = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : common.i18n.t('common.mail.title', {domain: getDomain()});
|
||||
return `"${defaultBlogTitle}" <${address}>`;
|
||||
}
|
||||
|
||||
return from;
|
||||
};
|
||||
return address;
|
||||
}
|
||||
|
||||
// Moved it to its own module
|
||||
GhostMailer.prototype.getDomain = function () {
|
||||
var domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
|
||||
return domain && domain[1];
|
||||
};
|
||||
|
||||
// Sends an email message enforcing `to` (blog owner) and `from` fields
|
||||
// This assumes that api.settings.read('email') was already done on the API level
|
||||
GhostMailer.prototype.send = function (message) {
|
||||
var self = this,
|
||||
to,
|
||||
help = common.i18n.t('errors.api.authentication.checkEmailConfigInstructions', {url: 'https://ghost.org/docs/concepts/config/#mail'}),
|
||||
errorMessage = common.i18n.t('errors.mail.failedSendingEmail.error');
|
||||
|
||||
// important to clone message as we modify it
|
||||
message = _.clone(message) || {};
|
||||
to = message.to || false;
|
||||
|
||||
if (!(message && message.subject && message.html && message.to)) {
|
||||
return Promise.reject(new common.errors.EmailError({
|
||||
message: common.i18n.t('errors.mail.incompleteMessageData.error'),
|
||||
help: help
|
||||
}));
|
||||
}
|
||||
|
||||
message = _.extend(message, {
|
||||
from: self.from(),
|
||||
to: to,
|
||||
function createMessage(message) {
|
||||
return Object.assign({}, message, {
|
||||
from: getFromAddress(),
|
||||
generateTextFromHTML: true,
|
||||
encoding: 'base64'
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
self.transport.sendMail(message, function (err, response) {
|
||||
if (err) {
|
||||
errorMessage += common.i18n.t('errors.mail.reason', {reason: err.message || err});
|
||||
function createMailError({message, err, ignoreDefaultMessage} = {message: ''}) {
|
||||
const fullErrorMessage = defaultErrorMessage + message;
|
||||
return new common.errors.EmailError({
|
||||
message: ignoreDefaultMessage ? message : fullErrorMessage,
|
||||
err: err,
|
||||
help: helpMessage
|
||||
});
|
||||
}
|
||||
|
||||
return reject(new common.errors.EmailError({
|
||||
message: errorMessage,
|
||||
err: err,
|
||||
help: help
|
||||
}));
|
||||
module.exports = class GhostMailer {
|
||||
constructor() {
|
||||
const nodemailer = require('nodemailer');
|
||||
const transport = config.get('mail') && config.get('mail').transport || 'direct';
|
||||
// nodemailer mutates the options passed to createTransport
|
||||
const options = config.get('mail') && _.clone(config.get('mail').options) || {};
|
||||
|
||||
this.state = {
|
||||
usingDirect: transport === 'direct'
|
||||
};
|
||||
this.transport = nodemailer.createTransport(transport, options);
|
||||
}
|
||||
|
||||
send(message) {
|
||||
if (!(message && message.subject && message.html && message.to)) {
|
||||
return Promise.reject(createMailError({
|
||||
message: common.i18n.t('errors.mail.incompleteMessageData.error'),
|
||||
ignoreDefaultMessage: true
|
||||
}));
|
||||
}
|
||||
|
||||
const messageToSend = createMessage(message);
|
||||
|
||||
return this.sendMail(messageToSend).then((response) => {
|
||||
if (this.transport.transportType === 'DIRECT') {
|
||||
return this.handleDirectTransportResponse(response);
|
||||
}
|
||||
return response;
|
||||
});
|
||||
}
|
||||
|
||||
if (self.transport.transportType !== 'DIRECT') {
|
||||
return resolve(response);
|
||||
}
|
||||
sendMail(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.transport.sendMail(message, (err, response) => {
|
||||
if (err) {
|
||||
reject(createMailError({
|
||||
message: common.i18n.t('errors.mail.reason', {reason: err.message || err}),
|
||||
err
|
||||
}));
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleDirectTransportResponse(response) {
|
||||
return new Promise((resolve, reject) => {
|
||||
response.statusHandler.once('failed', function (data) {
|
||||
if (data.error && data.error.errno === 'ENOTFOUND') {
|
||||
errorMessage += common.i18n.t('errors.mail.noMailServerAtAddress.error', {domain: data.domain});
|
||||
reject(createMailError({
|
||||
message: common.i18n.t('errors.mail.noMailServerAtAddress.error', {domain: data.domain})
|
||||
}));
|
||||
}
|
||||
|
||||
return reject(new common.errors.EmailError({
|
||||
message: errorMessage,
|
||||
help: help
|
||||
}));
|
||||
reject(createMailError());
|
||||
});
|
||||
|
||||
response.statusHandler.once('requeue', function (data) {
|
||||
if (data.error && data.error.message) {
|
||||
errorMessage += common.i18n.t('errors.mail.reason', {reason: data.error.message});
|
||||
reject(createMailError({
|
||||
message: common.i18n.t('errors.mail.reason', {reason: data.error.message})
|
||||
}));
|
||||
}
|
||||
|
||||
return reject(new common.errors.EmailError({
|
||||
message: errorMessage,
|
||||
help: help
|
||||
}));
|
||||
reject(createMailError());
|
||||
});
|
||||
|
||||
response.statusHandler.once('sent', function () {
|
||||
return resolve(common.i18n.t('notices.mail.messageSent'));
|
||||
resolve(common.i18n.t('notices.mail.messageSent'));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = GhostMailer;
|
||||
|
@ -3,12 +3,14 @@ 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');
|
||||
|
||||
function createMember({email}) {
|
||||
function createMember({email, name}) {
|
||||
return models.Member.add({
|
||||
email
|
||||
email,
|
||||
name
|
||||
}).then((member) => {
|
||||
return member.toJSON();
|
||||
});
|
||||
@ -26,6 +28,29 @@ function getMember(data, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
async function setMemberMetadata(member, module, metadata) {
|
||||
if (module !== 'stripe') {
|
||||
return;
|
||||
}
|
||||
await models.Member.edit({
|
||||
stripe_customers: metadata
|
||||
}, {id: member.id, withRelated: ['stripe_customers']});
|
||||
return;
|
||||
}
|
||||
|
||||
async function getMemberMetadata(member, module) {
|
||||
if (module !== 'stripe') {
|
||||
return;
|
||||
}
|
||||
const model = await models.Member.where({id: member.id}).fetch({withRelated: ['stripe_customers']});
|
||||
const metadata = await model.related('stripe_customers');
|
||||
return metadata.toJSON();
|
||||
}
|
||||
|
||||
function updateMember({name}, options) {
|
||||
return models.Member.edit({name}, options);
|
||||
}
|
||||
|
||||
function deleteMember(options) {
|
||||
options = options || {};
|
||||
return models.Member.destroy(options).catch(models.Member.NotFoundError, () => {
|
||||
@ -61,9 +86,6 @@ const ghostMailer = new mail.GhostMailer();
|
||||
|
||||
function getStripePaymentConfig() {
|
||||
const subscriptionSettings = settingsCache.get('members_subscription_settings');
|
||||
if (!subscriptionSettings || subscriptionSettings.isPaid === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stripePaymentProcessor = subscriptionSettings.paymentProcessors.find(
|
||||
paymentProcessor => paymentProcessor.adapter === 'stripe'
|
||||
@ -73,13 +95,27 @@ function getStripePaymentConfig() {
|
||||
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: siteUrl,
|
||||
checkoutCancelUrl: siteUrl,
|
||||
checkoutSuccessUrl: checkoutSuccessUrl.href,
|
||||
checkoutCancelUrl: checkoutCancelUrl.href,
|
||||
webhookHandlerUrl: webhookHandlerUrl.href,
|
||||
product: stripePaymentProcessor.config.product,
|
||||
plans: stripePaymentProcessor.config.plans
|
||||
plans: stripePaymentProcessor.config.plans,
|
||||
appInfo: {
|
||||
name: 'Ghost',
|
||||
partner_id: 'pp_partner_DKmRVtTs4j9pwZ',
|
||||
version: ghostVersion.original,
|
||||
url: 'https://ghost.org/'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -93,9 +129,10 @@ function createApiInstance() {
|
||||
privateKey: settingsCache.get('members_private_key')
|
||||
},
|
||||
auth: {
|
||||
getSigninURL(token) {
|
||||
getSigninURL(token, type) {
|
||||
const signinURL = new URL(siteUrl);
|
||||
signinURL.searchParams.set('token', token);
|
||||
signinURL.searchParams.set('action', type);
|
||||
return signinURL.href;
|
||||
}
|
||||
},
|
||||
@ -107,18 +144,42 @@ function createApiInstance() {
|
||||
}
|
||||
return ghostMailer.send(Object.assign({subject: 'Signin'}, message));
|
||||
}
|
||||
},
|
||||
getText(url, type) {
|
||||
switch (type) {
|
||||
case 'subscribe':
|
||||
return `Click here to confirm your subscription ${url}`;
|
||||
case 'signup':
|
||||
return `Click here to confirm your email address and sign up ${url}`;
|
||||
case 'signin':
|
||||
default:
|
||||
return `Click here to sign in ${url}`;
|
||||
}
|
||||
},
|
||||
getHTML(url, type) {
|
||||
switch (type) {
|
||||
case 'subscribe':
|
||||
return `<a href="${url}">Click here to confirm your subscription</a>`;
|
||||
case 'signup':
|
||||
return `<a href="${url}">Click here to confirm your email address and sign up</a>`;
|
||||
case 'signin':
|
||||
default:
|
||||
return `<a href="${url}">Click here to sign in</a>`;
|
||||
}
|
||||
}
|
||||
},
|
||||
paymentConfig: {
|
||||
stripe: getStripePaymentConfig()
|
||||
},
|
||||
setMemberMetadata,
|
||||
getMemberMetadata,
|
||||
createMember,
|
||||
updateMember,
|
||||
getMember,
|
||||
deleteMember,
|
||||
listMembers
|
||||
listMembers,
|
||||
logger: common.logging
|
||||
});
|
||||
|
||||
membersApiInstance.setLogger(common.logging);
|
||||
|
||||
return membersApiInstance;
|
||||
}
|
||||
|
@ -44,9 +44,7 @@ const membersService = {
|
||||
cookieKeys: [settingsCache.get('theme_session_secret')],
|
||||
cookieName: 'ghost-members-ssr',
|
||||
cookieCacheName: 'ghost-members-ssr-cache',
|
||||
// This is passed as a function so that updates to the instance
|
||||
// are picked up in the ssr module
|
||||
membersApi: () => membersApi
|
||||
getMembersApi: () => membersService.api
|
||||
})
|
||||
};
|
||||
|
||||
|
@ -20,6 +20,7 @@ const notImplemented = function (req, res, next) {
|
||||
tags: ['GET', 'PUT', 'DELETE', 'POST'],
|
||||
users: ['GET'],
|
||||
themes: ['POST', 'PUT'],
|
||||
members: ['GET', 'PUT', 'DELETE', 'POST'],
|
||||
subscribers: ['GET', 'PUT', 'DELETE', 'POST'],
|
||||
config: ['GET'],
|
||||
webhooks: ['POST', 'DELETE'],
|
||||
|
@ -102,7 +102,19 @@ module.exports = function apiRoutes() {
|
||||
|
||||
// ## Members
|
||||
router.get('/members', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.browse));
|
||||
router.post('/members', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.add));
|
||||
|
||||
router.get('/members/csv', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.exportCSV));
|
||||
router.post('/members/csv',
|
||||
shared.middlewares.labs.members,
|
||||
mw.authAdminApi,
|
||||
upload.single('membersfile'),
|
||||
shared.middlewares.validation.upload({type: 'members'}),
|
||||
http(apiCanary.members.importCSV)
|
||||
);
|
||||
|
||||
router.get('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.read));
|
||||
router.put('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.edit));
|
||||
router.del('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiCanary.members.destroy));
|
||||
|
||||
// ## Roles
|
||||
|
@ -18,9 +18,9 @@ module.exports = function setupMembersApiApp() {
|
||||
const siteUrl = new URL(urlUtils.getSiteUrl());
|
||||
apiApp.use(cors(siteUrl.origin));
|
||||
|
||||
// Set up the api endpoints and the gateway
|
||||
// NOTE: this is wrapped in a function to ensure we always go via the getter
|
||||
apiApp.use((req, res, next) => membersService.api(req, res, next));
|
||||
apiApp.post('/send-magic-link', (req, res, next) => membersService.api.middleware.sendMagicLink(req, res, next));
|
||||
apiApp.post('/create-stripe-checkout-session', (req, res, next) => membersService.api.middleware.createCheckoutSession(req, res, next));
|
||||
|
||||
// API error handling
|
||||
apiApp.use(shared.middlewares.errorHandler.resourceNotFound);
|
||||
|
@ -19,6 +19,7 @@ const notImplemented = function (req, res, next) {
|
||||
tags: ['GET', 'PUT', 'DELETE', 'POST'],
|
||||
users: ['GET'],
|
||||
themes: ['POST', 'PUT'],
|
||||
members: ['GET', 'PUT', 'DELETE', 'POST'],
|
||||
subscribers: ['GET', 'PUT', 'DELETE', 'POST'],
|
||||
config: ['GET'],
|
||||
webhooks: ['POST', 'DELETE'],
|
||||
|
@ -102,7 +102,19 @@ module.exports = function apiRoutes() {
|
||||
|
||||
// ## Members
|
||||
router.get('/members', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.browse));
|
||||
router.post('/members', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.add));
|
||||
|
||||
router.get('/members/csv', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.exportCSV));
|
||||
router.post('/members/csv',
|
||||
shared.middlewares.labs.members,
|
||||
mw.authAdminApi,
|
||||
upload.single('membersfile'),
|
||||
shared.middlewares.validation.upload({type: 'members'}),
|
||||
http(apiv2.members.importCSV)
|
||||
);
|
||||
|
||||
router.get('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.read));
|
||||
router.put('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.edit));
|
||||
router.del('/members/:id', shared.middlewares.labs.members, mw.authAdminApi, http(apiv2.members.destroy));
|
||||
|
||||
// ## Roles
|
||||
|
@ -18,9 +18,9 @@ module.exports = function setupMembersApiApp() {
|
||||
const siteUrl = new URL(urlUtils.getSiteUrl());
|
||||
apiApp.use(cors(siteUrl.origin));
|
||||
|
||||
// Set up the api endpoints and the gateway
|
||||
// NOTE: this is wrapped in a function to ensure we always go via the getter
|
||||
apiApp.use((req, res, next) => membersService.api(req, res, next));
|
||||
apiApp.post('/send-magic-link', (req, res, next) => membersService.api.middleware.sendMagicLink(req, res, next));
|
||||
apiApp.post('/create-stripe-checkout-session', (req, res, next) => membersService.api.middleware.createCheckoutSession(req, res, next));
|
||||
|
||||
// API error handling
|
||||
apiApp.use(shared.middlewares.errorHandler.resourceNotFound);
|
||||
|
@ -22,6 +22,7 @@ module.exports = function setupParentApp(options = {}) {
|
||||
// (X-Forwarded-Proto header will be checked, if present)
|
||||
parentApp.enable('trust proxy');
|
||||
|
||||
parentApp.use(shared.middlewares.requestId);
|
||||
parentApp.use(shared.middlewares.logRequest);
|
||||
|
||||
// Register event emmiter on req/res to trigger cache invalidation webhook event
|
||||
|
@ -55,6 +55,10 @@ module.exports = {
|
||||
return require('./pretty-urls');
|
||||
},
|
||||
|
||||
get requestId() {
|
||||
return require('./request-id');
|
||||
},
|
||||
|
||||
get serveFavicon() {
|
||||
return require('./serve-favicon');
|
||||
},
|
||||
|
@ -1,17 +1,13 @@
|
||||
const uuid = require('uuid');
|
||||
const common = require('../../../lib/common');
|
||||
|
||||
/**
|
||||
* @TODO:
|
||||
* - move middleware to ignition?
|
||||
* @TODO: move this middleware to ignition?
|
||||
*/
|
||||
module.exports = function logRequest(req, res, next) {
|
||||
const startTime = Date.now(),
|
||||
requestId = req.get('X-Request-ID') || uuid.v4();
|
||||
const startTime = Date.now();
|
||||
|
||||
function logResponse() {
|
||||
res.responseTime = (Date.now() - startTime) + 'ms';
|
||||
req.requestId = requestId;
|
||||
req.userId = req.user ? (req.user.id ? req.user.id : req.user) : null;
|
||||
|
||||
if (req.err && req.err.statusCode !== 404) {
|
||||
|
18
core/server/web/shared/middlewares/request-id.js
Normal file
18
core/server/web/shared/middlewares/request-id.js
Normal file
@ -0,0 +1,18 @@
|
||||
const uuid = require('uuid');
|
||||
|
||||
/**
|
||||
* @TODO: move this middleware to ignition?
|
||||
*/
|
||||
module.exports = (req, res, next) => {
|
||||
const requestId = req.get('X-Request-ID') || uuid.v4();
|
||||
|
||||
// Set a value for internal use
|
||||
req.requestId = requestId;
|
||||
|
||||
// If the header was set on the request, return it on the response
|
||||
if (req.get('X-Request-ID')) {
|
||||
res.set('X-Request-ID', requestId);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
@ -11,7 +11,8 @@ const apps = require('../../services/apps');
|
||||
const constants = require('../../lib/constants');
|
||||
const storage = require('../../adapters/storage');
|
||||
const urlService = require('../../../frontend/services/url');
|
||||
const urlUtils = require('../../../server/lib/url-utils');
|
||||
const labsService = require('../../services/labs');
|
||||
const urlUtils = require('../../lib/url-utils');
|
||||
const sitemapHandler = require('../../../frontend/services/sitemap/handler');
|
||||
const themeMiddleware = require('../../../frontend/services/themes').middleware;
|
||||
const membersService = require('../../services/members');
|
||||
@ -133,45 +134,60 @@ module.exports = function setupSiteApp(options = {}) {
|
||||
|
||||
// @TODO only loads this stuff if members is enabled
|
||||
// Set req.member & res.locals.member if a cookie is set
|
||||
siteApp.get('/members/ssr', shared.middlewares.labs.members, function (req, res) {
|
||||
membersService.ssr.getIdentityTokenForMemberFromSession(req, res).then((token) => {
|
||||
siteApp.get('/members/ssr', shared.middlewares.labs.members, async function (req, res) {
|
||||
try {
|
||||
const token = await membersService.ssr.getIdentityTokenForMemberFromSession(req, res);
|
||||
res.writeHead(200);
|
||||
res.end(token);
|
||||
}).catch((err) => {
|
||||
} catch (err) {
|
||||
common.logging.warn(err.message);
|
||||
res.writeHead(err.statusCode);
|
||||
res.end(err.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
siteApp.post('/members/ssr', shared.middlewares.labs.members, function (req, res) {
|
||||
membersService.ssr.exchangeTokenForSession(req, res).then(() => {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
}).catch((err) => {
|
||||
common.logging.warn(err.message);
|
||||
res.writeHead(err.statusCode);
|
||||
res.end(err.message);
|
||||
});
|
||||
});
|
||||
siteApp.delete('/members/ssr', shared.middlewares.labs.members, function (req, res) {
|
||||
membersService.ssr.deleteSession(req, res).then(() => {
|
||||
|
||||
siteApp.delete('/members/ssr', shared.middlewares.labs.members, async function (req, res) {
|
||||
try {
|
||||
await membersService.ssr.deleteSession(req, res);
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
}).catch((err) => {
|
||||
} catch (err) {
|
||||
common.logging.warn(err.message);
|
||||
res.writeHead(err.statusCode);
|
||||
res.end(err.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
siteApp.use(function (req, res, next) {
|
||||
membersService.ssr.getMemberDataFromSession(req, res).then((member) => {
|
||||
req.member = member;
|
||||
next();
|
||||
}).catch((err) => {
|
||||
common.logging.warn(err.message);
|
||||
siteApp.post('/members/webhooks/stripe', (req, res, next) => membersService.api.middleware.handleStripeWebhook(req, res, next));
|
||||
siteApp.use(async function (req, res, next) {
|
||||
if (!labsService.isSet('members')) {
|
||||
req.member = null;
|
||||
return next();
|
||||
}
|
||||
try {
|
||||
const member = await membersService.ssr.getMemberDataFromSession(req, res);
|
||||
Object.assign(req, {member});
|
||||
next();
|
||||
});
|
||||
} catch (err) {
|
||||
common.logging.warn(err.message);
|
||||
Object.assign(req, {member: null});
|
||||
next();
|
||||
}
|
||||
});
|
||||
siteApp.use(async function (req, res, next) {
|
||||
if (!labsService.isSet('members')) {
|
||||
return next();
|
||||
}
|
||||
if (!req.url.includes('token=')) {
|
||||
return next();
|
||||
}
|
||||
try {
|
||||
const member = await membersService.ssr.exchangeTokenForSession(req, res);
|
||||
Object.assign(req, {member});
|
||||
next();
|
||||
} catch (err) {
|
||||
common.logging.warn(err.message);
|
||||
return next();
|
||||
}
|
||||
});
|
||||
siteApp.use(function (req, res, next) {
|
||||
res.locals.member = req.member;
|
||||
|
@ -11,6 +11,9 @@
|
||||
"eslint:recommended",
|
||||
"plugin:ghost/test"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
},
|
||||
"rules": {
|
||||
// these rules were were not previously enforced in our custom rules,
|
||||
// they're turned off here because they _are_ enforced in our plugin.
|
||||
|
@ -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(26);
|
||||
Object.keys(jsonResponse.db[0].data).length.should.eql(27);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -152,6 +152,10 @@ describe('Settings API', function () {
|
||||
{
|
||||
key: 'twitter_description',
|
||||
value: 'twitter description'
|
||||
},
|
||||
{
|
||||
key: 'labs',
|
||||
value: '{"subscribers":false,"members":true,"default_content_visibility":"paid"}'
|
||||
}
|
||||
]
|
||||
};
|
||||
@ -216,6 +220,9 @@ describe('Settings API', function () {
|
||||
putBody.settings[12].key.should.eql('twitter_description');
|
||||
should.equal(putBody.settings[12].value, 'twitter description');
|
||||
|
||||
putBody.settings[13].key.should.eql('labs');
|
||||
should.equal(putBody.settings[13].value, '{"subscribers":false,"members":true,"default_content_visibility":"paid"}');
|
||||
|
||||
localUtils.API.checkResponse(putBody, 'settings');
|
||||
done();
|
||||
});
|
||||
|
@ -28,7 +28,6 @@ const expectedProperties = {
|
||||
.keys()
|
||||
// by default we only return mobildoc
|
||||
.without('html', 'plaintext')
|
||||
.without('visibility')
|
||||
.without('locale')
|
||||
.without('page')
|
||||
// v2 API doesn't return new type field
|
||||
@ -49,7 +48,6 @@ const expectedProperties = {
|
||||
.keys()
|
||||
// by default we only return mobildoc
|
||||
.without('html', 'plaintext')
|
||||
.without('visibility')
|
||||
.without('locale')
|
||||
.without('page')
|
||||
// v2 API doesn't return new type field
|
||||
|
@ -83,7 +83,7 @@ describe('Posts Content API', function () {
|
||||
should.exist(urlParts.host);
|
||||
|
||||
res.body.posts[7].slug.should.eql('not-so-short-bit-complex');
|
||||
res.body.posts[7].html.should.match(/<a href="\/about#nowhere" title="Relative URL/);
|
||||
res.body.posts[7].html.should.match(/<a href="http:\/\/127.0.0.1:2369\/about#nowhere" title="Relative URL/);
|
||||
res.body.posts[9].slug.should.eql('ghostly-kitchen-sink');
|
||||
res.body.posts[9].html.should.match(/<img src="http:\/\/127.0.0.1:2369\/content\/images\/lol.jpg"/);
|
||||
|
||||
|
255
core/test/regression/api/canary/admin/members_spec.js
Normal file
255
core/test/regression/api/canary/admin/members_spec.js
Normal file
@ -0,0 +1,255 @@
|
||||
const path = require('path');
|
||||
const should = require('should');
|
||||
const supertest = require('supertest');
|
||||
const sinon = require('sinon');
|
||||
const testUtils = require('../../../../utils');
|
||||
const localUtils = require('./utils');
|
||||
const config = require('../../../../../server/config');
|
||||
const labs = require('../../../../../server/services/labs');
|
||||
|
||||
const ghost = testUtils.startGhost;
|
||||
|
||||
let request;
|
||||
|
||||
describe('Members API', function () {
|
||||
before(function () {
|
||||
sinon.stub(labs, 'isSet').withArgs('members').returns(true);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function () {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request, 'member');
|
||||
});
|
||||
});
|
||||
|
||||
it('Can browse', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery('members/'))
|
||||
.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.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member');
|
||||
|
||||
testUtils.API.isISO8601(jsonResponse.members[0].created_at).should.be.true();
|
||||
jsonResponse.members[0].created_at.should.be.an.instanceof(String);
|
||||
|
||||
jsonResponse.meta.pagination.should.have.property('page', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('limit', 15);
|
||||
jsonResponse.meta.pagination.should.have.property('pages', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('total', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('next', null);
|
||||
jsonResponse.meta.pagination.should.have.property('prev', null);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can read', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`members/${testUtils.DataGenerator.Content.members[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.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'stripe');
|
||||
});
|
||||
});
|
||||
|
||||
it('Can add', function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestAdd@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
|
||||
jsonResponse.members[0].name.should.equal(member.name);
|
||||
jsonResponse.members[0].email.should.equal(member.email);
|
||||
})
|
||||
.then(() => {
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(422);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail when passing incorrect email_type query parameter', function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestAdd@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/?send_email=true&email_type=lel`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(422);
|
||||
});
|
||||
|
||||
it('Can edit by id', function () {
|
||||
const memberToChange = {
|
||||
name: 'change me',
|
||||
email: 'member2Change@test.com'
|
||||
};
|
||||
|
||||
const memberChanged = {
|
||||
name: 'changed',
|
||||
email: 'cantChangeMe@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [memberToChange]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
|
||||
return jsonResponse.members[0];
|
||||
})
|
||||
.then((newMember) => {
|
||||
return request
|
||||
.put(localUtils.API.getApiQuery(`members/${newMember.id}/`))
|
||||
.send({members: [memberChanged]})
|
||||
.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.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member');
|
||||
jsonResponse.members[0].name.should.equal(memberChanged.name);
|
||||
jsonResponse.members[0].email.should.not.equal(memberChanged.email);
|
||||
jsonResponse.members[0].email.should.equal(memberToChange.email);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Can destroy', function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestDestroy@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
|
||||
const jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
|
||||
return jsonResponse.members[0];
|
||||
})
|
||||
.then((newMember) => {
|
||||
return request
|
||||
.delete(localUtils.API.getApiQuery(`members/${newMember.id}`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(204)
|
||||
.then(() => newMember);
|
||||
})
|
||||
.then((newMember) => {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`members/${newMember.id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can export CSV', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`members/csv/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /text\/csv/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.headers['content-disposition'].should.match(/Attachment;\sfilename="members/);
|
||||
res.text.should.match(/id,email,name,created_at,deleted_at/);
|
||||
res.text.should.match(/member1@test.com/);
|
||||
res.text.should.match(/Mr Egg/);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can import CSV', function () {
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/csv/`))
|
||||
.attach('membersfile', path.join(__dirname, '/../../../../utils/fixtures/csv/valid-members-import.csv'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.meta);
|
||||
should.exist(jsonResponse.meta.stats);
|
||||
|
||||
jsonResponse.meta.stats.imported.should.equal(2);
|
||||
jsonResponse.meta.stats.duplicates.should.equal(0);
|
||||
jsonResponse.meta.stats.invalid.should.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
@ -342,6 +342,32 @@ describe('Posts API', function () {
|
||||
res.body.posts[0].slug.should.equal('this-is-invisible');
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts visibility parameter', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
return request
|
||||
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({
|
||||
posts: [{
|
||||
visibility: 'members',
|
||||
updated_at: res.body.posts[0].updated_at
|
||||
}]
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200);
|
||||
})
|
||||
.then((res) => {
|
||||
should.exist(res.body.posts);
|
||||
should.exist(res.body.posts[0].visibility);
|
||||
res.body.posts[0].visibility.should.equal('members');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Destroy', function () {
|
||||
|
@ -22,7 +22,6 @@ const expectedProperties = {
|
||||
.keys()
|
||||
// by default we only return mobiledoc
|
||||
.without('html', 'plaintext')
|
||||
.without('visibility')
|
||||
.without('locale')
|
||||
.without('page')
|
||||
.without('author_id', 'author')
|
||||
@ -54,6 +53,9 @@ const expectedProperties = {
|
||||
subscriber: _(schema.subscribers)
|
||||
.keys()
|
||||
,
|
||||
member: _(schema.members)
|
||||
.keys()
|
||||
,
|
||||
role: _(schema.roles)
|
||||
.keys()
|
||||
,
|
||||
|
255
core/test/regression/api/v2/admin/members_spec.js
Normal file
255
core/test/regression/api/v2/admin/members_spec.js
Normal file
@ -0,0 +1,255 @@
|
||||
const path = require('path');
|
||||
const should = require('should');
|
||||
const supertest = require('supertest');
|
||||
const sinon = require('sinon');
|
||||
const testUtils = require('../../../../utils');
|
||||
const localUtils = require('./utils');
|
||||
const config = require('../../../../../server/config');
|
||||
const labs = require('../../../../../server/services/labs');
|
||||
|
||||
const ghost = testUtils.startGhost;
|
||||
|
||||
let request;
|
||||
|
||||
describe('Members API', function () {
|
||||
before(function () {
|
||||
sinon.stub(labs, 'isSet').withArgs('members').returns(true);
|
||||
});
|
||||
|
||||
after(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
before(function () {
|
||||
return ghost()
|
||||
.then(function () {
|
||||
request = supertest.agent(config.get('url'));
|
||||
})
|
||||
.then(function () {
|
||||
return localUtils.doAuth(request, 'member');
|
||||
});
|
||||
});
|
||||
|
||||
it('Can browse', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery('members/'))
|
||||
.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.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member');
|
||||
|
||||
testUtils.API.isISO8601(jsonResponse.members[0].created_at).should.be.true();
|
||||
jsonResponse.members[0].created_at.should.be.an.instanceof(String);
|
||||
|
||||
jsonResponse.meta.pagination.should.have.property('page', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('limit', 15);
|
||||
jsonResponse.meta.pagination.should.have.property('pages', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('total', 1);
|
||||
jsonResponse.meta.pagination.should.have.property('next', null);
|
||||
jsonResponse.meta.pagination.should.have.property('prev', null);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can read', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`members/${testUtils.DataGenerator.Content.members[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.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member', 'stripe');
|
||||
});
|
||||
});
|
||||
|
||||
it('Can add', function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestAdd@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
|
||||
jsonResponse.members[0].name.should.equal(member.name);
|
||||
jsonResponse.members[0].email.should.equal(member.email);
|
||||
})
|
||||
.then(() => {
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(422);
|
||||
});
|
||||
});
|
||||
|
||||
it('Should fail when passing incorrect email_type query parameter', function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestAdd@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/?send_email=true&email_type=lel`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(422);
|
||||
});
|
||||
|
||||
it('Can edit by id', function () {
|
||||
const memberToChange = {
|
||||
name: 'change me',
|
||||
email: 'member2Change@test.com'
|
||||
};
|
||||
|
||||
const memberChanged = {
|
||||
name: 'changed',
|
||||
email: 'cantChangeMe@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [memberToChange]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
|
||||
return jsonResponse.members[0];
|
||||
})
|
||||
.then((newMember) => {
|
||||
return request
|
||||
.put(localUtils.API.getApiQuery(`members/${newMember.id}/`))
|
||||
.send({members: [memberChanged]})
|
||||
.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.members);
|
||||
jsonResponse.members.should.have.length(1);
|
||||
localUtils.API.checkResponse(jsonResponse.members[0], 'member');
|
||||
jsonResponse.members[0].name.should.equal(memberChanged.name);
|
||||
jsonResponse.members[0].email.should.not.equal(memberChanged.email);
|
||||
jsonResponse.members[0].email.should.equal(memberToChange.email);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('Can destroy', function () {
|
||||
const member = {
|
||||
name: 'test',
|
||||
email: 'memberTestDestroy@test.com'
|
||||
};
|
||||
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/`))
|
||||
.send({members: [member]})
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
|
||||
const jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.members);
|
||||
|
||||
return jsonResponse.members[0];
|
||||
})
|
||||
.then((newMember) => {
|
||||
return request
|
||||
.delete(localUtils.API.getApiQuery(`members/${newMember.id}`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(204)
|
||||
.then(() => newMember);
|
||||
})
|
||||
.then((newMember) => {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`members/${newMember.id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can export CSV', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`members/csv/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /text\/csv/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
res.headers['content-disposition'].should.match(/Attachment;\sfilename="members/);
|
||||
res.text.should.match(/id,email,name,created_at,deleted_at/);
|
||||
res.text.should.match(/member1@test.com/);
|
||||
res.text.should.match(/Mr Egg/);
|
||||
});
|
||||
});
|
||||
|
||||
it('Can import CSV', function () {
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery(`members/csv/`))
|
||||
.attach('membersfile', path.join(__dirname, '/../../../../utils/fixtures/csv/valid-members-import.csv'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(201)
|
||||
.then((res) => {
|
||||
should.not.exist(res.headers['x-cache-invalidate']);
|
||||
const jsonResponse = res.body;
|
||||
|
||||
should.exist(jsonResponse);
|
||||
should.exist(jsonResponse.meta);
|
||||
should.exist(jsonResponse.meta.stats);
|
||||
|
||||
jsonResponse.meta.stats.imported.should.equal(2);
|
||||
jsonResponse.meta.stats.duplicates.should.equal(0);
|
||||
jsonResponse.meta.stats.invalid.should.equal(0);
|
||||
});
|
||||
});
|
||||
});
|
@ -342,6 +342,32 @@ describe('Posts API', function () {
|
||||
res.body.posts[0].slug.should.equal('this-is-invisible');
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts visibility parameter', function () {
|
||||
return request
|
||||
.get(localUtils.API.getApiQuery(`posts/${testUtils.DataGenerator.Content.posts[0].id}/`))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect(200)
|
||||
.then((res) => {
|
||||
return request
|
||||
.put(localUtils.API.getApiQuery('posts/' + testUtils.DataGenerator.Content.posts[0].id + '/'))
|
||||
.set('Origin', config.get('url'))
|
||||
.send({
|
||||
posts: [{
|
||||
visibility: 'members',
|
||||
updated_at: res.body.posts[0].updated_at
|
||||
}]
|
||||
})
|
||||
.expect('Content-Type', /json/)
|
||||
.expect('Cache-Control', testUtils.cacheRules.private)
|
||||
.expect(200);
|
||||
})
|
||||
.then((res) => {
|
||||
should.exist(res.body.posts);
|
||||
should.exist(res.body.posts[0].visibility);
|
||||
res.body.posts[0].visibility.should.equal('members');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Destroy', function () {
|
||||
|
@ -53,6 +53,9 @@ const expectedProperties = {
|
||||
subscriber: _(schema.subscribers)
|
||||
.keys()
|
||||
,
|
||||
member: _(schema.members)
|
||||
.keys()
|
||||
,
|
||||
role: _(schema.roles)
|
||||
.keys()
|
||||
,
|
||||
|
@ -8,6 +8,7 @@ var should = require('should'),
|
||||
urlService = require('../../../frontend/services/url'),
|
||||
ghostBookshelf = require('../../../server/models/base'),
|
||||
models = require('../../../server/models'),
|
||||
settingsCache = require('../../../server/services/settings/cache'),
|
||||
common = require('../../../server/lib/common'),
|
||||
configUtils = require('../../utils/configUtils'),
|
||||
DataGenerator = testUtils.DataGenerator,
|
||||
@ -731,6 +732,81 @@ describe('Post Model', function () {
|
||||
(!!createdPost.get('page')).should.equal(false);
|
||||
|
||||
should.equal(createdPost.get('locale'), null);
|
||||
should.equal(createdPost.get('visibility'), 'public');
|
||||
|
||||
// testing for nulls
|
||||
(createdPost.get('feature_image') === null).should.equal(true);
|
||||
|
||||
createdPost.get('created_at').should.be.above(new Date(0).getTime());
|
||||
createdPost.get('created_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
||||
createdPost.get('author_id').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
||||
createdPost.has('author').should.equal(false);
|
||||
createdPost.get('created_by').should.equal(createdPost.get('author_id'));
|
||||
createdPost.get('updated_at').should.be.above(new Date(0).getTime());
|
||||
createdPost.get('updated_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
||||
should.equal(createdPost.get('published_at'), null);
|
||||
should.equal(createdPost.get('published_by'), null);
|
||||
|
||||
createdPostUpdatedDate = createdPost.get('updated_at');
|
||||
|
||||
Object.keys(eventsTriggered).length.should.eql(2);
|
||||
should.exist(eventsTriggered['post.added']);
|
||||
should.exist(eventsTriggered['user.attached']);
|
||||
|
||||
// Set the status to published to check that `published_at` is set.
|
||||
return createdPost.save({status: 'published'}, context);
|
||||
}).then(function (publishedPost) {
|
||||
publishedPost.get('published_at').should.be.instanceOf(Date);
|
||||
publishedPost.get('published_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
||||
publishedPost.get('updated_at').should.be.instanceOf(Date);
|
||||
publishedPost.get('updated_by').should.equal(testUtils.DataGenerator.Content.users[0].id);
|
||||
publishedPost.get('updated_at').should.not.equal(createdPostUpdatedDate);
|
||||
|
||||
Object.keys(eventsTriggered).length.should.eql(4);
|
||||
should.exist(eventsTriggered['post.published']);
|
||||
should.exist(eventsTriggered['post.edited']);
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('can add, default visibility is taken from settings cache', function (done) {
|
||||
var originalSettingsCacheGetFn = settingsCache.get;
|
||||
sinon.stub(settingsCache, 'get')
|
||||
.callsFake(function (key, options) {
|
||||
if (key === 'labs') {
|
||||
return {
|
||||
members: true
|
||||
};
|
||||
} else if (key === 'default_content_visibility') {
|
||||
return 'paid';
|
||||
}
|
||||
|
||||
return originalSettingsCacheGetFn(key, options);
|
||||
});
|
||||
|
||||
var createdPostUpdatedDate,
|
||||
newPost = testUtils.DataGenerator.forModel.posts[2],
|
||||
newPostDB = testUtils.DataGenerator.Content.posts[2];
|
||||
|
||||
models.Post.add(newPost, _.merge({withRelated: ['author']}, context)).then(function (createdPost) {
|
||||
return models.Post.findOne({id: createdPost.id, status: 'all'});
|
||||
}).then(function (createdPost) {
|
||||
should.exist(createdPost);
|
||||
createdPost.has('uuid').should.equal(true);
|
||||
createdPost.get('status').should.equal('draft');
|
||||
createdPost.get('title').should.equal(newPost.title, 'title is correct');
|
||||
createdPost.get('mobiledoc').should.equal(newPost.mobiledoc, 'mobiledoc is correct');
|
||||
createdPost.has('html').should.equal(true);
|
||||
createdPost.get('html').should.equal(newPostDB.html);
|
||||
createdPost.has('plaintext').should.equal(true);
|
||||
createdPost.get('plaintext').should.match(/^testing/);
|
||||
// createdPost.get('slug').should.equal(newPostDB.slug + '-3');
|
||||
(!!createdPost.get('featured')).should.equal(false);
|
||||
(!!createdPost.get('page')).should.equal(false);
|
||||
|
||||
should.equal(createdPost.get('locale'), null);
|
||||
should.equal(createdPost.get('visibility'), 'paid');
|
||||
|
||||
// testing for nulls
|
||||
(createdPost.get('feature_image') === null).should.equal(true);
|
||||
@ -1038,6 +1114,44 @@ describe('Post Model', function () {
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
|
||||
it('transforms absolute urls to relative', function (done) {
|
||||
const post = {
|
||||
title: 'Absolute->Relative URL Transform Test',
|
||||
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"http://127.0.0.1:2369/content/images/card.jpg"}]],"markups":[["a",["href","http://127.0.0.1:2369/test"]]],"sections":[[1,"p",[[0,[0],1,"Testing"]]],[10,0]]}',
|
||||
custom_excerpt: 'Testing <a href="http://127.0.0.1:2369/internal">links</a> in custom excerpts',
|
||||
codeinjection_head: '<script src="http://127.0.0.1:2369/assets/head.js"></script>',
|
||||
codeinjection_foot: '<script src="http://127.0.0.1:2369/assets/foot.js"></script>',
|
||||
feature_image: 'http://127.0.0.1:2369/content/images/feature.png',
|
||||
og_image: 'http://127.0.0.1:2369/content/images/og.png',
|
||||
twitter_image: 'http://127.0.0.1:2369/content/images/twitter.png',
|
||||
canonical_url: 'http://127.0.0.1:2369/canonical'
|
||||
};
|
||||
|
||||
models.Post.add(post, context).then((createdPost) => {
|
||||
createdPost.get('mobiledoc').should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/card.jpg"}]],"markups":[["a",["href","/test"]]],"sections":[[1,"p",[[0,[0],1,"Testing"]]],[10,0]]}');
|
||||
createdPost.get('html').should.equal('<p><a href="/test">Testing</a></p><figure class="kg-card kg-image-card"><img src="/content/images/card.jpg" class="kg-image"></figure>');
|
||||
createdPost.get('custom_excerpt').should.equal('Testing <a href="/internal">links</a> in custom excerpts');
|
||||
createdPost.get('codeinjection_head').should.equal('<script src="/assets/head.js"></script>');
|
||||
createdPost.get('codeinjection_foot').should.equal('<script src="/assets/foot.js"></script>');
|
||||
createdPost.get('feature_image').should.equal('/content/images/feature.png');
|
||||
createdPost.get('og_image').should.equal('/content/images/og.png');
|
||||
createdPost.get('twitter_image').should.equal('/content/images/twitter.png');
|
||||
createdPost.get('canonical_url').should.equal('/canonical');
|
||||
|
||||
// ensure canonical_url is not transformed when protocol does not match
|
||||
return createdPost.save({
|
||||
canonical_url: 'https://127.0.0.1:2369/https-internal',
|
||||
// sanity check for general absolute->relative transform during edits
|
||||
feature_image: 'http://127.0.0.1:2369/content/images/updated_feature.png'
|
||||
});
|
||||
}).then((updatedPost) => {
|
||||
updatedPost.get('canonical_url').should.equal('https://127.0.0.1:2369/https-internal');
|
||||
updatedPost.get('feature_image').should.equal('/content/images/updated_feature.png');
|
||||
|
||||
done();
|
||||
}).catch(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', function () {
|
||||
|
@ -221,174 +221,6 @@ describe('Unit: canary/utils/serializers/input/posts', function () {
|
||||
});
|
||||
|
||||
describe('edit', function () {
|
||||
describe('Ensure relative urls are returned for standard image urls', function () {
|
||||
describe('no subdir', function () {
|
||||
let sandbox;
|
||||
|
||||
after(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
before(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
urlUtils.stubUrlUtils({url: 'https://mysite.com'}, sandbox);
|
||||
});
|
||||
|
||||
it('when mobiledoc contains an absolute URL to image', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}]]}'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
|
||||
let postData = frame.data.posts[0];
|
||||
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}]]}');
|
||||
});
|
||||
|
||||
it('when mobiledoc contains multiple absolute URLs to images with different protocols', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}],["image",{"src":"http://mysite.com/content/images/2019/02/image.png"}]]'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
|
||||
let postData = frame.data.posts[0];
|
||||
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}],["image",{"src":"/content/images/2019/02/image.png"}]]');
|
||||
});
|
||||
|
||||
it('when blog url is without subdir', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
},
|
||||
withRelated: ['tags', 'authors']
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
feature_image: 'https://mysite.com/content/images/image.jpg',
|
||||
og_image: 'https://mysite.com/mycustomstorage/images/image.jpg',
|
||||
twitter_image: 'https://mysite.com/blog/content/images/image.jpg',
|
||||
tags: [{
|
||||
id: 'id3',
|
||||
feature_image: 'http://mysite.com/content/images/image.jpg'
|
||||
}],
|
||||
authors: [{
|
||||
id: 'id4',
|
||||
name: 'Ghosty',
|
||||
profile_image: 'https://somestorage.com/blog/images/image.jpg'
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
let postData = frame.data.posts[0];
|
||||
postData.feature_image.should.eql('/content/images/image.jpg');
|
||||
postData.og_image.should.eql('https://mysite.com/mycustomstorage/images/image.jpg');
|
||||
postData.twitter_image.should.eql('https://mysite.com/blog/content/images/image.jpg');
|
||||
postData.tags[0].feature_image.should.eql('/content/images/image.jpg');
|
||||
postData.authors[0].profile_image.should.eql('https://somestorage.com/blog/images/image.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with subdir', function () {
|
||||
let sandbox;
|
||||
|
||||
after(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
before(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
urlUtils.stubUrlUtils({url: 'https://mysite.com/blog'}, sandbox);
|
||||
});
|
||||
|
||||
it('when blog url is with subdir', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
},
|
||||
withRelated: ['tags', 'authors']
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
feature_image: 'https://mysite.com/blog/content/images/image.jpg',
|
||||
og_image: 'https://mysite.com/content/images/image.jpg',
|
||||
twitter_image: 'https://mysite.com/mycustomstorage/images/image.jpg',
|
||||
tags: [{
|
||||
id: 'id3',
|
||||
feature_image: 'http://mysite.com/blog/mycustomstorage/content/images/image.jpg'
|
||||
}],
|
||||
authors: [{
|
||||
id: 'id4',
|
||||
name: 'Ghosty',
|
||||
profile_image: 'https://somestorage.com/blog/content/images/image.jpg'
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
let postData = frame.data.posts[0];
|
||||
postData.feature_image.should.eql('/blog/content/images/image.jpg');
|
||||
postData.og_image.should.eql('https://mysite.com/content/images/image.jpg');
|
||||
postData.twitter_image.should.eql('https://mysite.com/mycustomstorage/images/image.jpg');
|
||||
postData.tags[0].feature_image.should.eql('http://mysite.com/blog/mycustomstorage/content/images/image.jpg');
|
||||
postData.authors[0].profile_image.should.eql('https://somestorage.com/blog/content/images/image.jpg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ensure html to mobiledoc conversion', function () {
|
||||
it('no transformation when no html source option provided', function () {
|
||||
const apiConfig = {};
|
||||
|
@ -0,0 +1,136 @@
|
||||
const should = require('should');
|
||||
const sinon = require('sinon');
|
||||
const labs = require('../../../../../../../../server/services/labs');
|
||||
const members = require('../../../../../../../../server/api/canary/utils/serializers/output/utils/members');
|
||||
|
||||
describe('Unit: canary/utils/serializers/output/utils/members', function () {
|
||||
describe('for post', function () {
|
||||
it('does not modify attributes when members is disabled', function () {
|
||||
const attrs = {
|
||||
plaintext: 'no touching',
|
||||
html: '<p>I am here to stay</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
options: {}
|
||||
};
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('no touching');
|
||||
});
|
||||
|
||||
describe('labs.members enabled', function () {
|
||||
before(function () {
|
||||
sinon.stub(labs, 'isSet').returns(true);
|
||||
});
|
||||
|
||||
it('should NOT hide content attributes when visibility is public', function () {
|
||||
const attrs = {
|
||||
visibility: 'public',
|
||||
plaintext: 'no touching',
|
||||
html: '<p>I am here to stay</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('no touching');
|
||||
});
|
||||
|
||||
it('should hide content attributes when visibility is "members"', function () {
|
||||
const attrs = {
|
||||
visibility: 'members',
|
||||
plaintext: 'no touching. secret stuff',
|
||||
html: '<p>I am here to stay</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('');
|
||||
attrs.html.should.eql('');
|
||||
});
|
||||
|
||||
it('should NOT hide content attributes when visibility is "members" and member is present', function () {
|
||||
const attrs = {
|
||||
visibility: 'members',
|
||||
plaintext: 'I see dead people',
|
||||
html: '<p>What\'s the matter?</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {
|
||||
member: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('I see dead people');
|
||||
attrs.html.should.eql('<p>What\'s the matter?</p>');
|
||||
});
|
||||
|
||||
it('should hide content attributes when visibility is "paid" and member has no subscription', function () {
|
||||
const attrs = {
|
||||
visibility: 'paid',
|
||||
plaintext: 'I see dead people',
|
||||
html: '<p>What\'s the matter?</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {
|
||||
member: {
|
||||
stripe: {
|
||||
subscriptions: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('');
|
||||
attrs.html.should.eql('');
|
||||
});
|
||||
|
||||
it('should NOT hide content attributes when visibility is "paid" and member has a subscription', function () {
|
||||
const attrs = {
|
||||
visibility: 'paid',
|
||||
plaintext: 'Secret paid content',
|
||||
html: '<p>Can read this</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {
|
||||
member: {
|
||||
stripe: {
|
||||
subscriptions: ['I pay money dollaz']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('Secret paid content');
|
||||
attrs.html.should.eql('<p>Can read this</p>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -9,7 +9,9 @@ describe('Unit: canary/utils/serializers/output/utils/url', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(urlService, 'getUrlByResourceId').returns('getUrlByResourceId');
|
||||
sinon.stub(urlUtils, 'urlFor').returns('urlFor');
|
||||
sinon.stub(urlUtils, 'relativeToAbsolute').returns('relativeToAbsolute');
|
||||
sinon.stub(urlUtils, 'htmlRelativeToAbsolute').returns({html: sinon.stub()});
|
||||
sinon.stub(urlUtils, 'mobiledocRelativeToAbsolute').returns({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -28,23 +30,32 @@ describe('Unit: canary/utils/serializers/output/utils/url', function () {
|
||||
it('meta & models & relations', function () {
|
||||
const post = pageModel(testUtils.DataGenerator.forKnex.createPost({
|
||||
id: 'id1',
|
||||
feature_image: 'value'
|
||||
mobiledoc: '{}',
|
||||
html: 'html',
|
||||
custom_excerpt: 'customExcerpt',
|
||||
codeinjection_head: 'codeinjectionHead',
|
||||
codeinjection_foot: 'codeinjectionFoot',
|
||||
feature_image: 'featureImage',
|
||||
og_image: 'ogImage',
|
||||
twitter_image: 'twitterImage',
|
||||
canonical_url: 'canonicalUrl'
|
||||
}));
|
||||
|
||||
urlUtil.forPost(post.id, post, {options: {}});
|
||||
|
||||
post.hasOwnProperty('url').should.be.true();
|
||||
|
||||
urlUtils.urlFor.callCount.should.eql(2);
|
||||
urlUtils.urlFor.getCall(0).args.should.eql(['image', {image: 'value'}, true]);
|
||||
urlUtils.urlFor.getCall(1).args.should.eql(['home', true]);
|
||||
// feature_image, og_image, twitter_image, canonical_url
|
||||
urlUtils.relativeToAbsolute.callCount.should.eql(4);
|
||||
|
||||
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(1);
|
||||
// mobiledoc
|
||||
urlUtils.mobiledocRelativeToAbsolute.callCount.should.eql(1);
|
||||
|
||||
// html, codeinjection_head, codeinjection_foot
|
||||
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(3);
|
||||
urlUtils.htmlRelativeToAbsolute.getCall(0).args.should.eql([
|
||||
'## markdown',
|
||||
'urlFor',
|
||||
'getUrlByResourceId',
|
||||
{assetsOnly: true}
|
||||
'html',
|
||||
'getUrlByResourceId'
|
||||
]);
|
||||
|
||||
urlService.getUrlByResourceId.callCount.should.eql(1);
|
||||
|
@ -13,45 +13,5 @@ describe('Unit: v2/utils/serializers/input/utils/url', function () {
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it('should transform canonical_url when protocol and domain match', function () {
|
||||
const attrs = {
|
||||
canonical_url: 'https://blogurl.com/hello-world'
|
||||
};
|
||||
|
||||
url.forPost(attrs, {});
|
||||
|
||||
should.equal(attrs.canonical_url, '/hello-world');
|
||||
});
|
||||
|
||||
it('should transform canonical_url when protocol and domain match with backslash in the end', function () {
|
||||
const attrs = {
|
||||
canonical_url: 'https://blogurl.com/hello-world/'
|
||||
};
|
||||
|
||||
url.forPost(attrs, {});
|
||||
|
||||
should.equal(attrs.canonical_url, '/hello-world/');
|
||||
});
|
||||
|
||||
it('should not transform canonical_url when different domains', function () {
|
||||
const attrs = {
|
||||
canonical_url: 'http://ghost.org/no-transform'
|
||||
};
|
||||
|
||||
url.forPost(attrs, {});
|
||||
|
||||
should.equal(attrs.canonical_url, 'http://ghost.org/no-transform');
|
||||
});
|
||||
|
||||
it('should not transform canonical_url when different protocols', function () {
|
||||
const attrs = {
|
||||
canonical_url: 'http://blogurl.com/no-transform'
|
||||
};
|
||||
|
||||
url.forPost(attrs, {});
|
||||
|
||||
should.equal(attrs.canonical_url, 'http://blogurl.com/no-transform');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ const {UnauthorizedError} = require('../../../../server/lib/common/errors');
|
||||
const sessionController = require('../../../../server/api/v2/session');
|
||||
const sessionServiceMiddleware = require('../../../../server/services/auth/session/middleware');
|
||||
|
||||
describe('Session controller', function () {
|
||||
describe('v2 Session controller', function () {
|
||||
before(function () {
|
||||
models.init();
|
||||
});
|
||||
|
@ -221,174 +221,6 @@ describe('Unit: v2/utils/serializers/input/posts', function () {
|
||||
});
|
||||
|
||||
describe('edit', function () {
|
||||
describe('Ensure relative urls are returned for standard image urls', function () {
|
||||
describe('no subdir', function () {
|
||||
let sandbox;
|
||||
|
||||
after(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
before(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
urlUtils.stubUrlUtils({url: 'https://mysite.com'}, sandbox);
|
||||
});
|
||||
|
||||
it('when mobiledoc contains an absolute URL to image', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}]]}'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
|
||||
let postData = frame.data.posts[0];
|
||||
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}]]}');
|
||||
});
|
||||
|
||||
it('when mobiledoc contains multiple absolute URLs to images with different protocols', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}],["image",{"src":"http://mysite.com/content/images/2019/02/image.png"}]]'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
|
||||
let postData = frame.data.posts[0];
|
||||
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}],["image",{"src":"/content/images/2019/02/image.png"}]]');
|
||||
});
|
||||
|
||||
it('when blog url is without subdir', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
},
|
||||
withRelated: ['tags', 'authors']
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
feature_image: 'https://mysite.com/content/images/image.jpg',
|
||||
og_image: 'https://mysite.com/mycustomstorage/images/image.jpg',
|
||||
twitter_image: 'https://mysite.com/blog/content/images/image.jpg',
|
||||
tags: [{
|
||||
id: 'id3',
|
||||
feature_image: 'http://mysite.com/content/images/image.jpg'
|
||||
}],
|
||||
authors: [{
|
||||
id: 'id4',
|
||||
name: 'Ghosty',
|
||||
profile_image: 'https://somestorage.com/blog/images/image.jpg'
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
let postData = frame.data.posts[0];
|
||||
postData.feature_image.should.eql('/content/images/image.jpg');
|
||||
postData.og_image.should.eql('https://mysite.com/mycustomstorage/images/image.jpg');
|
||||
postData.twitter_image.should.eql('https://mysite.com/blog/content/images/image.jpg');
|
||||
postData.tags[0].feature_image.should.eql('/content/images/image.jpg');
|
||||
postData.authors[0].profile_image.should.eql('https://somestorage.com/blog/images/image.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with subdir', function () {
|
||||
let sandbox;
|
||||
|
||||
after(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
before(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
urlUtils.stubUrlUtils({url: 'https://mysite.com/blog'}, sandbox);
|
||||
});
|
||||
|
||||
it('when blog url is with subdir', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
},
|
||||
withRelated: ['tags', 'authors']
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
feature_image: 'https://mysite.com/blog/content/images/image.jpg',
|
||||
og_image: 'https://mysite.com/content/images/image.jpg',
|
||||
twitter_image: 'https://mysite.com/mycustomstorage/images/image.jpg',
|
||||
tags: [{
|
||||
id: 'id3',
|
||||
feature_image: 'http://mysite.com/blog/mycustomstorage/content/images/image.jpg'
|
||||
}],
|
||||
authors: [{
|
||||
id: 'id4',
|
||||
name: 'Ghosty',
|
||||
profile_image: 'https://somestorage.com/blog/content/images/image.jpg'
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
let postData = frame.data.posts[0];
|
||||
postData.feature_image.should.eql('/blog/content/images/image.jpg');
|
||||
postData.og_image.should.eql('https://mysite.com/content/images/image.jpg');
|
||||
postData.twitter_image.should.eql('https://mysite.com/mycustomstorage/images/image.jpg');
|
||||
postData.tags[0].feature_image.should.eql('http://mysite.com/blog/mycustomstorage/content/images/image.jpg');
|
||||
postData.authors[0].profile_image.should.eql('https://somestorage.com/blog/content/images/image.jpg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ensure html to mobiledoc conversion', function () {
|
||||
it('no transformation when no html source option provided', function () {
|
||||
const apiConfig = {};
|
||||
|
@ -0,0 +1,136 @@
|
||||
const should = require('should');
|
||||
const sinon = require('sinon');
|
||||
const labs = require('../../../../../../../../server/services/labs');
|
||||
const members = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/members');
|
||||
|
||||
describe('Unit: v2/utils/serializers/output/utils/members', function () {
|
||||
describe('for post', function () {
|
||||
it('does not modify attributes when members is disabled', function () {
|
||||
const attrs = {
|
||||
plaintext: 'no touching',
|
||||
html: '<p>I am here to stay</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
options: {}
|
||||
};
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('no touching');
|
||||
});
|
||||
|
||||
describe('labs.members enabled', function () {
|
||||
before(function () {
|
||||
sinon.stub(labs, 'isSet').returns(true);
|
||||
});
|
||||
|
||||
it('should NOT hide content attributes when visibility is public', function () {
|
||||
const attrs = {
|
||||
visibility: 'public',
|
||||
plaintext: 'no touching',
|
||||
html: '<p>I am here to stay</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('no touching');
|
||||
});
|
||||
|
||||
it('should hide content attributes when visibility is "members"', function () {
|
||||
const attrs = {
|
||||
visibility: 'members',
|
||||
plaintext: 'no touching. secret stuff',
|
||||
html: '<p>I am here to stay</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('');
|
||||
attrs.html.should.eql('');
|
||||
});
|
||||
|
||||
it('should NOT hide content attributes when visibility is "members" and member is present', function () {
|
||||
const attrs = {
|
||||
visibility: 'members',
|
||||
plaintext: 'I see dead people',
|
||||
html: '<p>What\'s the matter?</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {
|
||||
member: {}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('I see dead people');
|
||||
attrs.html.should.eql('<p>What\'s the matter?</p>');
|
||||
});
|
||||
|
||||
it('should hide content attributes when visibility is "paid" and member has no subscription', function () {
|
||||
const attrs = {
|
||||
visibility: 'paid',
|
||||
plaintext: 'I see dead people',
|
||||
html: '<p>What\'s the matter?</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {
|
||||
member: {
|
||||
stripe: {
|
||||
subscriptions: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('');
|
||||
attrs.html.should.eql('');
|
||||
});
|
||||
|
||||
it('should NOT hide content attributes when visibility is "paid" and member has a subscription', function () {
|
||||
const attrs = {
|
||||
visibility: 'paid',
|
||||
plaintext: 'Secret paid content',
|
||||
html: '<p>Can read this</p>'
|
||||
};
|
||||
|
||||
const frame = {
|
||||
original: {
|
||||
context: {
|
||||
member: {
|
||||
stripe: {
|
||||
subscriptions: ['I pay money dollaz']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
members.forPost(attrs, frame);
|
||||
|
||||
attrs.plaintext.should.eql('Secret paid content');
|
||||
attrs.html.should.eql('<p>Can read this</p>');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -9,7 +9,9 @@ describe('Unit: v2/utils/serializers/output/utils/url', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(urlService, 'getUrlByResourceId').returns('getUrlByResourceId');
|
||||
sinon.stub(urlUtils, 'urlFor').returns('urlFor');
|
||||
sinon.stub(urlUtils, 'relativeToAbsolute').returns('relativeToAbsolute');
|
||||
sinon.stub(urlUtils, 'htmlRelativeToAbsolute').returns({html: sinon.stub()});
|
||||
sinon.stub(urlUtils, 'mobiledocRelativeToAbsolute').returns({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -28,21 +30,31 @@ describe('Unit: v2/utils/serializers/output/utils/url', function () {
|
||||
it('meta & models & relations', function () {
|
||||
const post = pageModel(testUtils.DataGenerator.forKnex.createPost({
|
||||
id: 'id1',
|
||||
feature_image: 'value'
|
||||
mobiledoc: '{}',
|
||||
html: 'html',
|
||||
custom_excerpt: 'customExcerpt',
|
||||
codeinjection_head: 'codeinjectionHead',
|
||||
codeinjection_foot: 'codeinjectionFoot',
|
||||
feature_image: 'featureImage',
|
||||
og_image: 'ogImage',
|
||||
twitter_image: 'twitterImage',
|
||||
canonical_url: 'canonicalUrl'
|
||||
}));
|
||||
|
||||
urlUtil.forPost(post.id, post, {options: {}});
|
||||
|
||||
post.hasOwnProperty('url').should.be.true();
|
||||
|
||||
urlUtils.urlFor.callCount.should.eql(2);
|
||||
urlUtils.urlFor.getCall(0).args.should.eql(['image', {image: 'value'}, true]);
|
||||
urlUtils.urlFor.getCall(1).args.should.eql(['home', true]);
|
||||
// feature_image, og_image, twitter_image, canonical_url
|
||||
urlUtils.relativeToAbsolute.callCount.should.eql(4);
|
||||
|
||||
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(1);
|
||||
// mobiledoc
|
||||
urlUtils.mobiledocRelativeToAbsolute.callCount.should.eql(1);
|
||||
|
||||
// html, codeinjection_head, codeinjection_foot
|
||||
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(3);
|
||||
urlUtils.htmlRelativeToAbsolute.getCall(0).args.should.eql([
|
||||
'## markdown',
|
||||
'urlFor',
|
||||
'html',
|
||||
'getUrlByResourceId',
|
||||
{assetsOnly: true}
|
||||
]);
|
||||
|
@ -7,7 +7,7 @@ const {UnauthorizedError} = require('../../../../server/lib/common/errors');
|
||||
const sessionController = require('../../../../server/api/canary/session');
|
||||
const sessionServiceMiddleware = require('../../../../server/services/auth/session/middleware');
|
||||
|
||||
describe('Session controller', function () {
|
||||
describe('v3 Session controller', function () {
|
||||
before(function () {
|
||||
models.init();
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
const should = require('should');
|
||||
const serializers = require('../../../../../../../server/api/v2/utils/serializers');
|
||||
|
||||
describe('Unit: v2/utils/serializers/input/pages', function () {
|
||||
describe('Unit: v3/utils/serializers/input/pages', function () {
|
||||
describe('browse', function () {
|
||||
it('default', function () {
|
||||
const apiConfig = {};
|
||||
|
@ -3,7 +3,7 @@ const sinon = require('sinon');
|
||||
const serializers = require('../../../../../../../server/api/v2/utils/serializers');
|
||||
const urlUtils = require('../../../../../../utils/urlUtils');
|
||||
|
||||
describe('Unit: v2/utils/serializers/input/posts', function () {
|
||||
describe('Unit: v3/utils/serializers/input/posts', function () {
|
||||
describe('browse', function () {
|
||||
it('default', function () {
|
||||
const apiConfig = {};
|
||||
@ -221,174 +221,6 @@ describe('Unit: v2/utils/serializers/input/posts', function () {
|
||||
});
|
||||
|
||||
describe('edit', function () {
|
||||
describe('Ensure relative urls are returned for standard image urls', function () {
|
||||
describe('no subdir', function () {
|
||||
let sandbox;
|
||||
|
||||
after(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
before(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
urlUtils.stubUrlUtils({url: 'https://mysite.com'}, sandbox);
|
||||
});
|
||||
|
||||
it('when mobiledoc contains an absolute URL to image', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}]]}'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
|
||||
let postData = frame.data.posts[0];
|
||||
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}]]}');
|
||||
});
|
||||
|
||||
it('when mobiledoc contains multiple absolute URLs to images with different protocols', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
}
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
mobiledoc: '{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"https://mysite.com/content/images/2019/02/image.jpg"}],["image",{"src":"http://mysite.com/content/images/2019/02/image.png"}]]'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
|
||||
let postData = frame.data.posts[0];
|
||||
postData.mobiledoc.should.equal('{"version":"0.3.1","atoms":[],"cards":[["image",{"src":"/content/images/2019/02/image.jpg"}],["image",{"src":"/content/images/2019/02/image.png"}]]');
|
||||
});
|
||||
|
||||
it('when blog url is without subdir', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
},
|
||||
withRelated: ['tags', 'authors']
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
feature_image: 'https://mysite.com/content/images/image.jpg',
|
||||
og_image: 'https://mysite.com/mycustomstorage/images/image.jpg',
|
||||
twitter_image: 'https://mysite.com/blog/content/images/image.jpg',
|
||||
tags: [{
|
||||
id: 'id3',
|
||||
feature_image: 'http://mysite.com/content/images/image.jpg'
|
||||
}],
|
||||
authors: [{
|
||||
id: 'id4',
|
||||
name: 'Ghosty',
|
||||
profile_image: 'https://somestorage.com/blog/images/image.jpg'
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
let postData = frame.data.posts[0];
|
||||
postData.feature_image.should.eql('/content/images/image.jpg');
|
||||
postData.og_image.should.eql('https://mysite.com/mycustomstorage/images/image.jpg');
|
||||
postData.twitter_image.should.eql('https://mysite.com/blog/content/images/image.jpg');
|
||||
postData.tags[0].feature_image.should.eql('/content/images/image.jpg');
|
||||
postData.authors[0].profile_image.should.eql('https://somestorage.com/blog/images/image.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with subdir', function () {
|
||||
let sandbox;
|
||||
|
||||
after(function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
before(function () {
|
||||
sandbox = sinon.createSandbox();
|
||||
urlUtils.stubUrlUtils({url: 'https://mysite.com/blog'}, sandbox);
|
||||
});
|
||||
|
||||
it('when blog url is with subdir', function () {
|
||||
const apiConfig = {};
|
||||
const frame = {
|
||||
options: {
|
||||
context: {
|
||||
user: 0,
|
||||
api_key: {
|
||||
id: 1,
|
||||
type: 'content'
|
||||
}
|
||||
},
|
||||
withRelated: ['tags', 'authors']
|
||||
},
|
||||
data: {
|
||||
posts: [
|
||||
{
|
||||
id: 'id1',
|
||||
feature_image: 'https://mysite.com/blog/content/images/image.jpg',
|
||||
og_image: 'https://mysite.com/content/images/image.jpg',
|
||||
twitter_image: 'https://mysite.com/mycustomstorage/images/image.jpg',
|
||||
tags: [{
|
||||
id: 'id3',
|
||||
feature_image: 'http://mysite.com/blog/mycustomstorage/content/images/image.jpg'
|
||||
}],
|
||||
authors: [{
|
||||
id: 'id4',
|
||||
name: 'Ghosty',
|
||||
profile_image: 'https://somestorage.com/blog/content/images/image.jpg'
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
serializers.input.posts.edit(apiConfig, frame);
|
||||
let postData = frame.data.posts[0];
|
||||
postData.feature_image.should.eql('/blog/content/images/image.jpg');
|
||||
postData.og_image.should.eql('https://mysite.com/content/images/image.jpg');
|
||||
postData.twitter_image.should.eql('https://mysite.com/mycustomstorage/images/image.jpg');
|
||||
postData.tags[0].feature_image.should.eql('http://mysite.com/blog/mycustomstorage/content/images/image.jpg');
|
||||
postData.authors[0].profile_image.should.eql('https://somestorage.com/blog/content/images/image.jpg');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ensure html to mobiledoc conversion', function () {
|
||||
it('no transformation when no html source option provided', function () {
|
||||
const apiConfig = {};
|
||||
|
@ -3,13 +3,15 @@ const sinon = require('sinon');
|
||||
const testUtils = require('../../../../../../../utils');
|
||||
const urlService = require('../../../../../../../../frontend/services/url');
|
||||
const urlUtils = require('../../../../../../../../server/lib/url-utils');
|
||||
const urlUtil = require('../../../../../../../../server/api/canary/utils/serializers/output/utils/url');
|
||||
const urlUtil = require('../../../../../../../../server/api/v2/utils/serializers/output/utils/url');
|
||||
|
||||
describe('Unit: v3/utils/serializers/output/utils/url', function () {
|
||||
beforeEach(function () {
|
||||
sinon.stub(urlService, 'getUrlByResourceId').returns('getUrlByResourceId');
|
||||
sinon.stub(urlUtils, 'urlFor').returns('urlFor');
|
||||
sinon.stub(urlUtils, 'relativeToAbsolute').returns('relativeToAbsolute');
|
||||
sinon.stub(urlUtils, 'htmlRelativeToAbsolute').returns({html: sinon.stub()});
|
||||
sinon.stub(urlUtils, 'mobiledocRelativeToAbsolute').returns({});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
@ -28,21 +30,31 @@ describe('Unit: v3/utils/serializers/output/utils/url', function () {
|
||||
it('meta & models & relations', function () {
|
||||
const post = pageModel(testUtils.DataGenerator.forKnex.createPost({
|
||||
id: 'id1',
|
||||
feature_image: 'value'
|
||||
mobiledoc: '{}',
|
||||
html: 'html',
|
||||
custom_excerpt: 'customExcerpt',
|
||||
codeinjection_head: 'codeinjectionHead',
|
||||
codeinjection_foot: 'codeinjectionFoot',
|
||||
feature_image: 'featureImage',
|
||||
og_image: 'ogImage',
|
||||
twitter_image: 'twitterImage',
|
||||
canonical_url: 'canonicalUrl'
|
||||
}));
|
||||
|
||||
urlUtil.forPost(post.id, post, {options: {}});
|
||||
|
||||
post.hasOwnProperty('url').should.be.true();
|
||||
|
||||
urlUtils.urlFor.callCount.should.eql(2);
|
||||
urlUtils.urlFor.getCall(0).args.should.eql(['image', {image: 'value'}, true]);
|
||||
urlUtils.urlFor.getCall(1).args.should.eql(['home', true]);
|
||||
// feature_image, og_image, twitter_image, canonical_url
|
||||
urlUtils.relativeToAbsolute.callCount.should.eql(4);
|
||||
|
||||
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(1);
|
||||
// mobiledoc
|
||||
urlUtils.mobiledocRelativeToAbsolute.callCount.should.eql(1);
|
||||
|
||||
// html, codeinjection_head, codeinjection_foot
|
||||
urlUtils.htmlRelativeToAbsolute.callCount.should.eql(3);
|
||||
urlUtils.htmlRelativeToAbsolute.getCall(0).args.should.eql([
|
||||
'## markdown',
|
||||
'urlFor',
|
||||
'html',
|
||||
'getUrlByResourceId',
|
||||
{assetsOnly: true}
|
||||
]);
|
||||
|
@ -19,7 +19,7 @@ var should = require('should'),
|
||||
*/
|
||||
describe('DB version integrity', function () {
|
||||
// Only these variables should need updating
|
||||
const currentSchemaHash = '6d99c4ed8317240d392eb2046df10368';
|
||||
const currentSchemaHash = '4b68ed24fc3aee70a25de6238663d0b8';
|
||||
const currentFixturesHash = 'a7152a9f4a59d30e0ccf7785fd6a8cc9';
|
||||
|
||||
// If this test is failing, then it is likely a change has been made that requires a DB version bump,
|
||||
|
238
core/test/unit/lib/mobiledoc/cards/bookmark_spec.js
Normal file
238
core/test/unit/lib/mobiledoc/cards/bookmark_spec.js
Normal file
@ -0,0 +1,238 @@
|
||||
const should = require('should');
|
||||
const card = require('../../../../../server/lib/mobiledoc/cards/bookmark');
|
||||
const SimpleDom = require('simple-dom');
|
||||
const serializer = new SimpleDom.HTMLSerializer(SimpleDom.voidMap);
|
||||
|
||||
describe('Bookmark card', function () {
|
||||
it('renders', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {
|
||||
url: 'http://example.com',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
icon: 'http://example.com/icon.png',
|
||||
thumbnail: 'http://exampple.com/thumbnail.png',
|
||||
author: 'Author',
|
||||
publisher: 'Publisher'
|
||||
},
|
||||
caption: 'Caption'
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('<figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="http://example.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Title</div><div class="kg-bookmark-description">Description</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="http://example.com/icon.png"><span class="kg-bookmark-author">Author</span><span class="kg-bookmark-publisher">Publisher</span></div></div><div class="kg-bookmark-thumbnail"><img src="http://exampple.com/thumbnail.png"></div></a><figcaption>Caption</figcaption></figure>');
|
||||
});
|
||||
|
||||
it('skips icon when missing', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {
|
||||
url: 'http://example.com',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
icon: null,
|
||||
thumbnail: 'http://exampple.com/thumbnail.png',
|
||||
author: 'Author',
|
||||
publisher: 'Publisher'
|
||||
},
|
||||
caption: 'Caption'
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('<figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="http://example.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Title</div><div class="kg-bookmark-description">Description</div><div class="kg-bookmark-metadata"><span class="kg-bookmark-author">Author</span><span class="kg-bookmark-publisher">Publisher</span></div></div><div class="kg-bookmark-thumbnail"><img src="http://exampple.com/thumbnail.png"></div></a><figcaption>Caption</figcaption></figure>');
|
||||
});
|
||||
|
||||
it('skips thumbnail when missing', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {
|
||||
url: 'http://example.com',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
icon: 'http://example.com/icon.png',
|
||||
thumbnail: null,
|
||||
author: 'Author',
|
||||
publisher: 'Publisher'
|
||||
},
|
||||
caption: 'Caption'
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('<figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="http://example.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Title</div><div class="kg-bookmark-description">Description</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="http://example.com/icon.png"><span class="kg-bookmark-author">Author</span><span class="kg-bookmark-publisher">Publisher</span></div></div></a><figcaption>Caption</figcaption></figure>');
|
||||
});
|
||||
|
||||
it('skips author when missing', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {
|
||||
url: 'http://example.com',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
icon: 'http://example.com/icon.png',
|
||||
thumbnail: 'http://exampple.com/thumbnail.png',
|
||||
author: null,
|
||||
publisher: 'Publisher'
|
||||
},
|
||||
caption: 'Caption'
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('<figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="http://example.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Title</div><div class="kg-bookmark-description">Description</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="http://example.com/icon.png"><span class="kg-bookmark-publisher">Publisher</span></div></div><div class="kg-bookmark-thumbnail"><img src="http://exampple.com/thumbnail.png"></div></a><figcaption>Caption</figcaption></figure>');
|
||||
});
|
||||
|
||||
it('skips publisher when missing', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {
|
||||
url: 'http://example.com',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
icon: 'http://example.com/icon.png',
|
||||
thumbnail: 'http://exampple.com/thumbnail.png',
|
||||
author: 'Author',
|
||||
publisher: null
|
||||
},
|
||||
caption: 'Caption'
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('<figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="http://example.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Title</div><div class="kg-bookmark-description">Description</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="http://example.com/icon.png"><span class="kg-bookmark-author">Author</span></div></div><div class="kg-bookmark-thumbnail"><img src="http://exampple.com/thumbnail.png"></div></a><figcaption>Caption</figcaption></figure>');
|
||||
});
|
||||
|
||||
it('skips caption when missing', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {
|
||||
url: 'http://example.com',
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
icon: 'http://example.com/icon.png',
|
||||
thumbnail: 'http://exampple.com/thumbnail.png',
|
||||
author: 'Author',
|
||||
publisher: 'Publisher'
|
||||
},
|
||||
caption: ''
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('<figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="http://example.com"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Title</div><div class="kg-bookmark-description">Description</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="http://example.com/icon.png"><span class="kg-bookmark-author">Author</span><span class="kg-bookmark-publisher">Publisher</span></div></div><div class="kg-bookmark-thumbnail"><img src="http://exampple.com/thumbnail.png"></div></a></figure>');
|
||||
});
|
||||
|
||||
it('renders nothing when payload is undefined', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: undefined
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('');
|
||||
});
|
||||
|
||||
it('renders nothing when payload metadata is empty', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {}
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('');
|
||||
});
|
||||
|
||||
it('renders nothing when url is missing', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {
|
||||
url: null,
|
||||
title: 'Test bookmark',
|
||||
description: 'This is just a test'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('');
|
||||
});
|
||||
|
||||
it('renders nothing when title is missing', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {
|
||||
url: 'http://example.com',
|
||||
title: null,
|
||||
description: 'This is just a test'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('');
|
||||
});
|
||||
|
||||
it('renders nothing when description is missing', function () {
|
||||
let opts = {
|
||||
env: {dom: new SimpleDom.Document()},
|
||||
payload: {
|
||||
metadata: {
|
||||
url: 'http://example.com',
|
||||
title: 'Test bookmark',
|
||||
description: null
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts))
|
||||
.should.equal('');
|
||||
});
|
||||
|
||||
it('transforms urls absolute to relative', function () {
|
||||
let payload = {
|
||||
metadata: {
|
||||
url: 'http://127.0.0.1:2369/post'
|
||||
},
|
||||
caption: 'A link to <a href="http://127.0.0.1:2369/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.absoluteToRelative(payload, {});
|
||||
|
||||
transformed.metadata.url
|
||||
.should.equal('/post');
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="/post">an internal post</a>');
|
||||
});
|
||||
|
||||
it('transforms urls relative to absolute', function () {
|
||||
let payload = {
|
||||
metadata: {
|
||||
url: '/post'
|
||||
},
|
||||
caption: 'A link to <a href="/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.relativeToAbsolute(payload, {});
|
||||
|
||||
transformed.metadata.url
|
||||
.should.equal('http://127.0.0.1:2369/post');
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="http://127.0.0.1:2369/post">an internal post</a>');
|
||||
});
|
||||
});
|
@ -58,4 +58,26 @@ describe('Code card', function () {
|
||||
|
||||
serializer.serialize(card.render(opts)).should.match('<figure class="kg-card kg-code-card"><pre><code class="language-html"><p>Test</p></code></pre><figcaption>Some <strong>HTML</strong></figcaption></figure>');
|
||||
});
|
||||
|
||||
it('transforms urls absolute to relative', function () {
|
||||
let payload = {
|
||||
caption: 'A link to <a href="http://127.0.0.1:2369/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.absoluteToRelative(payload, {});
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="/post">an internal post</a>');
|
||||
});
|
||||
|
||||
it('transforms urls relative to absolute', function () {
|
||||
let payload = {
|
||||
caption: 'A link to <a href="/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.relativeToAbsolute(payload, {});
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="http://127.0.0.1:2369/post">an internal post</a>');
|
||||
});
|
||||
});
|
||||
|
@ -69,4 +69,26 @@ describe('Embed card', function () {
|
||||
|
||||
serializer.serialize(card.render(opts)).should.match('<figure class="kg-card kg-embed-card kg-card-hascaption">Testing<figcaption><strong>Caption</strong></figcaption></figure>');
|
||||
});
|
||||
|
||||
it('transforms urls absolute to relative', function () {
|
||||
let payload = {
|
||||
caption: 'A link to <a href="http://127.0.0.1:2369/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.absoluteToRelative(payload, {});
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="/post">an internal post</a>');
|
||||
});
|
||||
|
||||
it('transforms urls relative to absolute', function () {
|
||||
let payload = {
|
||||
caption: 'A link to <a href="/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.relativeToAbsolute(payload, {});
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="http://127.0.0.1:2369/post">an internal post</a>');
|
||||
});
|
||||
});
|
||||
|
@ -138,4 +138,74 @@ describe('Gallery card', function () {
|
||||
|
||||
serializer.serialize(card.render(opts)).should.eql('<figure class="kg-card kg-gallery-card kg-width-wide kg-card-hascaption"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="/content/images/2018/08/NatGeo01-9.jpg" width="3200" height="1600"></div><div class="kg-gallery-image"><img src="/content/images/2018/08/NatGeo03-6.jpg" width="3200" height="1600"></div></div></div><figcaption>Test caption</figcaption></figure>');
|
||||
});
|
||||
|
||||
it('transforms urls absolute to relative', function () {
|
||||
let payload = {
|
||||
images: [
|
||||
{
|
||||
row: 0,
|
||||
fileName: 'NatGeo01.jpg',
|
||||
src: 'http://127.0.0.1:2369/content/images/2018/08/NatGeo01-9.jpg',
|
||||
width: 3200,
|
||||
height: 1600,
|
||||
caption: 'A link to <a href="http://127.0.0.1:2369/post">an internal post</a>'
|
||||
},
|
||||
{
|
||||
row: 0,
|
||||
fileName: 'NatGeo02.jpg',
|
||||
src: 'http://127.0.0.1:2369/content/images/2018/08/NatGeo02-10.jpg',
|
||||
caption: 'A link to <a href="http://127.0.0.1:2369/post">an internal post</a>'
|
||||
}
|
||||
],
|
||||
caption: 'A link to <a href="http://127.0.0.1:2369/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.absoluteToRelative(payload, {});
|
||||
|
||||
transformed.images[0].src
|
||||
.should.equal('/content/images/2018/08/NatGeo01-9.jpg');
|
||||
|
||||
transformed.images[0].caption
|
||||
.should.equal('A link to <a href="/post">an internal post</a>');
|
||||
|
||||
transformed.images[1].src
|
||||
.should.equal('/content/images/2018/08/NatGeo02-10.jpg');
|
||||
|
||||
transformed.images[1].caption
|
||||
.should.equal('A link to <a href="/post">an internal post</a>');
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="/post">an internal post</a>');
|
||||
});
|
||||
|
||||
it('transforms urls relative to absolute', function () {
|
||||
let payload = {
|
||||
images: [
|
||||
{
|
||||
row: 0,
|
||||
fileName: 'NatGeo01.jpg',
|
||||
src: '/content/images/2018/08/NatGeo01-9.jpg',
|
||||
width: 3200,
|
||||
height: 1600
|
||||
},
|
||||
{
|
||||
row: 0,
|
||||
fileName: 'NatGeo02.jpg',
|
||||
src: '/content/images/2018/08/NatGeo02-10.jpg'
|
||||
}
|
||||
],
|
||||
caption: 'A link to <a href="/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.relativeToAbsolute(payload, {});
|
||||
|
||||
transformed.images[0].src
|
||||
.should.equal('http://127.0.0.1:2369/content/images/2018/08/NatGeo01-9.jpg');
|
||||
|
||||
transformed.images[1].src
|
||||
.should.equal('http://127.0.0.1:2369/content/images/2018/08/NatGeo02-10.jpg');
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="http://127.0.0.1:2369/post">an internal post</a>');
|
||||
});
|
||||
});
|
||||
|
@ -55,4 +55,26 @@ describe('HTML card', function () {
|
||||
|
||||
serializer.serialize(card.render(opts)).should.eql('');
|
||||
});
|
||||
|
||||
it('transforms urls absolute to relative', function () {
|
||||
let payload = {
|
||||
html: 'A link to <a href="http://127.0.0.1:2369/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.absoluteToRelative(payload, {});
|
||||
|
||||
transformed.html
|
||||
.should.equal('A link to <a href="/post">an internal post</a>');
|
||||
});
|
||||
|
||||
it('transforms urls relative to absolute', function () {
|
||||
let payload = {
|
||||
html: 'A link to <a href="/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.relativeToAbsolute(payload, {});
|
||||
|
||||
transformed.html
|
||||
.should.equal('A link to <a href="http://127.0.0.1:2369/post">an internal post</a>');
|
||||
});
|
||||
});
|
||||
|
@ -116,4 +116,34 @@ describe('Image card', function () {
|
||||
serializer.serialize(card.render(opts)).should.eql('<figure class="kg-card kg-image-card kg-width-full"><img src="https://www.ghost.org/image.png" class="kg-image"></figure>');
|
||||
});
|
||||
});
|
||||
|
||||
it('transforms urls absolute to relative', function () {
|
||||
let payload = {
|
||||
src: 'http://127.0.0.1:2369/content/images/2018/08/NatGeo01-9.jpg',
|
||||
caption: 'A link to <a href="http://127.0.0.1:2369/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.absoluteToRelative(payload, {});
|
||||
|
||||
transformed.src
|
||||
.should.equal('/content/images/2018/08/NatGeo01-9.jpg');
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="/post">an internal post</a>');
|
||||
});
|
||||
|
||||
it('transforms urls relative to absolute', function () {
|
||||
let payload = {
|
||||
src: '/content/images/2018/08/NatGeo01-9.jpg',
|
||||
caption: 'A link to <a href="/post">an internal post</a>'
|
||||
};
|
||||
|
||||
const transformed = card.relativeToAbsolute(payload, {});
|
||||
|
||||
transformed.src
|
||||
.should.equal('http://127.0.0.1:2369/content/images/2018/08/NatGeo01-9.jpg');
|
||||
|
||||
transformed.caption
|
||||
.should.equal('A link to <a href="http://127.0.0.1:2369/post">an internal post</a>');
|
||||
});
|
||||
});
|
||||
|
@ -4,44 +4,64 @@ const SimpleDom = require('simple-dom');
|
||||
const serializer = new SimpleDom.HTMLSerializer(SimpleDom.voidMap);
|
||||
|
||||
describe('Markdown card', function () {
|
||||
describe('default', function () {
|
||||
it('Markdown Card renders', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: '#HEADING\r\n- list\r\n- items'
|
||||
}
|
||||
};
|
||||
it('renders', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: '#HEADING\r\n- list\r\n- items'
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts)).should.eql('<!--kg-card-begin: markdown--><h1 id="heading">HEADING</h1>\n<ul>\n<li>list</li>\n<li>items</li>\n</ul>\n<!--kg-card-end: markdown-->');
|
||||
});
|
||||
serializer.serialize(card.render(opts)).should.eql('<!--kg-card-begin: markdown--><h1 id="heading">HEADING</h1>\n<ul>\n<li>list</li>\n<li>items</li>\n</ul>\n<!--kg-card-end: markdown-->');
|
||||
});
|
||||
|
||||
it('Accepts invalid HTML in markdown', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: '#HEADING\r\n<h2>Heading 2>'
|
||||
}
|
||||
};
|
||||
it('Accepts invalid HTML in markdown', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: '#HEADING\r\n<h2>Heading 2>'
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts)).should.eql('<!--kg-card-begin: markdown--><h1 id="heading">HEADING</h1>\n<h2>Heading 2><!--kg-card-end: markdown-->');
|
||||
});
|
||||
serializer.serialize(card.render(opts)).should.eql('<!--kg-card-begin: markdown--><h1 id="heading">HEADING</h1>\n<h2>Heading 2><!--kg-card-end: markdown-->');
|
||||
});
|
||||
|
||||
it('Renders nothing when payload is undefined', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: undefined
|
||||
}
|
||||
};
|
||||
it('Renders nothing when payload is undefined', function () {
|
||||
let opts = {
|
||||
env: {
|
||||
dom: new SimpleDom.Document()
|
||||
},
|
||||
payload: {
|
||||
markdown: undefined
|
||||
}
|
||||
};
|
||||
|
||||
serializer.serialize(card.render(opts)).should.eql('');
|
||||
});
|
||||
serializer.serialize(card.render(opts)).should.eql('');
|
||||
});
|
||||
|
||||
it('transforms urls absolute to relative', function () {
|
||||
let payload = {
|
||||
markdown: 'A link to [an internal post](http://127.0.0.1:2369/post)'
|
||||
};
|
||||
|
||||
const transformed = card.absoluteToRelative(payload, {});
|
||||
|
||||
transformed.markdown
|
||||
.should.equal('A link to [an internal post](/post)');
|
||||
});
|
||||
|
||||
it('transforms urls relative to absolute', function () {
|
||||
let payload = {
|
||||
markdown: 'A link to [an internal post](/post)'
|
||||
};
|
||||
|
||||
const transformed = card.relativeToAbsolute(payload, {});
|
||||
|
||||
transformed.markdown
|
||||
.should.equal('A link to [an internal post](http://127.0.0.1:2369/post)');
|
||||
});
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user