diff --git a/.gitignore b/.gitignore index ce0e20c..f80b5fd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ /spec/reports/ /test/tmp/ /test/version_tmp/ -!/tmp/ +/tmp/ .DS_Store ## Specific to RubyMotion: diff --git a/CHANGELOG.md b/CHANGELOG.md index ce7d2fe..e531895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +0.2.0 +----- + +- Added file cache, can setup how many images you want generate by `config.cache_limit`, + RuCaptcha will use cache for next requests. + When you restart Rails processes it will generate new again and clean the old caches. + 0.1.4 ----- diff --git a/Gemfile.lock b/Gemfile.lock index 2300048..a210923 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - rucaptcha (0.1.4) + rucaptcha (0.2.0) posix-spawn (>= 0.3.0) GEM diff --git a/README.md b/README.md index 9f526cf..b8beebe 100644 --- a/README.md +++ b/README.md @@ -11,17 +11,37 @@ Idea by: https://ruby-china.org/topics/20558#reply4 [中文介绍和使用说明](https://ruby-china.org/topics/27832) -### Requirements + +## Feature + +- Only need `ImageMagick`, No `RMagick`, No `mini_magick`; +- For Rails Application; +- Simple, Easy to use; +- File Caching for performance. + +## Requirements - ImageMagick -### Example +#### Ubuntu + +``` +sudo apt-get install imagemagick +``` + +#### Mac OS X + +```bash +brew install imagemagick ghostscript +``` + +## Example ![rucaptcha1](https://cloud.githubusercontent.com/assets/5518/10726119/a844dfce-7c0b-11e5-99c3-a818f3ef3dd2.png) ![rucaptcha2](https://cloud.githubusercontent.com/assets/5518/10747608/2f2f5f10-7c92-11e5-860b-914db5695a57.png) ![rucaptcha3](https://cloud.githubusercontent.com/assets/5518/10747609/2f5bbac4-7c92-11e5-8192-4aa5dfb025b7.png) ![rucaptcha4](https://cloud.githubusercontent.com/assets/5518/10747611/2f7c6a12-7c92-11e5-8730-de7295b36dd6.png) ![rucaptcha5](https://cloud.githubusercontent.com/assets/5518/10747610/2f7a9d86-7c92-11e5-911a-44596c9aeef5.png) -### Usage +## Usage Put rucaptcha in your `Gemfile`: @@ -39,6 +59,9 @@ RuCaptcha.configure do self.width = 180 # Image height, default: 48 self.height = 48 + # Cache generated images in file store, this is config files limit, default: 100 + # set 0 to disable file cache. + self.cache_limit = 100 end ``` @@ -80,7 +103,7 @@ View `app/views/account/new.html.erb` ``` -## Test skip captcha validation +### Write your test skip captcha validation ```rb describe 'sign up and login', type: :feature do @@ -92,7 +115,5 @@ describe 'sign up and login', type: :feature do end ``` -## TODO -- Use [rtesseract](https://github.com/dannnylo/rtesseract) to test OCR. diff --git a/lib/rucaptcha.rb b/lib/rucaptcha.rb index a75661a..ccb0472 100644 --- a/lib/rucaptcha.rb +++ b/lib/rucaptcha.rb @@ -5,6 +5,7 @@ require_relative 'rucaptcha/version' require_relative 'rucaptcha/configuration' require_relative 'rucaptcha/controller_helpers' require_relative 'rucaptcha/view_helpers' +require_relative 'rucaptcha/cache' require_relative 'rucaptcha/captcha' require_relative 'rucaptcha/engine' @@ -13,19 +14,24 @@ module RuCaptcha def config return @config if defined?(@config) @config = Configuration.new - @config.len = 4 - @config.width = 150 - @config.height = 48 - @config.implode = 0.4 + @config.len = 4 + @config.width = 150 + @config.height = 48 + @config.implode = 0.4 + @config.cache_limit = 100 @config end def configure(&block) config.instance_exec(&block) + + # enable cache if cache_limit less than 1 + if config.cache_limit >= 1 + RuCaptcha::Captcha.send(:include, RuCaptcha::Cache) + end end end end - -ActionController::Base.send :include, RuCaptcha::ControllerHelpers -ActionView::Base.send :include, RuCaptcha::ViewHelpers +ActionController::Base.send(:include, RuCaptcha::ControllerHelpers) +ActionView::Base.send(:include, RuCaptcha::ViewHelpers) diff --git a/lib/rucaptcha/cache.rb b/lib/rucaptcha/cache.rb new file mode 100644 index 0000000..783f7bd --- /dev/null +++ b/lib/rucaptcha/cache.rb @@ -0,0 +1,44 @@ +module RuCaptcha + # File Cache + module Cache + extend ActiveSupport::Concern + + included do + class << self + alias_method_chain :create, :cache + alias_method_chain :random_chars, :cache + end + end + + module ClassMethods + def create_with_cache(code) + cache.fetch(code) do + create_without_cache(code) + end + end + + def random_chars_with_cache + if cached_codes.length >= RuCaptcha.config.cache_limit + return cached_codes.sample + else + code = random_chars_without_cache + cached_codes << code + return code + end + end + + def cache + return @cache if defined?(@cache) + + cache_path = Rails.root.join('tmp', 'cache', 'rucaptcha') + @cache = ActiveSupport::Cache::FileStore.new(cache_path) + @cache.clear + @cache + end + + def cached_codes + @cached_codes ||= [] + end + end + end +end diff --git a/lib/rucaptcha/captcha.rb b/lib/rucaptcha/captcha.rb index b5a6497..ea4790d 100644 --- a/lib/rucaptcha/captcha.rb +++ b/lib/rucaptcha/captcha.rb @@ -2,44 +2,53 @@ require 'posix-spawn' module RuCaptcha class Captcha - def self.rand_color - r = rand(129).to_s(8).to_i - rgb = [rand(100).to_s(8), rand(100).to_s(8), rand(100).to_s(8)] + class << self + def rand_color + rgb = [rand(100).to_s(8), rand(100).to_s(8), rand(100).to_s(8)] - "rgba(#{rgb.join(',')},1)" - end - - def self.create(code) - size = "#{RuCaptcha.config.width}x#{RuCaptcha.config.height}" - font_size = (RuCaptcha.config.height * 0.8).to_i - half_width = RuCaptcha.config.width / 2 - half_height = RuCaptcha.config.height / 2 - line_color = rand_color - - chars = code.split('') - text_opts = [] - text_top = (RuCaptcha.config.height - font_size) / 2 - text_padding = 5 - text_width = (RuCaptcha.config.width / chars.size) - text_padding * 2 - text_left = 5 - chars.each_with_index do |char, i| - text_opts << %(-fill '#{rand_color}' -draw 'text #{(text_left + text_width) * i + text_left},#{text_top} "#{char}"') + "rgba(#{rgb.join(',')},1)" end - command = <<-CODE - convert -size #{size} #{text_opts.join(' ')} \ - -draw 'stroke #{line_color} line #{rand(10)},#{rand(20)} #{half_width + rand(half_width)},#{rand(half_height)}' \ - -draw 'stroke #{line_color} line #{rand(10)},#{rand(25)} #{half_width + rand(half_width)},#{half_height + rand(half_height)}' \ - -draw 'stroke #{line_color} line #{rand(10)},#{rand(30)} #{half_width + rand(half_width)},#{half_height + rand(half_height)}' \ - -wave #{rand(2) + 2}x#{rand(2) + 1} \ - -gravity NorthWest -sketch 1x10+#{rand(1)} -pointsize #{font_size} -weight 700 \ - -implode #{RuCaptcha.config.implode} label:- png:- - CODE - command.strip! - # puts command - pid, stdin, stdout, stderr = POSIX::Spawn.popen4(command) - Process.waitpid(pid) - stdout.read + def random_chars + chars = SecureRandom.hex(RuCaptcha.config.len / 2).downcase + chars.gsub!(/[0ol1]/i, (rand(8) + 2).to_s) + chars + end + + # Create Captcha image by code + def create(code) + size = "#{RuCaptcha.config.width}x#{RuCaptcha.config.height}" + font_size = (RuCaptcha.config.height * 0.8).to_i + half_width = RuCaptcha.config.width / 2 + half_height = RuCaptcha.config.height / 2 + line_color = rand_color + chars = code.split('') + text_opts = [] + text_top = (RuCaptcha.config.height - font_size) / 2 + text_padding = 5 + text_width = (RuCaptcha.config.width / chars.size) - text_padding * 2 + text_left = 5 + + chars.each_with_index do |char, i| + text_opts << %(-fill '#{rand_color}' -draw 'text #{(text_left + text_width) * i + text_left},#{text_top} "#{char}"') + end + + command = <<-CODE + convert -size #{size} #{text_opts.join(' ')} \ + -draw 'stroke #{line_color} line #{rand(10)},#{rand(20)} #{half_width + rand(half_width)},#{rand(half_height)}' \ + -draw 'stroke #{line_color} line #{rand(10)},#{rand(25)} #{half_width + rand(half_width)},#{half_height + rand(half_height)}' \ + -draw 'stroke #{line_color} line #{rand(10)},#{rand(30)} #{half_width + rand(half_width)},#{half_height + rand(half_height)}' \ + -wave #{rand(2) + 2}x#{rand(2) + 1} \ + -gravity NorthWest -sketch 1x10+#{rand(1)} -pointsize #{font_size} -weight 700 \ + -implode #{RuCaptcha.config.implode} label:- png:- + CODE + + command.strip! + # puts command + pid, stdin, stdout, stderr = POSIX::Spawn.popen4(command) + Process.waitpid(pid) + stdout.read + end end end end diff --git a/lib/rucaptcha/configuration.rb b/lib/rucaptcha/configuration.rb index 0ce026a..570ce97 100644 --- a/lib/rucaptcha/configuration.rb +++ b/lib/rucaptcha/configuration.rb @@ -1,5 +1,15 @@ module RuCaptcha class Configuration - attr_accessor :width, :height, :font_size, :len, :implode + # Image width, default 150 + attr_accessor :width + # Image height, default 48 + attr_accessor :height + # Number of chars, default 4 + attr_accessor :len + # implode, default 0.4 + attr_accessor :implode + # Number of Captcha codes limit + # set 0 to disable limit and file cache, default: 100 + attr_accessor :cache_limit end end diff --git a/lib/rucaptcha/controller_helpers.rb b/lib/rucaptcha/controller_helpers.rb index a5c68ca..ac669c2 100644 --- a/lib/rucaptcha/controller_helpers.rb +++ b/lib/rucaptcha/controller_helpers.rb @@ -7,14 +7,8 @@ module RuCaptcha end def generate_rucaptcha - session[:_rucaptcha] = random_rucaptcha_chars - return RuCaptcha::Captcha.create(session[:_rucaptcha]) - end - - def random_rucaptcha_chars - chars = SecureRandom.hex(RuCaptcha.config.len / 2).downcase - chars.gsub!(/[0ol1]/i, (rand(8) + 2).to_s) - chars + session[:_rucaptcha] = RuCaptcha::Captcha.random_chars + RuCaptcha::Captcha.create(session[:_rucaptcha]) end def verify_rucaptcha?(resource = nil) diff --git a/lib/rucaptcha/version.rb b/lib/rucaptcha/version.rb index 515cd1c..4269518 100644 --- a/lib/rucaptcha/version.rb +++ b/lib/rucaptcha/version.rb @@ -1,3 +1,3 @@ module RuCaptcha - VERSION = '0.1.4' + VERSION = '0.2.0' end diff --git a/spec/cache_spec.rb b/spec/cache_spec.rb new file mode 100644 index 0000000..4cab649 --- /dev/null +++ b/spec/cache_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe RuCaptcha::Cache do + describe '.random_chars_with_cache' do + it 'should generate max chars by config.cache_limit' do + allow(RuCaptcha.config).to receive(:cache_limit).and_return(5) + items = [] + 10.times do + items << RuCaptcha::Captcha.random_chars_with_cache + end + expect(items.uniq.length).to eq 5 + expect(RuCaptcha::Captcha.cached_codes).to eq items.uniq + end + end + + describe '.create' do + it 'should work' do + expect(RuCaptcha::Captcha).to receive(:create_without_cache).and_return('aabb') + expect(RuCaptcha::Captcha.create('abcd')).to eq('aabb') + end + end +end diff --git a/spec/captcha_spec.rb b/spec/captcha_spec.rb new file mode 100644 index 0000000..a67a9a4 --- /dev/null +++ b/spec/captcha_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe RuCaptcha::Captcha do + describe '.random_chars' do + it 'should len equal config.len' do + expect(RuCaptcha::Captcha.random_chars_without_cache.length).to eq(RuCaptcha.config.len) + end + + it 'should return 0-9 and lower str' do + expect(RuCaptcha::Captcha.random_chars_without_cache).to match(/[a-z0-9]/) + end + + it 'should not include [0ol1]' do + 10000.times do + expect(RuCaptcha::Captcha.random_chars_without_cache).not_to match(/[0ol1]/i) + end + end + end +end diff --git a/spec/controller_helpers_spec.rb b/spec/controller_helpers_spec.rb index 1a3ecc1..77beec9 100644 --- a/spec/controller_helpers_spec.rb +++ b/spec/controller_helpers_spec.rb @@ -15,28 +15,12 @@ describe RuCaptcha do describe '.generate_rucaptcha' do it 'should work' do - expect(simple).to receive(:random_rucaptcha_chars).and_return('abcd') + expect(RuCaptcha::Captcha).to receive(:random_chars).and_return('abcd') expect(simple.generate_rucaptcha).not_to be_nil expect(simple.session[:_rucaptcha]).to eq('abcd') end end - describe '.random_rucaptcha_chars' do - it 'should len equal config.len' do - expect(simple.random_rucaptcha_chars.length).to eq(RuCaptcha.config.len) - end - - it 'should return 0-9 and lower str' do - expect(simple.random_rucaptcha_chars).to match(/[a-z0-9]/) - end - - it 'should not include [0ol1]' do - 10000.times do - expect(simple.random_rucaptcha_chars).not_to match(/[0ol1]/i) - end - end - end - describe '.verify_rucaptcha?' do context 'Correct chars in params' do it 'should work' do diff --git a/spec/ocr_spec.rb b/spec/ocr_spec.rb index 6942773..efe9e38 100644 --- a/spec/ocr_spec.rb +++ b/spec/ocr_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'fileutils' describe 'OCR' do before do @@ -18,7 +19,8 @@ describe 'OCR' do end after do - `rm #{File.join(File.dirname(__FILE__), '../tmp/*.png')}` + path = File.expand_path File.join(File.dirname(__FILE__), '..', 'tmp/*.png') + FileUtils.rm_f(path) end it 'should not read by OCR lib' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8eaa0d4..86cb03b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -11,6 +11,14 @@ if !File.exists?(tmp_path) Dir.mkdir(tmp_path) end +module Rails + class << self + def root + Pathname.new(File.join(File.dirname(__FILE__), '..')) + end + end +end + RuCaptcha.configure do self.len = 2 self.width = 123