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

Commit

Permalink
Add v3.38.0
Browse files Browse the repository at this point in the history
YannickRe committed Nov 17, 2020
1 parent 7b81ae0 commit fe9d9f6
Showing 103 changed files with 1,700 additions and 1,646 deletions.
1 change: 0 additions & 1 deletion Gruntfile.js
Original file line number Diff line number Diff line change
@@ -40,7 +40,6 @@ const configureGrunt = function (grunt) {
grunt.loadNpmTasks('grunt-contrib-compress');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-contrib-symlink');
grunt.loadNpmTasks('grunt-contrib-uglify');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-express-server');
grunt.loadNpmTasks('grunt-mocha-cli');
2 changes: 1 addition & 1 deletion content/themes/casper/assets/built/screen.css

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion content/themes/casper/assets/built/screen.css.map

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions content/themes/casper/assets/css/screen.css
Original file line number Diff line number Diff line change
@@ -2263,9 +2263,9 @@ Usage (In Ghost editor):
flex-wrap: wrap;
align-items: center;
margin-top: 14px;
color: color-mod(var(--midgrey) l(-10%));
color: var(--darkgrey);
font-size: 1.5rem;
font-weight: 400;
font-weight: 500;
}

.post-full-content .kg-bookmark-icon {
@@ -2289,6 +2289,8 @@ Usage (In Ghost editor):
line-height: 1.5em;
text-overflow: ellipsis;
white-space: nowrap;
color: color-mod(var(--midgrey) l(-10%));
font-weight: 400;
}

@media (max-width: 800px) {
10 changes: 5 additions & 5 deletions content/themes/casper/package.json
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
"name": "casper",
"description": "A clean, minimal default theme for the Ghost publishing platform",
"demo": "https://demo.ghost.io",
"version": "3.1.1",
"version": "3.1.2",
"engines": {
"ghost": ">=3.0.0",
"ghost-api": "v3"
@@ -46,19 +46,19 @@
"bugs": "https://github.com/TryGhost/Casper/issues",
"contributors": "https://github.com/TryGhost/Casper/graphs/contributors",
"devDependencies": {
"@tryghost/release-utils": "0.6.7",
"autoprefixer": "10.0.1",
"@tryghost/release-utils": "0.6.8",
"autoprefixer": "10.0.2",
"beeper": "2.0.0",
"cssnano": "4.1.10",
"gscan": "3.5.7",
"gscan": "3.6.0",
"gulp": "4.0.2",
"gulp-concat": "2.6.1",
"gulp-livereload": "4.0.2",
"gulp-postcss": "9.0.0",
"gulp-uglify": "3.0.2",
"gulp-zip": "5.0.2",
"inquirer": "7.3.3",
"postcss": "8.1.2",
"postcss": "8.1.7",
"postcss-color-mod-function": "3.0.3",
"postcss-easy-import": "3.0.0",
"pump": "3.0.0"
459 changes: 257 additions & 202 deletions content/themes/casper/yarn.lock

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.

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions core/built/assets/icons/heart.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.

2 changes: 1 addition & 1 deletion core/frontend/helpers/ghost_head.js
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ function getMembersHelper() {
const stripeConnectAccountId = settingsCache.get('stripe_connect_account_id');

let membersHelper = `<script defer src="https://unpkg.com/@tryghost/portal@latest/umd/portal.min.js" data-ghost="${urlUtils.getSiteUrl()}"></script>`;
membersHelper += (`<style type='text/css'> ${templateStyles}</style>`);
membersHelper += (`<style> ${templateStyles}</style>`);
if ((!!stripeDirectSecretKey && !!stripeDirectPublishableKey) || !!stripeConnectAccountId) {
membersHelper += '<script async src="https://js.stripe.com/v3/"></script>';
}
195 changes: 173 additions & 22 deletions core/frontend/services/redirects/settings.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
const fs = require('fs-extra');
const path = require('path');
const Promise = require('bluebird');
const moment = require('moment-timezone');

const yaml = require('js-yaml');
const Promise = require('bluebird');
const validation = require('./validation');

const config = require('../../../shared/config');
const {i18n} = require('../../../server/lib/common');
const errors = require('@tryghost/errors');

/**
* Redirect configuration object
* @typedef {Object} RedirectConfig
* @property {String} from - Defines the relative incoming URL or pattern (regex)
* @property {String} to - Defines where the incoming traffic should be redirected to, which can be a static URL, or a dynamic value using regex (example: "to": "/$1/")
* @property {boolean} permanent - Can be defined with true for a permanent HTTP 301 redirect, or false for a temporary HTTP 302 redirect
*/

const readRedirectsFile = (redirectsPath) => {
return fs.readFile(redirectsPath, 'utf-8')
.then((content) => {
try {
content = JSON.parse(content);
} catch (err) {
throw new errors.BadRequestError({
message: i18n.t('errors.general.jsonParse', {context: err.message})
});
}

return content;
})
.catch((err) => {
if (err.code === 'ENOENT') {
return Promise.resolve([]);
@@ -37,16 +34,126 @@ const readRedirectsFile = (redirectsPath) => {
});
};

const setFromFilePath = (filePath) => {
const redirectsPath = path.join(config.getContentPath('data'), 'redirects.json');
const backupRedirectsPath = path.join(config.getContentPath('data'), `redirects-${moment().format('YYYY-MM-DD-HH-mm-ss')}.json`);
/**
*
* @param {String} content serialized JSON or YAML configuration
* @param {String} ext one of `.json` or `.yaml` extensions
*
* @returns {RedirectConfig[]} of parsed redirect config objects
*/
const parseRedirectsFile = (content, ext) => {
if (ext === '.json') {
let redirects;

try {
redirects = JSON.parse(content);
} catch (err) {
throw new errors.BadRequestError({
message: i18n.t('errors.general.jsonParse', {context: err.message})
});
}

return redirects;
}

if (ext === '.yaml') {
let redirects = [];
let configYaml = yaml.safeLoad(content);

// yaml.safeLoad passes almost every yaml code.
// Because of that, it's hard to detect if there's an error in the file.
// But one of the obvious errors is the plain string output.
// Here we check if the user made this mistake.
if (typeof configYaml === 'string') {
throw new errors.BadRequestError({
message: i18n.t('errors.api.redirects.yamlParse'),
help: 'https://ghost.org/docs/api/handlebars-themes/routing/redirects/'
});
}

/**
* 302: Temporary redirects
*/
for (const redirect in configYaml['302']) {
redirects.push({
from: redirect,
to: configYaml['302'][redirect],
permanent: false
});
}

/**
* 301: Permanent redirects
*/
for (const redirect in configYaml['301']) {
redirects.push({
from: redirect,
to: configYaml['301'][redirect],
permanent: true
});
}

return redirects;
}

throw new errors.IncorrectUsageError();
};

const createRedirectsFilePath = (ext) => {
return path.join(config.getContentPath('data'), `redirects${ext}`);
};

const getRedirectsFilePath = async () => {
const yamlPath = createRedirectsFilePath('.yaml');
const jsonPath = createRedirectsFilePath('.json');

const yamlExists = await fs.pathExists(yamlPath);

if (yamlExists) {
return yamlPath;
}

const jsonExist = await fs.pathExists(jsonPath);

return fs.pathExists(redirectsPath)
.then((redirectExists) => {
if (!redirectExists) {
if (jsonExist) {
return jsonPath;
}

return null;
};

const getCurrentRedirectsFilePathSync = () => {
const yamlPath = createRedirectsFilePath('.yaml');
const jsonPath = createRedirectsFilePath('.json');

if (fs.existsSync(yamlPath)) {
return yamlPath;
}

if (fs.existsSync(jsonPath)) {
return jsonPath;
}

return null;
};

const getBackupRedirectsFilePath = (filePath) => {
const {dir, name, ext} = path.parse(filePath);

return path.join(dir, `${name}-${moment().format('YYYY-MM-DD-HH-mm-ss')}${ext}`);
};

// '.json' is the default here because 'yaml' redirects file format is added later in v3.
// @TODO: change the default to '.yaml' in v4. It may be even removed if '.json' format is deprecated.
const setFromFilePath = (filePath, ext = '.json') => {
return getRedirectsFilePath()
.then((redirectsFilePath) => {
if (!redirectsFilePath) {
return null;
}

const backupRedirectsPath = getBackupRedirectsFilePath(redirectsFilePath);

return fs.pathExists(backupRedirectsPath)
.then((backupExists) => {
if (!backupExists) {
@@ -56,21 +163,65 @@ const setFromFilePath = (filePath) => {
return fs.unlink(backupRedirectsPath);
})
.then(() => {
return fs.move(redirectsPath, backupRedirectsPath);
return fs.move(redirectsFilePath, backupRedirectsPath);
});
})
.then(() => {
return readRedirectsFile(filePath)
.then((content) => {
return parseRedirectsFile(content, ext);
})
.then((content) => {
validation.validate(content);
return fs.writeFile(redirectsPath, JSON.stringify(content), 'utf-8');

if (ext === '.json') {
return fs.writeFile(createRedirectsFilePath('.json'), JSON.stringify(content), 'utf-8');
}

if (ext === '.yaml') {
return fs.copy(filePath, createRedirectsFilePath('.yaml'));
}
});
});
};

// @TODO: When yaml has been changed as the default redirects file format,
// change this like `const defaultRedirectsContent = ''`.
// Default json content is []. But the default YAML content is an empty string.
const defaultJsonFileContent = [];

const get = () => {
return readRedirectsFile(path.join(config.getContentPath('data'), 'redirects.json'));
return getRedirectsFilePath().then((filePath) => {
if (filePath === null) {
return defaultJsonFileContent;
}

return readRedirectsFile(filePath).then((content) => {
return path.extname(filePath) === '.json'
? parseRedirectsFile(content, '.json')
: content;
});
});
};

/**
* Syncrounously loads current oncifg file and parses it's content
*
* @returns {{RedirectConfig[]}} of parsed redirect configurations
*/
const loadRedirectsFile = () => {
const filePath = getCurrentRedirectsFilePathSync();

if (filePath === null) {
return defaultJsonFileContent;
}

const content = fs.readFileSync(filePath);

return parseRedirectsFile(content, path.extname(filePath));
};

module.exports.get = get;
module.exports.setFromFilePath = setFromFilePath;
module.exports.getRedirectsFilePath = getRedirectsFilePath;
module.exports.loadRedirectsFile = loadRedirectsFile;
1 change: 1 addition & 0 deletions core/frontend/services/routing/settings.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const moment = require('moment-timezone');
const fs = require('fs-extra');
const path = require('path');
2 changes: 1 addition & 1 deletion core/frontend/services/settings/ensure-settings.js
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ module.exports = function ensureSettingsFiles(knownSettings) {
const defaultFileName = `default-${fileName}`;
const filePath = path.join(contentPath, fileName);

return fs.readFile(filePath, 'utf8')
return Promise.resolve(fs.readFile(filePath, 'utf8'))
.catch({code: 'ENOENT'}, () => {
const defaultFilePath = path.join(defaultSettingsPath, defaultFileName);
// CASE: file doesn't exist, copy it from our defaults
4 changes: 4 additions & 0 deletions core/frontend/services/url/Resource.js
Original file line number Diff line number Diff line change
@@ -6,6 +6,10 @@ const errors = require('@tryghost/errors');
* Resource cache.
*/
class Resource extends EventEmitter {
/**
* @param {('posts'|'pages'|'tags'|'authors')} type - of the resource
* @param {Object} obj - object data to sotre
*/
constructor(type, obj) {
super();

14 changes: 2 additions & 12 deletions core/frontend/services/url/UrlGenerator.js
Original file line number Diff line number Diff line change
@@ -142,7 +142,7 @@ class UrlGenerator {

/**
* @description Try to own a resource and generate it's url if so.
* @param {Resource} resource
* @param {import('./Resource')} resource - instance of the Resource class
* @returns {boolean}
* @private
*/
@@ -161,17 +161,7 @@ class UrlGenerator {

// CASE 1: route has no custom filter, it will own the resource for sure
// CASE 2: find out if my filter matches the resource
if (!this.filter) {
this.urls.add({
url: url,
generatorId: this.uid,
resource: resource
});

resource.reserve();
this._resourceListeners(resource);
return true;
} else if (this.nql.queryJSON(resource.data)) {
if ((!this.filter) || (this.nql.queryJSON(resource.data))) {
this.urls.add({
url: url,
generatorId: this.uid,
1 change: 1 addition & 0 deletions core/server/api/canary/images.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const storage = require('../../adapters/storage');

module.exports = {
19 changes: 15 additions & 4 deletions core/server/api/canary/posts.js
Original file line number Diff line number Diff line change
@@ -88,8 +88,7 @@ module.exports = {
options: [
'include',
'formats',
'source',
'send_email_when_published'
'source'
],
validation: {
options: {
@@ -125,6 +124,7 @@ module.exports = {
'id',
'formats',
'source',
'email_recipient_filter',
'send_email_when_published',
'force_rerender',
// NOTE: only for internal context
@@ -141,6 +141,12 @@ module.exports = {
},
source: {
values: ['html']
},
email_recipient_filter: {
values: ['none', 'free', 'paid', 'all']
},
send_email_when_published: {
values: [true, false]
}
}
},
@@ -149,14 +155,19 @@ module.exports = {
},
async query(frame) {
/**Check host limits for members when send email is true**/
if (frame.options.send_email_when_published) {
if ((frame.options.email_recipient_filter && frame.options.email_recipient_filter !== 'none') || frame.options.send_email_when_published) {
await membersService.checkHostLimit();
}

let model = await models.Post.edit(frame.data.posts[0], frame.options);

if (!frame.options.email_recipient_filter && frame.options.send_email_when_published) {
frame.options.email_recipient_filter = model.get('visibility') === 'paid' ? 'paid' : 'all';
model = await models.Post.edit(frame.data.posts[0], frame.options);
}

/**Handle newsletter email */
if (model.get('send_email_when_published')) {
if (model.get('email_recipient_filter') !== 'none') {
const postPublished = model.wasChanged() && (model.get('status') === 'published') && (model.previous('status') !== 'published');
if (postPublished) {
let postEmail = model.relations.email;
23 changes: 21 additions & 2 deletions core/server/api/canary/redirects.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const path = require('path');

const web = require('../../web');
const redirects = require('../../../frontend/services/redirects');

@@ -8,10 +10,27 @@ module.exports = {
headers: {
disposition: {
type: 'file',
value: 'redirects.json'
value() {
return redirects.settings.getRedirectsFilePath()
.then((filePath) => {
// TODO: Default file type is .json for backward compatibility.
// When .yaml becomes default or .json is removed at v4,
// This part should be changed.
return filePath === null || path.extname(filePath) === '.json'
? 'redirects.json'
: 'redirects.yaml';
});
}
}
},
permissions: true,
response: {
async format() {
const filePath = await redirects.settings.getRedirectsFilePath();

return filePath === null || path.extname(filePath) === '.json' ? 'json' : 'plain';
}
},
query() {
return redirects.settings.get();
}
@@ -23,7 +42,7 @@ module.exports = {
cacheInvalidate: true
},
query(frame) {
return redirects.settings.setFromFilePath(frame.file.path)
return redirects.settings.setFromFilePath(frame.file.path, frame.file.ext)
.then(() => {
// CASE: trigger that redirects are getting re-registered
web.shared.middlewares.customRedirects.reload();
8 changes: 8 additions & 0 deletions core/server/api/canary/utils/serializers/input/posts.js
Original file line number Diff line number Diff line change
@@ -108,6 +108,10 @@ module.exports = {

forcePageFilter(frame);

if (frame.options.columns && frame.options.columns.includes('send_email_when_published')) {
frame.options.columns.push('email_recipient_filter');
}

/**
* ## current cases:
* - context object is empty (functional call, content api access)
@@ -136,6 +140,10 @@ module.exports = {

forcePageFilter(frame);

if (frame.options.columns && frame.options.columns.includes('send_email_when_published')) {
frame.options.columns.push('email_recipient_filter');
}

/**
* ## current cases:
* - context object is empty (functional call, content api access)
1 change: 1 addition & 0 deletions core/server/api/canary/utils/serializers/output/roles.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const debug = require('ghost-ignition').debug('api:canary:utils:serializers:output:roles');
const canThis = require('../../../../../services/permissions').canThis;

12 changes: 11 additions & 1 deletion core/server/api/canary/utils/serializers/output/utils/clean.js
Original file line number Diff line number Diff line change
@@ -70,6 +70,8 @@ const author = (attrs, frame) => {
};

const post = (attrs, frame) => {
const columns = frame && frame.options && frame.options.columns || null;
const fields = frame && frame.original && frame.original.query && frame.original.query.fields || null;
if (localUtils.isContentAPI(frame)) {
// @TODO: https://github.com/TryGhost/Ghost/issues/10335
// delete attrs.page;
@@ -95,13 +97,21 @@ const post = (attrs, frame) => {
attrs.og_description = null;
}
// NOTE: the visibility column has to be always present in Content API response to perform content gating
if (frame.options.columns && frame.options.columns.includes('visibility') && !frame.original.query.fields.includes('visibility')) {
if (columns && columns.includes('visibility') && fields && !fields.includes('visibility')) {
delete attrs.visibility;
}
} else {
delete attrs.page;
}

if (columns && columns.includes('email_recipient_filter') && fields && !fields.includes('email_recipient_filter')) {
delete attrs.email_recipient_filter;
}

if (fields && !fields.includes('send_email_when_published')) {
delete attrs.send_email_when_published;
}

if (!attrs.tags) {
delete attrs.primary_tag;
}
Original file line number Diff line number Diff line change
@@ -47,6 +47,14 @@ const mapPost = (model, frame) => {
gating.forPost(jsonModel, frame);
}

if (typeof jsonModel.email_recipient_filter === 'undefined') {
jsonModel.send_email_when_published = null;
} else if (jsonModel.email_recipient_filter === 'none') {
jsonModel.send_email_when_published = false;
} else {
jsonModel.send_email_when_published = true;
}

clean.post(jsonModel, frame);

if (frame.options && frame.options.withRelated) {
@@ -89,6 +97,7 @@ const mapPage = (model, frame) => {

delete jsonModel.email_subject;
delete jsonModel.send_email_when_published;
delete jsonModel.email_recipient_filter;

return jsonModel;
};
Original file line number Diff line number Diff line change
@@ -10,7 +10,8 @@ const groupTypeMapping = {
members: 'members',
private: 'private',
portal: 'portal',
email: 'bulk_email'
email: 'bulk_email',
newsletter: 'newsletter'
};

const mapGroupToType = (group) => {
32 changes: 27 additions & 5 deletions core/server/api/shared/http.js
Original file line number Diff line number Diff line change
@@ -85,13 +85,35 @@ const http = (apiImpl) => {
// CASE: generate headers based on the api ctrl configuration
res.set(headers);

if (apiImpl.response && apiImpl.response.format === 'plain') {
debug('plain text response');
return res.send(result);
const send = (format) => {
if (format === 'plain') {
debug('plain text response');
return res.send(result);
}

debug('json response');
res.json(result || {});
};

let responseFormat;

if (apiImpl.response){
if (typeof apiImpl.response.format === 'function') {
const apiResponseFormat = apiImpl.response.format();

if (apiResponseFormat.then) { // is promise
return apiResponseFormat.then((formatName) => {
send(formatName);
});
} else {
responseFormat = apiResponseFormat;
}
} else {
responseFormat = apiImpl.response.format;
}
}

debug('json response');
res.json(result || {});
send(responseFormat);
})
.catch((err) => {
req.frameOptions = {
1 change: 1 addition & 0 deletions core/server/api/v2/utils/serializers/output/roles.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:roles');
const canThis = require('../../../../../services/permissions').canThis;

Original file line number Diff line number Diff line change
@@ -76,6 +76,7 @@ const mapPost = (model, frame) => {

delete jsonModel.posts_meta;
delete jsonModel.send_email_when_published;
delete jsonModel.email_recipient_filter;
delete jsonModel.email_subject;

return jsonModel;
Original file line number Diff line number Diff line change
@@ -10,7 +10,8 @@ const groupTypeMapping = {
members: 'members',
private: 'private',
portal: 'portal',
email: 'bulk_email'
email: 'bulk_email',
newsletter: 'newsletter'
};

const mapGroupToType = (group) => {
2 changes: 1 addition & 1 deletion core/server/data/db/backup.js
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ const exporter = require('../exporter');
const writeExportFile = function writeExportFile(exportResult) {
const filename = path.resolve(urlUtils.urlJoin(config.get('paths').contentPath, 'data', exportResult.filename));

return fs.writeFile(filename, JSON.stringify(exportResult.data)).return(filename);
return Promise.resolve(fs.writeFile(filename, JSON.stringify(exportResult.data))).return(filename);
};

const readBackup = async (filename) => {
9 changes: 9 additions & 0 deletions core/server/data/importer/importers/data/posts.js
Original file line number Diff line number Diff line change
@@ -33,6 +33,15 @@ class PostsImporter extends BaseImporter {
}
delete obj.page;
}

if (_.has(obj, 'send_email_when_published')) {
if (obj.send_email_when_published) {
obj.email_recipient_filter = obj.visibility === 'paid' ? 'paid' : 'all';
} else {
obj.email_recipient_filter = 'none';
}
delete obj.send_email_when_published;
}
});
}

86 changes: 86 additions & 0 deletions core/server/data/migrations/utils.js
Original file line number Diff line number Diff line change
@@ -266,6 +266,89 @@ function combineTransactionalMigrations(...migrations) {
};
}

/**
* @param {Migration[]} migrations
*
* @returns {Migration}
*/
function combineNonTransactionalMigrations(...migrations) {
return {
config: {
transaction: false
},
async up(config) {
for (const migration of migrations) {
await migration.up(config);
}
},
async down(config) {
// Down migrations must be run backwards!!
const reverseMigrations = migrations.slice().reverse();
for (const migration of reverseMigrations) {
await migration.down(config);
}
}
};
}

/**
* @param {string} table
* @param {string} column
* @param {Object} columnDefinition
*
* @returns {Migration}
*/
function createAddColumnMigration(table, column, columnDefinition) {
return createNonTransactionalMigration(
// up
commands.createColumnMigration({
table,
column,
dbIsInCorrectState: hasColumn => hasColumn === true,
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition
}),
// down
commands.createColumnMigration({
table,
column,
dbIsInCorrectState: hasColumn => hasColumn === false,
operation: commands.dropColumn,
operationVerb: 'Removing'
})
);
}

/**
* @param {string} table
* @param {string} column
* @param {Object} columnDefinition
*
* @returns {Migration}
*/
function createDropColumnMigration(table, column, columnDefinition) {
return createNonTransactionalMigration(
// up
commands.createColumnMigration({
table,
column,
dbIsInCorrectState: hasColumn => hasColumn === false,
operation: commands.dropColumn,
operationVerb: 'Removing'
}),
// down
commands.createColumnMigration({
table,
column,
dbIsInCorrectState: hasColumn => hasColumn === true,
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition
})
);
}

module.exports = {
addTable,
addPermission,
@@ -274,6 +357,9 @@ module.exports = {
createTransactionalMigration,
createNonTransactionalMigration,
combineTransactionalMigrations,
combineNonTransactionalMigrations,
createAddColumnMigration,
createDropColumnMigration,
meta: {
MIGRATION_USER
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;
const table = 'integrations';
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const commands = require('../../../schema').commands;
const logging = require('../../../../../shared/logging');

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');
const config = require('../../../../../shared/config');
const {URL} = require('url');
74 changes: 19 additions & 55 deletions core/server/data/migrations/versions/2.29/1-add-post-page-column.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,20 @@
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;

const createLog = type => msg => logging[type](msg);

function createColumnMigration({table, column, dbIsInCorrectState, operation, operationVerb}) {
return function columnMigrations({transacting}) {
return transacting.schema.hasColumn(table, column)
.then(dbIsInCorrectState)
.then((isInCorrectState) => {
const log = createLog(isInCorrectState ? 'warn' : 'info');

log(`${operationVerb} ${table}.${column}`);

if (!isInCorrectState) {
// has to be passed directly in case of migration to 3.0
// ref: https://github.com/TryGhost/Ghost/commit/9d7190d69255ac011848c6bf654886be81abeedc#diff-c20cac44dad77922cf53ffd7b094cd8cL22
const columnSpec = {
type: 'bool',
nullable: false,
defaultTo: false
};

return operation(table, column, transacting, columnSpec);
}
});
};
}

module.exports.up = function (options) {
return options.transacting.schema.hasColumn('posts', 'type').then((hasTypeColumn) => {
if (!hasTypeColumn) {
// no-op'd post.page->post.type migrations were never run
return Promise.resolve();
const {createNonTransactionalMigration} = require('../../utils');
const commands = require('../../../schema/commands');

module.exports = createNonTransactionalMigration(
commands.createColumnMigration({
table: 'posts',
column: 'page',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
type: 'bool',
nullable: false,
defaultTo: false
}

return createColumnMigration({
table: 'posts',
column: 'page',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
})(options);
});
};

// `up` only runs in order to fix a previous migration so we don't want to do
// anything in `down` because it would put previously-fine sites into the wrong
// state
module.exports.down = () => Promise.resolve();

module.exports.config = {
transaction: true
};
}),
() => Promise.resolve()
);
Original file line number Diff line number Diff line change
@@ -1,48 +1,15 @@
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;

const createLog = type => msg => logging[type](msg);

function createColumnMigration({table, column, dbIsInCorrectState, operation, operationVerb, columnDefinition}) {
return function columnMigrations({transacting}) {
return transacting.schema.hasColumn(table, column)
.then(dbIsInCorrectState)
.then((isInCorrectState) => {
const log = createLog(isInCorrectState ? 'warn' : 'info');

log(`${operationVerb} ${table}.${column}`);

if (!isInCorrectState) {
return operation(table, column, transacting, columnDefinition);
}
});
};
}

module.exports.up = function (options) {
return options.transacting.schema.hasColumn('posts', 'type').then((hasTypeColumn) => {
if (!hasTypeColumn) {
// no-op'd post.page->post.type migrations were never run
return Promise.resolve();
}

return createColumnMigration({
table: 'posts',
column: 'type',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
})(options);
});
};

// `up` only runs in order to fix a previous migration so we don't want to do
// anything in `down` because it would put previously-fine sites into the wrong
// state
module.exports.down = () => Promise.resolve();

module.exports.config = {
transaction: true
};
const {createNonTransactionalMigration} = require('../../utils');
const commands = require('../../../schema/commands');

module.exports = createNonTransactionalMigration(
commands.createColumnMigration({
table: 'posts',
column: 'type',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
}),
() => Promise.resolve()
);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;
const table = 'webhooks';
Original file line number Diff line number Diff line change
@@ -1,55 +1,15 @@
const commands = require('../../../schema').commands;
const {combineNonTransactionalMigrations, createDropColumnMigration} = require('../../utils');

module.exports = {

up: commands.createColumnMigration({
table: 'members',
column: 'password',
dbIsInCorrectState(hasColumn) {
return hasColumn === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
}, {
table: 'members',
column: 'name',
dbIsInCorrectState(hasColumn) {
return hasColumn === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
}),

down: commands.createColumnMigration({
table: 'members',
column: 'password',
dbIsInCorrectState(hasColumn) {
return hasColumn === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
type: 'string',
maxlength: 60,
nullable: true
}
}, {
table: 'members',
column: 'name',
dbIsInCorrectState(hasColumn) {
return hasColumn === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
type: 'string',
maxlength: 191,
nullable: false,
defaultTo: ''
}
module.exports = combineNonTransactionalMigrations(
createDropColumnMigration('members', 'password', {
type: 'string',
maxlength: 60,
nullable: true
}),

config: {
transaction: true
}
};
createDropColumnMigration('members', 'name', {
type: 'string',
maxlength: 191,
nullable: false,
defaultTo: ''
})
);
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

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
}
};
module.exports = createAddColumnMigration('members', 'name', {
type: 'string',
maxlength: 191,
nullable: true
});
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

module.exports = {

up: commands.createColumnMigration({
table: 'members_stripe_customers',
column: 'email',
dbIsInCorrectState(hasColumn) {
return hasColumn === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
}),

down: commands.createColumnMigration({
table: 'members_stripe_customers',
column: 'email',
dbIsInCorrectState(hasColumn) {
return hasColumn === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
}),

config: {
transaction: true
}
};
module.exports = createAddColumnMigration('members_stripe_customers', 'email', {
type: 'string',
maxlength: 191,
nullable: true
});
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

module.exports = {

up: commands.createColumnMigration({
table: 'members_stripe_customers',
column: 'name',
dbIsInCorrectState(hasColumn) {
return hasColumn === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
}),

down: commands.createColumnMigration({
table: 'members_stripe_customers',
column: 'name',
dbIsInCorrectState(hasColumn) {
return hasColumn === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
}),

config: {
transaction: true
}
};
module.exports = createAddColumnMigration('members_stripe_customers', 'name', {
type: 'string',
maxlength: 191,
nullable: true
});
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

module.exports = {

up: commands.createColumnMigration({
table: 'members',
column: 'note',
dbIsInCorrectState(hasColumn) {
return hasColumn === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
}),

down: commands.createColumnMigration({
table: 'members',
column: 'note',
dbIsInCorrectState(hasColumn) {
return hasColumn === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
}),

config: {
transaction: true
}
};
module.exports = createAddColumnMigration('members', 'note', {
type: 'string',
maxlength: 2000,
nullable: true
});
Original file line number Diff line number Diff line change
@@ -1,78 +1,14 @@
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;

const createLog = type => msg => logging[type](msg);

function createColumnMigrations(migrations) {
return function columnMigrations({transacting}) {
return Promise.each(migrations, function ({table, column, dbIsInCorrectState, operation, operationVerb, columnDefinition}) {
return transacting.schema.hasColumn(table, column)
.then(dbIsInCorrectState)
.then((isInCorrectState) => {
const log = createLog(isInCorrectState ? 'warn' : 'info');

log(`${operationVerb} ${table}.${column}`);

if (!isInCorrectState) {
return operation(table, column, transacting, columnDefinition);
}
});
});
};
}

module.exports.up = createColumnMigrations([
{
table: 'users',
column: 'ghost_auth_access_token',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
},
{
table: 'users',
column: 'ghost_auth_id',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
}
]);

module.exports.down = createColumnMigrations([
{
table: 'users',
column: 'ghost_auth_access_token',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
type: 'string',
nullable: true,
maxlength: 32
}
},
{
table: 'users',
column: 'ghost_auth_id',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
type: 'string',
nullable: true,
maxlength: 24
}
}
]);

module.exports.config = {
transaction: true
};
const {combineNonTransactionalMigrations, createDropColumnMigration} = require('../../utils');

module.exports = combineNonTransactionalMigrations(
createDropColumnMigration('users', 'ghost_auth_access_token', {
type: 'string',
nullable: true,
maxlength: 32
}),
createDropColumnMigration('users', 'ghost_auth_id', {
type: 'string',
nullable: true,
maxlength: 24
})
);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;

Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const postsMetaSchema = require('../../../schema').tables.posts_meta;
const ObjectId = require('bson-objectid');
const _ = require('lodash');
185 changes: 19 additions & 166 deletions core/server/data/migrations/versions/3.0/06-remove-posts-meta-columns.js
Original file line number Diff line number Diff line change
@@ -1,191 +1,44 @@
const commands = require('../../../schema').commands;
const {combineNonTransactionalMigrations, createDropColumnMigration} = require('../../utils');

module.exports.up = commands.createColumnMigration({
table: 'posts',
column: 'meta_title',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
},
{
table: 'posts',
column: 'meta_description',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
},
{
table: 'posts',
column: 'og_image',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
},
{
table: 'posts',
column: 'og_title',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
},
{
table: 'posts',
column: 'og_description',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
},
{
table: 'posts',
column: 'twitter_image',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
},
{
table: 'posts',
column: 'twitter_title',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
},
{
table: 'posts',
column: 'twitter_description',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Dropping'
});

module.exports.down = commands.createColumnMigration({
table: 'posts',
column: 'meta_title',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
module.exports = combineNonTransactionalMigrations(
createDropColumnMigration('posts', 'meta_title', {
type: 'string',
nullable: true,
maxlength: 2000
}
},
{
table: 'posts',
column: 'meta_description',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
}),
createDropColumnMigration('posts', 'meta_description', {
type: 'string',
nullable: true,
maxlength: 2000
}
},
{
table: 'posts',
column: 'og_image',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
}),
createDropColumnMigration('posts', 'og_image', {
type: 'string',
nullable: true,
maxlength: 2000
}
},
{
table: 'posts',
column: 'og_title',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
}),
createDropColumnMigration('posts', 'og_title', {
type: 'string',
nullable: true,
maxlength: 300
}
},
{
table: 'posts',
column: 'og_description',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
}),
createDropColumnMigration('posts', 'og_description', {
type: 'string',
nullable: true,
maxlength: 500
}
},
{
table: 'posts',
column: 'twitter_image',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
}),
createDropColumnMigration('posts', 'twitter_image', {
type: 'string',
nullable: true,
maxlength: 2000
}
},
{
table: 'posts',
column: 'twitter_title',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
}),
createDropColumnMigration('posts', 'twitter_title', {
type: 'string',
nullable: true,
maxlength: 300
}
},
{
table: 'posts',
column: 'twitter_description',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
}),
createDropColumnMigration('posts', 'twitter_description', {
type: 'string',
nullable: true,
maxlength: 500
}
});

module.exports.config = {
transaction: true
};
})
);
Original file line number Diff line number Diff line change
@@ -1,44 +1,8 @@
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

const createLog = type => msg => logging[type](msg);

function createColumnMigration({table, column, dbIsInCorrectState, operation, operationVerb}) {
return function columnMigrations({transacting}) {
return transacting.schema.hasColumn(table, column)
.then(dbIsInCorrectState)
.then((isInCorrectState) => {
const log = createLog(isInCorrectState ? 'warn' : 'info');

log(`${operationVerb} ${table}.${column}`);

if (!isInCorrectState) {
return operation(table, column, transacting);
}
});
};
}

module.exports.up = createColumnMigration({
table: 'posts',
column: 'type',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
module.exports = createAddColumnMigration('posts', 'type', {
type: 'string',
maxlength: 50,
nullable: false,
defaultTo: 'post'
});

module.exports.down = createColumnMigration({
table: 'posts',
column: 'type',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
});

module.exports.config = {
transaction: true
};
Original file line number Diff line number Diff line change
@@ -1,49 +1,7 @@
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;
const {createDropColumnMigration} = require('../../utils');

const createLog = type => msg => logging[type](msg);

function createColumnMigration({table, column, dbIsInCorrectState, operation, operationVerb, columnDefinition}) {
return function columnMigrations({transacting}) {
return transacting.schema.hasColumn(table, column)
.then(dbIsInCorrectState)
.then((isInCorrectState) => {
const log = createLog(isInCorrectState ? 'warn' : 'info');

log(`${operationVerb} ${table}.${column}`);

if (!isInCorrectState) {
return operation(table, column, transacting, columnDefinition);
}
});
};
}

module.exports.up = createColumnMigration({
table: 'posts',
column: 'page',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
module.exports = createDropColumnMigration('posts', 'page', {
type: 'bool',
nullable: false,
defaultTo: false
});

module.exports.down = createColumnMigration({
table: 'posts',
column: 'page',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding',
columnDefinition: {
type: 'bool',
nullable: false,
defaultTo: false
}
});

module.exports.config = {
transaction: true
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const ObjectId = require('bson-objectid');
const _ = require('lodash');
const logging = require('../../../../../shared/logging');
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;

Original file line number Diff line number Diff line change
@@ -1,25 +1,7 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

module.exports.up = commands.createColumnMigration({
table: 'posts',
column: 'send_email_when_published',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
module.exports = createAddColumnMigration('posts', 'send_email_when_published', {
type: 'bool',
nullable: true,
defaultTo: false
});

module.exports.down = commands.createColumnMigration({
table: 'posts',
column: 'send_email_when_published',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
});

module.exports.config = {
transaction: true
};
Original file line number Diff line number Diff line change
@@ -1,25 +1,7 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

module.exports.up = commands.createColumnMigration({
table: 'posts_meta',
column: 'email_subject',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
module.exports = createAddColumnMigration('posts_meta', 'email_subject', {
type: 'string',
maxlength: 300,
nullable: true
});

module.exports.down = commands.createColumnMigration({
table: 'posts_meta',
column: 'email_subject',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
});

module.exports.config = {
transaction: true
};
Original file line number Diff line number Diff line change
@@ -1,44 +1,7 @@
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

const createLog = type => msg => logging[type](msg);

function createColumnMigration({table, column, dbIsInCorrectState, operation, operationVerb}) {
return function columnMigrations({transacting}) {
return transacting.schema.hasColumn(table, column)
.then(dbIsInCorrectState)
.then((isInCorrectState) => {
const log = createLog(isInCorrectState ? 'warn' : 'info');

log(`${operationVerb} ${table}.${column}`);

if (!isInCorrectState) {
return operation(table, column, transacting);
}
});
};
}

module.exports.up = createColumnMigration({
table: 'members',
column: 'subscribed',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
module.exports = createAddColumnMigration('members', 'subscribed', {
type: 'bool',
nullable: true,
defaultTo: true
});

module.exports.down = createColumnMigration({
table: 'members',
column: 'subscribed',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
});

module.exports.config = {
transaction: true
};
Original file line number Diff line number Diff line change
@@ -1,44 +1,8 @@
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

const createLog = type => msg => logging[type](msg);

function createColumnMigration({table, column, dbIsInCorrectState, operation, operationVerb}) {
return function columnMigrations({transacting}) {
return transacting.schema.hasColumn(table, column)
.then(dbIsInCorrectState)
.then((isInCorrectState) => {
const log = createLog(isInCorrectState ? 'warn' : 'info');

log(`${operationVerb} ${table}.${column}`);

if (!isInCorrectState) {
return operation(table, column, transacting);
}
});
};
}

module.exports.up = createColumnMigration({
table: 'members',
column: 'uuid',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
module.exports = createAddColumnMigration('members', 'uuid', {
type: 'string',
maxlength: 36,
nullable: true,
unique: true
});

module.exports.down = createColumnMigration({
table: 'members',
column: 'uuid',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
});

module.exports.config = {
transaction: true
};
Original file line number Diff line number Diff line change
@@ -1,25 +1,8 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

module.exports.up = commands.createColumnMigration({
table: 'emails',
column: 'error_data',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
module.exports = createAddColumnMigration('emails', 'error_data', {
type: 'text',
maxlength: 1000000000,
fieldtype: 'long',
nullable: true
});

module.exports.down = commands.createColumnMigration({
table: 'emails',
column: 'error_data',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
});

module.exports.config = {
transaction: true
};
Original file line number Diff line number Diff line change
@@ -1,25 +1,7 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

module.exports.up = commands.createColumnMigration({
table: 'members_stripe_customers_subscriptions',
column: 'cancel_at_period_end',
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
module.exports = createAddColumnMigration('members_stripe_customers_subscriptions', 'cancel_at_period_end', {
type: 'bool',
nullable: false,
defaultTo: false
});

module.exports.down = commands.createColumnMigration({
table: 'members_stripe_customers_subscriptions',
column: 'cancel_at_period_end',
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
});

module.exports.config = {
transaction: true
};
Original file line number Diff line number Diff line change
@@ -1,49 +1,15 @@
const commands = require('../../../schema').commands;

module.exports = {
config: {
transaction: true
},

async up(options) {
function addSettingsColumn(column) {
return {
table: 'settings',
column,
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
};
}

const columnMigration = commands.createColumnMigration(
addSettingsColumn('group'),
addSettingsColumn('flags')
);

return columnMigration(options);
},

async down(options) {
function removeSettingsColumn(column) {
return {
table: 'settings',
column,
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
};
}

const columnMigration = commands.createColumnMigration(
removeSettingsColumn('group'),
removeSettingsColumn('flags')
);

return columnMigration(options);
}
};
const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');

module.exports = combineNonTransactionalMigrations(
createAddColumnMigration('settings', 'group', {
type: 'string',
maxlength: 50,
nullable: false,
defaultTo: 'core'
}),
createAddColumnMigration('settings', 'flags', {
type: 'string',
maxlength: 50,
nullable: true
})
);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');

// settings with new groups
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');

// type mapping for settings. object types are ignored for now
@@ -117,6 +118,12 @@ module.exports = {
.transacting('settings')
.where('key', key)
.select('group');

if (groupResult.length === 0) {
logging.warn(`Could not find group for ${key}`);
return;
}

const groupValue = groupResult[0].group;
return await options
.transacting('settings')
Original file line number Diff line number Diff line change
@@ -1,97 +1,54 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');

const newColumns = [{
column: 'og_image',
columnDefinition: {
module.exports = combineNonTransactionalMigrations(
createAddColumnMigration('tags', 'og_image', {
type: 'string',
maxlength: 2000,
nullable: true
}
}, {
column: 'og_title',
columnDefinition: {
}),
createAddColumnMigration('tags', 'og_title', {
type: 'string',
maxlength: 300,
nullable: true
}
}, {
column: 'og_description',
columnDefinition: {
}),
createAddColumnMigration('tags', 'og_description', {
type: 'string',
maxlength: 500,
nullable: true
}
}, {
column: 'twitter_image',
columnDefinition: {
}),
createAddColumnMigration('tags', 'twitter_image', {
type: 'string',
maxlength: 2000,
nullable: true
}
}, {
column: 'twitter_title',
columnDefinition: {
}),
createAddColumnMigration('tags', 'twitter_title', {
type: 'string',
maxlength: 300,
nullable: true
}
}, {
column: 'twitter_description',
columnDefinition: {
}),
createAddColumnMigration('tags', 'twitter_description', {
type: 'string',
maxlength: 500,
nullable: true
}
}, {
column: 'codeinjection_head',
columnDefinition: {
}),
createAddColumnMigration('tags', 'codeinjection_head', {
type: 'text',
maxlength: 65535,
nullable: true
}
}, {
column: 'codeinjection_foot',
columnDefinition: {
}),
createAddColumnMigration('tags', 'codeinjection_foot', {
type: 'text',
maxlength: 65535,
nullable: true
}
}, {
column: 'canonical_url',
columnDefinition: {
}),
createAddColumnMigration('tags', 'canonical_url', {
type: 'string',
maxlength: 2000,
nullable: true
}
}, {
column: 'accent_color',
columnDefinition: {
}),
createAddColumnMigration('tags', 'accent_color', {
type: 'string',
maxlength: 50,
nullable: true
}
}];

module.exports = {
config: {
transaction: true
},

up: commands.createColumnMigration(...newColumns.map((column) => {
return Object.assign({}, column, {
table: 'tags',
dbIsInCorrectState: hasColumn => hasColumn === true,
operation: commands.addColumn,
operationVerb: 'Adding'
});
})),

down: commands.createColumnMigration(...newColumns.map((column) => {
return Object.assign({}, column, {
table: 'tags',
dbIsInCorrectState: hasColumn => hasColumn === false,
operation: commands.dropColumn,
operationVerb: 'Removing'
});
}))
};
})
);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');

// new setting keys and group mapping
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const Promise = require('bluebird');
const logging = require('../../../../../shared/logging');
const commands = require('../../../schema').commands;

Original file line number Diff line number Diff line change
@@ -1,41 +1,14 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration, combineNonTransactionalMigrations} = require('../../utils');

const newColumns = [{
column: 'from',
columnDefinition: {
module.exports = combineNonTransactionalMigrations(
createAddColumnMigration('emails', 'from', {
type: 'string',
maxlength: 191,
nullable: true
}
}, {
column: 'reply_to',
columnDefinition: {
}),
createAddColumnMigration('emails', 'reply_to', {
type: 'string',
maxlength: 191,
nullable: true
}
}];

module.exports = {
config: {
transaction: true
},

up: commands.createColumnMigration(...newColumns.map((column) => {
return Object.assign({}, column, {
table: 'emails',
dbIsInCorrectState: hasColumn => hasColumn === true,
operation: commands.addColumn,
operationVerb: 'Adding'
});
})),

down: commands.createColumnMigration(...newColumns.map((column) => {
return Object.assign({}, column, {
table: 'emails',
dbIsInCorrectState: hasColumn => hasColumn === false,
operation: commands.dropColumn,
operationVerb: 'Removing'
});
}))
};
})
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const {createAddColumnMigration} = require('../../utils');

module.exports = createAddColumnMigration('posts', 'email_recipient_filter', {
type: 'string',
maxlength: 50,
nullable: false,
defaultTo: 'none'
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const {createTransactionalMigration} = require('../../utils');
const logging = require('../../../../../shared/logging');

module.exports = createTransactionalMigration(
async function up(connection) {
logging.info('Updating email_recipient_filter values based on visibility and send_email_when_published');
await connection('posts')
.update('email_recipient_filter', 'paid')
.where({
send_email_when_published: true,
visibility: 'paid'
});

await connection('posts')
.update('email_recipient_filter', 'all')
.where({
send_email_when_published: true,
visibility: 'members'
});

await connection('posts')
.update('email_recipient_filter', 'all')
.where({
send_email_when_published: true,
visibility: 'public'
});
},

async function down(connection) {
logging.info('Updating email_recipient_filter values to none');
await connection('posts')
.update('email_recipient_filter', 'none');
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const {createAddColumnMigration} = require('../../utils');

module.exports = createAddColumnMigration('emails', 'recipient_filter', {
type: 'string',
maxlength: 50,
nullable: false,
defaultTo: 'paid'
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const {createTransactionalMigration} = require('../../utils');
const logging = require('../../../../../shared/logging');

module.exports = createTransactionalMigration(
async function up(connection) {
logging.info('Updating emails.recipient_filter values based on posts.visibility');

const paidPostIds = (await connection('posts')
.select('id')
.where('visibility', 'paid')).map(row => row.id);

const membersPostIds = (await connection('posts')
.select('id')
.where('visibility', 'members')).map(row => row.id);

const publicPostIds = (await connection('posts')
.select('id')
.where('visibility', 'public')).map(row => row.id);

await connection('emails')
.update('recipient_filter', 'paid')
.whereIn('post_id', paidPostIds);

await connection('emails')
.update('recipient_filter', 'all')
.whereIn('post_id', membersPostIds.concat(publicPostIds));
},

async function down(connection) {
logging.info('Updating emails.recipient_filter values to paid');
await connection('emails')
.update('recipient_filter', 'paid');
}
);

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const {createAddColumnMigration} = require('../../utils');

module.exports = createAddColumnMigration('emails', 'track_opens', {
type: 'bool',
nullable: false,
defaultTo: false
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const logging = require('../../../../../shared/logging');
const {createTransactionalMigration} = require('../../utils');

module.exports = createTransactionalMigration(

async function up(connection) {
logging.info('Updating newsletter settings - newsletter_show_badge, newsletter_show_header, newsletter_body_font_category, newsletter_footer_content - to newsletter group');
await connection('settings')
.whereIn('key', ['newsletter_show_badge', 'newsletter_show_header', 'newsletter_body_font_category', 'newsletter_footer_content'])
.update({
group: 'newsletter'
});
},

async function down() {}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const {createTransactionalMigration} = require('../../utils');
const config = require('../../../../../shared/config');
const logging = require('../../../../../shared/logging');

module.exports = createTransactionalMigration(
async function up(connection) {
const emailTemplateConfig = config.get('members:emailTemplate');

logging.info('Updating newsletter_show_header setting from members.emailTemplate.showSiteHeader config');
await connection('settings')
.update({
value: emailTemplateConfig.showSiteHeader ? 'true' : 'false'
})
.where({
key: 'newsletter_show_header'
});

logging.info('Updating newsletter_show_badge setting from members.emailTemplate.showPoweredBy config');
await connection('settings')
.update({
value: emailTemplateConfig.showPoweredBy ? 'true' : 'false'
})
.where({
key: 'newsletter_show_badge'
});
},

async function down(connection) {
logging.info('Updating newsletter_show_header setting to default "true"');
await connection('settings')
.update({
value: 'true'
})
.where({
key: 'newsletter_show_header'
});

logging.info('Updating newsletter_show_badge setting to default "false"');
await connection('settings')
.update({
value: 'true'
})
.where({
key: 'newsletter_show_badge'
});
}
);

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const logging = require('../../../../../shared/logging');
const {createTransactionalMigration} = require('../../utils');

module.exports = createTransactionalMigration(
async function up() {},

async function down(connection) {
logging.info('Setting "send_email_when_published" based on "email_recipient_filter"');
await connection('posts')
.update({
send_email_when_published: true
})
.whereNot({
email_recipient_filter: 'none'
});

await connection('posts')
.update({
send_email_when_published: false
})
.where({
email_recipient_filter: 'none'
});
}
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const {createDropColumnMigration} = require('../../utils');

module.exports = createDropColumnMigration('posts', 'send_email_when_published', {
type: 'bool',
nullable: true,
defaultTo: false
});
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
const commands = require('../../../schema').commands;
const {createAddColumnMigration} = require('../../utils');

const table = 'members';
const column = 'geolocation';

module.exports.up = commands.createColumnMigration({
table,
column,
dbIsInCorrectState(columnExists) {
return columnExists === true;
},
operation: commands.addColumn,
operationVerb: 'Adding'
});

module.exports.down = commands.createColumnMigration({
table,
column,
dbIsInCorrectState(columnExists) {
return columnExists === false;
},
operation: commands.dropColumn,
operationVerb: 'Removing'
module.exports = createAddColumnMigration('members', 'geolocation', {
type: 'string',
maxlength: 2000,
nullable: true
});

module.exports.config = {
transaction: true
};
4 changes: 1 addition & 3 deletions core/server/data/schema/commands.js
Original file line number Diff line number Diff line change
@@ -162,9 +162,7 @@ function createColumnMigration(...migrations) {
}
}

return async function columnMigration(options) {
const conn = options.transacting || options.connection;

return async function columnMigration(conn) {
for (const migration of migrations) {
await runColumnMigration(conn, migration);
}
45 changes: 45 additions & 0 deletions core/server/data/schema/default-settings.json
Original file line number Diff line number Diff line change
@@ -383,5 +383,50 @@
"defaultValue": "[]",
"type": "array"
}
},
"newsletter": {
"newsletter_show_badge": {
"defaultValue": "true",
"validations": {
"isEmpty": false,
"isIn": [
[
"true",
"false"
]
]
},
"type": "boolean"
},
"newsletter_show_header": {
"defaultValue": "true",
"validations": {
"isEmpty": false,
"isIn": [
[
"true",
"false"
]
]
},
"type": "boolean"
},
"newsletter_body_font_category": {
"defaultValue": "sans_serif",
"validations": {
"isEmpty": false,
"isIn": [
[
"serif",
"sans_serif"
]
]
},
"type": "string"
},
"newsletter_footer_content": {
"defaultValue": "",
"type": "string"
}
}
}
16 changes: 15 additions & 1 deletion core/server/data/schema/schema.js
Original file line number Diff line number Diff line change
@@ -29,7 +29,13 @@ module.exports = {
defaultTo: 'public',
validations: {isIn: [['public', 'members', 'paid']]}
},
send_email_when_published: {type: 'bool', nullable: true, defaultTo: false},
email_recipient_filter: {
type: 'string',
maxlength: 50,
nullable: false,
defaultTo: 'none',
validations: {isIn: [['none', 'all', 'free', 'paid']]}
},
/**
* @deprecated: `author_id`, might be removed in Ghost 3.0
* If we keep it, then only, because you can easier query post.author_id than posts_authors[*].sort_order.
@@ -446,6 +452,13 @@ module.exports = {
defaultTo: 'pending',
validations: {isIn: [['pending', 'submitting', 'submitted', 'failed']]}
},
recipient_filter: {
type: 'string',
maxlength: 50,
nullable: false,
defaultTo: 'paid',
validations: {isIn: [['all', 'free', 'paid']]}
},
error: {type: 'string', maxlength: 2000, nullable: true},
error_data: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
meta: {type: 'text', maxlength: 65535, nullable: true},
@@ -456,6 +469,7 @@ module.exports = {
reply_to: {type: 'string', maxlength: 2000, nullable: true},
html: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
plaintext: {type: 'text', maxlength: 1000000000, fieldtype: 'long', nullable: true},
track_opens: {type: 'bool', nullable: false, defaultTo: false},
submitted_at: {type: 'dateTime', nullable: false},
created_at: {type: 'dateTime', nullable: false},
created_by: {type: 'string', maxlength: 24, nullable: false},
2 changes: 1 addition & 1 deletion core/server/lib/fs/package-json/read.js
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ const readPackage = function readPackage(packagePath, packageName) {
};

const readPackages = function readPackages(packagePath) {
return fs.readdir(packagePath)
return Promise.resolve(fs.readdir(packagePath))
.filter(function (packageName) {
// Filter out things which are not packages by regex
if (packageName.match(notAPackageRegex)) {
2 changes: 1 addition & 1 deletion core/server/lib/image/gravatar.js
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ module.exports.lookup = function lookup(userData, timeout) {
return Promise.resolve();
}

return request('https:' + gravatarUrl + '&d=404&r=x', {timeout: timeout || 2 * 1000})
return Promise.resolve(request('https:' + gravatarUrl + '&d=404&r=x', {timeout: timeout || 2 * 1000}))
.then(function () {
gravatarUrl += '&d=mm&r=x';

4 changes: 3 additions & 1 deletion core/server/models/email.js
Original file line number Diff line number Diff line change
@@ -8,14 +8,16 @@ const Email = ghostBookshelf.Model.extend({
return {
uuid: uuid.v4(),
status: 'pending',
recipient_filter: 'paid',
stats: JSON.stringify({
delivered: 0,
failed: 0,
opened: 0,
clicked: 0,
unsubscribed: 0,
complaints: 0
})
}),
track_opens: false
};
},

20 changes: 9 additions & 11 deletions core/server/models/post.js
Original file line number Diff line number Diff line change
@@ -50,12 +50,12 @@ Post = ghostBookshelf.Model.extend({
}

return {
send_email_when_published: false,
uuid: uuid.v4(),
status: 'draft',
featured: false,
type: 'post',
visibility: visibility
visibility: visibility,
email_recipient_filter: 'none'
};
},

@@ -504,19 +504,17 @@ Post = ghostBookshelf.Model.extend({
}
}

// send_email_when_published is read-only and should only be set using a query param when publishing/scheduling
if (options.send_email_when_published && this.hasChanged('status') && (newStatus === 'published' || newStatus === 'scheduled')) {
this.set('send_email_when_published', true);
// email_recipient_filter is read-only and should only be set using a query param when publishing/scheduling
if (options.email_recipient_filter && options.email_recipient_filter !== 'none' && this.hasChanged('status') && (newStatus === 'published' || newStatus === 'scheduled')) {
this.set('email_recipient_filter', options.email_recipient_filter);
}

// ensure draft posts have the send_email_when_published reset unless an email has already been sent
// ensure draft posts have the email_recipient_filter reset unless an email has already been sent
if (newStatus === 'draft' && this.hasChanged('status')) {
ops.push(function ensureSendEmailWhenPublishedIsUnchanged() {
return self.related('email').fetch({transacting: options.transacting}).then((email) => {
if (email) {
self.set('send_email_when_published', true);
} else {
self.set('send_email_when_published', false);
if (!email) {
self.set('email_recipient_filter', 'none');
}
});
});
@@ -842,7 +840,7 @@ Post = ghostBookshelf.Model.extend({
findPage: ['status'],
findAll: ['columns', 'filter'],
destroy: ['destroyAll', 'destroyBy'],
edit: ['filter', 'send_email_when_published', 'force_rerender']
edit: ['filter', 'email_recipient_filter', 'force_rerender']
};

// The post model additionally supports having a formats option
5 changes: 0 additions & 5 deletions core/server/overrides.js
Original file line number Diff line number Diff line change
@@ -23,8 +23,3 @@ const {extract, hasProvider} = require('oembed-parser'); // eslint-disable-line
* - be careful when you work with date operations, therefor always wrap a date into moment
*/
moment.tz.setDefault('UTC');

/**
* https://github.com/TryGhost/Ghost/issues/9064
*/
global.Promise = require('bluebird');
6 changes: 3 additions & 3 deletions core/server/services/auth/session/index.js
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ const ssoAdapter = adapterManager.getAdapter('sso');
module.exports.createSessionFromToken = sessionFromToken({
callNextWithError: false,
createSession: sessionService.createSessionForUser,
findUserByLookup: ssoAdapter.getUserForIdentity,
getLookupFromToken: ssoAdapter.getIdentityFromCredentials,
getTokenFromRequest: ssoAdapter.getRequestCredentials
findUserByLookup: ssoAdapter.getUserForIdentity.bind(ssoAdapter),
getLookupFromToken: ssoAdapter.getIdentityFromCredentials.bind(ssoAdapter),
getTokenFromRequest: ssoAdapter.getRequestCredentials.bind(ssoAdapter)
});
1 change: 1 addition & 0 deletions core/server/services/bulk-email/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const _ = require('lodash');
const Promise = require('bluebird');
const moment = require('moment-timezone');
const errors = require('@tryghost/errors');
const {i18n} = require('../../lib/common');
5 changes: 5 additions & 0 deletions core/server/services/bulk-email/mailgun.js
Original file line number Diff line number Diff line change
@@ -92,6 +92,11 @@ function send(message, recipientData, replacements) {
messageData['o:testmode'] = true;
}

// enable tracking if turned on for this email
if (message.track_opens) {
messageData['o:tracking-opens'] = true;
}

return new Promise((resolve, reject) => {
mailgunInstance.messages().send(messageData, (error, body) => {
if (error) {
38 changes: 34 additions & 4 deletions core/server/services/mega/mega.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
const _ = require('lodash');
const Promise = require('bluebird');
const debug = require('ghost-ignition').debug('mega');
const url = require('url');
const moment = require('moment');
const ObjectID = require('bson-objectid');
const errors = require('@tryghost/errors');
const {events, i18n} = require('../../lib/common');
const logging = require('../../../shared/logging');
const config = require('../../../shared/config');
const settingsCache = require('../settings/cache');
const membersService = require('../members');
const bulkEmailService = require('../bulk-email');
@@ -68,6 +70,9 @@ const sendTestEmail = async (postModel, toEmails) => {
};
}));

// enable tracking for previews to match real-world behaviour
emailData.track_opens = config.get('enableDeveloperExperiments');

const response = await bulkEmailService.send(emailData, recipients);

if (response instanceof bulkEmailService.FailedBatch) {
@@ -90,8 +95,21 @@ const addEmail = async (postModel, options) => {
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
const filterOptions = Object.assign({}, knexOptions, {filter: 'subscribed:true', limit: 1});

if (postModel.get('visibility') === 'paid') {
const emailRecipientFilter = postModel.get('email_recipient_filter');

switch (emailRecipientFilter) {
case 'paid':
filterOptions.paid = true;
break;
case 'free':
filterOptions.paid = false;
break;
case 'all':
break;
case 'none':
throw new Error('Cannot sent email to "none" email_recipient_filter');
default:
throw new Error(`Unknown email_recipient_filter ${emailRecipientFilter}`);
}

const startRetrieve = Date.now();
@@ -121,7 +139,9 @@ const addEmail = async (postModel, options) => {
reply_to: emailData.replyTo,
html: emailData.html,
plaintext: emailData.plaintext,
submitted_at: moment().toDate()
submitted_at: moment().toDate(),
track_opens: config.get('enableDeveloperExperiments'),
recipient_filter: emailRecipientFilter
}, knexOptions);
} else {
return existing;
@@ -258,13 +278,23 @@ async function sendEmailJob({emailModel, options}) {
// instantiations and associated processing and event loop blocking
async function getEmailMemberRows({emailModel, options}) {
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
const postModel = await models.Post.findOne({id: emailModel.get('post_id')}, knexOptions);

// TODO: this will clobber a user-assigned filter if/when we allow emails to be sent to filtered member lists
const filterOptions = Object.assign({}, knexOptions, {filter: 'subscribed:true'});

if (postModel.get('visibility') === 'paid') {
const recipientFilter = emailModel.get('recipient_filter');

switch (recipientFilter) {
case 'paid':
filterOptions.paid = true;
break;
case 'free':
filterOptions.paid = false;
break;
case 'all':
break;
default:
throw new Error(`Unknown recipient_filter ${recipientFilter}`);
}

const startRetrieve = Date.now();
10 changes: 7 additions & 3 deletions core/server/services/mega/post-email-serializer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
const _ = require('lodash');
const juice = require('juice');
const template = require('./template');
const config = require('../../../shared/config');
const settingsCache = require('../../services/settings/cache');
const urlUtils = require('../../../shared/url-utils');
const moment = require('moment-timezone');
@@ -153,8 +152,13 @@ const serialize = async (postModel, options = {isBrowserPreview: false}) => {
uppercaseHeadings: false
});

const templateConfig = config.get('members:emailTemplate');
let htmlTemplate = template({post, site: getSite(), templateConfig});
const templateSettings = {
showSiteHeader: settingsCache.get('newsletter_show_header'),
bodyFontCategory: settingsCache.get('newsletter_body_font_category'),
showBadge: settingsCache.get('newsletter_show_badge'),
footerContent: settingsCache.get('newsletter_footer_content')
};
let htmlTemplate = template({post, site: getSite(), templateSettings});
if (options.isBrowserPreview) {
const previewUnsubscribeUrl = createUnsubscribeUrl();
htmlTemplate = htmlTemplate.replace('%recipient.unsubscribe_url%', previewUnsubscribeUrl);
37 changes: 28 additions & 9 deletions core/server/services/mega/template.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint indent: warn, no-irregular-whitespace: warn */
module.exports = ({post, site, templateConfig}) => {
const iff = (cond, yes, no) => (cond ? yes : no);
module.exports = ({post, site, templateSettings}) => {
const date = new Date();
return `<!doctype html>
<html>
@@ -346,6 +347,15 @@ figure blockquote p {
border-bottom: 1px solid #e5eff5;
}
.post-content-sans-serif {
max-width: 600px !important;
font-size: 17px;
line-height: 1.5em;
color: #23323D;
padding-bottom: 20px;
border-bottom: 1px solid #e5eff5;
}
.post-content a {
color: #08121A;
text-decoration: underline;
@@ -517,8 +527,16 @@ figure blockquote p {
margin-top: 20px;
text-align: center;
font-size: 13px;
padding-bottom: 40px;
padding-top: 50px;
padding-bottom: 10px;
padding-top: 10px;
padding-left: 30px;
padding-right: 30px;
line-height: 1.5em;
}
.footer a {
color: #738a94;
text-decoration: underline;
}
/* -------------------------------------
@@ -815,9 +833,10 @@ figure blockquote p {
}
${ templateConfig.showPoweredBy ? `
${ templateSettings.showBadge ? `
.footer-powered {
text-align: center;
padding-top: 70px;
padding-bottom: 40px;
}
@@ -858,7 +877,7 @@ ${ templateConfig.showPoweredBy ? `
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
${ templateConfig.showSiteHeader ? `
${ templateSettings.showSiteHeader ? `
<tr>
<td class="site-info" width="100%" align="center">
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
@@ -898,11 +917,10 @@ ${ templateConfig.showPoweredBy ? `
</tr>
` : ``}
<tr>
<td class="post-content">
<td class="${(templateSettings.bodyFontCategory === 'sans_serif') ? `post-content-sans-serif` : `post-content` }">
<!-- POST CONTENT START -->
${post.html}
<!-- POST CONTENT END -->
</td>
</tr>
</table>
@@ -913,12 +931,13 @@ ${ templateConfig.showPoweredBy ? `
<tr>
<td class="wrapper" align="center">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%">
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="100%" style="padding-top: 40px; padding-bottom: 30px;">
${iff(!!templateSettings.footerContent, `<tr><td class="footer">${templateSettings.footerContent}</td></tr>`, '')}
<tr>
<td class="footer">${site.title} &copy; ${date.getFullYear()} – <a href="%recipient.unsubscribe_url%">Unsubscribe</a></td>
</tr>
${ templateConfig.showPoweredBy ? `
${ templateSettings.showBadge ? `
<tr>
<td class="footer-powered"><a href="https://ghost.org/"><img src="https://static.ghost.org/v3.0.0/images/powered.png" border="0" width="142" height="30" class="gh-powered" alt="Publish with Ghost"></a></td>
</tr>
3 changes: 2 additions & 1 deletion core/server/translations/en.json
Original file line number Diff line number Diff line change
@@ -369,7 +369,8 @@
},
"redirects": {
"missingFile": "Please select a JSON file.",
"invalidFile": "Please select a valid JSON file to import."
"invalidFile": "Please select a valid JSON file to import.",
"yamlParse": "YAML input cannot be a plain string. Check the format of your YAML file."
},
"resource": {
"resourceNotFound": "{resource} not found."
8 changes: 4 additions & 4 deletions core/server/web/admin/views/default-prod.html
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
<title>Ghost Admin</title>


<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%223.37%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%223.38%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />

<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
@@ -34,7 +34,7 @@


<link rel="stylesheet" href="assets/vendor.min-5f3241a89eba4699965f4f257ab2e40a.css">
<link rel="stylesheet" href="assets/ghost.min-3a4d4a32ec8c3202fb84555bfc4ff07f.css" title="light">
<link rel="stylesheet" href="assets/ghost.min-87dcc5855d1ebbf07c16a5d766b1c88b.css" title="light">



@@ -52,8 +52,8 @@
<div id="ember-basic-dropdown-wormhole"></div>


<script src="assets/vendor.min-927298149aff3b1dd094bfb6b92f6de3.js"></script>
<script src="assets/ghost.min-576a456dcad5dddbdab6de6aa934dc9f.js"></script>
<script src="assets/vendor.min-b6b84abab9c092983d4c6fbfc7ad7551.js"></script>
<script src="assets/ghost.min-b4187f4b25eb5b6f59c3e9c3f6215fca.js"></script>

</body>
</html>
8 changes: 4 additions & 4 deletions core/server/web/admin/views/default.html
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
<title>Ghost Admin</title>


<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%223.37%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
<meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%223.38%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />

<meta name="HandheldFriendly" content="True" />
<meta name="MobileOptimized" content="320" />
@@ -34,7 +34,7 @@


<link rel="stylesheet" href="assets/vendor.min-5f3241a89eba4699965f4f257ab2e40a.css">
<link rel="stylesheet" href="assets/ghost.min-3a4d4a32ec8c3202fb84555bfc4ff07f.css" title="light">
<link rel="stylesheet" href="assets/ghost.min-87dcc5855d1ebbf07c16a5d766b1c88b.css" title="light">



@@ -52,8 +52,8 @@
<div id="ember-basic-dropdown-wormhole"></div>


<script src="assets/vendor.min-927298149aff3b1dd094bfb6b92f6de3.js"></script>
<script src="assets/ghost.min-576a456dcad5dddbdab6de6aa934dc9f.js"></script>
<script src="assets/vendor.min-b6b84abab9c092983d4c6fbfc7ad7551.js"></script>
<script src="assets/ghost.min-b4187f4b25eb5b6f59c3e9c3f6215fca.js"></script>

</body>
</html>
11 changes: 10 additions & 1 deletion core/server/web/api/canary/admin/routes.js
Original file line number Diff line number Diff line change
@@ -219,14 +219,23 @@ module.exports = function apiRoutes() {
router.post('/invites', mw.authAdminApi, http(apiCanary.invites.add));
router.del('/invites/:id', mw.authAdminApi, http(apiCanary.invites.destroy));

// ## Redirects (JSON based)
// ## Redirects
// TODO: yaml support has been added to https://github.com/TryGhost/Ghost/issues/11085
// The `/json` endpoints below are left for backward compatibility. They'll be removed in v4.
router.get('/redirects/json', mw.authAdminApi, http(apiCanary.redirects.download));
router.post('/redirects/json',
mw.authAdminApi,
apiMw.upload.single('redirects'),
apiMw.upload.validation({type: 'redirects'}),
http(apiCanary.redirects.upload)
);
router.get('/redirects/download', mw.authAdminApi, http(apiCanary.redirects.download));
router.post('/redirects/upload',
mw.authAdminApi,
apiMw.upload.single('redirects'),
apiMw.upload.validation({type: 'redirects'}),
http(apiCanary.redirects.upload)
);

// ## Webhooks (RESTHooks)
router.post('/webhooks', mw.authAdminApi, http(apiCanary.webhooks.add));
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const logging = require('../../../shared/logging');
const express = require('../../../shared/express');
const jobService = require('../../services/jobs');
const path = require('path');
const logging = require('../../../../shared/logging');
const express = require('../../../../shared/express');
const jobService = require('../../../services/jobs');

/** A bunch of helper routes for testing purposes */
module.exports = function testRoutes() {
@@ -40,5 +41,32 @@ module.exports = function testRoutes() {
res.sendStatus(202);
});

router.get('/schedule/:schedule/:name*?', (req, res) => {
if (!req.params.schedule) {
return res.sendStatus(400, 'schedule parameter cannot be mepty');
}

const schedule = req.params.schedule;
logging.info('Achedule a Job with schedule of:', schedule, req.params.name);

if (req.params.name) {
const jobPath = path.resolve(__dirname, 'jobs', req.params.name);
jobService.scheduleJob(schedule, jobPath);
} else {
jobService.scheduleJob(schedule, () => {
return new Promise((resolve) => {
logging.info('Start scheduled Job');

setTimeout(() => {
logging.info('End scheduled Job run', schedule);
resolve();
}, 20 * 1000);
});
}, {});
}

res.sendStatus(202);
});

return router;
};
17 changes: 17 additions & 0 deletions core/server/web/api/testmode/jobs/say-hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const logging = require('../../../../../shared/logging');

const helloJob = () => {
logging.info('Starting hello job');

logging.info('Gonna say "hi" in 5 seconds');

return new Promise((resolve) => {
setTimeout(() => {
logging.info('hi!');
logging.info('Ending hello job run.');
resolve();
}, 5 * 1000);
});
};

module.exports = helloJob;
11 changes: 5 additions & 6 deletions core/server/web/shared/middlewares/custom-redirects.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
const fs = require('fs-extra');
const express = require('../../../../shared/express');
const url = require('url');
const path = require('path');
const debug = require('ghost-ignition').debug('web:shared:mw:custom-redirects');
const config = require('../../../../shared/config');
const urlUtils = require('../../../../shared/url-utils');
@@ -20,8 +18,8 @@ _private.registerRoutes = () => {
customRedirectsRouter = express.Router('redirects');

try {
let redirects = fs.readFileSync(path.join(config.getContentPath('data'), 'redirects.json'), 'utf-8');
redirects = JSON.parse(redirects);
const redirects = redirectsService.settings.loadRedirectsFile();

redirectsService.validation.validate(redirects);

redirects.forEach((redirect) => {
@@ -77,11 +75,12 @@ _private.registerRoutes = () => {
} catch (err) {
if (errors.utils.isIgnitionError(err)) {
logging.error(err);
} else if (err.code !== 'ENOENT') {
} else {
logging.error(new errors.IncorrectUsageError({
message: i18n.t('errors.middleware.redirects.register'),
context: err.message,
help: 'https://ghost.org/docs/api/handlebars-themes/routing/redirects/'
help: 'https://ghost.org/docs/api/handlebars-themes/routing/redirects/',
err
}));
}
}
2 changes: 1 addition & 1 deletion core/shared/config/defaults.json
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@
"paymentProcessors": [],
"emailTemplate": {
"showSiteHeader": true,
"showPoweredBy": false
"showPoweredBy": true
}
},
"logging": {
4 changes: 2 additions & 2 deletions core/shared/config/overrides.json
Original file line number Diff line number Diff line change
@@ -43,8 +43,8 @@
"contentTypes": ["application/zip", "application/x-zip-compressed", "application/octet-stream"]
},
"redirects": {
"extensions": [".json"],
"contentTypes": ["text/plain", "application/octet-stream", "application/json"]
"extensions": [".json", ".yaml"],
"contentTypes": ["text/plain", "text/yaml", "application/octet-stream", "application/json", "application/yaml", "application/x-yaml"]
},
"routes": {
"extensions": [".yaml"],
8 changes: 4 additions & 4 deletions core/shared/sentry.js
Original file line number Diff line number Diff line change
@@ -2,10 +2,6 @@ const config = require('./config');
const sentryConfig = config.get('sentry');
const errors = require('@tryghost/errors');

const expressNoop = function (req, res, next) {
next();
};

if (sentryConfig && !sentryConfig.disabled) {
const Sentry = require('@sentry/node');
const version = require('../server/lib/ghost-version').full;
@@ -34,6 +30,10 @@ if (sentryConfig && !sentryConfig.disabled) {
captureException: Sentry.captureException
};
} else {
const expressNoop = function (req, res, next) {
next();
};

module.exports = {
requestHandler: expressNoop,
errorHandler: expressNoop,
49 changes: 24 additions & 25 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ghost",
"version": "3.37.1",
"version": "3.38.0",
"description": "The professional publishing platform",
"author": "Ghost Foundation",
"homepage": "https://ghost.org",
@@ -41,33 +41,33 @@
},
"dependencies": {
"@nexes/nql": "0.5.0",
"@sentry/node": "5.27.2",
"@tryghost/adapter-manager": "0.1.11",
"@tryghost/admin-api-schema": "1.3.0",
"@tryghost/bootstrap-socket": "0.2.2",
"@tryghost/constants": "0.1.1",
"@tryghost/errors": "0.2.4",
"@sentry/node": "5.27.4",
"@tryghost/adapter-manager": "0.2.0",
"@tryghost/admin-api-schema": "1.4.0",
"@tryghost/bootstrap-socket": "0.2.3",
"@tryghost/constants": "0.1.2",
"@tryghost/errors": "0.2.5",
"@tryghost/helpers": "1.1.34",
"@tryghost/image-transform": "1.0.3",
"@tryghost/job-manager": "0.1.1",
"@tryghost/kg-card-factory": "2.1.3",
"@tryghost/job-manager": "0.2.0",
"@tryghost/kg-card-factory": "2.1.4",
"@tryghost/kg-default-atoms": "2.0.2",
"@tryghost/kg-default-cards": "3.0.0",
"@tryghost/kg-markdown-html-renderer": "2.0.3",
"@tryghost/kg-default-cards": "3.0.1",
"@tryghost/kg-markdown-html-renderer": "2.0.4",
"@tryghost/kg-mobiledoc-html-renderer": "3.0.1",
"@tryghost/magic-link": "0.6.1",
"@tryghost/members-api": "0.34.2",
"@tryghost/members-csv": "0.3.2",
"@tryghost/members-ssr": "0.8.5",
"@tryghost/mw-session-from-token": "0.1.8",
"@tryghost/promise": "0.1.1",
"@tryghost/security": "0.2.0",
"@tryghost/session-service": "0.1.9",
"@tryghost/social-urls": "0.1.15",
"@tryghost/mw-session-from-token": "0.1.9",
"@tryghost/promise": "0.1.2",
"@tryghost/security": "0.2.1",
"@tryghost/session-service": "0.1.10",
"@tryghost/social-urls": "0.1.16",
"@tryghost/string": "0.1.14",
"@tryghost/url-utils": "0.6.23",
"@tryghost/vhost-middleware": "1.0.9",
"@tryghost/zip": "1.1.4",
"@tryghost/vhost-middleware": "1.0.10",
"@tryghost/zip": "1.1.5",
"ajv": "6.12.6",
"amperize": "0.6.1",
"analytics-node": "3.4.0-beta.3",
@@ -90,7 +90,7 @@
"express-query-boolean": "2.0.0",
"express-session": "1.17.1",
"fs-extra": "9.0.1",
"ghost-ignition": "4.2.3",
"ghost-ignition": "4.2.4",
"ghost-storage-base": "0.0.4",
"glob": "7.1.6",
"got": "9.6.0",
@@ -129,7 +129,7 @@
"path-match": "1.2.4",
"probe-image-size": "5.0.0",
"rss": "1.2.2",
"sanitize-html": "2.1.1",
"sanitize-html": "2.1.2",
"semver": "7.3.2",
"stoppable": "1.1.0",
"tough-cookie": "4.0.0",
@@ -139,22 +139,21 @@
"applicationinsights": "^1.0.0"
},
"optionalDependencies": {
"@tryghost/html-to-mobiledoc": "0.7.6",
"@tryghost/html-to-mobiledoc": "0.7.7",
"sqlite3": "4.2.0"
},
"devDependencies": {
"@lodder/grunt-postcss": "3.0.0",
"coffeescript": "2.5.1",
"cssnano": "4.1.10",
"eslint": "7.12.1",
"eslint": "7.13.0",
"eslint-plugin-ghost": "2.0.0",
"grunt": "1.3.0",
"grunt-bg-shell": "2.3.3",
"grunt-contrib-clean": "2.0.0",
"grunt-contrib-compress": "1.6.0",
"grunt-contrib-copy": "1.0.0",
"grunt-contrib-symlink": "1.0.0",
"grunt-contrib-uglify": "5.0.0",
"grunt-contrib-watch": "1.1.0",
"grunt-express-server": "0.5.4",
"grunt-mocha-cli": "6.0.0",
@@ -164,13 +163,13 @@
"jwks-rsa": "1.11.0",
"mocha": "8.2.1",
"mock-knex": "0.4.9",
"nock": "13.0.4",
"nock": "13.0.5",
"papaparse": "5.3.0",
"proxyquire": "2.1.3",
"rewire": "5.0.0",
"should": "13.2.3",
"sinon": "9.2.1",
"supertest": "6.0.0",
"supertest": "6.0.1",
"tmp": "0.0.33"
},
"resolutions": {
438 changes: 232 additions & 206 deletions yarn.lock

Large diffs are not rendered by default.

0 comments on commit fe9d9f6

Please sign in to comment.