mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Previously, reviewable reasons from custom flag producers were treated as inline text, and pre-escaping prevented Markdown and line-break formatting from being recognized. This change cooks a constrained Markdown subset at serialization so every producer shares one sanitization boundary, renders cooked block HTML directly, and keeps watched-word matches literal because their Markdown metacharacters are data rather than formatting.
224 lines
7.2 KiB
Ruby
Vendored
224 lines
7.2 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe ReviewableScoreSerializer do
|
|
fab!(:reviewable, :reviewable_flagged_post)
|
|
fab!(:admin)
|
|
|
|
reasons = %w[
|
|
post_count
|
|
trust_level
|
|
new_topics_unless_trust_level
|
|
fast_typer
|
|
auto_silence_regex
|
|
staged
|
|
must_approve_users
|
|
invite_only
|
|
email_spam
|
|
suspect_user
|
|
contains_media
|
|
watched_word
|
|
]
|
|
|
|
describe "#reason" do
|
|
context "with regular links" do
|
|
it "adds a link for watched words" do
|
|
serialized = serialized_score("watched_word")
|
|
link_url = "#{Discourse.base_url}/admin/customize/watched_words"
|
|
watched_words_link =
|
|
"<a href=\"#{link_url}\">#{I18n.t("reviewables.reasons.links.watched_word")}</a>"
|
|
|
|
expect(serialized.reason).to include(watched_words_link)
|
|
end
|
|
|
|
it "adds a link for category settings" do
|
|
category = Fabricate(:category, name: "Reviewable Category", slug: "reviewable-category")
|
|
reviewable.category = category
|
|
serialized = serialized_score("category")
|
|
link_url = "#{Discourse.base_url}/c/#{category.slug}/edit/settings"
|
|
category_link =
|
|
"<a href=\"#{link_url}\">#{I18n.t("reviewables.reasons.links.category")}</a>"
|
|
|
|
expect(serialized.reason).to include(category_link)
|
|
end
|
|
end
|
|
|
|
context "with site setting links" do
|
|
reasons
|
|
.reject { |r| r == "watched_word" }
|
|
.each do |r|
|
|
it "adds a link to a site setting for the #{r} reason" do
|
|
serialized = serialized_score(r)
|
|
setting_name = described_class::REASONS_AND_SETTINGS[r.to_sym]
|
|
link_url =
|
|
"#{Discourse.base_url}/admin/site_settings/category/all_results?filter=#{setting_name}"
|
|
link = "<a href=\"#{link_url}\">#{setting_name.gsub("_", " ")}</a>"
|
|
|
|
expect(serialized.reason).to include(link)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "with custom reasons" do
|
|
it "serializes it without doing any translation" do
|
|
custom = "completely custom flag reason"
|
|
serialized = serialized_score(custom)
|
|
|
|
expect(serialized.reason).to match_html("<p>#{custom}</p>")
|
|
end
|
|
|
|
it "cooks and sanitizes basic Markdown without post-specific transformations" do
|
|
custom = <<~MARKDOWN
|
|
**Details**
|
|
|
|
- first
|
|
- second
|
|
|
|
first<br>second
|
|
|
|
@#{admin.username} :smile:
|
|
|
|
<script>alert("unsafe")</script>
|
|
MARKDOWN
|
|
|
|
serialized = serialized_score(custom)
|
|
|
|
expect(serialized.reason).to match_html(<<~HTML)
|
|
<p><strong>Details</strong></p>
|
|
<ul>
|
|
<li>first</li>
|
|
<li>second</li>
|
|
</ul>
|
|
<p>first<br>second</p>
|
|
<p>@#{admin.username} :smile:</p>
|
|
HTML
|
|
end
|
|
end
|
|
|
|
context "with watched words" do
|
|
let(:link) do
|
|
"<a href=\"#{Discourse.base_url}/admin/customize/watched_words\">#{I18n.t("reviewables.reasons.links.watched_word")}</a>"
|
|
end
|
|
it "tries to guess the watched words if they weren't recorded at the time of flagging" do
|
|
raw = "I'm a post with some bad words like 'bad' and 'words'."
|
|
reviewable.target = Fabricate(:post, raw:)
|
|
|
|
score = serialized_score("watched_word")
|
|
|
|
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "bad")
|
|
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "words")
|
|
|
|
expect(score.reason).to include("bad, words")
|
|
|
|
expect(reviewable.target.raw).to eq(raw)
|
|
end
|
|
|
|
it "handles guessing the watched words when the post hasn't been created yet" do
|
|
queued_reviewable = Fabricate(:reviewable_queued_post_topic)
|
|
raw = queued_reviewable.payload["raw"].clone
|
|
reviewable_score =
|
|
ReviewableScore.new(reviewable: queued_reviewable, reason: "watched_word")
|
|
|
|
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "contents")
|
|
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "title")
|
|
|
|
result = described_class.new(reviewable_score, scope: Guardian.new(admin), root: nil)
|
|
expect(result.reason).to include("contents, title")
|
|
expect(queued_reviewable.payload["raw"]).to eq(raw)
|
|
end
|
|
|
|
it "renders matched words containing HTML and Markdown syntax literally" do
|
|
words = [
|
|
"<notifications@example.discoursemail.com>",
|
|
"[urgent](https://example.com)",
|
|
"**spam**",
|
|
]
|
|
score =
|
|
ReviewableScore.new(
|
|
reviewable: reviewable,
|
|
reason: "watched_word",
|
|
context: words.join(","),
|
|
)
|
|
serialized = described_class.new(score, scope: admin.guardian, root: nil)
|
|
reason =
|
|
I18n.t(
|
|
"reviewables.reasons.watched_word",
|
|
link: link,
|
|
words: CGI.escapeHTML(words.join(", ")),
|
|
count: words.length,
|
|
)
|
|
|
|
expect(serialized.reason).to match_html("<p>#{reason}</p>")
|
|
end
|
|
|
|
it "uses the no-context message if the post has no watched words" do
|
|
reviewable.target = Fabricate(:post, raw: "This post contains no bad words.")
|
|
|
|
score = serialized_score("watched_word")
|
|
|
|
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "superbad")
|
|
|
|
reason =
|
|
I18n.t("reviewables.reasons.no_context.watched_word", link:, default: "watched_word")
|
|
|
|
expect(score.reason).to match_html("<p>#{reason}</p>")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#reason_type" do
|
|
reasons.each do |reason|
|
|
it "returns the correct reason type for #{reason}" do
|
|
serialized = serialized_score(reason)
|
|
expect(serialized.reason_type).to eq(reason)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#reason_data" do
|
|
reasons
|
|
.reject { |r| r == "watched_word" }
|
|
.each do |reason|
|
|
it "returns nil for #{reason}" do
|
|
serialized = serialized_score(reason)
|
|
expect(serialized.reason_data).to be_nil
|
|
end
|
|
end
|
|
|
|
it "returns the watched words found for the watched_word reason" do
|
|
raw = "I'm a post with some bad words like 'bad' and 'words'."
|
|
reviewable.target = Fabricate(:post, raw:)
|
|
|
|
score = serialized_score("watched_word")
|
|
|
|
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "bad")
|
|
Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "words")
|
|
|
|
expect(score.reason_data).to include("bad", "words")
|
|
end
|
|
end
|
|
|
|
describe "#setting_name_for_reason" do
|
|
after { DiscoursePluginRegistry.reset_register!(:reviewable_score_links) }
|
|
|
|
describe "when a plugin adds a setting name to linkify" do
|
|
it "gets the setting name from the registry" do
|
|
reason = :plugin_reason
|
|
setting_name = "max_username_length"
|
|
DiscoursePluginRegistry.register_reviewable_score_link(
|
|
{ reason: reason, setting: setting_name },
|
|
Plugin::Instance.new,
|
|
)
|
|
|
|
score = serialized_score(reason)
|
|
|
|
expect(score.setting_name_for_reason(reason)).to eq(setting_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
def serialized_score(reason)
|
|
score = ReviewableScore.new(reviewable: reviewable, reason: reason)
|
|
|
|
described_class.new(score, scope: Guardian.new(admin), root: nil)
|
|
end
|
|
end
|