Commit 5994c119 authored by Patricio Cano's avatar Patricio Cano

Further refactor and syntax fixes.

parent 43e756d4
class Admin::SpamLogsController < Admin::ApplicationController class Admin::SpamLogsController < Admin::ApplicationController
def index def index
@spam_logs = SpamLog.order(id: :desc).page(params[:page]) @spam_logs = SpamLog.order(id: :desc).page(params[:page])
end end
...@@ -19,10 +18,10 @@ def destroy ...@@ -19,10 +18,10 @@ def destroy
def mark_as_ham def mark_as_ham
spam_log = SpamLog.find(params[:id]) spam_log = SpamLog.find(params[:id])
if SpamService.new(spam_log).mark_as_ham! if HamService.new(spam_log).mark_as_ham!
redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.' redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.'
else else
redirect_to admin_spam_logs_path, notice: 'Error with Akismet. Please check the logs for more info.' redirect_to admin_spam_logs_path, alert: 'Error with Akismet. Please check the logs for more info.'
end end
end end
end end
...@@ -6,18 +6,17 @@ module SpammableActions ...@@ -6,18 +6,17 @@ module SpammableActions
end end
def mark_as_spam def mark_as_spam
if SpamService.new(spammable).mark_as_spam!(current_user) if SpamService.new(spammable).mark_as_spam!
redirect_to spammable, notice: 'Issue was submitted to Akismet successfully.' redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
else else
flash[:error] = 'Error with Akismet. Please check the logs for more info.' redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
redirect_to spammable
end end
end end
private private
def spammable def spammable
raise NotImplementedError raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end end
def authorize_submit_spammable! def authorize_submit_spammable!
......
...@@ -9,16 +9,19 @@ def attr_spammable(attr, options = {}) ...@@ -9,16 +9,19 @@ def attr_spammable(attr, options = {})
included do included do
has_one :user_agent_detail, as: :subject, dependent: :destroy has_one :user_agent_detail, as: :subject, dependent: :destroy
attr_accessor :spam attr_accessor :spam
after_validation :spam_detected?, on: :create
after_validation :check_for_spam, on: :create
cattr_accessor :spammable_attrs, instance_accessor: false do cattr_accessor :spammable_attrs, instance_accessor: false do
[] []
end end
delegate :submitted?, to: :user_agent_detail, allow_nil: true
delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
end end
def can_be_submitted? def submittable_as_spam?
if user_agent_detail if user_agent_detail
user_agent_detail.submittable? user_agent_detail.submittable?
else else
...@@ -30,46 +33,29 @@ def spam? ...@@ -30,46 +33,29 @@ def spam?
@spam @spam
end end
def spam_detected? def check_for_spam
self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
end end
def owner_id
if self.respond_to?(:author_id)
self.author_id
elsif self.respond_to?(:creator_id)
self.creator_id
end
end
def owner
User.find(owner_id)
end
def spam_title def spam_title
attr = self.class.spammable_attrs.select do |_, options| attr = self.class.spammable_attrs.find do |_, options|
options.fetch(:spam_title, false) options.fetch(:spam_title, false)
end end
attr = attr[0].first public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
public_send(attr) if respond_to?(attr.to_sym)
end end
def spam_description def spam_description
attr = self.class.spammable_attrs.select do |_, options| attr = self.class.spammable_attrs.find do |_, options|
options.fetch(:spam_description, false) options.fetch(:spam_description, false)
end end
attr = attr[0].first public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
public_send(attr) if respond_to?(attr.to_sym)
end end
def spammable_text def spammable_text
result = [] result = self.class.spammable_attrs.map do |attr|
self.class.spammable_attrs.map do |attr| public_send(attr.first)
result << public_send(attr.first)
end end
result.reject(&:blank?).join("\n") result.reject(&:blank?).join("\n")
...@@ -77,6 +63,6 @@ def spammable_text ...@@ -77,6 +63,6 @@ def spammable_text
# Override in Spammable if further checks are necessary # Override in Spammable if further checks are necessary
def check_for_spam? def check_for_spam?
current_application_settings.akismet_enabled true
end end
end end
...@@ -8,7 +8,6 @@ class Issue < ActiveRecord::Base ...@@ -8,7 +8,6 @@ class Issue < ActiveRecord::Base
include Taskable include Taskable
include Spammable include Spammable
include FasterCacheKeys include FasterCacheKeys
include AkismetSubmittable
DueDateStruct = Struct.new(:title, :name).freeze DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
...@@ -269,6 +268,6 @@ def overdue? ...@@ -269,6 +268,6 @@ def overdue?
# Only issues on public projects should be checked for spam # Only issues on public projects should be checked for spam
def check_for_spam? def check_for_spam?
super && project.public? project.public?
end end
end end
class UserAgentDetail < ActiveRecord::Base class UserAgentDetail < ActiveRecord::Base
belongs_to :subject, polymorphic: true belongs_to :subject, polymorphic: true
validates :user_agent, validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true
presence: true
validates :ip_address,
presence: true
validates :subject_id,
presence: true
validates :subject_type,
presence: true
def submittable? def submittable?
user_agent.present? && ip_address.present? !submitted?
end end
end end
class AkismetService class AkismetService
attr_accessor :spammable attr_accessor :owner, :text, :options
def initialize(spammable) def initialize(owner, text, options = {})
@spammable = spammable @owner = owner
@text = text
@options = options
end end
def client_ip(env) def is_spam?
env['action_dispatch.remote_ip'].to_s return false unless akismet_enabled?
end
def user_agent(env)
env['HTTP_USER_AGENT']
end
def is_spam?(environment)
ip_address = client_ip(environment)
user_agent = user_agent(environment)
params = { params = {
type: 'comment', type: 'comment',
text: spammable.spammable_text, text: text,
created_at: DateTime.now, created_at: DateTime.now,
author: spammable.owner.name, author: owner.name,
author_email: spammable.owner.email, author_email: owner.email,
referrer: environment['HTTP_REFERER'], referrer: options[:referrer],
} }
begin begin
is_spam, is_blatant = akismet_client.check(ip_address, user_agent, params) is_spam, is_blatant = akismet_client.check(options[:ip_address], options[:user_agent], params)
is_spam || is_blatant is_spam || is_blatant
rescue => e rescue => e
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check") Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
...@@ -35,16 +28,18 @@ def is_spam?(environment) ...@@ -35,16 +28,18 @@ def is_spam?(environment)
end end
end end
def ham! def submit_ham
return false unless akismet_enabled?
params = { params = {
type: 'comment', type: 'comment',
text: spammable.text, text: text,
author: spammable.user.name, author: owner.name,
author_email: spammable.user.email author_email: owner.email
} }
begin begin
akismet_client.submit_ham(spammable.source_ip, spammable.user_agent, params) akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
true true
rescue => e rescue => e
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
...@@ -52,16 +47,18 @@ def ham! ...@@ -52,16 +47,18 @@ def ham!
end end
end end
def spam! def submit_spam
return false unless akismet_enabled?
params = { params = {
type: 'comment', type: 'comment',
text: spammable.spammable_text, text: text,
author: spammable.owner.name, author: owner.name,
author_email: spammable.owner.email author_email: owner.email
} }
begin begin
akismet_client.submit_spam(spammable.user_agent_detail.ip_address, spammable.user_agent_detail.user_agent, params) akismet_client.submit_spam(options[:ip_address], options[:user_agent], params)
true true
rescue => e rescue => e
Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!") Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
...@@ -75,4 +72,8 @@ def akismet_client ...@@ -75,4 +72,8 @@ def akismet_client
@akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key, @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
Gitlab.config.gitlab.url) Gitlab.config.gitlab.url)
end end
def akismet_enabled?
current_application_settings.akismet_enabled
end
end end
class HamService
attr_accessor :spam_log
def initialize(spam_log)
@spam_log = spam_log
end
def mark_as_ham!
if akismet.submit_ham
spam_log.update_attribute(:submitted_as_ham, true)
else
false
end
end
private
def akismet
@akismet ||= AkismetService.new(
spam_log.user,
spam_log.text,
ip_address: spam_log.source_ip,
user_agent: spam_log.user_agent
)
end
end
...@@ -8,7 +8,7 @@ def execute ...@@ -8,7 +8,7 @@ def execute
@issue = project.issues.new(params) @issue = project.issues.new(params)
@issue.author = params[:author] || current_user @issue.author = params[:author] || current_user
@issue.spam = spam_service.check(@api, @request) @issue.spam = spam_service.check(@api)
if @issue.save if @issue.save
@issue.update_attributes(label_ids: label_params) @issue.update_attributes(label_ids: label_params)
...@@ -26,7 +26,7 @@ def execute ...@@ -26,7 +26,7 @@ def execute
private private
def spam_service def spam_service
SpamService.new(@issue) SpamService.new(@issue, @request)
end end
def user_agent_detail_service def user_agent_detail_service
......
class SpamService class SpamService
attr_accessor :spammable attr_accessor :spammable, :request, :options
def initialize(spammable) def initialize(spammable, request = nil)
@spammable = spammable @spammable = spammable
@request = request
@options = {}
if @request
@options[:ip_address] = @request.env['action_dispatch.remote_ip'].to_s
@options[:user_agent] = @request.env['HTTP_USER_AGENT']
@options[:referrer] = @request.env['HTTP_REFERRER']
else
@options[:ip_address] = @spammable.ip_address
@options[:user_agent] = @spammable.user_agent
end
end end
def check(api, request) def check(api = false)
return false unless request && spammable.check_for_spam? return false unless request && check_for_spam?
return false unless akismet.is_spam?(request.env)
create_spam_log(api, request) return false unless akismet.is_spam?
create_spam_log(api)
true true
end end
def mark_as_spam!(current_user) def mark_as_spam!
return false unless akismet_enabled? && spammable.can_be_submitted? return false unless spammable.submittable_as_spam?
if akismet.spam!
spammable.user_agent_detail.update_attribute(:submitted, true)
if spammable.is_a?(Issuable) if akismet.submit_spam
SystemNoteService.submit_spam(spammable, spammable.project, current_user) spammable.user_agent_detail.update_attribute(:submitted, true)
end
true
else else
false false
end end
end end
def mark_as_ham! private
return false unless spammable.is_a?(SpamLog)
if akismet.ham! def akismet
spammable.update_attribute(:submitted_as_ham, true) @akismet ||= AkismetService.new(
true spammable_owner,
else spammable.spammable_text,
false options
end )
end end
private def spammable_owner
@user ||= User.find(spammable_owner_id)
end
def akismet def spammable_owner_id
@akismet ||= AkismetService.new(spammable) @owner_id ||=
if spammable.respond_to?(:author_id)
spammable.author_id
elsif spammable.respond_to?(:creator_id)
spammable.creator_id
end
end end
def akismet_enabled? def check_for_spam?
current_application_settings.akismet_enabled spammable.check_for_spam?
end end
def create_spam_log(api, request) def create_spam_log(api)
SpamLog.create( SpamLog.create(
{ {
user_id: spammable.owner_id, user_id: spammable_owner_id,
title: spammable.spam_title, title: spammable.spam_title,
description: spammable.spam_description, description: spammable.spam_description,
source_ip: akismet.client_ip(request.env), source_ip: options[:ip_address],
user_agent: akismet.user_agent(request.env), user_agent: options[:user_agent],
noteable_type: spammable.class.to_s, noteable_type: spammable.class.to_s,
via_api: api via_api: api
} }
......
...@@ -395,23 +395,6 @@ def noteable_moved(noteable, project, noteable_ref, author, direction:) ...@@ -395,23 +395,6 @@ def noteable_moved(noteable, project, noteable_ref, author, direction:)
create_note(noteable: noteable, project: project, author: author, note: body) create_note(noteable: noteable, project: project, author: author, note: body)
end end
# Called when Issuable is submitted as spam
#
# noteable - Noteable object
# project - Project owning noteable
# author - User performing the change
#
# Example Note text:
#
# "Issue submitted as spam."
#
# Returns the created Note object
def submit_spam(noteable, project, author)
body = "Submitted this #{noteable.class.to_s.downcase} as spam"
create_note(noteable: noteable, project: project, author: author, note: body)
end
private private
def notes_for_mentioner(mentioner, noteable, notes) def notes_for_mentioner(mentioner, noteable, notes)
......
...@@ -7,6 +7,7 @@ def initialize(spammable, request) ...@@ -7,6 +7,7 @@ def initialize(spammable, request)
def create def create
return unless request return unless request
spammable.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
end end
...@@ -37,10 +37,9 @@ ...@@ -37,10 +37,9 @@
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%li %li
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue) = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.can_be_submitted? && current_user.admin? - if @issue.submittable_as_spam? && current_user.admin?
- unless @issue.submitted? %li
%li = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
- if can?(current_user, :create_issue, @project) - if can?(current_user, :create_issue, @project)
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
...@@ -48,10 +47,9 @@ ...@@ -48,10 +47,9 @@
- if can?(current_user, :update_issue, @issue) - if can?(current_user, :update_issue, @issue)
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' - if @issue.submittable_as_spam? && current_user.admin?
- if @issue.can_be_submitted? && current_user.admin?
- unless @issue.submitted?