{{/if}}
diff --git a/assets/stylesheets/common/signatures.scss b/assets/stylesheets/common/signatures.scss
index 1e8e271..e87afff 100644
--- a/assets/stylesheets/common/signatures.scss
+++ b/assets/stylesheets/common/signatures.scss
@@ -2,6 +2,10 @@
max-width: calc(100% - 5px);
}
+.signature-img {
+ max-width: calc(100% - 5px);
+}
+
.signature-preferences .d-editor {
width: 100%;
@@ -13,3 +17,15 @@
flex: 1;
}
}
+
+.signature-char-count {
+ display: block;
+ margin-top: 0.25em;
+ font-size: var(--font-down-1);
+ color: var(--primary-medium);
+}
+
+.signature-not-allowed {
+ color: var(--primary-medium);
+ font-style: italic;
+}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index b1cf1fa..a95a0de 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -5,3 +5,7 @@ en:
show_signatures: "See user signatures below posts"
my_signature: "My Signature"
signature_placeholder: "A valid URL to an image"
+ not_allowed: "Your account is not in a group that is allowed to use signatures."
+ characters_remaining:
+ one: "%{count} character remaining"
+ other: "%{count} characters remaining"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 251e20e..6df3757 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -3,3 +3,8 @@ en:
signatures_enabled: "Enable user-made signatures below posts?"
signatures_advanced_mode: "Let users use a full blown post editor to create signatures. RESETS all existing signatures."
signatures_visible_by_default: "Make signatures visible by default. This makes signatures visible for anonymous visitors as well."
+ signatures_allowed_groups: "Only allow users in these groups to have signatures. Leave empty to allow all users."
+ signatures_show_in_categories: "Only show signatures in these categories. Leave empty to show in all categories."
+ signatures_first_post_only: "Only show signatures on the first post in a topic."
+ signatures_max_length: "Maximum number of characters allowed in advanced mode signatures."
+ signatures_max_image_height: "Maximum height in pixels for signature images."
diff --git a/config/settings.yml b/config/settings.yml
index 593eba3..2fc28c3 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -8,3 +8,25 @@ plugins:
signatures_visible_by_default:
default: false
client: true
+ signatures_allowed_groups:
+ type: group_list
+ list_type: compact
+ default: ""
+ allow_any: false
+ refresh: true
+ signatures_show_in_categories:
+ type: category_list
+ client: true
+ default: ""
+ signatures_first_post_only:
+ default: false
+ client: true
+ signatures_max_length:
+ default: 500
+ min: 50
+ max: 10000
+ signatures_max_image_height:
+ default: 150
+ min: 20
+ max: 1000
+ client: true
diff --git a/plugin.rb b/plugin.rb
index d3cbed5..2c31029 100644
--- a/plugin.rb
+++ b/plugin.rb
@@ -3,7 +3,7 @@
# name: discourse-signatures
# about: Adds signatures to Discourse posts
# meta_topic_id: 42263
-# version: 2.1.0
+# version: 2.2.0
# author: Rafael Silva
# url: https://github.com/discourse/discourse-signatures
@@ -15,8 +15,8 @@ DiscoursePluginRegistry.serialized_current_user_fields << "signature_raw"
after_initialize do
register_user_custom_field_type("see_signatures", :boolean)
- register_user_custom_field_type("signature_url", :string, max_length: 32_000)
- register_user_custom_field_type("signature_raw", :string, max_length: 1000)
+ register_user_custom_field_type("signature_url", :string, max_length: 2048)
+ register_user_custom_field_type("signature_raw", :string, max_length: 10_000)
# add to class and serializer to allow for default value for the setting
add_to_class(:user, :see_signatures) do
@@ -29,6 +29,11 @@ after_initialize do
add_to_serializer(:user, :see_signatures) { object.see_signatures }
+ add_to_serializer(:current_user, :can_have_signature) do
+ allowed_groups = SiteSetting.signatures_allowed_groups_map
+ allowed_groups.blank? || object.in_any_groups?(allowed_groups)
+ end
+
register_editable_user_custom_field :see_signatures
register_editable_user_custom_field :signature_url
register_editable_user_custom_field :signature_raw
@@ -37,19 +42,48 @@ after_initialize do
allow_public_user_custom_field :signature_url
add_to_serializer(:post, :user_signature) do
+ return nil unless object.user
+
+ allowed_groups = SiteSetting.signatures_allowed_groups_map
+ return nil if allowed_groups.present? && !object.user.in_any_groups?(allowed_groups)
+
if SiteSetting.signatures_advanced_mode
- object.user.custom_fields["signature_cooked"] if object.user
+ object.user.custom_fields["signature_cooked"]
else
- object.user.custom_fields["signature_url"] if object.user
+ object.user.custom_fields["signature_url"]
end
end
- # This is the code responsible for cooking a new advanced mode sig on user update
on(:user_updated) do |user|
+ allowed_groups = SiteSetting.signatures_allowed_groups_map
+ if allowed_groups.present? && !user.in_any_groups?(allowed_groups)
+ user.custom_fields.delete("signature_url")
+ user.custom_fields.delete("signature_raw")
+ user.custom_fields.delete("signature_cooked")
+ user.save
+ next
+ end
+
+ if user.custom_fields["signature_url"].present?
+ url = user.custom_fields["signature_url"]
+ begin
+ parsed = URI.parse(url)
+ raise URI::InvalidURIError unless parsed.is_a?(URI::HTTP) || parsed.is_a?(URI::HTTPS)
+ rescue URI::InvalidURIError
+ user.custom_fields.delete("signature_url")
+ user.save
+ end
+ end
+
if SiteSetting.signatures_advanced_mode && user.custom_fields["signature_raw"]
+ raw = user.custom_fields["signature_raw"]
+ max_length = SiteSetting.signatures_max_length
+ raw = raw[0...max_length] if raw.length > max_length
+ user.custom_fields["signature_raw"] = raw
+
cooked_sig =
PrettyText.cook(
- user.custom_fields["signature_raw"],
+ raw,
omit_nofollow: user.has_trust_level?(TrustLevel[3]) && !SiteSetting.tl3_links_no_follow,
)
# avoid infinite recursion
diff --git a/spec/integration/signatures_spec.rb b/spec/integration/signatures_spec.rb
new file mode 100644
index 0000000..28d434c
--- /dev/null
+++ b/spec/integration/signatures_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+RSpec.describe "Discourse Signatures" do
+ before do
+ enable_current_plugin
+ SiteSetting.signatures_enabled = true
+ end
+
+ describe "group-based restriction" do
+ fab!(:user)
+ fab!(:group)
+ fab!(:topic)
+
+ before do
+ SiteSetting.signatures_advanced_mode = false
+ user.custom_fields["signature_url"] = "https://example.com/sig.png"
+ user.save_custom_fields
+ end
+
+ context "when signatures_allowed_groups is empty" do
+ before { SiteSetting.signatures_allowed_groups = "" }
+
+ it "includes user_signature in the post serializer" do
+ post = Fabricate(:post, topic:, user:)
+ json = PostSerializer.new(post, scope: Guardian.new(user), root: false).as_json
+ expect(json[:user_signature]).to eq("https://example.com/sig.png")
+ end
+
+ it "sets can_have_signature to true for current user" do
+ json = CurrentUserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
+ expect(json[:can_have_signature]).to eq(true)
+ end
+ end
+
+ context "when user is in the allowed group" do
+ before do
+ SiteSetting.signatures_allowed_groups = group.id.to_s
+ group.add(user)
+ end
+
+ it "includes user_signature in the post serializer" do
+ post = Fabricate(:post, topic:, user:)
+ json = PostSerializer.new(post, scope: Guardian.new(user), root: false).as_json
+ expect(json[:user_signature]).to eq("https://example.com/sig.png")
+ end
+
+ it "sets can_have_signature to true" do
+ json = CurrentUserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
+ expect(json[:can_have_signature]).to eq(true)
+ end
+ end
+
+ context "when user is not in the allowed group" do
+ before { SiteSetting.signatures_allowed_groups = group.id.to_s }
+
+ it "returns nil for user_signature in the post serializer" do
+ post = Fabricate(:post, topic:, user:)
+ json = PostSerializer.new(post, scope: Guardian.new(user), root: false).as_json
+ expect(json[:user_signature]).to be_nil
+ end
+
+ it "sets can_have_signature to false" do
+ json = CurrentUserSerializer.new(user, scope: Guardian.new(user), root: false).as_json
+ expect(json[:can_have_signature]).to eq(false)
+ end
+ end
+ end
+
+ describe "signature length enforcement" do
+ fab!(:user)
+
+ before do
+ SiteSetting.signatures_advanced_mode = true
+ SiteSetting.signatures_max_length = 100
+ end
+
+ it "truncates signature_raw to max_length on user update" do
+ user.custom_fields["signature_raw"] = "a" * 200
+ user.save_custom_fields
+
+ DiscourseEvent.trigger(:user_updated, user)
+ user.reload
+
+ expect(user.custom_fields["signature_raw"].length).to eq(100)
+ end
+
+ it "does not truncate signatures within the limit" do
+ user.custom_fields["signature_raw"] = "short signature"
+ user.save_custom_fields
+
+ DiscourseEvent.trigger(:user_updated, user)
+ user.reload
+
+ expect(user.custom_fields["signature_raw"]).to eq("short signature")
+ end
+ end
+
+ describe "URL validation" do
+ fab!(:user)
+
+ before { SiteSetting.signatures_advanced_mode = false }
+
+ it "removes invalid URLs on user update" do
+ user.custom_fields["signature_url"] = "not a valid url %%"
+ user.save_custom_fields
+
+ DiscourseEvent.trigger(:user_updated, user)
+ user.reload
+
+ expect(user.custom_fields["signature_url"]).to be_nil
+ end
+
+ it "keeps valid URLs on user update" do
+ user.custom_fields["signature_url"] = "https://example.com/sig.png"
+ user.save_custom_fields
+
+ DiscourseEvent.trigger(:user_updated, user)
+ user.reload
+
+ expect(user.custom_fields["signature_url"]).to eq("https://example.com/sig.png")
+ end
+
+ it "removes URLs with non-HTTP schemes on user update" do
+ user.custom_fields["signature_url"] = "javascript:alert(1)"
+ user.save_custom_fields
+
+ DiscourseEvent.trigger(:user_updated, user)
+ user.reload
+
+ expect(user.custom_fields["signature_url"]).to be_nil
+ end
+ end
+
+ describe "group restriction on user update" do
+ fab!(:user)
+ fab!(:group)
+
+ before do
+ SiteSetting.signatures_advanced_mode = false
+ user.custom_fields["signature_url"] = "https://example.com/sig.png"
+ user.save_custom_fields
+ end
+
+ it "clears signature data when user is not in the allowed group" do
+ SiteSetting.signatures_allowed_groups = group.id.to_s
+
+ DiscourseEvent.trigger(:user_updated, user)
+ user.reload
+
+ expect(user.custom_fields["signature_url"]).to be_nil
+ end
+
+ it "keeps signature data when user is in the allowed group" do
+ SiteSetting.signatures_allowed_groups = group.id.to_s
+ group.add(user)
+
+ DiscourseEvent.trigger(:user_updated, user)
+ user.reload
+
+ expect(user.custom_fields["signature_url"]).to eq("https://example.com/sig.png")
+ end
+ end
+end
diff --git a/spec/system/image_signatures_spec.rb b/spec/system/image_signatures_spec.rb
index 5f4e8d7..d8f200f 100644
--- a/spec/system/image_signatures_spec.rb
+++ b/spec/system/image_signatures_spec.rb
@@ -4,7 +4,9 @@ RSpec.describe "Image signatures" do
fab!(:user)
fab!(:topic) { Fabricate(:topic, category: Fabricate(:category)) }
fab!(:post) { Fabricate(:post, topic:) }
- let(:signature_image_url) { "data:abcdef," }
+ let(:signature_image_url) do
+ "http://#{Discourse.current_hostname}/images/discourse-logo-sketch-small.png"
+ end
context "when signatures plugin is enabled" do
before do
diff --git a/spec/system/signature_restrictions_spec.rb b/spec/system/signature_restrictions_spec.rb
new file mode 100644
index 0000000..9160ed7
--- /dev/null
+++ b/spec/system/signature_restrictions_spec.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+RSpec.describe "Signature restrictions" do
+ fab!(:user)
+ fab!(:category)
+ fab!(:topic) { Fabricate(:topic, category:) }
+ let(:signature_image_url) { "data:abcdef," }
+
+ before do
+ enable_current_plugin
+ SiteSetting.signatures_enabled = true
+ SiteSetting.signatures_advanced_mode = false
+ SiteSetting.signatures_visible_by_default = true
+ end
+
+ describe "group-based restriction" do
+ fab!(:group)
+ fab!(:post_with_sig) { Fabricate(:post, topic:, user:) }
+
+ before do
+ user.custom_fields["signature_url"] = signature_image_url
+ user.save_custom_fields
+ end
+
+ context "when signatures_allowed_groups is empty" do
+ before { SiteSetting.signatures_allowed_groups = "" }
+
+ it "shows signatures for all users" do
+ sign_in(user)
+ visit topic.url
+ expect(page).to have_css("img.signature-img")
+ end
+ end
+
+ context "when user is in an allowed group" do
+ before do
+ SiteSetting.signatures_allowed_groups = group.id.to_s
+ group.add(user)
+ end
+
+ it "shows the signature below their post" do
+ sign_in(user)
+ visit topic.url
+ expect(page).to have_css("img.signature-img[src='#{signature_image_url}']")
+ end
+
+ it "shows the signature editing UI in preferences" do
+ sign_in(user)
+ visit "/my/preferences/profile"
+ expect(page).to have_content(I18n.t("js.signatures.my_signature"))
+ expect(page).to have_no_content(I18n.t("js.signatures.not_allowed"))
+ end
+ end
+
+ context "when user is not in an allowed group" do
+ before { SiteSetting.signatures_allowed_groups = group.id.to_s }
+
+ it "does not show the signature below their post" do
+ sign_in(Fabricate(:user))
+ visit topic.url
+ expect(page).to have_no_css("img.signature-img")
+ end
+
+ it "shows the restriction message in preferences" do
+ sign_in(user)
+ visit "/my/preferences/profile"
+ expect(page).to have_content(I18n.t("js.signatures.not_allowed"))
+ expect(page).to have_no_field(placeholder: I18n.t("js.signatures.signature_placeholder"))
+ end
+ end
+ end
+
+ describe "category-based display" do
+ fab!(:other_category, :category)
+ fab!(:other_topic) { Fabricate(:topic, category: other_category) }
+
+ before do
+ user.custom_fields["signature_url"] = signature_image_url
+ user.save_custom_fields
+ end
+
+ context "when signatures_show_in_categories is empty" do
+ before { SiteSetting.signatures_show_in_categories = "" }
+
+ it "shows signatures in all categories" do
+ Fabricate(:post, topic:, user:)
+ sign_in(user)
+ visit topic.url
+ expect(page).to have_css("img.signature-img")
+ end
+ end
+
+ context "when signatures_show_in_categories is set" do
+ before { SiteSetting.signatures_show_in_categories = category.id.to_s }
+
+ it "shows signatures in the allowed category" do
+ Fabricate(:post, topic:, user:)
+ sign_in(user)
+ visit topic.url
+ expect(page).to have_css("img.signature-img")
+ end
+
+ it "does not show signatures in other categories" do
+ Fabricate(:post, topic: other_topic, user:)
+ sign_in(user)
+ visit other_topic.url
+ expect(page).to have_no_css("img.signature-img")
+ end
+ end
+ end
+
+ describe "first-post-only display" do
+ before do
+ user.custom_fields["signature_url"] = signature_image_url
+ user.save_custom_fields
+ end
+
+ context "when signatures_first_post_only is false" do
+ before { SiteSetting.signatures_first_post_only = false }
+
+ it "shows signatures on all posts" do
+ Fabricate(:post, topic:, user:)
+ Fabricate(:post, topic:, user:)
+ sign_in(user)
+ visit topic.url
+ expect(page).to have_css("img.signature-img", count: 2)
+ end
+ end
+
+ context "when signatures_first_post_only is true" do
+ before { SiteSetting.signatures_first_post_only = true }
+
+ it "shows signature only on the first post" do
+ Fabricate(:post, topic:, user:)
+ Fabricate(:post, topic:, user:)
+ sign_in(user)
+ visit topic.url
+ expect(page).to have_css("img.signature-img", count: 1)
+ end
+ end
+ end
+end