rucaptcha 3.2.4-aarch64-linux-musl

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.
@@ -0,0 +1,14 @@
1
+ [package]
2
+ edition = "2021"
3
+ name = "rucaptcha"
4
+ version = "3.0.0"
5
+
6
+ [lib]
7
+ crate-type = ["cdylib"]
8
+
9
+ [dependencies]
10
+ image = "0.24"
11
+ imageproc = "0.23"
12
+ magnus = "0.6"
13
+ rand = "0.8.5"
14
+ rusttype = "0.9.2"
@@ -0,0 +1,4 @@
1
+ require "mkmf"
2
+ require "rb_sys/mkmf"
3
+
4
+ create_rust_makefile("rucaptcha/rucaptcha")
@@ -0,0 +1,341 @@
1
+ use image::{ImageBuffer, Rgba};
2
+ use rand::{thread_rng, Rng};
3
+ use rusttype::{Font, Scale};
4
+ use std::io::Cursor;
5
+
6
+ static BASIC_CHAR: [char; 54] = [
7
+ '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M',
8
+ 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
9
+ 'h', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
10
+ ];
11
+
12
+ static FONT_BYTES1: &[u8; 145008] = include_bytes!("../fonts/FuzzyBubbles-Regular.ttf");
13
+ static FONT_BYTES2: &[u8; 37792] = include_bytes!("../fonts/Handlee-Regular.ttf");
14
+
15
+ // https://coolors.co/cc0b8f-7c0abe-5700c8-3c2ea4-3d56a8-3fa67e-45bb30-69d003-a0d003-d8db02
16
+ static COLORS: [(u8, u8, u8, u8); 14] = [
17
+ (197, 166, 3, 255),
18
+ (187, 87, 5, 255),
19
+ (176, 7, 7, 255),
20
+ (186, 9, 56, 255),
21
+ (204, 11, 143, 255),
22
+ (124, 10, 190, 255),
23
+ (87, 0, 200, 255),
24
+ (61, 86, 168, 255),
25
+ (63, 166, 126, 255),
26
+ (69, 187, 48, 255),
27
+ (105, 208, 3, 255),
28
+ (160, 208, 3, 255),
29
+ (216, 219, 2, 255),
30
+ (50, 50, 50, 255),
31
+ ];
32
+
33
+ static SCALE_SM: u32 = 32;
34
+ static SCALE_MD: u32 = 45;
35
+ static SCALE_LG: u32 = 55;
36
+
37
+ fn rand_num(len: usize) -> usize {
38
+ let mut rng = thread_rng();
39
+ rng.gen_range(0..=len)
40
+ }
41
+
42
+ fn get_captcha(len: usize) -> Vec<String> {
43
+ let mut res = vec![];
44
+ for _ in 0..len {
45
+ let rnd = rand_num(53);
46
+ res.push(BASIC_CHAR[rnd].to_string())
47
+ }
48
+ res
49
+ }
50
+
51
+ #[allow(unused)]
52
+ fn get_color() -> Rgba<u8> {
53
+ let rnd = rand_num(COLORS.len() - 1);
54
+ let c = COLORS[rnd];
55
+ Rgba([c.0, c.1, c.2, c.3])
56
+ }
57
+
58
+ fn get_colors(num: usize) -> Vec<Rgba<u8>> {
59
+ let rnd = rand_num(COLORS.len());
60
+ let mut out = vec![];
61
+ for i in 0..num {
62
+ let c = COLORS[(rnd + i) % COLORS.len()];
63
+ out.push(Rgba([c.0, c.1, c.2, c.3]))
64
+ }
65
+
66
+ out
67
+ }
68
+
69
+ fn get_next(min: f32, max: u32) -> f32 {
70
+ min + rand_num(max as usize - min as usize) as f32
71
+ }
72
+
73
+ fn get_font() -> Font<'static> {
74
+ match rand_num(2) {
75
+ 0 => Font::try_from_bytes(FONT_BYTES1).unwrap(),
76
+ 1 => Font::try_from_bytes(FONT_BYTES2).unwrap(),
77
+ _ => Font::try_from_bytes(FONT_BYTES1).unwrap(),
78
+ }
79
+ }
80
+
81
+ fn get_image(width: usize, height: usize) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
82
+ ImageBuffer::from_fn(width as u32, height as u32, |_, _| {
83
+ image::Rgba([255, 255, 255, 255])
84
+ })
85
+ }
86
+
87
+ fn cyclic_write_character(res: &[String], image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, lines: bool) {
88
+ let c = (image.width() - 20) / res.len() as u32;
89
+ let y = image.height() / 3 - 15;
90
+
91
+ let h = image.height() as f32;
92
+
93
+ let scale = match res.len() {
94
+ 1..=3 => SCALE_LG,
95
+ 4..=5 => SCALE_MD,
96
+ _ => SCALE_SM,
97
+ } as f32;
98
+
99
+ let colors = get_colors(res.len());
100
+ let line_colors = get_colors(res.len());
101
+
102
+ let xscale = scale - rand_num((scale * 0.2) as usize) as f32;
103
+ let yscale = h as f32 - rand_num((h * 0.2) as usize) as f32;
104
+
105
+ // Draw line, ellipse first as background
106
+ for (i, _) in res.iter().enumerate() {
107
+ let line_color = line_colors[i];
108
+
109
+ if lines {
110
+ draw_interference_line(1, image, line_color);
111
+ }
112
+ draw_interference_ellipse(1, image, line_color);
113
+ }
114
+
115
+ // Draw text
116
+ for (i, _) in res.iter().enumerate() {
117
+ let text = &res[i];
118
+
119
+ let color = colors[i];
120
+ let font = get_font();
121
+
122
+ for j in 0..(rand_num(3) + 1) as i32 {
123
+ // Draw text again with offset
124
+ let offset = j * (rand_num(2) as i32);
125
+ imageproc::drawing::draw_text_mut(
126
+ image,
127
+ color,
128
+ 10 + offset + (i as u32 * c) as i32,
129
+ y as i32 as i32,
130
+ Scale {
131
+ x: xscale + offset as f32,
132
+ y: yscale as f32,
133
+ },
134
+ &font,
135
+ text,
136
+ );
137
+ }
138
+ }
139
+ }
140
+
141
+ fn draw_interference_line(num: usize, image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>, color: Rgba<u8>) {
142
+ for _ in 0..num {
143
+ let width = image.width();
144
+ let height = image.height();
145
+ let x1: f32 = 5.0;
146
+ let y1 = get_next(x1, height / 2);
147
+
148
+ let x2 = (width - 5) as f32;
149
+ let y2 = get_next(5.0, height - 5);
150
+
151
+ let ctrl_x = get_next((width / 6) as f32, width / 4 * 3);
152
+ let ctrl_y = get_next(x1, height - 5);
153
+
154
+ let ctrl_x2 = get_next((width / 12) as f32, width / 12 * 3);
155
+ let ctrl_y2 = get_next(x1, height - 5);
156
+ // Randomly draw bezier curves
157
+ imageproc::drawing::draw_cubic_bezier_curve_mut(
158
+ image,
159
+ (x1, y1),
160
+ (x2, y2),
161
+ (ctrl_x, ctrl_y),
162
+ (ctrl_x2, ctrl_y2),
163
+ color,
164
+ );
165
+ }
166
+ }
167
+
168
+ fn draw_interference_ellipse(
169
+ num: usize,
170
+ image: &mut ImageBuffer<Rgba<u8>, Vec<u8>>,
171
+ color: Rgba<u8>,
172
+ ) {
173
+ for _ in 0..num {
174
+ // max cycle width 20px
175
+ let w = (10 + rand_num(10)) as i32;
176
+ let x = rand_num((image.width() - 25) as usize) as i32;
177
+ let y = rand_num((image.height() - 15) as usize) as i32;
178
+
179
+ imageproc::drawing::draw_filled_ellipse_mut(image, (x, y), w, w, color);
180
+ }
181
+ }
182
+
183
+ pub struct Captcha {
184
+ pub text: String,
185
+ pub image: Vec<u8>,
186
+ }
187
+
188
+ pub struct CaptchaBuilder {
189
+ length: usize,
190
+ width: usize,
191
+ height: usize,
192
+ complexity: usize,
193
+ line: bool,
194
+ noise: bool,
195
+ format: image::ImageFormat,
196
+ }
197
+
198
+ impl CaptchaBuilder {
199
+ pub fn new() -> Self {
200
+ CaptchaBuilder {
201
+ length: 4,
202
+ width: 220,
203
+ height: 70,
204
+ complexity: 5,
205
+ line: true,
206
+ noise: false,
207
+ format: image::ImageFormat::Png,
208
+ }
209
+ }
210
+
211
+ pub fn length(mut self, length: usize) -> Self {
212
+ self.length = length;
213
+ self
214
+ }
215
+
216
+ pub fn line(mut self, line: bool) -> Self {
217
+ self.line = line;
218
+ self
219
+ }
220
+
221
+ pub fn noise(mut self, noise: bool) -> Self {
222
+ self.noise = noise;
223
+ self
224
+ }
225
+
226
+ pub fn format(mut self, format: &str) -> Self {
227
+ self.format = match format {
228
+ "png" => image::ImageFormat::Png,
229
+ "jpg" | "jpeg" => image::ImageFormat::Jpeg,
230
+ "webp" => image::ImageFormat::WebP,
231
+ _ => image::ImageFormat::Png,
232
+ };
233
+
234
+ self
235
+ }
236
+
237
+ pub fn complexity(mut self, complexity: usize) -> Self {
238
+ let mut complexity = complexity;
239
+ if complexity > 10 {
240
+ complexity = 10;
241
+ }
242
+ if complexity < 1 {
243
+ complexity = 1;
244
+ }
245
+ self.complexity = complexity;
246
+ self
247
+ }
248
+
249
+ pub fn build(self) -> Captcha {
250
+ // Generate an array of captcha characters
251
+ let res = get_captcha(self.length);
252
+
253
+ let text = res.join("");
254
+
255
+ // Create a white background image
256
+ let mut image = get_image(self.width, self.height);
257
+
258
+ // Loop to write the verification code string into the background image
259
+ cyclic_write_character(&res, &mut image, self.line);
260
+
261
+ if self.noise {
262
+ imageproc::noise::gaussian_noise_mut(
263
+ &mut image,
264
+ (self.complexity - 1) as f64,
265
+ ((10 * self.complexity) - 10) as f64,
266
+ ((5 * self.complexity) - 5) as u64,
267
+ );
268
+ }
269
+
270
+ let mut bytes: Vec<u8> = Vec::new();
271
+ image
272
+ .write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)
273
+ .unwrap();
274
+
275
+ Captcha { text, image: bytes }
276
+ }
277
+ }
278
+
279
+ #[cfg(test)]
280
+ mod tests {
281
+ use super::*;
282
+
283
+ #[test]
284
+ fn test_format() {
285
+ let mut builder = CaptchaBuilder::new();
286
+ assert_eq!(builder.format, image::ImageFormat::Png);
287
+
288
+ builder = builder.format("jpg");
289
+ assert_eq!(builder.format, image::ImageFormat::Jpeg);
290
+ builder = builder.format("jpeg");
291
+ assert_eq!(builder.format, image::ImageFormat::Jpeg);
292
+ builder = builder.format("webp");
293
+ assert_eq!(builder.format, image::ImageFormat::WebP);
294
+ builder = builder.format("png");
295
+ assert_eq!(builder.format, image::ImageFormat::Png);
296
+ builder = builder.format("gif");
297
+ assert_eq!(builder.format, image::ImageFormat::Png);
298
+ }
299
+
300
+ #[test]
301
+ fn test_line() {
302
+ let mut builder = CaptchaBuilder::new();
303
+ assert!(builder.line);
304
+
305
+ builder = builder.line(false);
306
+ assert!(!builder.line);
307
+ }
308
+
309
+ #[test]
310
+ fn test_noise() {
311
+ let mut builder = CaptchaBuilder::new();
312
+ assert!(!builder.noise);
313
+
314
+ builder = builder.noise(true);
315
+ assert!(builder.noise);
316
+ }
317
+
318
+ #[test]
319
+ fn test_difficulty() {
320
+ let mut builder = CaptchaBuilder::new();
321
+ assert_eq!(builder.complexity, 5);
322
+
323
+ builder = builder.complexity(10);
324
+ assert_eq!(builder.complexity, 10);
325
+
326
+ builder = builder.complexity(11);
327
+ assert_eq!(builder.complexity, 10);
328
+
329
+ builder = builder.complexity(0);
330
+ assert_eq!(builder.complexity, 1);
331
+ }
332
+
333
+ #[test]
334
+ fn test_length() {
335
+ let mut builder = CaptchaBuilder::new();
336
+ assert_eq!(builder.length, 4);
337
+
338
+ builder = builder.length(10);
339
+ assert_eq!(builder.length, 10);
340
+ }
341
+ }
@@ -0,0 +1,30 @@
1
+ use magnus::{define_class, function, Error, Object};
2
+
3
+ mod captcha;
4
+
5
+ pub fn create(
6
+ len: usize,
7
+ difficulty: usize,
8
+ line: bool,
9
+ noise: bool,
10
+ format: String,
11
+ ) -> (String, Vec<u8>) {
12
+ let c = captcha::CaptchaBuilder::new();
13
+ let out = c
14
+ .complexity(difficulty)
15
+ .length(len)
16
+ .line(line)
17
+ .noise(noise)
18
+ .format(&format)
19
+ .build();
20
+
21
+ (out.text, out.image)
22
+ }
23
+
24
+ #[magnus::init]
25
+ fn init() -> Result<(), Error> {
26
+ let class = define_class("RuCaptchaCore", magnus::class::object())?;
27
+ class.define_singleton_method("create", function!(create, 5))?;
28
+
29
+ Ok(())
30
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1,12 @@
1
+ require "fileutils"
2
+
3
+ module RuCaptcha
4
+ class << self
5
+ def cache
6
+ return @cache if defined? @cache
7
+
8
+ @cache = ActiveSupport::Cache.lookup_store(RuCaptcha.config.cache_store)
9
+ @cache
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ module RuCaptcha
2
+ class Configuration
3
+ # Store Captcha code where, this config more like Rails config.cache_store
4
+ # default: Rails application config.cache_store
5
+ attr_accessor :cache_store
6
+ # rucaptcha expire time, default 2 minutes
7
+ attr_accessor :expires_in
8
+ # Chars length: default 5, allows: [3..7]
9
+ attr_accessor :length
10
+ # Hard mode, default: 5, allows: [1..10]
11
+ attr_accessor :difficulty
12
+ # Enable or disable strikethrough lines on captcha image, default: true
13
+ attr_accessor :line
14
+ # Enable or disable noise on captcha image, default: false
15
+ attr_accessor :noise
16
+ # Image format allow: ['jpeg', 'png', 'webp'], default: 'png'
17
+ attr_accessor :format
18
+ # skip_cache_store_check, default: false
19
+ attr_accessor :skip_cache_store_check
20
+ # custom rucaptcha mount path, default: '/rucaptcha'
21
+ attr_accessor :mount_path
22
+ end
23
+ end
@@ -0,0 +1,100 @@
1
+ module RuCaptcha
2
+ module ControllerHelpers
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :verify_rucaptcha?
7
+ end
8
+
9
+ def rucaptcha_session_id
10
+ cookies[:_rucaptcha_session_id]
11
+ end
12
+
13
+ # session key of rucaptcha
14
+ def rucaptcha_sesion_key_key
15
+ warning_when_session_invalid if rucaptcha_session_id.blank?
16
+
17
+ # With https://github.com/rack/rack/commit/7fecaee81f59926b6e1913511c90650e76673b38
18
+ # to protected session_id into secret
19
+ session_id_digest = Digest::SHA256.hexdigest(rucaptcha_session_id.inspect)
20
+ ["rucaptcha-session", session_id_digest].join(":")
21
+ end
22
+
23
+ # Generate a new Captcha
24
+ def generate_rucaptcha
25
+ generate_rucaptcha_session_id
26
+
27
+ res = RuCaptcha.generate
28
+ session_val = {
29
+ code: res[0],
30
+ time: Time.now.to_i
31
+ }
32
+ RuCaptcha.cache.write(rucaptcha_sesion_key_key, session_val, expires_in: RuCaptcha.config.expires_in)
33
+ res[1]
34
+ end
35
+
36
+ # Verify captcha code
37
+ #
38
+ # params:
39
+ # resource - [optional] a ActiveModel object, if given will add validation error message to object.
40
+ # :keep_session - if true, RuCaptcha will not delete the captcha code session.
41
+ # :captcha - if given, the value of it will be used to verify the captcha,
42
+ # if do not give or blank, the value of params[:_rucaptcha] will be used to verify the captcha
43
+ #
44
+ # exmaples:
45
+ #
46
+ # verify_rucaptcha?
47
+ # verify_rucaptcha?(user, keep_session: true)
48
+ # verify_rucaptcha?(nil, keep_session: true)
49
+ # verify_rucaptcha?(nil, captcha: params[:user][:captcha])
50
+ #
51
+ def verify_rucaptcha?(_resource = nil, opts = {})
52
+ opts ||= {}
53
+
54
+ store_info = RuCaptcha.cache.read(rucaptcha_sesion_key_key)
55
+ # make sure move used key
56
+ RuCaptcha.cache.delete(rucaptcha_sesion_key_key) unless opts[:keep_session]
57
+
58
+ # Make sure session exist
59
+ return add_rucaptcha_validation_error if store_info.blank?
60
+
61
+ # Make sure not expire
62
+ return add_rucaptcha_validation_error if (Time.now.to_i - store_info[:time]) > RuCaptcha.config.expires_in
63
+
64
+ # Make sure parama have captcha
65
+ captcha = (opts[:captcha] || params[:_rucaptcha] || "").downcase.strip
66
+ return add_rucaptcha_validation_error if captcha.blank?
67
+
68
+ return add_rucaptcha_validation_error if captcha != store_info[:code]
69
+
70
+ true
71
+ end
72
+
73
+ private
74
+
75
+ def generate_rucaptcha_session_id
76
+ return if rucaptcha_session_id.present?
77
+
78
+ cookies[:_rucaptcha_session_id] = {
79
+ value: SecureRandom.hex(16),
80
+ expires: 1.day
81
+ }
82
+ end
83
+
84
+ def add_rucaptcha_validation_error
85
+ if defined?(resource) && resource && resource.respond_to?(:errors)
86
+ resource.errors.add(:base, t("rucaptcha.invalid"))
87
+ end
88
+ false
89
+ end
90
+
91
+ def warning_when_session_invalid
92
+ return unless Rails.env.development?
93
+
94
+ Rails.logger.warn "
95
+ WARNING! The session.id is blank, RuCaptcha can't work properly, please keep session available.
96
+ More details about this: https://github.com/huacnlee/rucaptcha/pull/66
97
+ "
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,15 @@
1
+ module RuCaptcha
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace RuCaptcha
4
+
5
+ initializer "rucaptcha.init" do |app|
6
+ # https://github.com/rails/rails/blob/3-2-stable/actionpack/lib/action_dispatch/routing/route_set.rb#L268
7
+ # `app.routes.prepend` start from Rails 3.2 - 5.0
8
+ app.routes.prepend do
9
+ mount RuCaptcha::Engine => RuCaptcha.config.mount_path
10
+ end
11
+
12
+ RuCaptcha.check_cache_store! unless RuCaptcha.config.skip_cache_store_check
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ module RuCaptcha
2
+ module Errors
3
+ class Configuration < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module RuCaptcha
2
+ VERSION = "3.2.4"
3
+ end
@@ -0,0 +1,22 @@
1
+ module RuCaptcha
2
+ module ViewHelpers
3
+ def rucaptcha_input_tag(opts = {})
4
+ opts[:name] = "_rucaptcha"
5
+ opts[:type] = "text"
6
+ opts[:autocorrect] = "off"
7
+ opts[:autocapitalize] = "off"
8
+ opts[:pattern] = "[a-zA-Z0-9]*"
9
+ opts[:autocomplete] = "off"
10
+ opts[:maxlength] = RuCaptcha.config.length
11
+ tag(:input, opts)
12
+ end
13
+
14
+ def rucaptcha_image_tag(opts = {})
15
+ @rucaptcha_image_tag__image_path_in_this_request ||= "#{ru_captcha.root_path}?t=#{Time.now.strftime('%s%L')}"
16
+ opts[:class] = opts[:class] || "rucaptcha-image"
17
+ opts[:src] = @rucaptcha_image_tag__image_path_in_this_request
18
+ opts[:onclick] = "this.src = '#{ru_captcha.root_path}?t=' + Date.now();"
19
+ tag(:img, opts)
20
+ end
21
+ end
22
+ end
data/lib/rucaptcha.rb ADDED
@@ -0,0 +1,87 @@
1
+ require "rails"
2
+ require "action_controller"
3
+ require "active_support/all"
4
+
5
+ begin
6
+ # load the precompiled extension file
7
+ ruby_version = /(\d+\.\d+)/.match(::RUBY_VERSION)
8
+ require_relative "rucaptcha/#{ruby_version}/rucaptcha"
9
+ rescue LoadError
10
+ require "rucaptcha/rucaptcha"
11
+ end
12
+
13
+ require "rucaptcha/version"
14
+ require "rucaptcha/configuration"
15
+ require "rucaptcha/controller_helpers"
16
+ require "rucaptcha/view_helpers"
17
+ require "rucaptcha/cache"
18
+ require "rucaptcha/engine"
19
+ require "rucaptcha/errors/configuration"
20
+
21
+ module RuCaptcha
22
+ class << self
23
+ def config
24
+ return @config if defined?(@config)
25
+
26
+ @config = Configuration.new
27
+ @config.length = 5
28
+ @config.difficulty = 3
29
+ @config.expires_in = 2.minutes
30
+ @config.skip_cache_store_check = false
31
+ @config.line = true
32
+ @config.noise = true
33
+ @config.format = "png"
34
+ @config.mount_path = "/rucaptcha"
35
+
36
+ @config.cache_store = if Rails.application
37
+ Rails.application.config.cache_store
38
+ else
39
+ :mem_cache_store
40
+ end
41
+ @config.cache_store
42
+ @config
43
+ end
44
+
45
+ def configure(&block)
46
+ config.instance_exec(&block)
47
+ end
48
+
49
+ def generate
50
+ length = config.length
51
+
52
+ raise RuCaptcha::Errors::Configuration, "length config error, value must in 3..7" unless length.in?(3..7)
53
+
54
+ result = RuCaptchaCore.create(length, config.difficulty || 5, config.line, config.noise, config.format)
55
+ [result[0].downcase, result[1].pack("c*")]
56
+ end
57
+
58
+ def check_cache_store!
59
+ cache_store = RuCaptcha.config.cache_store
60
+ store_name = cache_store.is_a?(Array) ? cache_store.first : cache_store
61
+ if %i[memory_store null_store file_store].include?(store_name)
62
+ RuCaptcha.config.cache_store = [:file_store, Rails.root.join("tmp/cache/rucaptcha/session")]
63
+
64
+ puts "
65
+
66
+ RuCaptcha's cache_store requirements are stored across processes and machines,
67
+ such as :mem_cache_store, :redis_store, or other distributed storage.
68
+ But your current set is #{cache_store}, it has changed to :file_store for working.
69
+ NOTE: :file_store is still not a good way, it only works with single server case.
70
+
71
+ Please make config file `config/initializers/rucaptcha.rb` to setup `cache_store`.
72
+ More infomation please read GitHub RuCaptcha README file.
73
+ https://github.com/huacnlee/rucaptcha
74
+
75
+ "
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ ActiveSupport.on_load(:action_controller) do
82
+ ActionController::Base.include RuCaptcha::ControllerHelpers
83
+ end
84
+
85
+ ActiveSupport.on_load(:action_view) do
86
+ include RuCaptcha::ViewHelpers
87
+ end