mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-12-07 14:09:47 +00:00
- Implementation of milestone 6. from **Task F. Moderation features: Reporting** (part of [amendment of the workplan](https://codeberg.org/forgejo/sustainability/src/branch/main/2022-12-01-nlnet/2025-02-07-extended-workplan.md#task-f-moderation-features-reporting) for NLnet 2022-12-035): `6. Forgejo admins can perform common actions on the listed reports (content deletion, locking of user account)` --- Follow-up of !7905 (and !6977) --- This adds some action buttons within the _Moderation reports_ section (/admin/moderation/reports) within the _Site administration_ page, so that administrators can: - mark a report as Handled or as Ignored (without performing any action on the reported content); - mark a user account as suspended (set `prohibit_login` = true); - delete (and purge) a user / organization and mark the linked reports as Handled; - delete a repository and mark the linked reports as Handled; - delete an issue / pull request and mark the linked reports as Handled; - delete a comment and mark the linked reports as Handled; The buttons were added on the sight side of each report from the overview, below the existing counter (that show how many times the content was reported and opens the details page). Only the buttons for updating the status of the report are directly visible - as `✓` and `✗` icons with some tooltips - while the content actions are hidden under a `⋯` dropdown. The implementation was done using HTMX so that the page is not refreshed after each action. Some discussions regarding the UI/UX started with https://codeberg.org/forgejo/design/issues/30#issuecomment-5958634 ### Manual testing - First make sure that moderation in enabled ([moderation] ENABLED config is set as true within app.ini). - Report multiple users, organizations, repositories, issues, pull requests and comments. - Go to _Moderation reports_ overview section section and make sure the buttons are visible; - The `✓` and `✗` should be available for each shown report; - The horizontal dropdown menu (`⋯`) should not be visible for reports linked to already deleted content. - The actions available within the dropdown menu should correspond to the reported content type (e.g. 'Suspend account' and 'Delete account' for users/organizations, 'Delete repository' for repositories, etc.). - When an action is successful a flash message should be displayed above the overview. - Warnings should be displayed (as flash messages) when trying to suspend or delete your account (in case someone reported you) or an organization. - An info (flash message) should be displayed when trying to suspend a user that is already suspended. - Mark a report as Handled / Ignored and observe that a success flash message confirms the action and the report is removed from the list without reloading the page; - Refresh the page to make sure the report will not be loaded again (also check in the DB that the status was updated and the resolved timestamp is correctly set). - Suspend a user account and make sure the report remains in the list (it is not resolved); - Make sure the above user gets the 'Suspended account' notice after login. - Delete a user account and observe that a success flash message confirms the action and the report is removed from the list without reloading the page; - Make sure that all owned organizations and repositories as well as all the issues, PRs and comments posted in other repositories were deleted; - Make sure the linked abuse reports are marked as Handled (and resolved timestamp is set). - Delete an organization and make sure that owned repositories were also deleted. - Similarly, delete a repository / issue / PR / comment and check that the contents are not available any more and the linked reports are resolved. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8716 Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: floss4good <floss4good@disroot.org> Co-committed-by: floss4good <floss4good@disroot.org>
152 lines
5.2 KiB
Go
152 lines
5.2 KiB
Go
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package moderation
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"forgejo.org/models/db"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/timeutil"
|
|
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
type AbuseReportDetailed struct {
|
|
AbuseReport `xorm:"extends"`
|
|
ReportedTimes int // only for overview
|
|
ReporterName string
|
|
ContentReference string
|
|
ShadowCopyDate timeutil.TimeStamp // only for details
|
|
ShadowCopyRawValue string // only for details
|
|
}
|
|
|
|
func (ard AbuseReportDetailed) ContentTypeIconName() string {
|
|
switch ard.ContentType {
|
|
case ReportedContentTypeUser:
|
|
return "octicon-person"
|
|
case ReportedContentTypeRepository:
|
|
return "octicon-repo"
|
|
case ReportedContentTypeIssue:
|
|
return "octicon-issue-opened"
|
|
case ReportedContentTypeComment:
|
|
return "octicon-comment"
|
|
default:
|
|
return "octicon-question"
|
|
}
|
|
}
|
|
|
|
func (ard AbuseReportDetailed) ContentURL() string {
|
|
switch ard.ContentType {
|
|
case ReportedContentTypeUser:
|
|
return strings.TrimLeft(ard.ContentReference, "@")
|
|
case ReportedContentTypeIssue:
|
|
return strings.ReplaceAll(ard.ContentReference, "#", "/issues/")
|
|
default:
|
|
return ard.ContentReference
|
|
}
|
|
}
|
|
|
|
func (ard AbuseReportDetailed) ReportedContentIsUser() bool {
|
|
return ard.ContentType == ReportedContentTypeUser
|
|
}
|
|
|
|
func (ard AbuseReportDetailed) ReportedContentIsRepo() bool {
|
|
return ard.ContentType == ReportedContentTypeRepository
|
|
}
|
|
|
|
func (ard AbuseReportDetailed) ReportedContentIsIssue() bool {
|
|
return ard.ContentType == ReportedContentTypeIssue
|
|
}
|
|
|
|
func (ard AbuseReportDetailed) ReportedContentIsComment() bool {
|
|
return ard.ContentType == ReportedContentTypeComment
|
|
}
|
|
|
|
func GetOpenReports(ctx context.Context) ([]*AbuseReportDetailed, error) {
|
|
var reports []*AbuseReportDetailed
|
|
|
|
// - For PostgreSQL user table name should be escaped.
|
|
// - Escaping can be done with double quotes (") but this doesn't work for MariaDB.
|
|
// - For SQLite index column name should be escaped.
|
|
// - Escaping can be done with double quotes (") or backticks (`).
|
|
// - For MariaDB/MySQL there is no need to escape the above.
|
|
// - Therefore we will use double quotes (") but only for PostgreSQL and SQLite.
|
|
// - Also, note that builder.Union() is broken: gitea.com/xorm/builder/issues/71
|
|
identifierEscapeChar := ``
|
|
if setting.Database.Type.IsPostgreSQL() || setting.Database.Type.IsSQLite3() {
|
|
identifierEscapeChar = `"`
|
|
}
|
|
|
|
err := db.GetEngine(ctx).SQL(fmt.Sprintf(`SELECT AR.*, ARD.reported_times, U.name AS reporter_name, REFS.ref AS content_reference
|
|
FROM abuse_report AR
|
|
INNER JOIN (
|
|
SELECT min(id) AS id, count(id) AS reported_times
|
|
FROM abuse_report
|
|
WHERE status = %[2]d
|
|
GROUP BY content_type, content_id
|
|
) ARD ON ARD.id = AR.id
|
|
LEFT JOIN %[1]suser%[1]s U ON U.id = AR.reporter_id
|
|
LEFT JOIN (
|
|
SELECT %[3]d AS type, id, concat('@', name) AS "ref"
|
|
FROM %[1]suser%[1]s WHERE id IN (
|
|
SELECT content_id FROM abuse_report WHERE status = %[2]d AND content_type = %[3]d
|
|
)
|
|
UNION
|
|
SELECT %[4]d AS "type", id, concat(owner_name, '/', name) AS "ref"
|
|
FROM repository WHERE id IN (
|
|
SELECT content_id FROM abuse_report WHERE status = %[2]d AND content_type = %[4]d
|
|
)
|
|
UNION
|
|
SELECT %[5]d AS "type", I.id, concat(IR.owner_name, '/', IR.name, '#', I.%[1]sindex%[1]s) AS "ref"
|
|
FROM issue I
|
|
LEFT JOIN repository IR ON IR.id = I.repo_id
|
|
WHERE I.id IN (
|
|
SELECT content_id FROM abuse_report WHERE status = %[2]d AND content_type = %[5]d
|
|
)
|
|
UNION
|
|
SELECT %[6]d AS "type", C.id, concat(CIR.owner_name, '/', CIR.name, '/issues/', CI.%[1]sindex%[1]s, '#issuecomment-', C.id) AS "ref"
|
|
FROM comment C
|
|
LEFT JOIN issue CI ON CI.id = C.issue_id
|
|
LEFT JOIN repository CIR ON CIR.id = CI.repo_id
|
|
WHERE C.id IN (
|
|
SELECT content_id FROM abuse_report WHERE status = %[2]d AND content_type = %[6]d
|
|
)
|
|
) REFS ON REFS.type = AR.content_type AND REFS.id = AR.content_id
|
|
ORDER BY AR.created_unix ASC`, identifierEscapeChar, ReportStatusTypeOpen,
|
|
ReportedContentTypeUser, ReportedContentTypeRepository, ReportedContentTypeIssue, ReportedContentTypeComment)).
|
|
Find(&reports)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return reports, nil
|
|
}
|
|
|
|
func GetOpenReportsByTypeAndContentID(ctx context.Context, contentType ReportedContentType, contentID int64) ([]*AbuseReportDetailed, error) {
|
|
var reports []*AbuseReportDetailed
|
|
|
|
// Some remarks concerning PostgreSQL:
|
|
// - user table should be escaped (e.g. `user`);
|
|
// - tried to use aliases for table names but errors like 'pq: invalid reference to FROM-clause entry'
|
|
// or 'pq: missing FROM-clause entry' were returned;
|
|
err := db.GetEngine(ctx).
|
|
Select("abuse_report.*, `user`.name AS reporter_name, abuse_report_shadow_copy.created_unix AS shadow_copy_date, abuse_report_shadow_copy.raw_value AS shadow_copy_raw_value").
|
|
Table("abuse_report").
|
|
Join("LEFT", "user", "`user`.id = abuse_report.reporter_id").
|
|
Join("LEFT", "abuse_report_shadow_copy", "abuse_report_shadow_copy.id = abuse_report.shadow_copy_id").
|
|
Where(builder.Eq{
|
|
"content_type": contentType,
|
|
"content_id": contentID,
|
|
"status": ReportStatusTypeOpen,
|
|
}).
|
|
Asc("abuse_report.created_unix").
|
|
Find(&reports)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return reports, nil
|
|
}
|