diff --git a/openreview/arr/arr.py b/openreview/arr/arr.py index aec01a931..8b43dea22 100644 --- a/openreview/arr/arr.py +++ b/openreview/arr/arr.py @@ -855,6 +855,7 @@ def create_bid_stages(self): def create_comment_stage(self): self.venue.comment_stage = self.comment_stage + self.venue.comment_stage.preprocess_path = '../arr/process/comment_pre_process.js' self.venue.comment_stage.process_path = '../arr/process/comment_process.py' return self.venue.create_comment_stage() diff --git a/openreview/arr/process/comment_pre_process.js b/openreview/arr/process/comment_pre_process.js new file mode 100644 index 000000000..732ad1730 --- /dev/null +++ b/openreview/arr/process/comment_pre_process.js @@ -0,0 +1,129 @@ +async function process(client, edit, invitation) { + client.throwErrors = true + + const { note } = edit + if (note.ddate) { + return + } + + const [res1, res2] = await Promise.all([ + client.getNotes({ id: note.forum }), + client.getGroups({ id: invitation.domain }), + ]) + const forum = res1.notes[0] + const domain = res2.groups[0] + const readers = note.readers || [] + const authorsName = domain.content?.authors_name?.value || "Authors" + const authorGroupId = `${domain.id}/Submission${forum.number}/${authorsName}` + const commentText = note.content?.comment?.value || "" + + function containsLink(text) { + const tokenChecks = [ + ["includes", ["http://", "https://", "www."]] + ] + const popularDomainSuffixes = [ + ".com", + ".net", + ".org", + ".io", + ".co", + ".tv", + ".cn", + ".de", + ".uk", + ".ru", + ".nl", + ".br" + ] + const boundaryChars = "<>()[]{}.,;:!?\"'" + let lowerText = String(text || "").toLowerCase() + + for (const whitespace of ["\n", "\r", "\t"]) { + lowerText = lowerText.split(whitespace).join(" ") + } + + function trimToken(token) { + let start = 0 + let end = token.length + while (start < end && boundaryChars.includes(token[start])) { + start += 1 + } + while (end > start && boundaryChars.includes(token[end - 1])) { + end -= 1 + } + return token.slice(start, end) + } + + function matches(value, checks) { + for (const [operation, parts] of checks) { + for (const part of parts) { + if (value[operation](part)) { + return true + } + } + } + return false + } + + for (const rawToken of lowerText.split(" ")) { + const token = trimToken(rawToken) + // Skip empty tokens, emails, and tokens that cannot be hostnames. + if (!token || token.includes("@") || !token.includes(".")) { + continue + } + // Return early for explicit URL schemes and common www-style hosts. + if (matches(token, tokenChecks)) { + return true + } + + // Trim the token to a host-like prefix before matching common web suffixes. + let host = token + for (const separator of ["/", "?", "#", ":"]) { + const index = host.indexOf(separator) + if (index !== -1) { + host = host.slice(0, index) + } + } + host = trimToken(host) + + // Match hosts that end in one of the common web suffixes. + const firstDot = host.indexOf(".") + if (firstDot > 0 && firstDot < host.length - 1) { + for (const suffix of popularDomainSuffixes) { + if (host.endsWith(suffix)) { + return true + } + } + } + } + + return false + } + + if ((readers.includes(authorGroupId) || readers.includes("everyone")) && containsLink(commentText)) { + return Promise.reject( + new OpenReviewError({ + name: "Error", + message: "Links are not allowed in official comments that are visible to authors.", + }) + ) + } + + if (readers.includes("everyone")) { + return + } + + const commentMandatoryReaders = + domain.content?.comment_mandatory_readers?.value || [] + for (const m of commentMandatoryReaders) { + const reader = m.replace("{number}", forum.number) + if (!readers.includes(reader)) { + return Promise.reject( + new OpenReviewError({ + name: "Error", + message: reader + " must be readers of the comment", + }) + ) + } + } +} diff --git a/tests/test_arr_venue_v2.py b/tests/test_arr_venue_v2.py index 790d96d3a..6f200040d 100644 --- a/tests/test_arr_venue_v2.py +++ b/tests/test_arr_venue_v2.py @@ -5327,6 +5327,183 @@ def test_author_response(self, client, openreview_client, helpers, test_client, reviewer_client = openreview.api.OpenReviewClient(username = 'reviewer2@aclrollingreview.com', password=helpers.strong_password) anon_id = reviewer_client.get_groups(prefix=f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Reviewer_', signatory='~Reviewer_ARRTwo1')[0].id ac_client = openreview.api.OpenReviewClient(username = 'ac1@aclrollingreview.com', password=helpers.strong_password) + + with pytest.raises(openreview.OpenReviewException, match=r'Links are not allowed in official comments that are visible to authors.'): + reviewer_client.post_note_edit( + invitation=f"aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/-/Official_Comment", + writers=['aclweb.org/ACL/ARR/2023/August'], + signatures=[anon_id], + note=openreview.api.Note( + replyto=submissions[1].id, + readers=[ + 'aclweb.org/ACL/ARR/2023/August/Program_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Senior_Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Reviewers', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors' + ], + content={ + "comment": { "value": "Please see https://example.com/details for more information."} + } + ) + ) + + with pytest.raises(openreview.OpenReviewException, match=r'Links are not allowed in official comments that are visible to authors.'): + test_client.post_note_edit( + invitation=f"aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/-/Official_Comment", + writers=['aclweb.org/ACL/ARR/2023/August'], + signatures=[f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors'], + note=openreview.api.Note( + replyto=submissions[1].id, + readers=[ + 'aclweb.org/ACL/ARR/2023/August/Program_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Senior_Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Reviewers', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors' + ], + content={ + "comment": { "value": "Please see [this note](https://example.com/details)." } + } + ) + ) + + with pytest.raises(openreview.OpenReviewException, match=r'Links are not allowed in official comments that are visible to authors.'): + test_client.post_note_edit( + invitation=f"aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/-/Official_Comment", + writers=['aclweb.org/ACL/ARR/2023/August'], + signatures=[f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors'], + note=openreview.api.Note( + replyto=submissions[1].id, + readers=[ + 'aclweb.org/ACL/ARR/2023/August/Program_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Senior_Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Reviewers', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors' + ], + content={ + "comment": { "value": "Please see www.example.com for more information." } + } + ) + ) + + with pytest.raises(openreview.OpenReviewException, match=r'Links are not allowed in official comments that are visible to authors.'): + test_client.post_note_edit( + invitation=f"aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/-/Official_Comment", + writers=['aclweb.org/ACL/ARR/2023/August'], + signatures=[f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors'], + note=openreview.api.Note( + replyto=submissions[1].id, + readers=[ + 'aclweb.org/ACL/ARR/2023/August/Program_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Senior_Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Reviewers', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors' + ], + content={ + "comment": { "value": "Please see example.com for more information." } + } + ) + ) + + with pytest.raises(openreview.OpenReviewException, match=r'Links are not allowed in official comments that are visible to authors.'): + test_client.post_note_edit( + invitation=f"aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/-/Official_Comment", + writers=['aclweb.org/ACL/ARR/2023/August'], + signatures=[f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors'], + note=openreview.api.Note( + replyto=submissions[1].id, + readers=[ + 'aclweb.org/ACL/ARR/2023/August/Program_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Senior_Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Reviewers', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors' + ], + content={ + "comment": { "value": "Please see example.co.uk for more information." } + } + ) + ) + + comment_edit = pc_client_v2.post_note_edit( + invitation=f"aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/-/Official_Comment", + writers=['aclweb.org/ACL/ARR/2023/August'], + signatures=['aclweb.org/ACL/ARR/2023/August/Program_Chairs'], + note=openreview.api.Note( + replyto=submissions[0].id, + readers=[ + 'aclweb.org/ACL/ARR/2023/August/Program_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/Senior_Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/Authors' + ], + content={ + "comment": { "value": "Please email pc@example.com for more information." } + } + ) + ) + helpers.await_queue_edit(openreview_client, edit_id=comment_edit['id']) + + comment_edit = pc_client_v2.post_note_edit( + invitation=f"aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/-/Official_Comment", + writers=['aclweb.org/ACL/ARR/2023/August'], + signatures=['aclweb.org/ACL/ARR/2023/August/Program_Chairs'], + note=openreview.api.Note( + replyto=submissions[0].id, + readers=[ + 'aclweb.org/ACL/ARR/2023/August/Program_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/Senior_Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/Authors' + ], + content={ + "comment": { "value": "Please see example.museum for more information." } + } + ) + ) + helpers.await_queue_edit(openreview_client, edit_id=comment_edit['id']) + + with pytest.raises(openreview.OpenReviewException, match=r'Links are not allowed in official comments that are visible to authors.'): + test_client.post_note_edit( + invitation=f"aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/-/Official_Comment", + writers=['aclweb.org/ACL/ARR/2023/August'], + signatures=[f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors'], + note=openreview.api.Note( + replyto=submissions[1].id, + readers=[ + 'aclweb.org/ACL/ARR/2023/August/Program_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Senior_Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Reviewers', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors' + ], + content={ + "comment": { "value": "Please see example.io for more information." } + } + ) + ) + + comment_edit = pc_client_v2.post_note_edit( + invitation=f"aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/-/Official_Comment", + writers=['aclweb.org/ACL/ARR/2023/August'], + signatures=['aclweb.org/ACL/ARR/2023/August/Program_Chairs'], + note=openreview.api.Note( + replyto=submissions[0].id, + readers=[ + 'aclweb.org/ACL/ARR/2023/August/Program_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/Senior_Area_Chairs', + f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[0].number}/Area_Chairs' + ], + content={ + "comment": { "value": "Internal discussion: https://example.com/internal-doc" } + } + ) + ) + helpers.await_queue_edit(openreview_client, edit_id=comment_edit['id']) + clients = [reviewer_client, test_client] signatures = [anon_id, f'aclweb.org/ACL/ARR/2023/August/Submission{submissions[1].number}/Authors'] root_note_id = None