Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRobert Speicher <robert@gitlab.com>2016-08-16 01:59:47 +0300
committerRobert Speicher <robert@gitlab.com>2016-08-16 01:59:47 +0300
commit7fef2f7b75e284b702f327cc073476724b51d5ac (patch)
tree05929a1c38682ee0bb17856492ff424915f85029
parent12918b0b9e99449181ce6f24c9bdb5d58ae6b89a (diff)
parent5994c11910822463faeabb7b5f11d6529036db9d (diff)
Merge branch 'akismet-submittable' into 'master'
Submit to Akismet Part 1 (Issues) Related to #5932 #5573 gitlab-com/infrastructure#14 See merge request !5538
-rw-r--r--CHANGELOG1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/controllers/admin/spam_logs_controller.rb10
-rw-r--r--app/controllers/concerns/spammable_actions.rb25
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/models/concerns/spammable.rb52
-rw-r--r--app/models/issue.rb8
-rw-r--r--app/models/spam_log.rb4
-rw-r--r--app/models/user_agent_detail.rb9
-rw-r--r--app/services/akismet_service.rb79
-rw-r--r--app/services/create_spam_log_service.rb13
-rw-r--r--app/services/ham_service.rb26
-rw-r--r--app/services/issues/create_service.rb35
-rw-r--r--app/services/spam_check_service.rb38
-rw-r--r--app/services/spam_service.rb78
-rw-r--r--app/services/user_agent_detail_service.rb13
-rw-r--r--app/views/admin/spam_logs/_spam_log.html.haml5
-rw-r--r--app/views/projects/issues/show.html.haml9
-rw-r--r--config/routes.rb7
-rw-r--r--db/migrate/20160727163552_create_user_agent_details.rb18
-rw-r--r--db/migrate/20160729173930_remove_project_id_from_spam_logs.rb29
-rw-r--r--db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb20
-rw-r--r--db/schema.rb12
-rw-r--r--doc/integration/akismet.md23
-rw-r--r--doc/integration/img/spam_log.pngbin0 -> 187190 bytes
-rw-r--r--doc/integration/img/submit_issue.pngbin0 -> 176450 bytes
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/gitlab/akismet_helper.rb47
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb12
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb50
-rw-r--r--spec/factories/user_agent_details.rb7
-rw-r--r--spec/lib/gitlab/akismet_helper_spec.rb35
-rw-r--r--spec/models/concerns/spammable_spec.rb33
-rw-r--r--spec/models/user_agent_detail_spec.rb31
-rw-r--r--spec/requests/api/issues_spec.rb5
35 files changed, 583 insertions, 159 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 616f006b29e..fc9291eefd5 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -82,6 +82,7 @@ v 8.11.0 (unreleased)
- Make "New issue" button in Issue page less obtrusive !5457 (winniehell)
- Gitlab::Metrics.current_transaction needs to be public for RailsQueueDuration
- Fix search for notes which belongs to deleted objects
+ - Allow Akismet to be trained by submitting issues as spam or ham !5538
- Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
- Allow branch names ending with .json for graph and network page !5579 (winniehell)
- Add the `sprockets-es6` gem
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 473530cf094..f1fe1697d30 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -164,6 +164,10 @@
@include btn-outline($white-light, $orange-normal, $orange-normal, $orange-light, $white-light, $orange-light);
}
+ &.btn-spam {
+ @include btn-outline($white-light, $red-normal, $red-normal, $red-light, $white-light, $red-light);
+ }
+
&.btn-danger,
&.btn-remove,
&.btn-red {
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index 3a2f0185315..2abfa22712d 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -14,4 +14,14 @@ class Admin::SpamLogsController < Admin::ApplicationController
head :ok
end
end
+
+ def mark_as_ham
+ spam_log = SpamLog.find(params[:id])
+
+ if HamService.new(spam_log).mark_as_ham!
+ redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.'
+ else
+ redirect_to admin_spam_logs_path, alert: 'Error with Akismet. Please check the logs for more info.'
+ end
+ end
end
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
new file mode 100644
index 00000000000..29e243c66a3
--- /dev/null
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -0,0 +1,25 @@
+module SpammableActions
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :authorize_submit_spammable!, only: :mark_as_spam
+ end
+
+ def mark_as_spam
+ if SpamService.new(spammable).mark_as_spam!
+ redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
+ else
+ redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
+ end
+ end
+
+ private
+
+ def spammable
+ raise NotImplementedError, "#{self.class} does not implement #{__method__}"
+ end
+
+ def authorize_submit_spammable!
+ access_denied! unless current_user.admin?
+ end
+end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 660e0eba06f..e9fb11e8f94 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableActions
include ToggleAwardEmoji
include IssuableCollections
+ include SpammableActions
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
@@ -185,6 +186,7 @@ class Projects::IssuesController < Projects::ApplicationController
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
alias_method :awardable, :issue
+ alias_method :spammable, :issue
def authorize_read_issue!
return render_404 unless can?(current_user, :read_issue, @issue)
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index 3b8e6df2da9..ce54fe5d3bf 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -1,9 +1,32 @@
module Spammable
extend ActiveSupport::Concern
+ module ClassMethods
+ def attr_spammable(attr, options = {})
+ spammable_attrs << [attr.to_s, options]
+ end
+ end
+
included do
+ has_one :user_agent_detail, as: :subject, dependent: :destroy
+
attr_accessor :spam
+
after_validation :check_for_spam, on: :create
+
+ cattr_accessor :spammable_attrs, instance_accessor: false do
+ []
+ end
+
+ delegate :ip_address, :user_agent, to: :user_agent_detail, allow_nil: true
+ end
+
+ def submittable_as_spam?
+ if user_agent_detail
+ user_agent_detail.submittable?
+ else
+ false
+ end
end
def spam?
@@ -13,4 +36,33 @@ module Spammable
def check_for_spam
self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam?
end
+
+ def spam_title
+ attr = self.class.spammable_attrs.find do |_, options|
+ options.fetch(:spam_title, false)
+ end
+
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ end
+
+ def spam_description
+ attr = self.class.spammable_attrs.find do |_, options|
+ options.fetch(:spam_description, false)
+ end
+
+ public_send(attr.first) if attr && respond_to?(attr.first.to_sym)
+ end
+
+ def spammable_text
+ result = self.class.spammable_attrs.map do |attr|
+ public_send(attr.first)
+ end
+
+ result.reject(&:blank?).join("\n")
+ end
+
+ # Override in Spammable if further checks are necessary
+ def check_for_spam?
+ true
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d62ffb21467..788611305fe 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -36,6 +36,9 @@ class Issue < ActiveRecord::Base
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+ attr_spammable :title, spam_title: true
+ attr_spammable :description, spam_description: true
+
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
@@ -262,4 +265,9 @@ class Issue < ActiveRecord::Base
def overdue?
due_date.try(:past?) || false
end
+
+ # Only issues on public projects should be checked for spam
+ def check_for_spam?
+ project.public?
+ end
end
diff --git a/app/models/spam_log.rb b/app/models/spam_log.rb
index 12df68ef83b..3b8b9833565 100644
--- a/app/models/spam_log.rb
+++ b/app/models/spam_log.rb
@@ -7,4 +7,8 @@ class SpamLog < ActiveRecord::Base
user.block
user.destroy
end
+
+ def text
+ [title, description].join("\n")
+ end
end
diff --git a/app/models/user_agent_detail.rb b/app/models/user_agent_detail.rb
new file mode 100644
index 00000000000..0949c6ef083
--- /dev/null
+++ b/app/models/user_agent_detail.rb
@@ -0,0 +1,9 @@
+class UserAgentDetail < ActiveRecord::Base
+ belongs_to :subject, polymorphic: true
+
+ validates :user_agent, :ip_address, :subject_id, :subject_type, presence: true
+
+ def submittable?
+ !submitted?
+ end
+end
diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb
new file mode 100644
index 00000000000..5c60addbe7c
--- /dev/null
+++ b/app/services/akismet_service.rb
@@ -0,0 +1,79 @@
+class AkismetService
+ attr_accessor :owner, :text, :options
+
+ def initialize(owner, text, options = {})
+ @owner = owner
+ @text = text
+ @options = options
+ end
+
+ def is_spam?
+ return false unless akismet_enabled?
+
+ params = {
+ type: 'comment',
+ text: text,
+ created_at: DateTime.now,
+ author: owner.name,
+ author_email: owner.email,
+ referrer: options[:referrer],
+ }
+
+ begin
+ is_spam, is_blatant = akismet_client.check(options[:ip_address], options[:user_agent], params)
+ is_spam || is_blatant
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
+ false
+ end
+ end
+
+ def submit_ham
+ return false unless akismet_enabled?
+
+ params = {
+ type: 'comment',
+ text: text,
+ author: owner.name,
+ author_email: owner.email
+ }
+
+ begin
+ akismet_client.submit_ham(options[:ip_address], options[:user_agent], params)
+ true
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
+ false
+ end
+ end
+
+ def submit_spam
+ return false unless akismet_enabled?
+
+ params = {
+ type: 'comment',
+ text: text,
+ author: owner.name,
+ author_email: owner.email
+ }
+
+ begin
+ akismet_client.submit_spam(options[:ip_address], options[:user_agent], params)
+ true
+ rescue => e
+ Rails.logger.error("Unable to connect to Akismet: #{e}, skipping!")
+ false
+ end
+ end
+
+ private
+
+ def akismet_client
+ @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
+ Gitlab.config.gitlab.url)
+ end
+
+ def akismet_enabled?
+ current_application_settings.akismet_enabled
+ end
+end
diff --git a/app/services/create_spam_log_service.rb b/app/services/create_spam_log_service.rb
deleted file mode 100644
index 59a66fde47a..00000000000
--- a/app/services/create_spam_log_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-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
diff --git a/app/services/ham_service.rb b/app/services/ham_service.rb
new file mode 100644
index 00000000000..b0e1799b489
--- /dev/null
+++ b/app/services/ham_service.rb
@@ -0,0 +1,26 @@
+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
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 5e2de2ccf64..65550ab8ec6 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -3,29 +3,34 @@ module Issues
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 = spam_service.check(@api)
- 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)
- 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)
+ def spam_service
+ SpamService.new(@issue, @request)
+ end
+
+ def user_agent_detail_service
+ UserAgentDetailService.new(@issue, @request)
end
end
end
diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb
deleted file mode 100644
index 7c3e692bde9..00000000000
--- a/app/services/spam_check_service.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-class SpamCheckService < BaseService
- include Gitlab::AkismetHelper
-
- attr_accessor :request, :api
-
- def execute(request, api)
- @request, @api = request, api
- return false unless request || check_for_spam?(project)
- return false unless is_spam?(request.env, current_user, text)
-
- create_spam_log
-
- true
- end
-
- private
-
- def text
- [params[:title], params[:description]].reject(&:blank?).join("\n")
- end
-
- def spam_log_attrs
- {
- user_id: current_user.id,
- project_id: project.id,
- title: params[:title],
- description: params[:description],
- source_ip: client_ip(request.env),
- user_agent: user_agent(request.env),
- noteable_type: 'Issue',
- via_api: api
- }
- end
-
- def create_spam_log
- CreateSpamLogService.new(project, current_user, spam_log_attrs).execute
- end
-end
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
new file mode 100644
index 00000000000..48903291799
--- /dev/null
+++ b/app/services/spam_service.rb
@@ -0,0 +1,78 @@
+class SpamService
+ attr_accessor :spammable, :request, :options
+
+ def initialize(spammable, request = nil)
+ @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
+
+ def check(api = false)
+ return false unless request && check_for_spam?
+
+ return false unless akismet.is_spam?
+
+ create_spam_log(api)
+ true
+ end
+
+ def mark_as_spam!
+ return false unless spammable.submittable_as_spam?
+
+ if akismet.submit_spam
+ spammable.user_agent_detail.update_attribute(:submitted, true)
+ else
+ false
+ end
+ end
+
+ private
+
+ def akismet
+ @akismet ||= AkismetService.new(
+ spammable_owner,
+ spammable.spammable_text,
+ options
+ )
+ end
+
+ def spammable_owner
+ @user ||= User.find(spammable_owner_id)
+ end
+
+ def spammable_owner_id
+ @owner_id ||=
+ if spammable.respond_to?(:author_id)
+ spammable.author_id
+ elsif spammable.respond_to?(:creator_id)
+ spammable.creator_id
+ end
+ end
+
+ def check_for_spam?
+ spammable.check_for_spam?
+ end
+
+ def create_spam_log(api)
+ SpamLog.create(
+ {
+ user_id: spammable_owner_id,
+ title: spammable.spam_title,
+ description: spammable.spam_description,
+ source_ip: options[:ip_address],
+ user_agent: options[:user_agent],
+ noteable_type: spammable.class.to_s,
+ via_api: api
+ }
+ )
+ end
+end
diff --git a/app/services/user_agent_detail_service.rb b/app/services/user_agent_detail_service.rb
new file mode 100644
index 00000000000..a1ee3df5fe1
--- /dev/null
+++ b/app/services/user_agent_detail_service.rb
@@ -0,0 +1,13 @@
+class UserAgentDetailService
+ attr_accessor :spammable, :request
+
+ def initialize(spammable, request)
+ @spammable, @request = spammable, request
+ end
+
+ def create
+ 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)
+ end
+end
diff --git a/app/views/admin/spam_logs/_spam_log.html.haml b/app/views/admin/spam_logs/_spam_log.html.haml
index 8aea67f4497..4ce4eab8753 100644
--- a/app/views/admin/spam_logs/_spam_log.html.haml
+++ b/app/views/admin/spam_logs/_spam_log.html.haml
@@ -24,6 +24,11 @@
= link_to 'Remove user', admin_spam_log_path(spam_log, remove_user: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
%td
+ - if spam_log.submitted_as_ham?
+ .btn.btn-xs.disabled
+ Submitted as ham
+ - else
+ = link_to 'Submit as ham', mark_as_ham_admin_spam_log_path(spam_log), method: :post, class: 'btn btn-xs btn-warning'
- if user && !user.blocked?
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
- else
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index e5cce16a171..9f1a046ea74 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -37,14 +37,19 @@
= 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
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
+ - if @issue.submittable_as_spam? && current_user.admin?
+ %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'
+
- 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
New 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 '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_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' do
- Edit
+ - if @issue.submittable_as_spam? && current_user.admin?
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
.issue-details.issuable-details
diff --git a/config/routes.rb b/config/routes.rb
index cf6773917cf..1d2db91344f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -252,7 +252,11 @@ Rails.application.routes.draw do
resource :impersonation, only: :destroy
resources :abuse_reports, only: [:index, :destroy]
- resources :spam_logs, only: [:index, :destroy]
+ resources :spam_logs, only: [:index, :destroy] do
+ member do
+ post :mark_as_ham
+ end
+ end
resources :applications
@@ -813,6 +817,7 @@ Rails.application.routes.draw do
member do
post :toggle_subscription
post :toggle_award_emoji
+ post :mark_as_spam
get :referenced_merge_requests
get :related_branches
get :can_create_branch
diff --git a/db/migrate/20160727163552_create_user_agent_details.rb b/db/migrate/20160727163552_create_user_agent_details.rb
new file mode 100644
index 00000000000..ed4ccfedc0a
--- /dev/null
+++ b/db/migrate/20160727163552_create_user_agent_details.rb
@@ -0,0 +1,18 @@
+class CreateUserAgentDetails < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :user_agent_details do |t|
+ t.string :user_agent, null: false
+ t.string :ip_address, null: false
+ t.integer :subject_id, null: false
+ t.string :subject_type, null: false
+ t.boolean :submitted, default: false, null: false
+
+ t.timestamps null: false
+ end
+ end
+end
diff --git a/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
new file mode 100644
index 00000000000..e28ab31d629
--- /dev/null
+++ b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb
@@ -0,0 +1,29 @@
+# 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 column 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
diff --git a/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
new file mode 100644
index 00000000000..296f1dfac7b
--- /dev/null
+++ b/db/migrate/20160801163709_add_submitted_as_ham_to_spam_logs.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddSubmittedAsHamToSpamLogs < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # 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 = ''
+
+ disable_ddl_transaction!
+
+ def change
+ add_column_with_default :spam_logs, :submitted_as_ham, :boolean, default: false
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f008a6bd7a7..52ba60ace11 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -926,12 +926,12 @@ ActiveRecord::Schema.define(version: 20160810142633) do
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"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.boolean "submitted_as_ham", default: false, null: false
end
create_table "subscriptions", force: :cascade do |t|
@@ -999,6 +999,16 @@ ActiveRecord::Schema.define(version: 20160810142633) do
add_index "u2f_registrations", ["key_handle"], name: "index_u2f_registrations_on_key_handle", using: :btree
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
+ create_table "user_agent_details", force: :cascade do |t|
+ t.string "user_agent", null: false
+ t.string "ip_address", null: false
+ t.integer "subject_id", null: false
+ t.string "subject_type", null: false
+ t.boolean "submitted", default: false, null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md
index c222d21612f..06c787cfcc7 100644
--- a/doc/integration/akismet.md
+++ b/doc/integration/akismet.md
@@ -33,3 +33,26 @@ To use Akismet:
7. Save the configuration.
![Screenshot of Akismet settings](img/akismet_settings.png)
+
+
+## Training
+
+> *Note:* Training the Akismet filter is only available in 8.11 and above.
+
+As a way to better recognize between spam and ham, you can train the Akismet
+filter whenever there is a false positive or false negative.
+
+When an entry is recognized as spam, it is rejected and added to the Spam Logs.
+From here you can review if they are really spam. If one of them is not really
+spam, you can use the `Submit as ham` button to tell Akismet that it falsely
+recognized an entry as spam.
+
+![Screenshot of Spam Logs](img/spam_log.png)
+
+If an entry that is actually spam was not recognized as such, you will be able
+to also submit this to Akismet. The `Submit as spam` button will only appear
+to admin users.
+
+![Screenshot of Issue](img/submit_issue.png)
+
+Training Akismet will help it to recognize spam more accurately in the future.
diff --git a/doc/integration/img/spam_log.png b/doc/integration/img/spam_log.png
new file mode 100644
index 00000000000..8d574448690
--- /dev/null
+++ b/doc/integration/img/spam_log.png
Binary files differ
diff --git a/doc/integration/img/submit_issue.png b/doc/integration/img/submit_issue.png
new file mode 100644
index 00000000000..9fd9c20f6b3
--- /dev/null
+++ b/doc/integration/img/submit_issue.png
Binary files differ
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index c4d3134da6c..077258faee1 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -3,8 +3,6 @@ module API
class Issues < Grape::API
before { authenticate! }
- helpers ::Gitlab::AkismetHelper
-
helpers do
def filter_issues_state(issues, state)
case state
diff --git a/lib/gitlab/akismet_helper.rb b/lib/gitlab/akismet_helper.rb
deleted file mode 100644
index 207736b59db..00000000000
--- a/lib/gitlab/akismet_helper.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Gitlab
- module AkismetHelper
- def akismet_enabled?
- current_application_settings.akismet_enabled
- end
-
- def akismet_client
- @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key,
- Gitlab.config.gitlab.url)
- end
-
- def client_ip(env)
- env['action_dispatch.remote_ip'].to_s
- end
-
- 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)
- user_agent = user_agent(environment)
-
- params = {
- type: 'comment',
- text: text,
- created_at: DateTime.now,
- author: user.name,
- author_email: user.email,
- referrer: environment['HTTP_REFERER'],
- }
-
- begin
- is_spam, is_blatant = client.check(ip_address, user_agent, params)
- is_spam || is_blatant
- rescue => e
- Rails.logger.error("Unable to connect to Akismet: #{e}, skipping check")
- false
- end
- end
- end
-end
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 520a4f6f9c5..585ca31389d 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -34,4 +34,16 @@ describe Admin::SpamLogsController do
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
+
+ describe '#mark_as_ham' do
+ before do
+ allow_any_instance_of(AkismetService).to receive(:submit_ham).and_return(true)
+ end
+ it 'submits the log as ham' do
+ post :mark_as_ham, id: first_spam.id
+
+ expect(response).to have_http_status(302)
+ expect(SpamLog.find(first_spam.id).submitted_as_ham).to be_truthy
+ end
+ end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index b6a0276846c..0836b71056c 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -274,8 +274,8 @@ describe Projects::IssuesController do
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(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
def post_spam_issue
@@ -300,6 +300,52 @@ describe Projects::IssuesController do
expect(spam_logs[0].title).to eq('Spam Title')
end
end
+
+ context 'user agent details are saved' do
+ before do
+ request.env['action_dispatch.remote_ip'] = '127.0.0.1'
+ end
+
+ def post_new_issue
+ sign_in(user)
+ project = create(:empty_project, :public)
+ post :create, {
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ issue: { title: 'Title', description: 'Description' }
+ }
+ end
+
+ it 'creates a user agent detail' do
+ expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
+ end
+ end
+ end
+
+ describe 'POST #mark_as_spam' do
+ context 'properly submits to Akismet' do
+ before do
+ allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true)
+ allow_any_instance_of(ApplicationSetting).to receive_messages(akismet_enabled: true)
+ end
+
+ def post_spam
+ admin = create(:admin)
+ create(:user_agent_detail, subject: issue)
+ project.team << [admin, :master]
+ sign_in(admin)
+ post :mark_as_spam, {
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: issue.iid
+ }
+ end
+
+ it 'updates issue' do
+ post_spam
+ expect(issue.submittable_as_spam?).to be_falsey
+ end
+ end
end
describe "DELETE #destroy" do
diff --git a/spec/factories/user_agent_details.rb b/spec/factories/user_agent_details.rb
new file mode 100644
index 00000000000..9763cc0cf15
--- /dev/null
+++ b/spec/factories/user_agent_details.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :user_agent_detail do
+ ip_address '127.0.0.1'
+ user_agent 'AppleWebKit/537.36'
+ association :subject, factory: :issue
+ end
+end
diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb
deleted file mode 100644
index b08396da4d2..00000000000
--- a/spec/lib/gitlab/akismet_helper_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::AkismetHelper, type: :helper do
- let(:project) { create(:project, :public) }
- let(:user) { create(:user) }
-
- before do
- allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
- allow_any_instance_of(ApplicationSetting).to receive(:akismet_enabled).and_return(true)
- 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 = {
- 'action_dispatch.remote_ip' => '127.0.0.1',
- 'HTTP_USER_AGENT' => 'Test User Agent'
- }
-
- allow_any_instance_of(::Akismet::Client).to receive(:check).and_return([true, true])
- expect(helper.is_spam?(environment, user, 'Is this spam?')).to eq(true)
- end
- end
-end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
new file mode 100644
index 00000000000..32935bc0b09
--- /dev/null
+++ b/spec/models/concerns/spammable_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Issue, 'Spammable' do
+ let(:issue) { create(:issue, description: 'Test Desc.') }
+
+ describe 'Associations' do
+ it { is_expected.to have_one(:user_agent_detail).dependent(:destroy) }
+ end
+
+ describe 'ClassMethods' do
+ it 'should return correct attr_spammable' do
+ expect(issue.spammable_text).to eq("#{issue.title}\n#{issue.description}")
+ end
+ end
+
+ describe 'InstanceMethods' do
+ it 'should be invalid if spam' do
+ issue = build(:issue, spam: true)
+ expect(issue.valid?).to be_falsey
+ end
+
+ describe '#check_for_spam?' do
+ 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
+end
diff --git a/spec/models/user_agent_detail_spec.rb b/spec/models/user_agent_detail_spec.rb
new file mode 100644
index 00000000000..a8c25766e73
--- /dev/null
+++ b/spec/models/user_agent_detail_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+
+describe UserAgentDetail, type: :model do
+ describe '.submittable?' do
+ it 'is submittable when not already submitted' do
+ detail = build(:user_agent_detail)
+
+ expect(detail.submittable?).to be_truthy
+ end
+
+ it 'is not submittable when already submitted' do
+ detail = build(:user_agent_detail, submitted: true)
+
+ expect(detail.submittable?).to be_falsey
+ end
+ end
+
+ describe '.valid?' do
+ it 'is valid with a subject' do
+ detail = build(:user_agent_detail)
+
+ expect(detail).to be_valid
+ end
+
+ it 'is invalid without a subject' do
+ detail = build(:user_agent_detail, subject: nil)
+
+ expect(detail).not_to be_valid
+ end
+ end
+end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 3cd4e981fb2..a40e1a93b71 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -531,8 +531,8 @@ describe API::API, api: true do
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(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true)
end
let(:params) do
@@ -554,7 +554,6 @@ describe API::API, api: true do
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