diff --git a/app/assets/javascripts/discourse/controllers/preferences_username_controller.js b/app/assets/javascripts/discourse/controllers/preferences_username_controller.js
index 1f325a19b..b2c31b56a 100644
--- a/app/assets/javascripts/discourse/controllers/preferences_username_controller.js
+++ b/app/assets/javascripts/discourse/controllers/preferences_username_controller.js
@@ -12,20 +12,20 @@ Discourse.PreferencesUsernameController = Discourse.ObjectController.extend({
error: false,
errorMessage: null,
- saveDisabled: (function() {
+ saveDisabled: function() {
if (this.get('saving')) return true;
if (this.blank('newUsername')) return true;
if (this.get('taken')) return true;
if (this.get('unchanged')) return true;
if (this.get('errorMessage')) return true;
return false;
- }).property('newUsername', 'taken', 'errorMessage', 'unchanged', 'saving'),
+ }.property('newUsername', 'taken', 'errorMessage', 'unchanged', 'saving'),
- unchanged: (function() {
+ unchanged: function() {
return this.get('newUsername') === this.get('content.username');
- }).property('newUsername', 'content.username'),
+ }.property('newUsername', 'content.username'),
- checkTaken: (function() {
+ checkTaken: function() {
if( this.get('newUsername') && this.get('newUsername').length < 3 ) {
this.set('errorMessage', Em.String.i18n('user.name.too_short'));
} else {
@@ -42,12 +42,12 @@ Discourse.PreferencesUsernameController = Discourse.ObjectController.extend({
}
});
}
- }).observes('newUsername'),
+ }.observes('newUsername'),
- saveButtonText: (function() {
+ saveButtonText: function() {
if (this.get('saving')) return Em.String.i18n("saving");
return Em.String.i18n("user.change_username.action");
- }).property('saving'),
+ }.property('saving'),
changeUsername: function() {
var preferencesUsernameController = this;
diff --git a/app/assets/javascripts/discourse/views/post_link_view.js b/app/assets/javascripts/discourse/views/post_link_view.js
index 4023a26c2..c4f74b789 100644
--- a/app/assets/javascripts/discourse/views/post_link_view.js
+++ b/app/assets/javascripts/discourse/views/post_link_view.js
@@ -10,10 +10,10 @@ Discourse.PostLinkView = Discourse.View.extend({
tagName: 'li',
classNameBindings: ['direction'],
- direction: (function() {
+ direction: function() {
if (this.get('content.reflection')) return 'incoming';
return null;
- }).property('content.reflection'),
+ }.property('content.reflection'),
render: function(buffer) {
var clicks;
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index b6c78eb38..f3800fe47 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -39,8 +39,11 @@ class PostsController < ApplicationController
meta_data: params[:meta_data],
auto_close_days: params[:auto_close_days])
post = post_creator.create
-
if post_creator.errors.present?
+
+ # If the post was spam, flag all the user's posts as spam
+ current_user.flag_linked_posts_as_spam if post_creator.spam?
+
render_json_error(post_creator)
else
post_serializer = PostSerializer.new(post, scope: guardian, root: false)
diff --git a/app/models/post.rb b/app/models/post.rb
index d5c1724ea..b1c929a99 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -99,6 +99,7 @@ class Post < ActiveRecord::Base
@white_listed_image_classes ||= ['avatar', 'favicon', 'thumbnail']
end
+ # How many images are present in the post
def image_count
return 0 unless raw.present?
@@ -110,19 +111,28 @@ class Post < ActiveRecord::Base
end.count
end
- def link_count
- return 0 unless raw.present?
+ # Returns an array of all links in a post
+ def raw_links
+ return [] unless raw.present?
+
+ return @raw_links if @raw_links.present?
# Don't include @mentions in the link count
- total = 0
+ @raw_links = []
cooked_document.search("a[href]").each do |l|
html_class = l.attributes['class']
+ url = l.attributes['href'].to_s
if html_class.present?
next if html_class.to_s == 'mention' && l.attributes['href'].to_s =~ /^\/users\//
end
- total +=1
+ @raw_links << url
end
- total
+ @raw_links
+ end
+
+ # How many links are present in the post
+ def link_count
+ raw_links.size
end
# Sometimes the post is being edited by someone else, for example, a mod.
@@ -136,6 +146,7 @@ class Post < ActiveRecord::Base
@acting_user = pu
end
+ # Ensure maximum amount of mentions in a post
def max_mention_validator
if acting_user.present? && acting_user.has_trust_level?(:basic)
errors.add(:base, I18n.t(:too_many_mentions, count: SiteSetting.max_mentions_per_post)) if raw_mentions.size > SiteSetting.max_mentions_per_post
@@ -144,17 +155,57 @@ class Post < ActiveRecord::Base
end
end
+ # Ensure new users can not put too many images in a post
def max_images_validator
return if acting_user.present? && acting_user.has_trust_level?(:basic)
errors.add(:base, I18n.t(:too_many_images, count: SiteSetting.newuser_max_images)) if image_count > SiteSetting.newuser_max_images
end
+ # Ensure new users can not put too many links in a post
def max_links_validator
return if acting_user.present? && acting_user.has_trust_level?(:basic)
errors.add(:base, I18n.t(:too_many_links, count: SiteSetting.newuser_max_links)) if link_count > SiteSetting.newuser_max_links
end
+ # Count how many hosts are linked in the post
+ def linked_hosts
+ return {} if raw_links.blank?
+
+ return @linked_hosts if @linked_hosts.present?
+
+ @linked_hosts = {}
+ raw_links.each do |u|
+ uri = URI.parse(u)
+ host = uri.host
+ @linked_hosts[host] = (@linked_hosts[host] || 0) + 1
+ end
+ @linked_hosts
+ end
+
+ def total_hosts_usage
+ hosts = linked_hosts.clone
+
+ # Count hosts in previous posts the user has made, PLUS these new ones
+ TopicLink.where(domain: hosts.keys, user_id: acting_user.id).each do |tl|
+ hosts[tl.domain] = (hosts[tl.domain] || 0) + 1
+ end
+
+ hosts
+ end
+
+ # Prevent new users from posting the same hosts too many times.
+ def has_host_spam?
+ return false if acting_user.present? && acting_user.has_trust_level?(:basic)
+
+ total_hosts_usage.each do |host, count|
+ return true if count >= SiteSetting.newuser_spam_host_threshold
+ end
+
+ false
+ end
+
+
def raw_mentions
return [] if raw.blank?
diff --git a/app/models/post_action.rb b/app/models/post_action.rb
index ac3f68779..9cec12945 100644
--- a/app/models/post_action.rb
+++ b/app/models/post_action.rb
@@ -64,11 +64,8 @@ class PostAction < ActiveRecord::Base
end
PostAction.update_all({ deleted_at: Time.zone.now, deleted_by: moderator_id }, { post_id: post.id, post_action_type_id: actions })
-
f = actions.map{|t| ["#{PostActionType.types[t]}_count", 0]}
-
Post.with_deleted.update_all(Hash[*f.flatten], id: post.id)
-
update_flagged_posts_count
end
@@ -145,6 +142,7 @@ class PostAction < ActiveRecord::Base
post_action_type_id == PostActionType.types[:notify_user] ||
post_action_type_id == PostActionType.types[:notify_moderators]
end
+
# A custom rate limiter for this model
def post_action_rate_limiter
return unless is_flag? || is_bookmark? || is_like?
@@ -174,6 +172,30 @@ class PostAction < ActiveRecord::Base
.exists?
end
+ # Returns the flag counts for a post, taking into account that some users
+ # can weigh flags differently.
+ def self.flag_counts_for(post_id)
+ flag_counts = exec_sql("SELECT SUM(CASE
+ WHEN pa.deleted_at IS NULL AND u.admin THEN :flags_required_to_hide_post
+ WHEN pa.deleted_at IS NULL AND (NOT u.admin) THEN 1
+ ELSE 0
+ END) AS new_flags,
+ SUM(CASE
+ WHEN pa.deleted_at IS NOT NULL AND u.admin THEN :flags_required_to_hide_post
+ WHEN pa.deleted_at IS NOT NULL AND (NOT u.admin) THEN 1
+ ELSE 0
+ END) AS old_flags
+ FROM post_actions AS pa
+ INNER JOIN users AS u ON u.id = pa.user_id
+ WHERE pa.post_id = :post_id AND
+ pa.post_action_type_id IN (:post_action_types)",
+ post_id: post_id,
+ post_action_types: PostActionType.auto_action_flag_types.values,
+ flags_required_to_hide_post: SiteSetting.flags_required_to_hide_post).first
+
+ [flag_counts['old_flags'].to_i, flag_counts['new_flags'].to_i]
+ end
+
after_save do
# Update denormalized counts
post_action_type = PostActionType.types[post_action_type_id]
@@ -195,11 +217,7 @@ class PostAction < ActiveRecord::Base
if PostActionType.auto_action_flag_types.include?(post_action_type) && SiteSetting.flags_required_to_hide_post > 0
# automatic hiding of posts
- flag_counts = exec_sql("SELECT SUM(CASE WHEN deleted_at IS NULL THEN 1 ELSE 0 END) AS new_flags,
- SUM(CASE WHEN deleted_at IS NOT NULL THEN 1 ELSE 0 END) AS old_flags
- FROM post_actions
- WHERE post_id = ? AND post_action_type_id IN (?)", post.id, PostActionType.auto_action_flag_types.values).first
- old_flags, new_flags = flag_counts['old_flags'].to_i, flag_counts['new_flags'].to_i
+ old_flags, new_flags = PostAction.flag_counts_for(post.id)
if new_flags >= SiteSetting.flags_required_to_hide_post
reason = old_flags > 0 ? Post.hidden_reasons[:flag_threshold_reached_again] : Post.hidden_reasons[:flag_threshold_reached]
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index eb5442bb8..5a770f0d2 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -178,6 +178,8 @@ class SiteSetting < ActiveRecord::Base
setting(:newuser_max_links, 2)
setting(:newuser_max_images, 0)
+ setting(:newuser_spam_host_threshold, 3)
+
setting(:title_fancy_entities, true)
# The default locale for the site
diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb
index 6133e87c0..433ee111e 100644
--- a/app/models/topic_link.rb
+++ b/app/models/topic_link.rb
@@ -68,12 +68,12 @@ class TopicLink < ActiveRecord::Base
added_urls << url
TopicLink.create(post_id: post.id,
- user_id: post.user_id,
- topic_id: post.topic_id,
- url: url,
- domain: parsed.host || Discourse.current_hostname,
- internal: internal,
- link_topic_id: topic_id)
+ user_id: post.user_id,
+ topic_id: post.topic_id,
+ url: url,
+ domain: parsed.host || Discourse.current_hostname,
+ internal: internal,
+ link_topic_id: topic_id)
# Create the reflection if we can
if topic_id.present?
diff --git a/app/models/user.rb b/app/models/user.rb
index fae98a183..572a1e708 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -3,6 +3,7 @@ require_dependency 'email_token'
require_dependency 'trust_level'
require_dependency 'pbkdf2'
require_dependency 'summarize'
+require_dependency 'discourse'
class User < ActiveRecord::Base
attr_accessible :name, :username, :password, :email, :bio_raw, :website
@@ -22,6 +23,8 @@ class User < ActiveRecord::Base
has_many :views
has_many :user_visits
has_many :invites
+ has_many :topic_links
+
has_one :twitter_user_info, dependent: :destroy
has_one :github_user_info, dependent: :destroy
belongs_to :approved_by, class_name: 'User'
@@ -570,6 +573,17 @@ class User < ActiveRecord::Base
cats.map{|c| c.id}.sort
end
+ # Flag all posts from a user as spam
+ def flag_linked_posts_as_spam
+ admin = Discourse.system_user
+ topic_links.includes(:post).each do |tl|
+ begin
+ PostAction.act(admin, tl.post, PostActionType.types[:spam])
+ rescue PostAction::AlreadyActed
+ # If the user has already acted, just ignore it
+ end
+ end
+ end
protected
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 495d0c94c..e139fb215 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -32,6 +32,7 @@ en:
zero: "Sorry, new users can't put links in posts."
one: "Sorry, new users can only put one link in a post."
other: "Sorry, new users can only put %{count} links in a post."
+ spamming_host: "Sorry you cannot post a link to that host."
just_posted_that: "is too similar to what you recently posted"
has_already_been_used: "has already been used"
@@ -574,6 +575,8 @@ en:
tos_url: "If you have a Terms of Service document hosted elsewhere that you want to use, provide the full URL here."
privacy_policy_url: "If you have a Privacy Policy document hosted elsewhere that you want to use, provide the full URL here."
+ newuser_spam_host_threshold: "How many times a new user can post a link to the same host within their `newuser_spam_host_posts` posts before being considered spam."
+
notification_types:
mentioned: "%{display_username} mentioned you in %{link}"
liked: "%{display_username} liked your post in %{link}"
diff --git a/lib/discourse.rb b/lib/discourse.rb
index d621cb12e..456856b77 100644
--- a/lib/discourse.rb
+++ b/lib/discourse.rb
@@ -73,6 +73,12 @@ module Discourse
end
end
+ # Either returns the system_username user or the first admin.
+ def self.system_user
+ user = User.where(username_lower: SiteSetting.system_username).first if SiteSetting.system_username.present?
+ user = User.admins.order(:id).first if user.blank?
+ user
+ end
private
diff --git a/lib/post_creator.rb b/lib/post_creator.rb
index d71cbbbb2..3285b771a 100644
--- a/lib/post_creator.rb
+++ b/lib/post_creator.rb
@@ -31,9 +31,16 @@ class PostCreator
# If we don't do this we introduce a rather risky dependency
@user = user
@opts = opts
+ @spam = false
+
raise Discourse::InvalidParameters.new(:raw) if @opts[:raw].blank?
end
+ # True if the post was considered spam
+ def spam?
+ @spam
+ end
+
def guardian
@guardian ||= Guardian.new(@user)
end
@@ -63,6 +70,16 @@ class PostCreator
post.image_sizes = @opts[:image_sizes] if @opts[:image_sizes].present?
post.invalidate_oneboxes = @opts[:invalidate_oneboxes] if @opts[:invalidate_oneboxes].present?
+
+
+ # If the post has host spam, roll it back.
+ if post.has_host_spam?
+ post.errors.add(:base, I18n.t(:spamming_host))
+ @errors = post.errors
+ @spam = true
+ raise ActiveRecord::Rollback.new
+ end
+
unless post.save
@errors = post.errors
raise ActiveRecord::Rollback.new
diff --git a/lib/system_message.rb b/lib/system_message.rb
index 162c3f37b..8e5ed4e5d 100644
--- a/lib/system_message.rb
+++ b/lib/system_message.rb
@@ -1,6 +1,7 @@
# Handle sending a message to a user from the system.
require_dependency 'post_creator'
require_dependency 'topic_subtype'
+require_dependency 'discourse'
class SystemMessage
@@ -30,7 +31,7 @@ class SystemMessage
title = I18n.t("system_messages.#{type}.subject_template", params)
raw_body = I18n.t("system_messages.#{type}.text_body_template", params)
- PostCreator.create(SystemMessage.system_user,
+ PostCreator.create(Discourse.system_user,
raw: raw_body,
title: title,
archetype: Archetype.private_message,
@@ -39,11 +40,6 @@ class SystemMessage
end
- # Either returns the system_username user or the first admin.
- def self.system_user
- user = User.where(username_lower: SiteSetting.system_username).first if SiteSetting.system_username.present?
- user = User.admins.order(:id).first if user.blank?
- user
- end
+
end
diff --git a/spec/components/discourse_spec.rb b/spec/components/discourse_spec.rb
index 0059511a8..dad0b8e17 100644
--- a/spec/components/discourse_spec.rb
+++ b/spec/components/discourse_spec.rb
@@ -16,7 +16,6 @@ describe Discourse do
end
context 'base_url' do
-
context 'when ssl is off' do
before do
SiteSetting.expects(:use_ssl?).returns(false)
@@ -45,12 +44,26 @@ describe Discourse do
it "returns the non standart port in the base url" do
Discourse.base_url.should == "http://foo.com:3000"
end
-
end
-
-
end
+ context '#system_user' do
+
+ let!(:admin) { Fabricate(:admin) }
+ let!(:another_admin) { Fabricate(:another_admin) }
+
+ it 'returns the user specified by the site setting system_username' do
+ SiteSetting.stubs(:system_username).returns(another_admin.username)
+ Discourse.system_user.should == another_admin
+ end
+
+ it 'returns the first admin user otherwise' do
+ SiteSetting.stubs(:system_username).returns(nil)
+ Discourse.system_user.should == admin
+ end
+
+ end
+
end
diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb
index f7c9dba1c..ba6557a36 100644
--- a/spec/components/post_creator_spec.rb
+++ b/spec/components/post_creator_spec.rb
@@ -32,6 +32,11 @@ describe PostCreator do
context 'success' do
+ it "doesn't return true for spam" do
+ creator.create
+ creator.spam?.should be_false
+ end
+
it 'generates the correct messages for a secure topic' do
admin = Fabricate(:admin)
@@ -60,7 +65,6 @@ describe PostCreator do
].sort
admin_ids = [Group[:admins].id]
messages.any?{|m| m.group_ids != admin_ids}.should be_false
-
end
it 'generates the correct messages for a normal topic' do
@@ -187,6 +191,25 @@ describe PostCreator do
end
+
+ context "host spam" do
+
+ let!(:topic) { Fabricate(:topic, user: user) }
+ let(:basic_topic_params) { { raw: 'test reply', topic_id: topic.id, reply_to_post_number: 4} }
+ let(:creator) { PostCreator.new(user, basic_topic_params) }
+
+ before do
+ Post.any_instance.expects(:has_host_spam?).returns(true)
+ end
+
+ it "does not create the post" do
+ creator.create
+ creator.errors.should be_present
+ creator.spam?.should be_true
+ end
+
+ end
+
# more integration testing ... maximise our testing
context 'existing topic' do
let!(:topic) { Fabricate(:topic, user: user) }
diff --git a/spec/components/system_message_spec.rb b/spec/components/system_message_spec.rb
index c32656816..7db9728b7 100644
--- a/spec/components/system_message_spec.rb
+++ b/spec/components/system_message_spec.rb
@@ -21,18 +21,5 @@ describe SystemMessage do
end
end
- context '#system_user' do
-
- it 'returns the user specified by the site setting system_username' do
- SiteSetting.stubs(:system_username).returns(admin.username)
- SystemMessage.system_user.should == admin
- end
-
- it 'returns the first admin user otherwise' do
- SiteSetting.stubs(:system_username).returns(nil)
- SystemMessage.system_user.should == admin
- end
-
- end
end
diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb
index 2456d952a..35f9c59d7 100644
--- a/spec/controllers/posts_controller_spec.rb
+++ b/spec/controllers/posts_controller_spec.rb
@@ -273,6 +273,31 @@ describe PostsController do
::JSON.parse(response.body).should be_present
end
+ context "errors" do
+
+ let(:post_with_errors) { Fabricate.build(:post, user: user)}
+
+ before do
+ post_with_errors.errors.add(:base, I18n.t(:spamming_host))
+ PostCreator.any_instance.stubs(:errors).returns(post_with_errors.errors)
+ PostCreator.any_instance.expects(:create).returns(post_with_errors)
+ end
+
+ it "does not succeed" do
+ xhr :post, :create, post: {raw: 'test'}
+ User.any_instance.expects(:flag_linked_posts_as_spam).never
+ response.should_not be_success
+ end
+
+ it "it triggers flag_linked_posts_as_spam when the post creator returns spam" do
+ PostCreator.any_instance.expects(:spam?).returns(true)
+ User.any_instance.expects(:flag_linked_posts_as_spam)
+ xhr :post, :create, post: {raw: 'test'}
+ end
+
+ end
+
+
context "parameters" do
let(:post_creator) { mock }
diff --git a/spec/models/post_action_spec.rb b/spec/models/post_action_spec.rb
index 8183efc4c..4b93d8b8d 100644
--- a/spec/models/post_action_spec.rb
+++ b/spec/models/post_action_spec.rb
@@ -155,6 +155,29 @@ describe PostAction do
describe 'flagging' do
+ context "flag_counts_for" do
+ it "returns the correct flag counts" do
+ post = Fabricate(:post)
+
+ SiteSetting.stubs(:flags_required_to_hide_post).returns(7)
+
+ # A post with no flags has 0 for flag counts
+ PostAction.flag_counts_for(post.id).should == [0, 0]
+
+ flag = PostAction.act(Fabricate(:evil_trout), post, PostActionType.types[:spam])
+ PostAction.flag_counts_for(post.id).should == [0, 1]
+
+ # If an admin flags the post, it is counted higher
+ admin = Fabricate(:admin)
+ PostAction.act(admin, post, PostActionType.types[:spam])
+ PostAction.flag_counts_for(post.id).should == [0, 8]
+
+ # If a flag is dismissed
+ PostAction.clear_flags!(post, admin)
+ PostAction.flag_counts_for(post.id).should == [8, 0]
+ end
+ end
+
it 'does not allow you to flag stuff with 2 reasons' do
post = Fabricate(:post)
u1 = Fabricate(:evil_trout)
diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb
index 785d6a7f0..f93f37cbf 100644
--- a/spec/models/post_spec.rb
+++ b/spec/models/post_spec.rb
@@ -7,6 +7,13 @@ describe Post do
ImageSorcery.any_instance.stubs(:convert).returns(false)
end
+ # Help us build a post with a raw body
+ def post_with_body(body, user=nil)
+ args = post_args.merge(raw: body)
+ args[:user] = user if user.present?
+ Fabricate.build(:post, args)
+ end
+
it { should belong_to :user }
it { should belong_to :topic }
it { should validate_presence_of :raw }
@@ -89,12 +96,12 @@ describe Post do
describe "maximum images" do
let(:newuser) { Fabricate(:user, trust_level: TrustLevel.levels[:newuser]) }
let(:post_no_images) { Fabricate.build(:post, post_args.merge(user: newuser)) }
- let(:post_one_image) { Fabricate.build(:post, post_args.merge(raw: "", user: newuser)) }
- let(:post_two_images) { Fabricate.build(:post, post_args.merge(raw: "
", user: newuser)) }
- let(:post_with_avatars) { Fabricate.build(:post, post_args.merge(raw: '
', user: newuser)) }
- let(:post_with_favicon) { Fabricate.build(:post, post_args.merge(raw: '
', user: newuser)) }
- let(:post_with_thumbnail) { Fabricate.build(:post, post_args.merge(raw: '
', user: newuser)) }
- let(:post_with_two_classy_images) { Fabricate.build(:post, post_args.merge(raw: "
", user: newuser)) }
+ let(:post_one_image) { post_with_body("", newuser) }
+ let(:post_two_images) { post_with_body("
", newuser) }
+ let(:post_with_avatars) { post_with_body('
', newuser) }
+ let(:post_with_favicon) { post_with_body('
', newuser) }
+ let(:post_with_thumbnail) { post_with_body('
', newuser) }
+ let(:post_with_two_classy_images) { post_with_body("
", newuser) }
it "returns 0 images for an empty post" do
Fabricate.build(:post).image_count.should == 0
@@ -159,11 +166,78 @@ describe Post do
end
+ context "links" do
+ let(:newuser) { Fabricate(:user, trust_level: TrustLevel.levels[:newuser]) }
+ let(:no_links) { post_with_body("hello world my name is evil trout", newuser) }
+ let(:one_link) { post_with_body("[jlawr](http://www.imdb.com/name/nm2225369)", newuser) }
+ let(:two_links) { post_with_body("disney reddit", newuser)}
+ let(:three_links) { post_with_body("http://discourse.org and http://discourse.org/another_url and http://www.imdb.com/name/nm2225369", newuser)}
+
+ describe "raw_links" do
+ it "returns a blank collection for a post with no links" do
+ no_links.raw_links.should be_blank
+ end
+
+ it "finds a link within markdown" do
+ one_link.raw_links.should == ["http://www.imdb.com/name/nm2225369"]
+ end
+
+ it "can find two links from html" do
+ two_links.raw_links.should == ["http://disneyland.disney.go.com/", "http://reddit.com"]
+ end
+
+ it "can find three links without markup" do
+ three_links.raw_links.should == ["http://discourse.org", "http://discourse.org/another_url", "http://www.imdb.com/name/nm2225369"]
+ end
+ end
+
+ describe "linked_hosts" do
+ it "returns blank with no links" do
+ no_links.linked_hosts.should be_blank
+ end
+
+ it "returns the host and a count for links" do
+ two_links.linked_hosts.should == {"disneyland.disney.go.com" => 1, "reddit.com" => 1}
+ end
+
+ it "it counts properly with more than one link on the same host" do
+ three_links.linked_hosts.should == {"discourse.org" => 2, "www.imdb.com" => 1}
+ end
+ end
+
+ describe "total host usage" do
+
+ it "has none for a regular post" do
+ no_links.total_hosts_usage.should be_blank
+ end
+
+ context "with a previous host" do
+
+ let(:user) { old_post.newuser }
+ let(:another_disney_link) { post_with_body("[radiator springs](http://disneyland.disney.go.com/disney-california-adventure/radiator-springs-racers/)", newuser) }
+
+ before do
+ another_disney_link.save
+ TopicLink.extract_from(another_disney_link)
+ end
+
+ it "contains the new post's links, PLUS the previous one" do
+ two_links.total_hosts_usage.should == {'disneyland.disney.go.com' => 2, 'reddit.com' => 1}
+ end
+
+ end
+
+ end
+
+
+ end
+
+
describe "maximum links" do
let(:newuser) { Fabricate(:user, trust_level: TrustLevel.levels[:newuser]) }
- let(:post_one_link) { Fabricate.build(:post, post_args.merge(raw: "[sherlock](http://www.bbc.co.uk/programmes/b018ttws)", user: newuser)) }
- let(:post_two_links) { Fabricate.build(:post, post_args.merge(raw: "discourse twitter", user: newuser)) }
- let(:post_with_mentions) { Fabricate.build(:post, post_args.merge(raw: "hello @#{newuser.username} how are you doing?") )}
+ let(:post_one_link) { post_with_body("[sherlock](http://www.bbc.co.uk/programmes/b018ttws)", newuser) }
+ let(:post_two_links) { post_with_body("discourse twitter", newuser) }
+ let(:post_with_mentions) { post_with_body("hello @#{newuser.username} how are you doing?", newuser) }
it "returns 0 links for an empty post" do
Fabricate.build(:post).link_count.should == 0
@@ -251,8 +325,8 @@ describe Post do
context "max mentions" do
let(:newuser) { Fabricate(:user, trust_level: TrustLevel.levels[:newuser]) }
- let(:post_with_one_mention) { Fabricate.build(:post, post_args.merge(raw: "@Jake is the person I'm mentioning", user: newuser)) }
- let(:post_with_two_mentions) { Fabricate.build(:post, post_args.merge(raw: "@Jake @Finn are the people I'm mentioning", user: newuser)) }
+ let(:post_with_one_mention) { post_with_body("@Jake is the person I'm mentioning", newuser) }
+ let(:post_with_two_mentions) { post_with_body("@Jake @Finn are the people I'm mentioning", newuser) }
context 'new user' do
before do
@@ -298,7 +372,7 @@ describe Post do
context "raw_hash" do
let(:raw) { "this is our test post body"}
- let(:post) { Fabricate.build(:post, raw: raw) }
+ let(:post) { post_with_body(raw) }
it "returns a value" do
post.raw_hash.should be_present
@@ -310,19 +384,19 @@ describe Post do
end
it "returns the same value for the same raw" do
- post.raw_hash.should == Fabricate.build(:post, raw: raw).raw_hash
+ post.raw_hash.should == post_with_body(raw).raw_hash
end
it "returns a different value for a different raw" do
- post.raw_hash.should_not == Fabricate.build(:post, raw: "something else").raw_hash
+ post.raw_hash.should_not == post_with_body("something else").raw_hash
end
it "returns the same hash even with different white space" do
- post.raw_hash.should == Fabricate.build(:post, raw: " thisis ourt est postbody").raw_hash
+ post.raw_hash.should == post_with_body(" thisis ourt est postbody").raw_hash
end
it "returns the same hash even with different text case" do
- post.raw_hash.should == Fabricate.build(:post, raw: "THIS is OUR TEST post BODy").raw_hash
+ post.raw_hash.should == post_with_body("THIS is OUR TEST post BODy").raw_hash
end
end
@@ -600,12 +674,12 @@ describe Post do
end
describe 'urls' do
- it 'no-ops for empty list' do
+ it 'no-ops for empty list' do
Post.urls([]).should == {}
end
- # integration test -> should move to centralized integration test
- it 'finds urls for posts presented' do
+ # integration test -> should move to centralized integration test
+ it 'finds urls for posts presented' do
p1 = Fabricate(:post)
p2 = Fabricate(:post)
Post.urls([p1.id, p2.id]).should == {p1.id => p1.url, p2.id => p2.url}
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b83513ff5..061f87b1f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -266,7 +266,7 @@ describe User do
describe "trust levels" do
- # NOTE be sure to use build to avoid db calls
+ # NOTE be sure to use build to avoid db calls
let(:user) { Fabricate.build(:user, trust_level: TrustLevel.levels[:newuser]) }
it "sets to the default trust level setting" do
@@ -770,6 +770,31 @@ describe User do
end
end
+ describe "flag_linked_posts_as_spam" do
+ let(:user) { Fabricate(:user) }
+ let!(:admin) { Fabricate(:admin) }
+ let!(:post) { PostCreator.new(user, title: "this topic contains spam", raw: "this post has a link: http://discourse.org").create }
+ let!(:another_post) { PostCreator.new(user, title: "this topic also contains spam", raw: "this post has a link: http://discourse.org/asdfa").create }
+ let!(:post_without_link) { PostCreator.new(user, title: "this topic shouldn't be spam", raw: "this post has no links in it.").create }
+
+ it "has flagged all the user's posts as spam" do
+ user.flag_linked_posts_as_spam
+
+ post.reload
+ post.spam_count.should == 1
+
+ another_post.reload
+ another_post.spam_count.should == 1
+
+ post_without_link.reload
+ post_without_link.spam_count.should == 0
+
+ # It doesn't raise an exception if called again
+ user.flag_linked_posts_as_spam
+
+ end
+
+ end
describe 'update_time_read!' do
let(:user) { Fabricate(:user) }