acts_as_textcaptcha 4.2.0 → 4.5.2
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.
- checksums.yaml +4 -4
- data/.gitignore +2 -1
- data/.rubocop.yml +1165 -0
- data/.simplecov +11 -0
- data/.travis.yml +30 -8
- data/Appraisals +9 -6
- data/CHANGELOG.md +94 -34
- data/CODE_OF_CONDUCT.md +54 -31
- data/CONTRIBUTING.md +14 -9
- data/Gemfile +2 -2
- data/LICENSE +165 -0
- data/PULL_REQUEST_TEMPLATE.md +16 -0
- data/README.md +206 -228
- data/Rakefile +26 -19
- data/acts_as_textcaptcha.gemspec +57 -41
- data/bin/console +2 -5
- data/bin/setup +7 -0
- data/gemfiles/rails_3.gemfile +1 -0
- data/gemfiles/rails_4.gemfile +2 -1
- data/gemfiles/rails_5.gemfile +2 -1
- data/gemfiles/rails_6.gemfile +8 -0
- data/lib/acts_as_textcaptcha.rb +9 -4
- data/lib/acts_as_textcaptcha/errors.rb +21 -0
- data/lib/acts_as_textcaptcha/framework/rails.rb +3 -1
- data/lib/acts_as_textcaptcha/railtie.rb +9 -0
- data/lib/acts_as_textcaptcha/tasks/textcaptcha.rake +17 -0
- data/lib/acts_as_textcaptcha/textcaptcha.rb +75 -72
- data/lib/acts_as_textcaptcha/textcaptcha_api.rb +39 -45
- data/lib/acts_as_textcaptcha/textcaptcha_cache.rb +12 -16
- data/lib/acts_as_textcaptcha/textcaptcha_config.rb +47 -0
- data/lib/acts_as_textcaptcha/textcaptcha_helper.rb +13 -14
- data/lib/acts_as_textcaptcha/version.rb +3 -1
- metadata +68 -46
- data/.coveralls.yml +0 -1
- data/LICENSE.txt +0 -21
- data/config/textcaptcha.yml +0 -34
- data/lib/tasks/textcaptcha.rake +0 -21
- data/test/schema.rb +0 -34
- data/test/test_helper.rb +0 -44
- data/test/test_models.rb +0 -69
- data/test/textcaptcha_api_test.rb +0 -46
- data/test/textcaptcha_cache_test.rb +0 -25
- data/test/textcaptcha_helper_test.rb +0 -68
- data/test/textcaptcha_test.rb +0 -198
data/Rakefile
CHANGED
@@ -1,32 +1,39 @@
|
|
1
|
-
|
2
|
-
require 'bundler/gem_tasks'
|
3
|
-
require 'rake/testtask'
|
1
|
+
# frozen_string_literal: true
|
4
2
|
|
5
|
-
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rake/testtask"
|
5
|
+
require "rdoc/task"
|
6
|
+
require "rubocop/rake_task"
|
6
7
|
|
8
|
+
# generate docs
|
9
|
+
RDoc::Task.new do |rd|
|
10
|
+
rd.main = "README.md"
|
11
|
+
rd.title = "ActsAsTextcaptcha"
|
12
|
+
rd.rdoc_dir = "doc"
|
13
|
+
rd.options << "--all"
|
14
|
+
rd.rdoc_files.include("README.md", "LICENSE", "lib/**/*.rb")
|
15
|
+
end
|
16
|
+
|
17
|
+
# run tests
|
7
18
|
Rake::TestTask.new(:test) do |t|
|
8
19
|
t.libs << "test"
|
9
20
|
t.libs << "lib"
|
10
|
-
t.test_files = FileList[
|
21
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
11
22
|
end
|
12
23
|
|
13
|
-
|
24
|
+
# run lint
|
25
|
+
RuboCop::RakeTask.new(:rubocop) do |t|
|
26
|
+
t.options = ["--display-cop-names"]
|
27
|
+
end
|
14
28
|
|
15
|
-
# code coverage
|
29
|
+
# run tests with code coverage (default)
|
16
30
|
namespace :test do
|
17
|
-
desc "Run all tests and generate a code coverage report
|
31
|
+
desc "Run all tests and features and generate a code coverage report"
|
18
32
|
task :coverage do
|
19
|
-
ENV[
|
20
|
-
Rake::Task[
|
33
|
+
ENV["COVERAGE"] = "true"
|
34
|
+
Rake::Task["test"].execute
|
35
|
+
Rake::Task["rubocop"].execute
|
21
36
|
end
|
22
37
|
end
|
23
38
|
|
24
|
-
|
25
|
-
require 'rdoc/task'
|
26
|
-
RDoc::Task.new do |rd|
|
27
|
-
rd.main = "README.md"
|
28
|
-
rd.title = 'acts_as_textcaptcha'
|
29
|
-
rd.rdoc_dir = 'doc'
|
30
|
-
rd.options << "--all"
|
31
|
-
rd.rdoc_files.include("README.md", "LICENSE.txt", "lib/**/*.rb")
|
32
|
-
end
|
39
|
+
task default: [:rubocop, "test:coverage"]
|
data/acts_as_textcaptcha.gemspec
CHANGED
@@ -1,45 +1,61 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require "acts_as_textcaptcha/version"
|
5
6
|
|
6
|
-
Gem::Specification.new do |
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "acts_as_textcaptcha"
|
9
|
+
spec.version = ActsAsTextcaptcha::VERSION
|
10
|
+
spec.authors = ["Matthew Hutchinson"]
|
11
|
+
spec.email = ["matt@hiddenloop.com"]
|
12
|
+
spec.homepage = "http://github.com/matthutchinson/acts_as_textcaptcha"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.summary = "A text-based logic question captcha for Rails"
|
15
|
+
|
16
|
+
spec.description = <<-DESCRIPTION
|
17
|
+
ActsAsTextcaptcha provides spam protection for Rails models with text-based
|
18
|
+
logic question captchas. Questions are fetched from Rob Tuley's
|
19
|
+
textcaptcha.com They can be solved easily by humans but are tough for robots
|
20
|
+
to crack.
|
21
|
+
DESCRIPTION
|
22
|
+
|
23
|
+
spec.metadata = {
|
24
|
+
"homepage_uri" => "https://github.com/matthutchinson/acts_as_textcaptcha",
|
25
|
+
"changelog_uri" => "https://github.com/matthutchinson/acts_as_textcaptcha/blob/master/CHANGELOG.md",
|
26
|
+
"source_code_uri" => "https://github.com/matthutchinson/acts_as_textcaptcha",
|
27
|
+
"bug_tracker_uri" => "https://github.com/matthutchinson/acts_as_textcaptcha/issues",
|
28
|
+
"allowed_push_host" => "https://rubygems.org"
|
29
|
+
}
|
30
|
+
|
31
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
32
|
+
spec.test_files = `git ls-files -- {test}/*`.split("\n")
|
33
|
+
spec.bindir = "bin"
|
34
|
+
spec.require_paths = ["lib"]
|
35
|
+
|
36
|
+
# documentation
|
37
|
+
spec.extra_rdoc_files = ["README.md", "LICENSE"]
|
38
|
+
spec.rdoc_options << "--title" << "ActAsTextcaptcha" << "--main" << "README.md" << "-ri"
|
39
|
+
|
40
|
+
# non-gem dependecies
|
41
|
+
spec.required_ruby_version = ">= 2.5"
|
42
|
+
|
43
|
+
# dev gems
|
44
|
+
spec.add_development_dependency("bundler")
|
45
|
+
spec.add_development_dependency("pry-byebug")
|
46
|
+
spec.add_development_dependency "rake"
|
47
|
+
|
48
|
+
# Lint
|
49
|
+
spec.add_development_dependency("rubocop")
|
50
|
+
|
51
|
+
# docs
|
52
|
+
spec.add_development_dependency("rdoc")
|
53
|
+
|
54
|
+
# testing
|
55
|
+
spec.add_development_dependency("appraisal")
|
56
|
+
spec.add_development_dependency("minitest")
|
57
|
+
spec.add_development_dependency("rails", "~> 6.0.3.4")
|
58
|
+
spec.add_development_dependency("simplecov", "~> 0.19.1")
|
59
|
+
spec.add_development_dependency("sqlite3")
|
60
|
+
spec.add_development_dependency("webmock")
|
45
61
|
end
|
data/bin/console
CHANGED
@@ -1,11 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require "bundler/setup"
|
4
|
+
require "rails/all"
|
4
5
|
require "acts_as_textcaptcha"
|
5
|
-
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# start irb
|
10
6
|
require "irb"
|
7
|
+
|
11
8
|
IRB.start
|
data/bin/setup
ADDED
data/gemfiles/rails_3.gemfile
CHANGED
data/gemfiles/rails_4.gemfile
CHANGED
data/gemfiles/rails_5.gemfile
CHANGED
data/lib/acts_as_textcaptcha.rb
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
4
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "acts_as_textcaptcha/version"
|
4
|
+
require "acts_as_textcaptcha/errors"
|
5
|
+
require "acts_as_textcaptcha/textcaptcha"
|
6
|
+
require "acts_as_textcaptcha/textcaptcha_config"
|
7
|
+
require "acts_as_textcaptcha/textcaptcha_helper"
|
8
|
+
require "acts_as_textcaptcha/framework/rails"
|
9
|
+
require "acts_as_textcaptcha/railtie" if defined?(Rails::Railtie)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActsAsTextcaptcha
|
4
|
+
class ResponseError < StandardError
|
5
|
+
def initialize(url, exception)
|
6
|
+
super("fetching '#{url}' failed - #{exception}")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class ApiKeyError < StandardError
|
11
|
+
def initialize(api_key, exception)
|
12
|
+
super("Api key '#{api_key}' is invalid - #{exception}")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ParseError < StandardError
|
17
|
+
def initialize(url)
|
18
|
+
super("parsing JSON from '#{url}' failed")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails"
|
4
|
+
require "acts_as_textcaptcha/textcaptcha_config"
|
5
|
+
|
6
|
+
namespace :textcaptcha do
|
7
|
+
desc "Creates an example textcaptcha config at config/textcaptcha.yml"
|
8
|
+
task :config do
|
9
|
+
path = File.join((Rails.root || "."), "config", "textcaptcha.yml")
|
10
|
+
if File.exist?(path)
|
11
|
+
puts "Ooops, a textcaptcha config file at #{path} already exists ... aborting."
|
12
|
+
else
|
13
|
+
ActsAsTextcaptcha::TextcaptchaConfig.create(path: path)
|
14
|
+
puts "Done, config generated at #{path}\nEdit this file to add your TextCaptcha API key (see https://textcaptcha.com)."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -1,80 +1,63 @@
|
|
1
|
-
|
2
|
-
require 'net/http'
|
3
|
-
require 'digest/md5'
|
4
|
-
require 'acts_as_textcaptcha/textcaptcha_cache'
|
5
|
-
require 'acts_as_textcaptcha/textcaptcha_api'
|
1
|
+
# frozen_string_literal: true
|
6
2
|
|
7
|
-
|
8
|
-
|
9
|
-
|
3
|
+
require "yaml"
|
4
|
+
require "net/http"
|
5
|
+
require "digest/md5"
|
6
|
+
require "acts_as_textcaptcha/textcaptcha_cache"
|
7
|
+
require "acts_as_textcaptcha/textcaptcha_api"
|
10
8
|
|
9
|
+
module ActsAsTextcaptcha
|
10
|
+
module Textcaptcha
|
11
11
|
def acts_as_textcaptcha(options = nil)
|
12
12
|
cattr_accessor :textcaptcha_config
|
13
|
-
attr_accessor
|
13
|
+
attr_accessor :textcaptcha_question, :textcaptcha_answer, :textcaptcha_key
|
14
14
|
|
15
|
-
#
|
16
|
-
if respond_to?(:accessible_attributes) && respond_to?(:attr_accessible)
|
17
|
-
attr_accessible :textcaptcha_answer, :textcaptcha_key
|
18
|
-
end
|
15
|
+
# ensure these attrs are accessible (Rails 3)
|
16
|
+
attr_accessible :textcaptcha_answer, :textcaptcha_key if respond_to?(:accessible_attributes) && respond_to?(:attr_accessible)
|
19
17
|
|
20
|
-
|
18
|
+
self.textcaptcha_config = build_textcaptcha_config(options).symbolize_keys!
|
21
19
|
|
22
|
-
if
|
23
|
-
self.textcaptcha_config = options.symbolize_keys!
|
24
|
-
else
|
25
|
-
begin
|
26
|
-
self.textcaptcha_config = YAML.load(File.read("#{Rails.root ? Rails.root.to_s : '.'}/config/textcaptcha.yml"))[Rails.env].symbolize_keys!
|
27
|
-
rescue
|
28
|
-
raise ArgumentError.new('could not find any textcaptcha options, in config/textcaptcha.yml or model - run rake textcaptcha:config to generate a template config file')
|
29
|
-
end
|
30
|
-
end
|
20
|
+
validate :validate_textcaptcha, if: :perform_textcaptcha?
|
31
21
|
|
32
22
|
include InstanceMethods
|
33
23
|
end
|
34
24
|
|
35
|
-
|
36
25
|
module InstanceMethods
|
37
|
-
|
38
|
-
#
|
39
|
-
# by default this will only allow new records to be
|
40
|
-
# protected with textcaptchas
|
26
|
+
# override this method to toggle textcaptcha checking, by default this
|
27
|
+
# will only allow new records to be protected with textcaptchas
|
41
28
|
def perform_textcaptcha?
|
42
|
-
!respond_to?(
|
29
|
+
(!respond_to?("new_record?") || new_record?)
|
43
30
|
end
|
44
31
|
|
45
|
-
# generate and assign textcaptcha
|
46
32
|
def textcaptcha
|
47
|
-
if perform_textcaptcha? && textcaptcha_config
|
48
|
-
question = answers = nil
|
49
|
-
|
50
|
-
# get textcaptcha from api
|
51
|
-
if textcaptcha_config[:api_key]
|
52
|
-
question, answers = TextcaptchaApi.fetch(textcaptcha_config[:api_key], textcaptcha_config)
|
53
|
-
end
|
54
|
-
|
55
|
-
# fall back to config based textcaptcha
|
56
|
-
unless question && answers
|
57
|
-
question, answers = textcaptcha_config_questions
|
58
|
-
end
|
59
|
-
|
60
|
-
if question && answers
|
61
|
-
assign_textcaptcha(question, answers)
|
62
|
-
end
|
63
|
-
end
|
33
|
+
assign_textcaptcha(fetch_q_and_a || config_q_and_a) if perform_textcaptcha? && textcaptcha_config
|
64
34
|
end
|
65
35
|
|
66
|
-
|
67
36
|
private
|
68
37
|
|
69
|
-
def
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
38
|
+
def fetch_q_and_a
|
39
|
+
return unless should_fetch?
|
40
|
+
|
41
|
+
TextcaptchaApi.new(
|
42
|
+
api_key: textcaptcha_config[:api_key],
|
43
|
+
api_endpoint: textcaptcha_config[:api_endpoint],
|
44
|
+
raise_errors: textcaptcha_config[:raise_errors]
|
45
|
+
).fetch
|
46
|
+
end
|
47
|
+
|
48
|
+
def should_fetch?
|
49
|
+
textcaptcha_config[:api_key] || textcaptcha_config[:api_endpoint]
|
74
50
|
end
|
75
51
|
|
52
|
+
def config_q_and_a
|
53
|
+
return unless textcaptcha_config[:questions]
|
76
54
|
|
77
|
-
|
55
|
+
random_question = textcaptcha_config[:questions][rand(textcaptcha_config[:questions].size)].symbolize_keys!
|
56
|
+
answers = (random_question[:answers] || "").split(",").map { |answer| safe_md5(answer) }
|
57
|
+
{ "q" => random_question[:question], "a" => answers } if random_question && answers.present?
|
58
|
+
end
|
59
|
+
|
60
|
+
# check textcaptcha, if incorrect, generate a new textcaptcha
|
78
61
|
def validate_textcaptcha
|
79
62
|
valid_answers = textcaptcha_cache.read(textcaptcha_key) || []
|
80
63
|
reset_textcaptcha
|
@@ -84,18 +67,20 @@ module ActsAsTextcaptcha
|
|
84
67
|
textcaptcha_cache.write(textcaptcha_key, valid_answers, textcaptcha_cache_options)
|
85
68
|
true
|
86
69
|
else
|
87
|
-
|
88
|
-
# took too long to answer
|
89
|
-
errors.add(:textcaptcha_answer, :expired, :message => 'was not submitted quickly enough, try another question instead')
|
90
|
-
else
|
91
|
-
# incorrect answer
|
92
|
-
errors.add(:textcaptcha_answer, :incorrect, :message => 'is incorrect, try another question instead')
|
93
|
-
end
|
70
|
+
add_textcaptcha_error(too_slow: valid_answers.empty?)
|
94
71
|
textcaptcha
|
95
72
|
false
|
96
73
|
end
|
97
74
|
end
|
98
75
|
|
76
|
+
def add_textcaptcha_error(too_slow: false)
|
77
|
+
if too_slow
|
78
|
+
errors.add(:textcaptcha_answer, :expired, message: "was not submitted quickly enough, try another question instead")
|
79
|
+
else
|
80
|
+
errors.add(:textcaptcha_answer, :incorrect, message: "is incorrect, try another question instead")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
99
84
|
def reset_textcaptcha
|
100
85
|
if textcaptcha_key
|
101
86
|
textcaptcha_cache.delete(textcaptcha_key)
|
@@ -103,36 +88,54 @@ module ActsAsTextcaptcha
|
|
103
88
|
end
|
104
89
|
end
|
105
90
|
|
106
|
-
def assign_textcaptcha(
|
107
|
-
|
108
|
-
|
109
|
-
|
91
|
+
def assign_textcaptcha(q_and_a)
|
92
|
+
return unless q_and_a
|
93
|
+
|
94
|
+
self.textcaptcha_question = q_and_a["q"]
|
95
|
+
self.textcaptcha_key = textcaptcha_random_key
|
96
|
+
textcaptcha_cache.write(textcaptcha_key, q_and_a["a"], textcaptcha_cache_options)
|
110
97
|
end
|
111
98
|
|
112
|
-
# strip whitespace pass through mb_chars (a multibyte
|
113
|
-
#
|
99
|
+
# strip whitespace pass through mb_chars (a multibyte safe proxy for
|
100
|
+
# strings) then downcase
|
114
101
|
def safe_md5(answer)
|
115
102
|
Digest::MD5.hexdigest(answer.to_s.strip.mb_chars.downcase)
|
116
103
|
end
|
117
104
|
|
118
|
-
# a random cache key, time based
|
105
|
+
# a random cache key, time based, random
|
119
106
|
def textcaptcha_random_key
|
120
107
|
safe_md5(Time.now.to_i + rand(1_000_000))
|
121
108
|
end
|
122
109
|
|
123
110
|
def textcaptcha_cache_options
|
124
111
|
if textcaptcha_config[:cache_expiry_minutes]
|
125
|
-
{ :
|
112
|
+
{ expires_in: textcaptcha_config[:cache_expiry_minutes].to_f.minutes }
|
126
113
|
else
|
127
114
|
{}
|
128
115
|
end
|
129
116
|
end
|
130
117
|
|
131
|
-
# cache is used to persist textcaptcha questions and answers
|
132
|
-
# between requests
|
133
118
|
def textcaptcha_cache
|
134
|
-
|
119
|
+
@textcaptcha_cache ||= TextcaptchaCache.new
|
135
120
|
end
|
136
121
|
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# rubocop:disable Security/YAMLLoad
|
126
|
+
def build_textcaptcha_config(options)
|
127
|
+
if options.is_a?(Hash)
|
128
|
+
options
|
129
|
+
else
|
130
|
+
YAML.load(ERB.new(read_textcaptcha_config).result)[Rails.env]
|
131
|
+
end
|
132
|
+
rescue StandardError
|
133
|
+
raise ArgumentError, "could not find any textcaptcha options, in config/textcaptcha.yml or model - run rake textcaptcha:config to generate a template config file"
|
134
|
+
end
|
135
|
+
# rubocop:enable Security/YAMLLoad
|
136
|
+
|
137
|
+
def read_textcaptcha_config
|
138
|
+
File.read("#{Rails.root || "."}/config/textcaptcha.yml")
|
139
|
+
end
|
137
140
|
end
|
138
141
|
end
|