lib/defender/spammable.rb in defender-2.0.2 vs lib/defender/spammable.rb in defender-2.0.3

- old
+ new

@@ -1,197 +1,238 @@ module Defender - ## - # Include this in your ActiveModel-supporting model to enable spam - # filtering. + # Includes all the model magic for Defender. This module should be included in + # your model to enable Defender on it. Defender does some automatic detection + # on your setup, but you need to do some configuration yourself. You can set + # the API key in two different ways. If you only use Defender on one model, + # you can configure the API key in the model with the configure_defender + # method. If you prefer to use initializers, or you have multiple models, you + # probably want to define it in an initializer. You can do this by calling + # Defender.api_key directly, like this: # - # Defender will try to autodetect details about your rails setup, but you - # need to do some configuration yourself. If you already have an application - # config file that loads into a constant named APP_CONFIG, and your - # comment model has an attribute named 'body', 'content' or 'comment' - # including the comment body, then you are almost ready to go. Create the - # 'spam' and 'defensio_sig' attribute in the database (a boolean and a - # string, respectively) and then include {Defender::Spammable} in your - # model. You can now call #spam? on your model after saving it. - # Congratulations! + # Defender.api_key = 'This is your API key' # - # Defender requires the model to have callbacks, more exactly, the - # before_save callback. Most ActiveModel-libraries should have that, so you - # should only need to worry if you're making your own models. Just look at - # {Defender::Test::Comment} for an example comment model. + # Defender only requires your models to have a before_save callback, but since + # most ActiveModel libraries should have this you only need to worry about it + # if you're making your own models. Just have a look at + # Defender::Test::Comment for an example comment model. module Spammable - # These are the default attribute names Defender will pull information - # from if no other names are configured. So the content of the comment - # will be pulled from 'body', if that attribute exists. Otherwise, it will - # pull from 'content'. If that doesn't exist either, it will pull from - # 'comment'. If that doesn't exist either, you should configure your own - # name in {Defender::Spammable::ClassMethods.configure_defender}. + # Public: These are the default attribute names Defender will pull + # information from if no other names are configured. So the content of the + # comment will be pulled from 'body', if that attribute exists. Otherwise, + # it will be pulled from 'content'. If that doesn't exist either, it will + # pull from 'comment'. If that attribute doesn't exist either, you should + # configure your own attributes with the configure_defender method. DEFENSIO_KEYS = { 'content' => [:body, :content, :comment], 'author-name' => [:author_name, :author], 'author-email' => [:author_email, :email], 'author-ip' => [:author_ip, :ip], 'author-url' => [:author_url, :url] - } - - ## - # These methods will be pulled in as class methods in your model when - # including {Defender::Spammable}. + }.freeze + + # Public: Methods that will be included as class methods when including + # Defender::Spammable into your model. module ClassMethods - ## - # Configure Defender by passing a set of options. + # Public: Configures various Defender options. # - # @param [Hash] options Options for configuring Defender. - # @option options [Hash] :keys Mapping between field names in the - # database and in defensio. - # @option options [String] :api_key Your Defensio API key. Get one at - # defensio.com. - # @option options [Boolean] :test_mode Set Defender in test mode. See - # {#test_mode}. - # + # options - The hash options used to configure Defender: + # :keys - A Hash which maps field names in the database to + # Defensio field names (optional). + # :api_key - Your Defensio API key String (optional). + # :test_mode - Set this to true to enable the test mode. See + # Defender.test_mode for more information. + # + # Examples + # + # configure_defender :keys => { 'content' => :comment_content }, + # :api_key => 'Your API key.', :test_mode => true + # + # Returns nothing def configure_defender(options) keys = options.delete(:keys) _defensio_keys.merge!(keys) unless keys.nil? api_key = options.delete(:api_key) Defender.api_key = api_key unless api_key.nil? - self.test_mode = options.delete(:test_mode) + Defender.test_mode = options.delete(:test_mode) end - - ## - # Set this to true to put Defender in "test mode". When in test mode, you - # can check if your code is working properly you can specify in the - # content field what kind of response you want. If you want a comment to - # be marked as spam with a spaminess of 0.85 you write [spam,0.85] - # somewhere in the content field of the document. If you want a malicious - # response with a spaminess of 0.99 you write [malicious,0.99] and for an - # innocent response you write [innocent,0.25]. This is the preferred way - # of testing, if you write spammy comments you might hurt the Defensio - # performance. - attr_accessor :test_mode - - ## - # Returns the key-attribute mapping used. + + # Deprecated: Returns whether Defender is in "test mode". # - # Will automatically set it to the defaults in {DEFENSIO_KEYS} if - # nothing else has been set before. + # Use Defender.test_mode instead. + def test_mode + Defender.test_mode + end + + # Deprecated: Enables/disables Defender's test mode. + # + # Use Defender.test_mode= instead. + def test_mode=(test_mode) + Defender.test_mode = test_mode + end + + # Internal: Returns the key-attribute mapping Hash used. + # + # This will default to DEFENSIO_KEYS, but can be modified. + # + # The Public API has access to this through configure_defender. def _defensio_keys @_defensio_keys ||= DEFENSIO_KEYS.dup end end - - ## - # These methods will be pulled in as instance methods in your model when - # including {Defender::Spammable}. + + # Public: Methods that will be included as instance methods when including + # Defender::Spammable into your model. module InstanceMethods - ## - # Returns true if the comment is recognized as spam or malicious. + # Public: Whether the comment is recognized a malicious comment or as + # spam. # - # If the value is stored in the database that value will be returned. - # If nil is returned, the comment has not yet been submitted to - # Defensio. - # - # @raise [Defender::DefenderError] Raised if there is no spam attribute - # in the model. - # @return [Boolean] Whether the comment is spam or not. + # Returns the Boolean value stored in the database, or nil if the comment + # hasn't been submitted to Defensio yet. + # Raises Defender::DefenderError if there is no spam attribute in the + # model. def spam? if self.new_record? nil elsif self.respond_to?(:spam) && !self.spam.nil? return self.spam else raise Defender::DefenderError, 'You need to add a spam attribute to the model' end end - - ## - # Pass in some data to be sent to defensio. You can use this method to - # pass in more data that you don't want to save in the model. + + # Public: Report a false positive to Defensio and update the spam + # attribute. # - # This can be called several times if you want to add more data or - # update data already added (using the same key twice will overwrite). + # A false positive is a legitimate comment incorrectly marked as spam. # - # Returns the data to be sent. Pass without a parameter to not modify - # the data. + # This must be done within 30 days of the comment originally being + # submitted. If you need to update this after that, just set the spam + # attribute on your model and save it. # - # @param [Hash<String => Object>] data The data to send to defensio. See - # the README for the possible key values. + # Raises a Defender::DefenderError if Defensio returns an error. + def false_positive! + document = Defender.defensio.put_document(self.defensio_sig, {'allow' => 'true'}).last + if document['status'] == 'failed' + raise DefenderError, document['message'] + end + update_attribute(:spam, false) + end + + # Public: Report a false negative to Defensio and update the spam + # attribute. + # + # A false negative is a spammy comment incorrectly marked as legitimate. + # + # This must be done within 30 days of the comment originally being + # submitted. If you need to update this after that, just set the spam + # attribute on your model and save it. + # + # Raises a Defender::DefenderError if Defensio returns an error. + def false_negative! + document = Defender.defensio.put_document(self.defensio_sig, {'allow' => 'false'}).last + if document['status'] == 'failed' + raise DefenderError, document['message'] + end + update_attribute(:spam, true) + end + + # Public: Pass in more data to be sent to Defensio. You should use this + # for data you don't want to save in the model, for instance HTTP headers. + # + # This can be called several times, the new data will be merged into the + # existing data. If you use the same key twice, the new value will + # overwrite the old. + # + # data - The Hash data to send to Defensio. Check the README for the + # possible keys. + # + # Examples + # + # def create # A Rails controller action + # @comment = Comment.new(params[:comment]) + # @comment.defensio_data( + # 'http-headers' => request.env.map {|k,v| "#{k}: #{v}" }.join("\n") + # ) + # end + # + # Returns the data to be sent. def defensio_data(data={}) @_defensio_data ||= {} @_defensio_data.merge!(data) @_defensio_data end - + private - - ## - # The callback that will be run before a document is saved. + + # Internal: The callback that will be run before a document is created.. # - # This will gather all the data and send it off to Defensio, and then - # set the spam and defensio_sig attributes (and spaminess if it's - # defined) before the model will be saved. + # This will gather all the data and send it off to Defensio, and then set + # the spam and defensio_sig attributes (and spaminess if it's defined) + # before the model will be saved. # - # @raise Defender::DefenderError If Defensio returns an error. - def _defender_before_save + # Raises a Defender::DefenderError if Defensio returns an error. Please + # note that this will cancel the save. + def _defender_before_create data = {} _defensio_keys.each do |key, names| next if names.nil? data[key] = _pick_attribute(names) end data.merge!({ 'platform' => 'ruby', - 'type' => (self.class.test_mode ? 'test' : 'comment') + 'type' => (Defender.test_mode ? 'test' : 'comment') }) data.merge!(defensio_data) if defined?(@_defensio_data) - document = Defender.defensio.post_document(data).last - if document['status'] == 'failed' - raise DefenderError, document['message'] + if document = Defender.defensio.post_document(data).last + if document['status'] == 'failed' + raise DefenderError, document['message'] + end + self.spam = !document['allow'] + self.defensio_sig = document['signature'].to_s + self.spaminess = document['spaminess'] if self.respond_to?(:spaminess=) + else + raise DefenderError, 'Got nil response from Defensio API, service might be down' end - self.spam = !document['allow'] - self.defensio_sig = document['signature'].to_s - self.spaminess = document['spaminess'] if self.respond_to?(:spaminess=) + true end - - ## - # Return the first attribute value from a list of attribute names/ + + # Internal: Returns value of the first attribute that exists in a list of + # attributes. # - # @param [Array<Symbol>, Symbol] names A list of attribute names - # @return [] The attribute value of the first existing attribute - # @return [nil] If no attribute was found (or if attribute value is nil) + # names - A Symbol or Array of Symbols representing the attribute name(s). def _pick_attribute(names) [names].flatten.each do |name| return self.send(name) if self.respond_to?(name) end return nil end - - ## - # Retrieves the Defensio document from the server if it hasn't been - # retrieved before or if the first parameter is true. + + # Internal: Retrieves the Defensio document from the server if it hasn't + # been retrieved before or if the first parameter is true. # - # @param [Boolean] force Pass true to force a refetch, otherwise it will - # get the cached document (if one is cached). - # @return [Hash] The document retrieved from the server. + # force - A Boolean representing whether to force a refetch. If a refetch + # isn't forced, the document will only be fetched if it hasn't + # been fetched already. + # + # Returns the Hash with the information retrieved from the server. def _get_defensio_document(force=false) - if force || @_defensio_document.nil? + if force || !defined?(@_defensio_document) || @_defensio_document.nil? @_defensio_document = Defender.defensio.get_document(self.defensio_sig).last end @_defensio_document end - - ## - # Wrapper for {Defender::Spammable::ClassMethods._defensio_keys}. - # - # @see Defender::Spammable::ClassMethods._defensio_keys + + # Internal: Wrapper for the class method with the same name. def _defensio_keys self.class._defensio_keys end end - - ## - # Includes {Defender::Spammable::ClassMethods} and - # {Defender::Spammable::InstanceMethods} and sets up save callback. + + # Internal: Includes the ClassMethods and InstanceMethods and sets up the + # before_save callback. def self.included(receiver) receiver.extend ClassMethods receiver.send :include, InstanceMethods - receiver.send :before_save, :_defender_before_save + receiver.send :before_create, :_defender_before_create end end -end \ No newline at end of file +end