feat: Limit comment length

Limit the amount of characters that can be entered for a comment.

Signed-off-by: moson <moson@archlinux.org>
This commit is contained in:
moson 2024-02-25 10:46:47 +01:00
parent d050b626db
commit 21a23c9abe
No known key found for this signature in database
GPG key ID: 4A4760AB4EE15296
14 changed files with 128 additions and 14 deletions

View file

@ -1,6 +1,9 @@
from http import HTTPStatus
from typing import Any from typing import Any
from aurweb import db from fastapi import HTTPException
from aurweb import config, db
from aurweb.exceptions import ValidationError from aurweb.exceptions import ValidationError
from aurweb.models import PackageBase from aurweb.models import PackageBase
@ -12,8 +15,8 @@ def request(
merge_into: str, merge_into: str,
context: dict[str, Any], context: dict[str, Any],
) -> None: ) -> None:
if not comments: # validate comment
raise ValidationError(["The comment field must not be empty."]) comment(comments)
if type == "merge": if type == "merge":
# Perform merge-related checks. # Perform merge-related checks.
@ -32,3 +35,21 @@ def request(
if target.ID == pkgbase.ID: if target.ID == pkgbase.ID:
# TODO: This error needs to be translated. # TODO: This error needs to be translated.
raise ValidationError(["You cannot merge a package base into itself."]) raise ValidationError(["You cannot merge a package base into itself."])
def comment(comment: str):
if not comment:
raise ValidationError(["The comment field must not be empty."])
if len(comment) > config.getint("options", "max_chars_comment", 5000):
raise ValidationError(["Maximum number of characters for comment exceeded."])
def comment_raise_http_ex(comments: str):
try:
comment(comments)
except ValidationError as err:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail=err.data[0],
)

View file

@ -159,6 +159,8 @@ async def pkgbase_flag_post(
request, "pkgbase/flag.html", context, status_code=HTTPStatus.BAD_REQUEST request, "pkgbase/flag.html", context, status_code=HTTPStatus.BAD_REQUEST
) )
validate.comment_raise_http_ex(comments)
has_cred = request.user.has_credential(creds.PKGBASE_FLAG) has_cred = request.user.has_credential(creds.PKGBASE_FLAG)
if has_cred and not pkgbase.OutOfDateTS: if has_cred and not pkgbase.OutOfDateTS:
now = time.utcnow() now = time.utcnow()
@ -185,8 +187,7 @@ async def pkgbase_comments_post(
"""Add a new comment via POST request.""" """Add a new comment via POST request."""
pkgbase = get_pkg_or_base(name, PackageBase) pkgbase = get_pkg_or_base(name, PackageBase)
if not comment: validate.comment_raise_http_ex(comment)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST)
# If the provided comment is different than the record's version, # If the provided comment is different than the record's version,
# update the db record. # update the db record.
@ -304,9 +305,9 @@ async def pkgbase_comment_post(
pkgbase = get_pkg_or_base(name, PackageBase) pkgbase = get_pkg_or_base(name, PackageBase)
db_comment = get_pkgbase_comment(pkgbase, id) db_comment = get_pkgbase_comment(pkgbase, id)
if not comment: validate.comment_raise_http_ex(comment)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST)
elif request.user.ID != db_comment.UsersID: if request.user.ID != db_comment.UsersID:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED) raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED)
# If the provided comment is different than the record's version, # If the provided comment is different than the record's version,
@ -602,6 +603,9 @@ async def pkgbase_disown_post(
): ):
pkgbase = get_pkg_or_base(name, PackageBase) pkgbase = get_pkg_or_base(name, PackageBase)
if comments:
validate.comment_raise_http_ex(comments)
comaints = {c.User for c in pkgbase.comaintainers} comaints = {c.User for c in pkgbase.comaintainers}
approved = [pkgbase.Maintainer] + list(comaints) approved = [pkgbase.Maintainer] + list(comaints)
has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=approved) has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=approved)
@ -873,6 +877,7 @@ async def pkgbase_delete_post(
) )
if comments: if comments:
validate.comment_raise_http_ex(comments)
# Update any existing deletion requests' ClosureComment. # Update any existing deletion requests' ClosureComment.
with db.begin(): with db.begin():
requests = pkgbase.requests.filter( requests = pkgbase.requests.filter(
@ -966,6 +971,9 @@ async def pkgbase_merge_post(
request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST
) )
if comments:
validate.comment_raise_http_ex(comments)
with db.begin(): with db.begin():
update_closure_comment(pkgbase, MERGE_ID, comments, target=target) update_closure_comment(pkgbase, MERGE_ID, comments, target=target)

View file

@ -70,6 +70,7 @@ def make_context(request: Request, title: str, next: str = None):
commit_url = aurweb.config.get_with_fallback("devel", "commit_url", None) commit_url = aurweb.config.get_with_fallback("devel", "commit_url", None)
commit_hash = aurweb.config.get_with_fallback("devel", "commit_hash", None) commit_hash = aurweb.config.get_with_fallback("devel", "commit_hash", None)
max_chars_comment = aurweb.config.getint("options", "max_chars_comment", 5000)
if commit_hash: if commit_hash:
# Shorten commit_hash to a short Git hash. # Shorten commit_hash to a short Git hash.
commit_hash = commit_hash[:7] commit_hash = commit_hash[:7]
@ -92,6 +93,7 @@ def make_context(request: Request, title: str, next: str = None):
"creds": aurweb.auth.creds, "creds": aurweb.auth.creds,
"next": next if next else request.url.path, "next": next if next else request.url.path,
"version": os.environ.get("COMMIT_HASH", aurweb.config.AURWEB_VERSION), "version": os.environ.get("COMMIT_HASH", aurweb.config.AURWEB_VERSION),
"max_chars_comment": max_chars_comment,
} }

View file

@ -49,6 +49,8 @@ salt_rounds = 12
redis_address = redis://localhost redis_address = redis://localhost
; Toggles traceback display in templates/errors/500.html. ; Toggles traceback display in templates/errors/500.html.
traceback = 0 traceback = 0
; Maximum number of characters for a comment
max_chars_comment = 5000
[ratelimit] [ratelimit]
request_limit = 4000 request_limit = 4000

View file

@ -2371,3 +2371,7 @@ msgid "Note that if you hide your email address, it'll "
"receive an email. However, replies are typically sent to the " "receive an email. However, replies are typically sent to the "
"mailing-list and would then be visible in the archive." "mailing-list and would then be visible in the archive."
msgstr "" msgstr ""
#: templates/partials/packages/comment_form.html
msgid "Maximum number of characters"
msgstr ""

View file

@ -21,12 +21,15 @@ Routes:
| format('<a href="https://daringfireball.net/projects/markdown/syntax">', | format('<a href="https://daringfireball.net/projects/markdown/syntax">',
"</a>") "</a>")
| safe }} | safe }}
<br/>
{{ "Maximum number of characters" | tr }}: {{ max_chars_comment }}.
</p> </p>
<p> <p>
<textarea id="id_comment" <textarea id="id_comment"
name="comment" name="comment"
cols="80" cols="80"
rows="10" rows="10"
maxlength="{{ max_chars_comment }}"
>{% if comment %}{{ comment.Comments or '' }}{% endif %}</textarea> >{% if comment %}{{ comment.Comments or '' }}{% endif %}</textarea>
</p> </p>
<p> <p>

View file

@ -24,13 +24,17 @@
"</a>" "</a>"
) | safe ) | safe
}} }}
<br/>
{{ "Maximum number of characters" | tr }}: {{ max_chars_comment }}.
</p> </p>
<p> <p>
<textarea id="id_comment" <textarea id="id_comment"
name="comment" name="comment"
cols="80" cols="80"
rows="10">{{ comment.Comments }}</textarea> rows="10"
maxlength="{{ max_chars_comment }}"
>{{ comment.Comments }}</textarea>
</p> </p>
<p> <p>

View file

@ -50,7 +50,7 @@
<p> <p>
<label for="id_comments">{{ "Comments" | tr }}:</label> <label for="id_comments">{{ "Comments" | tr }}:</label>
<textarea id="id_comments" name="comments" <textarea id="id_comments" name="comments"
rows="5" cols="50" rows="5" cols="50" maxlength="{{ max_chars_comment }}"
placeholder="Related package request closure comments..." placeholder="Related package request closure comments..."
></textarea> ></textarea>
</p> </p>

View file

@ -55,6 +55,7 @@
<textarea id="id_comments" <textarea id="id_comments"
name="comments" name="comments"
rows="5" cols="50" rows="5" cols="50"
maxlength="{{ max_chars_comment }}"
placeholder="Related package request closure comments..." placeholder="Related package request closure comments..."
></textarea> ></textarea>
</p> </p>

View file

@ -60,7 +60,8 @@
<textarea id="id_comments" <textarea id="id_comments"
name="comments" name="comments"
rows="5" rows="5"
cols="50"></textarea> cols="50"
maxlength="{{ max_chars_comment }}"></textarea>
</p> </p>
<p> <p>
<input class="button" type="submit" value="{{ 'Flag' | tr }}" /> <input class="button" type="submit" value="{{ 'Flag' | tr }}" />

View file

@ -52,6 +52,7 @@
<textarea id="id_comments" <textarea id="id_comments"
name="comments" name="comments"
rows="5" cols="50" rows="5" cols="50"
maxlength="{{ max_chars_comment }}"
placeholder="Related package request closure comments..." placeholder="Related package request closure comments..."
></textarea> ></textarea>
</p> </p>

View file

@ -64,7 +64,10 @@
<p> <p>
<label for="id_comments">{{ "Comments" | tr }}:</label> <label for="id_comments">{{ "Comments" | tr }}:</label>
<textarea id="id_comments" name="comments" <textarea id="id_comments" name="comments"
rows="5" cols="50">{{ comments or '' }}</textarea> rows="5" cols="50"
maxlength="{{ max_chars_comment }}"
>{{ comments or '' }}
</textarea>
</p> </p>
<p id="deletion_hint"> <p id="deletion_hint">

View file

@ -26,7 +26,8 @@
<p> <p>
<label for="id_comments">{{ "Comments" | tr }}:</label> <label for="id_comments">{{ "Comments" | tr }}:</label>
<textarea id="id_comments" name="comments" <textarea id="id_comments" name="comments"
rows="5" cols="50"></textarea> rows="5" cols="50" maxlength="{{ max_chars_comment }}"
></textarea>
</p> </p>
<p> <p>

View file

@ -6,7 +6,7 @@ import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlalchemy import and_ from sqlalchemy import and_
from aurweb import asgi, db, time from aurweb import asgi, config, db, time
from aurweb.models.account_type import USER_ID, AccountType from aurweb.models.account_type import USER_ID, AccountType
from aurweb.models.dependency_type import DependencyType from aurweb.models.dependency_type import DependencyType
from aurweb.models.package import Package from aurweb.models.package import Package
@ -25,6 +25,8 @@ from aurweb.testing.email import Email
from aurweb.testing.html import get_errors, get_successes, parse_root from aurweb.testing.html import get_errors, get_successes, parse_root
from aurweb.testing.requests import Request from aurweb.testing.requests import Request
max_chars_comment = config.getint("options", "max_chars_comment", 5000)
def package_endpoint(package: Package) -> str: def package_endpoint(package: Package) -> str:
return f"/packages/{package.Name}" return f"/packages/{package.Name}"
@ -572,6 +574,38 @@ def test_pkgbase_comments(
assert "form" in data assert "form" in data
def test_pkgbase_comment_exceed_character_limit(
client: TestClient,
user: User,
package: Package,
comment: PackageComment,
):
# Post new comment
cookies = {"AURSID": user.login(Request(), "testPassword")}
pkgbasename = package.PackageBase.Name
endpoint = f"/pkgbase/{pkgbasename}/comments"
with client as request:
request.cookies = cookies
resp = request.post(
endpoint,
data={"comment": "x" * (max_chars_comment + 1)},
)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
assert "Maximum number of characters for comment exceeded." in resp.text
# Edit existing
cookies = {"AURSID": user.login(Request(), "testPassword")}
with client as request:
request.cookies = cookies
endp = f"/pkgbase/{pkgbasename}/comments/{comment.ID}"
response = request.post(
endp,
data={"comment": "x" * (max_chars_comment + 1)},
)
assert response.status_code == HTTPStatus.BAD_REQUEST
assert "Maximum number of characters for comment exceeded." in resp.text
def test_pkgbase_comment_edit_unauthorized( def test_pkgbase_comment_edit_unauthorized(
client: TestClient, client: TestClient,
user: User, user: User,
@ -935,6 +969,28 @@ def test_pkgbase_request_post_no_comment_error(
assert error.text.strip() == expected assert error.text.strip() == expected
def test_pkgbase_request_post_comment_exceed_character_limit(
client: TestClient, user: User, package: Package
):
endpoint = f"/pkgbase/{package.PackageBase.Name}/request"
cookies = {"AURSID": user.login(Request(), "testPassword")}
with client as request:
request.cookies = cookies
resp = request.post(
endpoint,
data={
"type": "deletion",
"comments": "x" * (max_chars_comment + 1),
},
)
assert resp.status_code == int(HTTPStatus.OK)
root = parse_root(resp.text)
error = root.xpath('//ul[@class="errorlist"]/li')[0]
expected = "Maximum number of characters for comment exceeded."
assert error.text.strip() == expected
def test_pkgbase_request_post_merge_not_found_error( def test_pkgbase_request_post_merge_not_found_error(
client: TestClient, user: User, package: Package client: TestClient, user: User, package: Package
): ):
@ -1087,6 +1143,13 @@ def test_pkgbase_flag(
assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.status_code == int(HTTPStatus.SEE_OTHER)
assert pkgbase.Flagger is None assert pkgbase.Flagger is None
# Try flagging with a comment that exceeds our character limit.
with client as request:
request.cookies = cookies
data = {"comments": "x" * (max_chars_comment + 1)}
resp = request.post(f"/pkgbase/{pkgbase.Name}/flag", data=data)
assert resp.status_code == int(HTTPStatus.BAD_REQUEST)
# Flag it again. # Flag it again.
with client as request: with client as request:
request.cookies = cookies request.cookies = cookies