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
         |