Files
contentdb/app/blueprints/report/__init__.py
rubenwardy d6e39fb896 Allow editors to see reports
Some reports are about packages and not users
2025-09-23 09:52:39 +01:00

183 lines
6.5 KiB
Python

# ContentDB
# Copyright (C) 2022 rubenwardy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import Blueprint, request, render_template, url_for, abort, flash
from flask_babel import lazy_gettext
from flask_login import current_user
from flask_wtf import FlaskForm
from werkzeug.utils import redirect
from wtforms import TextAreaField, SubmitField, URLField, StringField, SelectField, FileField
from wtforms.validators import InputRequired, Length, Optional, DataRequired
from app.logic.uploads import upload_file
from app.models import User, UserRank, Report, db, AuditSeverity, ReportCategory, Thread, Permission, ReportAttachment
from app.tasks.webhooktasks import post_discord_webhook
from app.utils import (is_no, abs_url_samesite, normalize_line_endings, rank_required, add_audit_log, abs_url_for,
random_string, add_replies)
bp = Blueprint("report", __name__)
class ReportForm(FlaskForm):
category = SelectField(lazy_gettext("Category"), [DataRequired()], choices=ReportCategory.choices(with_none=True), coerce=ReportCategory.coerce)
url = URLField(lazy_gettext("URL"), [Optional()])
title = StringField(lazy_gettext("Subject / Title"), [InputRequired(), Length(10, 300)])
message = TextAreaField(lazy_gettext("Message"), [Optional(), Length(0, 10000)], filters=[normalize_line_endings])
file_upload = FileField(lazy_gettext("Image Upload"), [Optional()])
submit = SubmitField(lazy_gettext("Report"))
@bp.route("/report/", methods=["GET", "POST"])
def report():
is_anon = not current_user.is_authenticated or not is_no(request.args.get("anon"))
url = request.args.get("url")
if url:
if url.startswith("/report/"):
abort(404)
url = abs_url_samesite(url)
form = ReportForm() if current_user.is_authenticated else None
if form and request.method == "GET":
try:
form.category.data = ReportCategory.coerce(request.args.get("category"))
except KeyError:
pass
form.url.data = url
form.title.data = request.args.get("title", "")
if form and form.validate_on_submit():
report = Report()
report.id = random_string(8)
report.user = current_user if current_user.is_authenticated else None
form.populate_obj(report)
if current_user.is_authenticated:
thread = Thread()
thread.title = f"Report: {form.title.data}"
thread.author = current_user
thread.private = True
thread.watchers.extend(User.query.filter(User.rank >= UserRank.MODERATOR).all())
db.session.add(thread)
db.session.flush()
report.thread = thread
add_replies(thread, current_user, f"**{report.category.title} report created**\n\n{form.message.data}")
else:
ip_addr = request.headers.get("X-Forwarded-For") or request.remote_addr
report.message = ip_addr + "\n\n" + report.message
db.session.add(report)
db.session.flush()
if form.file_upload.data:
atmt = ReportAttachment()
report.attachments.add(atmt)
uploaded_url, _ = upload_file(form.file_upload.data, "image", lazy_gettext("a PNG, JPEG, or WebP image file"))
atmt.url = uploaded_url
db.session.add(atmt)
if current_user.is_authenticated:
add_audit_log(AuditSeverity.USER, current_user, f"New report: {report.title}",
url_for("report.view", rid=report.id))
db.session.commit()
abs_url = abs_url_for("report.view", rid=report.id)
msg = f"**New Report**\nReport on `{report.url}`\n\n{report.title}\n\nView: {abs_url}"
post_discord_webhook.delay(None if is_anon else current_user.username, msg, True)
return redirect(url_for("report.report_received", rid=report.id))
return render_template("report/report.html", form=form, url=url, is_anon=is_anon, noindex=url is not None)
@bp.route("/report/received/")
def report_received():
rid = request.args.get("rid")
report = Report.query.get_or_404(rid)
return render_template("report/report_received.html", report=report)
@bp.route("/admin/reports/")
@rank_required(UserRank.EDITOR)
def list_all():
reports = Report.query.order_by(db.asc(Report.is_resolved), db.asc(Report.created_at)).all()
return render_template("report/list.html", reports=reports)
@bp.route("/admin/reports/<rid>/", methods=["GET", "POST"])
def view(rid: str):
report = Report.query.get_or_404(rid)
if not report.check_perm(current_user, Permission.SEE_REPORT):
abort(404)
if request.method == "POST":
if report.is_resolved:
if "reopen" in request.form:
report.is_resolved = False
url = url_for("report.view", rid=report.id)
add_audit_log(AuditSeverity.MODERATION, current_user, f"Reopened report \"{report.title}\"", url)
if report.thread:
add_replies(report.thread, current_user, f"Reopened report", is_status_update=True)
db.session.commit()
else:
if "completed" in request.form:
outcome = "as completed"
elif "removed" in request.form:
outcome = "as content removed"
elif "invalid" in request.form:
outcome = "without action"
if report.thread:
flash("Make sure to comment why the report is invalid in the thread", "warning")
else:
abort(400)
report.is_resolved = True
url = url_for("report.view", rid=report.id)
add_audit_log(AuditSeverity.MODERATION, current_user, f"Report closed {outcome} \"{report.title}\"", url)
if report.thread:
add_replies(report.thread, current_user, f"Closed report {outcome}", is_status_update=True)
db.session.commit()
return render_template("report/view.html", report=report)
@bp.route("/admin/reports/<rid>/edit/", methods=["GET", "POST"])
def edit(rid: str):
report = Report.query.get_or_404(rid)
if not report.check_perm(current_user, Permission.SEE_REPORT):
abort(404)
form = ReportForm(request.form, obj=report)
form.submit.label.text = lazy_gettext("Save")
if form.validate_on_submit():
form.populate_obj(report)
url = url_for("report.view", rid=report.id)
add_audit_log(AuditSeverity.MODERATION, current_user, f"Edited report \"{report.title}\"", url)
db.session.commit()
return redirect(url_for("report.view", rid=report.id))
return render_template("report/edit.html", report=report, form=form)