mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Added empty line trimming to comment messages
fixes https://github.com/TryGhost/Team/issues/1737 - Empty lines at start - Empty lines at end - Duplicate empty lines inside the comment message (max one allowed)
This commit is contained in:
parent
3f8ddd61f9
commit
e112f1cd40
5 changed files with 66 additions and 28 deletions
|
@ -9,6 +9,24 @@ const messages = {
|
||||||
notYourCommentToDestroy: 'You may only delete your own comments'
|
notYourCommentToDestroy: 'You may only delete your own comments'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function escapeRegex(string) {
|
||||||
|
return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove empty paragraps from the start and end
|
||||||
|
* + remove duplicate empty paragrapsh (only one empty line allowed)
|
||||||
|
*/
|
||||||
|
function trimParagraphs(str) {
|
||||||
|
const paragraph = '<p></p>';
|
||||||
|
const escapedParagraph = escapeRegex(paragraph);
|
||||||
|
|
||||||
|
const startReg = new RegExp('^(' + escapedParagraph + ')+');
|
||||||
|
const endReg = new RegExp('(' + escapedParagraph + ')+$');
|
||||||
|
const duplicates = new RegExp('(' + escapedParagraph + ')+');
|
||||||
|
return str.replace(startReg, '').replace(endReg, '').replace(duplicates, paragraph);
|
||||||
|
}
|
||||||
|
|
||||||
const Comment = ghostBookshelf.Model.extend({
|
const Comment = ghostBookshelf.Model.extend({
|
||||||
tableName: 'comments',
|
tableName: 'comments',
|
||||||
|
|
||||||
|
@ -49,20 +67,22 @@ const Comment = ghostBookshelf.Model.extend({
|
||||||
if (this.hasChanged('html')) {
|
if (this.hasChanged('html')) {
|
||||||
const sanitizeHtml = require('sanitize-html');
|
const sanitizeHtml = require('sanitize-html');
|
||||||
|
|
||||||
this.set('html', sanitizeHtml(this.get('html'), {
|
this.set('html', trimParagraphs(
|
||||||
allowedTags: ['p', 'br', 'a', 'blockquote'],
|
sanitizeHtml(this.get('html'), {
|
||||||
allowedAttributes: {
|
allowedTags: ['p', 'br', 'a', 'blockquote'],
|
||||||
a: ['href', 'target', 'rel']
|
allowedAttributes: {
|
||||||
},
|
a: ['href', 'target', 'rel']
|
||||||
selfClosing: ['br'],
|
},
|
||||||
// Enforce _blank and safe URLs
|
selfClosing: ['br'],
|
||||||
transformTags: {
|
// Enforce _blank and safe URLs
|
||||||
a: sanitizeHtml.simpleTransform('a', {
|
transformTags: {
|
||||||
target: '_blank',
|
a: sanitizeHtml.simpleTransform('a', {
|
||||||
rel: 'ugc noopener noreferrer nofollow'
|
target: '_blank',
|
||||||
})
|
rel: 'ugc noopener noreferrer nofollow'
|
||||||
}
|
})
|
||||||
}));
|
}
|
||||||
|
})
|
||||||
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ class CommentsServiceEmails {
|
||||||
postTitle: post.get('title'),
|
postTitle: post.get('title'),
|
||||||
postUrl: this.urlService.getUrlByResourceId(post.get('id'), {absolute: true}),
|
postUrl: this.urlService.getUrlByResourceId(post.get('id'), {absolute: true}),
|
||||||
commentHtml: comment.get('html'),
|
commentHtml: comment.get('html'),
|
||||||
commentText: htmlToPlaintext.email(comment.get('html')),
|
commentText: htmlToPlaintext.comment(comment.get('html')),
|
||||||
commentDate: moment(comment.get('created_at')).tz(this.settingsCache.get('timezone')).format('D MMM YYYY'),
|
commentDate: moment(comment.get('created_at')).tz(this.settingsCache.get('timezone')).format('D MMM YYYY'),
|
||||||
|
|
||||||
reporterName: reporter.name,
|
reporterName: reporter.name,
|
||||||
|
|
|
@ -34,6 +34,7 @@ const baseSettings = {
|
||||||
|
|
||||||
let excerptConverter;
|
let excerptConverter;
|
||||||
let emailConverter;
|
let emailConverter;
|
||||||
|
let commentConverter;
|
||||||
|
|
||||||
const loadConverters = () => {
|
const loadConverters = () => {
|
||||||
if (excerptConverter && emailConverter) {
|
if (excerptConverter && emailConverter) {
|
||||||
|
@ -63,8 +64,19 @@ const loadConverters = () => {
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const commentSettings = mergeSettings({
|
||||||
|
preserveNewlines: false,
|
||||||
|
selectors: [
|
||||||
|
// equiv hideLinkHrefIfSameAsText: true
|
||||||
|
{selector: 'a', options: {hideLinkHrefIfSameAsText: true}},
|
||||||
|
// No space between <p> tags. An empty <p> is needed
|
||||||
|
{selector: 'p', options: {leadingLineBreaks: 1, trailingLineBreaks: 1}}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
excerptConverter = compile(excerptSettings);
|
excerptConverter = compile(excerptSettings);
|
||||||
emailConverter = compile(emailSettings);
|
emailConverter = compile(emailSettings);
|
||||||
|
commentConverter = compile(commentSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.excerpt = (html) => {
|
module.exports.excerpt = (html) => {
|
||||||
|
@ -78,3 +90,9 @@ module.exports.email = (html) => {
|
||||||
|
|
||||||
return emailConverter(html);
|
return emailConverter(html);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.comment = (html) => {
|
||||||
|
loadConverters();
|
||||||
|
|
||||||
|
return commentConverter(html);
|
||||||
|
};
|
||||||
|
|
|
@ -40,7 +40,7 @@ Object {
|
||||||
Object {
|
Object {
|
||||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
"edited_at": null,
|
"edited_at": null,
|
||||||
"html": "<p>This is a message</p><p>New line</p>",
|
"html": "<p>This is a message</p><p></p><p>New line</p>",
|
||||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
"liked": Any<Boolean>,
|
"liked": Any<Boolean>,
|
||||||
"likes_count": Any<Number>,
|
"likes_count": Any<Number>,
|
||||||
|
@ -72,7 +72,7 @@ exports[`Comments API when authenticated Can browse all comments of a post 2: [h
|
||||||
Object {
|
Object {
|
||||||
"access-control-allow-origin": "*",
|
"access-control-allow-origin": "*",
|
||||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
"content-length": "1057",
|
"content-length": "1064",
|
||||||
"content-type": "application/json; charset=utf-8",
|
"content-type": "application/json; charset=utf-8",
|
||||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
"vary": "Accept-Encoding",
|
"vary": "Accept-Encoding",
|
||||||
|
@ -86,7 +86,7 @@ Object {
|
||||||
Object {
|
Object {
|
||||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
"edited_at": null,
|
"edited_at": null,
|
||||||
"html": "<p>This is a message</p><p>New line</p>",
|
"html": "<p>This is a message</p><p></p><p>New line</p>",
|
||||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
"liked": false,
|
"liked": false,
|
||||||
"likes_count": 0,
|
"likes_count": 0,
|
||||||
|
@ -101,7 +101,7 @@ exports[`Comments API when authenticated Can comment on a post 2: [headers] 1`]
|
||||||
Object {
|
Object {
|
||||||
"access-control-allow-origin": "*",
|
"access-control-allow-origin": "*",
|
||||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
"content-length": "219",
|
"content-length": "226",
|
||||||
"content-type": "application/json; charset=utf-8",
|
"content-type": "application/json; charset=utf-8",
|
||||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
|
||||||
|
@ -169,7 +169,7 @@ Object {
|
||||||
Object {
|
Object {
|
||||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
"edited_at": null,
|
"edited_at": null,
|
||||||
"html": "<p>This is a message</p><p>New line</p>",
|
"html": "<p>This is a message</p><p></p><p>New line</p>",
|
||||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
"liked": Any<Boolean>,
|
"liked": Any<Boolean>,
|
||||||
"likes_count": Any<Number>,
|
"likes_count": Any<Number>,
|
||||||
|
@ -239,7 +239,7 @@ exports[`Comments API when authenticated Can like a comment 2: [headers] 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"access-control-allow-origin": "*",
|
"access-control-allow-origin": "*",
|
||||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
"content-length": "648",
|
"content-length": "655",
|
||||||
"content-type": "application/json; charset=utf-8",
|
"content-type": "application/json; charset=utf-8",
|
||||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
"vary": "Accept-Encoding",
|
"vary": "Accept-Encoding",
|
||||||
|
@ -262,7 +262,7 @@ Object {
|
||||||
Object {
|
Object {
|
||||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
"edited_at": null,
|
"edited_at": null,
|
||||||
"html": "<p>This is a message</p><p>New line</p>",
|
"html": "<p>This is a message</p><p></p><p>New line</p>",
|
||||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
"liked": Any<Boolean>,
|
"liked": Any<Boolean>,
|
||||||
"likes_count": Any<Number>,
|
"likes_count": Any<Number>,
|
||||||
|
@ -301,7 +301,7 @@ exports[`Comments API when authenticated Can like a comment 5: [headers] 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"access-control-allow-origin": "*",
|
"access-control-allow-origin": "*",
|
||||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
"content-length": "647",
|
"content-length": "654",
|
||||||
"content-type": "application/json; charset=utf-8",
|
"content-type": "application/json; charset=utf-8",
|
||||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
"vary": "Accept-Encoding",
|
"vary": "Accept-Encoding",
|
||||||
|
@ -473,7 +473,7 @@ Object {
|
||||||
Object {
|
Object {
|
||||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||||
"edited_at": null,
|
"edited_at": null,
|
||||||
"html": "<p>This is a message</p><p>New line</p>",
|
"html": "<p>This is a message</p><p></p><p>New line</p>",
|
||||||
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
|
||||||
"liked": Any<Boolean>,
|
"liked": Any<Boolean>,
|
||||||
"likes_count": Any<Number>,
|
"likes_count": Any<Number>,
|
||||||
|
@ -512,7 +512,7 @@ exports[`Comments API when authenticated Can remove a like 3: [headers] 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"access-control-allow-origin": "*",
|
"access-control-allow-origin": "*",
|
||||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||||
"content-length": "648",
|
"content-length": "655",
|
||||||
"content-type": "application/json; charset=utf-8",
|
"content-type": "application/json; charset=utf-8",
|
||||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||||
"vary": "Accept-Encoding",
|
"vary": "Accept-Encoding",
|
||||||
|
|
|
@ -165,7 +165,7 @@ describe('Comments API', function () {
|
||||||
.post(`/api/comments/`)
|
.post(`/api/comments/`)
|
||||||
.body({comments: [{
|
.body({comments: [{
|
||||||
post_id: postId,
|
post_id: postId,
|
||||||
html: '<p>This is a <strong>message</strong></p><p>New line</p>'
|
html: '<div></div><p></p><p>This is a <strong>message</strong></p><p></p><p></p><p>New line</p><p></p>'
|
||||||
}]})
|
}]})
|
||||||
.expectStatus(201)
|
.expectStatus(201)
|
||||||
.matchHeaderSnapshot({
|
.matchHeaderSnapshot({
|
||||||
|
@ -184,7 +184,7 @@ describe('Comments API', function () {
|
||||||
subject: '💬 New comment on your post: ' + postTitle,
|
subject: '💬 New comment on your post: ' + postTitle,
|
||||||
to: fixtureManager.get('users', 0).email,
|
to: fixtureManager.get('users', 0).email,
|
||||||
// Note that the <strong> tag is removed by the sanitizer
|
// Note that the <strong> tag is removed by the sanitizer
|
||||||
html: new RegExp(escapeRegExp('<p>This is a message</p><p>New line</p>'))
|
html: new RegExp(escapeRegExp('<p>This is a message</p><p></p><p>New line</p>'))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for the dispatched events (because this happens async)
|
// Wait for the dispatched events (because this happens async)
|
||||||
|
@ -391,7 +391,7 @@ describe('Comments API', function () {
|
||||||
mockManager.assert.sentEmail({
|
mockManager.assert.sentEmail({
|
||||||
subject: '🚩 A comment has been reported on your post',
|
subject: '🚩 A comment has been reported on your post',
|
||||||
to: fixtureManager.get('users', 0).email,
|
to: fixtureManager.get('users', 0).email,
|
||||||
html: new RegExp(escapeRegExp('<p>This is a message</p><p>New line</p>')),
|
html: new RegExp(escapeRegExp('<p>This is a message</p><p></p><p>New line</p>')),
|
||||||
text: new RegExp(escapeRegExp('This is a message\n\nNew line'))
|
text: new RegExp(escapeRegExp('This is a message\n\nNew line'))
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue