acts_as_textcaptcha 4.4.1 → 4.6.0

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.
@@ -1,20 +1,19 @@
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
2
+
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"
6
8
 
7
9
  module ActsAsTextcaptcha
8
10
  module Textcaptcha
9
-
10
11
  def acts_as_textcaptcha(options = nil)
11
12
  cattr_accessor :textcaptcha_config
12
13
  attr_accessor :textcaptcha_question, :textcaptcha_answer, :textcaptcha_key
13
14
 
14
15
  # ensure these attrs are accessible (Rails 3)
15
- if respond_to?(:accessible_attributes) && respond_to?(:attr_accessible)
16
- attr_accessible :textcaptcha_answer, :textcaptcha_key
17
- end
16
+ attr_accessible :textcaptcha_answer, :textcaptcha_key if respond_to?(:accessible_attributes) && respond_to?(:attr_accessible)
18
17
 
19
18
  self.textcaptcha_config = build_textcaptcha_config(options).symbolize_keys!
20
19
 
@@ -24,122 +23,117 @@ module ActsAsTextcaptcha
24
23
  end
25
24
 
26
25
  module InstanceMethods
27
-
28
26
  # override this method to toggle textcaptcha checking, by default this
29
27
  # will only allow new records to be protected with textcaptchas
30
28
  def perform_textcaptcha?
31
- (!respond_to?('new_record?') || new_record?)
29
+ (!respond_to?("new_record?") || new_record?)
32
30
  end
33
31
 
34
32
  def textcaptcha
35
- if perform_textcaptcha? && textcaptcha_config
36
- assign_textcaptcha(fetch_q_and_a || config_q_and_a)
37
- end
33
+ assign_textcaptcha(fetch_q_and_a || config_q_and_a) if perform_textcaptcha? && textcaptcha_config
38
34
  end
39
35
 
40
-
41
36
  private
42
37
 
43
- def fetch_q_and_a
44
- return unless should_fetch?
38
+ def fetch_q_and_a
39
+ return unless should_fetch?
45
40
 
46
- TextcaptchaApi.new(
47
- api_key: textcaptcha_config[:api_key],
48
- api_endpoint: textcaptcha_config[:api_endpoint],
49
- raise_errors: textcaptcha_config[:raise_errors]
50
- ).fetch
51
- end
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
52
47
 
53
- def should_fetch?
54
- textcaptcha_config[:api_key] || textcaptcha_config[:api_endpoint]
55
- end
48
+ def should_fetch?
49
+ textcaptcha_config[:api_key] || textcaptcha_config[:api_endpoint]
50
+ end
56
51
 
57
- def config_q_and_a
58
- if textcaptcha_config[:questions]
59
- random_question = textcaptcha_config[:questions][rand(textcaptcha_config[:questions].size)].symbolize_keys!
60
- answers = (random_question[:answers] || '').split(',').map!{ |answer| safe_md5(answer) }
61
- if random_question && answers.present?
62
- { 'q' => random_question[:question], 'a' => answers }
63
- end
64
- end
65
- end
52
+ def config_q_and_a
53
+ return unless textcaptcha_config[:questions]
66
54
 
67
- # check textcaptcha, if incorrect, generate a new textcaptcha
68
- def validate_textcaptcha
69
- valid_answers = textcaptcha_cache.read(textcaptcha_key) || []
70
- reset_textcaptcha
71
- if valid_answers.include?(safe_md5(textcaptcha_answer))
72
- # answer was valid, mutate the key again
73
- self.textcaptcha_key = textcaptcha_random_key
74
- textcaptcha_cache.write(textcaptcha_key, valid_answers, textcaptcha_cache_options)
75
- true
76
- else
77
- add_textcaptcha_error(too_slow: valid_answers.empty?)
78
- textcaptcha
79
- false
80
- end
81
- end
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
82
59
 
83
- def add_textcaptcha_error(too_slow: false)
84
- if too_slow
85
- errors.add(:textcaptcha_answer, :expired, :message => 'was not submitted quickly enough, try another question instead')
86
- else
87
- errors.add(:textcaptcha_answer, :incorrect, :message => 'is incorrect, try another question instead')
88
- end
60
+ # check textcaptcha, if incorrect, generate a new textcaptcha
61
+ def validate_textcaptcha
62
+ valid_answers = textcaptcha_cache.read(textcaptcha_key) || []
63
+ reset_textcaptcha
64
+ if valid_answers.include?(safe_md5(textcaptcha_answer))
65
+ # answer was valid, mutate the key again
66
+ self.textcaptcha_key = textcaptcha_random_key
67
+ textcaptcha_cache.write(textcaptcha_key, valid_answers, textcaptcha_cache_options)
68
+ true
69
+ else
70
+ add_textcaptcha_error(too_slow: valid_answers.empty?)
71
+ textcaptcha
72
+ false
89
73
  end
74
+ end
90
75
 
91
- def reset_textcaptcha
92
- if textcaptcha_key
93
- textcaptcha_cache.delete(textcaptcha_key)
94
- self.textcaptcha_key = nil
95
- end
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")
96
81
  end
82
+ end
97
83
 
98
- def assign_textcaptcha(q_and_a)
99
- return unless q_and_a
100
- self.textcaptcha_question = q_and_a['q']
101
- self.textcaptcha_key = textcaptcha_random_key
102
- textcaptcha_cache.write(textcaptcha_key, q_and_a['a'], textcaptcha_cache_options)
84
+ def reset_textcaptcha
85
+ if textcaptcha_key
86
+ textcaptcha_cache.delete(textcaptcha_key)
87
+ self.textcaptcha_key = nil
103
88
  end
89
+ end
104
90
 
105
- # strip whitespace pass through mb_chars (a multibyte safe proxy for
106
- # strings) then downcase
107
- def safe_md5(answer)
108
- Digest::MD5.hexdigest(answer.to_s.strip.mb_chars.downcase)
109
- end
91
+ def assign_textcaptcha(q_and_a)
92
+ return unless q_and_a
110
93
 
111
- # a random cache key, time based, random
112
- def textcaptcha_random_key
113
- safe_md5(Time.now.to_i + rand(1_000_000))
114
- end
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)
97
+ end
115
98
 
116
- def textcaptcha_cache_options
117
- if textcaptcha_config[:cache_expiry_minutes]
118
- { :expires_in => textcaptcha_config[:cache_expiry_minutes].to_f.minutes }
119
- else
120
- {}
121
- end
122
- end
99
+ # strip whitespace pass through mb_chars (a multibyte safe proxy for
100
+ # strings) then downcase
101
+ def safe_md5(answer)
102
+ Digest::MD5.hexdigest(answer.to_s.strip.mb_chars.downcase)
103
+ end
104
+
105
+ # a random cache key, time based, random
106
+ def textcaptcha_random_key
107
+ safe_md5(Time.now.to_i + rand(1_000_000))
108
+ end
123
109
 
124
- def textcaptcha_cache
125
- @@textcaptcha_cache ||= TextcaptchaCache.new
110
+ def textcaptcha_cache_options
111
+ if textcaptcha_config[:cache_expiry_minutes]
112
+ { expires_in: textcaptcha_config[:cache_expiry_minutes].to_f.minutes }
113
+ else
114
+ {}
126
115
  end
116
+ end
117
+
118
+ def textcaptcha_cache
119
+ @textcaptcha_cache ||= TextcaptchaCache.new
120
+ end
127
121
  end
128
122
 
129
123
  private
130
124
 
131
- def build_textcaptcha_config(options)
132
- if options.is_a?(Hash)
133
- options
134
- else
135
- YAML.load(ERB.new(read_textcaptcha_config).result)[Rails.env]
136
- end
137
- rescue
138
- 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')
125
+ def build_textcaptcha_config(options)
126
+ if options.is_a?(Hash)
127
+ options
128
+ else
129
+ YAML.safe_load(ERB.new(read_textcaptcha_config).result, aliases: true)[Rails.env]
139
130
  end
131
+ rescue StandardError
132
+ raise ArgumentError, "could not find any textcaptcha options, in config/textcaptcha.yml or model - run rake textcaptcha:config to generate a template config file"
133
+ end
140
134
 
141
- def read_textcaptcha_config
142
- File.read("#{Rails.root ? Rails.root : '.'}/config/textcaptcha.yml")
143
- end
135
+ def read_textcaptcha_config
136
+ File.read("#{Rails.root || "."}/config/textcaptcha.yml")
137
+ end
144
138
  end
145
139
  end
@@ -1,19 +1,20 @@
1
- require 'json'
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
2
4
 
3
5
  module ActsAsTextcaptcha
4
6
  class TextcaptchaApi
5
-
6
- BASE_URL = 'http://textcaptcha.com'
7
+ BASE_URL = "http://textcaptcha.com"
7
8
 
8
9
  def initialize(api_key: nil, api_endpoint: nil, raise_errors: false)
9
- if api_endpoint
10
- self.uri = URI(api_endpoint)
11
- else
12
- self.uri = URI("#{BASE_URL}/#{api_key}.json")
13
- end
10
+ self.uri = if api_endpoint
11
+ URI(api_endpoint)
12
+ else
13
+ URI("#{BASE_URL}/#{api_key}.json")
14
+ end
14
15
  self.raise_errors = raise_errors || false
15
- rescue URI::InvalidURIError => exception
16
- raise ApiKeyError.new(api_key, exception)
16
+ rescue URI::InvalidURIError => e
17
+ raise ApiKeyError.new(api_key, e)
17
18
  end
18
19
 
19
20
  def fetch
@@ -22,35 +23,33 @@ module ActsAsTextcaptcha
22
23
 
23
24
  private
24
25
 
25
- attr_accessor :uri, :raise_errors
26
-
27
- def get
28
- response = Net::HTTP.new(uri.host, uri.port).get(uri.path)
29
- if response.code == '200'
30
- response.body
31
- else
32
- handle_error ResponseError.new(uri, "status: #{response.code}")
33
- end
34
- rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
35
- Errno::EHOSTUNREACH, EOFError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
36
- Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
37
- Net::ProtocolError => exception
38
- handle_error ResponseError.new(uri, exception)
39
- end
26
+ attr_accessor :uri, :raise_errors
40
27
 
41
- def parse(response)
42
- JSON.parse(response) unless response.empty?
43
- rescue JSON::ParserError
44
- handle_error ParseError.new(uri)
28
+ def get
29
+ response = Net::HTTP.new(uri.host, uri.port).get(uri.path)
30
+ if response.code == "200"
31
+ response.body
32
+ else
33
+ handle_error ResponseError.new(uri, "status: #{response.code}")
45
34
  end
35
+ rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
36
+ Errno::EHOSTUNREACH, EOFError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
37
+ Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
38
+ Net::ProtocolError => e
39
+ handle_error ResponseError.new(uri, e)
40
+ end
46
41
 
47
- def handle_error(error)
48
- if raise_errors
49
- raise error
50
- else
51
- Rails.logger.error("#{error.class} #{error.message}")
52
- nil
53
- end
54
- end
42
+ def parse(response)
43
+ JSON.parse(response) unless response.empty?
44
+ rescue JSON::ParserError
45
+ handle_error ParseError.new(uri)
46
+ end
47
+
48
+ def handle_error(error)
49
+ raise error if raise_errors
50
+
51
+ Rails.logger.error("#{error.class} #{error.message}")
52
+ nil
53
+ end
55
54
  end
56
55
  end
@@ -1,16 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A simple cache for storing Textcaptcha answers, Rails.cache is used as the
2
- # backend (ActiveSupport::Cache)
4
+ # backend (ActiveSupport::Cache). This must not be set as a `:null_store`.
3
5
 
4
6
  module ActsAsTextcaptcha
5
7
  class TextcaptchaCache
6
-
7
- KEY_PREFIX = 'acts_as_textcaptcha-'
8
+ KEY_PREFIX = "acts_as_textcaptcha-"
8
9
  DEFAULT_EXPIRY_MINUTES = 10
9
10
 
10
11
  def write(key, value, options = {})
11
- unless options.has_key?(:expires_in)
12
- options[:expires_in] = DEFAULT_EXPIRY_MINUTES.minutes
13
- end
12
+ options[:expires_in] = DEFAULT_EXPIRY_MINUTES.minutes unless options.has_key?(:expires_in)
14
13
  Rails.cache.write(cache_key(key), value, options)
15
14
  end
16
15
 
@@ -24,8 +23,8 @@ module ActsAsTextcaptcha
24
23
 
25
24
  private
26
25
 
27
- def cache_key(key)
28
- "#{KEY_PREFIX}#{key}"
29
- end
26
+ def cache_key(key)
27
+ "#{KEY_PREFIX}#{key}"
28
+ end
30
29
  end
31
30
  end
@@ -1,46 +1,47 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsTextcaptcha
2
4
  class TextcaptchaConfig
5
+ YAML = <<~CONFIG
6
+ development: &common_settings
7
+ api_key: 'TEXTCAPTCHA_API_IDENT' # see https://textcaptcha.com for details
8
+ # api_endpoint: nil # Optional API URL to fetch questions and answers from
9
+ # raise_errors: false # Optional flag, if true errors will be raised if the API endpoint fails
10
+ # cache_expiry_minutes: 10 # Optional minutes for captcha answers to persist in the cache (default 10 minutes)
3
11
 
4
- YAML = <<-CONFIG
5
- development: &common_settings
6
- api_key: 'TEXTCAPTCHA_API_IDENT' # see http://textcaptcha.com for details
7
- # api_endpoint: nil # Optional API URL to fetch questions and answers from
8
- # raise_errors: false # Optional flag, if true errors will be raised if the API endpoint fails
9
- # cache_expiry_minutes: 10 # Optional minutes for captcha answers to persist in the cache (default 10 minutes)
10
-
11
- questions:
12
- - question: 'Is ice hot or cold?'
13
- answers: 'cold'
14
- - question: 'what color is an orange?'
15
- answers: 'orange'
16
- - question: 'what is two plus 3?'
17
- answers: '5,five'
18
- - question: 'what is 5 times two?'
19
- answers: '10,ten'
20
- - question: 'How many colors in the list, green, brown, foot and blue?'
21
- answers: '3,three'
22
- - question: 'what is Georges name?'
23
- answers: 'george'
24
- - question: '11 minus 1?'
25
- answers: '10,ten'
26
- - question: 'is boiling water hot or cold?'
27
- answers: 'hot'
28
- - question: 'what color is my blue shirt today?'
29
- answers: 'blue'
30
- - question: 'what is 16 plus 4?'
31
- answers: '20,twenty'
12
+ questions:
13
+ - question: 'Is ice hot or cold?'
14
+ answers: 'cold'
15
+ - question: 'what color is an orange?'
16
+ answers: 'orange'
17
+ - question: 'what is two plus 3?'
18
+ answers: '5,five'
19
+ - question: 'what is 5 times two?'
20
+ answers: '10,ten'
21
+ - question: 'How many colors in the list, green, brown, foot and blue?'
22
+ answers: '3,three'
23
+ - question: 'what is Georges name?'
24
+ answers: 'george'
25
+ - question: '11 minus 1?'
26
+ answers: '10,ten'
27
+ - question: 'is boiling water hot or cold?'
28
+ answers: 'hot'
29
+ - question: 'what color is my blue shirt today?'
30
+ answers: 'blue'
31
+ - question: 'what is 16 plus 4?'
32
+ answers: '20,twenty'
32
33
 
33
- test:
34
- <<: *common_settings
35
- api_key: 'TEST_TEXTCAPTCHA_API_IDENT'
34
+ test:
35
+ <<: *common_settings
36
+ api_key: 'TEST_TEXTCAPTCHA_API_IDENT'
36
37
 
37
- production:
38
- <<: *common_settings
39
- CONFIG
38
+ production:
39
+ <<: *common_settings
40
+ CONFIG
40
41
 
41
- def self.create(path: './config/textcaptcha.yml')
42
+ def self.create(path: "./config/textcaptcha.yml")
42
43
  FileUtils.mkdir_p(File.dirname(path))
43
- File.open(path, 'w') { |f| f.write(YAML) }
44
+ File.write(path, YAML)
44
45
  end
45
46
  end
46
47
  end
@@ -1,22 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsTextcaptcha
2
4
  module TextcaptchaHelper
3
-
4
- def textcaptcha_fields(f, &block)
5
- if f.object.perform_textcaptcha? && f.object.textcaptcha_key
6
- build_textcaptcha_form_elements(f, &block)
7
- end
5
+ def textcaptcha_fields(form, &block)
6
+ build_textcaptcha_form_elements(form, &block) if form.object.perform_textcaptcha? && form.object.textcaptcha_key
8
7
  end
9
8
 
10
9
  private
11
10
 
12
- def build_textcaptcha_form_elements(f, &block)
13
- captcha_html = f.hidden_field(:textcaptcha_key)
14
- if f.object.textcaptcha_question
15
- captcha_html += capture(&block)
16
- elsif f.object.textcaptcha_answer
17
- captcha_html += f.hidden_field(:textcaptcha_answer)
18
- end
19
- captcha_html.html_safe
11
+ def build_textcaptcha_form_elements(form, &block)
12
+ captcha_html = form.hidden_field(:textcaptcha_key)
13
+ if form.object.textcaptcha_question
14
+ captcha_html += capture(&block)
15
+ elsif form.object.textcaptcha_answer
16
+ captcha_html += form.hidden_field(:textcaptcha_answer)
20
17
  end
18
+ captcha_html.html_safe
19
+ end
21
20
  end
22
21
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsTextcaptcha
2
- VERSION = '4.4.1'
4
+ VERSION = "4.6.0"
3
5
  end
@@ -1,6 +1,9 @@
1
- require 'acts_as_textcaptcha/version'
2
- require 'acts_as_textcaptcha/errors'
3
- require 'acts_as_textcaptcha/textcaptcha'
4
- require 'acts_as_textcaptcha/textcaptcha_config'
5
- require 'acts_as_textcaptcha/textcaptcha_helper'
6
- require 'acts_as_textcaptcha/framework/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)