FEATURE: setting allowing tl0/anonymous flag illegal content (#30785)

The new site setting `allow_anonymous_and_tl0_to_flag_illegal` allows
tl0 users to flag illegal content. In addition, anonymous users are
instructed on how to flag illegal content by sending emails.

Also `email_address_to_report_illegal_content` setting is added. If not
provided, then the site contact email is used.
This commit is contained in:
Krzysztof Kotlarek 2025-01-17 08:57:44 +11:00 committed by GitHub
parent d3a7b99699
commit 029bd6feda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 231 additions and 22 deletions

View File

@ -42,6 +42,7 @@
<input
id="radio_{{this.flag.name_key}}"
{{on "click" (fn this.changePostActionType this.flag)}}
checked={{this.selected}}
type="radio"
name="post_action_type_index"
/>

View File

@ -0,0 +1,39 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { isEmpty } from "@ember/utils";
import DModal from "discourse/components/d-modal";
import { getAbsoluteURL } from "discourse/lib/get-url";
import { i18n } from "discourse-i18n";
export default class AnonymousFlagModal extends Component {
@service siteSettings;
get description() {
return i18n("anonymous_flagging.description", {
email: this.#email,
topic_title: this.args.model.flagModel.topic.title,
url: getAbsoluteURL(this.args.model.flagModel.url),
});
}
get #email() {
if (isEmpty(this.siteSettings.email_address_to_report_illegal_content)) {
return this.siteSettings.contact_email;
}
return this.siteSettings.email_address_to_report_illegal_content;
}
<template>
<DModal
@title={{i18n "anonymous_flagging.title"}}
@closeModal={{@closeModal}}
@bodyClass="anonymous-flag-modal__body"
class="anonymous-flag-modal"
>
<:body>
{{htmlSafe this.description}}
</:body>
</DModal>
</template>
}

View File

@ -29,6 +29,10 @@ export default class Flag extends Component {
this.adminTools
?.checkSpammer(this.args.model.flagModel.user_id)
.then((result) => (this.spammerDetails = result));
if (this.flagsAvailable.length === 1) {
this.selected = this.flagsAvailable[0];
}
}
get flagActions() {

View File

@ -6,9 +6,21 @@ import concatClass from "discourse/helpers/concat-class";
import DiscourseURL from "discourse/lib/url";
export default class PostMenuFlagButton extends Component {
static shouldRender(args) {
static shouldRender(args, helper) {
const { reviewable_id, canFlag, hidden } = args.post;
return reviewable_id || (canFlag && !hidden);
return (
reviewable_id ||
(canFlag && !hidden) ||
(helper.siteSettings
.allow_tl0_and_anonymous_users_to_flag_illegal_content &&
!helper.currentUser)
);
}
get title() {
return this.args.post.currentUser
? "post.controls.flag"
: "post.controls.anonymous_flag";
}
@action
@ -36,7 +48,7 @@ export default class PostMenuFlagButton extends Component {
@action={{@buttonActions.showFlags}}
@icon="flag"
@label={{if @showLabel "post.controls.flag_action"}}
@title="post.controls.flag"
@title={{this.title}}
/>
</div>
</template>

View File

@ -3,6 +3,7 @@ import { cancel, schedule } from "@ember/runloop";
import { service } from "@ember/service";
import { isEmpty } from "@ember/utils";
import AddPmParticipants from "discourse/components/modal/add-pm-participants";
import AnonymousFlagModal from "discourse/components/modal/anonymous-flag";
import ChangeOwnerModal from "discourse/components/modal/change-owner";
import ChangeTimestampModal from "discourse/components/modal/change-timestamp";
import EditSlowModeModal from "discourse/components/modal/edit-slow-mode";
@ -27,6 +28,7 @@ const SCROLL_DELAY = 500;
export default class TopicRoute extends DiscourseRoute {
@service composer;
@service screenTrack;
@service currentUser;
@service modal;
@service router;
@ -104,7 +106,7 @@ export default class TopicRoute extends DiscourseRoute {
@action
showFlags(model) {
this.modal.show(FlagModal, {
this.modal.show(this.currentUser ? FlagModal : AnonymousFlagModal, {
model: {
flagTarget: new PostFlag(),
flagModel: model,

View File

@ -211,20 +211,30 @@ registerButton("flag-count", (attrs) => {
};
});
registerButton("flag", (attrs) => {
if (attrs.reviewableId || (attrs.canFlag && !attrs.hidden)) {
let button = {
action: "showFlags",
title: "post.controls.flag",
icon: "flag",
className: "create-flag",
};
if (attrs.reviewableId) {
button.before = "flag-count";
registerButton(
"flag",
(attrs, _state, siteSettings, _postMenuSettings, currentUser) => {
if (
attrs.reviewableId ||
(attrs.canFlag && !attrs.hidden) ||
(siteSettings.allow_tl0_and_anonymous_users_to_flag_illegal_content &&
!currentUser)
) {
const button = {
action: "showFlags",
title: currentUser
? "post.controls.flag"
: "post.controls.anonymous_flag",
icon: "flag",
className: "create-flag",
};
if (attrs.reviewableId) {
button.before = "flag-count";
}
return button;
}
return button;
}
});
);
registerButton("edit", (attrs) => {
if (attrs.canEdit) {

View File

@ -3894,6 +3894,7 @@ en:
edit_anonymous: "Sorry, but you need to be logged in to edit this post."
flag_action: "Flag"
flag: "privately flag this post for attention or send a personal message about it"
anonymous_flag: "send an email to staff to flag this post"
delete_action: "Delete post"
delete: "delete this post"
undelete_action: "Undelete post"
@ -4211,6 +4212,10 @@ en:
none: "no subcategories"
colors_disabled: "You cant select colors because you have a category style of none."
anonymous_flagging:
title: "Report illegal content"
description: "To report illegal content, please contact <a href='mailto:%{email}?subject=Illegal content: %{topic_title}&body=This post %{url} contains illegal content.'>%{email}</a>"
flagging:
title: "Thanks for keeping our community civil!"
action: "Flag Post"

View File

@ -364,6 +364,8 @@ en:
slow_down_crawler_user_agent_cannot_be_popular_browsers: "You cannot add any of the following values to the setting: %{values}."
strip_image_metadata_cannot_be_disabled_if_composer_media_optimization_image_enabled: "You cannot disable strip image metadata if 'composer media optimization image enabled' is enabled. Disable 'composer media optimization image enabled' before disabling strip image metadata."
twitter_summary_large_image_no_svg: "Twitter summary images used for twitter:image metadata cannot be an .svg image."
tl0_and_anonymous_flag: "Either 'site contact email' or 'email address to report illegal content' must be provided for anonymous users."
conflicting_google_user_id: 'The Google Account ID for this account has changed; staff intervention is required for security reasons. Please contact staff and point them to <br><a href="https://meta.discourse.org/t/76575">https://meta.discourse.org/t/76575</a>'
onebox:
invalid_address: "Sorry, we were unable to generate a preview for this web page, because the server '%{hostname}' could not be found. Instead of a preview, only a link will appear in your post. :cry:"
@ -2307,6 +2309,8 @@ en:
reviewable_default_visibility: "Don't show reviewable items unless they meet this priority"
reviewable_low_priority_threshold: "The priority filter hides reviewable items that don't meet this score unless the '(any)' filter is used."
high_trust_flaggers_auto_hide_posts: "New user posts are automatically hidden after being flagged as spam by a TL3+ user"
allow_tl0_and_anonymous_users_to_flag_illegal_content: "Anonymous users will see information that they have to e-mail administrators to report illegal content."
email_address_to_report_illegal_content: "If left blank the default site admin email will be used."
cooldown_hours_until_reflag: "How much time users will have to wait until they are able to reflag a post"
slow_mode_prevents_editing: "Does 'Slow Mode' prevent editing, after editing_grace_period?"

View File

@ -54,6 +54,7 @@ required:
default: ""
type: email
area: "about|notifications|legal"
client: true
contact_url:
default: ""
area: "about|legal"
@ -1938,6 +1939,14 @@ trust:
allow_any: false
refresh: true
area: "group_permissions"
allow_tl0_and_anonymous_users_to_flag_illegal_content:
default: false
area: "flags"
client: true
email_address_to_report_illegal_content:
default: ""
area: "flags"
client: true
min_trust_to_post_links:
default: 0
enum: "TrustLevelSetting"

View File

@ -94,6 +94,10 @@ module PostGuardian
post.topic.private_message?
)
) ||
(
action_key == :illegal &&
SiteSetting.allow_tl0_and_anonymous_users_to_flag_illegal_content
) ||
# not a flagging action, and haven't done it already
not(is_flag || already_taken_this_action) &&
# nothing except flagging on archived topics

View File

@ -267,6 +267,16 @@ module SiteSettings::Validations
validate_error :twitter_summary_large_image_no_svg
end
def validate_allow_tl0_and_anonymous_users_to_flag_illegal_content(new_val)
return if new_val == "f"
if SiteSetting.contact_email.present? ||
SiteSetting.email_address_to_report_illegal_content.present?
return
end
validate_error :tl0_and_anonymous_flag
end
private
def validate_bucket_setting(setting_name, upload_bucket, backup_bucket)

View File

@ -163,6 +163,16 @@ RSpec.describe Guardian do
Flag.reset_flag_settings!
end
it "return true for illegal if tl0 and allow_tl0_and_anonymous_users_to_flag_illegal_content" do
SiteSetting.flag_post_allowed_groups = ""
user.trust_level = TrustLevel[0]
expect(Guardian.new(user).post_can_act?(post, :illegal)).to be false
SiteSetting.email_address_to_report_illegal_content = "illegal@example.com"
SiteSetting.allow_tl0_and_anonymous_users_to_flag_illegal_content = true
expect(Guardian.new(user).post_can_act?(post, :illegal)).to be true
end
it "works as expected for silenced users" do
UserSilencer.silence(user, admin)

View File

@ -474,4 +474,19 @@ RSpec.describe SiteSettings::Validations do
expect { validations.validate_twitter_summary_large_image(nil) }.not_to raise_error
end
end
describe "#validate_allow_tl0_and_anonymous_users_to_flag_illegal_content" do
it "does not allow to enable when no contact email is provided" do
expect {
validations.validate_allow_tl0_and_anonymous_users_to_flag_illegal_content("t")
}.to raise_error(
Discourse::InvalidParameters,
I18n.t("errors.site_settings.tl0_and_anonymous_flag"),
)
SiteSetting.contact_email = "illegal@example.com"
expect {
validations.validate_allow_tl0_and_anonymous_users_to_flag_illegal_content("t")
}.not_to raise_error
end
end
end

View File

@ -71,6 +71,8 @@ RSpec.describe Admin::SiteSettingsController do
expect(response.status).to eq(200)
expect(response.parsed_body["site_settings"].map { |s| s["setting"] }).to match_array(
%w[
allow_tl0_and_anonymous_users_to_flag_illegal_content
email_address_to_report_illegal_content
silence_new_user_sensitivity
num_users_to_silence_new_user
flag_sockpuppets

View File

@ -164,6 +164,8 @@ describe "Admin Flags Page", type: :system do
admin_flags_page.click_tab("settings")
expect(page.all(".setting-label h3").map(&:text).map(&:downcase)).to eq(
[
"allow tl0 and anonymous users to flag illegal content",
"email address to report illegal content",
"silence new user sensitivity",
"num users to silence new user",
"flag sockpuppets",

View File

@ -2,21 +2,23 @@
describe "Flagging post", type: :system do
fab!(:current_user) { Fabricate(:admin) }
fab!(:first_post) { Fabricate(:post) }
fab!(:post_to_flag) { Fabricate(:post, topic: first_post.topic) }
fab!(:category)
fab!(:topic) { Fabricate(:topic, category: category) }
fab!(:first_post) { Fabricate(:post, topic: topic) }
fab!(:post_to_flag) { Fabricate(:post, topic: topic) }
let(:topic_page) { PageObjects::Pages::Topic.new }
let(:flag_modal) { PageObjects::Modals::Flag.new }
before { sign_in(current_user) }
describe "Using Take Action" do
before { sign_in(current_user) }
it "can select the default action to hide the post, agree with other flags, and reach the flag threshold" do
other_flag = Fabricate(:flag_post_action, post: post_to_flag, user: Fabricate(:moderator))
other_flag_reviewable =
Fabricate(:reviewable_flagged_post, target: post_to_flag, created_by: other_flag.user)
expect(other_flag.reload.agreed_at).to be_nil
topic_page.visit_topic(post_to_flag.topic)
topic_page.visit_topic(topic)
topic_page.expand_post_actions(post_to_flag)
topic_page.click_post_action_button(post_to_flag, :flag)
flag_modal.choose_type(:off_topic)
@ -34,8 +36,10 @@ describe "Flagging post", type: :system do
end
describe "As Illegal" do
before { sign_in(current_user) }
it do
topic_page.visit_topic(post_to_flag.topic)
topic_page.visit_topic(topic)
topic_page.expand_post_actions(post_to_flag)
topic_page.click_post_action_button(post_to_flag, :flag)
flag_modal.choose_type(:illegal)
@ -50,4 +54,63 @@ describe "Flagging post", type: :system do
expect(page).to have_content(I18n.t("js.post.actions.by_you.illegal"))
end
end
context "when tl0" do
fab!(:tl0_user) { Fabricate(:user, trust_level: TrustLevel[0]) }
before { sign_in(tl0_user) }
it "does not allow to mark posts as illegal" do
topic_page.visit_topic(topic)
topic_page.expand_post_actions(post_to_flag)
expect(topic_page).to have_no_flag_button
end
it "allows to mark posts as illegal when allow_tl0_and_anonymous_users_to_flag_illegal_content setting is enabled" do
SiteSetting.email_address_to_report_illegal_content = "illegal@example.com"
SiteSetting.allow_tl0_and_anonymous_users_to_flag_illegal_content = true
topic_page.visit_topic(topic).open_flag_topic_modal
expect(flag_modal).to have_choices(I18n.t("js.flagging.formatted_name.illegal"))
end
end
context "when anonymous" do
let(:anonymous_flag_modal) { PageObjects::Modals::AnonymousFlag.new }
it "does not allow to mark posts as illegal" do
topic_page.visit_topic(topic)
expect(topic_page).to have_no_post_more_actions(post_to_flag)
end
it "allows to mark posts as illegal when allow_tl0_and_anonymous_users_to_flag_illegal_content setting is enabled" do
SiteSetting.contact_email = "contact@example.com"
SiteSetting.allow_tl0_and_anonymous_users_to_flag_illegal_content = true
topic_page.visit_topic(topic, post_number: post_to_flag.post_number)
topic_page.expand_post_actions(post_to_flag)
topic_page.find_post_action_button(post_to_flag, :flag).click
expect(anonymous_flag_modal.body).to have_content(
ActionView::Base.full_sanitizer.sanitize(
I18n.t(
"js.anonymous_flagging.description",
{ email: "contact@example.com", topic_title: topic.title, url: current_url },
),
),
)
SiteSetting.email_address_to_report_illegal_content = "illegal@example.com"
topic_page.visit_topic(topic)
topic_page.expand_post_actions(post_to_flag)
topic_page.find_post_action_button(post_to_flag, :flag).click
expect(anonymous_flag_modal.body).to have_content(
ActionView::Base.full_sanitizer.sanitize(
I18n.t(
"js.anonymous_flagging.description",
{ email: "illegal@example.com", topic_title: topic.title, url: current_url },
),
),
)
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module PageObjects
module Modals
class AnonymousFlag < PageObjects::Modals::Base
BODY_SELECTOR = ".anonymous-flag-modal__body"
end
end
end

View File

@ -78,6 +78,10 @@ module PageObjects
".topic-post:not(.staged) #post_#{post_number}"
end
def has_no_post_more_actions?(post)
within_post(post) { has_no_css?(".show-more-actions") }
end
def has_post_more_actions?(post)
within_post(post) { has_css?(".show-more-actions") }
end
@ -300,6 +304,10 @@ module PageObjects
find(".modal.convert-to-public-topic")
end
def has_no_flag_button?
has_no_css?(".post-action-menu__flag.create-flag")
end
def open_flag_topic_modal
expect(page).to have_css(".flag-topic", wait: Capybara.default_max_wait_time * 3)
find(".flag-topic").click