otwarchive-symphonyarchive/app/models/abuse_report.rb
2026-03-11 22:22:11 +00:00

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