diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 86b4d39f87d..a93f95ffbad 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -47,6 +47,11 @@ export default Ember.Component.extend({ } }, + @computed + showLink() { + return this.currentUser && this.currentUser.get('link_posting_access') !== 'none'; + }, + @computed('composer.requiredCategoryMissing', 'composer.replyLength') disableTextarea(requiredCategoryMissing, replyLength) { return requiredCategoryMissing && replyLength === 0; diff --git a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs index ebf9a3d856f..1e99d9108af 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs @@ -11,7 +11,7 @@ validation=validation loading=composer.loading forcePreview=forcePreview - showLink=currentUser.can_post_link + showLink=showLink composerEvents=true onExpandPopupMenuOptions="onExpandPopupMenuOptions" onPopupMenuAction=onPopupMenuAction diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index 5de5a83f8ec..90c81f096ed 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -6,11 +6,11 @@ class PostAnalyzer def initialize(raw, topic_id) @raw = raw @topic_id = topic_id - @found_oneboxes = false + @onebox_urls = [] end def found_oneboxes? - @found_oneboxes + @onebox_urls.present? end def has_oneboxes? @@ -32,7 +32,7 @@ class PostAnalyzer end result = Oneboxer.apply(cooked) do |url| - @found_oneboxes = true + @onebox_urls << url Oneboxer.invalidate(url) if opts[:invalidate_oneboxes] Oneboxer.cached_onebox(url) end @@ -86,18 +86,20 @@ class PostAnalyzer # Count how many hosts are linked in the post def linked_hosts - return {} if raw_links.blank? + all_links = raw_links + @onebox_urls + + return {} if all_links.blank? return @linked_hosts if @linked_hosts.present? @linked_hosts = {} - raw_links.each do |u| + all_links.each do |u| begin uri = self.class.parse_uri_rfc2396(u) host = uri.host @linked_hosts[host] ||= 1 unless host.nil? - rescue URI::InvalidURIError - # An invalid URI does not count as a raw link. + rescue URI::InvalidURIError, URI::InvalidComponentError + # An invalid URI does not count as a host next end end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 37395c5b8b2..58a078c68d0 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -40,11 +40,11 @@ class CurrentUserSerializer < BasicUserSerializer :primary_group_id, :primary_group_name, :can_create_topic, - :can_post_link, + :link_posting_access, :external_id - def can_post_link - scope.can_post_link? + def link_posting_access + scope.link_posting_access end def can_create_topic diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index acd82130269..8f1e6f0b6e4 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1394,6 +1394,8 @@ en: min_trust_to_post_links: "The minimum trust level required to include links in posts" min_trust_to_post_images: "The minimum trust level required to include images in a post" + whitelisted_link_domains: "Domains that users may link to even if they don't have the appropriate trust level to post links" + newuser_max_links: "How many links a new user can add to a post." newuser_max_images: "How many images a new user can add to a post." newuser_max_attachments: "How many attachments a new user can add to a post." diff --git a/config/site_settings.yml b/config/site_settings.yml index b972b8f80d4..1d8f9555e75 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -614,6 +614,9 @@ posting: newuser_max_replies_per_topic: 3 newuser_max_mentions_per_post: 2 title_max_word_length: 30 + whitelisted_link_domains: + default: '' + type: list newuser_max_links: 2 newuser_max_images: client: true diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 81eb95f9f0e..ff7dbf2f15d 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -1,9 +1,25 @@ #mixin for all guardian methods dealing with post permissions module PostGuardian - def can_post_link? - authenticated? && - @user.has_trust_level?(TrustLevel[SiteSetting.min_trust_to_post_links]) + def unrestricted_link_posting? + authenticated? && @user.has_trust_level?(TrustLevel[SiteSetting.min_trust_to_post_links]) + end + + def link_posting_access + if unrestricted_link_posting? + 'full' + elsif SiteSetting.whitelisted_link_domains.present? + 'limited' + else + 'none' + end + end + + def can_post_link?(host: nil) + return false if host.blank? + + unrestricted_link_posting? || + SiteSetting.whitelisted_link_domains.split('|').include?(host) end # Can the user act on the post in a particular way. diff --git a/lib/validators/post_validator.rb b/lib/validators/post_validator.rb index f0b7b54a014..f8c58ec2e64 100644 --- a/lib/validators/post_validator.rb +++ b/lib/validators/post_validator.rb @@ -109,10 +109,12 @@ class Validators::PostValidator < ActiveModel::Validator end def can_post_links_validator(post) - if (post.link_count == 0 && !post.has_oneboxes?) || - Guardian.new(post.acting_user).can_post_link? || - private_message?(post) + if (post.link_count == 0 && !post.has_oneboxes?) || private_message?(post) + return newuser_links_validator(post) + end + guardian = Guardian.new(post.acting_user) + if post.linked_hosts.keys.all? { |h| guardian.can_post_link?(host: h) } return newuser_links_validator(post) end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 5b29ff6f430..f0d2b03ec20 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -26,21 +26,59 @@ describe Guardian do expect { Guardian.new(user) }.not_to raise_error end + describe "link_posting_access" do + it "is none for anonymous users" do + expect(Guardian.new.link_posting_access).to eq('none') + end + + it "is full for regular users" do + expect(Guardian.new(user).link_posting_access).to eq('full') + end + + it "is none for a user of a low trust level" do + user.trust_level = 0 + SiteSetting.min_trust_to_post_links = 1 + expect(Guardian.new(user).link_posting_access).to eq('none') + end + + it "is limited for a user of a low trust level with a whitelist" do + SiteSetting.whitelisted_link_domains = 'example.com' + user.trust_level = 0 + SiteSetting.min_trust_to_post_links = 1 + expect(Guardian.new(user).link_posting_access).to eq('limited') + end + end + describe "can_post_link?" do + let(:host) { "discourse.org" } + it "returns false for anonymous users" do - expect(Guardian.new.can_post_link?).to eq(false) + expect(Guardian.new.can_post_link?(host: host)).to eq(false) end it "returns true for a regular user" do - expect(Guardian.new(user).can_post_link?).to eq(true) + expect(Guardian.new(user).can_post_link?(host: host)).to eq(true) end it "supports customization by site setting" do user.trust_level = 0 SiteSetting.min_trust_to_post_links = 0 - expect(Guardian.new(user).can_post_link?).to eq(true) + expect(Guardian.new(user).can_post_link?(host: host)).to eq(true) SiteSetting.min_trust_to_post_links = 1 - expect(Guardian.new(user).can_post_link?).to eq(false) + expect(Guardian.new(user).can_post_link?(host: host)).to eq(false) + end + + describe "whitelisted host" do + before do + SiteSetting.whitelisted_link_domains = host + end + + it "allows a new user to post the link to the host" do + user.trust_level = 0 + SiteSetting.min_trust_to_post_links = 1 + expect(Guardian.new(user).can_post_link?(host: host)).to eq(true) + expect(Guardian.new(user).can_post_link?(host: 'another-host.com')).to eq(false) + end end end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 8a7d5776848..ee3ed2d57b2 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -452,6 +452,13 @@ describe Post do post_two_links.user.trust_level = TrustLevel[1] expect(post_one_link).not_to be_valid end + + it "will skip the check for whitelisted domains" do + SiteSetting.whitelisted_link_domains = 'www.bbc.co.uk' + SiteSetting.min_trust_to_post_links = 2 + post_two_links.user.trust_level = TrustLevel[1] + expect(post_one_link).to be_valid + end end end