diff --git a/apps/comments-ui/src/app-context.ts b/apps/comments-ui/src/app-context.ts index 5280e25c372..e19986f1d05 100644 --- a/apps/comments-ui/src/app-context.ts +++ b/apps/comments-ui/src/app-context.ts @@ -27,7 +27,7 @@ export type Comment = { member: Member | null, edited_at: string, created_at: string, - html: string + html: string | null } export type OpenCommentForm = { diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js index 22359d5aefa..d32b43ea1a1 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/output/mappers/comments.js @@ -48,25 +48,6 @@ const commentMapper = (model, frame) => { const isPublicRequest = utils.isMembersAPI(frame); - // Deleted comments are tombstones - just enough info to show "Reply to deleted comment" - if (jsonModel.status === 'deleted') { - const tombstone = { - id: jsonModel.id, - parent_id: jsonModel.parent_id || null, - status: 'deleted' - }; - // Include post relation if loaded - if (jsonModel.post) { - url.forPost(jsonModel.post.id, jsonModel.post, frame); - tombstone.post = _.pick(jsonModel.post, postFields); - } - // Include replies if present (for tree structure), recursively mapped - if (jsonModel.replies) { - tombstone.replies = jsonModel.replies.map(reply => commentMapper(reply, frame)); - } - return tombstone; - } - if (jsonModel.inReplyTo && (jsonModel.inReplyTo.status === 'published' || (!isPublicRequest && jsonModel.inReplyTo.status === 'hidden'))) { jsonModel.in_reply_to_snippet = htmlToPlaintext.commentSnippet(jsonModel.inReplyTo.html); } else if (jsonModel.inReplyTo && jsonModel.inReplyTo.status !== 'published') { @@ -115,6 +96,11 @@ const commentMapper = (model, frame) => { } } + // Deleted comments should never expose their content + if (jsonModel.status === 'deleted') { + response.html = null; + } + return response; }; diff --git a/ghost/core/core/server/models/comment.js b/ghost/core/core/server/models/comment.js index cb6503b0c7d..9dec629a57d 100644 --- a/ghost/core/core/server/models/comment.js +++ b/ghost/core/core/server/models/comment.js @@ -63,18 +63,24 @@ const Comment = ghostBookshelf.Model.extend({ // Called by our filtered-collection bookshelf plugin applyCustomQuery(options) { - const excludedCommentStatuses = options.isAdmin ? ['deleted'] : ['hidden', 'deleted']; + const excludedStatuses = options.isAdmin ? ['deleted'] : ['hidden', 'deleted']; this.query((qb) => { - qb.where(function () { - this.whereNotIn('comments.status', excludedCommentStatuses) - .orWhereExists(function () { - this.select(1) - .from('comments as replies') - .whereRaw('replies.parent_id = comments.id') - .whereNotIn('replies.status', excludedCommentStatuses); - }); - }); + if (options.browseAll) { + // Browse All: simply exclude statuses, no thread structure preservation + qb.whereNotIn('comments.status', excludedStatuses); + } else { + // Default: preserve thread structure by including deleted parents with replies + qb.where(function () { + this.whereNotIn('comments.status', excludedStatuses) + .orWhereExists(function () { + this.select(1) + .from('comments as replies') + .whereRaw('replies.parent_id = comments.id') + .whereNotIn('replies.status', excludedStatuses); + }); + }); + } }); }, @@ -317,9 +323,9 @@ const Comment = ghostBookshelf.Model.extend({ */ permittedOptions: function permittedOptions(methodName) { let options = ghostBookshelf.Model.permittedOptions.call(this, methodName); - // The comment model additionally supports having a parentId and isAdmin option options.push('parentId'); options.push('isAdmin'); + options.push('browseAll'); return options; } diff --git a/ghost/core/core/server/services/comments/CommentsService.js b/ghost/core/core/server/services/comments/CommentsService.js index 4021ae752e9..5fda4ea164f 100644 --- a/ghost/core/core/server/services/comments/CommentsService.js +++ b/ghost/core/core/server/services/comments/CommentsService.js @@ -202,16 +202,15 @@ class CommentsService { order, page, limit, - // If includeNested is false, only return top-level comments parentId: includeNested ? undefined : null, - // Admin context: see hidden comments with full content, and all statuses including deleted - isAdmin: true + isAdmin: true, + browseAll: true }); } async getAdminComments(options) { this.checkEnabled(); - const page = await this.models.Comment.findPage({...options, parentId: null}); + const page = await this.models.Comment.findPage({...options, parentId: null, isAdmin: true}); return page; } diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap index bc75eaac834..a2cb46754b3 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/comments.test.js.snap @@ -294,89 +294,22 @@ Object { } `; -exports[`Admin Comments API Browse All Includes deleted comments for admin 1: [body] 1`] = ` -Object { - "comments": Array [ - Object { - "count": Object { - "likes": 0, - "replies": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": Nullable, - "html": "

Published

", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": Nullable, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "replies": Array [], - "status": "published", - }, - Object { - "count": Object { - "likes": 0, - "replies": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": Nullable, - "html": "

Deleted

", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": Nullable, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "replies": Array [], - "status": "deleted", - }, - ], - "meta": Object { - "pagination": Object { - "limit": 15, - "next": null, - "page": 1, - "pages": 1, - "prev": null, - "total": 2, - }, - }, -} -`; - -exports[`Admin Comments API Browse All Includes hidden comments with full html for admin 1: [body] 1`] = ` +exports[`Admin Comments API Browse All Excludes deleted comments even when they have published replies 1: [body] 1`] = ` Object { "comments": Array [ Object { "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": Nullable, - "html": "

Hidden comment

", + "html": "

Published reply

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "in_reply_to_id": null, "in_reply_to_snippet": null, "member": Object { "avatar_image": null, - "email": "member1@test.com", + "email": "member2@test.com", "expertise": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", + "name": null, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "parent_id": Nullable, @@ -386,7 +319,7 @@ Object { "url": Any, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, - "status": "hidden", + "status": "published", }, ], "meta": Object { @@ -402,21 +335,16 @@ Object { } `; -exports[`Admin Comments API Browse All Includes member relation by default 1: [body] 1`] = ` +exports[`Admin Comments API Browse All Includes hidden comments with full html for admin 1: [body] 1`] = ` Object { "comments": Array [ Object { - "count": Object { - "likes": 0, - "replies": 0, - }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": Nullable, - "html": "

This is a comment

", + "html": "

Hidden comment

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "in_reply_to_id": null, "in_reply_to_snippet": null, - "liked": false, "member": Object { "avatar_image": null, "email": "member1@test.com", @@ -426,44 +354,13 @@ Object { "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "parent_id": Nullable, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "replies": Array [], - "status": "published", - }, - ], - "meta": Object { - "pagination": Object { - "limit": 15, - "next": null, - "page": 1, - "pages": 1, - "prev": null, - "total": 1, - }, - }, -} -`; - -exports[`Admin Comments API Browse All Includes post relation when requested 1: [body] 1`] = ` -Object { - "comments": Array [ - Object { - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": Nullable, - "html": "

This is a comment

", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "member": null, - "parent_id": Nullable, "post": Object { "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "title": "HTML Ipsum", + "title": "Ghostly Kitchen Sink", "url": Any, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "status": "published", + "status": "hidden", }, ], "meta": Object { @@ -544,58 +441,6 @@ Object { } `; -exports[`Admin Comments API Browse All Returns deleted parent as tombstone when it has published replies 1: [body] 1`] = ` -Object { - "comments": Array [ - Object { - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "parent_id": null, - "post": Object { - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "title": "Ghostly Kitchen Sink", - "url": Any, - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "status": "deleted", - }, - Object { - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": Nullable, - "html": "

Published reply

", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "member": Object { - "avatar_image": null, - "email": "member2@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": null, - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": Nullable, - "post": Object { - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "title": "Ghostly Kitchen Sink", - "url": Any, - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "status": "published", - }, - ], - "meta": Object { - "pagination": Object { - "limit": 15, - "next": null, - "page": 1, - "pages": 1, - "prev": null, - "total": 2, - }, - }, -} -`; - exports[`Admin Comments API Browse All Returns flat list including replies by default 1: [body] 1`] = ` Object { "comments": Array [ @@ -981,22 +826,6 @@ Object { } `; -exports[`Admin Comments API browse by post does not return deleted comments with only deleted replies 1: [body] 1`] = ` -Object { - "comments": Array [], - "meta": Object { - "pagination": Object { - "limit": 15, - "next": null, - "page": 1, - "pages": 1, - "prev": null, - "total": 0, - }, - }, -} -`; - exports[`Admin Comments API browse by post does not return deleted replies 1: [body] 1`] = ` Object { "comments": Array [ @@ -1083,13 +912,29 @@ Object { } `; -exports[`Admin Comments API browse by post includes all replies in count for admin 1: [body] 1`] = ` +exports[`Admin Comments API browse by post excludes deleted comments when all replies are also deleted 1: [body] 1`] = ` +Object { + "comments": Array [], + "meta": Object { + "pagination": Object { + "limit": 15, + "next": null, + "page": 1, + "pages": 1, + "prev": null, + "total": 0, + }, + }, +} +`; + +exports[`Admin Comments API browse by post includes hidden replies but not deleted replies in count 1: [body] 1`] = ` Object { "comments": Array [ Object { "count": Object { "likes": Any, - "replies": 3, + "replies": 2, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, @@ -1107,7 +952,6 @@ Object { "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "parent_id": Nullable, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "replies": Array [ Object { "count": Object { @@ -1129,32 +973,8 @@ Object { "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "status": "hidden", }, - Object { - "count": Object { - "likes": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Reply 2", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "status": "deleted", - }, Object { "count": Object { "likes": 0, @@ -1175,7 +995,6 @@ Object { "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "status": "published", }, ], @@ -1195,17 +1014,17 @@ Object { } `; -exports[`Admin Comments API browse by post includes hidden replies but not deleted replies in count 1: [body] 1`] = ` +exports[`Admin Comments API browse by post returns deleted comments if they have published replies 1: [body] 1`] = ` Object { "comments": Array [ Object { "count": Object { "likes": Any, - "replies": 2, + "replies": 1, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "Comment 1", + "html": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "in_reply_to_id": null, "in_reply_to_snippet": null, @@ -1240,32 +1059,10 @@ Object { "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "status": "hidden", - }, - Object { - "count": Object { - "likes": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Reply 3", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "status": "published", }, ], - "status": "published", + "status": "deleted", }, ], "meta": Object { @@ -1281,17 +1078,17 @@ Object { } `; -exports[`Admin Comments API browse by post returns all replies including deleted for admin 1: [body] 1`] = ` +exports[`Admin Comments API get by id does not include deleted replies 1: [body] 1`] = ` Object { "comments": Array [ Object { "count": Object { "likes": Any, - "replies": 3, + "replies": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "Comment 1", + "html": "

This is a comment

", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "in_reply_to_id": null, "in_reply_to_snippet": null, @@ -1305,344 +1102,9 @@ Object { "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "parent_id": Nullable, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "replies": Array [ - Object { - "count": Object { - "likes": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Reply 1", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "status": "deleted", - }, - Object { - "count": Object { - "likes": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Reply 2", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "status": "hidden", - }, - Object { - "count": Object { - "likes": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Reply 3", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "status": "published", - }, - ], - "status": "published", - }, - ], - "meta": Object { - "pagination": Object { - "limit": 15, - "next": null, - "page": 1, - "pages": 1, - "prev": null, - "total": 1, - }, - }, -} -`; - -exports[`Admin Comments API browse by post returns deleted comments as tombstones if they have published replies 1: [body] 1`] = ` -Object { - "comments": Array [ - Object { - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "parent_id": null, - "replies": Array [ - Object { - "count": Object { - "likes": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Reply 1", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "status": "published", - }, - ], - "status": "deleted", - }, - ], - "meta": Object { - "pagination": Object { - "limit": 15, - "next": null, - "page": 1, - "pages": 1, - "prev": null, - "total": 1, - }, - }, -} -`; - -exports[`Admin Comments API browse by post returns deleted comments for admin 1: [body] 1`] = ` -Object { - "comments": Array [ - Object { - "count": Object { - "likes": Any, - "replies": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Comment 1", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": Any, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": Nullable, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "replies": Array [], - "status": "deleted", - }, - ], - "meta": Object { - "pagination": Object { - "limit": 15, - "next": null, - "page": 1, - "pages": 1, - "prev": null, - "total": 1, - }, - }, -} -`; - -exports[`Admin Comments API browse by post returns deleted comments with deleted replies for admin 1: [body] 1`] = ` -Object { - "comments": Array [ - Object { - "count": Object { - "likes": Any, - "replies": 1, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Comment 1", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": Any, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": Nullable, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "replies": Array [ - Object { - "count": Object { - "likes": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Reply 1", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "status": "deleted", - }, - ], - "status": "deleted", - }, - ], - "meta": Object { - "pagination": Object { - "limit": 15, - "next": null, - "page": 1, - "pages": 1, - "prev": null, - "total": 1, - }, - }, -} -`; - -exports[`Admin Comments API get by id does not include deleted replies 1: [body] 1`] = ` -Object { - "comments": Array [ - Object { - "count": Object { - "likes": Any, - "replies": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "

This is a comment

", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": Any, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": Nullable, - "replies": Array [], - "status": "published", - }, - ], -} -`; - -exports[`Admin Comments API get by id includes deleted replies for admin 1: [body] 1`] = ` -Object { - "comments": Array [ - Object { - "count": Object { - "likes": Any, - "replies": 1, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "

This is a comment

", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": Any, - "member": Object { - "avatar_image": null, - "email": "member1@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": "Mr Egg", - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": Nullable, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "replies": Array [ - Object { - "count": Object { - "likes": 0, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "Reply 1", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "in_reply_to_id": null, - "in_reply_to_snippet": null, - "liked": false, - "member": Object { - "avatar_image": null, - "email": "member2@test.com", - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": null, - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "parent_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "post_id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "status": "deleted", - }, - ], "status": "published", }, ], } `; - -exports[`Admin Comments API get by id returns deleted comment as tombstone 1: [body] 1`] = ` -Object { - "comments": Array [ - Object { - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "parent_id": null, - "replies": Array [], - "status": "deleted", - }, - ], -} -`; diff --git a/ghost/core/test/e2e-api/admin/comments.test.js b/ghost/core/test/e2e-api/admin/comments.test.js index 089924eece0..da9e29ef2d0 100644 --- a/ghost/core/test/e2e-api/admin/comments.test.js +++ b/ghost/core/test/e2e-api/admin/comments.test.js @@ -439,7 +439,7 @@ describe(`Admin Comments API`, function () { }); }); - it('returns deleted comments as tombstones if they have published replies', async function () { + it('excludes deleted comments when all replies are also deleted', async function () { await dbFns.addCommentWithReplies({ member_id: fixtureManager.get('members', 0).id, html: 'Comment 1', @@ -447,35 +447,18 @@ describe(`Admin Comments API`, function () { replies: [{ member_id: fixtureManager.get('members', 0).id, html: 'Reply 1', - status: 'published' + status: 'deleted' }] }); - // Tombstone - just id, parent_id, status (per-post browse doesn't load post relation) - const tombstoneMatcher = { - id: anyObjectId - }; - - // Reply has full content including member - const replyMatcher = { - id: anyObjectId, - parent_id: anyObjectId, - created_at: anyISODateTime, - member: { - id: anyObjectId, - uuid: anyUuid - } - }; - - // Parent is tombstone with nested reply that has full content await adminApi.get('/comments/post/' + postId + '/') .expectStatus(200) .matchBodySnapshot({ - comments: [{...tombstoneMatcher, replies: [replyMatcher]}] + comments: [] }); }); - it('does not return deleted comments with only deleted replies', async function () { + it('returns deleted comments if they have published replies', async function () { await dbFns.addCommentWithReplies({ member_id: fixtureManager.get('members', 0).id, html: 'Comment 1', @@ -483,14 +466,27 @@ describe(`Admin Comments API`, function () { replies: [{ member_id: fixtureManager.get('members', 0).id, html: 'Reply 1', - status: 'deleted' + status: 'published' }] }); + const replyMatcher = { + id: anyObjectId, + parent_id: anyObjectId, + created_at: anyISODateTime, + member: { + id: anyObjectId, + uuid: anyUuid + } + }; + await adminApi.get('/comments/post/' + postId + '/') .expectStatus(200) .matchBodySnapshot({ - comments: [] + comments: [{ + ...membersCommentMatcher, + replies: [replyMatcher] + }] }); }); @@ -628,25 +624,6 @@ describe(`Admin Comments API`, function () { assert.equal(res.body.comments[0].html, 'Comment 1'); }); - it('returns deleted comment as tombstone', async function () { - const comment = await dbFns.addComment({ - member_id: fixtureManager.get('members', 0).id, - html: 'Comment 1', - status: 'deleted' - }); - - // Tombstone - just id, parent_id, status (no post relation loaded for this endpoint) - const tombstoneMatcher = { - id: anyObjectId - }; - - await adminApi.get(`/comments/${comment.id}/`) - .expectStatus(200) - .matchBodySnapshot({ - comments: [tombstoneMatcher] - }); - }); - it('includes published replies', async function () { const {parent} = await dbFns.addCommentWithReplies({ member_id: fixtureManager.get('members', 0).id, @@ -1317,7 +1294,7 @@ describe(`Admin Comments API`, function () { }); }); - it('Returns deleted parent as tombstone when it has published replies', async function () { + it('Excludes deleted comments even when they have published replies', async function () { await dbFns.addCommentWithReplies({ member_id: fixtureManager.get('members', 0).id, html: '

Deleted parent

', @@ -1329,21 +1306,11 @@ describe(`Admin Comments API`, function () { }] }); - // Tombstone has id, parent_id, status, post (no member, no html) - const tombstoneMatcher = { - id: anyObjectId, - post: { - id: anyObjectId, - uuid: anyUuid, - url: anyString - } - }; - - // Parent appears as tombstone, reply appears as separate item in flat list + // Only the reply is returned - admin always excludes deleted comments await adminApi.get('/comments/') .expectStatus(200) .matchBodySnapshot({ - comments: [tombstoneMatcher, commentMatcher] + comments: [commentMatcher] }); }); diff --git a/ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap b/ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap index a1c79303679..bc206d7f925 100644 --- a/ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap +++ b/ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap @@ -1491,7 +1491,24 @@ exports[`Comments API when commenting enabled for all when authenticated browse Object { "comments": Array [ Object { + "count": Object { + "likes": Any, + "replies": Any, + }, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "edited_at": null, + "html": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, + "in_reply_to_snippet": null, + "liked": Any, + "member": Object { + "avatar_image": null, + "expertise": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Mr Egg", + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, "parent_id": Nullable, "replies": Array [ Object { @@ -1536,7 +1553,7 @@ exports[`Comments API when commenting enabled for all when authenticated browse Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "588", + "content-length": "894", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -1764,7 +1781,24 @@ exports[`Comments API when commenting enabled for all when authenticated browse Object { "comments": Array [ Object { + "count": Object { + "likes": Any, + "replies": Any, + }, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "edited_at": null, + "html": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, + "in_reply_to_snippet": null, + "liked": Any, + "member": Object { + "avatar_image": null, + "expertise": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Mr Egg", + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, "parent_id": Nullable, "replies": Array [ Object { @@ -1809,7 +1843,7 @@ exports[`Comments API when commenting enabled for all when authenticated browse Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "588", + "content-length": "894", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", diff --git a/ghost/core/test/e2e-api/members-comments/comments.test.js b/ghost/core/test/e2e-api/members-comments/comments.test.js index aafb72bf462..2c3532815c1 100644 --- a/ghost/core/test/e2e-api/members-comments/comments.test.js +++ b/ghost/core/test/e2e-api/members-comments/comments.test.js @@ -120,19 +120,6 @@ const labsCommentMatcher = { in_reply_to_id: nullable(anyObjectId) }; -// Tombstone for deleted comments - minimal data only -const tombstoneMatcher = { - id: anyObjectId, - parent_id: nullable(anyObjectId) -}; - -function tombstoneMatcherWithReplies(replies = 0) { - return { - ...tombstoneMatcher, - replies: new Array(replies).fill(commentMatcher) - }; -} - /** * @param {Object} [options] * @param {number} [options.replies] @@ -678,12 +665,7 @@ describe('Comments API', function () { }] }); - // Deleted parent returned as tombstone with nested reply - const result = await testGetComments(`/api/comments/post/${postId}/`, [tombstoneMatcherWithReplies(1)]); - should(result.body.comments.length).eql(1); - should(result.body.comments[0].status).eql('deleted'); - should(result.body.comments[0].replies.length).eql(1); - should(result.body.meta.pagination.total).eql(1); + await testGetComments(`/api/comments/post/${postId}/`, [commentMatcherWithReplies({replies: 1})]); }); it('excludes deleted comments if all replies are hidden or deleted', async function () { @@ -796,8 +778,8 @@ describe('Comments API', function () { }] }); - // Deleted parent returned as tombstone, only 1 published reply visible - const result = await testGetComments(`/api/comments/post/${postId}/`, [tombstoneMatcherWithReplies(1)]); + // Deleted parent returned with full data, only 1 published reply visible + const result = await testGetComments(`/api/comments/post/${postId}/`, [commentMatcherWithReplies({replies: 1})]); should(result.body.comments[0].replies.length).eql(1); }); });