defender 1.0.3 → 2.0.0beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +25 -0
- data/Gemfile +7 -0
- data/README.md +102 -50
- data/ROADMAP.md +12 -0
- data/Rakefile +25 -0
- data/defender.gemspec +23 -0
- data/lib/defender.rb +24 -4
- data/lib/defender/defender_error.rb +7 -0
- data/lib/defender/spammable.rb +182 -0
- data/lib/defender/test/comment.rb +36 -0
- data/lib/defender/version.rb +2 -1
- data/spec/defender/spammable_spec.rb +124 -0
- data/spec/fake_defensio.rb +36 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +7 -0
- metadata +37 -36
- data/lib/defender/document.rb +0 -126
- data/lib/generators/defender/USAGE +0 -9
- data/lib/generators/defender/defender_generator.rb +0 -11
data/.document
ADDED
data/.gitignore
ADDED
data/Gemfile
ADDED
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
|
-
|
26
|
-
[Defensio][4].
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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://
|
176
|
+
* Gems: <http://rubygems.org/gems/defender>
|
125
177
|
|
126
178
|
This project uses [Semantic Versioning][sv].
|
127
179
|
|
data/ROADMAP.md
ADDED
data/Rakefile
ADDED
@@ -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
|
data/defender.gemspec
ADDED
@@ -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
|
data/lib/defender.rb
CHANGED
@@ -1,18 +1,36 @@
|
|
1
|
-
|
2
|
-
|
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
|
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,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
|
data/lib/defender/version.rb
CHANGED
@@ -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
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
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:
|
4
|
+
prerelease: true
|
5
5
|
segments:
|
6
|
-
-
|
6
|
+
- 2
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version:
|
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-
|
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:
|
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
|
-
|
46
|
-
|
46
|
+
- 0
|
47
|
+
version: 3.0.0
|
48
|
+
type: :runtime
|
47
49
|
version_requirements: *id002
|
48
50
|
- !ruby/object:Gem::Dependency
|
49
|
-
name:
|
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.
|
62
|
+
version: 1.0.0
|
60
63
|
type: :development
|
61
64
|
version_requirements: *id003
|
62
|
-
|
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
|
-
-
|
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
|
-
-
|
87
|
-
-
|
88
|
-
-
|
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://
|
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.
|
122
|
+
rubygems_version: 1.3.7
|
122
123
|
signing_key:
|
123
124
|
specification_version: 3
|
124
|
-
summary:
|
125
|
+
summary: ActiveModel plugin for Defensio.
|
125
126
|
test_files: []
|
126
127
|
|
data/lib/defender/document.rb
DELETED
@@ -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
|