ghost 4.15.1 → 4.16.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.
Files changed (47) hide show
  1. package/content/themes/casper/assets/built/screen.css +1 -1
  2. package/content/themes/casper/assets/built/screen.css.map +1 -1
  3. package/content/themes/casper/assets/css/screen.css +1 -1
  4. package/content/themes/casper/default.hbs +2 -2
  5. package/content/themes/casper/package.json +1 -1
  6. package/content/themes/casper/page.hbs +28 -26
  7. package/content/themes/casper/partials/post-card.hbs +2 -2
  8. package/content/themes/casper/post.hbs +67 -65
  9. package/content/themes/casper/tag.hbs +2 -2
  10. package/core/built/assets/ghost-dark-bb2831fc27fcb02893ed0a761207dc63.css +1 -0
  11. package/core/built/assets/{ghost.min-e35cfee26d942c364166f57f3dcc9e75.js → ghost.min-d1d99f3ed6e0f427874b2a11e7078475.js} +228 -187
  12. package/core/built/assets/ghost.min-e7612edfa72b0fe2c201b387923e6fc7.css +1 -0
  13. package/core/built/assets/icons/discount-bubble.svg +1 -0
  14. package/core/built/assets/{vendor.min-ca33abc718f21a51327841d58f8875d0.js → vendor.min-3660ec7864887f1496fe7a27fd23ab76.js} +44 -42
  15. package/core/frontend/helpers/ghost_head.js +7 -1
  16. package/core/frontend/services/settings/loader.js +2 -2
  17. package/core/frontend/services/theme-engine/middleware.js +4 -1
  18. package/core/server/api/canary/settings.js +13 -144
  19. package/core/server/api/canary/utils/validators/input/settings.js +23 -1
  20. package/core/server/api/v3/settings.js +13 -132
  21. package/core/server/api/v3/utils/validators/input/settings.js +23 -1
  22. package/core/server/data/exporter/table-lists.js +1 -0
  23. package/core/server/data/importer/import-manager.js +398 -0
  24. package/core/server/data/importer/importers/data/data-importer.js +162 -0
  25. package/core/server/data/importer/importers/data/index.js +1 -162
  26. package/core/server/data/importer/index.js +1 -379
  27. package/core/server/data/migrations/versions/4.16/01-add-custom-theme-settings-table.js +9 -0
  28. package/core/server/data/schema/schema.js +16 -0
  29. package/core/server/models/custom-theme-setting.js +9 -0
  30. package/core/server/models/index.js +2 -0
  31. package/core/server/services/custom-theme-settings.js +8 -0
  32. package/core/server/services/members/api.js +1 -0
  33. package/core/server/services/settings/index.js +13 -16
  34. package/core/server/services/settings/settings-bread-service.js +188 -0
  35. package/core/server/services/settings/settings-utils.js +32 -0
  36. package/core/server/services/themes/ThemeStorage.js +5 -4
  37. package/core/server/services/themes/activation-bridge.js +14 -0
  38. package/core/server/services/themes/validate.js +5 -2
  39. package/core/server/web/admin/views/default-prod.html +4 -4
  40. package/core/server/web/admin/views/default.html +4 -4
  41. package/core/server/web/members/app.js +2 -0
  42. package/core/shared/custom-theme-settings-cache.js +3 -0
  43. package/core/shared/labs.js +2 -1
  44. package/package.json +28 -27
  45. package/yarn.lock +806 -795
  46. package/core/built/assets/ghost-dark-faf931d90e92535e6c03ca16793cbe7b.css +0 -1
  47. package/core/built/assets/ghost.min-7aa074ad556a8455155ac88ceaca03ab.css +0 -1
@@ -1,15 +1,15 @@
1
1
  const Promise = require('bluebird');
2
2
  const _ = require('lodash');
3
- const validator = require('@tryghost/validator');
4
3
  const models = require('../../models');
5
4
  const routeSettings = require('../../services/route-settings');
6
5
  const frontendSettings = require('../../../frontend/services/settings');
7
6
  const i18n = require('../../../shared/i18n');
8
- const {BadRequestError, NoPermissionError, NotFoundError} = require('@tryghost/errors');
7
+ const {BadRequestError} = require('@tryghost/errors');
9
8
  const settingsService = require('../../services/settings');
10
- const settingsCache = require('../../../shared/settings-cache');
11
9
  const membersService = require('../../services/members');
12
10
 
11
+ const settingsBREADService = settingsService.getSettingsBREADServiceInstance();
12
+
13
13
  module.exports = {
14
14
  docName: 'settings',
15
15
 
@@ -17,26 +17,7 @@ module.exports = {
17
17
  options: ['group'],
18
18
  permissions: true,
19
19
  query(frame) {
20
- let settings = settingsCache.getAll();
21
-
22
- // CASE: no context passed (functional call)
23
- if (!frame.options.context) {
24
- return Promise.resolve(settings.filter((setting) => {
25
- return setting.group === 'site';
26
- }));
27
- }
28
-
29
- if (!frame.options.context.internal) {
30
- // CASE: omit core settings unless internal request
31
- settings = _.filter(settings, (setting) => {
32
- const isCore = setting.group === 'core';
33
- return !isCore;
34
- });
35
- // CASE: omit secret settings unless internal request
36
- settings = settings.map(settingsService.hideValueIfSecret);
37
- }
38
-
39
- return settings;
20
+ return settingsBREADService.browse(frame.options.context);
40
21
  }
41
22
  },
42
23
 
@@ -55,41 +36,7 @@ module.exports = {
55
36
  }
56
37
  },
57
38
  query(frame) {
58
- let setting;
59
- if (frame.options.key === 'slack') {
60
- const slackURL = settingsCache.get('slack_url', {resolve: false});
61
- const slackUsername = settingsCache.get('slack_username', {resolve: false});
62
-
63
- setting = slackURL || slackUsername;
64
- setting.key = 'slack';
65
- setting.value = [{
66
- url: slackURL && slackURL.value,
67
- username: slackUsername && slackUsername.value
68
- }];
69
- } else {
70
- setting = settingsCache.get(frame.options.key, {resolve: false});
71
- }
72
-
73
- if (!setting) {
74
- return Promise.reject(new NotFoundError({
75
- message: i18n.t('errors.api.settings.problemFindingSetting', {
76
- key: frame.options.key
77
- })
78
- }));
79
- }
80
-
81
- // @TODO: handle in settings model permissible fn
82
- if (setting.group === 'core' && !(frame.options.context && frame.options.context.internal)) {
83
- return Promise.reject(new NoPermissionError({
84
- message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
85
- }));
86
- }
87
-
88
- setting = settingsService.hideValueIfSecret(setting);
89
-
90
- return {
91
- [frame.options.key]: setting
92
- };
39
+ return settingsBREADService.read(frame.options.key, frame.options.context);
93
40
  }
94
41
  },
95
42
 
@@ -153,17 +100,7 @@ module.exports = {
153
100
  ],
154
101
  async query(frame) {
155
102
  const {email, type} = frame.data;
156
- if (typeof email !== 'string' || !validator.isEmail(email)) {
157
- throw new BadRequestError({
158
- message: i18n.t('errors.api.settings.invalidEmailReceived')
159
- });
160
- }
161
103
 
162
- if (!type || !['fromAddressUpdate', 'supportAddressUpdate'].includes(type)) {
163
- throw new BadRequestError({
164
- message: 'Invalid email type recieved'
165
- });
166
- }
167
104
  try {
168
105
  // Send magic link to update fromAddress
169
106
  await membersService.settings.sendEmailAddressUpdateMagicLink({
@@ -225,91 +162,23 @@ module.exports = {
225
162
  permissions: {
226
163
  unsafeAttrsObject(frame) {
227
164
  return _.find(frame.data.settings, {key: 'labs'});
228
- },
229
- async before(frame) {
230
- if (frame.options.context && frame.options.context.internal) {
231
- return;
232
- }
233
-
234
- const firstCoreSetting = frame.data.settings.find(setting => setting.group === 'core');
235
- if (firstCoreSetting) {
236
- throw new NoPermissionError({
237
- message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
238
- });
239
- }
240
165
  }
241
166
  },
242
167
  async query(frame) {
168
+ let stripeConnectData;
243
169
  const stripeConnectIntegrationToken = frame.data.settings.find(setting => setting.key === 'stripe_connect_integration_token');
244
170
 
245
- const settings = frame.data.settings.filter((setting) => {
246
- // The `stripe_connect_integration_token` "setting" is only used to set the `stripe_connect_*` settings.
247
- return ![
248
- 'stripe_connect_integration_token',
249
- 'stripe_connect_publishable_key',
250
- 'stripe_connect_secret_key',
251
- 'stripe_connect_livemode',
252
- 'stripe_connect_account_id',
253
- 'stripe_connect_display_name'
254
- ].includes(setting.key)
255
- // Remove obfuscated settings
256
- && !(setting.value === settingsService.obfuscatedSetting && settingsService.isSecretSetting(setting));
257
- });
258
-
259
- const getSetting = setting => settingsCache.get(setting.key, {resolve: false});
260
-
261
- const firstUnknownSetting = settings.find(setting => !getSetting(setting));
262
-
263
- if (firstUnknownSetting) {
264
- throw new NotFoundError({
265
- message: i18n.t('errors.api.settings.problemFindingSetting', {
266
- key: firstUnknownSetting.key
267
- })
268
- });
269
- }
270
-
271
- if (!(frame.options.context && frame.options.context.internal)) {
272
- const firstCoreSetting = settings.find(setting => getSetting(setting).group === 'core');
273
- if (firstCoreSetting) {
274
- throw new NoPermissionError({
275
- message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
276
- });
277
- }
278
- }
279
-
280
171
  if (stripeConnectIntegrationToken && stripeConnectIntegrationToken.value) {
281
172
  const getSessionProp = prop => frame.original.session[prop];
282
- try {
283
- const data = await membersService.stripeConnect.getStripeConnectTokenData(stripeConnectIntegrationToken.value, getSessionProp);
284
- settings.push({
285
- key: 'stripe_connect_publishable_key',
286
- value: data.public_key
287
- });
288
- settings.push({
289
- key: 'stripe_connect_secret_key',
290
- value: data.secret_key
291
- });
292
- settings.push({
293
- key: 'stripe_connect_livemode',
294
- value: data.livemode
295
- });
296
- settings.push({
297
- key: 'stripe_connect_display_name',
298
- value: data.display_name
299
- });
300
- settings.push({
301
- key: 'stripe_connect_account_id',
302
- value: data.account_id
303
- });
304
- } catch (err) {
305
- throw new BadRequestError({
306
- err,
307
- message: 'The Stripe Connect token could not be parsed.'
308
- });
309
- }
173
+
174
+ stripeConnectData = await settingsBREADService.getStripeConnectData(
175
+ stripeConnectIntegrationToken,
176
+ getSessionProp,
177
+ membersService.stripeConnect.getStripeConnectTokenData
178
+ );
310
179
  }
311
180
 
312
- return models.Settings.edit(settings, frame.options);
181
+ return await settingsBREADService.edit(frame.data.settings, frame.options, stripeConnectData);
313
182
  }
314
183
  },
315
184
 
@@ -1,7 +1,13 @@
1
1
  const Promise = require('bluebird');
2
2
  const _ = require('lodash');
3
3
  const i18n = require('../../../../../../shared/i18n');
4
- const {NotFoundError, ValidationError} = require('@tryghost/errors');
4
+ const {NotFoundError, ValidationError, BadRequestError} = require('@tryghost/errors');
5
+ const validator = require('@tryghost/validator');
6
+
7
+ const messages = {
8
+ invalidEmailReceived: 'Please send a valid email',
9
+ invalidEmailTypeReceived: 'Invalid email type received'
10
+ };
5
11
 
6
12
  module.exports = {
7
13
  read(apiConfig, frame) {
@@ -62,5 +68,21 @@ module.exports = {
62
68
  if (errors.length) {
63
69
  return Promise.reject(errors[0]);
64
70
  }
71
+ },
72
+
73
+ updateMembersEmail(apiConfig, frame) {
74
+ const {email, type} = frame.data;
75
+
76
+ if (typeof email !== 'string' || !validator.isEmail(email)) {
77
+ throw new BadRequestError({
78
+ message: messages.invalidEmailReceived
79
+ });
80
+ }
81
+
82
+ if (!type || !['fromAddressUpdate', 'supportAddressUpdate'].includes(type)) {
83
+ throw new BadRequestError({
84
+ message: messages.invalidEmailTypeReceived
85
+ });
86
+ }
65
87
  }
66
88
  };
@@ -1,15 +1,15 @@
1
1
  const Promise = require('bluebird');
2
2
  const _ = require('lodash');
3
- const validator = require('@tryghost/validator');
4
3
  const models = require('../../models');
5
4
  const routeSettings = require('../../services/route-settings');
6
5
  const frontendSettings = require('../../../frontend/services/settings');
7
6
  const i18n = require('../../../shared/i18n');
8
- const {BadRequestError, NoPermissionError, NotFoundError} = require('@tryghost/errors');
7
+ const {BadRequestError, NoPermissionError} = require('@tryghost/errors');
9
8
  const settingsService = require('../../services/settings');
10
- const settingsCache = require('../../../shared/settings-cache');
11
9
  const membersService = require('../../services/members');
12
10
 
11
+ const settingsBREADService = settingsService.getSettingsBREADServiceInstance();
12
+
13
13
  module.exports = {
14
14
  docName: 'settings',
15
15
 
@@ -17,26 +17,7 @@ module.exports = {
17
17
  options: ['type', 'group'],
18
18
  permissions: true,
19
19
  query(frame) {
20
- let settings = settingsCache.getAll();
21
-
22
- // CASE: no context passed (functional call)
23
- if (!frame.options.context) {
24
- return Promise.resolve(settings.filter((setting) => {
25
- return setting.group === 'site';
26
- }));
27
- }
28
-
29
- if (!frame.options.context.internal) {
30
- // CASE: omit core settings unless internal request
31
- settings = _.filter(settings, (setting) => {
32
- const isCore = setting.group === 'core';
33
- return !isCore;
34
- });
35
- // CASE: omit secret settings unless internal request
36
- settings = settings.map(settingsService.hideValueIfSecret);
37
- }
38
-
39
- return settings;
20
+ return settingsBREADService.browse(frame.options.context);
40
21
  }
41
22
  },
42
23
 
@@ -55,41 +36,7 @@ module.exports = {
55
36
  }
56
37
  },
57
38
  query(frame) {
58
- let setting;
59
- if (frame.options.key === 'slack') {
60
- const slackURL = settingsCache.get('slack_url', {resolve: false});
61
- const slackUsername = settingsCache.get('slack_username', {resolve: false});
62
-
63
- setting = slackURL || slackUsername;
64
- setting.key = 'slack';
65
- setting.value = [{
66
- url: slackURL && slackURL.value,
67
- username: slackUsername && slackUsername.value
68
- }];
69
- } else {
70
- setting = settingsCache.get(frame.options.key, {resolve: false});
71
- }
72
-
73
- if (!setting) {
74
- return Promise.reject(new NotFoundError({
75
- message: i18n.t('errors.api.settings.problemFindingSetting', {
76
- key: frame.options.key
77
- })
78
- }));
79
- }
80
-
81
- // @TODO: handle in settings model permissible fn
82
- if (setting.group === 'core' && !(frame.options.context && frame.options.context.internal)) {
83
- return Promise.reject(new NoPermissionError({
84
- message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
85
- }));
86
- }
87
-
88
- setting = settingsService.hideValueIfSecret(setting);
89
-
90
- return {
91
- [frame.options.key]: setting
92
- };
39
+ return settingsBREADService.read(frame.options.key, frame.options.context);
93
40
  }
94
41
  },
95
42
 
@@ -153,17 +100,7 @@ module.exports = {
153
100
  ],
154
101
  async query(frame) {
155
102
  const {email, type} = frame.data;
156
- if (typeof email !== 'string' || !validator.isEmail(email)) {
157
- throw new BadRequestError({
158
- message: i18n.t('errors.api.settings.invalidEmailReceived')
159
- });
160
- }
161
103
 
162
- if (!type || !['fromAddressUpdate', 'supportAddressUpdate'].includes(type)) {
163
- throw new BadRequestError({
164
- message: 'Invalid email type recieved'
165
- });
166
- }
167
104
  try {
168
105
  // Send magic link to update fromAddress
169
106
  await membersService.settings.sendEmailAddressUpdateMagicLink({
@@ -232,76 +169,20 @@ module.exports = {
232
169
  }
233
170
  },
234
171
  async query(frame) {
172
+ let stripeConnectData;
235
173
  const stripeConnectIntegrationToken = frame.data.settings.find(setting => setting.key === 'stripe_connect_integration_token');
236
174
 
237
- const settings = frame.data.settings.filter((setting) => {
238
- // The `stripe_connect_integration_token` "setting" is only used to set the `stripe_connect_*` settings.
239
- return ![
240
- 'stripe_connect_integration_token',
241
- 'stripe_connect_publishable_key',
242
- 'stripe_connect_secret_key',
243
- 'stripe_connect_livemode',
244
- 'stripe_connect_account_id',
245
- 'stripe_connect_display_name'
246
- ].includes(setting.key)
247
- // Remove obfuscated settings
248
- && !(setting.value === settingsService.obfuscatedSetting && settingsService.isSecretSetting(setting));
249
- });
250
-
251
- const getSetting = setting => settingsCache.get(setting.key, {resolve: false});
252
-
253
- const firstUnknownSetting = settings.find(setting => !getSetting(setting));
254
-
255
- if (firstUnknownSetting) {
256
- throw new NotFoundError({
257
- message: i18n.t('errors.api.settings.problemFindingSetting', {
258
- key: firstUnknownSetting.key
259
- })
260
- });
261
- }
262
-
263
- if (!(frame.options.context && frame.options.context.internal)) {
264
- const firstCoreSetting = settings.find(setting => getSetting(setting).group === 'core');
265
- if (firstCoreSetting) {
266
- throw new NoPermissionError({
267
- message: i18n.t('errors.api.settings.accessCoreSettingFromExtReq')
268
- });
269
- }
270
- }
271
-
272
175
  if (stripeConnectIntegrationToken && stripeConnectIntegrationToken.value) {
273
176
  const getSessionProp = prop => frame.original.session[prop];
274
- try {
275
- const data = await membersService.stripeConnect.getStripeConnectTokenData(stripeConnectIntegrationToken.value, getSessionProp);
276
- settings.push({
277
- key: 'stripe_connect_publishable_key',
278
- value: data.public_key
279
- });
280
- settings.push({
281
- key: 'stripe_connect_secret_key',
282
- value: data.secret_key
283
- });
284
- settings.push({
285
- key: 'stripe_connect_livemode',
286
- value: data.livemode
287
- });
288
- settings.push({
289
- key: 'stripe_connect_display_name',
290
- value: data.display_name
291
- });
292
- settings.push({
293
- key: 'stripe_connect_account_id',
294
- value: data.account_id
295
- });
296
- } catch (err) {
297
- throw new BadRequestError({
298
- err,
299
- message: 'The Stripe Connect token could not be parsed.'
300
- });
301
- }
177
+
178
+ stripeConnectData = await settingsBREADService.getStripeConnectData(
179
+ stripeConnectIntegrationToken,
180
+ getSessionProp,
181
+ membersService.stripeConnect.getStripeConnectTokenData
182
+ );
302
183
  }
303
184
 
304
- return models.Settings.edit(settings, frame.options);
185
+ return await settingsBREADService.edit(frame.data.settings, frame.options, stripeConnectData);
305
186
  }
306
187
  },
307
188
 
@@ -1,7 +1,13 @@
1
1
  const Promise = require('bluebird');
2
2
  const _ = require('lodash');
3
3
  const i18n = require('../../../../../../shared/i18n');
4
- const {NotFoundError, ValidationError} = require('@tryghost/errors');
4
+ const {NotFoundError, ValidationError, BadRequestError} = require('@tryghost/errors');
5
+ const validator = require('@tryghost/validator');
6
+
7
+ const messages = {
8
+ invalidEmailReceived: 'Please send a valid email',
9
+ invalidEmailTypeReceived: 'Invalid email type received'
10
+ };
5
11
 
6
12
  module.exports = {
7
13
  read(apiConfig, frame) {
@@ -62,5 +68,21 @@ module.exports = {
62
68
  if (errors.length) {
63
69
  return Promise.reject(errors[0]);
64
70
  }
71
+ },
72
+
73
+ updateMembersEmail(apiConfig, frame) {
74
+ const {email, type} = frame.data;
75
+
76
+ if (typeof email !== 'string' || !validator.isEmail(email)) {
77
+ throw new BadRequestError({
78
+ message: messages.invalidEmailReceived
79
+ });
80
+ }
81
+
82
+ if (!type || !['fromAddressUpdate', 'supportAddressUpdate'].includes(type)) {
83
+ throw new BadRequestError({
84
+ message: messages.invalidEmailTypeReceived
85
+ });
86
+ }
65
87
  }
66
88
  };
@@ -50,6 +50,7 @@ const TABLES_ALLOWLIST = [
50
50
  'roles',
51
51
  'roles_users',
52
52
  'settings',
53
+ 'custom_theme_settings',
53
54
  'tags',
54
55
  'users'
55
56
  ];