defender 1.0.3 → 2.0.0beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ README.md
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,25 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ doc
21
+ .yardoc
22
+
23
+ ## PROJECT::SPECIFIC
24
+
25
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+
3
+ gemspec
4
+
5
+ gem 'watchr', '~> 0.7'
6
+ gem 'rspec', '~> 2.1.0'
7
+ gem 'yard', '~> 0.6.3'
data/README.md CHANGED
@@ -22,54 +22,107 @@ spam and malicious content in the documents.
22
22
  A document contains content to be analyzed by Defensio, or that has been
23
23
  analyzed.
24
24
 
25
- Before starting to use Defender, you need to retrieve an API key from
26
- [Defensio][4]. After getting an API key, you need to let Defender know
27
- what it is by doing something like this somewhere in your code (before
28
- doing anything like saving documents):
29
-
30
- Defender.api_key = 'my-api-key'
31
-
32
- Submitting documents to Defensio is really easy. Here's a barebones
33
- example:
34
-
35
- require 'defender'
36
- document = Defender::Document.new
37
- document.data[:content] = 'Hello World!'
38
- document.data[:type] = 'comment'
39
- document.data[:platform] = 'defender'
40
- document.save
41
-
42
- The `document.data` hash can contain a lot of data. The ones you see
43
- here are the only required ones, but you should submit as much data as
44
- you can. Look at the [Defensio API docs][3] for information on the
45
- different data you can submit. Oh, and the keys can be symbols, and you
46
- can use underscores instead of dashes.
47
-
48
- After saving the document, Defender will set the `document.allow?`,
49
- `document.spaminess` and
50
- `document.signature` attributes. The first one tells you if you should
51
- display the document or not on your website. The second is a float which
52
- tells you just how spammy the document is. This could be useful for
53
- sorting the documents in an admin panel. The lower the spaminess is, the
54
- less chance is it for it being spam. The last attribute is an unique
55
- identifier you should save with your document in the database. This can
56
- be used for retrieving the status of your document again, and for
57
- retraining purposes.
58
-
59
- Did I say retraining? Oh yes, you can retrain Defensio! If some spam
60
- went through the filters, or some legit documents were marked as spam,
61
- tell Defensio by setting the `document.allow` attribute and save the
62
- document again:
63
-
64
- document.allow = true
65
- document.save
66
-
67
- This tells Defensio that the document should've been allowed. Don't have
68
- access to the `document` instance any more you say? No problem, just
69
- retrieve it again using the signature. You did save the signature,
70
- didn't you?
71
-
72
- document = Defender::Document.find(signature)
25
+ To use Defender, you need to retrieve an API key from
26
+ [Defensio][4]. Then add Defender (`gem 'defender'`) to your Gemfile and run
27
+ `bundle install`. To set up Defender, open your Comment class and include
28
+ Defender::Spammable, and configure it by adding your API key. Your comment
29
+ class should look something like this:
30
+
31
+ class Comment < ActiveRecord::Base
32
+ include Defender::Spammable
33
+ configure_defender :api_key => '0123456789abcdef'
34
+ end
35
+
36
+ Now you need to add a few fields to your model. Defender requires a boolean
37
+ field named "spam" (this will be `true` for comments marked as spam, and
38
+ `false otherwise`), and a string field named "defensio_sig" (this will include
39
+ a unique identifier so you can later mark false positives and negatives).
40
+ Defender will also set the spaminess field if it exists (the spaminess field
41
+ should be a float that can range between 0.00 and 1.00, where 0.00 is the
42
+ least spammy and 1.00 is the most spammy). After you've done this, you're
43
+ probably done setting up Defender, although you should read on as there's some
44
+ more things you should know.
45
+
46
+ Defender will automatically get the comment body, author name, email, IP and
47
+ URL if you have them in your comment class with "standard names". Defender
48
+ will look for fields named "body", "content" and "comment" (in that order) for
49
+ the comment content, "author_name", "author" for the author name,
50
+ "author_email", "email" for the author email, "author_ip", "ip" for the author
51
+ IP, "author_url", "url" for the author URL. Only the comment content is the
52
+ required one of these. If you're not using those attribute names, look further
53
+ down in the readme under "Defining your own attribute names".
54
+
55
+ Not using ActiveRecord? No problem, Defender supports all libraries that
56
+ support ActiveModel, including Mongoid, MongoMapper and DataMapper. The syntax
57
+ is the exact same, just use the method your library uses to set up the fields
58
+ needed.
59
+
60
+
61
+ Defining your own attribute names
62
+ ---------------------------------
63
+
64
+ Defensio supports a large amount of attributes you can send to it, and the
65
+ more you send the more accurately it can determine whether it's spam or not.
66
+ For some of these attributes, Defender will use conventions and try to find
67
+ the attribute, but not all are set up (look at
68
+ Defender::Spammable::DEFENSIO_KEYS for the exact keys it uses). If you are
69
+ using other attribute names, or want to add more to get more accurate spam
70
+ evaluation, you do that in the `configure_defender` method. Pass in another
71
+ option called `:keys`, which should be a hash of defensio key names and
72
+ attribute names. The list of defensio key names are after the code example,
73
+ and the attribute name is just a symbol. So if your comment content field is
74
+ called "the_comment_itself", your comment class should look like this:
75
+
76
+ class Comment
77
+ include Defender::Spammable
78
+ configure_defender :api_key => '0123456789abcdef', :keys => { 'content' => :the_comment_itself }
79
+ end
80
+
81
+ If you don't want to store all the information in the database, you can also
82
+ use the `defensio_data` method. In the model, before saving, call
83
+ `defensio_data` with a hash containing the data you want to send. The keys
84
+ should be strings, you can see all the possible values listed below. The
85
+ `defensio_data` method can be called several times with more data.
86
+
87
+ These are the keys defensio supports (at the time of writing, see
88
+ http://defensio.com/api for a completely up-to-date list):
89
+
90
+ * **author-email**: The email address of the author of the document.
91
+ * **author-ip**: The IP address of the author of the document.
92
+ * **author-logged-in**: Whether or not the user posting the document is logged
93
+ onto your Web site, either through your own authentication method or
94
+ through OpenID.
95
+ * **author-name**: The name of the author of the document.
96
+ * **author-openid**: The OpenID URL of the logged-on user. Must be used in
97
+ conjunction with user-logged-in=true.
98
+ * **author-trusted**: Whether or not the user is an administrator, moderator,
99
+ or editor of your Web site. Pass true only if you can guarantee that the
100
+ user has been authenticated, has a role of responsibility, and can be
101
+ trusted as a good Web citizen.
102
+ * **author-url**: The URL of the person posting the document.
103
+ * **browser-cookies**: Whether or not the Web browser used to post the
104
+ document (ie. the comment) has cookies enabled. If no such detection has
105
+ been made, leave this value empty.
106
+ * **browser-javascript**: Whether or not the Web browser used to post the
107
+ document (ie. the comment) has JavaScript enabled. If no such detection
108
+ has been made, leave this value empty.
109
+ * **document-permalink**: The URL to the document being posted.
110
+ * **http-headers**: Contains the HTTP headers sent with the request. You can
111
+ send a few values or all values. Because this information helps Defensio
112
+ determine if a document is innocent or not, the more headers you send, the
113
+ better. The format of this value is one key/value pair per line, each line
114
+ starting with the key followed by a colon and then the value.
115
+ * **parent-document-date**: The date the parent document was posted. For
116
+ example, on a blog, this would be the date the article related to the
117
+ comment (document) was posted. If you're using threaded comments, send the
118
+ date the article was posted, not the date the parent comment was posted.
119
+ * **parent-document-permalink**: The URL of the parent document. For example,
120
+ on a blog, this would be the URL the article related to the comment
121
+ (document) was posted.
122
+ * **referrer**: Provide the value of the HTTP_REFERER (note spelling) in this
123
+ field.
124
+ * **title**: Provide the title of the document being sent. For example, this
125
+ might be the title of a blog article.
73
126
 
74
127
 
75
128
  Development
@@ -81,7 +134,6 @@ First, you should clone the repo and run the features and specs:
81
134
 
82
135
  git clone git://github.com/dvyjones/defender.git
83
136
  cd defender
84
- rake features
85
137
  rake spec
86
138
 
87
139
  Feel free to ping the mailing list if you have any problems and we'll
@@ -121,7 +173,7 @@ Meta
121
173
  * Docs: <http://yardoc.org/docs/dvyjones-defender/>
122
174
  * Bugs: <http://github.com/dvyjones/defender/issues>
123
175
  * List: <defender@librelist.com>
124
- * Gems: <http://gemcutter.org/gems/defender>
176
+ * Gems: <http://rubygems.org/gems/defender>
125
177
 
126
178
  This project uses [Semantic Versioning][sv].
127
179
 
@@ -0,0 +1,12 @@
1
+ Roadmap
2
+ =======
3
+
4
+ v1.1.0
5
+ ------
6
+
7
+ * Statistics
8
+
9
+ v1.2.0
10
+ ------
11
+
12
+ * Asynchronous calls
@@ -0,0 +1,25 @@
1
+ # coding:utf-8
2
+ $:.unshift File.expand_path('../lib', __FILE__)
3
+
4
+ require 'bundler'
5
+ require 'rubygems'
6
+ require 'rubygems/specification'
7
+ require 'defender'
8
+
9
+ Bundler::GemHelper.install_tasks
10
+
11
+ require 'rspec/core/rake_task'
12
+ RSpec::Core::RakeTask.new(:spec) do |t|
13
+ t.rspec_opts = %w{-fs --color}
14
+ end
15
+
16
+ begin
17
+ require 'yard'
18
+ rescue LoadError
19
+ raise 'Run `gem install yard` to generate docs'
20
+ else
21
+ YARD::Rake::YardocTask.new do |conf|
22
+ conf.options = ['-mmarkdown', '-rREADME.md']
23
+ conf.files = ['lib/**/*.rb', '-', 'LICENSE']
24
+ end
25
+ end
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/defender/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "defender"
6
+ s.version = Defender::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Henrik Hodne']
9
+ s.email = ['dvyjones@dvyjones.com']
10
+ s.homepage = 'http://rubygems.org/gems/dvyjones'
11
+ s.summary = 'ActiveModel plugin for Defensio.'
12
+ s.description = 'An ActiveModel plugin for Defensio.'
13
+
14
+ s.required_rubygems_version = ">= 1.3.6"
15
+
16
+ s.add_dependency('defensio', '~> 0.9.1')
17
+ s.add_dependency('activemodel', '~> 3.0.0')
18
+ s.add_development_dependency('bundler', '~> 1.0.0')
19
+
20
+ s.files = `git ls-files`.split("\n")
21
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
22
+ s.require_path = 'lib'
23
+ end
@@ -1,18 +1,36 @@
1
- require 'defender/version'
2
- require 'defender/document'
3
-
1
+ ##
2
+ # Main Defender module. Everything should be under this module.
4
3
  module Defender
4
+ autoload :VERSION, 'defender/version'
5
+ autoload :Version, 'defender/version'
6
+ autoload :Spammable, 'defender/spammable'
7
+ autoload :DefenderError, 'defender/defender_error'
8
+
5
9
  ##
6
10
  # Set this to your Defensio API key. Get one at http://defensio.com.
11
+ #
12
+ # @see Defender::Spammable::ClassMethods.configure_defender
13
+ #
14
+ # @param [String] api_key The Defensio API key.
7
15
  def self.api_key=(api_key)
8
16
  @api_key = api_key.to_s
9
17
  end
18
+
19
+ ##
20
+ # Returns the Defensio API key set with {Defender.api_key=}.
21
+ #
22
+ # @see Defender.api_key=
23
+ # @return [String] The API key if set.
24
+ # @return [nil] If no API key has been set.
25
+ def self.api_key
26
+ @api_key
27
+ end
10
28
 
11
29
  ##
12
30
  # You most probably don't need to set this. It is used to replace the backend
13
31
  # when running the tests. If you for any reason need to use another backend
14
32
  # than the defensio gem, set this. The object needs to respond to the same
15
- # methods as the {Defensio} object does.
33
+ # methods as the Defensio object does.
16
34
  #
17
35
  # @param [Defensio] defensio The Defensio backend
18
36
  def self.defensio=(defensio)
@@ -22,6 +40,8 @@ module Defender
22
40
  ##
23
41
  # The Defensio backend. If no backend has been set yet, this will create one
24
42
  # with the api key set with {Defender.api_key}.
43
+ #
44
+ # @return [Defensio] The Defensio backend
25
45
  def self.defensio
26
46
  return @defensio if defined?(@defensio)
27
47
  require 'defensio'
@@ -0,0 +1,7 @@
1
+ module Defender
2
+ ##
3
+ # Defender exception class. This will be thrown on errors returned by
4
+ # Defensio.
5
+ class DefenderError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,182 @@
1
+ module Defender
2
+ ##
3
+ # Include this in your ActiveModel-supporting model to enable spam
4
+ # filtering.
5
+ #
6
+ # Defender will try to autodetect details about your rails setup, but you
7
+ # need to do some configuration yourself. If you already have an application
8
+ # config file that loads into a constant named APP_CONFIG, and your
9
+ # comment model has an attribute named 'body', 'content' or 'comment'
10
+ # including the comment body, then you are almost ready to go. Create the
11
+ # 'spam' and 'defensio_sig' attribute in the database (a boolean and a
12
+ # string, respectively) and then include {Defender::Spammable} in your
13
+ # model. You can now call #spam? on your model after saving it.
14
+ # Congratulations!
15
+ #
16
+ # Defender requires the model to have callbacks, more exactly, the
17
+ # before_save callback. Most ActiveModel-libraries should have that, so you
18
+ # should only need to worry if you're making your own models. Just look at
19
+ # {Defender::Test::Comment} for an example comment model.
20
+ module Spammable
21
+ # These are the default attribute names Defender will pull information
22
+ # from if no other names are configured. So the content of the comment
23
+ # will be pulled from 'body', if that attribute exists. Otherwise, it will
24
+ # pull from 'content'. If that doesn't exist either, it will pull from
25
+ # 'comment'. If that doesn't exist either, you should configure your own
26
+ # name in {Defender::Spammable::ClassMethods.configure_defender}.
27
+ DEFENSIO_KEYS = {
28
+ 'content' => [:body, :content, :comment],
29
+ 'author-name' => [:author_name, :author],
30
+ 'author-email' => [:author_email, :email],
31
+ 'author-ip' => [:author_ip, :ip],
32
+ 'author-url' => [:author_url, :url]
33
+ }
34
+
35
+ ##
36
+ # These methods will be pulled in as class methods in your model when
37
+ # including {Defender::Spammable}.
38
+ module ClassMethods
39
+ ##
40
+ # Configure Defender by passing a set of options.
41
+ #
42
+ # @param [Hash] options Options for configuring Defender.
43
+ # @option options [Hash] :keys Mapping between field names in the
44
+ # database and in defensio.
45
+ # @option options [String] :api_key Your Defensio API key. Get one at
46
+ # defensio.com.
47
+ #
48
+ def configure_defender(options)
49
+ keys = options.delete(:keys)
50
+ _defensio_keys.merge!(keys) unless keys.nil?
51
+ api_key = options.delete(:api_key)
52
+ Defender.api_key = api_key unless api_key.nil?
53
+ end
54
+
55
+ ##
56
+ # Returns the key-attribute mapping used.
57
+ #
58
+ # Will automatically set it to the defaults in {DEFENSIO_KEYS} if
59
+ # nothing else has been set before.
60
+ def _defensio_keys
61
+ @_defensio_keys ||= DEFENSIO_KEYS.dup
62
+ end
63
+ end
64
+
65
+ ##
66
+ # These methods will be pulled in as instance methods in your model when
67
+ # including {Defender::Spammable}.
68
+ module InstanceMethods
69
+ ##
70
+ # Returns true if the comment is recognized as spam or malicious.
71
+ #
72
+ # If the value is stored in the database that value will be returned.
73
+ # If nil is returned, the comment has not yet been submitted to
74
+ # Defensio.
75
+ #
76
+ # @raise [Defender::DefenderError] Raised if there is no spam attribute
77
+ # in the model.
78
+ # @return [Boolean] Whether the comment is spam or not.
79
+ def spam?
80
+ if self.new_record?
81
+ nil
82
+ elsif self.respond_to?(:spam) && !self.spam.nil?
83
+ return self.spam
84
+ else
85
+ raise Defender::DefenderError, 'You need to add a spam attribute to the model'
86
+ end
87
+ end
88
+
89
+ ##
90
+ # Pass in some data to be sent to defensio. You can use this method to
91
+ # pass in more data that you don't want to save in the model.
92
+ #
93
+ # This can be called several times if you want to add more data or
94
+ # update data already added (using the same key twice will overwrite).
95
+ #
96
+ # Returns the data to be sent. Pass without a parameter to not modify
97
+ # the data.
98
+ #
99
+ # @param [Hash<String => Object>] data The data to send to defensio. See
100
+ # the README for the possible key values.
101
+ def defensio_data(data={})
102
+ @_defensio_data ||= {}
103
+ @_defensio_data.merge!(data)
104
+ @_defensio_data
105
+ end
106
+
107
+ private
108
+
109
+ ##
110
+ # The callback that will be run before a document is saved.
111
+ #
112
+ # This will gather all the data and send it off to Defensio, and then
113
+ # set the spam and defensio_sig attributes (and spaminess if it's
114
+ # defined) before the model will be saved.
115
+ #
116
+ # @raise Defender::DefenderError If Defensio returns an error.
117
+ def _defender_before_save
118
+ data = {}
119
+ _defensio_keys.each do |key, names|
120
+ next if names.nil?
121
+ data[key] = _pick_attribute(names)
122
+ end
123
+ data.merge!({
124
+ 'platform' => 'ruby',
125
+ 'type' => 'comment'
126
+ })
127
+ data.merge!(defensio_data) if defined?(@_defensio_data)
128
+ document = Defender.defensio.post_document(data).last
129
+ if document['status'] == 'failed'
130
+ raise DefenderError, document['message']
131
+ end
132
+ self.spam = !document['allow']
133
+ self.defensio_sig = document['signature'].to_s
134
+ self.spaminess = document['spaminess'] if self.respond_to?(:spaminess=)
135
+ end
136
+
137
+ ##
138
+ # Return the first attribute value from a list of attribute names/
139
+ #
140
+ # @param [Array<Symbol>, Symbol] names A list of attribute names
141
+ # @return [] The attribute value of the first existing attribute
142
+ # @return [nil] If no attribute was found (or if attribute value is nil)
143
+ def _pick_attribute(names)
144
+ [names].flatten.each do |name|
145
+ return self.send(name) if self.respond_to?(name)
146
+ end
147
+ return nil
148
+ end
149
+
150
+ ##
151
+ # Retrieves the Defensio document from the server if it hasn't been
152
+ # retrieved before or if the first parameter is true.
153
+ #
154
+ # @param [Boolean] force Pass true to force a refetch, otherwise it will
155
+ # get the cached document (if one is cached).
156
+ # @return [Hash] The document retrieved from the server.
157
+ def _get_defensio_document(force=false)
158
+ if force || @_defensio_document.nil?
159
+ @_defensio_document = Defender.defensio.get_document(self.defensio_sig).last
160
+ end
161
+ @_defensio_document
162
+ end
163
+
164
+ ##
165
+ # Wrapper for {Defender::Spammable::ClassMethods._defensio_keys}.
166
+ #
167
+ # @see Defender::Spammable::ClassMethods._defensio_keys
168
+ def _defensio_keys
169
+ self.class._defensio_keys
170
+ end
171
+ end
172
+
173
+ ##
174
+ # Includes {Defender::Spammable::ClassMethods} and
175
+ # {Defender::Spammable::InstanceMethods} and sets up save callback.
176
+ def self.included(receiver)
177
+ receiver.extend ClassMethods
178
+ receiver.send :include, InstanceMethods
179
+ receiver.send :before_save, :_defender_before_save
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,36 @@
1
+ require 'active_model'
2
+
3
+ ##
4
+ # Stuff to test Defender. You probably shouldn't use this in your application,
5
+ # but it is included as an example of the minimum needed for a valid setup.
6
+ module Defender::Test
7
+ ##
8
+ # A fake Comment class to use. No need to require ActiveRecord and set up an
9
+ # actual database. We will use ActiveModel for callbacks though.
10
+ class Comment
11
+ extend ActiveModel::Naming
12
+ extend ActiveModel::Callbacks
13
+ define_model_callbacks :save
14
+
15
+ # We now have a "valid" model, let's bring in Defender.
16
+ include Defender::Spammable
17
+
18
+ attr_accessor :body, :author, :author_ip, :created_at, :spam, :defensio_sig
19
+
20
+ ##
21
+ # Returns true if save has been called, false otherwise.
22
+ def new_record?
23
+ !@saved ||= false
24
+ end
25
+
26
+ ##
27
+ # Run save callback and make {Defender::Test::Comment.new_record?} return false.
28
+ def save
29
+ _run_save_callbacks do
30
+ # We're not actually saving anything, just letting Defender know we
31
+ # would be.
32
+ @saved = true
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,3 +1,4 @@
1
1
  module Defender
2
- VERSION = '1.0.3'
2
+ # Current Defender version
3
+ VERSION = '2.0.0beta1'
3
4
  end
@@ -0,0 +1,124 @@
1
+ require 'spec_helper'
2
+
3
+ module Defender
4
+ describe Spammable do
5
+ describe '.configure_defender' do
6
+ it 'sets the attribute-data key mappers' do
7
+ Comment.configure_defender(:keys => {'foo' => :bar, 'foobar' => :baz})
8
+ Comment._defensio_keys.should include({'foo' => :bar, 'foobar' => :baz})
9
+ end
10
+
11
+ it 'sets the API key' do
12
+ Comment.configure_defender(:api_key => 'foobar')
13
+ Defender.api_key.should == 'foobar'
14
+ end
15
+ end
16
+
17
+ describe '#spam?' do
18
+ it 'returns the "spam" attribute unless it is nil' do
19
+ comment_class = Class.new
20
+ comment_class.instance_eval { attr_accessor :spam }
21
+ def comment_class.before_save(*args, &block); end
22
+ comment_class.send(:define_method, :new_record?) { false }
23
+ comment_class.send(:include, Defender::Spammable)
24
+ comment = comment_class.new
25
+ comment.spam = true
26
+ comment.spam?.should be_true
27
+ end
28
+
29
+ it 'returns nil for a new record' do
30
+ comment_class = Class.new
31
+ comment_class.instance_eval { attr_accessor :spam }
32
+ def comment_class.before_save(*args, &block); end
33
+ comment_class.send(:define_method, :new_record?) { true }
34
+ comment_class.send(:include, Defender::Spammable)
35
+ comment = comment_class.new
36
+ comment.spam?.should be_nil
37
+ end
38
+
39
+ it 'raises a DefenderError if no spam attribute exists' do
40
+ comment_class = Class.new
41
+ def comment_class.before_save(*args, &block); end
42
+ comment_class.send(:define_method, :new_record?) { false }
43
+ comment_class.send(:include, Defender::Spammable)
44
+ comment = comment_class.new
45
+ expect { comment.spam? }.to raise_error(Defender::DefenderError)
46
+ end
47
+ end
48
+
49
+ describe '#defensio_data' do
50
+ it 'merges in more data to be sent to Defensio' do
51
+ comment = Comment.new
52
+ comment.defensio_data({'foo' => 'FOOBAR', 'foobar' => 'baz'})
53
+ comment.defensio_data.should include({'foo' => 'FOOBAR', 'foobar' => 'baz'})
54
+ end
55
+
56
+ it 'overwrites values repassed' do
57
+ comment = Comment.new
58
+ comment.defensio_data({'foo' => 'FOOBAR'})
59
+ comment.defensio_data({'foo' => 'baz'})
60
+ comment.defensio_data['foo'].should == 'baz'
61
+ end
62
+
63
+ it 'leaves values that aren\'t modified' do
64
+ comment = Comment.new
65
+ comment.defensio_data({'foo' => 'baz'})
66
+ comment.defensio_data({'bar' => 'foobar'})
67
+ comment.defensio_data['foo'].should == 'baz'
68
+ end
69
+ end
70
+
71
+ describe '#_defender_before_save' do
72
+ it 'sets the attributes returned from defensio' do
73
+ comment = Comment.new
74
+ comment.body = '[innocent,0.9]'
75
+ comment.save
76
+ comment.spam.should be_false
77
+ comment.defensio_sig.should_not be_nil
78
+ end
79
+
80
+ it 'sends the information off to Defensio' do
81
+ old_defensio = Defender.defensio
82
+ defensio = double('defensio')
83
+ defensio.should_receive(:post_document) { [200, {'signature' => 1234567890, 'spaminess' => 0.9, 'allow' => true}] }
84
+ Defender.defensio = defensio
85
+ comment = Comment.new
86
+ comment.body = 'Hello, world!'
87
+ comment.save
88
+ Defender.defensio = old_defensio
89
+ end
90
+ end
91
+
92
+ describe '#_pick_attribute' do
93
+ it 'returns the value of the attribute passed if it exists' do
94
+ comment = Comment.new
95
+ comment.body = 'Foobar!'
96
+ comment.send(:_pick_attribute, :body).should == 'Foobar!'
97
+ end
98
+
99
+ it 'returns the value for the first attribute that exists in a list of attributes' do
100
+ comment = Comment.new
101
+ comment.body = 'Foobar!'
102
+ comment.send(:_pick_attribute, [:content, :body]).should == 'Foobar!'
103
+ end
104
+
105
+ it 'returns nil if no attribute with the given names exists' do
106
+ comment = Comment.new
107
+ comment.send(:_pick_attribute, :bogus_attribute).should be_nil
108
+ end
109
+ end
110
+
111
+ describe '#_get_defensio_document' do
112
+ it 'retrieves the document from Defensio' do
113
+ old_defensio = Defender.defensio
114
+ defensio = double('defensio')
115
+ defensio.should_receive(:get_document) { [200, {'status' => 'succeed'}] }
116
+ Defender.defensio = defensio
117
+ comment = Comment.new
118
+ comment.defensio_sig = '0123456789abcdef'
119
+ comment.send(:_get_defensio_document)
120
+ Defender.defensio = old_defensio
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,36 @@
1
+ class FakeDefensio
2
+ def initialize
3
+ @documents = {}
4
+ end
5
+
6
+ def post_document(data)
7
+ content = data[:content] || data['content']
8
+ classification, spaminess = content[1..-2].split(',')
9
+ signature = "#{rand}#{content}".hash
10
+
11
+ @documents[signature] = {
12
+ 'api-version' => '2.0',
13
+ 'status' => 'success',
14
+ 'message' => '',
15
+ 'signature' => signature,
16
+ 'allow' => (classification == 'innocent'),
17
+ 'classification' => classification,
18
+ 'spaminess' => spaminess.to_f,
19
+ 'profanity-match' => false
20
+ }
21
+
22
+ [200, @documents[signature]]
23
+ end
24
+
25
+ def get_document(signature)
26
+ if @documents.has_key?(signature)
27
+ [200, @documents[signature]]
28
+ else
29
+ [404,
30
+ 'api-version' => '2.0',
31
+ 'status' => 'failure',
32
+ 'message' => 'document not found'
33
+ ]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format nested
@@ -0,0 +1,7 @@
1
+ require 'defender'
2
+ require 'defender/test/comment'
3
+ require 'fake_defensio'
4
+
5
+ Comment = Defender::Test::Comment
6
+
7
+ Defender.defensio = FakeDefensio.new
metadata CHANGED
@@ -1,12 +1,12 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: defender
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: false
4
+ prerelease: true
5
5
  segments:
6
- - 1
6
+ - 2
7
7
  - 0
8
- - 3
9
- version: 1.0.3
8
+ - 0beta1
9
+ version: 2.0.0beta1
10
10
  platform: ruby
11
11
  authors:
12
12
  - Henrik Hodne
@@ -14,13 +14,14 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-04-28 00:00:00 +02:00
17
+ date: 2010-12-20 00:00:00 -08:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: defensio
22
22
  prerelease: false
23
23
  requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
24
25
  requirements:
25
26
  - - ~>
26
27
  - !ruby/object:Gem::Version
@@ -32,50 +33,38 @@ dependencies:
32
33
  type: :runtime
33
34
  version_requirements: *id001
34
35
  - !ruby/object:Gem::Dependency
35
- name: rspec
36
+ name: activemodel
36
37
  prerelease: false
37
38
  requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
38
40
  requirements:
39
41
  - - ~>
40
42
  - !ruby/object:Gem::Version
41
43
  segments:
42
- - 1
43
44
  - 3
44
45
  - 0
45
- version: 1.3.0
46
- type: :development
46
+ - 0
47
+ version: 3.0.0
48
+ type: :runtime
47
49
  version_requirements: *id002
48
50
  - !ruby/object:Gem::Dependency
49
- name: yard
51
+ name: bundler
50
52
  prerelease: false
51
53
  requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
52
55
  requirements:
53
56
  - - ~>
54
57
  - !ruby/object:Gem::Version
55
58
  segments:
59
+ - 1
56
60
  - 0
57
- - 5
58
61
  - 0
59
- version: 0.5.0
62
+ version: 1.0.0
60
63
  type: :development
61
64
  version_requirements: *id003
62
- - !ruby/object:Gem::Dependency
63
- name: cucumber
64
- prerelease: false
65
- requirement: &id004 !ruby/object:Gem::Requirement
66
- requirements:
67
- - - ~>
68
- - !ruby/object:Gem::Version
69
- segments:
70
- - 0
71
- - 6
72
- - 0
73
- version: 0.6.0
74
- type: :development
75
- version_requirements: *id004
76
- description: A wrapper of the Defensio spam filtering service.
65
+ description: An ActiveModel plugin for Defensio.
77
66
  email:
78
- - henrik.hodne@binaryhex.com
67
+ - dvyjones@dvyjones.com
79
68
  executables: []
80
69
 
81
70
  extensions: []
@@ -83,15 +72,25 @@ extensions: []
83
72
  extra_rdoc_files: []
84
73
 
85
74
  files:
86
- - lib/generators/defender/defender_generator.rb
87
- - lib/generators/defender/USAGE
88
- - lib/defender.rb
89
- - lib/defender/document.rb
90
- - lib/defender/version.rb
75
+ - .document
76
+ - .gitignore
77
+ - Gemfile
91
78
  - LICENSE
92
79
  - README.md
80
+ - ROADMAP.md
81
+ - Rakefile
82
+ - defender.gemspec
83
+ - lib/defender.rb
84
+ - lib/defender/defender_error.rb
85
+ - lib/defender/spammable.rb
86
+ - lib/defender/test/comment.rb
87
+ - lib/defender/version.rb
88
+ - spec/defender/spammable_spec.rb
89
+ - spec/fake_defensio.rb
90
+ - spec/spec.opts
91
+ - spec/spec_helper.rb
93
92
  has_rdoc: true
94
- homepage: http://github.com/dvyjones/defender
93
+ homepage: http://rubygems.org/gems/dvyjones
95
94
  licenses: []
96
95
 
97
96
  post_install_message:
@@ -100,6 +99,7 @@ rdoc_options: []
100
99
  require_paths:
101
100
  - lib
102
101
  required_ruby_version: !ruby/object:Gem::Requirement
102
+ none: false
103
103
  requirements:
104
104
  - - ">="
105
105
  - !ruby/object:Gem::Version
@@ -107,6 +107,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
107
  - 0
108
108
  version: "0"
109
109
  required_rubygems_version: !ruby/object:Gem::Requirement
110
+ none: false
110
111
  requirements:
111
112
  - - ">="
112
113
  - !ruby/object:Gem::Version
@@ -118,9 +119,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
119
  requirements: []
119
120
 
120
121
  rubyforge_project:
121
- rubygems_version: 1.3.6
122
+ rubygems_version: 1.3.7
122
123
  signing_key:
123
124
  specification_version: 3
124
- summary: Ruby API wrapper for Defensio
125
+ summary: ActiveModel plugin for Defensio.
125
126
  test_files: []
126
127
 
@@ -1,126 +0,0 @@
1
- module Defender
2
- ##
3
- # A document contains data to be analyzed by Defensio, or that has been
4
- # analyzed.
5
- class Document
6
- ##
7
- # Whether the document should be published on your Web site or not.
8
- #
9
- # For example, spam and malicious content are not allowed.
10
- #
11
- # @return [Boolean]
12
- attr_accessor :allow
13
- alias_method :allow?, :allow
14
-
15
- ##
16
- # The information about the document. This hash accepts so many parameters
17
- # I won't list them here. Go look at the [Defensio API docs]
18
- # (http://defensio.com/api) instead.
19
- #
20
- # Defender will replace all underscores in keys with dashes, so you can use
21
- # `:author_email` instead of `'author-email'`.
22
- #
23
- # @return [Hash{#to_s => #to_s}]
24
- attr_accessor :data
25
-
26
- ##
27
- # A unique identifier for the document.
28
- #
29
- # This is needed to retrieve the status back from Defensio and to submit
30
- # false negatives/positives to Defensio. Signatures should be kept private
31
- # and never shared with your users.
32
- #
33
- # @return [String]
34
- attr_reader :signature
35
-
36
- ##
37
- # A float denoting how spammy a document is.
38
- #
39
- # This could be useful for sorting document in an admin interface.
40
- #
41
- # @return[Float<0.0,1.0>]
42
- attr_reader :spaminess
43
-
44
- ##
45
- # Retrieves the status of a document back from Defensio.
46
- #
47
- # Please note that this only retrieves the status of the document (like
48
- # it's spaminess, whether it should be allowed or not, etc.) and not the
49
- # content of the request (all of the data in the {#data} hash).
50
- #
51
- # @param [String] signature The signature of the document to retrieve
52
- # @return [Document,nil] The document to retrieve, or nil
53
- def self.find(signature)
54
- document = new
55
- ret = Defender.call(:get_document, signature)
56
- if ret
57
- document.instance_variable_set(:@saved, true)
58
- document.instance_variable_set(:@allow, ret.last['allow'])
59
- document.instance_variable_set(:@signature, signature)
60
- document.instance_variable_set(:@spaminess, ret.last['spaminess'])
61
-
62
- document
63
- else
64
- nil
65
- end
66
- end
67
-
68
- ##
69
- # Initializes a new document
70
- def initialize
71
- @data = {}
72
- @saved = false
73
- end
74
-
75
- ##
76
- # @return [Boolean] Has the document been submitted to Defensio?
77
- def saved?
78
- @saved
79
- end
80
-
81
- ##
82
- # Submit the document to Defensio.
83
- #
84
- # This will send all of the {#data} if the document hasn't been saved
85
- # before. If it has been saved, it will submit whether the document was a
86
- # false positive/negative (set the {#allow} param before saving to do
87
- # this).
88
- #
89
- # @see #saved?
90
- # @return [Boolean] Whether the save succeded or not.
91
- def save
92
- if saved?
93
- ret = Defender.call(:put_document, @signature, {:allow => @allow})
94
- else
95
- ret = Defender.call(:post_document, self.class.normalize_data(@data))
96
- end
97
- return false if ret == false
98
- return true if saved?
99
-
100
- data = ret.last
101
- @allow = data['allow']
102
- @signature = data['signature']
103
- @spaminess = data['spaminess']
104
-
105
- @saved = true # This will also return true, since nothing failed as we got here
106
- end
107
-
108
-
109
- ##
110
- # Normalizes a data hash to submit to defensio.
111
- #
112
- # @param [Hash] hsh The hash to be normalized
113
- # @return [Hash{String => String}] The normalized hash
114
- def self.normalize_data(data)
115
- normalized = {}
116
- data.each { |key, value|
117
- if value.respond_to?(:strftime)
118
- value = value.strftime('%Y-%m-%d')
119
- end
120
- normalized[key.to_s.gsub('_','-')] = value.to_s
121
- }
122
-
123
- normalized
124
- end
125
- end
126
- end
@@ -1,9 +0,0 @@
1
- Description:
2
- Creates a migration that adds defender fields to the table of your choice.
3
- Pass the model name, either CamelCased or under_scored, as an argument.
4
-
5
- Example:
6
- rails generate defender Comment
7
-
8
- creates a migration for the Comment model:
9
- db/migrate/12345678901234_add_defender_to_comments.rb
@@ -1,11 +0,0 @@
1
- require 'rails/generators'
2
-
3
- class DefenderGenerator < Rails::Generators::NamedBase
4
- def self.source_root
5
- File.join(File.dirname(__FILE__), 'templates')
6
- end
7
-
8
- def create_migration
9
- generate(:migration, "add_defender_to_#{table_name} defensio_signature:string spaminess:float spam:boolean")
10
- end
11
- end