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.
- 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
|