easy_captcha-ftbpro 0.6.4
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 +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +10 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +109 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +154 -0
- data/Rakefile +6 -0
- data/easy_captcha.gemspec +41 -0
- data/init.rb +1 -0
- data/lib/easy_captcha.rb +132 -0
- data/lib/easy_captcha/captcha.rb +26 -0
- data/lib/easy_captcha/controller.rb +22 -0
- data/lib/easy_captcha/controller_helpers.rb +93 -0
- data/lib/easy_captcha/espeak.rb +79 -0
- data/lib/easy_captcha/generator.rb +8 -0
- data/lib/easy_captcha/generator/base.rb +19 -0
- data/lib/easy_captcha/generator/default.rb +116 -0
- data/lib/easy_captcha/model_helpers.rb +29 -0
- data/lib/easy_captcha/routes.rb +10 -0
- data/lib/easy_captcha/version.rb +3 -0
- data/lib/easy_captcha/view_helpers.rb +11 -0
- data/lib/generators/easy_captcha/install_generator.rb +23 -0
- data/lib/generators/templates/easy_captcha.rb +66 -0
- data/resources/captcha.ttf +0 -0
- data/spec/easy_captcha_spec.rb +116 -0
- data/spec/espeak_spec.rb +24 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +17 -0
- metadata +159 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module EasyCaptcha
|
3
|
+
# captcha generation class
|
4
|
+
class Captcha
|
5
|
+
# code for captcha generation
|
6
|
+
attr_reader :code
|
7
|
+
# blob of generated captcha image
|
8
|
+
attr_reader :image
|
9
|
+
|
10
|
+
# generate captcha by code
|
11
|
+
def initialize code
|
12
|
+
@code = code
|
13
|
+
generate_captcha
|
14
|
+
end
|
15
|
+
|
16
|
+
def inspect #:nodoc:
|
17
|
+
"<EasyCaptcha::Captcha @code=#{code}>"
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def generate_captcha #:nodoc:
|
23
|
+
@image = EasyCaptcha.generator.generate(@code)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module EasyCaptcha
|
2
|
+
# captcha controller
|
3
|
+
class Controller < ActionController::Base
|
4
|
+
before_filter :overwrite_cache_control
|
5
|
+
# captcha action send the generated image to browser
|
6
|
+
def captcha
|
7
|
+
if params[:format] == "wav" and EasyCaptcha.espeak?
|
8
|
+
send_data generate_speech_captcha, :disposition => 'inline', :type => 'audio/wav'
|
9
|
+
else
|
10
|
+
send_data generate_captcha, :disposition => 'inline', :type => 'image/png'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
# Overwrite cache control for Samsung Galaxy S3 (remove no-store)
|
16
|
+
def overwrite_cache_control
|
17
|
+
response.headers["Cache-Control"] = "no-cache, max-age=0, must-revalidate"
|
18
|
+
response.headers["Pragma"] = "no-cache"
|
19
|
+
response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module EasyCaptcha
|
2
|
+
# helper class for ActionController
|
3
|
+
module ControllerHelpers
|
4
|
+
|
5
|
+
def self.included(base) #:nodoc:
|
6
|
+
base.class_eval do
|
7
|
+
helper_method :valid_captcha?, :captcha_valid?
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# generate captcha image and return it as blob
|
12
|
+
def generate_captcha
|
13
|
+
if EasyCaptcha.cache
|
14
|
+
# create cache dir
|
15
|
+
FileUtils.mkdir_p(EasyCaptcha.cache_temp_dir)
|
16
|
+
|
17
|
+
# select all generated captchas from cache
|
18
|
+
files = Dir.glob(EasyCaptcha.cache_temp_dir + "*.png")
|
19
|
+
|
20
|
+
unless files.size < EasyCaptcha.cache_size
|
21
|
+
file = File.open(files.at(Kernel.rand(files.size)))
|
22
|
+
session[:captcha] = File.basename(file.path, '.*')
|
23
|
+
|
24
|
+
if file.mtime < EasyCaptcha.cache_expire.ago
|
25
|
+
File.unlink(file.path)
|
26
|
+
# remove speech version
|
27
|
+
File.unlink(file.path.gsub(/png\z/, "wav")) if File.exists?(file.path.gsub(/png\z/, "wav"))
|
28
|
+
else
|
29
|
+
return file.readlines.join
|
30
|
+
end
|
31
|
+
end
|
32
|
+
generated_code = generate_captcha_code
|
33
|
+
image = Captcha.new(generated_code).image
|
34
|
+
|
35
|
+
# write captcha for caching
|
36
|
+
File.open(captcha_cache_path(generated_code), 'w') { |f| f.write image }
|
37
|
+
|
38
|
+
# write speech file if u create a new captcha image
|
39
|
+
EasyCaptcha.espeak.generate(generated_code, speech_captcha_cache_path(generated_code)) if EasyCaptcha.espeak?
|
40
|
+
|
41
|
+
# return image
|
42
|
+
image
|
43
|
+
else
|
44
|
+
Captcha.new(generate_captcha_code).image
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# generate speech by captcha from session
|
49
|
+
def generate_speech_captcha
|
50
|
+
raise RuntimeError, "espeak disabled" unless EasyCaptcha.espeak?
|
51
|
+
if EasyCaptcha.cache
|
52
|
+
File.read(speech_captcha_cache_path(current_captcha_code))
|
53
|
+
else
|
54
|
+
wav_file = Tempfile.new("#{current_captcha_code}.wav")
|
55
|
+
EasyCaptcha.espeak.generate(current_captcha_code, wav_file.path)
|
56
|
+
File.read(wav_file.path)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# return cache path of captcha image
|
61
|
+
def captcha_cache_path(code)
|
62
|
+
"#{EasyCaptcha.cache_temp_dir}/#{code}.png"
|
63
|
+
end
|
64
|
+
|
65
|
+
# return cache path of speech captcha
|
66
|
+
def speech_captcha_cache_path(code)
|
67
|
+
"#{EasyCaptcha.cache_temp_dir}/#{code}.wav"
|
68
|
+
end
|
69
|
+
|
70
|
+
# current active captcha from session
|
71
|
+
def current_captcha_code
|
72
|
+
session[:captcha]
|
73
|
+
end
|
74
|
+
|
75
|
+
# generate captcha code, save in session and return
|
76
|
+
def generate_captcha_code
|
77
|
+
session[:captcha] = EasyCaptcha.length.times.collect { EasyCaptcha.chars[rand(EasyCaptcha.chars.size)] }.join
|
78
|
+
end
|
79
|
+
|
80
|
+
# validate given captcha code and re
|
81
|
+
def captcha_valid?(code)
|
82
|
+
return false if session[:captcha].blank? or code.blank?
|
83
|
+
session[:captcha].to_s.upcase == code.to_s.upcase
|
84
|
+
end
|
85
|
+
alias_method :valid_captcha?, :captcha_valid?
|
86
|
+
|
87
|
+
# reset the captcha code in session for security after each request
|
88
|
+
def reset_last_captcha_code!
|
89
|
+
session.delete(:captcha)
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module EasyCaptcha
|
2
|
+
# espeak wrapper
|
3
|
+
class Espeak
|
4
|
+
|
5
|
+
# generator for captcha images
|
6
|
+
def initialize(&block)
|
7
|
+
defaults
|
8
|
+
yield self if block_given?
|
9
|
+
end
|
10
|
+
|
11
|
+
# set default values
|
12
|
+
def defaults
|
13
|
+
@amplitude = 80..120
|
14
|
+
@pitch = 30..70
|
15
|
+
@gap = 80
|
16
|
+
@voice = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_writer :amplitude, :pitch, :gap, :voice
|
20
|
+
|
21
|
+
# return amplitude
|
22
|
+
def amplitude
|
23
|
+
if @amplitude.is_a? Range
|
24
|
+
@amplitude.to_a.sort_by { rand }.first
|
25
|
+
else
|
26
|
+
@amplitude.to_i
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# return amplitude
|
31
|
+
def pitch
|
32
|
+
if @pitch.is_a? Range
|
33
|
+
@pitch.to_a.sort_by { rand }.first
|
34
|
+
else
|
35
|
+
@pitch.to_i
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def gap
|
40
|
+
@gap.to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
def voice
|
44
|
+
if @voice.is_a? Array
|
45
|
+
v = @voice.sort_by { rand }.first
|
46
|
+
else
|
47
|
+
v = @voice
|
48
|
+
end
|
49
|
+
|
50
|
+
v.try :gsub, /[^A-Za-z0-9\-\+]/, ""
|
51
|
+
end
|
52
|
+
|
53
|
+
# generate wav file by captcha
|
54
|
+
def generate(captcha, wav_file)
|
55
|
+
# get code
|
56
|
+
if captcha.is_a? Captcha
|
57
|
+
code = captcha.code
|
58
|
+
elsif captcha.is_a? String
|
59
|
+
code = captcha
|
60
|
+
else
|
61
|
+
raise ArgumentError, "invalid captcha"
|
62
|
+
end
|
63
|
+
|
64
|
+
# add spaces
|
65
|
+
code = code.each_char.to_a.join(" ")
|
66
|
+
|
67
|
+
cmd = "espeak -g 10"
|
68
|
+
cmd << " -a #{amplitude}" unless @amplitude.nil?
|
69
|
+
cmd << " -p #{pitch}" unless @pitch.nil?
|
70
|
+
cmd << " -g #{gap}" unless @gap.nil?
|
71
|
+
cmd << " -v '#{voice}'" unless @voice.nil?
|
72
|
+
cmd << " -w #{wav_file} '#{code}'"
|
73
|
+
|
74
|
+
%x{#{cmd}}
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module EasyCaptcha
|
2
|
+
module Generator
|
3
|
+
|
4
|
+
# base class for generators
|
5
|
+
class Base
|
6
|
+
|
7
|
+
# generator for captcha images
|
8
|
+
def initialize(&block)
|
9
|
+
defaults
|
10
|
+
yield self if block_given?
|
11
|
+
end
|
12
|
+
|
13
|
+
# default values for generator
|
14
|
+
def defaults
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module EasyCaptcha
|
3
|
+
module Generator
|
4
|
+
|
5
|
+
# default generator
|
6
|
+
class Default < Base
|
7
|
+
|
8
|
+
# set default values
|
9
|
+
def defaults
|
10
|
+
@font_size = 24
|
11
|
+
@font_fill_color = '#333333'
|
12
|
+
@font = File.expand_path('../../../../resources/captcha.ttf', __FILE__)
|
13
|
+
@font_stroke = '#000000'
|
14
|
+
@font_stroke_color = 0
|
15
|
+
@image_background_color = '#FFFFFF'
|
16
|
+
@sketch = true
|
17
|
+
@sketch_radius = 3
|
18
|
+
@sketch_sigma = 1
|
19
|
+
@wave = true
|
20
|
+
@wave_length = (60..100)
|
21
|
+
@wave_amplitude = (3..5)
|
22
|
+
@implode = 0.05
|
23
|
+
@blur = true
|
24
|
+
@blur_radius = 1
|
25
|
+
@blur_sigma = 2
|
26
|
+
end
|
27
|
+
|
28
|
+
# Font
|
29
|
+
attr_accessor :font_size, :font_fill_color, :font, :font_family, :font_stroke, :font_stroke_color
|
30
|
+
|
31
|
+
# Background
|
32
|
+
attr_accessor :image_background_color, :background_image
|
33
|
+
|
34
|
+
# Sketch
|
35
|
+
attr_accessor :sketch, :sketch_radius, :sketch_sigma
|
36
|
+
|
37
|
+
# Wave
|
38
|
+
attr_accessor :wave, :wave_length, :wave_amplitude
|
39
|
+
|
40
|
+
# Implode
|
41
|
+
attr_accessor :implode
|
42
|
+
|
43
|
+
# Gaussian Blur
|
44
|
+
attr_accessor :blur, :blur_radius, :blur_sigma
|
45
|
+
|
46
|
+
def sketch? #:nodoc:
|
47
|
+
@sketch
|
48
|
+
end
|
49
|
+
|
50
|
+
def wave? #:nodoc:
|
51
|
+
@wave
|
52
|
+
end
|
53
|
+
|
54
|
+
def blur? #:nodoc:
|
55
|
+
@blur
|
56
|
+
end
|
57
|
+
|
58
|
+
# generate image
|
59
|
+
def generate(code)
|
60
|
+
config = self
|
61
|
+
canvas = Magick::Image.new(EasyCaptcha.image_width, EasyCaptcha.image_height) do |variable|
|
62
|
+
self.background_color = config.image_background_color unless config.image_background_color.nil?
|
63
|
+
self.background_color = 'none' if config.background_image.present?
|
64
|
+
end
|
65
|
+
|
66
|
+
# Render the text in the image
|
67
|
+
canvas.annotate(Magick::Draw.new, 0, 0, 0, 0, code) {
|
68
|
+
self.gravity = Magick::CenterGravity
|
69
|
+
self.font = config.font
|
70
|
+
self.font_weight = Magick::LighterWeight
|
71
|
+
self.fill = config.font_fill_color
|
72
|
+
if config.font_stroke.to_i > 0
|
73
|
+
self.stroke = config.font_stroke_color
|
74
|
+
self.stroke_width = config.font_stroke
|
75
|
+
end
|
76
|
+
self.pointsize = config.font_size
|
77
|
+
}
|
78
|
+
|
79
|
+
# Blur
|
80
|
+
canvas = canvas.blur_image(config.blur_radius, config.blur_sigma) if config.blur?
|
81
|
+
|
82
|
+
# Wave
|
83
|
+
w = config.wave_length
|
84
|
+
a = config.wave_amplitude
|
85
|
+
canvas = canvas.wave(rand(a.last - a.first) + a.first, rand(w.last - w.first) + w.first) if config.wave?
|
86
|
+
|
87
|
+
# Sketch
|
88
|
+
canvas = canvas.sketch(config.sketch_radius, config.sketch_sigma, rand(180)) if config.sketch?
|
89
|
+
|
90
|
+
# Implode
|
91
|
+
canvas = canvas.implode(config.implode.to_f) if config.implode.is_a? Float
|
92
|
+
|
93
|
+
# Crop image because to big after waveing
|
94
|
+
canvas = canvas.crop(Magick::CenterGravity, EasyCaptcha.image_width, EasyCaptcha.image_height)
|
95
|
+
|
96
|
+
|
97
|
+
# Combine images if background image is present
|
98
|
+
if config.background_image.present?
|
99
|
+
background = Magick::Image.read(config.background_image).first
|
100
|
+
background.composite!(canvas, Magick::CenterGravity, Magick::OverCompositeOp)
|
101
|
+
|
102
|
+
image = background.to_blob { self.format = 'PNG' }
|
103
|
+
else
|
104
|
+
image = canvas.to_blob { self.format = 'PNG' }
|
105
|
+
end
|
106
|
+
|
107
|
+
# ruby-1.9
|
108
|
+
image = image.force_encoding 'UTF-8' if image.respond_to? :force_encoding
|
109
|
+
|
110
|
+
canvas.destroy!
|
111
|
+
image
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module EasyCaptcha
|
2
|
+
module ModelHelpers #:nodoc:
|
3
|
+
# helper class for ActiveRecord
|
4
|
+
def self.included(base) #:nodoc:
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods #:nodoc:
|
9
|
+
# to activate model captcha validation
|
10
|
+
def acts_as_easy_captcha
|
11
|
+
include InstanceMethods
|
12
|
+
attr_writer :captcha, :captcha_verification
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module InstanceMethods #:nodoc:
|
17
|
+
|
18
|
+
def captcha #:nodoc:
|
19
|
+
""
|
20
|
+
end
|
21
|
+
|
22
|
+
# validate captcha
|
23
|
+
def captcha_valid?
|
24
|
+
errors.add(:captcha, :invalid) if @captcha.blank? or @captcha_verification.blank? or @captcha.to_s.upcase != @captcha_verification.to_s.upcase
|
25
|
+
end
|
26
|
+
alias_method :valid_captcha?, :captcha_valid?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module EasyCaptcha
|
2
|
+
# helper class for ActionView
|
3
|
+
module ViewHelpers
|
4
|
+
# generate an image_tag for captcha image
|
5
|
+
def captcha_tag(*args)
|
6
|
+
options = { :alt => 'captcha', :width => EasyCaptcha.image_width, :height => EasyCaptcha.image_height }
|
7
|
+
options.merge! args.extract_options!
|
8
|
+
image_tag(captcha_url(:i => Time.now.to_i), options)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module EasyCaptcha
|
2
|
+
module Generators #:nodoc:
|
3
|
+
class InstallGenerator < Rails::Generators::Base #:nodoc:
|
4
|
+
source_root File.expand_path("../../templates", __FILE__)
|
5
|
+
|
6
|
+
desc "Install easy_captcha"
|
7
|
+
|
8
|
+
def copy_initializer #:nodoc:
|
9
|
+
template "easy_captcha.rb", "config/initializers/easy_captcha.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_devise_routes #:nodoc:
|
13
|
+
route 'captcha_route'
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_after_filter #:nodoc:
|
17
|
+
inject_into_class "app/controllers/application_controller.rb", ApplicationController do
|
18
|
+
" # reset captcha code after each request for security\n after_filter :reset_last_captcha_code!\n\n"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|