Add new 'Report abuse' form and links for user profile, repository page, issues, pulls and comments.

This commit is contained in:
floss4good 2025-02-28 12:41:00 +02:00
parent bca6ac6f6c
commit 248e2d3861
No known key found for this signature in database
GPG key ID: 5B948B4F4DAF819D
18 changed files with 233 additions and 63 deletions

View file

@ -14,8 +14,8 @@ import (
// CommentData represents a trimmed down comment that is used for preserving
// only the fields needed for abusive content reports (mainly string fields).
type CommentData struct {
OriginalAuthor string // TODO: decide if this is needed
TreePath string // TODO: decide if this is needed
OriginalAuthor string // TODO: decide if this is useful
TreePath string // TODO: decide if this is useful
Content string
ContentVersion int
CreatedUnix timeutil.TimeStamp

View file

@ -25,8 +25,17 @@ const (
ReportStatusTypeIgnored // 3
)
// AbuseCategoryType defines the categories in which a user can include the reported content.
type AbuseCategoryType int //revive:disable-line:exported
type (
// AbuseCategoryType defines the categories in which a user can include the reported content.
AbuseCategoryType int //revive:disable-line:exported
// AbuseCategoryItem defines a pair of value and it's corresponding translation key
// (used when new reports are submitted).
AbuseCategoryItem struct {
Value AbuseCategoryType
TranslationKey string
}
)
const (
AbuseCategoryTypeSpam AbuseCategoryType = iota + 1 // 1
@ -35,6 +44,17 @@ const (
AbuseCategoryTypeOtherViolations // 4 (Other violations of platform rules)
)
// GetAbuseCategoriesList returns a list of pairs with the available abuse category types
// and their corresponding translation keys
func GetAbuseCategoriesList() []AbuseCategoryItem {
return []AbuseCategoryItem{
{AbuseCategoryTypeSpam, "moderation.abuse_category.spam"},
{AbuseCategoryTypeMalware, "moderation.abuse_category.malware"},
{AbuseCategoryTypeIllegalContent, "moderation.abuse_category.illegal_content"},
{AbuseCategoryTypeOtherViolations, "moderation.abuse_category.other_violations"},
}
}
// ReportedContentType defines the types of content that can be reported
// (i.e. user/organization profile, repository, issue/pull, comment).
type ReportedContentType int //revive:disable-line:exported
@ -97,6 +117,15 @@ func alreadyReportedBy(ctx context.Context, doerID int64, contentType ReportedCo
return reported
}
func ReportAbuse(ctx context.Context, report *AbuseReport) error {
if report.ContentType == ReportedContentTypeUser && report.ReporterID == report.ContentID {
return nil
}
return reportAbuse(ctx, report)
}
/*
// ReportUser creates a new abuse report regarding the user with the provided reportedUserID.
func ReportUser(ctx context.Context, reporterID int64, reportedUserID int64, remarks string) error {
if reporterID == reportedUserID {
@ -148,6 +177,7 @@ func ReportComment(ctx context.Context, reporterID int64, commentID int64, remar
return reportAbuse(ctx, report)
}
*/
func reportAbuse(ctx context.Context, report *AbuseReport) error {
if alreadyReportedBy(ctx, report.ReporterID, report.ContentType, report.ContentID) {
@ -156,8 +186,6 @@ func reportAbuse(ctx context.Context, report *AbuseReport) error {
}
report.Status = ReportStatusTypeOpen
report.Category = AbuseCategoryTypeOtherViolations // TODO: replace with user's selection
_, err := db.GetEngine(ctx).Insert(report)
return err

View file

@ -938,6 +938,8 @@ func UpdateUserCols(ctx context.Context, u *User, cols ...string) error {
return err
}
// If the user was reported as abusive and any of the columns being updated is relevant
// for moderation purposes a shadow copy should be created before first update.
if err := IfNeededCreateShadowCopyForUser(ctx, u, cols...); err != nil {
return err
}

View file

@ -3964,10 +3964,23 @@ filepreview.truncated = Preview has been truncated
report = Report
;already_reported = Already reported
report_abuse = Report abuse
report_comment = Report comment
report_content = Report content
report_abuse_form.header = Report abuse to administrator
report_abuse_form.details = This form should be used to report users who create spam profiles, repositories, issues, comments or behave inappropriately.
report_abuse_form.invalid = Invalid arguments
abuse_category = Category
abuse_category.placeholder = Please select a category
abuse_category.spam = Spam
abuse_category.malware = Malware
abuse_category.illegal_content = Illegal content
abuse_category.other_violations = Other violations of platform rules
report_remarks = Remarks
report_user = Report user
report_user.detail = Are you sure you that this user committed an abuse and you want to report them?<br>TODO: Reason dropdown<br>TODO: Remarks textarea
report_remarks.placeholder = Please provide some details regarding the abuse you are reporting.
submit_report = Submit report
reported_thank_you = Thank you for your report. An administrator will look into it shortly.
[translation_meta]
test = This is a test string. It is not displayed in Forgejo UI but is used for testing purposes. Feel free to enter "ok" to save time (or a fun fact of your choice) to hit that sweet 100% completion mark :)

View file

@ -0,0 +1,89 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package moderation
import (
"net/http"
"code.gitea.io/gitea/models/moderation"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
)
const (
tplSubmitAbuseReport base.TplName = "moderation/new_abuse_report"
)
// NewReport renders the page for new abuse reports.
func NewReport(ctx *context.Context) {
contentID := ctx.FormInt64("id")
if contentID <= 0 {
ctx.RenderWithErr(ctx.Tr("moderation.report_abuse_form.invalid"), tplSubmitAbuseReport, nil)
log.Warn("The content ID is expected to be an integer greater that 0; the provided value is %d.", contentID)
return
}
contentTypeString := ctx.FormString("type")
var contentType moderation.ReportedContentType
switch contentTypeString {
case "user", "org":
contentType = moderation.ReportedContentTypeUser
case "repo":
contentType = moderation.ReportedContentTypeRepository
case "issue", "pull":
contentType = moderation.ReportedContentTypeIssue
case "comment":
contentType = moderation.ReportedContentTypeComment
default:
ctx.RenderWithErr(ctx.Tr("moderation.report_abuse_form.invalid"), tplSubmitAbuseReport, nil)
log.Warn("The provided content type `%s` is not among the expected values.", contentTypeString)
return
}
setContextDataAndRender(ctx, contentType, contentID)
}
// setContextDataAndRender adds some values into context data and renders the new abuse report page.
func setContextDataAndRender(ctx *context.Context, contentType moderation.ReportedContentType, contentID int64) {
ctx.Data["Title"] = ctx.Tr("moderation.report_abuse")
ctx.Data["ContentID"] = contentID
ctx.Data["ContentType"] = contentType
ctx.Data["AbuseCategories"] = moderation.GetAbuseCategoriesList()
ctx.Data["CancelLink"] = ctx.Doer.DashboardLink()
ctx.HTML(http.StatusOK, tplSubmitAbuseReport)
}
// CreatePost handles the POST for creating a new abuse report.
func CreatePost(ctx *context.Context) {
form := *web.GetForm(ctx).(*forms.ReportAbuseForm)
if form.ContentID <= 0 || form.ContentType == 0 {
ctx.RenderWithErr(ctx.Tr("moderation.report_abuse_form.invalid"), tplSubmitAbuseReport, nil)
return
}
if ctx.HasError() {
setContextDataAndRender(ctx, form.ContentType, form.ContentID)
return
}
report := moderation.AbuseReport{
ReporterID: ctx.Doer.ID,
ContentType: form.ContentType,
ContentID: form.ContentID,
Category: form.AbuseCategory,
Remarks: form.Remarks,
}
if err := moderation.ReportAbuse(ctx, &report); err != nil {
ctx.ServerError("Something went wrong while trying to submit the new abuse report.", err)
return
}
ctx.Flash.Success(ctx.Tr("moderation.reported_thank_you"))
ctx.Redirect(ctx.Doer.DashboardLink())
}

View file

@ -13,7 +13,7 @@ import (
activities_model "code.gitea.io/gitea/models/activities"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/moderation"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base"
@ -360,8 +360,6 @@ func Action(ctx *context.Context) {
err = user_service.BlockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
case "unblock":
err = user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID)
case "report":
err = moderation.ReportUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID, "{remarks not implemented}")
}
if err != nil {

View file

@ -1,4 +1,5 @@
// Copyright 2017 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package web
@ -33,6 +34,7 @@ import (
"code.gitea.io/gitea/routers/web/feed"
"code.gitea.io/gitea/routers/web/healthcheck"
"code.gitea.io/gitea/routers/web/misc"
"code.gitea.io/gitea/routers/web/moderation"
"code.gitea.io/gitea/routers/web/org"
org_setting "code.gitea.io/gitea/routers/web/org/setting"
"code.gitea.io/gitea/routers/web/repo"
@ -479,6 +481,9 @@ func registerRoutes(m *web.Route) {
m.Get("/search", repo.SearchIssues)
}, reqSignIn)
m.Get("/-/abuse_reports/new", moderation.NewReport, reqSignIn)
m.Post("/-/abuse_reports/new", web.Bind(forms.ReportAbuseForm{}), moderation.CreatePost, reqSignIn)
m.Get("/pulls", reqSignIn, user.Pulls)
m.Get("/milestones", reqSignIn, reqMilestonesDashboardPageEnabled, user.Milestones)

View file

@ -0,0 +1,28 @@
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later
package forms
import (
"net/http"
"code.gitea.io/gitea/models/moderation"
"code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/context"
"code.forgejo.org/go-chi/binding"
)
// ReportAbuseForm is used to interact with the UI of the form that submits new abuse reports.
type ReportAbuseForm struct {
ContentID int64
ContentType moderation.ReportedContentType
AbuseCategory moderation.AbuseCategoryType `binding:"Required" locale:"moderation.abuse_category"`
Remarks string `binding:"Required;MinSize(20);MaxSize(500)" preprocess:"TrimSpace" locale:"moderation.report_remarks"`
}
// Validate validates the fields of ReportAbuseForm.
func (f *ReportAbuseForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetValidateContext(req)
return middleware.Validate(errs, ctx.Data, f, ctx.Locale)
}

View file

@ -1,4 +1,5 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
@ -216,6 +217,7 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
}
// ***** END: ExternalLoginUser *****
// If the user was reported as abusive, a shadow copy should be created before deletion.
if err = user_model.IfNeededCreateShadowCopyForUser(ctx, u); err != nil {
return err
}

View file

@ -0,0 +1,43 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content organization new org">
<div class="ui middle very relaxed page grid">
<div class="column">
<form class="ui form" action="{{.Link}}" method="post">
{{.CsrfTokenHtml}}
<h3 class="ui top attached header">
{{ctx.Locale.Tr "moderation.report_abuse_form.header"}}
</h3>
<div class="ui attached segment">
{{template "base/alert" .}}
<p class="ui center">{{ctx.Locale.Tr "moderation.report_abuse_form.details"}}</p>
<input type="hidden" name="content_id" value="{{.ContentID}}" />
<input type="hidden" name="content_type" value="{{.ContentType}}" />
<fieldset>
<label{{if .Err_AbuseCategory}} class="error"{{end}}>
{{ctx.Locale.Tr "moderation.abuse_category"}}
<select class="ui selection dropdown" id="abuse_category" name="abuse_category" required autofocus>
<option value="">{{ctx.Locale.Tr "moderation.abuse_category.placeholder"}}</option>
{{range $cat := .AbuseCategories}}
<option value="{{$cat.Value}}"{{if eq $.abuse_category $cat.Value}} selected{{end}}>{{ctx.Locale.Tr $cat.TranslationKey}}</option>
{{end}}
</select>
</label>
<label{{if .Err_Remarks}} class="error"{{end}}>
{{ctx.Locale.Tr "moderation.report_remarks"}}
<textarea id="remarks" name="remarks" required minlength="20" maxlength="500" placeholder="{{ctx.Locale.Tr "moderation.report_remarks.placeholder"}}">{{.remarks}}</textarea>
</label>
</fieldset>
<div class="divider"></div>
<div class="text right actions">
<a class="ui cancel button" href="{{$.CancelLink}}">{{ctx.Locale.Tr "cancel"}}</a>
<button class="ui primary button"{{if not .ContentID}} disabled{{end}}>{{ctx.Locale.Tr "moderation.submit_report"}}</button>
</div>
</div>
</form>
</div>
</div>
</div>
{{template "base/footer" .}}

View file

@ -67,6 +67,12 @@
{{if not $.DisableForks}}
{{template "repo/header_fork" $}}
{{end}}
<button class="ui small compact jump dropdown icon button"{{if not $.IsSigned}} disabled{{end}} data-tooltip-content="{{ctx.Locale.Tr "repo.more_operations"}}" aria-label="{{ctx.Locale.Tr "toggle_menu"}}">
{{svg "octicon-kebab-horizontal"}}
<div class="menu top left">
<a class="item context" href="/-/abuse_reports/new?type=repo&id={{$.Repository.ID}}">{{ctx.Locale.Tr "moderation.report_content"}}</a>
</div>
</button>
</div>
{{end}}
</div>

View file

@ -172,8 +172,6 @@
{{template "repo/issue/view_content/reference_issue_dialog" .}}
{{template "repo/issue/view_content/report_comment_dialog" .}}
<div class="tw-hidden" id="no-content">
<span class="no-content">{{ctx.Locale.Tr "repo.issues.no_content"}}</span>
</div>

View file

@ -24,8 +24,12 @@
{{end}}
{{end}}
{{if and .ctxData.IsSigned (not .IsCommentPoster)}}
{{$contentType := "comment"}}
{{if eq .item .ctxData.Issue}}
{{if .ctxData.Issue.IsPull}} {{$contentType = "pull"}} {{else}} {{$contentType = "issue"}} {{end}}
{{end}}
<div class="divider"></div>
<div class="item context js-aria-clickable report-comment" data-target="{{.item.HashTag}}-raw" data-modal="#report-comment-modal" data-repo-url="{{.ctxData.RepoLink}}" data-comment-id="{{.item.ID}}">{{ctx.Locale.Tr "moderation.report_abuse"}}</div>
<a class="item context" href="/-/abuse_reports/new?type={{$contentType}}&id={{.item.ID}}">{{ctx.Locale.Tr "moderation.report_content"}}</a>
{{end}}
</div>
</div>

View file

@ -1,18 +0,0 @@
<div class="ui small modal" id="report-comment-modal">
<div class="header">
{{ctx.Locale.Tr "moderation.report_comment"}}
</div>
<div class="content">
<form class="ui form" id="report-comment-form" action="" method="post">
{{.CsrfTokenHtml}}
<input type="hidden" name="comment-id" />
<div class="inline required field">
<label><strong>{{ctx.Locale.Tr "moderation.report_remarks"}}</strong></label>
<textarea name="remarks"></textarea>
</div>
<div class="text right">
<button class="ui primary button">{{ctx.Locale.Tr "moderation.report_comment"}}</button>
</div>
</form>
</div>
</div>

View file

@ -124,9 +124,7 @@
{{end}}
</li>
<li class="block" hx-target="#profile-avatar-card" hx-indicator="#profile-avatar-card">
<button type="submit" {{if $.IsReported}} disabled {{end}} class="ui basic orange button" data-modal-id="report-user" hx-post="{{.ContextUser.HomeLink}}?action=report" hx-confirm="-">
{{svg "octicon-blocked"}} {{ctx.Locale.Tr "moderation.report"}}
</button>
<a {{if $.IsReported}}disabled {{end}}class="ui basic orange button" href="/-/abuse_reports/new?type=user&id={{.ContextUser.ID}}">{{ctx.Locale.Tr "moderation.report_abuse"}}</a>
</li>
{{end}}
</ul>

View file

@ -77,14 +77,4 @@
{{template "base/modal_actions_confirm" .}}
</div>
<div class="ui g-modal-confirm delete modal" id="report-user">
<div class="header">
{{ctx.Locale.Tr "moderation.report_user"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "moderation.report_user.detail"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
{{template "base/footer" .}}

View file

@ -599,21 +599,6 @@ export function initRepoIssueReferenceIssue() {
});
}
export function initRepoIssueReportComment() {
// Report abusive comment
$(document).on('click', '.report-comment', function (event) {
const $this = $(this);
const repo_url = $this.data('repo-url');
const comment_id = $this.data('comment-id');
const $modal = $($this.data('modal'));
$modal.find('#report-comment-form').attr('action',`${repo_url}/comments/${comment_id}/report`);
$modal.find('input[name="comment-id"]').val(`${comment_id}`);
$modal.modal('show');
event.preventDefault();
});
}
export function initRepoIssueWipToggle() {
// Toggle WIP
$('.toggle-wip a, .toggle-wip button').on('click', async (e) => {

View file

@ -1,7 +1,7 @@
import $ from 'jquery';
import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue, initRepoIssueReportComment,
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
initRepoIssueTitleEdit, initRepoIssueWipToggle,
initRepoPullRequestUpdate, updateIssuesMeta, handleReply, initIssueTemplateCommentEditors, initSingleCommentEditor,
initRepoIssueAssignMe, reloadConfirmDraftComment,
@ -561,7 +561,6 @@ export function initRepository() {
initRepoDiffConversationNav();
initRepoIssueReferenceIssue();
initRepoIssueReportComment();
initRepoIssueCommentDelete();
initRepoIssueDependencyDelete();