# frozen_string_literal: true

# = DenylistValidator
#
# A naively simple mixin used to ban words in ActiveModel objects.
#
# == Usage
#
# Let's say that your applications lets people post messages, but don't want
# them using the word "viagra" in posts as a naively simple way of preventing
# spam.
#
# You'd first create a config/denylist.txt file with a line like:
#   \bviagrab\b
#
# And then you'd include the denylisting feature into your Message class like:
#
#   class Message < Calagator::ApplicationRecord
#     validates :title, :content, denylist: true
#   end
#
# Now including the word "viagra" in your record's values will fail:
#
#   message = Message.new(title: "foo viagra bar")
#   message.valid? # => false
#
# Available validator options:
#   * patterns: Array of regular expressions that will be matched
#     against the given attribute contents and any matches will cause the
#     record to be marked invalid.
#   * denylist: Reads an array of denylisted regular expressions from
#     a filename.
#   * message: Error message to use on invalid records.
#
# If no :patterns or :denylist is given, patterns are read from:
#   * config/denylist.txt
#   * config/denylist-local.txt

class DenylistValidator < ActiveModel::EachValidator
  DENYLIST_DEFAULT_MESSAGE = "contains denylisted content"

  def validate_each(record, attribute, value)
    if value.present? && patterns.any? { |pattern| value.match(pattern) }
      record.errors.add attribute, message
    end
  end

  private

  def message
    options.fetch(:message, DENYLIST_DEFAULT_MESSAGE)
  end

  def patterns
    @patterns ||= options.fetch(:patterns) do
      [
        Calagator.denylist_patterns,
        get_denylist_patterns_from(options.fetch(:denylist, "denylist.txt"))
      ].flatten.compact
    end
  end

  def get_denylist_patterns_from(filename)
    unless %r{[/\\]}.match?(filename)
      filename = Rails.root.join("config", filename)
    end
    return unless File.exist?(filename)

    File.readlines(filename).map do |line|
      Regexp.new(line.strip, Regexp::IGNORECASE)
    end
  end
end
