ghost
Version:
The professional publishing platform
276 lines (237 loc) • 8.55 kB
JavaScript
const _ = require('lodash');
const errors = require('@tryghost/errors');
/**
* @typedef {import('@tryghost/api-framework').Frame} Frame
*/
const {MethodNotAllowedError} = require('@tryghost/errors');
const tpl = require('@tryghost/tpl');
const messages = {
cannotDestroyComments: 'You cannot destroy comments.',
memberNotFound: 'Unable to find member'
};
module.exports = class CommentsController {
/**
* @param {import('./CommentsService')} service
* @param {import('./CommentsStatsService')} stats
*/
constructor(service, stats) {
this.service = service;
this.stats = stats;
}
async #setImpersonationContext(options) {
if (options.impersonate_member_uuid) {
options.context = options.context || {};
options.context.member = options.context.member || {};
options.context.member.id = await this.service.getMemberIdByUUID(options.impersonate_member_uuid);
}
}
#checkMember(frame) {
if (!frame.options?.context?.member?.id) {
throw new errors.UnauthorizedError({
message: tpl(messages.memberNotFound)
});
}
}
/**
* @param {Frame} frame
*/
async browse(frame) {
if (frame.options.post_id) {
if (frame.options.filter) {
frame.options.mongoTransformer = function (query) {
return {
$and: [
{
post_id: frame.options.post_id
},
query
]
};
};
} else {
frame.options.filter = `post_id:${frame.options.post_id}`;
}
}
return await this.service.getComments(frame.options);
}
async adminBrowse(frame) {
if (frame.options.post_id) {
if (frame.options.filter) {
frame.options.mongoTransformer = function (query) {
return {
$and: [
{
post_id: frame.options.post_id
},
query
]
};
};
} else {
frame.options.filter = `post_id:${frame.options.post_id}`;
}
}
frame.options.isAdmin = true;
// Admin routes in Comments-UI lack member context due to cross-domain constraints (CORS), which prevents
// credentials from being passed. This causes issues like the inability to determine if a
// logged-in admin (acting on behalf of a member) has already liked a comment.
// To resolve this, we retrieve the `impersonate_member_uuid` from the request params and
// explicitly set it in the context options as the acting member's ID.
// Note: This approach is applied to several admin routes where member context is required.
await this.#setImpersonationContext(frame.options);
return await this.service.getAdminComments(frame.options);
}
/**
* @param {Frame} frame
*/
async replies(frame) {
return this.service.getReplies(frame.options.id, _.omit(frame.options, 'id'));
}
/**
* @param {Frame} frame
*/
async adminReplies(frame) {
frame.options.isAdmin = true;
frame.options.order = 'created_at asc'; // we always want to load replies from oldest to newest
await this.#setImpersonationContext(frame.options);
return this.service.getReplies(frame.options.id, _.omit(frame.options, 'id'));
}
/**
* @param {Frame} frame
*/
async read(frame) {
await this.#setImpersonationContext(frame.options);
return await this.service.getCommentByID(frame.data.id, frame.options);
}
/**
* @param {Frame} frame
*/
async edit(frame) {
this.#checkMember(frame);
let result;
if (frame.data.comments[0].status === 'deleted') {
result = await this.service.deleteComment(
frame.options.id,
frame?.options?.context?.member?.id,
frame.options
);
} else {
result = await this.service.editCommentContent(
frame.options.id,
frame?.options?.context?.member?.id,
frame.data.comments[0].html,
frame.options
);
}
if (result) {
const postId = result.get('post_id');
const parentId = result.get('parent_id');
const pathsToInvalidate = [
postId ? `/api/members/comments/post/${postId}/` : null,
parentId ? `/api/members/comments/${parentId}/replies/` : null
].filter(path => path !== null);
frame.setHeader('X-Cache-Invalidate', pathsToInvalidate.join(', '));
}
return result;
}
/**
* @param {Frame} frame
*/
async add(frame) {
this.#checkMember(frame);
const data = frame.data.comments[0];
let result;
if (data.parent_id) {
result = await this.service.replyToComment(
data.parent_id,
data.in_reply_to_id,
frame.options.context.member.id,
data.html,
frame.options
);
} else {
result = await this.service.commentOnPost(
data.post_id,
frame.options.context.member.id,
data.html,
frame.options
);
}
if (result) {
const postId = result.get('post_id');
const parentId = result.get('parent_id');
const pathsToInvalidate = [
postId ? `/api/members/comments/post/${postId}/` : null,
parentId ? `/api/members/comments/${parentId}/replies/` : null
].filter(path => path !== null);
frame.setHeader('X-Cache-Invalidate', pathsToInvalidate.join(', '));
}
return result;
}
async destroy() {
throw new MethodNotAllowedError({
message: tpl(messages.cannotDestroyComments)
});
}
async count(frame) {
if (!frame?.options?.ids) {
return await this.stats.getAllCounts();
}
const ids = frame?.options?.ids.split(',');
return await this.stats.getCountsByPost(ids);
}
/**
* @param {Frame} frame
*/
async like(frame) {
this.#checkMember(frame);
const result = await this.service.likeComment(
frame.options.id,
frame.options?.context?.member,
frame.options
);
const comment = await this.service.getCommentByID(frame.options.id);
if (comment) {
const postId = comment.get('post_id');
const parentId = comment.get('parent_id');
const pathsToInvalidate = [
postId ? `/api/members/comments/post/${postId}/` : null,
parentId ? `/api/members/comments/${parentId}/replies/` : null
].filter(path => path !== null);
frame.setHeader('X-Cache-Invalidate', pathsToInvalidate.join(', '));
}
return result;
}
/**
* @param {Frame} frame
*/
async unlike(frame) {
this.#checkMember(frame);
const result = await this.service.unlikeComment(
frame.options.id,
frame.options?.context?.member,
frame.options
);
const comment = await this.service.getCommentByID(frame.options.id);
if (comment) {
const postId = comment.get('post_id');
const parentId = comment.get('parent_id');
const pathsToInvalidate = [
postId ? `/api/members/comments/post/${postId}/` : null,
parentId ? `/api/members/comments/${parentId}/replies/` : null
].filter(path => path !== null);
frame.setHeader('X-Cache-Invalidate', pathsToInvalidate.join(', '));
}
return result;
}
/**
* @param {Frame} frame
*/
async report(frame) {
this.#checkMember(frame);
return await this.service.reportComment(
frame.options.id,
frame.options?.context?.member
);
}
};