ghost 5.4.0 → 5.6.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 (116) hide show
  1. package/PRIVACY.md +3 -2
  2. package/components/tryghost-adapter-manager-0.0.0.tgz +0 -0
  3. package/components/tryghost-api-version-compatibility-service-0.0.0.tgz +0 -0
  4. package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
  5. package/components/tryghost-constants-0.0.0.tgz +0 -0
  6. package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
  7. package/components/tryghost-domain-events-0.0.0.tgz +0 -0
  8. package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
  9. package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
  10. package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
  11. package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
  12. package/components/tryghost-extract-api-key-0.0.0.tgz +0 -0
  13. package/components/tryghost-job-manager-0.0.0.tgz +0 -0
  14. package/components/tryghost-magic-link-0.0.0.tgz +0 -0
  15. package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
  16. package/components/tryghost-member-events-0.0.0.tgz +0 -0
  17. package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
  18. package/components/tryghost-members-api-0.0.0.tgz +0 -0
  19. package/components/tryghost-members-csv-0.0.0.tgz +0 -0
  20. package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
  21. package/components/tryghost-members-importer-0.0.0.tgz +0 -0
  22. package/components/tryghost-members-offers-0.0.0.tgz +0 -0
  23. package/components/tryghost-members-payments-0.0.0.tgz +0 -0
  24. package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
  25. package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
  26. package/components/tryghost-minifier-0.0.0.tgz +0 -0
  27. package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
  28. package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
  29. package/components/tryghost-mw-session-from-token-0.0.0.tgz +0 -0
  30. package/components/tryghost-mw-update-user-last-seen-0.0.0.tgz +0 -0
  31. package/components/tryghost-mw-vhost-0.0.0.tgz +0 -0
  32. package/components/tryghost-package-json-0.0.0.tgz +0 -0
  33. package/components/tryghost-security-0.0.0.tgz +0 -0
  34. package/components/tryghost-session-service-0.0.0.tgz +0 -0
  35. package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
  36. package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
  37. package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
  38. package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
  39. package/content/themes/casper/assets/built/global.css +1 -1
  40. package/content/themes/casper/assets/built/global.css.map +1 -1
  41. package/content/themes/casper/assets/built/screen.css +1 -1
  42. package/content/themes/casper/assets/built/screen.css.map +1 -1
  43. package/content/themes/casper/assets/css/screen.css +9 -1
  44. package/content/themes/casper/gulpfile.js +1 -1
  45. package/content/themes/casper/package.json +9 -9
  46. package/content/themes/casper/yarn.lock +1154 -1249
  47. package/core/boot.js +6 -1
  48. package/core/built/assets/{chunk.3.dc389a0f93cb5fabd695.js → chunk.3.33097bb5eb150719bdd2.js} +19 -19
  49. package/core/built/assets/fonts/Inter.ttf +0 -0
  50. package/core/built/assets/ghost-dark-1bdd57aba1fa4a23388121740454dab2.css +1 -0
  51. package/core/built/assets/{ghost.min-36b64813b14c45075770658269d4b478.js → ghost.min-31bd482d1bcfe706448bc6f401481a28.js} +3001 -2874
  52. package/core/built/assets/ghost.min-b0576e0e36343533e70992f3e5bb02bb.css +1 -0
  53. package/core/built/assets/icons/event-comment.svg +3 -0
  54. package/core/built/assets/{vendor.min-be0129c9c6897c9f10425e2402881d77.js → vendor.min-3dd40d3052381526f38fd290d13baa47.js} +2394 -924
  55. package/core/frontend/helpers/comments.js +39 -14
  56. package/core/frontend/helpers/ghost_head.js +22 -4
  57. package/core/frontend/utils/frontend-apps.js +33 -0
  58. package/core/server/api/endpoints/{comments-comments.js → comments-members.js} +24 -43
  59. package/core/server/api/endpoints/index.js +2 -6
  60. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  61. package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +17 -0
  62. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +18 -0
  63. package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +1 -0
  64. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +11 -0
  65. package/core/server/api/endpoints/utils/serializers/output/members.js +12 -1
  66. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +4 -0
  67. package/core/server/data/exporter/table-lists.js +2 -1
  68. package/core/server/data/migrations/versions/5.3/2022-07-06-09-17-add-ghost-explore-integration.js +0 -1
  69. package/core/server/data/migrations/versions/5.3/2022-07-06-09-26-add-ghost-explore-integration-api-key.js +0 -1
  70. package/core/server/data/migrations/versions/5.5/2022-07-18-14-29-add-comment-reporting-permissions.js +10 -0
  71. package/core/server/data/migrations/versions/5.5/2022-07-18-14-31-drop-reports-reason.js +3 -0
  72. package/core/server/data/migrations/versions/5.5/2022-07-18-14-32-drop-nullable-member-id-from-likes.js +4 -0
  73. package/core/server/data/migrations/versions/5.5/2022-07-18-14-33-fix-comments-on-delete-foreign-keys.js +119 -0
  74. package/core/server/data/migrations/versions/5.5/2022-07-21-08-56-add-jobs-table.js +11 -0
  75. package/core/server/data/migrations/versions/5.6/2022-07-27-13-40-change-explore-type.js +24 -0
  76. package/core/server/data/schema/commands.js +7 -2
  77. package/core/server/data/schema/fixtures/fixtures.json +6 -1
  78. package/core/server/data/schema/schema.js +12 -4
  79. package/core/server/ghost-server.js +0 -22
  80. package/core/server/models/comment-report.js +34 -0
  81. package/core/server/models/comment.js +8 -7
  82. package/core/server/models/job.js +9 -0
  83. package/core/server/services/bulk-email/bulk-email-processor.js +6 -0
  84. package/core/server/services/comments/controller.js +82 -0
  85. package/core/server/services/comments/email-templates/new-comment-reply.hbs +2 -2
  86. package/core/server/services/comments/email-templates/new-comment-reply.txt.js +7 -8
  87. package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
  88. package/core/server/services/comments/email-templates/new-comment.txt.js +7 -6
  89. package/core/server/services/comments/email-templates/report.hbs +199 -0
  90. package/core/server/services/comments/email-templates/report.txt.js +16 -0
  91. package/core/server/services/comments/emails.js +57 -1
  92. package/core/server/services/comments/index.js +6 -1
  93. package/core/server/services/comments/service.js +291 -9
  94. package/core/server/services/jobs/job-service.js +24 -1
  95. package/core/server/services/mail/GhostMailer.js +1 -0
  96. package/core/server/services/mega/email-preview.js +5 -1
  97. package/core/server/services/mega/mega.js +2 -4
  98. package/core/server/services/mega/post-email-serializer.js +97 -2
  99. package/core/server/services/mega/segment-parser.js +10 -1
  100. package/core/server/services/members/api.js +2 -1
  101. package/core/server/services/members/service.js +9 -4
  102. package/core/server/services/public-config/config.js +2 -1
  103. package/core/server/services/settings/settings-bread-service.js +1 -1
  104. package/core/server/services/stripe/service.js +9 -1
  105. package/core/server/web/admin/views/default-prod.html +4 -4
  106. package/core/server/web/admin/views/default.html +4 -4
  107. package/core/server/web/api/testmode/jobs/graceful-job.js +2 -2
  108. package/core/server/web/api/testmode/routes.js +14 -0
  109. package/core/server/web/comments/routes.js +10 -8
  110. package/core/shared/config/defaults.json +12 -7
  111. package/core/shared/config/env/config.testing.json +3 -2
  112. package/core/shared/labs.js +3 -1
  113. package/package.json +92 -59
  114. package/yarn.lock +1821 -2011
  115. package/core/built/assets/ghost-dark-739c1f5546bd048eeeb253965ef36712.css +0 -1
  116. package/core/built/assets/ghost.min-5211776b9497f36fac8c9e5f2584cbcc.css +0 -1
@@ -1,23 +1,47 @@
1
1
  const {SafeString} = require('../services/handlebars');
2
- const {config, urlUtils, getFrontendKey, labs} = require('../services/proxy');
2
+ const {urlUtils, getFrontendKey, labs, settingsCache} = require('../services/proxy');
3
+ const {getFrontendAppConfig, getDataAttributes} = require('../utils/frontend-apps');
3
4
 
4
5
  async function comments(options) {
5
6
  // todo: For now check on the comment id to exclude normal pages (we probably have a better way to do this)
6
7
 
7
8
  const commentId = this.comment_id;
8
-
9
+
9
10
  if (!commentId) {
10
11
  return;
11
12
  }
12
-
13
+
14
+ /**
15
+ * We need to check if comments enabled, because the theme might not be using the other available helpers to check
16
+ * if comments is enabled + the member has access
17
+ * @type {'all'|'paid'|'off'}
18
+ */
19
+ const commentsEnabled = settingsCache.get('comments_enabled');
20
+ const hasAccess = !!this.access;
21
+
22
+ if (commentsEnabled === 'off' || !hasAccess) {
23
+ return;
24
+ }
25
+
13
26
  let colorScheme = 'auto';
14
- if (options.hash.color_scheme === 'dark' || options.hash.color_scheme === 'light') {
15
- colorScheme = options.hash.color_scheme;
27
+ if (options.hash.mode === 'dark' || options.hash.mode === 'light') {
28
+ colorScheme = options.hash.mode;
16
29
  }
17
30
 
18
- let avatarSaturation = parseInt(options.hash.avatar_saturation);
31
+ let avatarSaturation = parseInt(options.hash.saturation);
19
32
  if (isNaN(avatarSaturation)) {
20
- avatarSaturation = 50;
33
+ avatarSaturation = 60;
34
+ }
35
+
36
+ let count = true;
37
+ if (options.hash.count === false) {
38
+ count = false;
39
+ }
40
+
41
+ // This is null so that the comments-ui can handle the default title
42
+ let title = null;
43
+ if (typeof options.hash.title === 'string') {
44
+ title = options.hash.title;
21
45
  }
22
46
 
23
47
  let accentColor = '';
@@ -26,28 +50,29 @@ async function comments(options) {
26
50
  }
27
51
 
28
52
  const frontendKey = await getFrontendKey();
53
+ const {scriptUrl, stylesUrl, appVersion} = getFrontendAppConfig('comments');
29
54
 
30
55
  const data = {
31
56
  'ghost-comments': urlUtils.getSiteUrl(),
32
57
  api: urlUtils.urlFor('api', {type: 'content'}, true),
33
58
  admin: urlUtils.urlFor('admin', true),
34
59
  key: frontendKey,
60
+ styles: stylesUrl,
61
+ title: title,
62
+ count: count,
35
63
  'post-id': this.id,
36
64
  'sentry-dsn': '', /* todo: insert sentry dsn key here */
37
65
  'color-scheme': colorScheme,
38
66
  'avatar-saturation': avatarSaturation,
39
67
  'accent-color': accentColor,
40
- 'app-version': config.get('comments:version')
68
+ 'app-version': appVersion,
69
+ 'comments-enabled': commentsEnabled
41
70
  };
42
71
 
43
- let dataAttributes = '';
44
-
45
- Object.entries(data).forEach(([key, value]) => {
46
- dataAttributes += `data-${key}="${value}" `;
47
- });
72
+ const dataAttributes = getDataAttributes(data);
48
73
 
49
74
  return new SafeString(`
50
- <script defer src="${config.get('comments:url')}" ${dataAttributes} crossorigin="anonymous"></script>
75
+ <script defer src="${scriptUrl}" ${dataAttributes} crossorigin="anonymous"></script>
51
76
  `);
52
77
  }
53
78
 
@@ -13,6 +13,7 @@ const logging = require('@tryghost/logging');
13
13
  const _ = require('lodash');
14
14
  const debug = require('@tryghost/debug')('ghost_head');
15
15
  const templateStyles = require('./tpl/styles');
16
+ const {getFrontendAppConfig, getDataAttributes} = require('../utils/frontend-apps');
16
17
 
17
18
  const {get: getMetaData, getAssetUrl} = metaData;
18
19
 
@@ -47,9 +48,20 @@ function getMembersHelper(data, frontendKey) {
47
48
  if (!settingsCache.get('members_enabled')) {
48
49
  return '';
49
50
  }
51
+ const {scriptUrl} = getFrontendAppConfig('portal');
52
+
53
+ const colorString = (_.has(data, 'site._preview') && data.site.accent_color) ? data.site.accent_color : '';
54
+ const attributes = {
55
+ ghost: urlUtils.getSiteUrl(),
56
+ key: frontendKey,
57
+ api: urlUtils.urlFor('api', {type: 'content'}, true)
58
+ };
59
+ if (colorString) {
60
+ attributes['accent-color'] = colorString;
61
+ }
62
+ const dataAttributes = getDataAttributes(attributes);
50
63
 
51
- const colorString = _.has(data, 'site._preview') && data.site.accent_color ? ` data-accent-color="${data.site.accent_color}"` : '';
52
- let membersHelper = `<script defer src="${config.get('portal:url')}" data-ghost="${urlUtils.getSiteUrl()}"${colorString} data-key="${frontendKey}" data-api="${urlUtils.urlFor('api', {type: 'content'}, true)}" crossorigin="anonymous"></script>`;
64
+ let membersHelper = `<script defer src="${scriptUrl}" ${dataAttributes} crossorigin="anonymous"></script>`;
53
65
  membersHelper += (`<style id="gh-members-styles">${templateStyles}</style>`);
54
66
  if (settingsCache.get('paid_members_enabled')) {
55
67
  membersHelper += '<script async src="https://js.stripe.com/v3/"></script>';
@@ -59,8 +71,14 @@ function getMembersHelper(data, frontendKey) {
59
71
 
60
72
  function getSearchHelper(frontendKey) {
61
73
  const adminUrl = urlUtils.getAdminUrl() || urlUtils.getSiteUrl();
62
-
63
- let helper = `<script defer src="${config.get('sodoSearch:url')}" data-sodo-search="${adminUrl}" data-version="${config.get('sodoSearch:version')}" data-key="${frontendKey}" crossorigin="anonymous"></script>`;
74
+ const {scriptUrl, stylesUrl} = getFrontendAppConfig('sodoSearch');
75
+ const attrs = {
76
+ key: frontendKey,
77
+ styles: stylesUrl,
78
+ 'sodo-search': adminUrl
79
+ };
80
+ const dataAttrs = getDataAttributes(attrs);
81
+ let helper = `<script defer src="${scriptUrl}" ${dataAttrs} crossorigin="anonymous"></script>`;
64
82
 
65
83
  return helper;
66
84
  }
@@ -0,0 +1,33 @@
1
+ const {config} = require('../services/proxy');
2
+
3
+ function getFrontendAppConfig(app) {
4
+ const appVersion = config.get(`${app}:version`);
5
+ let scriptUrl = config.get(`${app}:url`);
6
+ let stylesUrl = config.get(`${app}:styles`);
7
+ if (scriptUrl.includes('{version}')) {
8
+ scriptUrl = scriptUrl.replace('{version}', appVersion);
9
+ }
10
+ if (stylesUrl?.includes('{version}')) {
11
+ stylesUrl = stylesUrl.replace('{version}', appVersion);
12
+ }
13
+ return {
14
+ scriptUrl,
15
+ stylesUrl,
16
+ appVersion
17
+ };
18
+ }
19
+
20
+ function getDataAttributes(data) {
21
+ let dataAttributes = '';
22
+
23
+ if (!data) {
24
+ return dataAttributes;
25
+ }
26
+ Object.entries(data).forEach(([key, value]) => {
27
+ dataAttributes += `data-${key}="${value}" `;
28
+ });
29
+
30
+ return dataAttributes.trim();
31
+ }
32
+
33
+ module.exports = {getFrontendAppConfig, getDataAttributes};
@@ -3,6 +3,7 @@ const tpl = require('@tryghost/tpl');
3
3
  const errors = require('@tryghost/errors');
4
4
  const models = require('../../models');
5
5
  const db = require('../../data/db');
6
+ const commentsService = require('../../services/comments');
6
7
  const ALLOWED_INCLUDES = ['post', 'member', 'likes', 'replies'];
7
8
  const UNSAFE_ATTRS = ['status'];
8
9
 
@@ -33,7 +34,7 @@ module.exports = {
33
34
  },
34
35
  permissions: true,
35
36
  query(frame) {
36
- return models.Comment.findPage(frame.options);
37
+ return commentsService.controller.browse(frame);
37
38
  }
38
39
  },
39
40
 
@@ -52,16 +53,7 @@ module.exports = {
52
53
  },
53
54
  permissions: true,
54
55
  query(frame) {
55
- return models.Comment.findOne(frame.data, frame.options)
56
- .then((model) => {
57
- if (!model) {
58
- return Promise.reject(new errors.NotFoundError({
59
- message: tpl(messages.commentNotFound)
60
- }));
61
- }
62
-
63
- return model;
64
- });
56
+ return commentsService.controller.read(frame);
65
57
  }
66
58
  },
67
59
 
@@ -83,16 +75,7 @@ module.exports = {
83
75
  },
84
76
  permissions: true,
85
77
  query(frame) {
86
- return models.Comment.edit(frame.data.comments[0], frame.options)
87
- .then((model) => {
88
- if (!model) {
89
- return Promise.reject(new errors.NotFoundError({
90
- message: tpl(messages.commentNotFound)
91
- }));
92
- }
93
-
94
- return model;
95
- });
78
+ return commentsService.controller.edit(frame);
96
79
  }
97
80
  },
98
81
 
@@ -116,19 +99,7 @@ module.exports = {
116
99
  unsafeAttrs: UNSAFE_ATTRS
117
100
  },
118
101
  query(frame) {
119
- // TODO: move to comment service
120
- const data = frame.data.comments[0];
121
-
122
- if (frame.options?.context?.member?.id) {
123
- data.member_id = frame.options.context.member.id;
124
-
125
- // todo: add validation that the parent comment is on the same post, and not deleted
126
- return models.Comment.add(data, frame.options);
127
- } else {
128
- return Promise.reject(new errors.NotFoundError({
129
- message: tpl(messages.memberNotFound)
130
- }));
131
- }
102
+ return commentsService.controller.add(frame);
132
103
  }
133
104
  },
134
105
 
@@ -145,15 +116,7 @@ module.exports = {
145
116
  },
146
117
  permissions: true,
147
118
  query(frame) {
148
- frame.options.require = true;
149
-
150
- return models.Comment.destroy(frame.options)
151
- .then(() => null)
152
- .catch(models.Comment.NotFoundError, () => {
153
- return Promise.reject(new errors.NotFoundError({
154
- message: tpl(messages.commentNotFound)
155
- }));
156
- });
119
+ return commentsService.controller.destroy(frame);
157
120
  }
158
121
  },
159
122
 
@@ -243,5 +206,23 @@ module.exports = {
243
206
  }));
244
207
  }
245
208
  }
209
+ },
210
+
211
+ report: {
212
+ statusCode: 204,
213
+ options: [
214
+ 'id'
215
+ ],
216
+ validation: {},
217
+ permissions: true,
218
+ async query(frame) {
219
+ if (!frame.options?.context?.member?.id) {
220
+ return Promise.reject(new errors.UnauthorizedError({
221
+ message: tpl(messages.memberNotFound)
222
+ }));
223
+ }
224
+
225
+ await commentsService.api.reportComment(frame.options.id, frame.options?.context?.member);
226
+ }
246
227
  }
247
228
  };
@@ -225,11 +225,7 @@ module.exports = {
225
225
  return shared.pipeline(require('./offers-public'), localUtils, 'content');
226
226
  },
227
227
 
228
- /**
229
- * Comment API
230
- */
231
-
232
- get commentsComments() {
233
- return shared.pipeline(require('./comments-comments'), localUtils, 'comments');
228
+ get commentsMembers() {
229
+ return shared.pipeline(require('./comments-members'), localUtils, 'comments');
234
230
  }
235
231
  };
@@ -18,7 +18,8 @@ module.exports = {
18
18
  'mailgunIsConfigured',
19
19
  'emailAnalytics',
20
20
  'hostSettings',
21
- 'tenor'
21
+ 'tenor',
22
+ 'editor'
22
23
  ];
23
24
 
24
25
  frame.response = {
@@ -0,0 +1,17 @@
1
+ const mapComment = require('./comments');
2
+
3
+ const commentEventMapper = (json, frame) => {
4
+ return {
5
+ ...json,
6
+ data: mapComment(json.data, frame)
7
+ };
8
+ };
9
+
10
+ const activityFeedMapper = (event, frame) => {
11
+ if (event.type === 'comment_event') {
12
+ return commentEventMapper(event, frame);
13
+ }
14
+ return event;
15
+ };
16
+
17
+ module.exports = activityFeedMapper;
@@ -1,4 +1,5 @@
1
1
  const _ = require('lodash');
2
+ const url = require('../utils/url');
2
3
 
3
4
  const commentFields = [
4
5
  'id',
@@ -16,6 +17,13 @@ const memberFields = [
16
17
  'avatar_image'
17
18
  ];
18
19
 
20
+ const postFields = [
21
+ 'id',
22
+ 'uuid',
23
+ 'title',
24
+ 'url'
25
+ ];
26
+
19
27
  const commentMapper = (model, frame) => {
20
28
  const jsonModel = model.toJSON ? model.toJSON(frame.options) : model;
21
29
 
@@ -37,6 +45,16 @@ const commentMapper = (model, frame) => {
37
45
  response.replies = jsonModel.replies.map(reply => commentMapper(reply, frame));
38
46
  }
39
47
 
48
+ if (jsonModel.parent) {
49
+ response.parent = commentMapper(jsonModel.parent, frame);
50
+ }
51
+
52
+ if (jsonModel.post) {
53
+ // We could use the post mapper here, but we need less field + don't need al the async behaviour support
54
+ url.forPost(jsonModel.post.id, jsonModel.post, frame);
55
+ response.post = _.pick(jsonModel.post, postFields);
56
+ }
57
+
40
58
  // todo
41
59
  response.liked = false;
42
60
  if (jsonModel.likes && frame.original.context.member && frame.original.context.member.id) {
@@ -1,5 +1,6 @@
1
1
  module.exports = {
2
2
  actions: require('./actions'),
3
+ activityFeedEvents: require('./activity-feed-events'),
3
4
  authors: require('./authors'),
4
5
  comments: require('./comments'),
5
6
  emails: require('./emails'),
@@ -17,6 +17,8 @@ const postsMetaSchema = require('../../../../../../data/schema').tables.posts_me
17
17
  const getPostServiceInstance = require('../../../../../../services/posts/posts-service');
18
18
  const postsService = getPostServiceInstance();
19
19
 
20
+ const commentsService = require('../../../../../../services/comments');
21
+
20
22
  module.exports = async (model, frame, options = {}) => {
21
23
  const {tiers: tiersData} = options || {};
22
24
  const extendedOptions = Object.assign(_.cloneDeep(frame.options), {
@@ -54,6 +56,15 @@ module.exports = async (model, frame, options = {}) => {
54
56
  if (utils.isContentAPI(frame)) {
55
57
  date.forPost(jsonModel);
56
58
  gating.forPost(jsonModel, frame);
59
+ if (jsonModel.access) {
60
+ if (commentsService?.api?.enabled !== 'off') {
61
+ jsonModel.comments = true;
62
+ } else {
63
+ jsonModel.comments = false;
64
+ }
65
+ } else {
66
+ jsonModel.comments = false;
67
+ }
57
68
  }
58
69
 
59
70
  // Transforms post/page metadata to flat structure
@@ -1,6 +1,7 @@
1
1
  //@ts-check
2
2
  const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:members');
3
3
  const {unparse} = require('@tryghost/members-csv');
4
+ const mappers = require('./mappers');
4
5
 
5
6
  module.exports = {
6
7
  browse: createSerializer('browse', paginatedMembers),
@@ -18,7 +19,7 @@ module.exports = {
18
19
  importCSV: createSerializer('importCSV', passthrough),
19
20
  memberStats: createSerializer('memberStats', passthrough),
20
21
  mrrStats: createSerializer('mrrStats', passthrough),
21
- activityFeed: createSerializer('activityFeed', passthrough)
22
+ activityFeed: createSerializer('activityFeed', activityFeed)
22
23
  };
23
24
 
24
25
  /**
@@ -73,6 +74,16 @@ function bulkAction(bulkActionResult, _apiConfig, frame) {
73
74
  };
74
75
  }
75
76
 
77
+ /**
78
+ *
79
+ * @returns {{events: any[]}}
80
+ */
81
+ function activityFeed(data, _apiConfig, frame) {
82
+ return {
83
+ events: data.events.map(e => mappers.activityFeedEvents(e, frame))
84
+ };
85
+ }
86
+
76
87
  /**
77
88
  * @template PageMeta
78
89
  *
@@ -102,6 +102,10 @@ const post = (attrs, frame) => {
102
102
  if (columns && columns.includes('visibility') && fields && !fields.includes('visibility')) {
103
103
  delete attrs.visibility;
104
104
  }
105
+
106
+ if (fields && !fields.includes('comments')) {
107
+ delete attrs.comments;
108
+ }
105
109
  }
106
110
 
107
111
  if (columns && columns.includes('email_segment') && fields && !fields.includes('email_segment')) {
@@ -34,7 +34,8 @@ const BACKUP_TABLES = [
34
34
  'members_newsletters',
35
35
  'comments',
36
36
  'comment_likes',
37
- 'comment_reports'
37
+ 'comment_reports',
38
+ 'jobs'
38
39
  ];
39
40
 
40
41
  // NOTE: exposing only tables which are going to be included in a "default" export file
@@ -6,7 +6,6 @@ module.exports = createTransactionalMigration(
6
6
  async function up(knex) {
7
7
  logging.info('Creating Ghost Explore Integration');
8
8
  const existingIntegration = await knex('integrations').where({
9
- type: 'internal',
10
9
  name: 'Ghost Explore',
11
10
  slug: 'ghost-explore'
12
11
  }).first();
@@ -10,7 +10,6 @@ module.exports = createTransactionalMigration(
10
10
 
11
11
  const integration = await knex('integrations').where({
12
12
  slug: 'ghost-explore',
13
- type: 'internal',
14
13
  name: 'Ghost Explore'
15
14
  }).first();
16
15
 
@@ -0,0 +1,10 @@
1
+ const {addPermissionWithRoles} = require('../../utils');
2
+
3
+ module.exports = addPermissionWithRoles({
4
+ name: 'Report comments',
5
+ action: 'report',
6
+ object: 'comment'
7
+ }, [
8
+ 'Administrator',
9
+ 'Admin Integration'
10
+ ]);
@@ -0,0 +1,3 @@
1
+ const {createDropColumnMigration} = require('../../utils');
2
+
3
+ module.exports = createDropColumnMigration('comment_reports', 'reason', {type: 'text', maxlength: 65535, nullable: false});
@@ -0,0 +1,4 @@
1
+ const {createDropNullableMigration} = require('../../utils');
2
+
3
+ // We need to disable foreign key checks because if MySQL is missing the STRICT_TRANS_TABLES mode, we cannot drop nullable from a foreign key
4
+ module.exports = createDropNullableMigration('comment_likes', 'member_id', {disableForeignKeyChecks: true});
@@ -0,0 +1,119 @@
1
+ const {addForeign, dropForeign} = require('../../../schema/commands');
2
+ const logging = require('@tryghost/logging');
3
+ const {createTransactionalMigration} = require('../../utils');
4
+
5
+ module.exports = createTransactionalMigration(
6
+ async function up(knex) {
7
+ logging.info('Adding on delete SET NULL for comments');
8
+
9
+ await dropForeign({
10
+ fromTable: 'comments',
11
+ fromColumn: 'member_id',
12
+ toTable: 'members',
13
+ toColumn: 'id',
14
+ transaction: knex
15
+ });
16
+
17
+ await addForeign({
18
+ fromTable: 'comments',
19
+ fromColumn: 'member_id',
20
+ toTable: 'members',
21
+ toColumn: 'id',
22
+ setNullDelete: true,
23
+ transaction: knex
24
+ });
25
+
26
+ logging.info('Adding on delete CASCADE for comment_likes');
27
+
28
+ await dropForeign({
29
+ fromTable: 'comment_likes',
30
+ fromColumn: 'member_id',
31
+ toTable: 'members',
32
+ toColumn: 'id',
33
+ transaction: knex
34
+ });
35
+
36
+ await addForeign({
37
+ fromTable: 'comment_likes',
38
+ fromColumn: 'member_id',
39
+ toTable: 'members',
40
+ toColumn: 'id',
41
+ cascadeDelete: true,
42
+ transaction: knex
43
+ });
44
+
45
+ logging.info('Adding on delete SET NULL for comment_reports');
46
+
47
+ await dropForeign({
48
+ fromTable: 'comment_reports',
49
+ fromColumn: 'member_id',
50
+ toTable: 'members',
51
+ toColumn: 'id',
52
+ transaction: knex
53
+ });
54
+
55
+ await addForeign({
56
+ fromTable: 'comment_reports',
57
+ fromColumn: 'member_id',
58
+ toTable: 'members',
59
+ toColumn: 'id',
60
+ setNullDelete: true,
61
+ transaction: knex
62
+ });
63
+ },
64
+ async function down(knex) {
65
+ logging.info('Restoring foreign key for comments');
66
+
67
+ await dropForeign({
68
+ fromTable: 'comments',
69
+ fromColumn: 'member_id',
70
+ toTable: 'members',
71
+ toColumn: 'id',
72
+ transaction: knex
73
+ });
74
+
75
+ await addForeign({
76
+ fromTable: 'comments',
77
+ fromColumn: 'member_id',
78
+ toTable: 'members',
79
+ toColumn: 'id',
80
+ transaction: knex
81
+ });
82
+
83
+ logging.info('Restoring foreign key for comment_likes');
84
+
85
+ await dropForeign({
86
+ fromTable: 'comment_likes',
87
+ fromColumn: 'member_id',
88
+ toTable: 'members',
89
+ toColumn: 'id',
90
+ transaction: knex
91
+ });
92
+
93
+ await addForeign({
94
+ fromTable: 'comment_likes',
95
+ fromColumn: 'member_id',
96
+ toTable: 'members',
97
+ toColumn: 'id',
98
+ transaction: knex
99
+ });
100
+
101
+ logging.info('Restoring foreign key for comment_reports');
102
+
103
+ await dropForeign({
104
+ fromTable: 'comment_reports',
105
+ fromColumn: 'member_id',
106
+ toTable: 'members',
107
+ toColumn: 'id',
108
+ transaction: knex
109
+ });
110
+
111
+ await addForeign({
112
+ fromTable: 'comment_reports',
113
+ fromColumn: 'member_id',
114
+ toTable: 'members',
115
+ toColumn: 'id',
116
+ transaction: knex
117
+ });
118
+ }
119
+ );
@@ -0,0 +1,11 @@
1
+ const {addTable} = require('../../utils');
2
+
3
+ module.exports = addTable('jobs', {
4
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
5
+ name: {type: 'string', maxlength: 191, nullable: false, unique: true},
6
+ status: {type: 'string', maxlength: 50, nullable: false, defaultTo: 'queued', validations: {isIn: [['started', 'finished', 'failed', 'queued']]}},
7
+ started_at: {type: 'dateTime', nullable: true},
8
+ finished_at: {type: 'dateTime', nullable: true},
9
+ created_at: {type: 'dateTime', nullable: false},
10
+ updated_at: {type: 'dateTime', nullable: true}
11
+ });
@@ -0,0 +1,24 @@
1
+ const logging = require('@tryghost/logging');
2
+ const {createTransactionalMigration} = require('../../utils');
3
+
4
+ module.exports = createTransactionalMigration(
5
+ async function up(knex) {
6
+ logging.info('Changing Ghost Explore Integration to type "builtin"');
7
+ await knex('integrations')
8
+ .where({
9
+ name: 'Ghost Explore',
10
+ slug: 'ghost-explore'
11
+ })
12
+ .update('type', 'builtin');
13
+ },
14
+ async function down(knex) {
15
+ logging.info('Changing Ghost Explore Integration to type "internal"');
16
+
17
+ await knex('integrations')
18
+ .where({
19
+ name: 'Ghost Explore',
20
+ slug: 'ghost-explore'
21
+ })
22
+ .update('type', 'internal');
23
+ }
24
+ );