GitLab wurde erfolgreich aktualisiert. Dank regelmäßiger Updates bleibt das THM GitLab sicher und Sie profitieren von den neuesten Funktionen. Danke für Ihre Geduld.

Commit 7fef2f7b authored by Robert Speicher's avatar Robert Speicher

Merge branch 'akismet-submittable' into 'master'

Submit to Akismet Part 1 (Issues)

Related to #5932 #5573 gitlab-com/infrastructure#14

See merge request !5538
parents 12918b0b 5994c119
......@@ -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
......
......@@ -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 {
......
......@@ -14,4 +14,14 @@ def destroy
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
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
......@@ -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 @@ def issue
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)
......
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 @@ def spam?
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
......@@ -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 @@ def publicly_visible?
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
......@@ -7,4 +7,8 @@ def remove_user
user.block
user.destroy
end
def text
[title, description].join("\n")
end
end
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
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
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
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
......@@ -3,29 +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 = 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
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
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
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
......@@ -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
......
......@@ -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
......
......@@ -252,7 +252,11 @@
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 @@
member do
post :toggle_subscription
post :toggle_award_emoji
post :mark_as_spam
get :referenced_merge_requests
get :related_branches
get :can_create_branch
......
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
# 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
# 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