module Defensio mattr_accessor :config_file self.config_file = RAILS_ROOT + '/config/defensio.yml' module ActsAs module ClassMethods # Add Defensio magic for managing a comment or something that # can be marked as Spam or Ham. # By default those following fields are maped to the column of the # same name, if the column exists: # # author, content, title, author_email, author_url, # user_logged_in, trusted_user, article.permalink # # See +acts_as_defensio+ for more details. def acts_as_defensio_comment(options={}) acts_as_defensio :comment, options end # Add Defensio magic for managing an article or something that # can be commented (spamed) about. # By default those following fields are maped to the column of the # same name, if the column exists: # # author, author_email, title, content, permalink # # By default the article will be published after creation. You can # override this behaviour with the :announce_when option. Specified # a method returning if the article needs to be announced to the # Defensio server. If you're using the :announce_when option, you'll # also need to create an announced column as this will be set to # +true+ when the article is published. # See +acts_as_defensio+ for more details. def acts_as_defensio_article(options={}) acts_as_defensio :article, options end # Add Defensio magic to an ActiveRecord class. # # Options are the same as +Defensio::Client#new+ plus the :fields # option that allows to customize the fields that will be sent to # Defensio to help the classification of a comment. # For example if the content of a comment is stored in the comment # column set the :fields option to { :content => :comment }. # Try to map as much column as possible as this helps Defensio to # classify one comment effectively. # # You can also set the type of comment with the :comment_type option. # # Usage: # # acts_as_defensio_comment :owner_url => 'http://code.macournoyer.com/svn/plugins/defensio', # :fields => { :content => :comment } # # # You must specify the :api_key and :owner_url option. # Other options are optional. # All options can be specified in a YAML config file by default in # RAILS_ROOT/config/defensio.yml. def acts_as_defensio(type, options={}) include InstanceMethods case type when :article if options.has_key? :announce_when after_save :announce_article else after_create :announce_article! end when :comment after_create :audit_comment end @defensio_type = type self.defensio_options = options @defensio = Defensio::Client.new(@defensio_options) unless defensio_options.has_key?(:validate_key) && !defensio_options[:validate_key] raise Defensio::InvalidAPIKey unless @defensio.validate_key.success? end end def defensio @defensio end def defensio_options=(options) @defensio_options = {} @defensio_options.merge! File.open(Defensio.config_file) { |file| YAML.load(file) }[ENV['RAILS_ENV']] if File.exists?(Defensio.config_file) @defensio_options.merge! options @defensio_options.symbolize_keys! end def defensio_options @defensio_options end def defensio_type @defensio_type end def defensio_fields(field) (@defensio_options[:fields] || {})[field] || field end def comment_type @defensio_options[:comment_type] || 'comment' end def defensio_stats @defensio.get_stats end end module InstanceMethods def self.included(base) base.class_eval do alias_method :report_as_spam, :report_as_false_negative alias_method :report_as_ham, :report_as_false_positive end end def audit_comment raise Defensio::Error, "You have to pass the current request environement:\n\t@comment.env = request.env" unless @env article_field = self.class.defensio_fields(:article) raise Defensio::Error, "You must specify an assiociated object which acts_as_defensio_article" unless respond_to? article_field article = send article_field fields = { :user_ip => @env['REMOTE_ADDR'].to_s.split(",").last.strip, :referrer => @env['HTTP_REFERER'], :article_date => convert_to_defensio_date(article.created_at), :comment_type => self.class.comment_type } fields.merge! extract_optional_fields_value(self, :author, :content, :title, :author_email, :author_url, :prefix => 'comment_') fields.merge! extract_optional_fields_value(self, :user_logged_in, :trusted_user) fields.merge! extract_optional_fields_value(article, :permalink) response = nil log_and_ignore_error do response = self.class.defensio.audit_comment fields end if response raise Defensio::Error, response.message unless response.success? self.signature = response.signature self.spam = response.spam self.spaminess = response.spaminess else # In case of an error from Defensio, we mark as spam but w/ low spaminess self.spam = true self.spaminess = 0 end save(false) end def announce_article announce_when = self.class.defensio_options[:announce_when] announced_field = self.class.defensio_fields(:announced) raise Defensio::Error, "announced field not found" unless respond_to? announced_field return unless send(announce_when) && !send(announced_field) announce_article! self.announced = true save(false) end def announce_article! fields = {} fields.merge! extract_optional_fields_value(self, :author, :author_email, :title, :content, :prefix => 'article_') fields.merge! extract_optional_fields_value(self, :permalink) response = nil log_and_ignore_error do response = self.class.defensio.announce_article fields end if response raise Defensio::Error, response.message unless response.success? end end def env=(env) @env = env end def report_as_false_negative log_and_ignore_error do self.class.defensio.report_false_negatives :signatures => self.signature end self.spam = true self.spaminess = 1.0 save(false) end def report_as_false_positive log_and_ignore_error do self.class.defensio.report_false_positives :signatures => self.signature end self.spam = false self.spaminess = 0.0 save(false) end private def convert_to_defensio_date(date) [date.strftime('%Y'), date.month, date.day] * '/' end def extract_optional_fields_value(object, *fields) options = fields.last.is_a?(Hash) ? fields.pop : {} prefix = options.delete(:prefix) fields.inject({}) do |hash, field| field_name = object.class.defensio_fields(field) hash[:"#{prefix}#{field}"] = object.send(field_name) if object.respond_to? field_name hash end end def log_and_ignore_error tries = 0 begin tries += 1 yield rescue Exception => e if tries < 3 retry else RAILS_DEFAULT_LOGGER.error "[DEFENSIO] Could not contact server : #{e}" nil end end end end end end ActiveRecord::Base.extend Defensio::ActsAs::ClassMethods