ghost 4.13.0 → 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 (112) 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/{chunk.3.f80c7fbb7573ce508a05.js → chunk.3.4b1d9e20e57164ac9c29.js} +31 -29
  11. package/core/built/assets/ghost-dark-bb2831fc27fcb02893ed0a761207dc63.css +1 -0
  12. package/core/built/assets/{ghost.min-ba7f03a78d7d98444af386b8ae9347a7.js → ghost.min-d1d99f3ed6e0f427874b2a11e7078475.js} +1777 -1685
  13. package/core/built/assets/ghost.min-e7612edfa72b0fe2c201b387923e6fc7.css +1 -0
  14. package/core/built/assets/icons/check-2.svg +1 -0
  15. package/core/built/assets/icons/discount-bubble.svg +1 -0
  16. package/core/built/assets/icons/no-data-line-chart.svg +1 -0
  17. package/core/built/assets/icons/no-data-list.svg +9 -8
  18. package/core/built/assets/icons/no-data-subscription.svg +1 -0
  19. package/core/built/assets/{vendor.min-29784d514390cb5abc74ae660cb2fbc7.js → vendor.min-3660ec7864887f1496fe7a27fd23ab76.js} +1570 -1289
  20. package/core/frontend/helpers/ghost_head.js +7 -1
  21. package/core/frontend/helpers/match.js +19 -4
  22. package/core/frontend/helpers/products.js +68 -0
  23. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  24. package/core/frontend/services/routing/controllers/email-post.js +3 -2
  25. package/core/frontend/services/settings/loader.js +2 -2
  26. package/core/frontend/services/sitemap/base-generator.js +12 -8
  27. package/core/frontend/services/sitemap/manager.js +1 -1
  28. package/core/frontend/services/theme-engine/handlebars/helpers.js +1 -0
  29. package/core/frontend/services/theme-engine/middleware.js +4 -1
  30. package/core/server/api/canary/email-preview.js +15 -33
  31. package/core/server/api/canary/integrations.js +7 -30
  32. package/core/server/api/canary/labels.js +8 -9
  33. package/core/server/api/canary/members.js +13 -9
  34. package/core/server/api/canary/schedules.js +9 -57
  35. package/core/server/api/canary/settings.js +20 -158
  36. package/core/server/api/canary/themes.js +5 -59
  37. package/core/server/api/canary/utils/serializers/output/members.js +2 -14
  38. package/core/server/api/canary/utils/validators/input/settings.js +23 -1
  39. package/core/server/api/canary/webhooks.js +6 -24
  40. package/core/server/api/v2/schedules.js +9 -57
  41. package/core/server/api/v3/email-preview.js +15 -28
  42. package/core/server/api/v3/integrations.js +7 -30
  43. package/core/server/api/v3/labels.js +8 -9
  44. package/core/server/api/v3/members.js +4 -1
  45. package/core/server/api/v3/schedules.js +9 -57
  46. package/core/server/api/v3/settings.js +13 -132
  47. package/core/server/api/v3/utils/validators/input/settings.js +23 -1
  48. package/core/server/api/v3/webhooks.js +6 -28
  49. package/core/server/data/exporter/table-lists.js +1 -0
  50. package/core/server/data/importer/import-manager.js +398 -0
  51. package/core/server/data/importer/importers/data/data-importer.js +162 -0
  52. package/core/server/data/importer/importers/data/index.js +1 -162
  53. package/core/server/data/importer/index.js +1 -379
  54. package/core/server/data/migrations/versions/4.14/01-fix-comped-member-statuses.js +70 -0
  55. package/core/server/data/migrations/versions/4.14/02-fix-free-members-status-events.js +60 -0
  56. package/core/server/data/migrations/versions/4.15/01-add-temp-members-analytic-events-table.js +12 -0
  57. package/core/server/data/migrations/versions/4.16/01-add-custom-theme-settings-table.js +9 -0
  58. package/core/server/data/schema/fixtures/utils.js +6 -1
  59. package/core/server/data/schema/schema.js +26 -0
  60. package/core/server/lib/request-external.js +3 -2
  61. package/core/server/models/action.js +1 -1
  62. package/core/server/models/api-key.js +1 -1
  63. package/core/server/models/base/bookshelf.js +0 -3
  64. package/core/server/models/base/index.js +2 -0
  65. package/core/server/models/base/plugins/events.js +2 -2
  66. package/core/server/models/base/plugins/raw-knex.js +10 -10
  67. package/core/server/models/custom-theme-setting.js +9 -0
  68. package/core/server/models/email.js +2 -2
  69. package/core/server/models/index.js +2 -0
  70. package/core/server/models/integration.js +1 -1
  71. package/core/server/models/label.js +2 -2
  72. package/core/server/models/member-analytic-event.js +9 -0
  73. package/core/server/models/member.js +2 -2
  74. package/core/server/models/post.js +2 -2
  75. package/core/server/models/settings.js +2 -2
  76. package/core/server/models/tag.js +2 -2
  77. package/core/server/models/user.js +2 -2
  78. package/core/server/models/webhook.js +2 -2
  79. package/core/server/services/bulk-email/bulk-email-processor.js +1 -4
  80. package/core/server/services/custom-theme-settings.js +8 -0
  81. package/core/server/services/integrations/integrations-service.js +61 -0
  82. package/core/server/services/mail/GhostMailer.js +29 -37
  83. package/core/server/services/mega/email-preview.js +41 -0
  84. package/core/server/services/mega/index.js +4 -0
  85. package/core/server/services/mega/mega.js +27 -23
  86. package/core/server/services/mega/post-email-serializer.js +28 -21
  87. package/core/server/services/mega/template.js +11 -0
  88. package/core/server/services/members/api.js +1 -0
  89. package/core/server/services/members/emails/signup.js +1 -1
  90. package/core/server/services/oembed.js +7 -2
  91. package/core/server/services/posts/post-scheduling-service.js +100 -0
  92. package/core/server/services/settings/index.js +13 -16
  93. package/core/server/services/settings/settings-bread-service.js +188 -0
  94. package/core/server/services/settings/settings-utils.js +32 -0
  95. package/core/server/services/themes/ThemeStorage.js +5 -4
  96. package/core/server/services/themes/activation-bridge.js +14 -0
  97. package/core/server/services/themes/index.js +2 -0
  98. package/core/server/services/themes/installer.js +72 -0
  99. package/core/server/services/themes/validate.js +5 -2
  100. package/core/server/services/webhooks/webhooks-service.js +55 -0
  101. package/core/server/web/admin/views/default-prod.html +4 -4
  102. package/core/server/web/admin/views/default.html +4 -4
  103. package/core/server/web/members/app.js +3 -0
  104. package/core/shared/config/defaults.json +2 -2
  105. package/core/shared/custom-theme-settings-cache.js +3 -0
  106. package/core/shared/i18n/translations/en.json +1 -6
  107. package/core/shared/labs.js +5 -7
  108. package/package.json +64 -62
  109. package/yarn.lock +1490 -1055
  110. package/core/built/assets/ghost-dark-98d56e4973a502750748090f9dbc8280.css +0 -1
  111. package/core/built/assets/ghost.min-6932a664a1cb92a8e4a15f540cae3ad8.css +0 -1
  112. package/core/server/services/mega/template-labs.js +0 -1024
@@ -125,7 +125,10 @@ module.exports = {
125
125
  member = await membersService.api.members.create(frame.data.members[0], frame.options);
126
126
 
127
127
  if (frame.data.members[0].stripe_customer_id) {
128
- await membersService.api.members.linkStripeCustomer(frame.data.members[0].stripe_customer_id, member);
128
+ await membersService.api.members.linkStripeCustomer({
129
+ customer_id: frame.data.members[0].stripe_customer_id,
130
+ member_id: member.id
131
+ });
129
132
  }
130
133
 
131
134
  if (frame.data.members[0].comped) {
@@ -1,11 +1,6 @@
1
- const _ = require('lodash');
2
- const moment = require('moment');
3
- const config = require('../../../shared/config');
4
1
  const models = require('../../models');
5
- const urlUtils = require('../../../shared/url-utils');
6
- const i18n = require('../../../shared/i18n');
7
- const errors = require('@tryghost/errors');
8
- const api = require('./index');
2
+
3
+ const postSchedulingService = require('../../services/posts/post-scheduling-service')('v3');
9
4
 
10
5
  module.exports = {
11
6
  docName: 'schedules',
@@ -32,11 +27,8 @@ module.exports = {
32
27
  permissions: {
33
28
  docName: 'posts'
34
29
  },
35
- query(frame) {
36
- let resource;
30
+ async query(frame) {
37
31
  const resourceType = frame.options.resource;
38
- const publishAPostBySchedulerToleranceInMinutes = config.get('times').publishAPostBySchedulerToleranceInMinutes;
39
-
40
32
  const options = {
41
33
  status: 'scheduled',
42
34
  id: frame.options.id,
@@ -45,53 +37,13 @@ module.exports = {
45
37
  }
46
38
  };
47
39
 
48
- return api[resourceType].read({id: frame.options.id}, options)
49
- .then((result) => {
50
- resource = result[resourceType][0];
51
- const publishedAtMoment = moment(resource.published_at);
52
-
53
- if (publishedAtMoment.diff(moment(), 'minutes') > publishAPostBySchedulerToleranceInMinutes) {
54
- return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.job.notFound')}));
55
- }
56
-
57
- if (publishedAtMoment.diff(moment(), 'minutes') < publishAPostBySchedulerToleranceInMinutes * -1 && frame.data.force !== true) {
58
- return Promise.reject(new errors.NotFoundError({message: i18n.t('errors.api.job.publishInThePast')}));
59
- }
40
+ const {scheduledResource, preScheduledResource} = await postSchedulingService.publish(resourceType, frame.options.id, frame.data.force, options);
41
+ const cacheInvalidate = postSchedulingService.handleCacheInvalidation(scheduledResource, preScheduledResource);
42
+ this.headers.cacheInvalidate = cacheInvalidate;
60
43
 
61
- const editedResource = {};
62
- editedResource[resourceType] = [{
63
- status: 'published',
64
- updated_at: moment(resource.updated_at).toISOString(true)
65
- }];
66
-
67
- return api[resourceType].edit(
68
- editedResource,
69
- _.pick(options, ['context', 'id', 'transacting', 'forUpdate'])
70
- );
71
- })
72
- .then((result) => {
73
- const scheduledResource = result[resourceType][0];
74
-
75
- if (
76
- (scheduledResource.status === 'published' && resource.status !== 'published') ||
77
- (scheduledResource.status === 'draft' && resource.status === 'published')
78
- ) {
79
- this.headers.cacheInvalidate = true;
80
- } else if (
81
- (scheduledResource.status === 'draft' && resource.status !== 'published') ||
82
- (scheduledResource.status === 'scheduled' && resource.status !== 'scheduled')
83
- ) {
84
- this.headers.cacheInvalidate = {
85
- value: urlUtils.urlFor({
86
- relativeUrl: urlUtils.urlJoin('/p', scheduledResource.uuid, '/')
87
- })
88
- };
89
- } else {
90
- this.headers.cacheInvalidate = false;
91
- }
92
-
93
- return result;
94
- });
44
+ const response = {};
45
+ response[resourceType] = [scheduledResource];
46
+ return response;
95
47
  }
96
48
  },
97
49
 
@@ -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
  };
@@ -1,6 +1,11 @@
1
1
  const models = require('../../models');
2
2
  const i18n = require('../../../shared/i18n');
3
3
  const errors = require('@tryghost/errors');
4
+ const getWebhooksServiceInstance = require('../../services/webhooks/webhooks-service');
5
+
6
+ const webhooksService = getWebhooksServiceInstance({
7
+ WebhookModel: models.Webhook
8
+ });
4
9
 
5
10
  module.exports = {
6
11
  docName: 'webhooks',
@@ -15,34 +20,7 @@ module.exports = {
15
20
  data: [],
16
21
  permissions: true,
17
22
  async query(frame) {
18
- const isIntegrationRequest = frame.options.context && frame.options.context.integration && frame.options.context.integration.id;
19
-
20
- // NOTE: this check can be removed once `webhooks.integration_id` gets foreigh ke constraint (Ghost 4.0)
21
- if (!isIntegrationRequest && frame.data.webhooks[0].integration_id) {
22
- const integration = await models.Integration.findOne({id: frame.data.webhooks[0].integration_id}, {context: {internal: true}});
23
-
24
- if (!integration) {
25
- throw new errors.ValidationError({
26
- message: i18n.t('notices.data.validation.index.schemaValidationFailed', {
27
- key: 'integration_id'
28
- }),
29
- context: i18n.t('errors.api.webhooks.nonExistingIntegrationIdProvided.context'),
30
- help: i18n.t('errors.api.webhooks.nonExistingIntegrationIdProvided.help')
31
- });
32
- }
33
- }
34
-
35
- const webhook = await models.Webhook.getByEventAndTarget(
36
- frame.data.webhooks[0].event,
37
- frame.data.webhooks[0].target_url,
38
- frame.options
39
- );
40
-
41
- if (webhook) {
42
- throw new errors.ValidationError({message: i18n.t('errors.api.webhooks.webhookAlreadyExists')});
43
- }
44
-
45
- return models.Webhook.add(frame.data.webhooks[0], frame.options);
23
+ return await webhooksService.add(frame.data, frame.options);
46
24
  }
47
25
  },
48
26
 
@@ -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
  ];