ghost 5.128.1 → 5.129.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/components/tryghost-i18n-5.129.0.tgz +0 -0
- package/core/built/admin/assets/admin-x-activitypub/admin-x-activitypub.js +2 -2
- package/core/built/admin/assets/admin-x-activitypub/{index-CAyELgjK.mjs → index-Ou5BZ-qx.mjs} +7 -6
- package/core/built/admin/assets/admin-x-activitypub/{index-Dw9BlNtX.mjs → index-q-fcRpYq.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{CodeEditorView-BGEvx0X_.mjs → CodeEditorView-BdOUrhjH.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-D1zofiZM.mjs → index-BIcU5l8u.mjs} +105 -105
- package/core/built/admin/assets/admin-x-settings/{index-DbJiQD3s.mjs → index-CyYRbOBi.mjs} +5686 -5514
- package/core/built/admin/assets/admin-x-settings/{modals-Bm4XSg2N.mjs → modals-r5LXiWii.mjs} +8221 -8196
- package/core/built/admin/assets/{chunk.524.7d728f721968c0949b05.js → chunk.524.59a57ec279b535711b3f.js} +6 -6
- package/core/built/admin/assets/{chunk.582.44eba5152011d41c041f.js → chunk.582.324905279ae25a297c8e.js} +8 -8
- package/core/built/admin/assets/{ghost-bf31eeb6c36df41ac2d90d3135b7aefa.js → ghost-29e715d82674921e3f545fd4778b09ac.js} +172 -170
- package/core/built/admin/assets/{ghost-673f1a892cb83c3441c2ad448d4b634d.css → ghost-415f8e3c36dbe0e09f87608628da382d.css} +1 -1
- package/core/built/admin/assets/{ghost-dark-1e5d92b26a08ecf6f98999c53102f221.css → ghost-dark-2043bca95512f1fa2ff0bea2f8a632b0.css} +1 -1
- package/core/built/admin/assets/posts/posts.js +63002 -62842
- package/core/built/admin/assets/stats/stats.js +32122 -31899
- package/core/built/admin/assets/{vendor-9631e49a9ba32620e8fdc561f1380348.js → vendor-c89102f24c3d9502e9db741509767580.js} +20 -18
- package/core/built/admin/index.html +5 -5
- package/core/frontend/helpers/ghost_head.js +4 -2
- package/core/frontend/public/ghost-stats.min.js +1 -1
- package/core/frontend/src/ghost-stats/ghost-stats.js +9 -5
- package/core/server/api/endpoints/comments.js +86 -0
- package/core/server/api/endpoints/index.js +5 -1
- package/core/server/api/endpoints/search-index-public.js +26 -4
- package/core/server/api/endpoints/search-index.js +84 -0
- package/core/server/api/endpoints/utils/serializers/output/search-index.js +79 -18
- package/core/server/data/schema/schema.js +1 -1
- package/core/server/data/seeders/importers/TagsImporter.js +2 -2
- package/core/server/services/comments/CommentsService.js +21 -7
- package/core/server/services/media-inliner/ExternalMediaInliner.js +21 -2
- package/core/server/services/permissions/can-this.js +10 -3
- package/core/server/services/stats/PostsStatsService.js +52 -0
- package/core/server/web/api/endpoints/admin/middleware.js +5 -3
- package/core/server/web/api/endpoints/admin/routes.js +7 -0
- package/core/server/web/api/endpoints/content/routes.js +3 -3
- package/core/shared/labs.js +1 -2
- package/core/shared/settings-cache/CacheManager.js +1 -0
- package/package.json +6 -5
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +121 -90
- package/components/tryghost-i18n-5.128.1.tgz +0 -0
- package/core/server/services/search-index/SearchIndexService.js +0 -43
- package/core/server/services/search-index/index.js +0 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const models = require('../../models');
|
|
2
2
|
const commentsService = require('../../services/comments');
|
|
3
|
+
const errors = require('@tryghost/errors');
|
|
3
4
|
function handleCacheHeaders(model, frame) {
|
|
4
5
|
if (model) {
|
|
5
6
|
const postId = model.get('post_id');
|
|
@@ -12,6 +13,33 @@ function handleCacheHeaders(model, frame) {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
function validateCommentData(data) {
|
|
17
|
+
if (!data.post_id && !data.parent_id) {
|
|
18
|
+
throw new errors.ValidationError({
|
|
19
|
+
message: 'Either post_id (for top-level comments) or parent_id (for replies) must be provided'
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function validateCreatedAt(createdAt) {
|
|
25
|
+
if (!createdAt) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Only accept string or Date objects, reject other types like numbers
|
|
30
|
+
if (typeof createdAt !== 'string' && !(createdAt instanceof Date)) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const date = new Date(createdAt);
|
|
35
|
+
// Check if the date is valid and not in the future
|
|
36
|
+
if (!isNaN(date.getTime()) && date <= new Date()) {
|
|
37
|
+
return date;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
/** @type {import('@tryghost/api-framework').Controller} */
|
|
16
44
|
const controller = {
|
|
17
45
|
docName: 'comments',
|
|
@@ -67,6 +95,64 @@ const controller = {
|
|
|
67
95
|
permissions: true,
|
|
68
96
|
async query(frame) {
|
|
69
97
|
const result = await commentsService.controller.adminBrowse(frame);
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
add: {
|
|
102
|
+
statusCode: 201,
|
|
103
|
+
headers: {
|
|
104
|
+
cacheInvalidate: false
|
|
105
|
+
},
|
|
106
|
+
options: [
|
|
107
|
+
'include'
|
|
108
|
+
],
|
|
109
|
+
validation: {
|
|
110
|
+
options: {
|
|
111
|
+
include: {
|
|
112
|
+
values: ['post', 'member', 'replies', 'replies.member']
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
data: {
|
|
116
|
+
member_id: {
|
|
117
|
+
required: true
|
|
118
|
+
},
|
|
119
|
+
html: {
|
|
120
|
+
required: true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
permissions: true,
|
|
125
|
+
async query(frame) {
|
|
126
|
+
const data = frame.data.comments[0];
|
|
127
|
+
|
|
128
|
+
validateCommentData(data);
|
|
129
|
+
const validatedCreatedAt = validateCreatedAt(data.created_at);
|
|
130
|
+
|
|
131
|
+
// Set internal context to prevent notifications
|
|
132
|
+
if (!frame.options.context) {
|
|
133
|
+
frame.options.context = {};
|
|
134
|
+
}
|
|
135
|
+
frame.options.context.internal = true;
|
|
136
|
+
|
|
137
|
+
const result = data.parent_id
|
|
138
|
+
? await commentsService.api.replyToComment(
|
|
139
|
+
data.parent_id,
|
|
140
|
+
data.in_reply_to_id,
|
|
141
|
+
data.member_id,
|
|
142
|
+
data.html,
|
|
143
|
+
frame.options,
|
|
144
|
+
validatedCreatedAt
|
|
145
|
+
)
|
|
146
|
+
: await commentsService.api.commentOnPost(
|
|
147
|
+
data.post_id,
|
|
148
|
+
data.member_id,
|
|
149
|
+
data.html,
|
|
150
|
+
frame.options,
|
|
151
|
+
validatedCreatedAt
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
handleCacheHeaders(result, frame);
|
|
155
|
+
|
|
70
156
|
return result;
|
|
71
157
|
}
|
|
72
158
|
}
|
|
@@ -217,6 +217,10 @@ module.exports = {
|
|
|
217
217
|
return apiFramework.pipeline(require('./incoming-recommendations'), localUtils);
|
|
218
218
|
},
|
|
219
219
|
|
|
220
|
+
get searchIndex() {
|
|
221
|
+
return apiFramework.pipeline(require('./search-index'), localUtils);
|
|
222
|
+
},
|
|
223
|
+
|
|
220
224
|
/**
|
|
221
225
|
* Content API Controllers
|
|
222
226
|
*
|
|
@@ -269,7 +273,7 @@ module.exports = {
|
|
|
269
273
|
return apiFramework.pipeline(require('./recommendations-public'), localUtils, 'content');
|
|
270
274
|
},
|
|
271
275
|
|
|
272
|
-
get
|
|
276
|
+
get searchIndexPublic() {
|
|
273
277
|
return apiFramework.pipeline(require('./search-index-public'), localUtils, 'content');
|
|
274
278
|
}
|
|
275
279
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
const
|
|
1
|
+
const models = require('../../models');
|
|
2
|
+
const getPostServiceInstance = require('../../services/posts/posts-service');
|
|
3
|
+
const postsService = getPostServiceInstance();
|
|
2
4
|
|
|
3
5
|
/** @type {import('@tryghost/api-framework').Controller} */
|
|
4
6
|
const controller = {
|
|
@@ -9,7 +11,14 @@ const controller = {
|
|
|
9
11
|
},
|
|
10
12
|
permissions: true,
|
|
11
13
|
query() {
|
|
12
|
-
|
|
14
|
+
const options = {
|
|
15
|
+
filter: 'type:post',
|
|
16
|
+
limit: '10000',
|
|
17
|
+
order: 'updated_at DESC',
|
|
18
|
+
columns: ['id', 'slug', 'title', 'excerpt', 'url', 'updated_at', 'visibility']
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return postsService.browsePosts(options);
|
|
13
22
|
}
|
|
14
23
|
},
|
|
15
24
|
fetchAuthors: {
|
|
@@ -18,7 +27,13 @@ const controller = {
|
|
|
18
27
|
},
|
|
19
28
|
permissions: true,
|
|
20
29
|
query() {
|
|
21
|
-
|
|
30
|
+
const options = {
|
|
31
|
+
limit: '10000',
|
|
32
|
+
order: 'updated_at DESC',
|
|
33
|
+
columns: ['id', 'slug', 'name', 'url', 'profile_image']
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return models.Author.findPage(options);
|
|
22
37
|
}
|
|
23
38
|
},
|
|
24
39
|
fetchTags: {
|
|
@@ -27,7 +42,14 @@ const controller = {
|
|
|
27
42
|
},
|
|
28
43
|
permissions: true,
|
|
29
44
|
query() {
|
|
30
|
-
|
|
45
|
+
const options = {
|
|
46
|
+
limit: '10000',
|
|
47
|
+
order: 'updated_at DESC',
|
|
48
|
+
columns: ['id', 'slug', 'name', 'url'],
|
|
49
|
+
filter: 'visibility:public'
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return models.Tag.findPage(options);
|
|
31
53
|
}
|
|
32
54
|
}
|
|
33
55
|
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const models = require('../../models');
|
|
2
|
+
const getPostServiceInstance = require('../../services/posts/posts-service');
|
|
3
|
+
const postsService = getPostServiceInstance();
|
|
4
|
+
|
|
5
|
+
/** @type {import('@tryghost/api-framework').Controller} */
|
|
6
|
+
const controller = {
|
|
7
|
+
docName: 'search_index',
|
|
8
|
+
fetchPosts: {
|
|
9
|
+
headers: {
|
|
10
|
+
cacheInvalidate: false
|
|
11
|
+
},
|
|
12
|
+
permissions: {
|
|
13
|
+
docName: 'posts',
|
|
14
|
+
method: 'browse'
|
|
15
|
+
},
|
|
16
|
+
query() {
|
|
17
|
+
const options = {
|
|
18
|
+
filter: 'type:post',
|
|
19
|
+
limit: '10000',
|
|
20
|
+
order: 'updated_at DESC',
|
|
21
|
+
columns: ['id', 'url', 'title', 'status', 'published_at', 'visibility']
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return postsService.browsePosts(options);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
fetchPages: {
|
|
28
|
+
headers: {
|
|
29
|
+
cacheInvalidate: false
|
|
30
|
+
},
|
|
31
|
+
permissions: {
|
|
32
|
+
docName: 'posts',
|
|
33
|
+
method: 'browse'
|
|
34
|
+
},
|
|
35
|
+
query() {
|
|
36
|
+
const options = {
|
|
37
|
+
filter: 'type:page',
|
|
38
|
+
limit: '10000',
|
|
39
|
+
order: 'updated_at DESC',
|
|
40
|
+
columns: ['id', 'url', 'title', 'status', 'published_at', 'visibility']
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return postsService.browsePosts(options);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
fetchTags: {
|
|
47
|
+
headers: {
|
|
48
|
+
cacheInvalidate: false
|
|
49
|
+
},
|
|
50
|
+
permissions: {
|
|
51
|
+
docName: 'tags',
|
|
52
|
+
method: 'browse'
|
|
53
|
+
},
|
|
54
|
+
query() {
|
|
55
|
+
const options = {
|
|
56
|
+
limit: '10000',
|
|
57
|
+
order: 'updated_at DESC',
|
|
58
|
+
columns: ['id', 'slug', 'name', 'url']
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return models.Tag.findPage(options);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
fetchUsers: {
|
|
65
|
+
headers: {
|
|
66
|
+
cacheInvalidate: false
|
|
67
|
+
},
|
|
68
|
+
permissions: {
|
|
69
|
+
docName: 'users',
|
|
70
|
+
method: 'browse'
|
|
71
|
+
},
|
|
72
|
+
query() {
|
|
73
|
+
const options = {
|
|
74
|
+
limit: '10000',
|
|
75
|
+
order: 'updated_at DESC',
|
|
76
|
+
columns: ['id', 'slug', 'url', 'name', 'profile_image']
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return models.User.findPage(options);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
module.exports = controller;
|
|
@@ -1,33 +1,93 @@
|
|
|
1
1
|
const debug = require('@tryghost/debug')('api:endpoints:utils:serializers:output:search-index');
|
|
2
|
-
const mappers = require('./mappers');
|
|
3
2
|
const _ = require('lodash');
|
|
4
3
|
|
|
4
|
+
const mappers = require('./mappers');
|
|
5
|
+
const utils = require('../../index');
|
|
6
|
+
|
|
5
7
|
module.exports = {
|
|
6
8
|
async fetchPosts(models, apiConfig, frame) {
|
|
7
9
|
debug('fetchPosts');
|
|
8
10
|
|
|
9
11
|
let posts = [];
|
|
12
|
+
let keys = [];
|
|
13
|
+
|
|
14
|
+
if (utils.isContentAPI(frame)) {
|
|
15
|
+
keys.push(
|
|
16
|
+
'id',
|
|
17
|
+
'slug',
|
|
18
|
+
'title',
|
|
19
|
+
'excerpt',
|
|
20
|
+
'url',
|
|
21
|
+
'updated_at',
|
|
22
|
+
'visibility'
|
|
23
|
+
);
|
|
24
|
+
} else {
|
|
25
|
+
keys.push(
|
|
26
|
+
'id',
|
|
27
|
+
'url',
|
|
28
|
+
'title',
|
|
29
|
+
'status',
|
|
30
|
+
'published_at',
|
|
31
|
+
'visibility'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (let model of models.data) {
|
|
36
|
+
let post = await mappers.posts(model, frame, {});
|
|
37
|
+
post = _.pick(post, keys);
|
|
38
|
+
posts.push(post);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
frame.response = {
|
|
42
|
+
posts
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
async fetchPages(models, apiConfig, frame) {
|
|
47
|
+
debug('fetchPages');
|
|
48
|
+
|
|
49
|
+
let pages = [];
|
|
10
50
|
|
|
11
51
|
const keys = [
|
|
12
52
|
'id',
|
|
13
|
-
'slug',
|
|
14
|
-
'title',
|
|
15
|
-
'excerpt',
|
|
16
53
|
'url',
|
|
17
|
-
'
|
|
18
|
-
'
|
|
54
|
+
'title',
|
|
55
|
+
'status',
|
|
19
56
|
'published_at',
|
|
20
57
|
'visibility'
|
|
21
58
|
];
|
|
22
59
|
|
|
23
60
|
for (let model of models.data) {
|
|
24
|
-
let
|
|
25
|
-
|
|
26
|
-
|
|
61
|
+
let page = await mappers.pages(model, frame, {});
|
|
62
|
+
page = _.pick(page, keys);
|
|
63
|
+
pages.push(page);
|
|
27
64
|
}
|
|
28
65
|
|
|
29
66
|
frame.response = {
|
|
30
|
-
|
|
67
|
+
pages
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async fetchTags(models, apiConfig, frame) {
|
|
72
|
+
debug('fetchTags');
|
|
73
|
+
|
|
74
|
+
let tags = [];
|
|
75
|
+
|
|
76
|
+
const keys = [
|
|
77
|
+
'id',
|
|
78
|
+
'slug',
|
|
79
|
+
'name',
|
|
80
|
+
'url'
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
for (let model of models.data) {
|
|
84
|
+
let tag = await mappers.tags(model, frame);
|
|
85
|
+
tag = _.pick(tag, keys);
|
|
86
|
+
tags.push(tag);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
frame.response = {
|
|
90
|
+
tags
|
|
31
91
|
};
|
|
32
92
|
},
|
|
33
93
|
|
|
@@ -55,26 +115,27 @@ module.exports = {
|
|
|
55
115
|
};
|
|
56
116
|
},
|
|
57
117
|
|
|
58
|
-
async
|
|
59
|
-
debug('
|
|
118
|
+
async fetchUsers(models, apiConfig, frame) {
|
|
119
|
+
debug('fetchUsers');
|
|
60
120
|
|
|
61
|
-
let
|
|
121
|
+
let users = [];
|
|
62
122
|
|
|
63
123
|
const keys = [
|
|
64
124
|
'id',
|
|
65
125
|
'slug',
|
|
66
126
|
'name',
|
|
67
|
-
'url'
|
|
127
|
+
'url',
|
|
128
|
+
'profile_image'
|
|
68
129
|
];
|
|
69
130
|
|
|
70
131
|
for (let model of models.data) {
|
|
71
|
-
let
|
|
72
|
-
|
|
73
|
-
|
|
132
|
+
let user = await mappers.users(model, frame);
|
|
133
|
+
user = _.pick(user, keys);
|
|
134
|
+
users.push(user);
|
|
74
135
|
}
|
|
75
136
|
|
|
76
137
|
frame.response = {
|
|
77
|
-
|
|
138
|
+
users
|
|
78
139
|
};
|
|
79
140
|
}
|
|
80
141
|
};
|
|
@@ -135,7 +135,7 @@ module.exports = {
|
|
|
135
135
|
email: {type: 'string', maxlength: 191, nullable: false, unique: true, validations: {isEmail: true}},
|
|
136
136
|
profile_image: {type: 'string', maxlength: 2000, nullable: true},
|
|
137
137
|
cover_image: {type: 'string', maxlength: 2000, nullable: true},
|
|
138
|
-
bio: {type: 'text', maxlength: 65535, nullable: true, validations: {isLength: {max:
|
|
138
|
+
bio: {type: 'text', maxlength: 65535, nullable: true, validations: {isLength: {max: 250}}},
|
|
139
139
|
website: {type: 'string', maxlength: 2000, nullable: true, validations: {isEmptyOrURL: true}},
|
|
140
140
|
location: {type: 'text', maxlength: 65535, nullable: true, validations: {isLength: {max: 150}}},
|
|
141
141
|
facebook: {type: 'string', maxlength: 2000, nullable: true},
|
|
@@ -21,7 +21,7 @@ class TagsImporter extends TableImporter {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
generate() {
|
|
24
|
-
let name = `${faker.color.human()} ${faker.name.jobType()}`;
|
|
24
|
+
let name = `${faker.color.human()} ${faker.name.jobType()} ${faker.random.numeric(3)}`;
|
|
25
25
|
name = `${name[0].toUpperCase()}${name.slice(1)}`;
|
|
26
26
|
const threeYearsAgo = new Date();
|
|
27
27
|
threeYearsAgo.setFullYear(threeYearsAgo.getFullYear() - 3);
|
|
@@ -30,7 +30,7 @@ class TagsImporter extends TableImporter {
|
|
|
30
30
|
return {
|
|
31
31
|
id: this.fastFakeObjectId(),
|
|
32
32
|
name: name,
|
|
33
|
-
slug:
|
|
33
|
+
slug: slugify(name),
|
|
34
34
|
description: faker.lorem.sentence(),
|
|
35
35
|
created_at: dateToDatabaseString(faker.date.between(threeYearsAgo, twoYearsAgo)),
|
|
36
36
|
created_by: this.users[faker.datatype.number(this.users.length - 1)].id
|
|
@@ -204,7 +204,7 @@ class CommentsService {
|
|
|
204
204
|
|
|
205
205
|
if (!model) {
|
|
206
206
|
throw new errors.NotFoundError({
|
|
207
|
-
|
|
207
|
+
message: tpl(messages.commentNotFound)
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
210
|
|
|
@@ -216,8 +216,9 @@ class CommentsService {
|
|
|
216
216
|
* @param {string} member - The ID of the Member to comment as
|
|
217
217
|
* @param {string} comment - The HTML content of the Comment
|
|
218
218
|
* @param {any} options
|
|
219
|
+
* @param {Date} [createdAt] - Optional custom created_at timestamp
|
|
219
220
|
*/
|
|
220
|
-
async commentOnPost(post, member, comment, options) {
|
|
221
|
+
async commentOnPost(post, member, comment, options, createdAt) {
|
|
221
222
|
this.checkEnabled();
|
|
222
223
|
const memberModel = await this.models.Member.findOne({
|
|
223
224
|
id: member
|
|
@@ -239,13 +240,19 @@ class CommentsService {
|
|
|
239
240
|
|
|
240
241
|
this.checkPostAccess(postModel, memberModel);
|
|
241
242
|
|
|
242
|
-
const
|
|
243
|
+
const commentData = {
|
|
243
244
|
post_id: post,
|
|
244
245
|
member_id: member,
|
|
245
246
|
parent_id: null,
|
|
246
247
|
html: comment,
|
|
247
248
|
status: 'published'
|
|
248
|
-
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (createdAt) {
|
|
252
|
+
commentData.created_at = createdAt;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const model = await this.models.Comment.add(commentData, options);
|
|
249
256
|
|
|
250
257
|
if (!options.context.internal) {
|
|
251
258
|
await this.sendNewCommentNotifications(model);
|
|
@@ -267,8 +274,9 @@ class CommentsService {
|
|
|
267
274
|
* @param {string} member - The ID of the Member to comment as
|
|
268
275
|
* @param {string} comment - The HTML content of the Comment
|
|
269
276
|
* @param {any} options
|
|
277
|
+
* @param {Date} [createdAt] - Optional custom created_at timestamp
|
|
270
278
|
*/
|
|
271
|
-
async replyToComment(parent, inReplyTo, member, comment, options) {
|
|
279
|
+
async replyToComment(parent, inReplyTo, member, comment, options, createdAt) {
|
|
272
280
|
this.checkEnabled();
|
|
273
281
|
const memberModel = await this.models.Member.findOne({
|
|
274
282
|
id: member
|
|
@@ -319,14 +327,20 @@ class CommentsService {
|
|
|
319
327
|
}
|
|
320
328
|
}
|
|
321
329
|
|
|
322
|
-
const
|
|
330
|
+
const commentData = {
|
|
323
331
|
post_id: parentComment.get('post_id'),
|
|
324
332
|
member_id: member,
|
|
325
333
|
parent_id: parentComment.id,
|
|
326
334
|
in_reply_to_id: inReplyToComment && inReplyToComment.get('id'),
|
|
327
335
|
html: comment,
|
|
328
336
|
status: 'published'
|
|
329
|
-
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (createdAt) {
|
|
340
|
+
commentData.created_at = createdAt;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const model = await this.models.Comment.add(commentData, options);
|
|
330
344
|
|
|
331
345
|
if (!options.context.internal) {
|
|
332
346
|
await this.sendNewCommentNotifications(model);
|
|
@@ -5,6 +5,7 @@ const errors = require('@tryghost/errors');
|
|
|
5
5
|
const logging = require('@tryghost/logging');
|
|
6
6
|
const string = require('@tryghost/string');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const convert = require('heic-convert');
|
|
8
9
|
|
|
9
10
|
class ExternalMediaInliner {
|
|
10
11
|
/** @type {object} */
|
|
@@ -76,11 +77,12 @@ class ExternalMediaInliner {
|
|
|
76
77
|
*/
|
|
77
78
|
async extractFileDataFromResponse(requestURL, response) {
|
|
78
79
|
let extension;
|
|
80
|
+
let body = response.body;
|
|
79
81
|
|
|
80
82
|
// Attempt to get the file extension from the file itself
|
|
81
83
|
// If that fails, or if `.ext` is undefined, get the extension from the file path in the catch
|
|
82
84
|
try {
|
|
83
|
-
const fileInfo = await FileType.fromBuffer(
|
|
85
|
+
const fileInfo = await FileType.fromBuffer(body);
|
|
84
86
|
extension = fileInfo.ext;
|
|
85
87
|
} catch {
|
|
86
88
|
const headers = response.headers;
|
|
@@ -89,6 +91,23 @@ class ExternalMediaInliner {
|
|
|
89
91
|
extension = mime.extension(contentType) || extensionFromPath;
|
|
90
92
|
}
|
|
91
93
|
|
|
94
|
+
// If the file is heic or heif, attempt to convert it to jpeg
|
|
95
|
+
try {
|
|
96
|
+
if (extension === 'heic' || extension === 'heif') {
|
|
97
|
+
body = await convert({
|
|
98
|
+
buffer: body,
|
|
99
|
+
format: 'JPEG'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
extension = 'jpg';
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logging.error(`Error converting file to JPEG: ${requestURL}`);
|
|
106
|
+
logging.error(new errors.DataImportError({
|
|
107
|
+
err: error
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
|
|
92
111
|
const removeExtRegExp = new RegExp(`.${extension}`, '');
|
|
93
112
|
const fileNameNoExt = path.parse(requestURL).base.replace(removeExtRegExp, '');
|
|
94
113
|
|
|
@@ -100,7 +119,7 @@ class ExternalMediaInliner {
|
|
|
100
119
|
}).slice(-248).replace(/^-|-$/, '');
|
|
101
120
|
|
|
102
121
|
return {
|
|
103
|
-
fileBuffer:
|
|
122
|
+
fileBuffer: body,
|
|
104
123
|
filename: `${fileName}.${extension}`,
|
|
105
124
|
extension: `.${extension}`
|
|
106
125
|
};
|
|
@@ -81,9 +81,16 @@ class CanThisResult {
|
|
|
81
81
|
// Check api key permissions if they were passed
|
|
82
82
|
hasApiKeyPermission = true;
|
|
83
83
|
if (!_.isNull(apiKeyPermissions)) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
if (loadedPermissions.user) {
|
|
85
|
+
// Staff API key scenario: both user and API key present
|
|
86
|
+
// Use USER permissions and ignore API key permissions
|
|
87
|
+
hasApiKeyPermission = true; // Allow API key check to pass
|
|
88
|
+
} else {
|
|
89
|
+
// Traditional API key scenario: API key only, no user
|
|
90
|
+
// Use API key permissions as before
|
|
91
|
+
hasUserPermission = true;
|
|
92
|
+
hasApiKeyPermission = _.some(apiKeyPermissions, checkPermission);
|
|
93
|
+
}
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
// Offer a chance for the TargetModel to override the results
|
|
@@ -1193,6 +1193,7 @@ class PostsStatsService {
|
|
|
1193
1193
|
'p.title',
|
|
1194
1194
|
'p.published_at',
|
|
1195
1195
|
'p.feature_image',
|
|
1196
|
+
'p.status',
|
|
1196
1197
|
'emails.email_count',
|
|
1197
1198
|
'emails.opened_count'
|
|
1198
1199
|
)
|
|
@@ -1203,6 +1204,29 @@ class PostsStatsService {
|
|
|
1203
1204
|
.orderBy('p.published_at', 'desc')
|
|
1204
1205
|
.limit(limit);
|
|
1205
1206
|
|
|
1207
|
+
// Get authors for all posts
|
|
1208
|
+
const postIds = posts.map(p => p.post_id);
|
|
1209
|
+
const authorsData = postIds.length > 0 ? await this.knex('posts_authors as pa')
|
|
1210
|
+
.select('pa.post_id', 'u.name', 'pa.sort_order')
|
|
1211
|
+
.leftJoin('users as u', 'u.id', 'pa.author_id')
|
|
1212
|
+
.whereIn('pa.post_id', postIds)
|
|
1213
|
+
.whereNotNull('u.name')
|
|
1214
|
+
.orderBy(['pa.post_id', 'pa.sort_order']) : [];
|
|
1215
|
+
|
|
1216
|
+
// Group authors by post_id
|
|
1217
|
+
const authorsByPost = {};
|
|
1218
|
+
authorsData.forEach((author) => {
|
|
1219
|
+
if (!authorsByPost[author.post_id]) {
|
|
1220
|
+
authorsByPost[author.post_id] = [];
|
|
1221
|
+
}
|
|
1222
|
+
authorsByPost[author.post_id].push(author.name);
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// Add authors to posts
|
|
1226
|
+
posts.forEach((post) => {
|
|
1227
|
+
post.authors = (authorsByPost[post.post_id] || []).join(', ');
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1206
1230
|
// Get member attribution counts and click counts for these posts
|
|
1207
1231
|
const [memberAttributionCounts, clickCounts] = await Promise.all([
|
|
1208
1232
|
this._getMemberAttributionCounts(posts.map(p => p.post_id), options),
|
|
@@ -1227,6 +1251,8 @@ class PostsStatsService {
|
|
|
1227
1251
|
title: post.title,
|
|
1228
1252
|
published_at: post.published_at,
|
|
1229
1253
|
feature_image: post.feature_image ? urlUtils.transformReadyToAbsolute(post.feature_image) : post.feature_image,
|
|
1254
|
+
status: post.status,
|
|
1255
|
+
authors: post.authors,
|
|
1230
1256
|
views: row.visits,
|
|
1231
1257
|
sent_count: post.email_count || null,
|
|
1232
1258
|
opened_count: post.opened_count || null,
|
|
@@ -1257,6 +1283,7 @@ class PostsStatsService {
|
|
|
1257
1283
|
'p.title',
|
|
1258
1284
|
'p.published_at',
|
|
1259
1285
|
'p.feature_image',
|
|
1286
|
+
'p.status',
|
|
1260
1287
|
'emails.email_count',
|
|
1261
1288
|
'emails.opened_count'
|
|
1262
1289
|
)
|
|
@@ -1268,6 +1295,29 @@ class PostsStatsService {
|
|
|
1268
1295
|
.orderBy('p.published_at', 'desc')
|
|
1269
1296
|
.limit(remainingCount);
|
|
1270
1297
|
|
|
1298
|
+
// Get authors for additional posts
|
|
1299
|
+
const additionalPostIds = additionalPosts.map(p => p.post_id);
|
|
1300
|
+
const additionalAuthorsData = additionalPostIds.length > 0 ? await this.knex('posts_authors as pa')
|
|
1301
|
+
.select('pa.post_id', 'u.name', 'pa.sort_order')
|
|
1302
|
+
.leftJoin('users as u', 'u.id', 'pa.author_id')
|
|
1303
|
+
.whereIn('pa.post_id', additionalPostIds)
|
|
1304
|
+
.whereNotNull('u.name')
|
|
1305
|
+
.orderBy(['pa.post_id', 'pa.sort_order']) : [];
|
|
1306
|
+
|
|
1307
|
+
// Group authors by post_id for additional posts
|
|
1308
|
+
const additionalAuthorsByPost = {};
|
|
1309
|
+
additionalAuthorsData.forEach((author) => {
|
|
1310
|
+
if (!additionalAuthorsByPost[author.post_id]) {
|
|
1311
|
+
additionalAuthorsByPost[author.post_id] = [];
|
|
1312
|
+
}
|
|
1313
|
+
additionalAuthorsByPost[author.post_id].push(author.name);
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1316
|
+
// Add authors to additional posts
|
|
1317
|
+
additionalPosts.forEach((post) => {
|
|
1318
|
+
post.authors = (additionalAuthorsByPost[post.post_id] || []).join(', ');
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1271
1321
|
// Get member attribution counts and click counts for additional posts
|
|
1272
1322
|
if (additionalPosts.length > 0) {
|
|
1273
1323
|
[additionalMemberAttributionCounts, additionalClickCounts] = await Promise.all([
|
|
@@ -1289,6 +1339,8 @@ class PostsStatsService {
|
|
|
1289
1339
|
title: post.title,
|
|
1290
1340
|
published_at: post.published_at,
|
|
1291
1341
|
feature_image: post.feature_image ? urlUtils.transformReadyToAbsolute(post.feature_image) : post.feature_image,
|
|
1342
|
+
status: post.status,
|
|
1343
|
+
authors: post.authors,
|
|
1292
1344
|
views: 0,
|
|
1293
1345
|
sent_count: post.email_count || null,
|
|
1294
1346
|
opened_count: post.opened_count || null,
|