defender 1.0.3 → 2.0.0beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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