obscenity 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +24 -0
- data/.travis.yml +5 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +40 -0
- data/LICENSE.txt +20 -0
- data/README.md +280 -0
- data/Rakefile +52 -0
- data/config/blacklist.yml +566 -0
- data/config/international.yml +820 -0
- data/lib/obscenity.rb +36 -0
- data/lib/obscenity/active_model.rb +18 -0
- data/lib/obscenity/base.rb +73 -0
- data/lib/obscenity/config.rb +51 -0
- data/lib/obscenity/error.rb +7 -0
- data/lib/obscenity/rack.rb +89 -0
- data/lib/obscenity/version.rb +5 -0
- data/obscenity.gemspec +84 -0
- data/test/helper.rb +33 -0
- data/test/static/422.html +1 -0
- data/test/test_active_model.rb +69 -0
- data/test/test_base.rb +253 -0
- data/test/test_config.rb +66 -0
- data/test/test_obscenity.rb +68 -0
- data/test/test_rack.rb +199 -0
- data/test/test_version.rb +9 -0
- metadata +198 -0
data/lib/obscenity.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'obscenity/error'
|
2
|
+
require 'obscenity/config'
|
3
|
+
require 'obscenity/base'
|
4
|
+
require 'obscenity/version'
|
5
|
+
|
6
|
+
module Obscenity extend self
|
7
|
+
|
8
|
+
attr_accessor :config
|
9
|
+
|
10
|
+
def configure(&block)
|
11
|
+
@config = Config.new(&block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def config
|
15
|
+
@config ||= Config.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def profane?(word)
|
19
|
+
Obscenity::Base.profane?(word)
|
20
|
+
end
|
21
|
+
|
22
|
+
def sanitize(text)
|
23
|
+
Obscenity::Base.sanitize(text)
|
24
|
+
end
|
25
|
+
|
26
|
+
def replacement(chars)
|
27
|
+
Obscenity::Base.replacement(chars)
|
28
|
+
end
|
29
|
+
|
30
|
+
def offensive(text)
|
31
|
+
Obscenity::Base.offensive(text)
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
if defined?(ActiveModel)
|
2
|
+
module ActiveModel
|
3
|
+
module Validations
|
4
|
+
class ObscenityValidator < ActiveModel::EachValidator
|
5
|
+
|
6
|
+
def validate_each(record, attribute, value)
|
7
|
+
if options.present? && options.has_key?(:sanitize)
|
8
|
+
object = record.respond_to?(:[]) ? record[attribute] : record.send(attribute)
|
9
|
+
object = Obscenity.replacement(options[:replacement]).sanitize(object)
|
10
|
+
else
|
11
|
+
record.errors.add(attribute, options[:message] || 'cannot be profane') if Obscenity.profane?(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Obscenity
|
2
|
+
class Base
|
3
|
+
class << self
|
4
|
+
|
5
|
+
def blacklist
|
6
|
+
@blacklist ||= set_list_content(Obscenity.config.blacklist)
|
7
|
+
end
|
8
|
+
|
9
|
+
def blacklist=(value)
|
10
|
+
@blacklist = value == :default ? set_list_content(Obscenity::Config.new.blacklist) : value
|
11
|
+
end
|
12
|
+
|
13
|
+
def whitelist
|
14
|
+
@whitelist ||= set_list_content(Obscenity.config.whitelist)
|
15
|
+
end
|
16
|
+
|
17
|
+
def whitelist=(value)
|
18
|
+
@whitelist = value == :default ? set_list_content(Obscenity::Config.new.whitelist) : value
|
19
|
+
end
|
20
|
+
|
21
|
+
def profane?(text)
|
22
|
+
return(false) unless text.to_s.size >= 3
|
23
|
+
blacklist.each do |foul|
|
24
|
+
return(true) if text =~ /\b#{foul}\b/i && !whitelist.include?(foul)
|
25
|
+
end
|
26
|
+
false
|
27
|
+
end
|
28
|
+
|
29
|
+
def sanitize(text)
|
30
|
+
return(text) unless text.to_s.size >= 3
|
31
|
+
blacklist.each do |foul|
|
32
|
+
text.gsub!(/\b#{foul}\b/i, replace(foul)) unless whitelist.include?(foul)
|
33
|
+
end
|
34
|
+
@scoped_replacement = nil
|
35
|
+
text
|
36
|
+
end
|
37
|
+
|
38
|
+
def replacement(chars)
|
39
|
+
@scoped_replacement = chars
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
def offensive(text)
|
44
|
+
words = []
|
45
|
+
return(words) unless text.to_s.size >= 3
|
46
|
+
blacklist.each do |foul|
|
47
|
+
words << foul if text =~ /\b#{foul}\b/i && !whitelist.include?(foul)
|
48
|
+
end
|
49
|
+
words.uniq
|
50
|
+
end
|
51
|
+
|
52
|
+
def replace(word)
|
53
|
+
content = @scoped_replacement || Obscenity.config.replacement
|
54
|
+
case content
|
55
|
+
when :vowels then word.gsub(/[aeiou]/i, '*')
|
56
|
+
when :stars then '*' * word.size
|
57
|
+
when :default, :garbled then '$@!#%'
|
58
|
+
else content
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
def set_list_content(list)
|
64
|
+
case list
|
65
|
+
when Array then list
|
66
|
+
when String, Pathname then YAML.load_file( list.to_s )
|
67
|
+
else []
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Obscenity
|
2
|
+
class Config
|
3
|
+
|
4
|
+
attr_accessor :replacement
|
5
|
+
|
6
|
+
DEFAULT_WHITELIST = []
|
7
|
+
DEFAULT_BLACKLIST = File.dirname(__FILE__) + "/../../config/blacklist.yml"
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
yield(self) if block_given?
|
11
|
+
validate_config_options
|
12
|
+
end
|
13
|
+
|
14
|
+
def replacement
|
15
|
+
@replacement ||= :garbled
|
16
|
+
end
|
17
|
+
|
18
|
+
def blacklist
|
19
|
+
@blacklist ||= DEFAULT_BLACKLIST
|
20
|
+
end
|
21
|
+
|
22
|
+
def blacklist=(value)
|
23
|
+
@blacklist = value == :default ? DEFAULT_BLACKLIST : value
|
24
|
+
end
|
25
|
+
|
26
|
+
def whitelist
|
27
|
+
@whitelist ||= DEFAULT_WHITELIST
|
28
|
+
end
|
29
|
+
|
30
|
+
def whitelist=(value)
|
31
|
+
@whitelist = value == :default ? DEFAULT_WHITELIST : value
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def validate_config_options
|
36
|
+
[@blacklist, @whitelist].each{ |content| validate_list_content(content) if content }
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_list_content(content)
|
40
|
+
case content
|
41
|
+
when Array then !content.empty? || raise(Obscenity::EmptyContentList.new('Content array is empty.'))
|
42
|
+
when String then File.exists?(content) || raise(Obscenity::UnkownContentFile.new("Content file can't be found."))
|
43
|
+
when Pathname then content.exist? || raise(Obscenity::UnkownContentFile.new("Content file can't be found."))
|
44
|
+
when Symbol then content == :default || raise(Obscenity::UnkownContent.new("The only accepted symbol is :default."))
|
45
|
+
else
|
46
|
+
raise Obscenity::UnkownContent.new("The content can be either an Array, Pathname, or String path to a .yml file.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Rack
|
2
|
+
class Obscenity
|
3
|
+
|
4
|
+
def initialize(app, options = {})
|
5
|
+
@app, @options = app, options
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
rejectable = false
|
10
|
+
post_params = Rack::Utils.parse_query(env['rack.input'].read, "&")
|
11
|
+
get_params = Rack::Utils.parse_query(env['QUERY_STRING'], "&")
|
12
|
+
|
13
|
+
if @options.has_key?(:reject)
|
14
|
+
rejactable = validate_rejectability_of( select_params(:reject, get_params.update(post_params)) )
|
15
|
+
|
16
|
+
elsif @options.has_key?(:sanitize)
|
17
|
+
get_params = sanitize_contents_of(get_params)
|
18
|
+
post_params = sanitize_contents_of(post_params)
|
19
|
+
|
20
|
+
env['QUERY_STRING'] = Rack::Utils.build_query(get_params)
|
21
|
+
env['rack.input'] = StringIO.new(Rack::Utils.build_query(post_params))
|
22
|
+
end
|
23
|
+
|
24
|
+
rejactable ? reject : continue(env)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def continue(env)
|
29
|
+
@app.call(env)
|
30
|
+
end
|
31
|
+
|
32
|
+
def reject
|
33
|
+
length, content = 0, ''
|
34
|
+
if @options[:reject].is_a?(Hash)
|
35
|
+
if (message = @options[:reject][:message]).present?
|
36
|
+
content = message
|
37
|
+
length = message.size
|
38
|
+
elsif (path = @options[:reject][:path]).present?
|
39
|
+
if (path = ::File.expand_path(path)) && ::File.exists?(path)
|
40
|
+
content = ::File.read(path)
|
41
|
+
length = content.size
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
[422, {'Content-Type' => 'text/html', 'Content-Length' => length.to_s}, [content]]
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_rejectability_of(params = {})
|
50
|
+
should_reject_request = false
|
51
|
+
params.each_pair do |param, value|
|
52
|
+
if value.is_a?(Hash)
|
53
|
+
validates_rejectability_of(value)
|
54
|
+
elsif value.is_a?(String)
|
55
|
+
next unless value.size >= 3
|
56
|
+
if ::Obscenity.profane?(value)
|
57
|
+
should_reject_request = true
|
58
|
+
break
|
59
|
+
end
|
60
|
+
else
|
61
|
+
next
|
62
|
+
end
|
63
|
+
end
|
64
|
+
should_reject_request
|
65
|
+
end
|
66
|
+
|
67
|
+
def sanitize_contents_of(params)
|
68
|
+
sanitized_params = {}
|
69
|
+
replacement_method = @options[:sanitize].is_a?(Hash) && @options[:sanitize][:replacement]
|
70
|
+
select_params(:sanitize, params).each{|param, value|
|
71
|
+
if value.is_a?(String)
|
72
|
+
next unless value.size >= 3
|
73
|
+
sanitized_params[param] = ::Obscenity.replacement(replacement_method).sanitize(value)
|
74
|
+
else
|
75
|
+
next
|
76
|
+
end
|
77
|
+
}
|
78
|
+
params.update(sanitized_params)
|
79
|
+
end
|
80
|
+
|
81
|
+
def select_params(key, params = {})
|
82
|
+
if @options[key].is_a?(Hash) && @options[key][:params].is_a?(Array)
|
83
|
+
params.select{ |param, vvalue| @options[key][:params].include?(param.to_sym) }
|
84
|
+
else
|
85
|
+
params
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/obscenity.gemspec
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "obscenity"
|
8
|
+
s.version = "1.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Thiago Jackiw"]
|
12
|
+
s.date = "2012-05-28"
|
13
|
+
s.description = " Obscenity is a profanity filter gem for Ruby/Rubinius, Rails (through ActiveModel), and Rack middleware "
|
14
|
+
s.email = "tjackiw@gmail.com"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
".travis.yml",
|
23
|
+
"Gemfile",
|
24
|
+
"Gemfile.lock",
|
25
|
+
"LICENSE.txt",
|
26
|
+
"README.md",
|
27
|
+
"Rakefile",
|
28
|
+
"config/blacklist.yml",
|
29
|
+
"config/international.yml",
|
30
|
+
"lib/obscenity.rb",
|
31
|
+
"lib/obscenity/active_model.rb",
|
32
|
+
"lib/obscenity/base.rb",
|
33
|
+
"lib/obscenity/config.rb",
|
34
|
+
"lib/obscenity/error.rb",
|
35
|
+
"lib/obscenity/rack.rb",
|
36
|
+
"lib/obscenity/version.rb",
|
37
|
+
"obscenity.gemspec",
|
38
|
+
"test/helper.rb",
|
39
|
+
"test/static/422.html",
|
40
|
+
"test/test_active_model.rb",
|
41
|
+
"test/test_base.rb",
|
42
|
+
"test/test_config.rb",
|
43
|
+
"test/test_obscenity.rb",
|
44
|
+
"test/test_rack.rb",
|
45
|
+
"test/test_version.rb"
|
46
|
+
]
|
47
|
+
s.homepage = "http://github.com/tjackiw/obscenity"
|
48
|
+
s.licenses = ["MIT"]
|
49
|
+
s.require_paths = ["lib"]
|
50
|
+
s.rubygems_version = "1.8.24"
|
51
|
+
s.summary = "Obscenity is a profanity filter gem for Ruby/Rubinius, Rails (through ActiveModel), and Rack middleware"
|
52
|
+
s.test_files = ["test/helper.rb", "test/static/422.html", "test/test_active_model.rb", "test/test_base.rb", "test/test_config.rb", "test/test_obscenity.rb", "test/test_rack.rb", "test/test_version.rb"]
|
53
|
+
|
54
|
+
if s.respond_to? :specification_version then
|
55
|
+
s.specification_version = 3
|
56
|
+
|
57
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
58
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
59
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
60
|
+
s.add_development_dependency(%q<bundler>, [">= 0"])
|
61
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.8.3"])
|
62
|
+
s.add_development_dependency(%q<activemodel>, ["~> 3.0"])
|
63
|
+
s.add_development_dependency(%q<rack>, [">= 0"])
|
64
|
+
s.add_development_dependency(%q<rake>, [">= 0"])
|
65
|
+
else
|
66
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
67
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
68
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
69
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
70
|
+
s.add_dependency(%q<activemodel>, ["~> 3.0"])
|
71
|
+
s.add_dependency(%q<rack>, [">= 0"])
|
72
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
73
|
+
end
|
74
|
+
else
|
75
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
76
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
77
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
78
|
+
s.add_dependency(%q<jeweler>, ["~> 1.8.3"])
|
79
|
+
s.add_dependency(%q<activemodel>, ["~> 3.0"])
|
80
|
+
s.add_dependency(%q<rack>, [">= 0"])
|
81
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'test/unit'
|
11
|
+
require 'shoulda'
|
12
|
+
|
13
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
14
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
15
|
+
|
16
|
+
require 'active_model'
|
17
|
+
require 'obscenity'
|
18
|
+
require 'obscenity/active_model'
|
19
|
+
|
20
|
+
module Dummy
|
21
|
+
class BaseModel
|
22
|
+
include ActiveModel::Validations
|
23
|
+
|
24
|
+
attr_accessor :title
|
25
|
+
|
26
|
+
def initialize(attr_names)
|
27
|
+
attr_names.each{ |k,v| send("#{k}=", v) }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Test::Unit::TestCase
|
33
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
We don't accept profanity
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestActiveModel < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def generate_new_class(name, options = {})
|
6
|
+
Dummy.send(:remove_const, name) if Dummy.const_defined?(name)
|
7
|
+
klass = Class.new(Dummy::BaseModel) do
|
8
|
+
validates :title, options
|
9
|
+
end
|
10
|
+
Dummy.const_set(name, klass)
|
11
|
+
end
|
12
|
+
|
13
|
+
should "be invalid when title is profane" do
|
14
|
+
klass = generate_new_class("Post", obscenity: true)
|
15
|
+
post = klass.new(title: "He who poops, shits itself")
|
16
|
+
assert !post.valid?
|
17
|
+
assert post.errors.has_key?(:title)
|
18
|
+
assert_equal ['cannot be profane'], post.errors[:title]
|
19
|
+
end
|
20
|
+
|
21
|
+
should "be invalid when title is profane and should include a custom error message" do
|
22
|
+
klass = generate_new_class("Post", obscenity: { message: "can't be profane!" })
|
23
|
+
post = klass.new(title: "He who poops, shits itself")
|
24
|
+
assert !post.valid?
|
25
|
+
assert post.errors.has_key?(:title)
|
26
|
+
assert_equal ["can't be profane!"], post.errors[:title]
|
27
|
+
end
|
28
|
+
|
29
|
+
should "sanitize the title using the default replacement" do
|
30
|
+
klass = generate_new_class("Post", obscenity: { sanitize: true })
|
31
|
+
post = klass.new(title: "He who poops, shits itself")
|
32
|
+
assert post.valid?
|
33
|
+
assert !post.errors.has_key?(:title)
|
34
|
+
assert_equal "He who poops, $@!#% itself", post.title
|
35
|
+
end
|
36
|
+
|
37
|
+
should "sanitize the title using the :garbled replacement" do
|
38
|
+
klass = generate_new_class("Post", obscenity: { sanitize: true, replacement: :garbled })
|
39
|
+
post = klass.new(title: "He who poops, shits itself")
|
40
|
+
assert post.valid?
|
41
|
+
assert !post.errors.has_key?(:title)
|
42
|
+
assert_equal "He who poops, $@!#% itself", post.title
|
43
|
+
end
|
44
|
+
|
45
|
+
should "sanitize the title using the :stars replacement" do
|
46
|
+
klass = generate_new_class("Post", obscenity: { sanitize: true, replacement: :stars })
|
47
|
+
post = klass.new(title: "He who poops, shits itself")
|
48
|
+
assert post.valid?
|
49
|
+
assert !post.errors.has_key?(:title)
|
50
|
+
assert_equal "He who poops, ***** itself", post.title
|
51
|
+
end
|
52
|
+
|
53
|
+
should "sanitize the title using the :vowels replacement" do
|
54
|
+
klass = generate_new_class("Post", obscenity: { sanitize: true, replacement: :vowels })
|
55
|
+
post = klass.new(title: "He who poops, shits itself")
|
56
|
+
assert post.valid?
|
57
|
+
assert !post.errors.has_key?(:title)
|
58
|
+
assert_equal "He who poops, sh*ts itself", post.title
|
59
|
+
end
|
60
|
+
|
61
|
+
should "sanitize the title using a custom replacement" do
|
62
|
+
klass = generate_new_class("Post", obscenity: { sanitize: true, replacement: '[censored]' })
|
63
|
+
post = klass.new(title: "He who poops, shits itself")
|
64
|
+
assert post.valid?
|
65
|
+
assert !post.errors.has_key?(:title)
|
66
|
+
assert_equal "He who poops, [censored] itself", post.title
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|