ghost 4.34.0 → 4.35.0
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/README.md +1 -1
- package/core/built/assets/ghost-dark-cf32b34face01b83c9f3f30d650ba9a8.css +1 -0
- package/core/built/assets/ghost.min-73194305644461704a99cd2eb522637a.css +1 -0
- package/core/built/assets/{ghost.min-d329a469b9fa31acdde16e8e3a22cacc.js → ghost.min-f3a45466144e8273938175e32f788438.js} +1521 -1308
- package/core/built/assets/icons/get-started-members.svg +6 -0
- package/core/built/assets/icons/get-started-migrations.svg +6 -0
- package/core/built/assets/icons/get-started.svg +3 -0
- package/core/built/assets/{vendor.min-e915c5ce0e8f2aedde80180d0cc09fd6.js → vendor.min-b36a93f4fb64127af926ceefa488b00e.js} +1305 -1261
- package/core/frontend/helpers/prev_post.js +1 -1
- package/core/frontend/helpers/products.js +2 -6
- package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
- package/core/frontend/services/routing/controllers/email-post.js +1 -1
- package/core/frontend/services/routing/controllers/preview.js +1 -1
- package/core/frontend/services/routing/controllers/static.js +1 -1
- package/core/frontend/services/routing/helpers/entry-lookup.js +1 -1
- package/core/frontend/services/routing/helpers/fetch-data.js +1 -1
- package/core/frontend/views/unsubscribe.hbs +4 -2
- package/core/server/api/canary/email-post.js +1 -1
- package/core/server/api/canary/pages-public.js +1 -1
- package/core/server/api/canary/pages.js +1 -1
- package/core/server/api/canary/posts-public.js +1 -1
- package/core/server/api/canary/posts.js +1 -1
- package/core/server/api/canary/utils/serializers/input/pages.js +1 -9
- package/core/server/api/canary/utils/serializers/input/posts.js +1 -9
- package/core/server/api/canary/utils/serializers/output/email-posts.js +2 -2
- package/core/server/api/canary/utils/serializers/output/pages.js +9 -5
- package/core/server/api/canary/utils/serializers/output/posts.js +9 -5
- package/core/server/api/canary/utils/serializers/output/preview.js +3 -2
- package/core/server/api/canary/utils/serializers/output/products.js +2 -0
- package/core/server/api/canary/utils/serializers/output/utils/clean.js +0 -9
- package/core/server/api/canary/utils/serializers/output/utils/mapper.js +18 -3
- package/core/server/api/canary/utils/validators/input/pages.js +1 -1
- package/core/server/api/canary/utils/validators/input/posts.js +1 -1
- package/core/server/data/exporter/table-lists.js +1 -0
- package/core/server/data/migrations/versions/4.35/2022-01-20-05-55-add-post-products-table.js +8 -0
- package/core/server/data/migrations/versions/4.35/2022-01-30-15-17-set-welcome-page-url-from-settings.js +45 -0
- package/core/server/data/migrations/versions/4.35/2022-02-01-11-48-update-email-recipient-filter-column-type.js +18 -0
- package/core/server/data/migrations/versions/4.35/2022-02-01-12-03-update-recipient-filter-column-type.js +18 -0
- package/core/server/data/migrations/versions/4.35/2022-02-02-10-38-add-default-content-visibility-tiers-setting.js +8 -0
- package/core/server/data/migrations/versions/4.35/2022-02-02-13-10-transform-specific-tiers-default-content-visibility.js +147 -0
- package/core/server/data/migrations/versions/4.35/2022-02-04-04-34-populate-empty-portal-products.js +60 -0
- package/core/server/data/schema/default-settings.json +4 -0
- package/core/server/data/schema/schema.js +10 -4
- package/core/server/models/post.js +29 -5
- package/core/server/services/mega/template.js +1 -1
- package/core/server/services/members/content-gating.js +6 -1
- package/core/server/services/members/middleware.js +20 -3
- package/core/server/services/members/service.js +34 -8
- package/core/server/services/posts/posts-service.js +25 -1
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/shared/labs.js +3 -2
- package/package.json +33 -33
- package/yarn.lock +811 -799
- package/core/built/assets/ghost-dark-b8ad0651901bb6c34b7bb14aceed5550.css +0 -1
- package/core/built/assets/ghost.min-12d943f054c2f01efca103c8f9ca00e0.css +0 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const logging = require('@tryghost/logging');
|
|
2
|
+
|
|
3
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = createTransactionalMigration(
|
|
6
|
+
async function up(knex) {
|
|
7
|
+
logging.info('Checking default_content_visibility for specific tiers');
|
|
8
|
+
|
|
9
|
+
const settings = await knex('settings')
|
|
10
|
+
.select()
|
|
11
|
+
.whereIn('key', ['default_content_visibility', 'default_content_visibility_tiers']);
|
|
12
|
+
const contentVisibilitySetting = settings.find(d => d.key === 'default_content_visibility');
|
|
13
|
+
const visibilityTiersSetting = settings.find(d => d.key === 'default_content_visibility_tiers');
|
|
14
|
+
if (!contentVisibilitySetting) {
|
|
15
|
+
logging.warn('No default_content_visibility setting found.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!visibilityTiersSetting) {
|
|
20
|
+
logging.warn('No default_content_visibility_tiers setting found.');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const contentVisibility = contentVisibilitySetting.value;
|
|
25
|
+
|
|
26
|
+
if (['public', 'members', 'paid'].includes(contentVisibility)) {
|
|
27
|
+
logging.info(`Ignoring default_content_visibility change as already set to ${contentVisibility}.`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Transform visibility to tiers when stored as nql string
|
|
31
|
+
const isValidProductNqlFilter = /^(?:product:[\w-]+,?)+$/.test(contentVisibility);
|
|
32
|
+
const now = knex.raw('CURRENT_TIMESTAMP');
|
|
33
|
+
// Reset visibility value to paid if invalid string/filter
|
|
34
|
+
if (!isValidProductNqlFilter) {
|
|
35
|
+
logging.warn(`Found invalid default_content_visibility value - ${contentVisibility}, resetting to paid`);
|
|
36
|
+
await knex('settings')
|
|
37
|
+
.where({
|
|
38
|
+
key: 'default_content_visibility'
|
|
39
|
+
})
|
|
40
|
+
.update({
|
|
41
|
+
value: 'paid',
|
|
42
|
+
updated_at: now
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
logging.info(`Resetting default_content_visibility_tiers to []`);
|
|
46
|
+
await knex('settings')
|
|
47
|
+
.where({
|
|
48
|
+
key: 'default_content_visibility_tiers'
|
|
49
|
+
})
|
|
50
|
+
.update({
|
|
51
|
+
value: JSON.stringify([]),
|
|
52
|
+
updated_at: now
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// fetch product slugs from nql filter
|
|
58
|
+
const productSlugs = contentVisibility.split(',').map((segment) => {
|
|
59
|
+
return segment.replace('product:', '');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// get product ids for slugs
|
|
63
|
+
const products = await knex('products')
|
|
64
|
+
.select('id')
|
|
65
|
+
.whereIn('slug', productSlugs);
|
|
66
|
+
const productList = products.map((product) => {
|
|
67
|
+
return product.id;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
logging.info(`Updating default_content_visibility to tiers`);
|
|
71
|
+
await knex('settings')
|
|
72
|
+
.where({
|
|
73
|
+
key: 'default_content_visibility'
|
|
74
|
+
})
|
|
75
|
+
.update({
|
|
76
|
+
value: 'tiers',
|
|
77
|
+
updated_at: now
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
logging.info(`Updating default_content_visibility_tiers to ${productList}`);
|
|
81
|
+
await knex('settings')
|
|
82
|
+
.where({
|
|
83
|
+
key: 'default_content_visibility_tiers'
|
|
84
|
+
})
|
|
85
|
+
.update({
|
|
86
|
+
value: JSON.stringify(productList),
|
|
87
|
+
updated_at: now
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
async function down(knex) {
|
|
91
|
+
logging.info('Reverting default_content_visibility for specific tiers');
|
|
92
|
+
|
|
93
|
+
const settings = await knex('settings')
|
|
94
|
+
.select()
|
|
95
|
+
.whereIn('key', ['default_content_visibility', 'default_content_visibility_tiers']);
|
|
96
|
+
const contentVisibilitySetting = settings.find(d => d.key === 'default_content_visibility');
|
|
97
|
+
const visibilityTiersSetting = settings.find(d => d.key === 'default_content_visibility_tiers');
|
|
98
|
+
|
|
99
|
+
const visibilityValue = contentVisibilitySetting && contentVisibilitySetting.value;
|
|
100
|
+
const visibilityTiersValue = visibilityTiersSetting && visibilityTiersSetting.value;
|
|
101
|
+
|
|
102
|
+
if (visibilityValue !== 'tiers') {
|
|
103
|
+
logging.info(`Ignoring default_content_visibility as is set to ${visibilityValue}.`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!visibilityTiersValue) {
|
|
108
|
+
logging.warn(`Ignoring, found empty default_content_visibility_tiers value`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const parsedTiersValue = JSON.parse(visibilityTiersValue);
|
|
114
|
+
const products = await knex('products')
|
|
115
|
+
.select('slug')
|
|
116
|
+
.whereIn('id', parsedTiersValue);
|
|
117
|
+
const productSlugs = products.map((product) => {
|
|
118
|
+
return `product:${product.slug}`;
|
|
119
|
+
}).join(',');
|
|
120
|
+
const now = knex.raw('CURRENT_TIMESTAMP');
|
|
121
|
+
|
|
122
|
+
logging.info(`Setting default_content_visibility to ${productSlugs}`);
|
|
123
|
+
await knex('settings')
|
|
124
|
+
.where({
|
|
125
|
+
key: 'default_content_visibility'
|
|
126
|
+
})
|
|
127
|
+
.update({
|
|
128
|
+
value: productSlugs,
|
|
129
|
+
updated_at: now
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
logging.info(`Setting default_content_visibility_tiers to []`);
|
|
133
|
+
await knex('settings')
|
|
134
|
+
.where({
|
|
135
|
+
key: 'default_content_visibility_tiers'
|
|
136
|
+
})
|
|
137
|
+
.update({
|
|
138
|
+
value: JSON.stringify([]),
|
|
139
|
+
updated_at: now
|
|
140
|
+
});
|
|
141
|
+
} catch (e) {
|
|
142
|
+
logging.warn(`Invalid default_content_visibility_tiers value - ${visibilityTiersValue}`);
|
|
143
|
+
logging.warn(e);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
package/core/server/data/migrations/versions/4.35/2022-02-04-04-34-populate-empty-portal-products.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const {createTransactionalMigration} = require('../../utils');
|
|
2
|
+
const logging = require('@tryghost/logging');
|
|
3
|
+
|
|
4
|
+
module.exports = createTransactionalMigration(
|
|
5
|
+
async function up(knex) {
|
|
6
|
+
const products = await knex
|
|
7
|
+
.select('id')
|
|
8
|
+
.where({
|
|
9
|
+
type: 'paid'
|
|
10
|
+
})
|
|
11
|
+
.from('products');
|
|
12
|
+
|
|
13
|
+
if (products.length === 0) {
|
|
14
|
+
logging.warn(`Skipping updating portal_products, no product exists`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (products.length > 1) {
|
|
19
|
+
logging.warn(`Skipping updating portal_products, tiers beta is enabled`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const portalProductsSetting = await knex('settings')
|
|
24
|
+
.where('key', 'portal_products')
|
|
25
|
+
.select('value')
|
|
26
|
+
.first();
|
|
27
|
+
|
|
28
|
+
if (!portalProductsSetting) {
|
|
29
|
+
logging.warn(`Missing portal_products setting`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const currPortalProductsValue = JSON.parse(portalProductsSetting.value);
|
|
34
|
+
|
|
35
|
+
if (currPortalProductsValue.length > 0) {
|
|
36
|
+
logging.warn(`Ignoring - portal_products setting is not empty, - ${currPortalProductsValue}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const defaultProduct = products[0];
|
|
41
|
+
const portalProductsValue = [defaultProduct.id];
|
|
42
|
+
|
|
43
|
+
logging.info(`Setting portal_products setting to have product - ${defaultProduct.id}`);
|
|
44
|
+
|
|
45
|
+
const now = knex.raw('CURRENT_TIMESTAMP');
|
|
46
|
+
|
|
47
|
+
await knex('settings')
|
|
48
|
+
.where('key', 'portal_products')
|
|
49
|
+
.update({
|
|
50
|
+
value: JSON.stringify(portalProductsValue),
|
|
51
|
+
updated_at: now
|
|
52
|
+
});
|
|
53
|
+
} catch (e) {
|
|
54
|
+
logging.warn(`Ignoring, unable to parse portal_products setting value - ${portalProductsSetting.value}`);
|
|
55
|
+
logging.warn(e);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
// no-op - we don't want to return to invalid state
|
|
59
|
+
async function down() {}
|
|
60
|
+
);
|
|
@@ -30,8 +30,8 @@ module.exports = {
|
|
|
30
30
|
defaultTo: 'public'
|
|
31
31
|
},
|
|
32
32
|
email_recipient_filter: {
|
|
33
|
-
type: '
|
|
34
|
-
maxlength:
|
|
33
|
+
type: 'text',
|
|
34
|
+
maxlength: 1000000000,
|
|
35
35
|
nullable: false,
|
|
36
36
|
defaultTo: 'none'
|
|
37
37
|
},
|
|
@@ -424,6 +424,12 @@ module.exports = {
|
|
|
424
424
|
product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
|
|
425
425
|
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
|
|
426
426
|
},
|
|
427
|
+
posts_products: {
|
|
428
|
+
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
429
|
+
post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id', cascadeDelete: true},
|
|
430
|
+
product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
|
|
431
|
+
sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
|
|
432
|
+
},
|
|
427
433
|
members_payment_events: {
|
|
428
434
|
id: {type: 'string', maxlength: 24, nullable: false, primary: true},
|
|
429
435
|
member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
|
|
@@ -592,8 +598,8 @@ module.exports = {
|
|
|
592
598
|
validations: {isIn: [['pending', 'submitting', 'submitted', 'failed']]}
|
|
593
599
|
},
|
|
594
600
|
recipient_filter: {
|
|
595
|
-
type: '
|
|
596
|
-
maxlength:
|
|
601
|
+
type: 'text',
|
|
602
|
+
maxlength: 1000000000,
|
|
597
603
|
nullable: false,
|
|
598
604
|
defaultTo: 'status:-free'
|
|
599
605
|
},
|
|
@@ -54,9 +54,20 @@ Post = ghostBookshelf.Model.extend({
|
|
|
54
54
|
*/
|
|
55
55
|
defaults: function defaults() {
|
|
56
56
|
let visibility = 'public';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
let tiers = [];
|
|
58
|
+
const defaultContentVisibility = settingsCache.get('default_content_visibility');
|
|
59
|
+
if (defaultContentVisibility) {
|
|
60
|
+
if (defaultContentVisibility === 'tiers') {
|
|
61
|
+
const tiersData = settingsCache.get('default_content_visibility_tiers') || [];
|
|
62
|
+
visibility = 'tiers',
|
|
63
|
+
tiers = tiersData.map((tierId) => {
|
|
64
|
+
return {
|
|
65
|
+
id: tierId
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
} else if (defaultContentVisibility !== 'tiers') {
|
|
69
|
+
visibility = settingsCache.get('default_content_visibility');
|
|
70
|
+
}
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
return {
|
|
@@ -64,16 +75,18 @@ Post = ghostBookshelf.Model.extend({
|
|
|
64
75
|
status: 'draft',
|
|
65
76
|
featured: false,
|
|
66
77
|
type: 'post',
|
|
78
|
+
tiers,
|
|
67
79
|
visibility: visibility,
|
|
68
80
|
email_recipient_filter: 'none'
|
|
69
81
|
};
|
|
70
82
|
},
|
|
71
83
|
|
|
72
|
-
relationships: ['tags', 'authors', 'mobiledoc_revisions', 'posts_meta'],
|
|
84
|
+
relationships: ['tags', 'authors', 'mobiledoc_revisions', 'posts_meta', 'tiers'],
|
|
73
85
|
|
|
74
86
|
// NOTE: look up object, not super nice, but was easy to implement
|
|
75
87
|
relationshipBelongsTo: {
|
|
76
88
|
tags: 'tags',
|
|
89
|
+
tiers: 'products',
|
|
77
90
|
authors: 'users',
|
|
78
91
|
posts_meta: 'posts_meta'
|
|
79
92
|
},
|
|
@@ -89,6 +102,17 @@ Post = ghostBookshelf.Model.extend({
|
|
|
89
102
|
}
|
|
90
103
|
},
|
|
91
104
|
|
|
105
|
+
tiers() {
|
|
106
|
+
return this.belongsToMany('Product', 'posts_products', 'post_id', 'product_id')
|
|
107
|
+
.withPivot('sort_order')
|
|
108
|
+
.query('orderBy', 'sort_order', 'ASC')
|
|
109
|
+
.query((qb) => {
|
|
110
|
+
// avoids bookshelf adding a `DISTINCT` to the query
|
|
111
|
+
// we know the result set will already be unique and DISTINCT hurts query performance
|
|
112
|
+
qb.columns('products.*');
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
|
|
92
116
|
parse() {
|
|
93
117
|
const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);
|
|
94
118
|
|
|
@@ -171,7 +195,7 @@ Post = ghostBookshelf.Model.extend({
|
|
|
171
195
|
|
|
172
196
|
// transform visibility NQL queries to special-case values where necessary
|
|
173
197
|
// ensures checks against special-case values such as `{{#has visibility="paid"}}` continue working
|
|
174
|
-
if (attrs.visibility && !['public', 'members', 'paid'].includes(attrs.visibility)) {
|
|
198
|
+
if (attrs.visibility && !['public', 'members', 'paid', 'tiers'].includes(attrs.visibility)) {
|
|
175
199
|
if (attrs.visibility === 'status:-free') {
|
|
176
200
|
attrs.visibility = 'paid';
|
|
177
201
|
} else {
|
|
@@ -1151,7 +1151,7 @@ ${ templateSettings.showBadge ? `
|
|
|
1151
1151
|
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
|
1152
1152
|
${ templateSettings.showHeaderIcon && site.iconUrl ? `
|
|
1153
1153
|
<tr>
|
|
1154
|
-
<td class="site-icon"><a href="${site.url}"><img src="${site.iconUrl}" border="0"></a></td>
|
|
1154
|
+
<td class="site-icon"><a href="${site.url}"><img src="${site.iconUrl}" alt="${site.title}" border="0"></a></td>
|
|
1155
1155
|
</tr>
|
|
1156
1156
|
` : ``}
|
|
1157
1157
|
${ templateSettings.showHeaderTitle ? `
|
|
@@ -40,7 +40,12 @@ function checkPostAccess(post, member) {
|
|
|
40
40
|
return PERMIT_ACCESS;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
let visibility = post.visibility === 'paid' ? 'status:-free' : post.visibility;
|
|
44
|
+
if (visibility === 'tiers') {
|
|
45
|
+
visibility = post.tiers.map((product) => {
|
|
46
|
+
return `product:${product.slug}`;
|
|
47
|
+
}).join(',');
|
|
48
|
+
}
|
|
44
49
|
|
|
45
50
|
if (visibility && member.status && nql(visibility, {expansions: MEMBER_NQL_EXPANSIONS}).queryJSON(member)) {
|
|
46
51
|
return PERMIT_ACCESS;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const logging = require('@tryghost/logging');
|
|
3
3
|
const membersService = require('./service');
|
|
4
|
+
const models = require('../../models');
|
|
4
5
|
const offersService = require('../offers/service');
|
|
5
6
|
const urlUtils = require('../../../shared/url-utils');
|
|
6
7
|
const ghostVersion = require('@tryghost/version');
|
|
@@ -199,10 +200,26 @@ const createSessionFromMagicLink = async function (req, res, next) {
|
|
|
199
200
|
|
|
200
201
|
if (action === 'signup') {
|
|
201
202
|
let customRedirect = '';
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
const mostRecentActiveSubscription = subscriptions
|
|
204
|
+
.sort((a, b) => {
|
|
205
|
+
const aStartDate = new Date(a.start_date);
|
|
206
|
+
const bStartDate = new Date(b.start_date);
|
|
207
|
+
return bStartDate.valueOf() - aStartDate.valueOf();
|
|
208
|
+
})
|
|
209
|
+
.find(sub => ['active', 'trialing'].includes(sub.status));
|
|
210
|
+
if (mostRecentActiveSubscription) {
|
|
211
|
+
if (labsService.isSet('multipleProducts')) {
|
|
212
|
+
customRedirect = mostRecentActiveSubscription.tier.welcome_page_url;
|
|
213
|
+
} else {
|
|
214
|
+
customRedirect = settingsCache.get('members_paid_signup_redirect') || '';
|
|
215
|
+
}
|
|
204
216
|
} else {
|
|
205
|
-
|
|
217
|
+
if (labsService.isSet('multipleProducts')) {
|
|
218
|
+
const freeTier = await models.Product.findOne({type: 'free'});
|
|
219
|
+
customRedirect = freeTier && freeTier.get('welcome_page_url') || '';
|
|
220
|
+
} else {
|
|
221
|
+
customRedirect = settingsCache.get('members_free_signup_redirect') || '';
|
|
222
|
+
}
|
|
206
223
|
}
|
|
207
224
|
|
|
208
225
|
if (customRedirect && customRedirect !== '/') {
|
|
@@ -16,13 +16,12 @@ const models = require('../../models');
|
|
|
16
16
|
const {GhostMailer} = require('../mail');
|
|
17
17
|
const jobsService = require('../jobs');
|
|
18
18
|
const VerificationTrigger = require('@tryghost/verification-trigger');
|
|
19
|
+
const events = require('../../lib/common/events');
|
|
19
20
|
|
|
20
21
|
const messages = {
|
|
21
22
|
noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
|
|
22
23
|
sslRequiredForStripe: 'Cannot run Ghost without SSL when Stripe is connected. Please update your url config to use "https://".',
|
|
23
|
-
remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.'
|
|
24
|
-
emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability on a list of that size, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go.`,
|
|
25
|
-
emailVerificationEmailMessage: `Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.`
|
|
24
|
+
remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.'
|
|
26
25
|
};
|
|
27
26
|
|
|
28
27
|
const ghostMailer = new GhostMailer();
|
|
@@ -60,10 +59,11 @@ const processImport = async (options) => {
|
|
|
60
59
|
delete result.meta.originalImportSize;
|
|
61
60
|
|
|
62
61
|
const importThreshold = await verificationTrigger.getImportThreshold();
|
|
63
|
-
if (
|
|
62
|
+
if (importSize > importThreshold) {
|
|
64
63
|
await verificationTrigger.startVerificationProcess({
|
|
65
64
|
amountImported: importSize,
|
|
66
|
-
throwOnTrigger: true
|
|
65
|
+
throwOnTrigger: true,
|
|
66
|
+
source: 'import'
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -76,6 +76,32 @@ module.exports = {
|
|
|
76
76
|
const createMembersApiInstance = require('./api');
|
|
77
77
|
const env = config.get('env');
|
|
78
78
|
|
|
79
|
+
events.on('settings.edited', async function (settingModel) {
|
|
80
|
+
if (labsService.isSet('multipleProducts')) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const key = settingModel.get('key');
|
|
85
|
+
const value = settingModel.get('value');
|
|
86
|
+
|
|
87
|
+
if (key === 'members_free_signup_redirect') {
|
|
88
|
+
try {
|
|
89
|
+
await models.Product.forge().query().update('welcome_page_url', value).where('type', 'free');
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logging.error(err);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (key === 'members_paid_signup_redirect') {
|
|
96
|
+
try {
|
|
97
|
+
await models.Product.forge().query().update('welcome_page_url', value).where('type', 'paid');
|
|
98
|
+
} catch (err) {
|
|
99
|
+
logging.error(err);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
79
105
|
// @TODO Move to stripe service
|
|
80
106
|
if (env !== 'production') {
|
|
81
107
|
if (stripeService.api.configured && stripeService.api.mode === 'live') {
|
|
@@ -108,11 +134,11 @@ module.exports = {
|
|
|
108
134
|
const fromAddress = config.get('user_email');
|
|
109
135
|
|
|
110
136
|
if (escalationAddress) {
|
|
111
|
-
|
|
137
|
+
ghostMailer.send({
|
|
112
138
|
subject,
|
|
113
139
|
html: tpl(message, {
|
|
114
|
-
amountImported,
|
|
115
|
-
siteUrl:
|
|
140
|
+
importedNumber: amountImported,
|
|
141
|
+
siteUrl: urlUtils.getSiteUrl()
|
|
116
142
|
}),
|
|
117
143
|
forceTextContent: true,
|
|
118
144
|
from: fromAddress,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
const nql = require('@nexes/nql');
|
|
1
2
|
const {BadRequestError} = require('@tryghost/errors');
|
|
2
3
|
const tpl = require('@tryghost/tpl');
|
|
3
4
|
|
|
4
5
|
const messages = {
|
|
5
|
-
invalidEmailRecipientFilter: 'Invalid filter in email_recipient_filter param.'
|
|
6
|
+
invalidEmailRecipientFilter: 'Invalid filter in email_recipient_filter param.',
|
|
7
|
+
invalidVisibilityFilter: 'Invalid visibility filter.'
|
|
6
8
|
};
|
|
7
9
|
|
|
8
10
|
class PostsService {
|
|
@@ -78,6 +80,28 @@ class PostsService {
|
|
|
78
80
|
return model;
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
async getProductsFromVisibilityFilter(visibilityFilter) {
|
|
84
|
+
try {
|
|
85
|
+
const allProducts = await this.models.Product.findAll();
|
|
86
|
+
const visibilityFilterJson = nql(visibilityFilter).toJSON();
|
|
87
|
+
const productsData = (visibilityFilterJson.product ? [visibilityFilterJson] : visibilityFilterJson.$or) || [];
|
|
88
|
+
const tiers = productsData
|
|
89
|
+
.map((data) => {
|
|
90
|
+
return allProducts.find((p) => {
|
|
91
|
+
return p.get('slug') === data.product;
|
|
92
|
+
});
|
|
93
|
+
}).filter(p => !!p).map((d) => {
|
|
94
|
+
return d.toJSON();
|
|
95
|
+
});
|
|
96
|
+
return tiers;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return Promise.reject(new BadRequestError({
|
|
99
|
+
message: tpl(messages.invalidVisibilityFilter),
|
|
100
|
+
context: err.message
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
81
105
|
/**
|
|
82
106
|
* Calculates if the email should be tried to be sent out
|
|
83
107
|
* @private
|
|
@@ -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%224.
|
|
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%224.35%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-2c8ad32b7960bb605ebc20097fee5ebd.css">
|
|
41
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
41
|
+
<link rel="stylesheet" href="assets/ghost.min-73194305644461704a99cd2eb522637a.css" title="light">
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
<div id="ember-basic-dropdown-wormhole"></div>
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
<script src="assets/vendor.min-
|
|
60
|
-
<script src="assets/ghost.min-
|
|
59
|
+
<script src="assets/vendor.min-b36a93f4fb64127af926ceefa488b00e.js"></script>
|
|
60
|
+
<script src="assets/ghost.min-f3a45466144e8273938175e32f788438.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%224.
|
|
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%224.35%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-2c8ad32b7960bb605ebc20097fee5ebd.css">
|
|
41
|
-
<link rel="stylesheet" href="assets/ghost.min-
|
|
41
|
+
<link rel="stylesheet" href="assets/ghost.min-73194305644461704a99cd2eb522637a.css" title="light">
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
|
|
@@ -56,8 +56,8 @@
|
|
|
56
56
|
<div id="ember-basic-dropdown-wormhole"></div>
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
<script src="assets/vendor.min-
|
|
60
|
-
<script src="assets/ghost.min-
|
|
59
|
+
<script src="assets/vendor.min-b36a93f4fb64127af926ceefa488b00e.js"></script>
|
|
60
|
+
<script src="assets/ghost.min-f3a45466144e8273938175e32f788438.js"></script>
|
|
61
61
|
|
|
62
62
|
</body>
|
|
63
63
|
</html>
|
package/core/shared/labs.js
CHANGED
|
@@ -27,11 +27,12 @@ const BETA_FEATURES = [
|
|
|
27
27
|
const ALPHA_FEATURES = [
|
|
28
28
|
'oauthLogin',
|
|
29
29
|
'membersActivity',
|
|
30
|
-
'cardSettingsPanel',
|
|
31
30
|
'urlCache',
|
|
32
31
|
'beforeAfterCard',
|
|
33
32
|
'tweetGridCard',
|
|
34
|
-
'membersActivityFeed'
|
|
33
|
+
'membersActivityFeed',
|
|
34
|
+
'improvedOnboarding',
|
|
35
|
+
'tierWelcomePages'
|
|
35
36
|
];
|
|
36
37
|
|
|
37
38
|
module.exports.GA_KEYS = [...GA_FEATURES];
|