Commit 64ab2b3d authored by Patricio Cano's avatar Patricio Cano
Browse files

Refactored spam related code even further

- Removed unnecessary column from `SpamLog`
- Moved creation of SpamLogs out of its own service and into SpamCheckService
- Simplified code in SpamCheckService.
- Moved move spam related code into Spammable concern
parent bf9844c6ce67
......@@ -14,4 +14,10 @@ def destroy
head :ok
end
end
def ham
spam_log = SpamLog.find(params[:id])
Gitlab::AkismetHelper.ham!(spam_log.source_ip, spam_log.user_agent, spam_log.description, spam_log.user)
end
end
......@@ -28,26 +28,42 @@ def can_be_submitted?
end
end
def submit_ham
return unless akismet_enabled? && can_be_submitted?
ham!(user_agent_detail, spammable_text, creator)
end
def submit_spam
return unless akismet_enabled? && can_be_submitted?
spam!(user_agent_detail, spammable_text, creator)
spam!(user_agent_detail, spammable_text, owner)
end
def spam?(env, user)
is_spam?(env, user, spammable_text)
def spam_detected?(env)
@spam = is_spam?(env, owner, spammable_text)
end
def spam_detected?
def spam?
@spam
end
def check_for_spam
self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam_detected?
self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
end
def owner_id
if self.respond_to?(:author_id)
self.author_id
elsif self.respond_to?(:creator_id)
self.creator_id
end
end
# Override this method if an additional check is needed before calling Akismet
def check_for_spam?
akismet_enabled?
end
def spam_title
raise 'Implement in included model!'
end
def spam_description
raise 'Implement in included model!'
end
private
......@@ -60,11 +76,7 @@ def spammable_text
result.reject(&:blank?).join("\n")
end
def creator
if self.author_id
User.find(self.author_id)
elsif self.creator_id
User.find(self.creator_id)
end
def owner
User.find(owner_id)
end
end
......@@ -265,4 +265,17 @@ def publicly_visible?
def overdue?
due_date.try(:past?) || false
end
# To allow polymorphism with Spammable
def check_for_spam?
super && project.public?
end
def spam_title
title
end
def spam_description
description
end
end
class CreateSpamLogService < BaseService
def initialize(project, user, params)
super(project, user, params)
end
def execute
spam_params = params.merge({ user_id: @current_user.id,
project_id: @project.id } )
spam_log = SpamLog.new(spam_params)
spam_log.save
spam_log
end
end
......@@ -3,34 +3,34 @@ class CreateService < Issues::BaseService
def execute
filter_params
label_params = params.delete(:label_ids)
request = params.delete(:request)
api = params.delete(:api)
issue = project.issues.new(params)
issue.author = params[:author] || current_user
@request = params.delete(:request)
@api = params.delete(:api)
@issue = project.issues.new(params)
@issue.author = params[:author] || current_user
issue.spam = spam_check_service.execute(request, api, issue)
spam_check_service.execute
if issue.save
issue.update_attributes(label_ids: label_params)
notification_service.new_issue(issue, current_user)
todo_service.new_issue(issue, current_user)
event_service.open_issue(issue, current_user)
user_agent_detail_service(issue, request).create
issue.create_cross_references!(current_user)
execute_hooks(issue, 'open')
if @issue.save
@issue.update_attributes(label_ids: label_params)
notification_service.new_issue(@issue, current_user)
todo_service.new_issue(@issue, current_user)
event_service.open_issue(@issue, current_user)
user_agent_detail_service.create
@issue.create_cross_references!(current_user)
execute_hooks(@issue, 'open')
end
issue
@issue
end
private
def spam_check_service
SpamCheckService.new(project, current_user, params)
SpamCheckService.new(@request, @api, @issue)
end
def user_agent_detail_service(issue, request)
UserAgentDetailService.new(issue, request)
def user_agent_detail_service
UserAgentDetailService.new(@issue, @request)
end
end
end
class SpamCheckService < BaseService
attr_accessor :request, :api, :subject
class SpamCheckService
attr_accessor :request, :api, :spammable
def execute(request, api, subject)
@request, @api, @subject = request, api, subject
return false unless request || subject.check_for_spam?(project)
return false unless subject.spam?(request.env, current_user)
create_spam_log
def initialize(request, api, spammable)
@request, @api, @spammable = request, api, spammable
end
true
def execute
if request && spammable.check_for_spam?
if spammable.spam_detected?(request.env)
create_spam_log
end
end
end
private
def spam_log_attrs
{
user_id: current_user.id,
project_id: project.id,
title: params[:title],
description: params[:description],
source_ip: subject.client_ip(request.env),
user_agent: subject.user_agent(request.env),
noteable_type: subject.class.to_s,
user_id: spammable.owner_id,
title: spammable.spam_title,
description: spammable.spam_description,
source_ip: spammable.client_ip(request.env),
user_agent: spammable.user_agent(request.env),
noteable_type: spammable.class.to_s,
via_api: api
}
end
def create_spam_log
CreateSpamLogService.new(project, current_user, spam_log_attrs).execute
SpamLog.create(spam_log_attrs)
end
end
class UserAgentDetailService
attr_accessor :subject, :request
attr_accessor :spammable, :request
def initialize(subject, request)
@subject, @request = subject, request
def initialize(spammable, request)
@spammable, @request = spammable, request
end
def create
return unless request
subject.create_user_agent_detail(user_agent: request.env['HTTP_USER_AGENT'], ip_address: request.env['action_dispatch.remote_ip'].to_s)
spammable.create_user_agent_detail(user_agent: request.env['HTTP_USER_AGENT'], ip_address: request.env['action_dispatch.remote_ip'].to_s)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveProjectIdFromSpamLogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = true
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
DOWNTIME_REASON = 'Removing a table that contains data that is not used anywhere.'
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
remove_column :spam_logs, :project_id, :integer
end
end
......@@ -926,7 +926,6 @@
t.string "source_ip"
t.string "user_agent"
t.boolean "via_api"
t.integer "project_id"
t.string "noteable_type"
t.string "title"
t.text "description"
......
......@@ -160,7 +160,7 @@ def filter_issues_milestone(issues, milestone)
issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute
if issue.spam_detected?
if issue.spam?
render_api_error!({ error: 'Spam detected' }, 400)
end
......
......@@ -17,10 +17,6 @@ def user_agent(env)
env['HTTP_USER_AGENT']
end
def check_for_spam?(project)
akismet_enabled? && project.public?
end
def is_spam?(environment, user, text)
client = akismet_client
ip_address = client_ip(environment)
......@@ -44,7 +40,7 @@ def is_spam?(environment, user, text)
end
end
def ham!(details, text, user)
def ham!(ip_address, user_agent, text, user)
client = akismet_client
params = {
......@@ -55,7 +51,7 @@ def ham!(details, text, user)
}
begin
client.submit_ham(details.ip_address, details.user_agent, params)
client.submit_ham(ip_address, user_agent, params)
rescue => e
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
end
......
......@@ -274,7 +274,7 @@ def go(id:)
describe 'POST #create' do
context 'Akismet is enabled' do
before do
allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(Spammable).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true)
end
......
......@@ -10,17 +10,6 @@
allow_any_instance_of(ApplicationSetting).to receive(:akismet_api_key).and_return('12345')
end
describe '#check_for_spam?' do
it 'returns true for public project' do
expect(helper.check_for_spam?(project)).to eq(true)
end
it 'returns false for private project' do
project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
expect(helper.check_for_spam?(project)).to eq(false)
end
end
describe '#is_spam?' do
it 'returns true for spam' do
environment = {
......
......@@ -15,7 +15,7 @@
describe 'InstanceMethods' do
it 'should return the correct creator' do
expect(issue.send(:creator).id).to eq(issue.author_id)
expect(issue.send(:owner).id).to eq(issue.author_id)
end
it 'should be invalid if spam' do
......@@ -27,15 +27,28 @@
create(:user_agent_detail, subject_id: issue.id, subject_type: issue.class.to_s)
expect(issue.can_be_submitted?).to be_truthy
end
describe '#check_for_spam?' do
before do
allow_any_instance_of(Gitlab::AkismetHelper).to receive(:akismet_enabled?).and_return(true)
end
it 'returns true for public project' do
issue.project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
expect(issue.check_for_spam?).to eq(true)
end
it 'returns false for other visibility levels' do
expect(issue.check_for_spam?).to eq(false)
end
end
end
describe 'AkismetMethods' do
before do
allow_any_instance_of(Gitlab::AkismetHelper).to receive_messages(check_for_spam?: true, is_spam?: true, ham!: nil, spam!: nil)
allow_any_instance_of(Gitlab::AkismetHelper).to receive_messages(is_spam?: true, spam!: nil)
end
it { expect(issue.spam?(:mock_env, :mock_user)).to be_truthy }
it { expect(issue.spam_detected?(:mock_env)).to be_truthy }
it { expect(issue.submit_spam).to be_nil }
it { expect(issue.submit_ham).to be_nil }
end
end
......@@ -531,7 +531,7 @@
describe 'POST /projects/:id/issues with spam filtering' do
before do
allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(Spammable).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true)
end
......@@ -554,7 +554,6 @@
expect(spam_logs[0].description).to eq('content here')
expect(spam_logs[0].user).to eq(user)
expect(spam_logs[0].noteable_type).to eq('Issue')
expect(spam_logs[0].project_id).to eq(project.id)
end
end
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment