ghost 5.1.1 → 5.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +4 -4
- package/content/themes/casper/package.json +1 -1
- package/content/themes/casper/partials/post-card.hbs +11 -2
- package/core/built/assets/{ghost-dark-2c7356ee2c7b16f27a1f97932bd67e0b.css → ghost-dark-923c90399aa560625a983a6ae9abb38c.css} +1 -1
- package/core/built/assets/{ghost.min-95041d583ba9b90f587a92839509ebd6.js → ghost.min-971a0ff706bbf9e24f0b0a5770c8fc41.js} +159 -156
- package/core/built/assets/{ghost.min-e8119caef1eb246cf596cd83eb67b97e.css → ghost.min-c74f50609022cebf13ad28810def68b6.css} +1 -1
- package/core/built/assets/img/themes/Casper-19b7267aac5acc6abfaaed7e41eddae8.jpg +0 -0
- package/core/server/data/db/connection.js +13 -0
- package/core/server/data/importer/importers/data/products.js +29 -2
- package/core/server/data/importer/importers/data/stripe-products.js +24 -2
- package/core/server/models/base/bookshelf.js +2 -0
- package/core/server/models/base/plugins/data-manipulation.js +1 -0
- package/core/server/models/base/plugins/relations.js +30 -0
- package/core/server/models/post.js +7 -4
- package/core/server/models/user.js +32 -4
- package/core/server/services/bulk-email/bulk-email-processor.js +1 -1
- package/core/server/services/mega/email-preview.js +1 -1
- package/core/server/services/mega/mega.js +7 -6
- package/core/server/web/admin/views/default-prod.html +3 -3
- package/core/server/web/admin/views/default.html +3 -3
- package/core/server/web/api/canary/admin/app.js +2 -2
- package/core/server/web/api/canary/content/app.js +1 -1
- package/core/server/web/members/app.js +3 -3
- package/core/shared/config/defaults.json +2 -2
- package/core/shared/config/utils.js +0 -9
- package/package.json +43 -42
- package/yarn.lock +381 -503
- package/core/built/assets/img/themes/Casper-c7e784d7188cc5d7f097d9b6c97b0263.jpg +0 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
const _ = require('lodash');
|
|
1
2
|
const knex = require('knex');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const logging = require('@tryghost/logging');
|
|
2
6
|
const config = require('../../../shared/config');
|
|
3
7
|
let knexInstance;
|
|
4
8
|
|
|
@@ -30,6 +34,15 @@ function configure(dbConfig) {
|
|
|
30
34
|
// Force bthreads to use child_process backend until a worker_thread-compatible version of sqlite3 is published
|
|
31
35
|
// https://github.com/mapbox/node-sqlite3/issues/1386
|
|
32
36
|
process.env.BTHREADS_BACKEND = 'child_process';
|
|
37
|
+
|
|
38
|
+
// In the default SQLite test config we set the path to /tmp/ghost-test.db,
|
|
39
|
+
// but this won't work on Windows, so we need to replace the /tmp bit with
|
|
40
|
+
// the Windows temp folder
|
|
41
|
+
const filename = dbConfig.connection.filename;
|
|
42
|
+
if (process.platform === 'win32' && _.isString(filename) && filename.match(/^\/tmp/)) {
|
|
43
|
+
dbConfig.connection.filename = filename.replace(/^\/tmp/, os.tmpdir());
|
|
44
|
+
logging.info(`Ghost DB path: ${dbConfig.connection.filename}`);
|
|
45
|
+
}
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
if (client === 'mysql2') {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const BaseImporter = require('./base');
|
|
3
3
|
const models = require('../../../../models');
|
|
4
|
+
const debug = require('@tryghost/debug')('importer:products');
|
|
4
5
|
|
|
5
6
|
class ProductsImporter extends BaseImporter {
|
|
6
7
|
constructor(allDataFromFile) {
|
|
@@ -8,12 +9,12 @@ class ProductsImporter extends BaseImporter {
|
|
|
8
9
|
modelName: 'Product',
|
|
9
10
|
dataKeyToImport: 'products',
|
|
10
11
|
requiredFromFile: ['stripe_prices'],
|
|
11
|
-
requiredExistingData: ['stripe_prices']
|
|
12
|
+
requiredExistingData: ['stripe_prices', 'products']
|
|
12
13
|
});
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
fetchExisting(modelOptions) {
|
|
16
|
-
return models.Product.findAll(_.merge({columns: ['products.id as id']}, modelOptions))
|
|
17
|
+
return models.Product.findAll(_.merge({columns: ['products.id as id', 'name', 'slug']}, modelOptions))
|
|
17
18
|
.then((existingData) => {
|
|
18
19
|
this.existingData = existingData.toJSON();
|
|
19
20
|
});
|
|
@@ -58,8 +59,34 @@ class ProductsImporter extends BaseImporter {
|
|
|
58
59
|
this.dataToImport = this.dataToImport.filter(item => !invalidProducts.includes(item.id));
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
preventDuplicates() {
|
|
63
|
+
debug('preventDuplicates');
|
|
64
|
+
let duplicateProducts = [];
|
|
65
|
+
_.each(this.dataToImport, (objectInFile) => {
|
|
66
|
+
const existingObject = _.find(
|
|
67
|
+
this.requiredExistingData.products,
|
|
68
|
+
{name: objectInFile.name, slug: objectInFile.slug}
|
|
69
|
+
);
|
|
70
|
+
// CASE: tier already exists
|
|
71
|
+
if (existingObject) {
|
|
72
|
+
debug(`skipping existing product ${objectInFile.name}`);
|
|
73
|
+
this.problems.push({
|
|
74
|
+
message: 'Entry was not imported and ignored. Detected duplicated entry.',
|
|
75
|
+
help: this.modelName,
|
|
76
|
+
context: JSON.stringify({
|
|
77
|
+
product: objectInFile
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
duplicateProducts.push(objectInFile.id);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// ignore products that already exist
|
|
84
|
+
this.dataToImport = this.dataToImport.filter(item => !duplicateProducts.includes(item.id));
|
|
85
|
+
}
|
|
86
|
+
|
|
61
87
|
replaceIdentifiers() {
|
|
62
88
|
// this has to be in replaceIdentifiers because it's after required* fields are set
|
|
89
|
+
this.preventDuplicates();
|
|
63
90
|
this.validateStripePrice();
|
|
64
91
|
return super.replaceIdentifiers();
|
|
65
92
|
}
|
|
@@ -8,6 +8,7 @@ class StripeProductsImporter extends BaseImporter {
|
|
|
8
8
|
super(allDataFromFile, {
|
|
9
9
|
modelName: 'StripeProduct',
|
|
10
10
|
dataKeyToImport: 'stripe_products',
|
|
11
|
+
requiredFromFile: ['products'],
|
|
11
12
|
requiredImportedData: ['products'],
|
|
12
13
|
requiredExistingData: ['products']
|
|
13
14
|
});
|
|
@@ -41,11 +42,32 @@ class StripeProductsImporter extends BaseImporter {
|
|
|
41
42
|
objectInFile.product_id = importedObject.id;
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
|
|
46
|
+
const existingObjectById = _.find(this.requiredExistingData.products, {id: objectInFile.product_id});
|
|
45
47
|
// CASE: the product exists in the db already
|
|
46
|
-
if (
|
|
48
|
+
if (existingObjectById) {
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
51
|
+
|
|
52
|
+
// CASE: we skipped product import because a product with the same name and slug exists in the DB
|
|
53
|
+
debug('lookup product by name and slug');
|
|
54
|
+
const productFromFile = _.find(
|
|
55
|
+
this.requiredFromFile.products,
|
|
56
|
+
{id: objectInFile.product_id}
|
|
57
|
+
);
|
|
58
|
+
if (productFromFile) {
|
|
59
|
+
// look for the existing product with the same name and slug
|
|
60
|
+
const existingObjectByNameAndSlug = _.find(
|
|
61
|
+
this.requiredExistingData.products,
|
|
62
|
+
{name: productFromFile.name, slug: productFromFile.slug}
|
|
63
|
+
);
|
|
64
|
+
if (existingObjectByNameAndSlug) {
|
|
65
|
+
debug(`resolved ${objectInFile.product_id} to ${existingObjectByNameAndSlug.name}`);
|
|
66
|
+
objectInFile.product_id = existingObjectByNameAndSlug.id;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
// CASE: we don't know what product this is for
|
|
50
72
|
debug(`ignoring stripe product ${objectInFile.stripe_product_id}`);
|
|
51
73
|
invalidProducts.push(objectInFile.id);
|
|
@@ -61,6 +61,8 @@ ghostBookshelf.plugin(require('./plugins/data-manipulation'));
|
|
|
61
61
|
|
|
62
62
|
ghostBookshelf.plugin(require('./plugins/overrides'));
|
|
63
63
|
|
|
64
|
+
ghostBookshelf.plugin(require('./plugins/relations'));
|
|
65
|
+
|
|
64
66
|
// Manages nested updates (relationships)
|
|
65
67
|
ghostBookshelf.plugin('bookshelf-relations', {
|
|
66
68
|
allowedOptions: ['context', 'importing', 'migrating'],
|
|
@@ -31,6 +31,7 @@ module.exports = function (Bookshelf) {
|
|
|
31
31
|
|
|
32
32
|
_.each(attrs, function each(value, key) {
|
|
33
33
|
if (value !== null
|
|
34
|
+
&& Object.prototype.hasOwnProperty.call(schema.tables, self.tableName)
|
|
34
35
|
&& Object.prototype.hasOwnProperty.call(schema.tables[self.tableName], key)
|
|
35
36
|
&& schema.tables[self.tableName][key].type === 'dateTime') {
|
|
36
37
|
attrs[key] = moment(value).format('YYYY-MM-DD HH:mm:ss');
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('bookshelf')} Bookshelf
|
|
3
|
+
*/
|
|
4
|
+
module.exports = function (Bookshelf) {
|
|
5
|
+
Bookshelf.Model = Bookshelf.Model.extend({
|
|
6
|
+
/**
|
|
7
|
+
* Return a relation, and load it if it hasn't been loaded already (or force a refresh with the forceRefresh option).
|
|
8
|
+
* refs https://github.com/TryGhost/Team/issues/1626
|
|
9
|
+
* @param {string} name Name of the relation to load
|
|
10
|
+
* @param {Object} [options] Options to pass to the fetch when not yet loaded (or when force refreshing)
|
|
11
|
+
* @param {boolean} [options.forceRefresh] If true, the relation will be fetched again even if it has already been loaded.
|
|
12
|
+
* @returns {Promise<import('bookshelf').Model|import('bookshelf').Collection|null>}
|
|
13
|
+
*/
|
|
14
|
+
getLazyRelation: async function (name, options = {}) {
|
|
15
|
+
if (this.relations[name] && !options.forceRefresh) {
|
|
16
|
+
// Relation was already loaded
|
|
17
|
+
return this.relations[name];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!this[name]) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
// Not yet loaded, or force refresh
|
|
24
|
+
// Note that we don't use .refresh on the relation on options.forceRefresh
|
|
25
|
+
// Because the relation can also be a collection, which doesn't have a refresh method
|
|
26
|
+
this.relations[name] = this[name]();
|
|
27
|
+
return this.relations[name].fetch(options);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
};
|
|
@@ -645,12 +645,12 @@ Post = ghostBookshelf.Model.extend({
|
|
|
645
645
|
|
|
646
646
|
// ### Business logic for published_at and published_by
|
|
647
647
|
// If the current status is 'published' and published_at is not set, set it to now
|
|
648
|
-
if (newStatus === 'published' && !publishedAt) {
|
|
648
|
+
if ((newStatus === 'published' || newStatus === 'sent') && !publishedAt) {
|
|
649
649
|
this.set('published_at', new Date());
|
|
650
650
|
}
|
|
651
651
|
|
|
652
652
|
// If the current status is 'published' and the status has just changed ensure published_by is set correctly
|
|
653
|
-
if (newStatus === 'published' && this.hasChanged('status')) {
|
|
653
|
+
if ((newStatus === 'published' || newStatus === 'sent') && this.hasChanged('status')) {
|
|
654
654
|
// unless published_by is set and we're importing, set published_by to contextUser
|
|
655
655
|
if (!(this.get('published_by') && options.importing)) {
|
|
656
656
|
this.set('published_by', String(this.contextUser(options)));
|
|
@@ -666,7 +666,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
666
666
|
if (options.newsletter
|
|
667
667
|
&& !this.get('newsletter_id')
|
|
668
668
|
&& this.hasChanged('status')
|
|
669
|
-
&& (newStatus === 'published' || newStatus === 'scheduled')) {
|
|
669
|
+
&& (newStatus === 'published' || newStatus === 'scheduled' || newStatus === 'sent')) {
|
|
670
670
|
// Map the passed slug to the id + validate the passed newsletter
|
|
671
671
|
ops.push(async () => {
|
|
672
672
|
const newsletter = await Newsletter.findOne({slug: options.newsletter}, {transacting: options.transacting, filter: 'status:active'});
|
|
@@ -691,7 +691,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
691
691
|
// ensure draft posts have the email_recipient_filter reset unless an email has already been sent
|
|
692
692
|
if (newStatus === 'draft' && this.hasChanged('status')) {
|
|
693
693
|
ops.push(function ensureSendEmailWhenPublishedIsUnchanged() {
|
|
694
|
-
return self.
|
|
694
|
+
return self.getLazyRelation('email', {transacting: options.transacting}).then((email) => {
|
|
695
695
|
if (!email) {
|
|
696
696
|
self.set('email_recipient_filter', 'all');
|
|
697
697
|
self.set('newsletter_id', null);
|
|
@@ -705,6 +705,9 @@ Post = ghostBookshelf.Model.extend({
|
|
|
705
705
|
const hasEmailOnlyFlag = _.get(attrs, 'posts_meta.email_only') || model.related('posts_meta').get('email_only');
|
|
706
706
|
if (hasEmailOnlyFlag && (newStatus === 'published') && this.hasChanged('status')) {
|
|
707
707
|
this.set('status', 'sent');
|
|
708
|
+
} else if (!hasEmailOnlyFlag && (newStatus === 'sent') && this.hasChanged('status')) {
|
|
709
|
+
// Prevent setting status to 'sent' for non email only posts
|
|
710
|
+
this.set('status', 'published');
|
|
708
711
|
}
|
|
709
712
|
|
|
710
713
|
// If a title is set, not the same as the old title, a draft post, and has never been published
|
|
@@ -13,11 +13,17 @@ const validatePassword = require('../lib/validate-password');
|
|
|
13
13
|
const permissions = require('../services/permissions');
|
|
14
14
|
const urlUtils = require('../../shared/url-utils');
|
|
15
15
|
const activeStates = ['active', 'warn-1', 'warn-2', 'warn-3', 'warn-4'];
|
|
16
|
+
const ASSIGNABLE_ROLES = ['Administrator', 'Editor', 'Author', 'Contributor'];
|
|
16
17
|
|
|
17
18
|
const messages = {
|
|
18
19
|
valueCannotBeBlank: 'Value in [{tableName}.{columnKey}] cannot be blank.',
|
|
19
20
|
onlyOneRolePerUserSupported: 'Only one role per user is supported at the moment.',
|
|
20
21
|
methodDoesNotSupportOwnerRole: 'This method does not support assigning the owner role',
|
|
22
|
+
invalidRoleValue: {
|
|
23
|
+
message: 'Role should be an existing role id or a role name',
|
|
24
|
+
context: 'Invalid role assigned to the user',
|
|
25
|
+
help: 'Change the provided role to a role id or use a role name.'
|
|
26
|
+
},
|
|
21
27
|
userNotFound: 'User not found',
|
|
22
28
|
ownerNotFound: 'Owner not found',
|
|
23
29
|
notEnoughPermission: 'You do not have permission to perform this action',
|
|
@@ -507,7 +513,7 @@ User = ghostBookshelf.Model.extend({
|
|
|
507
513
|
}
|
|
508
514
|
|
|
509
515
|
ops.push(function update() {
|
|
510
|
-
return ghostBookshelf.Model.edit.call(self, data, options).then((user) => {
|
|
516
|
+
return ghostBookshelf.Model.edit.call(self, data, options).then(async (user) => {
|
|
511
517
|
let roleId;
|
|
512
518
|
|
|
513
519
|
if (!data.roles || !data.roles.length) {
|
|
@@ -521,7 +527,29 @@ User = ghostBookshelf.Model.extend({
|
|
|
521
527
|
if (roles.models[0].id === roleId) {
|
|
522
528
|
return;
|
|
523
529
|
}
|
|
524
|
-
|
|
530
|
+
|
|
531
|
+
if (ASSIGNABLE_ROLES.includes(roleId)) {
|
|
532
|
+
// return if the role is already assigned
|
|
533
|
+
if (roles.models[0].get('name') === roleId) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return ghostBookshelf.model('Role').findOne({
|
|
538
|
+
name: roleId
|
|
539
|
+
});
|
|
540
|
+
} else if (ObjectId.isValid(roleId)){
|
|
541
|
+
return ghostBookshelf.model('Role').findOne({
|
|
542
|
+
id: roleId
|
|
543
|
+
});
|
|
544
|
+
} else {
|
|
545
|
+
return Promise.reject(
|
|
546
|
+
new errors.ValidationError({
|
|
547
|
+
message: tpl(messages.invalidRoleValue.message),
|
|
548
|
+
context: tpl(messages.invalidRoleValue.context),
|
|
549
|
+
help: tpl(messages.invalidRoleValue.help)
|
|
550
|
+
})
|
|
551
|
+
);
|
|
552
|
+
}
|
|
525
553
|
}).then((roleToAssign) => {
|
|
526
554
|
if (roleToAssign && roleToAssign.get('name') === 'Owner') {
|
|
527
555
|
return Promise.reject(
|
|
@@ -529,9 +557,9 @@ User = ghostBookshelf.Model.extend({
|
|
|
529
557
|
message: tpl(messages.methodDoesNotSupportOwnerRole)
|
|
530
558
|
})
|
|
531
559
|
);
|
|
532
|
-
} else {
|
|
560
|
+
} else if (roleToAssign) {
|
|
533
561
|
// assign all other roles
|
|
534
|
-
return user.roles().updatePivot({role_id:
|
|
562
|
+
return user.roles().updatePivot({role_id: roleToAssign.id});
|
|
535
563
|
}
|
|
536
564
|
}).then(() => {
|
|
537
565
|
options.status = 'all';
|
|
@@ -168,7 +168,7 @@ module.exports = {
|
|
|
168
168
|
|
|
169
169
|
try {
|
|
170
170
|
// Load newsletter data on email
|
|
171
|
-
await emailBatchModel.relations.email.
|
|
171
|
+
await emailBatchModel.relations.email.getLazyRelation('newsletter', {require: false, ...knexOptions});
|
|
172
172
|
|
|
173
173
|
// send the email
|
|
174
174
|
const sendResponse = await this.send(emailBatchModel.relations.email.toJSON(), recipientRows, memberSegment);
|
|
@@ -10,7 +10,7 @@ class EmailPreview {
|
|
|
10
10
|
* @returns {Promise<Object>}
|
|
11
11
|
*/
|
|
12
12
|
async generateEmailContent(post, {newsletter, memberSegment} = {}) {
|
|
13
|
-
let newsletterModel =
|
|
13
|
+
let newsletterModel = await post.getLazyRelation('newsletter');
|
|
14
14
|
if (!newsletterModel) {
|
|
15
15
|
if (newsletter) {
|
|
16
16
|
newsletterModel = await models.Newsletter.findOne({slug: newsletter});
|
|
@@ -51,7 +51,7 @@ const getReplyToAddress = (fromAddress, replyAddressOption) => {
|
|
|
51
51
|
* @param {Object} options
|
|
52
52
|
*/
|
|
53
53
|
const getEmailData = async (postModel, options) => {
|
|
54
|
-
let newsletter =
|
|
54
|
+
let newsletter = await postModel.getLazyRelation('newsletter');
|
|
55
55
|
if (!newsletter) {
|
|
56
56
|
// The postModel doesn't have a newsletter in test emails
|
|
57
57
|
newsletter = await models.Newsletter.getDefaultNewsletter();
|
|
@@ -194,9 +194,8 @@ const addEmail = async (postModel, options) => {
|
|
|
194
194
|
|
|
195
195
|
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
|
196
196
|
const filterOptions = {...knexOptions, limit: 1};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const newsletter = postModel.relations.newsletter ?? await postModel.related('newsletter').fetch({require: true, ..._.pick(options, ['transacting'])});
|
|
197
|
+
const sharedOptions = _.pick(options, ['transacting']);
|
|
198
|
+
const newsletter = await postModel.getLazyRelation('newsletter', {require: true, ...sharedOptions});
|
|
200
199
|
|
|
201
200
|
if (newsletter.get('status') !== 'active') {
|
|
202
201
|
// A post might have been scheduled to an archived newsletter.
|
|
@@ -360,9 +359,10 @@ async function sendEmailJob({emailModel, options}) {
|
|
|
360
359
|
*/
|
|
361
360
|
async function getEmailMemberRows({emailModel, memberSegment, options}) {
|
|
362
361
|
const knexOptions = _.pick(options, ['transacting', 'forUpdate']);
|
|
362
|
+
const sharedOptions = _.pick(options, ['transacting']);
|
|
363
363
|
const filterOptions = Object.assign({}, knexOptions);
|
|
364
364
|
|
|
365
|
-
const newsletter =
|
|
365
|
+
const newsletter = await emailModel.getLazyRelation('newsletter', {require: true, ...sharedOptions});
|
|
366
366
|
const recipientFilter = transformEmailRecipientFilter(newsletter, emailModel.get('recipient_filter'), 'recipient_filter');
|
|
367
367
|
filterOptions.filter = recipientFilter;
|
|
368
368
|
|
|
@@ -541,7 +541,8 @@ module.exports = {
|
|
|
541
541
|
_partitionMembersBySegment: partitionMembersBySegment,
|
|
542
542
|
_getEmailMemberRows: getEmailMemberRows,
|
|
543
543
|
_getFromAddress: getFromAddress,
|
|
544
|
-
_getReplyToAddress: getReplyToAddress
|
|
544
|
+
_getReplyToAddress: getReplyToAddress,
|
|
545
|
+
_sendEmailJob: sendEmailJob
|
|
545
546
|
};
|
|
546
547
|
|
|
547
548
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Ghost Admin</title>
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
<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%225.
|
|
11
|
+
<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%225.2%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%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%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" />
|
|
12
12
|
|
|
13
13
|
<meta name="HandheldFriendly" content="True" />
|
|
14
14
|
<meta name="MobileOptimized" content="320" />
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
<link rel="stylesheet" href="assets/vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css">
|
|
41
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
41
|
+
<link rel="stylesheet" href="assets/ghost.min-c74f50609022cebf13ad28810def68b6.css" title="light">
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
<script src="assets/vendor.min-ea369e6487643585f35409d474b06789.js"></script>
|
|
60
|
-
<script src="assets/ghost.min-
|
|
60
|
+
<script src="assets/ghost.min-971a0ff706bbf9e24f0b0a5770c8fc41.js"></script>
|
|
61
61
|
|
|
62
62
|
</body>
|
|
63
63
|
</html>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<title>Ghost Admin</title>
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
<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%225.
|
|
11
|
+
<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%225.2%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%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%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" />
|
|
12
12
|
|
|
13
13
|
<meta name="HandheldFriendly" content="True" />
|
|
14
14
|
<meta name="MobileOptimized" content="320" />
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
<link rel="stylesheet" href="assets/vendor.min-ba66b98f7c24fa40e061c7ffc94f4e23.css">
|
|
41
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
41
|
+
<link rel="stylesheet" href="assets/ghost.min-c74f50609022cebf13ad28810def68b6.css" title="light">
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
<script src="assets/vendor.min-ea369e6487643585f35409d474b06789.js"></script>
|
|
60
|
-
<script src="assets/ghost.min-
|
|
60
|
+
<script src="assets/ghost.min-971a0ff706bbf9e24f0b0a5770c8fc41.js"></script>
|
|
61
61
|
|
|
62
62
|
</body>
|
|
63
63
|
</html>
|
|
@@ -16,8 +16,8 @@ module.exports = function setupApiApp() {
|
|
|
16
16
|
// API middleware
|
|
17
17
|
|
|
18
18
|
// Body parsing
|
|
19
|
-
apiApp.use(bodyParser.json({limit: '
|
|
20
|
-
apiApp.use(bodyParser.urlencoded({extended: true, limit: '
|
|
19
|
+
apiApp.use(bodyParser.json({limit: '50mb'}));
|
|
20
|
+
apiApp.use(bodyParser.urlencoded({extended: true, limit: '50mb'}));
|
|
21
21
|
|
|
22
22
|
// Query parsing
|
|
23
23
|
apiApp.use(boolParser());
|
|
@@ -15,7 +15,7 @@ module.exports = function setupApiApp() {
|
|
|
15
15
|
// API middleware
|
|
16
16
|
|
|
17
17
|
// @NOTE: req.body is undefined if we don't use this parser, this can trouble if components rely on req.body being present
|
|
18
|
-
apiApp.use(bodyParser.json({limit: '
|
|
18
|
+
apiApp.use(bodyParser.json({limit: '50mb'}));
|
|
19
19
|
|
|
20
20
|
// Query parsing
|
|
21
21
|
apiApp.use(boolParser());
|
|
@@ -36,12 +36,12 @@ module.exports = function setupMembersApp() {
|
|
|
36
36
|
|
|
37
37
|
// Manage newsletter subscription via unsubscribe link
|
|
38
38
|
membersApp.get('/api/member/newsletters', middleware.getMemberNewsletters);
|
|
39
|
-
membersApp.put('/api/member/newsletters', bodyParser.json({limit: '
|
|
39
|
+
membersApp.put('/api/member/newsletters', bodyParser.json({limit: '50mb'}), middleware.updateMemberNewsletters);
|
|
40
40
|
|
|
41
41
|
// Get and update member data
|
|
42
42
|
membersApp.get('/api/member', middleware.getMemberData);
|
|
43
|
-
membersApp.put('/api/member', bodyParser.json({limit: '
|
|
44
|
-
membersApp.post('/api/member/email', bodyParser.json({limit: '
|
|
43
|
+
membersApp.put('/api/member', bodyParser.json({limit: '50mb'}), middleware.updateMemberData);
|
|
44
|
+
membersApp.post('/api/member/email', bodyParser.json({limit: '50mb'}), (req, res) => membersService.api.middleware.updateEmailAddress(req, res));
|
|
45
45
|
|
|
46
46
|
// Manage session
|
|
47
47
|
membersApp.get('/api/session', middleware.getIdentityToken);
|
|
@@ -128,8 +128,8 @@
|
|
|
128
128
|
"emailAnalytics": true
|
|
129
129
|
},
|
|
130
130
|
"portal": {
|
|
131
|
-
"url": "https://unpkg.com/@tryghost/portal@~2.
|
|
132
|
-
"version": "2.
|
|
131
|
+
"url": "https://unpkg.com/@tryghost/portal@~2.2.0/umd/portal.min.js",
|
|
132
|
+
"version": "2.2"
|
|
133
133
|
},
|
|
134
134
|
"tenor": {
|
|
135
135
|
"publicReadOnlyApiKey": null,
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs-extra');
|
|
3
|
-
const os = require('os');
|
|
4
3
|
const _ = require('lodash');
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -73,14 +72,6 @@ const sanitizeDatabaseProperties = function sanitizeDatabaseProperties(nconf) {
|
|
|
73
72
|
|
|
74
73
|
if (nconf.get('database:client') === 'sqlite3') {
|
|
75
74
|
makePathsAbsolute(nconf, nconf.get('database:connection'), 'database:connection');
|
|
76
|
-
|
|
77
|
-
// In the default SQLite test config we set the path to /tmp/ghost-test.db,
|
|
78
|
-
// but this won't work on Windows, so we need to replace the /tmp bit with
|
|
79
|
-
// the Windows temp folder
|
|
80
|
-
const filename = nconf.get('database:connection:filename');
|
|
81
|
-
if (_.isString(filename) && filename.match(/^\/tmp/)) {
|
|
82
|
-
nconf.set('database:connection:filename', filename.replace(/^\/tmp/, os.tmpdir()));
|
|
83
|
-
}
|
|
84
75
|
}
|
|
85
76
|
};
|
|
86
77
|
|