acts_as_textcaptcha 4.4.1 → 4.6.0

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