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.
- package/PRIVACY.md +3 -2
- package/components/tryghost-adapter-manager-0.0.0.tgz +0 -0
- package/components/tryghost-api-version-compatibility-service-0.0.0.tgz +0 -0
- package/components/tryghost-bootstrap-socket-0.0.0.tgz +0 -0
- package/components/tryghost-constants-0.0.0.tgz +0 -0
- package/components/tryghost-custom-theme-settings-service-0.0.0.tgz +0 -0
- package/components/tryghost-domain-events-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-provider-mailgun-0.0.0.tgz +0 -0
- package/components/tryghost-email-analytics-service-0.0.0.tgz +0 -0
- package/components/tryghost-email-content-generator-0.0.0.tgz +0 -0
- package/components/tryghost-express-dynamic-redirects-0.0.0.tgz +0 -0
- package/components/tryghost-extract-api-key-0.0.0.tgz +0 -0
- package/components/tryghost-job-manager-0.0.0.tgz +0 -0
- package/components/tryghost-magic-link-0.0.0.tgz +0 -0
- package/components/tryghost-member-analytics-service-0.0.0.tgz +0 -0
- package/components/tryghost-member-events-0.0.0.tgz +0 -0
- package/components/tryghost-members-analytics-ingress-0.0.0.tgz +0 -0
- package/components/tryghost-members-api-0.0.0.tgz +0 -0
- package/components/tryghost-members-csv-0.0.0.tgz +0 -0
- package/components/tryghost-members-events-service-0.0.0.tgz +0 -0
- package/components/tryghost-members-importer-0.0.0.tgz +0 -0
- package/components/tryghost-members-offers-0.0.0.tgz +0 -0
- package/components/tryghost-members-payments-0.0.0.tgz +0 -0
- package/components/tryghost-members-ssr-0.0.0.tgz +0 -0
- package/components/tryghost-members-stripe-service-0.0.0.tgz +0 -0
- package/components/tryghost-minifier-0.0.0.tgz +0 -0
- package/components/tryghost-mw-api-version-mismatch-0.0.0.tgz +0 -0
- package/components/tryghost-mw-error-handler-0.0.0.tgz +0 -0
- package/components/tryghost-mw-session-from-token-0.0.0.tgz +0 -0
- package/components/tryghost-mw-update-user-last-seen-0.0.0.tgz +0 -0
- package/components/tryghost-mw-vhost-0.0.0.tgz +0 -0
- package/components/tryghost-package-json-0.0.0.tgz +0 -0
- package/components/tryghost-security-0.0.0.tgz +0 -0
- package/components/tryghost-session-service-0.0.0.tgz +0 -0
- package/components/tryghost-settings-path-manager-0.0.0.tgz +0 -0
- package/components/tryghost-update-check-service-0.0.0.tgz +0 -0
- package/components/tryghost-verification-trigger-0.0.0.tgz +0 -0
- package/components/tryghost-version-notifications-data-service-0.0.0.tgz +0 -0
- package/content/themes/casper/assets/built/global.css +1 -1
- package/content/themes/casper/assets/built/global.css.map +1 -1
- package/content/themes/casper/assets/built/screen.css +1 -1
- package/content/themes/casper/assets/built/screen.css.map +1 -1
- package/content/themes/casper/assets/css/screen.css +9 -1
- package/content/themes/casper/gulpfile.js +1 -1
- package/content/themes/casper/package.json +9 -9
- package/content/themes/casper/yarn.lock +1154 -1249
- package/core/boot.js +6 -1
- package/core/built/assets/{chunk.3.dc389a0f93cb5fabd695.js → chunk.3.33097bb5eb150719bdd2.js} +19 -19
- package/core/built/assets/fonts/Inter.ttf +0 -0
- package/core/built/assets/ghost-dark-1bdd57aba1fa4a23388121740454dab2.css +1 -0
- package/core/built/assets/ghost.min-8f5c061e0892b93adecc2b9e37ad2f3a.css +1 -0
- package/core/built/assets/{ghost.min-36b64813b14c45075770658269d4b478.js → ghost.min-ff9ba089fd81cb40831f4b62e63a2ca9.js} +3015 -2874
- package/core/built/assets/icons/event-comment.svg +3 -0
- package/core/built/assets/{vendor.min-be0129c9c6897c9f10425e2402881d77.js → vendor.min-3dd40d3052381526f38fd290d13baa47.js} +2394 -924
- package/core/frontend/helpers/comments.js +39 -14
- package/core/frontend/helpers/ghost_head.js +22 -4
- package/core/frontend/helpers/img_url.js +67 -6
- package/core/frontend/utils/frontend-apps.js +33 -0
- package/core/frontend/web/middleware/handle-image-sizes.js +7 -11
- package/core/server/api/endpoints/{comments-comments.js → comments-members.js} +24 -43
- package/core/server/api/endpoints/index.js +2 -6
- package/core/server/api/endpoints/utils/serializers/output/config.js +2 -1
- package/core/server/api/endpoints/utils/serializers/output/mappers/activity-feed-events.js +17 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +18 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/index.js +1 -0
- package/core/server/api/endpoints/utils/serializers/output/mappers/posts.js +11 -0
- package/core/server/api/endpoints/utils/serializers/output/members.js +12 -1
- package/core/server/api/endpoints/utils/serializers/output/utils/clean.js +4 -0
- package/core/server/data/exporter/table-lists.js +2 -1
- package/core/server/data/migrations/versions/5.3/2022-07-06-09-17-add-ghost-explore-integration.js +0 -1
- package/core/server/data/migrations/versions/5.3/2022-07-06-09-26-add-ghost-explore-integration-api-key.js +0 -1
- package/core/server/data/migrations/versions/5.5/2022-07-18-14-29-add-comment-reporting-permissions.js +10 -0
- package/core/server/data/migrations/versions/5.5/2022-07-18-14-31-drop-reports-reason.js +3 -0
- package/core/server/data/migrations/versions/5.5/2022-07-18-14-32-drop-nullable-member-id-from-likes.js +4 -0
- package/core/server/data/migrations/versions/5.5/2022-07-18-14-33-fix-comments-on-delete-foreign-keys.js +119 -0
- package/core/server/data/migrations/versions/5.5/2022-07-21-08-56-add-jobs-table.js +11 -0
- package/core/server/data/migrations/versions/5.6/2022-07-27-13-40-change-explore-type.js +24 -0
- package/core/server/data/schema/commands.js +7 -2
- package/core/server/data/schema/fixtures/fixtures.json +6 -1
- package/core/server/data/schema/schema.js +12 -4
- package/core/server/ghost-server.js +0 -22
- package/core/server/models/comment-report.js +34 -0
- package/core/server/models/comment.js +8 -7
- package/core/server/models/job.js +9 -0
- package/core/server/services/bulk-email/bulk-email-processor.js +6 -0
- package/core/server/services/comments/controller.js +82 -0
- package/core/server/services/comments/email-templates/new-comment-reply.hbs +2 -2
- package/core/server/services/comments/email-templates/new-comment-reply.txt.js +7 -8
- package/core/server/services/comments/email-templates/new-comment.hbs +2 -2
- package/core/server/services/comments/email-templates/new-comment.txt.js +7 -6
- package/core/server/services/comments/email-templates/report.hbs +199 -0
- package/core/server/services/comments/email-templates/report.txt.js +16 -0
- package/core/server/services/comments/emails.js +57 -1
- package/core/server/services/comments/index.js +6 -1
- package/core/server/services/comments/service.js +291 -9
- package/core/server/services/jobs/job-service.js +24 -1
- package/core/server/services/mail/GhostMailer.js +1 -0
- package/core/server/services/mega/email-preview.js +5 -1
- package/core/server/services/mega/mega.js +2 -4
- package/core/server/services/mega/post-email-serializer.js +97 -2
- package/core/server/services/mega/segment-parser.js +10 -1
- package/core/server/services/members/api.js +2 -1
- package/core/server/services/members/service.js +9 -4
- package/core/server/services/public-config/config.js +2 -1
- package/core/server/services/stripe/service.js +9 -1
- package/core/server/web/admin/views/default-prod.html +4 -4
- package/core/server/web/admin/views/default.html +4 -4
- package/core/server/web/api/testmode/jobs/graceful-job.js +2 -2
- package/core/server/web/api/testmode/routes.js +14 -0
- package/core/server/web/comments/routes.js +10 -8
- package/core/shared/config/defaults.json +12 -7
- package/core/shared/config/env/config.testing.json +3 -2
- package/core/shared/labs.js +5 -2
- package/package.json +92 -59
- package/yarn.lock +1821 -2011
- package/core/built/assets/ghost-dark-739c1f5546bd048eeeb253965ef36712.css +0 -1
- package/core/built/assets/ghost.min-5211776b9497f36fac8c9e5f2584cbcc.css +0 -1
|
@@ -1,23 +1,47 @@
|
|
|
1
1
|
const {SafeString} = require('../services/handlebars');
|
|
2
|
-
const {
|
|
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.
|
|
15
|
-
colorScheme = options.hash.
|
|
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.
|
|
31
|
+
let avatarSaturation = parseInt(options.hash.saturation);
|
|
19
32
|
if (isNaN(avatarSaturation)) {
|
|
20
|
-
avatarSaturation =
|
|
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':
|
|
68
|
+
'app-version': appVersion,
|
|
69
|
+
'comments-enabled': commentsEnabled
|
|
41
70
|
};
|
|
42
71
|
|
|
43
|
-
|
|
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="${
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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),
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|
|
@@ -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) {
|
|
@@ -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',
|
|
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')) {
|
package/core/server/data/migrations/versions/5.3/2022-07-06-09-17-add-ghost-explore-integration.js
CHANGED
|
@@ -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();
|
|
@@ -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});
|