rails_captcha 0.0.1
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.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +40 -0
- data/app/controllers/rails_captcha/application_controller.rb +4 -0
- data/app/controllers/rails_captcha/captcha_controller.rb +17 -0
- data/app/helpers/rails_captcha/application_helper.rb +7 -0
- data/app/views/layouts/rails_captcha/application.html.erb +14 -0
- data/app/views/rails_captcha/captcha/_form.html.erb +9 -0
- data/app/views/rails_captcha/captcha/load_captcha.js.erb +16 -0
- data/config/locales/ru.yml +6 -0
- data/config/routes.rb +4 -0
- data/lib/rails_captcha/captcha/action.rb +35 -0
- data/lib/rails_captcha/captcha/cipher.rb +47 -0
- data/lib/rails_captcha/captcha/config.rb +76 -0
- data/lib/rails_captcha/captcha/generator.rb +33 -0
- data/lib/rails_captcha/captcha/image.rb +49 -0
- data/lib/rails_captcha/captcha/model.rb +58 -0
- data/lib/rails_captcha/captcha/railtie.rb +19 -0
- data/lib/rails_captcha/engine.rb +11 -0
- data/lib/rails_captcha/version.rb +3 -0
- data/lib/rails_captcha.rb +7 -0
- data/lib/tasks/rails_captcha_tasks.rake +6 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +15 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/config/application.rb +59 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/rails_captcha_test.rb +7 -0
- data/test/test_helper.rb +15 -0
- metadata +161 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'RailsCaptcha'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
24
|
+
load 'rails/tasks/engine.rake'
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
Bundler::GemHelper.install_tasks
|
29
|
+
|
30
|
+
require 'rake/testtask'
|
31
|
+
|
32
|
+
Rake::TestTask.new(:test) do |t|
|
33
|
+
t.libs << 'lib'
|
34
|
+
t.libs << 'test'
|
35
|
+
t.pattern = 'test/**/*_test.rb'
|
36
|
+
t.verbose = false
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
task :default => :test
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
class RailsCaptcha::CaptchaController < RailsCaptcha::ApplicationController
|
3
|
+
acts_as_captcha
|
4
|
+
|
5
|
+
def captcha
|
6
|
+
reset_captcha
|
7
|
+
text = session[:captcha].blank? ? "" : session[:captcha]
|
8
|
+
render :text => text
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_captcha
|
12
|
+
reset_captcha
|
13
|
+
respond_to do |format|
|
14
|
+
format.js
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>RailsCaptcha</title>
|
5
|
+
<%= stylesheet_link_tag "rails_captcha/application", :media => "all" %>
|
6
|
+
<%= javascript_include_tag "rails_captcha/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<label for="captcha">
|
2
|
+
<%= t('rails_captcha.security_code') %>
|
3
|
+
(<a id="update_captcha" href="javascript://"><%= t('rails_captcha.update') %></a>)
|
4
|
+
</label>
|
5
|
+
<% unless object.errors[:captcha].blank? %>
|
6
|
+
<span class="error"><%= object.errors[:captcha].join %></span>
|
7
|
+
<% end %>
|
8
|
+
<span class="<%= Rails.env.to_s %>" id="captcha_image_container"> </span>
|
9
|
+
<%= text_field_tag :captcha, nil, :maxlength => "6" %>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
$(document).ready(function() {
|
2
|
+
$('#captcha_image_container').html("<%= escape_javascript image_tag("/images/captchas/#{Rails.env.to_s}/#{session[:captcha]}.jpg", :id => "captcha_image", :rel => Rails.env.to_s, :alt => t('rails_captcha.security_code')) %>");
|
3
|
+
$("#update_captcha").click(function () {
|
4
|
+
var env = $('#captcha_image_container').attr('class');
|
5
|
+
$.ajax({
|
6
|
+
type: "GET",
|
7
|
+
url: '/captcha/captcha.js',
|
8
|
+
dataType: "html",
|
9
|
+
success: function(data){
|
10
|
+
$('#captcha_image_container').html('<img id="captcha_image" src="'+'/images/captchas/'+env+'/'+data+'.jpg'+'" alt="<%= t('rails_captcha.security_code') %>" />');
|
11
|
+
},
|
12
|
+
error: function(result) { alert('Captcha ERROR'); }
|
13
|
+
});
|
14
|
+
return false;
|
15
|
+
});
|
16
|
+
});
|
data/config/routes.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module RailsCaptcha
|
3
|
+
module Action
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def acts_as_captcha
|
11
|
+
unless included_modules.include? InstanceMethods
|
12
|
+
include InstanceMethods
|
13
|
+
end
|
14
|
+
before_filter :assign_captcha
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module InstanceMethods
|
19
|
+
private
|
20
|
+
|
21
|
+
def assign_captcha
|
22
|
+
unless session[:captcha] && RailsCaptcha::Config.exists?(session[:captcha])
|
23
|
+
files = RailsCaptcha::Config.captchas
|
24
|
+
session[:captcha] = File.basename(files[rand(files.length)], '.jpg')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset_captcha
|
29
|
+
session[:captcha] = nil
|
30
|
+
assign_captcha
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest/sha1'
|
5
|
+
|
6
|
+
module RailsCaptcha
|
7
|
+
class Cipher
|
8
|
+
@@key = Digest::SHA1.hexdigest(Config.options[:password])
|
9
|
+
@@iv = 'captchas'*2
|
10
|
+
|
11
|
+
def self.encrypt(text)
|
12
|
+
# Encrypt
|
13
|
+
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
14
|
+
cipher.encrypt
|
15
|
+
cipher.key = @@key
|
16
|
+
cipher.iv = @@iv
|
17
|
+
encrypted = cipher.update(text)
|
18
|
+
encrypted << cipher.final
|
19
|
+
# Turn into chr codes separated by underscores
|
20
|
+
# 135_14_163_53_43_135_172_31_1_23_169_81_49_110_49_230
|
21
|
+
if encrypted.respond_to?(:codepoints)
|
22
|
+
encrypted = encrypted.codepoints.to_a
|
23
|
+
else
|
24
|
+
encrypted = (0..encrypted.length-1).collect do |x|
|
25
|
+
encrypted[x]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
encrypted.join('_')
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.decrypt(text)
|
32
|
+
# Decode chr coded string
|
33
|
+
encrypted = text.split('_').collect do |x|
|
34
|
+
x.to_i.chr
|
35
|
+
end
|
36
|
+
encrypted = encrypted.join('')
|
37
|
+
# Decrypt
|
38
|
+
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
39
|
+
cipher.decrypt
|
40
|
+
cipher.key = @@key
|
41
|
+
cipher.iv = @@iv
|
42
|
+
decrypted = cipher.update(encrypted)
|
43
|
+
decrypted << cipher.final
|
44
|
+
decrypted.gsub(/[^a-z]/, '')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module RailsCaptcha
|
3
|
+
class Config
|
4
|
+
ONE_DAY = 24 * 60 * 60
|
5
|
+
|
6
|
+
@@options = {
|
7
|
+
:password => 'captcha',
|
8
|
+
:colors => {
|
9
|
+
:background => '#FFFFFF',
|
10
|
+
:font => '#080288'
|
11
|
+
},
|
12
|
+
# number of captcha images to generate
|
13
|
+
:count => Rails.env.production? ? 100 : 3,
|
14
|
+
:destination => "public/images/captchas/#{Rails.env.to_s}",
|
15
|
+
:dimensions => {
|
16
|
+
# canvas height (px)
|
17
|
+
:height => 32,
|
18
|
+
# canvas width (px)
|
19
|
+
:width => 110
|
20
|
+
},
|
21
|
+
:generate_every => Rails.env.production? ? ONE_DAY * 1 : ONE_DAY * 10000,
|
22
|
+
# http://www.imagemagick.org/RMagick/doc/image2.html#implode
|
23
|
+
:implode => 0.2,
|
24
|
+
:letters => {
|
25
|
+
# text baseline (px)
|
26
|
+
:baseline => 25,
|
27
|
+
# number of letters in captcha
|
28
|
+
:count => 6,
|
29
|
+
:ignore => ['a','e','i','o','u','l','j','q','v','f','t'],
|
30
|
+
# font size (pts)
|
31
|
+
:points => 36,
|
32
|
+
# width of a character (used to decrease or increase space between characters) (px)
|
33
|
+
:width => 17
|
34
|
+
},
|
35
|
+
:ttf => File.expand_path("#{File.dirname(__FILE__)}/../resources/captcha.ttf"),
|
36
|
+
# http://www.imagemagick.org/RMagick/doc/image3.html#wave
|
37
|
+
:wave => {
|
38
|
+
# range is used for randomness (px)
|
39
|
+
:wavelength => (40..60),
|
40
|
+
# distance between peak and valley of sin wave (px)
|
41
|
+
:amplitude => 2
|
42
|
+
}
|
43
|
+
}
|
44
|
+
|
45
|
+
def initialize(options={})
|
46
|
+
@@options.merge!(options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.captchas
|
50
|
+
Dir["#{@@options[:destination]}/*.jpg"]
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.codes
|
54
|
+
self.captchas.collect do |f|
|
55
|
+
File.basename f, '.jpg'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.exists?(code)
|
60
|
+
File.exists?("#{@@options[:destination]}/#{code}.jpg")
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.options
|
64
|
+
@@options
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.last_modified
|
68
|
+
file = self.captchas.first
|
69
|
+
if file && File.exists?(file)
|
70
|
+
File.mtime(file)
|
71
|
+
else
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module RailsCaptcha
|
3
|
+
class Generator
|
4
|
+
def initialize
|
5
|
+
generate
|
6
|
+
end
|
7
|
+
|
8
|
+
def generate
|
9
|
+
require 'etc'
|
10
|
+
uid = File.stat("#{Rails.root}/Gemfile").uid
|
11
|
+
owner = Etc.getpwuid(uid).name
|
12
|
+
return unless Config.options
|
13
|
+
return if Config.last_modified && Config.last_modified > Time.now - Config.options[:generate_every]
|
14
|
+
path = Rails.root.join(Config.options[:destination])
|
15
|
+
Config.captchas.each do |captcha|
|
16
|
+
FileUtils.rm_f captcha
|
17
|
+
end
|
18
|
+
FileUtils.rm_rf path
|
19
|
+
FileUtils.mkdir path
|
20
|
+
FileUtils.chown_R owner, owner, path if Rails.env.production?
|
21
|
+
(1..Config.options[:count]).each do |x|
|
22
|
+
image = Image.new Config.options
|
23
|
+
path = "#{Config.options[:destination]}/#{Cipher.encrypt(image.code)}.jpg"
|
24
|
+
next if File.exists?(path)
|
25
|
+
File.open(path, 'w') do |f|
|
26
|
+
FileUtils.chown owner, owner, path if Rails.env.production?
|
27
|
+
f << image.data.force_encoding("UTF-8")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
GC.start
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'RMagick'
|
4
|
+
|
5
|
+
module RailsCaptcha
|
6
|
+
class Image
|
7
|
+
|
8
|
+
include Magick
|
9
|
+
attr_reader :code, :data
|
10
|
+
|
11
|
+
def initialize(o)
|
12
|
+
generate_code o
|
13
|
+
|
14
|
+
canvas = Magick::Image.new(o[:dimensions][:width], o[:dimensions][:height]) {
|
15
|
+
self.background_color = o[:colors][:background]
|
16
|
+
}
|
17
|
+
|
18
|
+
text = Magick::Draw.new
|
19
|
+
text.font = File.expand_path o[:ttf]
|
20
|
+
text.pointsize = o[:letters][:points]
|
21
|
+
|
22
|
+
cur = 0
|
23
|
+
@code.each { |c|
|
24
|
+
text.annotate(canvas, 0, 0, cur, o[:letters][:baseline], c) {
|
25
|
+
self.fill = o[:colors][:font]
|
26
|
+
}
|
27
|
+
cur += o[:letters][:width]
|
28
|
+
}
|
29
|
+
|
30
|
+
w = o[:wave][:wavelength]
|
31
|
+
canvas = canvas.wave(o[:wave][:amplitude], rand(w.last - w.first) + w.first)
|
32
|
+
canvas = canvas.implode(o[:implode])
|
33
|
+
|
34
|
+
@code = @code.to_s
|
35
|
+
@data = canvas.to_blob { self.format = "JPG" }
|
36
|
+
canvas.destroy!
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def generate_code(o)
|
42
|
+
chars = ('a'..'z').to_a - o[:letters][:ignore]
|
43
|
+
@code = []
|
44
|
+
1.upto(o[:letters][:count]) do
|
45
|
+
@code << chars[rand(chars.length)]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module RailsCaptcha
|
3
|
+
module Model
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ActMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ActMethods
|
9
|
+
def acts_as_captcha(options={})
|
10
|
+
extend ClassMethods
|
11
|
+
include InstanceMethods
|
12
|
+
attr_reader :captcha, :known_captcha
|
13
|
+
cattr_accessor :captcha_options
|
14
|
+
self.captcha_options = options
|
15
|
+
validate :captcha_must_match_known_captcha
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
module InstanceMethods
|
23
|
+
def captcha=(c)
|
24
|
+
@captcha = c || ''
|
25
|
+
end
|
26
|
+
|
27
|
+
def known_captcha=(c)
|
28
|
+
@known_captcha = c || ''
|
29
|
+
end
|
30
|
+
|
31
|
+
def captcha_must_match_known_captcha
|
32
|
+
return true if self.captcha.nil? || self.known_captcha.nil?
|
33
|
+
decrypted = RailsCaptcha::Cipher.decrypt(self.known_captcha)
|
34
|
+
if self.captcha.strip.downcase != decrypted
|
35
|
+
if self.captcha_options[:base]
|
36
|
+
self.errors.add_to_base(
|
37
|
+
case self.captcha_options[:base]
|
38
|
+
when true
|
39
|
+
I18n.t("rails_captcha.enter_the_correct_captcha")
|
40
|
+
else
|
41
|
+
self.captcha_options[:base]
|
42
|
+
end
|
43
|
+
)
|
44
|
+
else
|
45
|
+
self.errors.add(:captcha,
|
46
|
+
case self.captcha_options[:field]
|
47
|
+
when true, nil
|
48
|
+
I18n.t("rails_captcha.wrong_captcha")
|
49
|
+
else
|
50
|
+
self.captcha_options[:field]
|
51
|
+
end
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|