Normalize line endings in form submissions

Fixes #506
This commit is contained in:
rubenwardy 2024-06-22 13:22:37 +01:00
parent d6e25f38a8
commit da090fd3f5
10 changed files with 29 additions and 21 deletions

@ -22,14 +22,14 @@ from wtforms.validators import InputRequired, Length
from app.markdown import render_markdown from app.markdown import render_markdown
from app.tasks.emails import send_user_email, send_bulk_email as task_send_bulk from app.tasks.emails import send_user_email, send_bulk_email as task_send_bulk
from app.utils import rank_required, add_audit_log from app.utils import rank_required, add_audit_log, normalize_line_endings
from . import bp from . import bp
from app.models import UserRank, User, AuditSeverity from app.models import UserRank, User, AuditSeverity
class SendEmailForm(FlaskForm): class SendEmailForm(FlaskForm):
subject = StringField("Subject", [InputRequired(), Length(1, 300)]) subject = StringField("Subject", [InputRequired(), Length(1, 300)])
text = TextAreaField("Message", [InputRequired()]) text = TextAreaField("Message", [InputRequired()], filters=[normalize_line_endings])
submit = SubmitField("Send") submit = SubmitField("Send")

@ -22,7 +22,7 @@ from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import InputRequired, Length, Optional from wtforms.validators import InputRequired, Length, Optional
from app.models import db, AuditSeverity, UserRank, Language, Package, PackageState, PackageTranslation from app.models import db, AuditSeverity, UserRank, Language, Package, PackageState, PackageTranslation
from app.utils import add_audit_log, rank_required from app.utils import add_audit_log, rank_required, normalize_line_endings
from . import bp from . import bp
@ -38,7 +38,7 @@ def language_list():
class LanguageForm(FlaskForm): class LanguageForm(FlaskForm):
id = StringField("Id", [InputRequired(), Length(2, 10)]) id = StringField("Id", [InputRequired(), Length(2, 10)])
title = TextAreaField("Title", [Optional(), Length(2, 100)]) title = TextAreaField("Title", [Optional(), Length(2, 100)], filters=[normalize_line_endings])
submit = SubmitField("Save") submit = SubmitField("Save")

@ -23,7 +23,7 @@ from wtforms.validators import InputRequired, Length, Optional, Regexp
from . import bp from . import bp
from app.models import Permission, Tag, db, AuditSeverity from app.models import Permission, Tag, db, AuditSeverity
from app.utils import add_audit_log from app.utils import add_audit_log, normalize_line_endings
@bp.route("/tags/") @bp.route("/tags/")
@ -44,7 +44,7 @@ def tag_list():
class TagForm(FlaskForm): class TagForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(3, 100)]) title = StringField("Title", [InputRequired(), Length(3, 100)])
description = TextAreaField("Description", [Optional(), Length(0, 500)]) description = TextAreaField("Description", [Optional(), Length(0, 500)], filters=[normalize_line_endings])
name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, name = StringField("Name", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0,
"Lower case letters (a-z), digits (0-9), and underscores (_) only")]) "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
submit = SubmitField("Save") submit = SubmitField("Save")

@ -20,7 +20,7 @@ from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField from wtforms import StringField, TextAreaField, SubmitField
from wtforms.validators import InputRequired, Length, Optional, Regexp from wtforms.validators import InputRequired, Length, Optional, Regexp
from app.utils import rank_required from app.utils import rank_required, normalize_line_endings
from . import bp from . import bp
from app.models import UserRank, ContentWarning, db from app.models import UserRank, ContentWarning, db
@ -33,7 +33,7 @@ def warning_list():
class WarningForm(FlaskForm): class WarningForm(FlaskForm):
title = StringField("Title", [InputRequired(), Length(3, 100)]) title = StringField("Title", [InputRequired(), Length(3, 100)])
description = TextAreaField("Description", [Optional(), Length(0, 500)]) description = TextAreaField("Description", [Optional(), Length(0, 500)], filters=[normalize_line_endings])
name = StringField("Name", [Optional(), Length(1, 20), name = StringField("Name", [Optional(), Length(1, 20),
Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")]) Regexp("^[a-z0-9_]", 0, "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
submit = SubmitField("Save") submit = SubmitField("Save")

@ -25,7 +25,7 @@ from wtforms import StringField, BooleanField, SubmitField, FieldList, HiddenFie
from wtforms.validators import InputRequired, Length, Optional, Regexp from wtforms.validators import InputRequired, Length, Optional, Regexp
from app.models import Collection, db, Package, Permission, CollectionPackage, User, UserRank, AuditSeverity from app.models import Collection, db, Package, Permission, CollectionPackage, User, UserRank, AuditSeverity
from app.utils import nonempty_or_none from app.utils import nonempty_or_none, normalize_line_endings
from app.utils.models import is_package_page, add_audit_log, create_session from app.utils.models import is_package_page, add_audit_log, create_session
bp = Blueprint("collections", __name__) bp = Blueprint("collections", __name__)
@ -78,7 +78,7 @@ class CollectionForm(FlaskForm):
name = StringField("URL", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0, name = StringField("URL", [Optional(), Length(1, 20), Regexp("^[a-z0-9_]", 0,
"Lower case letters (a-z), digits (0-9), and underscores (_) only")]) "Lower case letters (a-z), digits (0-9), and underscores (_) only")])
short_description = StringField(lazy_gettext("Short Description"), [Optional(), Length(0, 200)]) short_description = StringField(lazy_gettext("Short Description"), [Optional(), Length(0, 200)])
long_description = TextAreaField(lazy_gettext("Page Content"), [Optional()], filters=[nonempty_or_none]) long_description = TextAreaField(lazy_gettext("Page Content"), [Optional()], filters=[nonempty_or_none, normalize_line_endings])
private = BooleanField(lazy_gettext("Private")) private = BooleanField(lazy_gettext("Private"))
pinned = BooleanField(lazy_gettext("Pinned to my profile")) pinned = BooleanField(lazy_gettext("Pinned to my profile"))
descriptions = FieldList( descriptions = FieldList(

@ -44,7 +44,8 @@ from app.models import Package, Tag, db, User, Tags, PackageState, Permission, P
PackageScreenshot, NotificationType, AuditLogEntry, PackageAlias, PackageProvides, PackageGameSupport, \ PackageScreenshot, NotificationType, AuditLogEntry, PackageAlias, PackageProvides, PackageGameSupport, \
PackageDailyStats, Collection PackageDailyStats, Collection
from app.utils import is_user_bot, get_int_or_abort, is_package_page, abs_url_for, add_audit_log, get_package_by_info, \ from app.utils import is_user_bot, get_int_or_abort, is_package_page, abs_url_for, add_audit_log, get_package_by_info, \
add_notification, get_system_user, rank_required, get_games_from_csv, get_daterange_options, post_to_approval_thread add_notification, get_system_user, rank_required, get_games_from_csv, get_daterange_options, \
post_to_approval_thread, normalize_line_endings
from app.logic.package_approval import validate_package_for_approval, can_move_to_state from app.logic.package_approval import validate_package_for_approval, can_move_to_state
from app.logic.game_support import game_support_set from app.logic.game_support import game_support_set
@ -238,7 +239,7 @@ class PackageForm(FlaskForm):
license = QuerySelectField(lazy_gettext("License"), [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) license = QuerySelectField(lazy_gettext("License"), [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
media_license = QuerySelectField(lazy_gettext("Media License"), [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) media_license = QuerySelectField(lazy_gettext("Media License"), [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
desc = TextAreaField(lazy_gettext("Long Description (Markdown)"), [Optional(), Length(0,10000)]) desc = TextAreaField(lazy_gettext("Long Description (Markdown)"), [Optional(), Length(0,10000)], filters=[normalize_line_endings])
repo = StringField(lazy_gettext("VCS Repository URL"), [Optional(), URL()], filters = [lambda x: x or None]) repo = StringField(lazy_gettext("VCS Repository URL"), [Optional(), URL()], filters = [lambda x: x or None])
website = StringField(lazy_gettext("Website URL"), [Optional(), URL()], filters = [lambda x: x or None]) website = StringField(lazy_gettext("Website URL"), [Optional(), URL()], filters = [lambda x: x or None])

@ -29,7 +29,7 @@ from app.models import db, PackageReview, Thread, ThreadReply, NotificationType,
Permission, AuditSeverity, PackageState, Language Permission, AuditSeverity, PackageState, Language
from app.tasks.webhooktasks import post_discord_webhook from app.tasks.webhooktasks import post_discord_webhook
from app.utils import is_package_page, add_notification, get_int_or_abort, is_yes, is_safe_url, rank_required, \ from app.utils import is_package_page, add_notification, get_int_or_abort, is_yes, is_safe_url, rank_required, \
add_audit_log, has_blocked_domains, should_return_json add_audit_log, has_blocked_domains, should_return_json, normalize_line_endings
from . import bp from . import bp
@ -57,7 +57,7 @@ class ReviewForm(FlaskForm):
get_pk=lambda a: a.id, get_pk=lambda a: a.id,
get_label=lambda a: a.title, get_label=lambda a: a.title,
default=get_default_language) default=get_default_language)
comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(10, 2000)]) comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(10, 2000)], filters=[normalize_line_endings])
rating = RadioField(lazy_gettext("Rating"), [InputRequired()], rating = RadioField(lazy_gettext("Rating"), [InputRequired()],
choices=[("5", lazy_gettext("Yes")), ("3", lazy_gettext("Neutral")), ("1", lazy_gettext("No"))]) choices=[("5", lazy_gettext("Yes")), ("3", lazy_gettext("Neutral")), ("1", lazy_gettext("No"))])
btn_submit = SubmitField(lazy_gettext("Save")) btn_submit = SubmitField(lazy_gettext("Save"))

@ -25,13 +25,13 @@ from wtforms.validators import InputRequired, Length
from app.models import User, UserRank from app.models import User, UserRank
from app.tasks.emails import send_user_email from app.tasks.emails import send_user_email
from app.tasks.webhooktasks import post_discord_webhook from app.tasks.webhooktasks import post_discord_webhook
from app.utils import is_no, abs_url_samesite from app.utils import is_no, abs_url_samesite, normalize_line_endings
bp = Blueprint("report", __name__) bp = Blueprint("report", __name__)
class ReportForm(FlaskForm): class ReportForm(FlaskForm):
message = TextAreaField(lazy_gettext("Message"), [InputRequired(), Length(10, 10000)]) message = TextAreaField(lazy_gettext("Message"), [InputRequired(), Length(10, 10000)], filters=[normalize_line_endings])
submit = SubmitField(lazy_gettext("Report")) submit = SubmitField(lazy_gettext("Report"))

@ -16,8 +16,7 @@
from flask import Blueprint, request, render_template, abort, flash, redirect, url_for from flask import Blueprint, request, render_template, abort, flash, redirect, url_for
from flask_babel import gettext, lazy_gettext from flask_babel import gettext, lazy_gettext
from sqlalchemy import or_ from sqlalchemy.orm import selectinload
from sqlalchemy.orm import selectinload, joinedload
from app.markdown import get_user_mentions, render_markdown from app.markdown import get_user_mentions, render_markdown
from app.tasks.webhooktasks import post_discord_webhook from app.tasks.webhooktasks import post_discord_webhook
@ -27,7 +26,8 @@ bp = Blueprint("threads", __name__)
from flask_login import current_user, login_required from flask_login import current_user, login_required
from app.models import Package, db, User, Permission, Thread, UserRank, AuditSeverity, \ from app.models import Package, db, User, Permission, Thread, UserRank, AuditSeverity, \
NotificationType, ThreadReply NotificationType, ThreadReply
from app.utils import add_notification, is_yes, add_audit_log, get_system_user, has_blocked_domains from app.utils import add_notification, is_yes, add_audit_log, get_system_user, has_blocked_domains, \
normalize_line_endings
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SubmitField, BooleanField from wtforms import StringField, TextAreaField, SubmitField, BooleanField
from wtforms.validators import InputRequired, Length from wtforms.validators import InputRequired, Length
@ -178,7 +178,7 @@ def delete_reply(id):
class CommentForm(FlaskForm): class CommentForm(FlaskForm):
comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(2, 2000)]) comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(2, 2000)], filters=[normalize_line_endings])
btn_submit = SubmitField(lazy_gettext("Comment")) btn_submit = SubmitField(lazy_gettext("Comment"))
@ -279,7 +279,7 @@ def view(id):
class ThreadForm(FlaskForm): class ThreadForm(FlaskForm):
title = StringField(lazy_gettext("Title"), [InputRequired(), Length(3,100)]) title = StringField(lazy_gettext("Title"), [InputRequired(), Length(3,100)])
comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(10, 2000)]) comment = TextAreaField(lazy_gettext("Comment"), [InputRequired(), Length(10, 2000)], filters=[normalize_line_endings])
private = BooleanField(lazy_gettext("Private")) private = BooleanField(lazy_gettext("Private"))
btn_submit = SubmitField(lazy_gettext("Open Thread")) btn_submit = SubmitField(lazy_gettext("Open Thread"))

@ -52,6 +52,13 @@ def nonempty_or_none(str):
return str return str
def normalize_line_endings(value: Optional[str]) -> Optional[str]:
if value is None:
return None
return value.replace("\r\n", "\n").strip() + "\n"
def should_return_json(): def should_return_json():
return "application/json" in request.accept_mimetypes and \ return "application/json" in request.accept_mimetypes and \
not "text/html" in request.accept_mimetypes not "text/html" in request.accept_mimetypes