214 lines
7.4 KiB
Ruby
214 lines
7.4 KiB
Ruby
class AbuseReport < ApplicationRecord
|
|
validates :email, email_format: { allow_blank: false }
|
|
validates_presence_of :language
|
|
validates_presence_of :summary
|
|
validates_presence_of :comment
|
|
validates :url, presence: true, length: { maximum: 2080 }
|
|
validate :url_is_not_over_reported
|
|
validate :email_is_not_over_reporting
|
|
validates_length_of :summary, maximum: ArchiveConfig.FEEDBACK_SUMMARY_MAX,
|
|
too_long: ts('must be less than %{max}
|
|
characters long.',
|
|
max: ArchiveConfig.FEEDBACK_SUMMARY_MAX_DISPLAYED)
|
|
|
|
before_validation :truncate_url, if: :will_save_change_to_url?
|
|
|
|
# It doesn't have the type set properly in the database, so override it here:
|
|
attribute :summary_sanitizer_version, :integer, default: 0
|
|
|
|
# Truncates the user-provided URL to the maximum we can store in the database. We don't want to reject reports with very long URLs, but we need to do
|
|
# something to avoid a 500 error for long URLs.
|
|
def truncate_url
|
|
self.url = url[0..2079]
|
|
end
|
|
|
|
validate :check_for_spam
|
|
def check_for_spam
|
|
approved = logged_in_with_matching_email? || !Akismetor.spam?(akismet_attributes)
|
|
errors.add(:base, ts("This report looks like spam to our system!")) unless approved
|
|
end
|
|
|
|
def logged_in_with_matching_email?
|
|
User.current_user.present? && User.current_user.email.downcase == email.downcase
|
|
end
|
|
|
|
def akismet_attributes
|
|
name = username ? username : ""
|
|
# If the user is logged in and we're sending info to Akismet, we can assume
|
|
# the email does not match.
|
|
role = User.current_user.present? ? "user-with-nonmatching-email" : "guest"
|
|
{
|
|
comment_type: "contact-form",
|
|
key: ArchiveConfig.AKISMET_KEY,
|
|
blog: ArchiveConfig.AKISMET_NAME,
|
|
user_ip: ip_address,
|
|
user_role: role,
|
|
comment_author: name,
|
|
comment_author_email: email,
|
|
comment_content: comment
|
|
}
|
|
end
|
|
|
|
scope :by_date, -> { order('created_at DESC') }
|
|
|
|
# Standardize the format of work, chapter, and profile URLs to get it ready
|
|
# for the url_is_not_over_reported validation.
|
|
# Work URLs: "works/123"
|
|
# Chapter URLs: "chapters/123"
|
|
# Profile URLs: "users/username"
|
|
before_validation :standardize_url, on: :create
|
|
def standardize_url
|
|
return unless url =~ %r{((chapters|works)/\d+)} || url =~ %r{(users\/\w+)}
|
|
|
|
self.url = add_scheme_to_url(url)
|
|
self.url = clean_url(url)
|
|
self.url = add_work_id_to_url(self.url)
|
|
end
|
|
|
|
def add_scheme_to_url(url)
|
|
uri = Addressable::URI.parse(url)
|
|
return url unless uri.scheme.nil?
|
|
|
|
"https://#{uri}"
|
|
end
|
|
|
|
# Clean work or profile URLs so we can prevent the same URLs from getting
|
|
# reported too many times.
|
|
# If the URL ends without a / at the end, add it: url_is_not_over_reported
|
|
# uses the / so "/works/1234" isn't a match for "/works/123"
|
|
def clean_url(url)
|
|
uri = Addressable::URI.parse(url)
|
|
|
|
uri.query = nil
|
|
uri.fragment = nil
|
|
uri.path += "/" unless uri.path.end_with? "/"
|
|
|
|
uri.to_s
|
|
end
|
|
|
|
# Get the chapter id from the URL and try to get the work id
|
|
# If successful, add the work id to the URL in front of "/chapters"
|
|
def add_work_id_to_url(url)
|
|
return url unless url =~ %r{(chapters/\d+)} && url !~ %r{(works/\d+)}
|
|
|
|
chapter_regex = %r{(chapters/)(\d+)}
|
|
regex_groups = chapter_regex.match url
|
|
chapter_id = regex_groups[2]
|
|
work_id = Chapter.find_by(id: chapter_id).try(:work_id)
|
|
|
|
return url if work_id.nil?
|
|
|
|
uri = Addressable::URI.parse(url)
|
|
uri.path = "/works/#{work_id}" + uri.path
|
|
|
|
uri.to_s
|
|
end
|
|
|
|
validate :url_on_archive, if: :will_save_change_to_url?
|
|
def url_on_archive
|
|
parsed_url = Addressable::URI.heuristic_parse(url)
|
|
errors.add(:url, :not_on_archive) unless ArchiveConfig.PERMITTED_HOSTS.include?(parsed_url.host)
|
|
rescue Addressable::URI::InvalidURIError
|
|
errors.add(:url, :not_on_archive)
|
|
end
|
|
|
|
def email_and_send
|
|
UserMailer.abuse_report(id).deliver_later
|
|
send_report
|
|
end
|
|
|
|
def send_report
|
|
return unless zoho_enabled?
|
|
|
|
reporter = AbuseReporter.new(
|
|
title: summary,
|
|
description: comment,
|
|
language: language,
|
|
email: email,
|
|
username: username,
|
|
ip_address: ip_address,
|
|
url: url,
|
|
creator_ids: creator_ids
|
|
)
|
|
response = reporter.send_report!
|
|
ticket_id = response["id"]
|
|
return if ticket_id.blank?
|
|
|
|
attach_work_download(ticket_id)
|
|
end
|
|
|
|
def creator_ids
|
|
work_id = reported_work_id
|
|
return unless work_id
|
|
|
|
work = Work.find_by(id: work_id)
|
|
return "deletedwork" unless work
|
|
|
|
ids = work.pseuds.pluck(:user_id).push(*work.original_creators.pluck(:user_id)).uniq.sort
|
|
ids.prepend("orphanedwork") if ids.delete(User.orphan_account.id)
|
|
ids.join(", ")
|
|
end
|
|
|
|
# ID of the reported work, unless the report is about comment(s) on the work
|
|
def reported_work_id
|
|
comments = url[%r{/comments/}, 0]
|
|
url[%r{/works/(\d+)}, 1] if comments.nil?
|
|
end
|
|
|
|
def attach_work_download(ticket_id)
|
|
work_id = reported_work_id
|
|
return unless work_id
|
|
|
|
work = Work.find_by(id: work_id)
|
|
ReportAttachmentJob.perform_later(ticket_id, work) if work
|
|
end
|
|
|
|
# if the URL clearly belongs to a work (i.e. contains "/works/123")
|
|
# or a user profile (i.e. contains "/users/username")
|
|
# make sure it isn't reported more than ABUSE_REPORTS_PER_WORK_MAX
|
|
# or ABUSE_REPORTS_PER_USER_MAX times per month
|
|
def url_is_not_over_reported
|
|
message = ts('This page has already been reported. Our volunteers only
|
|
need one report in order to investigate and resolve an issue,
|
|
so please be patient and do not submit another report.')
|
|
if url =~ /\/works\/\d+/
|
|
# use "/works/123/" to avoid matching chapter or external work ids
|
|
work_params_only = url.match(/\/works\/\d+\//).to_s
|
|
existing_reports_total = AbuseReport.where('created_at > ? AND
|
|
url LIKE ?',
|
|
1.month.ago,
|
|
"%#{work_params_only}%").count
|
|
if existing_reports_total >= ArchiveConfig.ABUSE_REPORTS_PER_WORK_MAX
|
|
errors.add(:base, message)
|
|
end
|
|
elsif url =~ /\/users\/\w+/
|
|
user_params_only = url.match(/\/users\/\w+\//).to_s
|
|
existing_reports_total = AbuseReport.where('created_at > ? AND
|
|
url LIKE ?',
|
|
1.month.ago,
|
|
"%#{user_params_only}%").count
|
|
if existing_reports_total >= ArchiveConfig.ABUSE_REPORTS_PER_USER_MAX
|
|
errors.add(:base, message)
|
|
end
|
|
end
|
|
end
|
|
|
|
def email_is_not_over_reporting
|
|
existing_reports_total = AbuseReport.where("created_at > ? AND
|
|
email LIKE ?",
|
|
1.day.ago,
|
|
email).count
|
|
return if existing_reports_total < ArchiveConfig.ABUSE_REPORTS_PER_EMAIL_MAX
|
|
|
|
errors.add(:base, ts("You have reached our daily reporting limit. To keep our
|
|
volunteers from being overwhelmed, please do not seek
|
|
out violations to report, but only report violations you
|
|
encounter during your normal browsing."))
|
|
end
|
|
|
|
private
|
|
|
|
def zoho_enabled?
|
|
%w[staging production].include?(Rails.env)
|
|
end
|
|
end
|