diff --git a/app/blueprints/packages/reviews.py b/app/blueprints/packages/reviews.py index f9cf631c..b3ae9697 100644 --- a/app/blueprints/packages/reviews.py +++ b/app/blueprints/packages/reviews.py @@ -26,7 +26,8 @@ from wtforms import * from wtforms.validators import * from app.models import db, PackageReview, Thread, ThreadReply, NotificationType, PackageReviewVote, Package, UserRank, \ Permission, AuditSeverity, PackageState -from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, addAuditLog +from app.utils import is_package_page, addNotification, get_int_or_abort, isYes, is_safe_url, rank_required, \ + addAuditLog, has_blocked_domains from app.tasks.webhooktasks import post_discord_webhook @@ -73,61 +74,64 @@ def review(package): # Validate and submit elif can_review and form.validate_on_submit(): - was_new = False - if not review: - was_new = True - review = PackageReview() - review.package = package - review.author = current_user - db.session.add(review) - - review.recommends = form.recommends.data == "yes" - - thread = review.thread - if not thread: - thread = Thread() - thread.author = current_user - thread.private = False - thread.package = package - thread.review = review - db.session.add(thread) - - thread.watchers.append(current_user) - - reply = ThreadReply() - reply.thread = thread - reply.author = current_user - reply.comment = form.comment.data - db.session.add(reply) - - thread.replies.append(reply) + if has_blocked_domains(form.comment.data, current_user.username, f"review of {package.getId()}"): + flash(gettext("Linking to malicious sites is not allowed."), "danger") else: - reply = thread.first_reply - reply.comment = form.comment.data + was_new = False + if not review: + was_new = True + review = PackageReview() + review.package = package + review.author = current_user + db.session.add(review) - thread.title = form.title.data + review.recommends = form.recommends.data == "yes" - db.session.commit() + thread = review.thread + if not thread: + thread = Thread() + thread.author = current_user + thread.private = False + thread.package = package + thread.review = review + db.session.add(thread) - package.recalcScore() + thread.watchers.append(current_user) - if was_new: - notif_msg = "New review '{}'".format(form.title.data) - type = NotificationType.NEW_REVIEW - else: - notif_msg = "Updated review '{}'".format(form.title.data) - type = NotificationType.OTHER + reply = ThreadReply() + reply.thread = thread + reply.author = current_user + reply.comment = form.comment.data + db.session.add(reply) - addNotification(package.maintainers, current_user, type, notif_msg, - url_for("threads.view", id=thread.id), package) + thread.replies.append(reply) + else: + reply = thread.first_reply + reply.comment = form.comment.data - if was_new: - post_discord_webhook.delay(thread.author.username, - "Reviewed {}: {}".format(package.title, thread.getViewURL(absolute=True)), False) + thread.title = form.title.data - db.session.commit() + db.session.commit() - return redirect(package.getURL("packages.view")) + package.recalcScore() + + if was_new: + notif_msg = "New review '{}'".format(form.title.data) + type = NotificationType.NEW_REVIEW + else: + notif_msg = "Updated review '{}'".format(form.title.data) + type = NotificationType.OTHER + + addNotification(package.maintainers, current_user, type, notif_msg, + url_for("threads.view", id=thread.id), package) + + if was_new: + post_discord_webhook.delay(thread.author.username, + "Reviewed {}: {}".format(package.title, thread.getViewURL(absolute=True)), False) + + db.session.commit() + + return redirect(package.getURL("packages.view")) return render_template("packages/review_create_edit.html", form=form, package=package, review=review) @@ -217,7 +221,6 @@ def review_vote(package, review_id): return redirect(review.thread.getViewURL()) - @bp.route("/packages///review-votes/") @rank_required(UserRank.ADMIN) @is_package_page diff --git a/app/blueprints/threads/__init__.py b/app/blueprints/threads/__init__.py index abce922a..4b1a7b9d 100644 --- a/app/blueprints/threads/__init__.py +++ b/app/blueprints/threads/__init__.py @@ -23,7 +23,7 @@ bp = Blueprint("threads", __name__) from flask_login import current_user, login_required from app.models import * -from app.utils import addNotification, isYes, addAuditLog, get_system_user, rank_required +from app.utils import addNotification, isYes, addAuditLog, get_system_user, rank_required, has_blocked_domains from flask_wtf import FlaskForm from wtforms import * from wtforms.validators import * @@ -189,7 +189,7 @@ def edit_reply(id): if reply_id is None: abort(404) - reply = ThreadReply.query.get(reply_id) + reply: ThreadReply = ThreadReply.query.get(reply_id) if reply is None or reply.thread != thread: abort(404) @@ -199,17 +199,19 @@ def edit_reply(id): form = CommentForm(formdata=request.form, obj=reply) if form.validate_on_submit(): comment = form.comment.data + if has_blocked_domains(comment, current_user.username, f"edit to reply {reply.get_url(True)}"): + flash(gettext("Linking to malicious sites is not allowed."), "danger") + else: + msg = "Edited reply by {}".format(reply.author.display_name) + severity = AuditSeverity.NORMAL if current_user == reply.author else AuditSeverity.MODERATION + addNotification(reply.author, current_user, NotificationType.OTHER, msg, thread.getViewURL(), thread.package) + addAuditLog(severity, current_user, msg, thread.getViewURL(), thread.package, reply.comment) - msg = "Edited reply by {}".format(reply.author.display_name) - severity = AuditSeverity.NORMAL if current_user == reply.author else AuditSeverity.MODERATION - addNotification(reply.author, current_user, NotificationType.OTHER, msg, thread.getViewURL(), thread.package) - addAuditLog(severity, current_user, msg, thread.getViewURL(), thread.package, reply.comment) + reply.comment = comment - reply.comment = comment + db.session.commit() - db.session.commit() - - return redirect(thread.getViewURL()) + return redirect(thread.getViewURL()) return render_template("threads/edit_reply.html", thread=thread, reply=reply, form=form) @@ -230,6 +232,10 @@ def view(id): flash(gettext("Please wait before commenting again"), "danger") return redirect(thread.getViewURL()) + if has_blocked_domains(comment, current_user.username, f"reply to {thread.getViewURL(True)}"): + flash(gettext("Linking to malicious sites is not allowed."), "danger") + return render_template("threads/view.html", thread=thread, form=form) + reply = ThreadReply() reply.author = current_user reply.comment = comment @@ -318,55 +324,58 @@ def new(): # Validate and submit elif form.validate_on_submit(): - thread = Thread() - thread.author = current_user - thread.title = form.title.data - thread.private = form.private.data if allow_private_change else def_is_private - thread.package = package - db.session.add(thread) + if has_blocked_domains(form.comment.data, current_user.username, f"new thread"): + flash(gettext("Linking to malicious sites is not allowed."), "danger") + else: + thread = Thread() + thread.author = current_user + thread.title = form.title.data + thread.private = form.private.data if allow_private_change else def_is_private + thread.package = package + db.session.add(thread) - thread.watchers.append(current_user) - if package and package.author != current_user: - thread.watchers.append(package.author) + thread.watchers.append(current_user) + if package and package.author != current_user: + thread.watchers.append(package.author) - reply = ThreadReply() - reply.thread = thread - reply.author = current_user - reply.comment = form.comment.data - db.session.add(reply) + reply = ThreadReply() + reply.thread = thread + reply.author = current_user + reply.comment = form.comment.data + db.session.add(reply) - thread.replies.append(reply) + thread.replies.append(reply) - db.session.commit() + db.session.commit() - if is_review_thread: - package.review_thread = thread + if is_review_thread: + package.review_thread = thread - for mentioned_username in get_user_mentions(render_markdown(form.comment.data)): - mentioned = User.query.filter_by(username=mentioned_username).first() - if mentioned is None: - continue + for mentioned_username in get_user_mentions(render_markdown(form.comment.data)): + mentioned = User.query.filter_by(username=mentioned_username).first() + if mentioned is None: + continue - msg = "Mentioned by {} in new thread '{}'".format(current_user.display_name, thread.title) - addNotification(mentioned, current_user, NotificationType.NEW_THREAD, - msg, thread.getViewURL(), thread.package) + msg = "Mentioned by {} in new thread '{}'".format(current_user.display_name, thread.title) + addNotification(mentioned, current_user, NotificationType.NEW_THREAD, + msg, thread.getViewURL(), thread.package) - thread.watchers.append(mentioned) + thread.watchers.append(mentioned) - notif_msg = "New thread '{}'".format(thread.title) - if package is not None: - addNotification(package.maintainers, current_user, NotificationType.NEW_THREAD, notif_msg, thread.getViewURL(), package) + notif_msg = "New thread '{}'".format(thread.title) + if package is not None: + addNotification(package.maintainers, current_user, NotificationType.NEW_THREAD, notif_msg, thread.getViewURL(), package) - approvers = User.query.filter(User.rank >= UserRank.APPROVER).all() - addNotification(approvers, current_user, NotificationType.EDITOR_MISC, notif_msg, thread.getViewURL(), package) + approvers = User.query.filter(User.rank >= UserRank.APPROVER).all() + addNotification(approvers, current_user, NotificationType.EDITOR_MISC, notif_msg, thread.getViewURL(), package) - if is_review_thread: - post_discord_webhook.delay(current_user.username, - "Opened approval thread: {}".format(thread.getViewURL(absolute=True)), True) + if is_review_thread: + post_discord_webhook.delay(current_user.username, + "Opened approval thread: {}".format(thread.getViewURL(absolute=True)), True) - db.session.commit() + db.session.commit() - return redirect(thread.getViewURL()) + return redirect(thread.getViewURL()) return render_template("threads/new.html", form=form, allow_private_change=allow_private_change, package=package) diff --git a/app/blueprints/users/settings.py b/app/blueprints/users/settings.py index 357e9221..05131b0e 100644 --- a/app/blueprints/users/settings.py +++ b/app/blueprints/users/settings.py @@ -7,7 +7,7 @@ from wtforms import * from wtforms.validators import * from app.models import * -from app.utils import nonEmptyOrNone, addAuditLog, randomString, rank_required +from app.utils import nonEmptyOrNone, addAuditLog, randomString, rank_required, has_blocked_domains from app.tasks.emails import send_verify_email from . import bp @@ -53,7 +53,7 @@ class UserProfileForm(FlaskForm): submit = SubmitField(lazy_gettext("Save")) -def handle_profile_edit(form, user, username): +def handle_profile_edit(form: UserProfileForm, user: User, username: str): severity = AuditSeverity.NORMAL if current_user == user else AuditSeverity.MODERATION addAuditLog(severity, current_user, "Edited {}'s profile".format(user.display_name), url_for("users.profile", username=username)) @@ -80,8 +80,13 @@ def handle_profile_edit(form, user, username): url_for("users.profile", username=username)) if user.checkPerm(current_user, Permission.CHANGE_PROFILE_URLS): - user.website_url = form["website_url"].data - user.donate_url = form["donate_url"].data + if has_blocked_domains(form.website_url.data, current_user.username, f"{user.username}'s website_url") or \ + has_blocked_domains(form.donate_url.data, current_user.username, f"{user.username}'s donate_url"): + flash(gettext("Linking to malicious sites is not allowed."), "danger") + return + + user.website_url = form.website_url.data + user.donate_url = form.donate_url.data db.session.commit() diff --git a/app/logic/packages.py b/app/logic/packages.py index d5e7675e..11203d8a 100644 --- a/app/logic/packages.py +++ b/app/logic/packages.py @@ -22,7 +22,7 @@ from flask_babel import lazy_gettext from app.logic.LogicError import LogicError from app.models import User, Package, PackageType, MetaPackage, Tag, ContentWarning, db, Permission, AuditSeverity, \ License, UserRank, PackageDevState -from app.utils import addAuditLog +from app.utils import addAuditLog, has_blocked_domains from app.utils.url import clean_youtube_url @@ -118,6 +118,11 @@ def do_edit_package(user: User, package: Package, was_new: bool, was_web: bool, validate(data) + for field in ["short_desc", "desc", "website", "issueTracker", "repo", "video_url"]: + if field in data and has_blocked_domains(data[field], user.username, + f"{field} of {package.getId()}"): + raise LogicError(403, lazy_gettext("Linking to malicious sites is not allowed.")) + if "type" in data: data["type"] = PackageType.coerce(data["type"]) diff --git a/app/models/threads.py b/app/models/threads.py index 3190c044..2ca1d7b4 100644 --- a/app/models/threads.py +++ b/app/models/threads.py @@ -144,8 +144,8 @@ class ThreadReply(db.Model): created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow) - def get_url(self): - return url_for('threads.view', id=self.thread.id) + "#reply-" + str(self.id) + def get_url(self, absolute=False): + return self.thread.getViewURL(absolute) + "#reply-" + str(self.id) def checkPerm(self, user, perm): if not user.is_authenticated: diff --git a/app/utils/__init__.py b/app/utils/__init__.py index d77b69de..3c73fa69 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -20,6 +20,7 @@ import secrets from .flask import * from .models import * from .user import * +from flask import current_app YESES = ["yes", "true", "1", "on"] @@ -51,3 +52,19 @@ def shouldReturnJson(): def randomString(n): return secrets.token_hex(int(n / 2)) + + +def has_blocked_domains(text: str, username: str, location: str) -> bool: + if text is None: + return False + + blocked_domains = current_app.config["BLOCKED_DOMAINS"] + for domain in blocked_domains: + if domain in text: + from app.tasks.webhooktasks import post_discord_webhook + post_discord_webhook.delay(username, + f"Attempted to post link to blocked domain {domain} in {location}", + True) + return True + + return False diff --git a/config.example.cfg b/config.example.cfg index 31b820b8..2f5de9f8 100644 --- a/config.example.cfg +++ b/config.example.cfg @@ -34,6 +34,4 @@ DISCORD_WEBHOOK_QUEUE = None TEMPLATES_AUTO_RELOAD = False LOG_SQL = False -LANGUAGES = { - 'en': 'English', -} +BLOCKED_DOMAINS = []