acts_as_textcaptcha 4.2.0 → 4.5.2

Sign up to get free protection for your applications and to get access to all the features.
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