rucaptcha 3.2.4-aarch64-linux-musl

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