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