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

Commit

Permalink
Add v3.19.0
Browse files Browse the repository at this point in the history
  • Loading branch information
YannickRe committed Jun 9, 2020
1 parent 08dc6b2 commit aa00e92
Show file tree
Hide file tree
Showing 35 changed files with 1,431 additions and 917 deletions.
1 change: 0 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ const configureGrunt = function (grunt) {
ui: 'bdd',
reporter: grunt.option('reporter') || 'spec',
timeout: '60000',
save: grunt.option('reporter-output'),
require: ['core/server/overrides'],
retries: '3',
exit: true
Expand Down

Large diffs are not rendered by default.

This file was deleted.

Large diffs are not rendered by default.

This file was deleted.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions core/built/assets/icons/close-stroke.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions core/built/assets/icons/file-tabular-data.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions core/built/assets/icons/upload.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions core/frontend/services/themes/Storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {i18n} = require('../../../server/lib/common');
const logging = require('../../../shared/logging');
const errors = require('@tryghost/errors');
const debug = require('ghost-ignition').debug('api:themes');
const ObjectID = require('bson-objectid');

let themeStorage;

Expand All @@ -37,6 +38,7 @@ module.exports = {
},
setFromZip: (zip) => {
const shortName = getStorage().getSanitizedFileName(zip.name.split('.zip')[0]);
const backupName = `${shortName}_${ObjectID()}`;

// check if zip name is casper.zip
if (zip.name === 'casper.zip') {
Expand All @@ -54,9 +56,9 @@ module.exports = {
return getStorage().exists(shortName);
})
.then((themeExists) => {
// CASE: delete existing theme
// CASE: move the existing theme to a backup folder
if (themeExists) {
return getStorage().delete(shortName);
return getStorage().rename(shortName, backupName);
}
})
.then(() => {
Expand Down Expand Up @@ -86,14 +88,20 @@ module.exports = {
})
.finally(() => {
// @TODO: we should probably do this as part of saving the theme
// CASE: remove extracted dir from gscan
// happens in background
// CASE: remove extracted dir from gscan happens in background
if (checkedTheme) {
fs.remove(checkedTheme.path)
.catch((err) => {
logging.error(new errors.GhostError({err: err}));
});
}

// CASE: remove the backup we created earlier
getStorage()
.delete(backupName)
.catch((err) => {
logging.error(new errors.GhostError({err: err}));
});
});
},
destroy: function (themeName) {
Expand Down
20 changes: 19 additions & 1 deletion core/frontend/services/themes/ThemeStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,26 @@ class ThemeStorage extends LocalFileStorage {
};
}

/**
* Rename a file / folder
*
*
* @param String fileName
*/
rename(srcName, destName) {
let src = path.join(this.getTargetDir(), srcName);
let dest = path.join(this.getTargetDir(), destName);

return fs.move(src, dest);
}

/**
* Rename a file / folder
*
* @param String backupName
*/
delete(fileName) {
return fs.remove(path.join(this.storagePath, fileName));
return fs.remove(path.join(this.getTargetDir(), fileName));
}
}

Expand Down
16 changes: 11 additions & 5 deletions core/server/api/canary/labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,7 @@ module.exports = {

add: {
statusCode: 201,
headers: {
cacheInvalidate: true
},
headers: {},
options: [
'include'
],
Expand All @@ -78,8 +76,16 @@ module.exports = {
}
},
permissions: true,
query(frame) {
return models.Label.add(frame.data.labels[0], frame.options);
async query(frame) {
try {
return await models.Label.add(frame.data.labels[0], frame.options);
} catch (error) {
if (error.code && error.message.toLowerCase().indexOf('unique') !== -1) {
throw new errors.ValidationError({message: i18n.t('errors.api.labels.labelAlreadyExists')});
}

throw error;
}
}
},

Expand Down
41 changes: 39 additions & 2 deletions core/server/api/canary/members.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ const sanitizeInput = (members) => {
};

function serializeMemberLabels(labels) {
if (labels) {
if (_.isString(labels)) {
return [{
name: labels.trim()
}];
} else if (labels) {
return labels.filter((label) => {
return !!label;
}).map((label) => {
Expand Down Expand Up @@ -96,6 +100,27 @@ const listMembers = async function (options) {
};
};

const createLabels = async (labels, options) => {
const api = require('./index');

return await Promise.all(labels.map((label) => {
return api.labels.add.query({
data: {
labels: [label]
},
options: {
context: options.context
}
}).catch((error) => {
if (error.errorType === 'ValidationError') {
return;
}

throw error;
});
}));
};

const members = {
docName: 'members',
browse: {
Expand Down Expand Up @@ -184,6 +209,11 @@ const members = {

// NOTE: failed to link Stripe customer/plan/subscription
if (model && error.message && (error.message.indexOf('customer') || error.message.indexOf('plan') || error.message.indexOf('subscription'))) {
if (error.message.indexOf('customer') && error.code === 'resource_missing') {
error.context = i18n.t('errors.api.members.stripeCustomerNotFound.context');
error.help = i18n.t('errors.api.members.stripeCustomerNotFound.help');
}

const api = require('./index');

await api.members.destroy.query({
Expand Down Expand Up @@ -341,6 +371,11 @@ const members = {
lookup: /created_at/i
}];

// NOTE: custom labels have to be created in advance otherwise there are conflicts
// when processing member creation in parallel later on in import process
const importSetLabels = serializeMemberLabels(frame.data.labels);
await createLabels(importSetLabels, frame.options);

return fsLib.readCSV({
path: filePath,
columnsToExtract: columnsToExtract
Expand All @@ -352,6 +387,8 @@ const members = {
const api = require('./index');
entry.labels = (entry.labels && entry.labels.split(',')) || [];
const entryLabels = serializeMemberLabels(entry.labels);
const mergedLabels = _.unionBy(entryLabels, importSetLabels, 'name');

cleanupUndefined(entry);

let subscribed;
Expand All @@ -370,7 +407,7 @@ const members = {
subscribed: subscribed,
stripe_customer_id: entry.stripe_customer_id,
comped: (String(entry.complimentary_plan).toLocaleLowerCase() === 'true'),
labels: entryLabels,
labels: mergedLabels,
created_at: entry.created_at === '' ? undefined : entry.created_at
}]
},
Expand Down
38 changes: 26 additions & 12 deletions core/server/api/canary/oembed.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,18 @@ async function fetchBookmarkData(url, html) {
delete metadata.image;
delete metadata.logo;

if (metadata.title && metadata.description) {
if (metadata.title) {
return Promise.resolve({
type: 'bookmark',
url,
metadata
});
}
return Promise.resolve();

return Promise.reject(new errors.ValidationError({
message: i18n.t('errors.api.oembed.insufficientMetadata'),
context: url
}));
}

const findUrlWithProvider = (url) => {
Expand Down Expand Up @@ -125,10 +129,10 @@ function fetchOembedData(_url) {
method: 'GET',
timeout: 2 * 1000,
followRedirect: true
}).then((response) => {
}).then((pageResponse) => {
// url changed after fetch, see if we were redirected to a known oembed
if (response.url !== url) {
({url, provider} = findUrlWithProvider(response.url));
if (pageResponse.url !== url) {
({url, provider} = findUrlWithProvider(pageResponse.url));
if (provider) {
return knownProvider(url);
}
Expand All @@ -137,7 +141,7 @@ function fetchOembedData(_url) {
// check for <link rel="alternate" type="application/json+oembed"> element
let oembedUrl;
try {
oembedUrl = cheerio('link[type="application/json+oembed"]', response.body).attr('href');
oembedUrl = cheerio('link[type="application/json+oembed"]', pageResponse.body).attr('href');
} catch (e) {
return unknownProvider(url);
}
Expand All @@ -154,10 +158,10 @@ function fetchOembedData(_url) {
json: true,
timeout: 2 * 1000,
followRedirect: true
}).then((response) => {
}).then((oembedResponse) => {
// validate the fetched json against the oembed spec to avoid
// leaking non-oembed responses
const body = response.body;
const body = oembedResponse.body;
const hasRequiredFields = body.type && body.version;
const hasValidType = ['photo', 'video', 'link', 'rich'].includes(body.type);

Expand Down Expand Up @@ -197,6 +201,18 @@ function fetchOembedData(_url) {
});
}

function errorHandler(url) {
return function (err) {
// allow specific validation errors through for better error messages
if (errors.utils.isIgnitionError(err) && err.errorType === 'ValidationError') {
return Promise.reject(err);
}

// default to unknown provider to avoid leaking any app specifics
return unknownProvider(url);
};
}

module.exports = {
docName: 'oembed',

Expand All @@ -212,7 +228,7 @@ module.exports = {

if (type === 'bookmark') {
return fetchBookmarkData(url)
.catch(() => unknownProvider(url));
.catch(errorHandler(url));
}

return fetchOembedData(url).then((response) => {
Expand All @@ -225,9 +241,7 @@ module.exports = {
return unknownProvider(url);
}
return response;
}).catch(() => {
return unknownProvider(url);
});
}).catch(errorHandler(url));
}
}
};
70 changes: 70 additions & 0 deletions core/server/api/canary/settings.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const Promise = require('bluebird');
const _ = require('lodash');
const validator = require('validator');
const models = require('../../models');
const routing = require('../../../frontend/services/routing');
const {i18n} = require('../../lib/common');
Expand Down Expand Up @@ -80,6 +81,75 @@ module.exports = {
}
},

validateMembersFromEmail: {
options: [
'token'
],
permissions: false,
validation: {
options: {
token: {
required: true
}
}
},
async query(frame) {
// This is something you have to do if you want to use the "framework" with access to the raw req/res
frame.response = async function (req, res) {
try {
const updatedFromAddress = membersService.settings.getEmailFromToken({token: frame.options.token});
if (updatedFromAddress) {
let subscriptionSetting = settingsCache.get('members_subscription_settings', {resolve: false});
const settingsValue = subscriptionSetting.value ? JSON.parse(subscriptionSetting.value) : {};
settingsValue.fromAddress = updatedFromAddress;
return models.Settings.edit({
key: 'members_subscription_settings',
value: JSON.stringify(settingsValue)
}).then(() => {
// Redirect to Ghost-Admin settings page
const adminLink = membersService.settings.getAdminRedirectLink();
res.redirect(adminLink);
});
} else {
return Promise.reject(new BadRequestError({
message: 'Invalid token!'
}));
}
} catch (err) {
return Promise.reject(new BadRequestError({
err,
message: 'Invalid token!'
}));
}
};
}
},

updateMembersFromEmail: {
permissions: {
method: 'edit'
},
async query(frame) {
const email = frame.data.from_address;
if (typeof email !== 'string' || !validator.isEmail(email)) {
throw new BadRequestError({
message: i18n.t('errors.api.settings.invalidEmailReceived')
});
}
try {
// Send magic link to update fromAddress
await membersService.settings.sendFromAddressUpdateMagicLink({
email
});
} catch (err) {
throw new BadRequestError({
err,
message: i18n.t('errors.mail.failedSendingEmail.error')
});
}
}
},

edit: {
headers: {
cacheInvalidate: true
Expand Down
Loading

0 comments on commit aa00e92

Please sign in to comment.