ghost 5.4.1 → 5.7.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 (117) 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-8f5c061e0892b93adecc2b9e37ad2f3a.css +1 -0
  52. package/core/built/assets/{ghost.min-36b64813b14c45075770658269d4b478.js → ghost.min-ff9ba089fd81cb40831f4b62e63a2ca9.js} +3015 -2874
  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/helpers/img_url.js +67 -6
  58. package/core/frontend/utils/frontend-apps.js +33 -0
  59. package/core/frontend/web/middleware/handle-image-sizes.js +7 -11
  60. package/core/server/api/endpoints/{comments-comments.js → comments-members.js} +24 -43
  61. package/core/server/api/endpoints/index.js +2 -6
  62. package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
  63. package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +17 -0
  64. package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +18 -0
  65. package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +1 -0
  66. package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +11 -0
  67. package/core/server/api/endpoints/utils/serializers/output/members.js +12 -1
  68. package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +4 -0
  69. package/core/server/data/exporter/table-lists.js +2 -1
  70. package/core/server/data/migrations/versions/5.3/2022-07-06-09-17-add-ghost-explore-integration.js +0 -1
  71. package/core/server/data/migrations/versions/5.3/2022-07-06-09-26-add-ghost-explore-integration-api-key.js +0 -1
  72. package/core/server/data/migrations/versions/5.5/2022-07-18-14-29-add-comment-reporting-permissions.js +10 -0
  73. package/core/server/data/migrations/versions/5.5/2022-07-18-14-31-drop-reports-reason.js +3 -0
  74. package/core/server/data/migrations/versions/5.5/2022-07-18-14-32-drop-nullable-member-id-from-likes.js +4 -0
  75. package/core/server/data/migrations/versions/5.5/2022-07-18-14-33-fix-comments-on-delete-foreign-keys.js +119 -0
  76. package/core/server/data/migrations/versions/5.5/2022-07-21-08-56-add-jobs-table.js +11 -0
  77. package/core/server/data/migrations/versions/5.6/2022-07-27-13-40-change-explore-type.js +24 -0
  78. package/core/server/data/schema/commands.js +7 -2
  79. package/core/server/data/schema/fixtures/fixtures.json +6 -1
  80. package/core/server/data/schema/schema.js +12 -4
  81. package/core/server/ghost-server.js +0 -22
  82. package/core/server/models/comment-report.js +34 -0
  83. package/core/server/models/comment.js +8 -7
  84. package/core/server/models/job.js +9 -0
  85. package/core/server/services/bulk-email/bulk-email-processor.js +6 -0
  86. package/core/server/services/comments/controller.js +82 -0
  87. package/core/server/services/comments/email-templates/new-comment-reply.hbs +2 -2
  88. package/core/server/services/comments/email-templates/new-comment-reply.txt.js +7 -8
  89. package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
  90. package/core/server/services/comments/email-templates/new-comment.txt.js +7 -6
  91. package/core/server/services/comments/email-templates/report.hbs +199 -0
  92. package/core/server/services/comments/email-templates/report.txt.js +16 -0
  93. package/core/server/services/comments/emails.js +57 -1
  94. package/core/server/services/comments/index.js +6 -1
  95. package/core/server/services/comments/service.js +291 -9
  96. package/core/server/services/jobs/job-service.js +24 -1
  97. package/core/server/services/mail/GhostMailer.js +1 -0
  98. package/core/server/services/mega/email-preview.js +5 -1
  99. package/core/server/services/mega/mega.js +2 -4
  100. package/core/server/services/mega/post-email-serializer.js +97 -2
  101. package/core/server/services/mega/segment-parser.js +10 -1
  102. package/core/server/services/members/api.js +2 -1
  103. package/core/server/services/members/service.js +9 -4
  104. package/core/server/services/public-config/config.js +2 -1
  105. package/core/server/services/stripe/service.js +9 -1
  106. package/core/server/web/admin/views/default-prod.html +4 -4
  107. package/core/server/web/admin/views/default.html +4 -4
  108. package/core/server/web/api/testmode/jobs/graceful-job.js +2 -2
  109. package/core/server/web/api/testmode/routes.js +14 -0
  110. package/core/server/web/comments/routes.js +10 -8
  111. package/core/shared/config/defaults.json +12 -7
  112. package/core/shared/config/env/config.testing.json +3 -2
  113. package/core/shared/labs.js +5 -2
  114. package/package.json +92 -59
  115. package/yarn.lock +1821 -2011
  116. package/core/built/assets/ghost-dark-739c1f5546bd048eeeb253965ef36712.css +0 -1
  117. 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
  }
@@ -12,6 +12,7 @@ const url = require('url');
12
12
  const _ = require('lodash');
13
13
  const logging = require('@tryghost/logging');
14
14
  const tpl = require('@tryghost/tpl');
15
+ const imageTransform = require('@tryghost/image-transform');
15
16
 
16
17
  const messages = {
17
18
  attrIsRequired: 'Attribute is required e.g. {{img_url feature_image}}'
@@ -41,15 +42,26 @@ module.exports = function imgUrl(requestedImageUrl, options) {
41
42
 
42
43
  // CASE: if you pass an external image, there is nothing we want to do to it!
43
44
  const isInternalImage = detectInternalImage(requestedImageUrl);
45
+ const sizeOptions = getImageSizeOptions(options);
46
+
44
47
  if (!isInternalImage) {
48
+ // Detect Unsplash width and format
49
+ const isUnsplashImage = /images\.unsplash\.com/.test(requestedImageUrl);
50
+ if (isUnsplashImage) {
51
+ try {
52
+ return getUnsplashImage(requestedImageUrl, sizeOptions);
53
+ } catch (e) {
54
+ // ignore errors and just return the original URL
55
+ }
56
+ }
57
+
45
58
  return requestedImageUrl;
46
59
  }
47
60
 
48
- const {requestedSize, imageSizes} = getImageSizeOptions(options);
49
61
  const absoluteUrlRequested = getAbsoluteOption(options);
50
62
 
51
63
  function applyImageSizes(image) {
52
- return getImageWithSize(image, requestedSize, imageSizes);
64
+ return getImageWithSize(image, sizeOptions);
53
65
  }
54
66
 
55
67
  function getImageUrl(image) {
@@ -79,10 +91,12 @@ function getAbsoluteOption(options) {
79
91
  function getImageSizeOptions(options) {
80
92
  const requestedSize = options && options.hash && options.hash.size;
81
93
  const imageSizes = options && options.data && options.data.config && options.data.config.image_sizes;
94
+ const requestedFormat = options && options.hash && options.hash.format;
82
95
 
83
96
  return {
84
97
  requestedSize,
85
- imageSizes
98
+ imageSizes,
99
+ requestedFormat
86
100
  };
87
101
  }
88
102
 
@@ -99,12 +113,58 @@ function detectInternalImage(requestedImageUrl) {
99
113
  return isAbsoluteInternalImage || isRelativeInternalImage;
100
114
  }
101
115
 
102
- function getImageWithSize(imagePath, requestedSize, imageSizes) {
116
+ function getUnsplashImage(imagePath, sizeOptions) {
117
+ const parsedUrl = new URL(imagePath);
118
+ const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
119
+
120
+ if (requestedFormat) {
121
+ const supportedFormats = ['avif', 'gif', 'jpg', 'png', 'webp'];
122
+ if (supportedFormats.includes(requestedFormat)) {
123
+ parsedUrl.searchParams.set('fm', requestedFormat);
124
+ } else if (requestedFormat === 'jpeg') {
125
+ // Map to alias
126
+ parsedUrl.searchParams.set('fm', 'jpg');
127
+ }
128
+ }
129
+
130
+ if (!imageSizes || !imageSizes[requestedSize]) {
131
+ return parsedUrl.toString();
132
+ }
133
+
134
+ const {width, height} = imageSizes[requestedSize];
135
+
136
+ if (!width && !height) {
137
+ return parsedUrl.toString();
138
+ }
139
+
140
+ parsedUrl.searchParams.delete('w');
141
+ parsedUrl.searchParams.delete('h');
142
+
143
+ if (width) {
144
+ parsedUrl.searchParams.set('w', width);
145
+ }
146
+ if (height) {
147
+ parsedUrl.searchParams.set('h', height);
148
+ }
149
+ return parsedUrl.toString();
150
+ }
151
+
152
+ /**
153
+ *
154
+ * @param {string} imagePath
155
+ * @param {Object} sizeOptions
156
+ * @param {string} sizeOptions.requestedSize
157
+ * @param {Object[]} sizeOptions.imageSizes
158
+ * @param {string} [sizeOptions.requestedFormat]
159
+ * @returns
160
+ */
161
+ function getImageWithSize(imagePath, sizeOptions) {
103
162
  const hasLeadingSlash = imagePath[0] === '/';
104
163
 
105
164
  if (hasLeadingSlash) {
106
- return '/' + getImageWithSize(imagePath.slice(1), requestedSize, imageSizes);
165
+ return '/' + getImageWithSize(imagePath.slice(1), sizeOptions);
107
166
  }
167
+ const {requestedSize, imageSizes, requestedFormat} = sizeOptions;
108
168
 
109
169
  if (!requestedSize) {
110
170
  return imagePath;
@@ -123,8 +183,9 @@ function getImageWithSize(imagePath, requestedSize, imageSizes) {
123
183
  const [imgBlogUrl, imageName] = imagePath.split(STATIC_IMAGE_URL_PREFIX);
124
184
 
125
185
  const sizeDirectoryName = prefixIfPresent('w', width) + prefixIfPresent('h', height);
186
+ const formatPrefix = requestedFormat && imageTransform.canTransformToFormat(requestedFormat) ? `/format/${requestedFormat}` : '';
126
187
 
127
- return [imgBlogUrl, STATIC_IMAGE_URL_PREFIX, `/size/${sizeDirectoryName}`, imageName].join('');
188
+ return [imgBlogUrl, STATIC_IMAGE_URL_PREFIX, `/size/${sizeDirectoryName}`, formatPrefix, imageName].join('');
128
189
  }
129
190
 
130
191
  function prefixIfPresent(prefix, string) {
@@ -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};
@@ -58,11 +58,6 @@ module.exports = function (req, res, next) {
58
58
  const themeImageSizes = activeTheme.get().config('image_sizes');
59
59
  const imageSizes = _.merge({}, themeImageSizes, internalImageSizes, contentImageSizes);
60
60
 
61
- // CASE: no image_sizes config (NOTE - unlikely to be reachable now we have content sizes)
62
- if (!imageSizes) {
63
- return redirectToOriginal();
64
- }
65
-
66
61
  // build a new object with keys that match the strings used in size paths like "w640h480"
67
62
  const imageDimensions = {};
68
63
  Object.keys(imageSizes).forEach((size) => {
@@ -106,16 +101,16 @@ module.exports = function (req, res, next) {
106
101
  return redirectToOriginal();
107
102
  }
108
103
 
104
+ // exit early if sharp isn't installed to avoid extra file reads
105
+ if (!imageTransform.canTransformFiles()) {
106
+ return redirectToOriginal();
107
+ }
108
+
109
109
  storageInstance.exists(req.url).then((exists) => {
110
110
  if (exists) {
111
111
  return;
112
112
  }
113
113
 
114
- // exit early if sharp isn't installed to avoid extra file reads
115
- if (!imageTransform.canTransformFiles()) {
116
- return redirectToOriginal();
117
- }
118
-
119
114
  const {dir, name, ext} = path.parse(imagePath);
120
115
  const [imageNameMatched, imageName, imageNumber] = name.match(/^(.+?)(-\d+)?$/) || [null];
121
116
 
@@ -148,7 +143,8 @@ module.exports = function (req, res, next) {
148
143
  }).then(() => {
149
144
  if (format) {
150
145
  // File extension won't match the new format, so we need to update the Content-Type header manually here
151
- res.type(format);
146
+ // Express JS still uses an out of date mime package, which doesn't support avif
147
+ res.type(format === 'avif' ? 'image/avif' : format);
152
148
  }
153
149
  next();
154
150
  }).catch(function (err) {
@@ -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});