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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -1
  3. data/.rubocop.yml +1165 -0
  4. data/.simplecov +11 -0
  5. data/.travis.yml +30 -8
  6. data/Appraisals +9 -6
  7. data/CHANGELOG.md +94 -34
  8. data/CODE_OF_CONDUCT.md +54 -31
  9. data/CONTRIBUTING.md +14 -9
  10. data/Gemfile +2 -2
  11. data/LICENSE +165 -0
  12. data/PULL_REQUEST_TEMPLATE.md +16 -0
  13. data/README.md +206 -228
  14. data/Rakefile +26 -19
  15. data/acts_as_textcaptcha.gemspec +57 -41
  16. data/bin/console +2 -5
  17. data/bin/setup +7 -0
  18. data/gemfiles/rails_3.gemfile +1 -0
  19. data/gemfiles/rails_4.gemfile +2 -1
  20. data/gemfiles/rails_5.gemfile +2 -1
  21. data/gemfiles/rails_6.gemfile +8 -0
  22. data/lib/acts_as_textcaptcha.rb +9 -4
  23. data/lib/acts_as_textcaptcha/errors.rb +21 -0
  24. data/lib/acts_as_textcaptcha/framework/rails.rb +3 -1
  25. data/lib/acts_as_textcaptcha/railtie.rb +9 -0
  26. data/lib/acts_as_textcaptcha/tasks/textcaptcha.rake +17 -0
  27. data/lib/acts_as_textcaptcha/textcaptcha.rb +75 -72
  28. data/lib/acts_as_textcaptcha/textcaptcha_api.rb +39 -45
  29. data/lib/acts_as_textcaptcha/textcaptcha_cache.rb +12 -16
  30. data/lib/acts_as_textcaptcha/textcaptcha_config.rb +47 -0
  31. data/lib/acts_as_textcaptcha/textcaptcha_helper.rb +13 -14
  32. data/lib/acts_as_textcaptcha/version.rb +3 -1
  33. metadata +68 -46
  34. data/.coveralls.yml +0 -1
  35. data/LICENSE.txt +0 -21
  36. data/config/textcaptcha.yml +0 -34
  37. data/lib/tasks/textcaptcha.rake +0 -21
  38. data/test/schema.rb +0 -34
  39. data/test/test_helper.rb +0 -44
  40. data/test/test_models.rb +0 -69
  41. data/test/textcaptcha_api_test.rb +0 -46
  42. data/test/textcaptcha_cache_test.rb +0 -25
  43. data/test/textcaptcha_helper_test.rb +0 -68
  44. data/test/textcaptcha_test.rb +0 -198
data/Rakefile CHANGED
@@ -1,32 +1,39 @@
1
- require 'rubygems'
2
- require 'bundler/gem_tasks'
3
- require 'rake/testtask'
1
+ # frozen_string_literal: true
4
2
 
5
- gem 'rdoc'
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['test/**/*_test.rb']
21
+ t.test_files = FileList["test/**/*_test.rb"]
11
22
  end
12
23
 
13
- task :default => [:test]
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 (simplecov)"
31
+ desc "Run all tests and features and generate a code coverage report"
18
32
  task :coverage do
19
- ENV['COVERAGE'] = 'true'
20
- Rake::Task['test'].execute
33
+ ENV["COVERAGE"] = "true"
34
+ Rake::Task["test"].execute
35
+ Rake::Task["rubocop"].execute
21
36
  end
22
37
  end
23
38
 
24
- # rdoc tasks
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"]
@@ -1,45 +1,61 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
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 |s|
7
- s.name = "acts_as_textcaptcha"
8
- s.version = ActsAsTextcaptcha::VERSION
9
- s.authors = ["Matthew Hutchinson"]
10
- s.email = ["matt@hiddenloop.com"]
11
- s.homepage = "http://github.com/matthutchinson/acts_as_textcaptcha"
12
- s.license = 'MIT'
13
- s.summary = %q{Spam protection for your models via logic questions and the textcaptcha.com API}
14
-
15
- s.description = %q{Simple question/answer based spam protection for your Rails models.
16
- You can define your own logic questions and/or fetch questions from the textcaptcha.com API.
17
- The questions involve human logic and are tough for spam bots to crack.
18
- For more reasons on why logic questions are a good idea visit; http://textcaptcha.com/why}
19
-
20
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the
21
- # 'allowed_push_host' to allow pushing to a single host or delete this section
22
- # to allow pushing to any host.
23
- if s.respond_to?(:metadata)
24
- s.metadata['allowed_push_host'] = "https://rubygems.org"
25
- else
26
- raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
27
- end
28
-
29
- s.files = `git ls-files`.split("\n")
30
- s.test_files = `git ls-files -- {test}/*`.split("\n")
31
- s.require_paths = ["lib"]
32
-
33
- # always test against latest rails version
34
- s.add_development_dependency('rails', '~> 5.1.4')
35
-
36
- s.add_development_dependency('mime-types')
37
- s.add_development_dependency('bundler')
38
- s.add_development_dependency('minitest')
39
- s.add_development_dependency('simplecov')
40
- s.add_development_dependency('rdoc')
41
- s.add_development_dependency('sqlite3')
42
- s.add_development_dependency('webmock')
43
- s.add_development_dependency('coveralls')
44
- s.add_development_dependency('appraisal')
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
@@ -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
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+ bundle exec rake rdoc
@@ -3,5 +3,6 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rails", "3.2.22.5"
6
+ gem "sqlite3", "~> 1.3.5"
6
7
 
7
8
  gemspec path: "../"
@@ -2,6 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "4.2.10"
5
+ gem "rails", "4.2.11.3"
6
+ gem "sqlite3", "~> 1.3.5"
6
7
 
7
8
  gemspec path: "../"
@@ -2,6 +2,7 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "rails", "5.1.4"
5
+ gem "rails", "5.2.4.4"
6
+ gem "sqlite3", "~> 1.4.2"
6
7
 
7
8
  gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "6.1.1"
6
+ gem "sqlite3", "~> 1.4.2"
7
+
8
+ gemspec path: "../"
@@ -1,4 +1,9 @@
1
- require 'acts_as_textcaptcha/version'
2
- require 'acts_as_textcaptcha/textcaptcha'
3
- require 'acts_as_textcaptcha/textcaptcha_helper'
4
- require 'acts_as_textcaptcha/framework/rails' if defined?(Rails)
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
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  ActiveSupport.on_load(:active_record) do
2
4
  extend ActsAsTextcaptcha::Textcaptcha
3
5
  end
4
6
 
5
7
  ActiveSupport.on_load(:action_view) do
6
8
  include ActsAsTextcaptcha::TextcaptchaHelper
7
- end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsTextcaptcha
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ Dir[File.join(File.dirname(__FILE__), "/tasks/*.rake")].each { |f| load f }
7
+ end
8
+ end
9
+ 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
- require 'yaml'
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
- module ActsAsTextcaptcha
8
-
9
- module Textcaptcha #:nodoc:
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 :textcaptcha_question, :textcaptcha_answer, :textcaptcha_key
13
+ attr_accessor :textcaptcha_question, :textcaptcha_answer, :textcaptcha_key
14
14
 
15
- # Rails 3, ensure these attrs are accessible
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
- validate :validate_textcaptcha, :if => :perform_textcaptcha?
18
+ self.textcaptcha_config = build_textcaptcha_config(options).symbolize_keys!
21
19
 
22
- if options.is_a?(Hash)
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
- # override this method to toggle textcaptcha checking
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?('new_record?') || new_record?
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 textcaptcha_config_questions
70
- if textcaptcha_config[:questions]
71
- random_question = textcaptcha_config[:questions][rand(textcaptcha_config[:questions].size)].symbolize_keys!
72
- [random_question[:question], (random_question[:answers] || '').split(',').map!{ |answer| safe_md5(answer) }]
73
- end
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
- # check textcaptcha, if incorrect, regenerate a new textcaptcha
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
- if valid_answers.empty?
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(question, answers)
107
- self.textcaptcha_question = question
108
- self.textcaptcha_key = textcaptcha_random_key
109
- textcaptcha_cache.write(textcaptcha_key, answers, textcaptcha_cache_options)
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
- # safe proxy for string methods) then downcase
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 and random
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
- { :expires_in => textcaptcha_config[:cache_expiry_minutes].to_f.minutes }
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
- @@textcaptcha_cache ||= TextcaptchaCache.new
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