incline 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +186 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +208 -0
- data/Rakefile +37 -0
- data/app/assets/fonts/incline/.keep +0 -0
- data/app/assets/images/incline/.keep +0 -0
- data/app/assets/images/incline/barcode-B.svg +181 -0
- data/app/assets/javascripts/incline/activate_classed_items.js +11 -0
- data/app/assets/javascripts/incline/application.js +30 -0
- data/app/assets/javascripts/incline/bootstrap-datepicker.js +1800 -0
- data/app/assets/javascripts/incline/datatables.js +22193 -0
- data/app/assets/javascripts/incline/escapeHtml.js +10 -0
- data/app/assets/javascripts/incline/inline_actions.js +479 -0
- data/app/assets/javascripts/incline/jquery.doubleScroll.js +112 -0
- data/app/assets/javascripts/incline/jquery.number.js +764 -0
- data/app/assets/javascripts/incline/regexMask.js +27 -0
- data/app/assets/javascripts/incline/select2/i18n/en.js +3 -0
- data/app/assets/javascripts/incline/select2/select2.full.js +6436 -0
- data/app/assets/stylesheets/incline/application.css +18 -0
- data/app/assets/stylesheets/incline/bootstrap-theme.min.css +5 -0
- data/app/assets/stylesheets/incline/custom.scss +279 -0
- data/app/assets/stylesheets/incline/datatables.css +494 -0
- data/app/assets/stylesheets/incline/datepicker3.css +790 -0
- data/app/assets/stylesheets/incline/select2.css +484 -0
- data/app/controllers/incline/access_groups_controller.rb +127 -0
- data/app/controllers/incline/access_test_controller.rb +30 -0
- data/app/controllers/incline/account_activations_controller.rb +28 -0
- data/app/controllers/incline/application_controller.rb +11 -0
- data/app/controllers/incline/contact_controller.rb +34 -0
- data/app/controllers/incline/password_resets_controller.rb +113 -0
- data/app/controllers/incline/security_controller.rb +100 -0
- data/app/controllers/incline/sessions_controller.rb +50 -0
- data/app/controllers/incline/users_controller.rb +304 -0
- data/app/controllers/incline/welcome_controller.rb +19 -0
- data/app/helpers/incline/.keep +0 -0
- data/app/mailers/incline/application_mailer_base.rb +11 -0
- data/app/mailers/incline/contact_form.rb +19 -0
- data/app/mailers/incline/user_mailer.rb +45 -0
- data/app/models/incline/access_group.rb +121 -0
- data/app/models/incline/access_group_group_member.rb +12 -0
- data/app/models/incline/access_group_user_member.rb +10 -0
- data/app/models/incline/action_group.rb +12 -0
- data/app/models/incline/action_security.rb +222 -0
- data/app/models/incline/contact_message.rb +37 -0
- data/app/models/incline/disable_info.rb +20 -0
- data/app/models/incline/password_reset.rb +14 -0
- data/app/models/incline/password_reset_request.rb +14 -0
- data/app/models/incline/user.rb +437 -0
- data/app/models/incline/user_login_history.rb +30 -0
- data/app/views/incline/access_groups/_details.json.jbuilder +10 -0
- data/app/views/incline/access_groups/_form.html.erb +19 -0
- data/app/views/incline/access_groups/_list.html.erb +60 -0
- data/app/views/incline/access_groups/_messages.json.jbuilder +6 -0
- data/app/views/incline/access_groups/edit.html.erb +2 -0
- data/app/views/incline/access_groups/index.html.erb +6 -0
- data/app/views/incline/access_groups/index.json.jbuilder +16 -0
- data/app/views/incline/access_groups/new.html.erb +2 -0
- data/app/views/incline/access_groups/show.html.erb +9 -0
- data/app/views/incline/access_groups/show.json.jbuilder +11 -0
- data/app/views/incline/contact/new.html.erb +22 -0
- data/app/views/incline/contact_form/contact.html.erb +16 -0
- data/app/views/incline/contact_form/contact.text.erb +13 -0
- data/app/views/incline/password_resets/edit.html.erb +16 -0
- data/app/views/incline/password_resets/new.html.erb +12 -0
- data/app/views/incline/security/_details.json.jbuilder +7 -0
- data/app/views/incline/security/_form.html.erb +20 -0
- data/app/views/incline/security/_list.html.erb +89 -0
- data/app/views/incline/security/_messages.json.jbuilder +6 -0
- data/app/views/incline/security/edit.html.erb +2 -0
- data/app/views/incline/security/index.html.erb +6 -0
- data/app/views/incline/security/index.json.jbuilder +16 -0
- data/app/views/incline/security/show.html.erb +31 -0
- data/app/views/incline/security/show.json.jbuilder +11 -0
- data/app/views/incline/sessions/new.html.erb +26 -0
- data/app/views/incline/user_mailer/account_activation.html.erb +7 -0
- data/app/views/incline/user_mailer/account_activation.text.erb +6 -0
- data/app/views/incline/user_mailer/invalid_password_reset.html.erb +3 -0
- data/app/views/incline/user_mailer/invalid_password_reset.text.erb +5 -0
- data/app/views/incline/user_mailer/password_reset.html.erb +8 -0
- data/app/views/incline/user_mailer/password_reset.text.erb +7 -0
- data/app/views/incline/users/_details.json.jbuilder +32 -0
- data/app/views/incline/users/_form.html.erb +21 -0
- data/app/views/incline/users/_list.html.erb +102 -0
- data/app/views/incline/users/_messages.json.jbuilder +6 -0
- data/app/views/incline/users/disable_confirm.html.erb +19 -0
- data/app/views/incline/users/edit.html.erb +5 -0
- data/app/views/incline/users/index.html.erb +6 -0
- data/app/views/incline/users/index.json.jbuilder +16 -0
- data/app/views/incline/users/new.html.erb +5 -0
- data/app/views/incline/users/show.html.erb +12 -0
- data/app/views/incline/users/show.json.jbuilder +11 -0
- data/app/views/incline/welcome/home.html.erb +5 -0
- data/app/views/layouts/application.html.erb +1 -0
- data/app/views/layouts/incline/_account_menu.html.erb +18 -0
- data/app/views/layouts/incline/_app_menu_anon.html.erb +1 -0
- data/app/views/layouts/incline/_app_menu_authenticated.html.erb +1 -0
- data/app/views/layouts/incline/_footer.html.erb +13 -0
- data/app/views/layouts/incline/_header.html.erb +21 -0
- data/app/views/layouts/incline/_html_mailer.html.erb +5 -0
- data/app/views/layouts/incline/_incline_app.html.erb +25 -0
- data/app/views/layouts/incline/_messages.html.erb +3 -0
- data/app/views/layouts/incline/_shim.html.erb +3 -0
- data/app/views/layouts/incline/_text_mailer.text.erb +1 -0
- data/app/views/layouts/incline/application.html.erb +1 -0
- data/app/views/layouts/mailer.html.erb +2 -0
- data/app/views/layouts/mailer.text.erb +2 -0
- data/bin/rails +12 -0
- data/bin/test_scaffold.sh +10 -0
- data/config/routes.rb +61 -0
- data/db/migrate/20170511230126_create_incline_users.rb +26 -0
- data/db/migrate/20170515003052_create_incline_access_groups.rb +10 -0
- data/db/migrate/20170515003221_create_incline_user_login_histories.rb +12 -0
- data/db/migrate/20170515150908_create_incline_access_group_user_members.rb +11 -0
- data/db/migrate/20170515151058_create_incline_access_group_group_members.rb +11 -0
- data/db/migrate/20170517193432_add_comments_to_incline_user.rb +5 -0
- data/db/migrate/20170622132700_create_incline_action_securities.rb +16 -0
- data/db/migrate/20170622172712_create_incline_action_groups.rb +11 -0
- data/db/migrate/20170622195742_add_non_standard_to_action_security.rb +5 -0
- data/db/migrate/20170622230422_add_visible_to_action_security.rb +5 -0
- data/db/seeds.rb +81 -0
- data/exe/new_incline_app +42 -0
- data/lib/generators/incline/install_generator.rb +259 -0
- data/lib/generators/incline/templates/_app_menu_anon.html.erb +1 -0
- data/lib/generators/incline/templates/_app_menu_authenticated.html.erb +1 -0
- data/lib/generators/incline/templates/incline_application.css +17 -0
- data/lib/generators/incline/templates/incline_application.html.erb +1 -0
- data/lib/generators/incline/templates/incline_application.js +12 -0
- data/lib/generators/incline/templates/incline_database.yml +25 -0
- data/lib/generators/incline/templates/incline_email.yml +20 -0
- data/lib/generators/incline/templates/incline_mailer.html.erb +2 -0
- data/lib/generators/incline/templates/incline_mailer.text.erb +2 -0
- data/lib/generators/incline/templates/incline_users.yml +64 -0
- data/lib/generators/incline/templates/incline_version.rb +3 -0
- data/lib/incline/auth_engine_base.rb +52 -0
- data/lib/incline/data_tables_request.rb +336 -0
- data/lib/incline/date_time_formats.rb +6 -0
- data/lib/incline/engine.rb +212 -0
- data/lib/incline/errors.rb +15 -0
- data/lib/incline/extensions/action_controller_base.rb +526 -0
- data/lib/incline/extensions/action_mailer_base.rb +66 -0
- data/lib/incline/extensions/action_view_base.rb +489 -0
- data/lib/incline/extensions/active_record_base.rb +308 -0
- data/lib/incline/extensions/application.rb +137 -0
- data/lib/incline/extensions/application_configuration.rb +50 -0
- data/lib/incline/extensions/connection_adapter.rb +55 -0
- data/lib/incline/extensions/date_time_value.rb +123 -0
- data/lib/incline/extensions/date_value.rb +77 -0
- data/lib/incline/extensions/decimal_value.rb +55 -0
- data/lib/incline/extensions/erb_scaffold_generator.rb +31 -0
- data/lib/incline/extensions/float_value.rb +59 -0
- data/lib/incline/extensions/form_builder.rb +617 -0
- data/lib/incline/extensions/integer_value.rb +54 -0
- data/lib/incline/extensions/jbuilder_generator.rb +38 -0
- data/lib/incline/extensions/jbuilder_template.rb +39 -0
- data/lib/incline/extensions/main_app.rb +40 -0
- data/lib/incline/extensions/numeric.rb +63 -0
- data/lib/incline/extensions/object.rb +31 -0
- data/lib/incline/extensions/resource_route_generator.rb +53 -0
- data/lib/incline/extensions/session.rb +113 -0
- data/lib/incline/extensions/string.rb +50 -0
- data/lib/incline/extensions/test_case.rb +764 -0
- data/lib/incline/extensions/time_zone_converter.rb +40 -0
- data/lib/incline/global_status.rb +236 -0
- data/lib/incline/helpers/route_hash_formatter.rb +46 -0
- data/lib/incline/json_log_formatter.rb +96 -0
- data/lib/incline/json_logger.rb +17 -0
- data/lib/incline/log.rb +153 -0
- data/lib/incline/number_formats.rb +17 -0
- data/lib/incline/recaptcha.rb +346 -0
- data/lib/incline/user_manager.rb +212 -0
- data/lib/incline/validators/email_validator.rb +45 -0
- data/lib/incline/validators/ip_address_validator.rb +32 -0
- data/lib/incline/validators/recaptcha_validator.rb +37 -0
- data/lib/incline/validators/safe_name_validator.rb +31 -0
- data/lib/incline/version.rb +3 -0
- data/lib/incline/work_path.rb +75 -0
- data/lib/incline.rb +197 -0
- data/lib/tasks/incline_tasks.rake +4 -0
- data/lib/templates/erb/scaffold/_form.html.erb +43 -0
- data/lib/templates/erb/scaffold/_list.html.erb +81 -0
- data/lib/templates/erb/scaffold/edit.html.erb +1 -0
- data/lib/templates/erb/scaffold/index.html.erb +6 -0
- data/lib/templates/erb/scaffold/new.html.erb +1 -0
- data/lib/templates/erb/scaffold/show.html.erb +34 -0
- data/lib/templates/jbuilder/scaffold/_details.json.jbuilder +20 -0
- data/lib/templates/jbuilder/scaffold/index.json.jbuilder +16 -0
- data/lib/templates/jbuilder/scaffold/show.json.jbuilder +16 -0
- data/lib/templates/rails/scaffold_controller/controller.rb +128 -0
- data/test/controllers/incline/access_groups_controller_test.rb +65 -0
- data/test/controllers/incline/access_test_controller_test.rb +53 -0
- data/test/controllers/incline/contact_controller_test.rb +32 -0
- data/test/controllers/incline/security_controller_test.rb +39 -0
- data/test/controllers/incline/welcome_controller_test.rb +16 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/images/.keep +0 -0
- data/test/dummy/app/assets/javascripts/application.js +12 -0
- data/test/dummy/app/assets/stylesheets/application.css +17 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/controllers/concerns/.keep +0 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/mailers/.keep +0 -0
- data/test/dummy/app/models/.keep +0 -0
- data/test/dummy/app/models/concerns/.keep +0 -0
- data/test/dummy/app/views/layouts/application.html.erb +1 -0
- data/test/dummy/app/views/layouts/incline/_app_menu_anon.html.erb +1 -0
- data/test/dummy/app/views/layouts/incline/_app_menu_authenticated.html.erb +1 -0
- data/test/dummy/app/views/layouts/mailer.html.erb +2 -0
- data/test/dummy/app/views/layouts/mailer.text.erb +2 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +29 -0
- data/test/dummy/config/application.rb +38 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +34 -0
- data/test/dummy/config/email.yml +24 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +45 -0
- data/test/dummy/config/environments/production.rb +85 -0
- data/test/dummy/config/environments/test.rb +44 -0
- data/test/dummy/config/initializers/assets.rb +11 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/to_time_preserves_timezone.rb +10 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +6 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/db/schema.rb +108 -0
- data/test/dummy/lib/assets/.keep +0 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/public/404.html +67 -0
- data/test/dummy/public/422.html +67 -0
- data/test/dummy/public/500.html +66 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/extensions/action_controller_base_extensions_test.rb +21 -0
- data/test/extensions/action_mailer_base_extensions_test.rb +20 -0
- data/test/extensions/action_view_base_extensions_test.rb +267 -0
- data/test/extensions/active_record_extensions_test.rb +173 -0
- data/test/extensions/application_configuration_extensions_test.rb +46 -0
- data/test/extensions/application_extensions_test.rb +23 -0
- data/test/extensions/connection_adapter_extensions_test.rb +54 -0
- data/test/extensions/date_time_value_extensions_test.rb +104 -0
- data/test/extensions/date_value_extensions_test.rb +102 -0
- data/test/extensions/decimal_value_extensions_test.rb +85 -0
- data/test/extensions/erb_scaffold_generator_extensions_test.rb +17 -0
- data/test/extensions/float_value_extensions_test.rb +78 -0
- data/test/extensions/form_builder_extensions_test.rb +28 -0
- data/test/extensions/integer_value_extensions_test.rb +78 -0
- data/test/extensions/jbuilder_generator_extensions_test.rb +21 -0
- data/test/extensions/jbuilder_template_extensions_test.rb +47 -0
- data/test/extensions/main_app_extensions_test.rb +55 -0
- data/test/extensions/numeric_extensions_test.rb +76 -0
- data/test/extensions/object_extensions_test.rb +104 -0
- data/test/extensions/session_extensions_test.rb +69 -0
- data/test/extensions/string_extensions_test.rb +32 -0
- data/test/extensions/test_case_extensions_test.rb +538 -0
- data/test/extensions/time_zone_converter_extensions_test.rb +10 -0
- data/test/fixtures/incline/access_group_group_members.yml +1 -0
- data/test/fixtures/incline/access_group_user_members.yml +1 -0
- data/test/fixtures/incline/access_groups.yml +13 -0
- data/test/fixtures/incline/action_groups.yml +6 -0
- data/test/fixtures/incline/action_securities.yml +18 -0
- data/test/fixtures/incline/user_login_histories.yml +1 -0
- data/test/fixtures/incline/users.yml +64 -0
- data/test/incline_test.rb +63 -0
- data/test/integration/incline/users_edit_test.rb +180 -0
- data/test/integration/incline/users_login_test.rb +105 -0
- data/test/integration/incline/users_signup_test.rb +147 -0
- data/test/integration/navigation_test.rb +11 -0
- data/test/lib/data_tables_request_test.rb +245 -0
- data/test/lib/date_time_formats_test.rb +111 -0
- data/test/lib/global_status_test.rb +89 -0
- data/test/lib/json_log_formatter_test.rb +43 -0
- data/test/lib/log_test.rb +36 -0
- data/test/lib/recaptcha_test.rb +75 -0
- data/test/lib/user_manager_test.rb +47 -0
- data/test/lib/work_path_test.rb +18 -0
- data/test/models/incline/access_group_group_member_test.rb +30 -0
- data/test/models/incline/access_group_test.rb +60 -0
- data/test/models/incline/access_group_user_member_test.rb +29 -0
- data/test/models/incline/action_group_test.rb +27 -0
- data/test/models/incline/action_security_test.rb +176 -0
- data/test/models/incline/contact_message_test.rb +66 -0
- data/test/models/incline/disable_info_test.rb +29 -0
- data/test/models/incline/password_reset_request_test.rb +35 -0
- data/test/models/incline/password_reset_test.rb +51 -0
- data/test/models/incline/user_login_history_test.rb +31 -0
- data/test/models/incline/user_test.rb +91 -0
- data/test/test_helper.rb +42 -0
- data/test/validators/email_validator_test.rb +102 -0
- data/test/validators/ip_address_validator_test.rb +107 -0
- data/test/validators/recaptcha_validator_test.rb +57 -0
- data/test/validators/safe_name_validator_test.rb +101 -0
- metadata +584 -0
@@ -0,0 +1,346 @@
|
|
1
|
+
require 'cgi/util'
|
2
|
+
require 'net/http'
|
3
|
+
require 'action_view'
|
4
|
+
|
5
|
+
module Incline
|
6
|
+
##
|
7
|
+
# A helper class for reCAPTCHA.
|
8
|
+
#
|
9
|
+
# To use reCAPTCHA, you will need to define +recaptcha_public+ and +recaptcha_private+ in your 'config/secrets.yml'.
|
10
|
+
# If you need to use a proxy server, you will need to configure the proxy settings as well.
|
11
|
+
#
|
12
|
+
# # config/secrets.yml
|
13
|
+
# default: &default
|
14
|
+
# recaptcha_public: SomeBase64StringFromGoogle
|
15
|
+
# recaptcha_private: AnotherBase64StringFromGoogle
|
16
|
+
# recaptcha_proxy:
|
17
|
+
# host: 10.10.10.10
|
18
|
+
# port: 1000
|
19
|
+
# user: username
|
20
|
+
# password: top_secret
|
21
|
+
#
|
22
|
+
class Recaptcha
|
23
|
+
|
24
|
+
##
|
25
|
+
# Defines a reCAPTCHA tag that can be used to supply a field in a model with a hash of values.
|
26
|
+
#
|
27
|
+
# Basically we define two fields for the model attribute, one for :remote_ip and one for :response.
|
28
|
+
# The :remote_ip field is set automatically and shouldn't be changed.
|
29
|
+
# The :response field is set when the user completes the challenge.
|
30
|
+
#
|
31
|
+
# Incline::Recaptcha::Tag.new(my_model, :is_robot).render
|
32
|
+
#
|
33
|
+
# <input type="hidden" name="my_model[is_robot][remote_ip]" id="my_model_is_robot_remote_ip" value="10.11.12.13">
|
34
|
+
# <input type="hidden" name="my_model[is_robot][response]" id="my_model_is_robot_response" value="">
|
35
|
+
#
|
36
|
+
# Incline::Recaptcha::verify model: my_model, attribute: :is_robot
|
37
|
+
class Tag < ActionView::Helpers::Tags::Base
|
38
|
+
|
39
|
+
##
|
40
|
+
# Generates the reCAPTCHA data.
|
41
|
+
def render
|
42
|
+
remote_ip =
|
43
|
+
if @template_object&.respond_to?(:request) && @template_object.send(:request)&.respond_to?(:remote_ip)
|
44
|
+
@template_object.request.remote_ip
|
45
|
+
else
|
46
|
+
ENV['REMOTE_ADDR']
|
47
|
+
end
|
48
|
+
|
49
|
+
if Incline::Recaptcha::disabled?
|
50
|
+
# very simple, if recaptcha is disabled, send the IP and 'disabled' to the form.
|
51
|
+
# for validation, recaptcha must still be disabled or it will fail.
|
52
|
+
return tag('input', type: 'hidden', id: tag_id, name: tag_name, value: "#{remote_ip}|disabled")
|
53
|
+
end
|
54
|
+
|
55
|
+
# reCAPTCHA is not disabled, so put everything we need into the form.
|
56
|
+
ret = tag('input', type: 'hidden', id: tag_id, name: tag_name, value: remote_ip)
|
57
|
+
ret += "\n"
|
58
|
+
|
59
|
+
div_id = tag_id + '_div'
|
60
|
+
|
61
|
+
ret += tag('div', { class: 'form-group' }, true)
|
62
|
+
ret += tag('div', { id: div_id }, true)
|
63
|
+
ret += "</div></div>\n".html_safe
|
64
|
+
|
65
|
+
sitekey = CGI::escape_html(Incline::Recaptcha::public_key)
|
66
|
+
onload = 'onload_' + tag_id
|
67
|
+
callback = 'update_' + tag_id
|
68
|
+
tabindex = @options[:tab_index].to_s.to_i
|
69
|
+
theme = make_valid(@options[:theme], VALID_THEMES, :light).to_s
|
70
|
+
type = make_valid(@options[:type], VALID_TYPES, :image).to_s
|
71
|
+
size = make_valid(@options[:size], VALID_SIZES, :normal).to_s
|
72
|
+
|
73
|
+
|
74
|
+
ret += <<-EOS.html_safe
|
75
|
+
<script type="text/javascript">
|
76
|
+
// <![CDATA[
|
77
|
+
function #{onload}() {
|
78
|
+
grecaptcha.render(#{div_id.inspect}, {
|
79
|
+
"sitekey" : #{sitekey.inspect},
|
80
|
+
"callback" : #{callback.inspect},
|
81
|
+
"tabindex" : #{tabindex},
|
82
|
+
"theme" : #{theme.inspect},
|
83
|
+
"type" : #{type.inspect},
|
84
|
+
"size" : #{size.inspect}
|
85
|
+
});
|
86
|
+
}
|
87
|
+
function #{callback}(response) {
|
88
|
+
var fld = $('##{tag_id}');
|
89
|
+
var val = fld.val();
|
90
|
+
if (val) { val = val.split('|'); val = val[0]; } else { val = ''; }
|
91
|
+
fld.val(val + '|' + response);
|
92
|
+
}
|
93
|
+
// ]]>
|
94
|
+
</script>
|
95
|
+
EOS
|
96
|
+
|
97
|
+
Incline::Recaptcha::onload_callbacks << onload
|
98
|
+
|
99
|
+
ret.html_safe
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def make_valid(value, valid, default)
|
105
|
+
return default if value.blank?
|
106
|
+
value = value.to_sym
|
107
|
+
return default unless valid.include?(value)
|
108
|
+
value
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
##
|
114
|
+
# Gets the valid themes for the reCAPTCHA field.
|
115
|
+
VALID_THEMES = [ :dark, :light ]
|
116
|
+
|
117
|
+
##
|
118
|
+
# Gets the valid types for the reCAPTCHA field.
|
119
|
+
VALID_TYPES = [ :audio, :image ]
|
120
|
+
|
121
|
+
##
|
122
|
+
# Gets the valid sizes for the reCAPTCHA field.
|
123
|
+
VALID_SIZES = [ :compact, :normal ]
|
124
|
+
|
125
|
+
##
|
126
|
+
# A string that will validated when reCAPTCHA is disabled.
|
127
|
+
DISABLED = '0.0.0.0|disabled'
|
128
|
+
|
129
|
+
##
|
130
|
+
# Determines if recaptcha is disabled either due to a test environment or because :recaptcha_public or :recaptcha_private is not defined in +secrets.yml+.
|
131
|
+
def self.disabled?
|
132
|
+
temp_lock || public_key.blank? || private_key.blank? || (Rails.env.test? && !enabled_for_testing?)
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Gets the public key.
|
137
|
+
def self.public_key
|
138
|
+
@public_key ||= Rails.application.secrets[:recaptcha_public].to_s.strip
|
139
|
+
end
|
140
|
+
|
141
|
+
##
|
142
|
+
# Gets the private key.
|
143
|
+
def self.private_key
|
144
|
+
@private_key ||= Rails.application.secrets[:recaptcha_private].to_s.strip
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
# Gets the proxy configuration (if any).
|
149
|
+
def self.proxy
|
150
|
+
@proxy ||= (Rails.application.secrets[:recaptcha_proxy] || {}).symbolize_keys
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
##
|
155
|
+
# Generates the bare minimum code needed to include a reCAPTCHA challenge in a form.
|
156
|
+
def self.add
|
157
|
+
unless disabled?
|
158
|
+
"<div class=\"g-recaptcha\" data-sitekey=\"#{CGI::escape_html(public_key)}\"></div>\n<script src=\"https://www.google.com/recaptcha/api.js\"></script><br>".html_safe
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
##
|
163
|
+
# Verifies the response from a reCAPTCHA challenge.
|
164
|
+
#
|
165
|
+
# Valid options:
|
166
|
+
# model::
|
167
|
+
# Sets the model that this challenge is verifying.
|
168
|
+
# attribute::
|
169
|
+
# If a model is provided, you can supply an attribute to retrieve the response data from.
|
170
|
+
# This attribute should return a hash with :response and :remote_ip keys.
|
171
|
+
# If this is provided, then the remaining options are ignored.
|
172
|
+
# response::
|
173
|
+
# If specified, defines the response from the reCAPTCHA challenge that we want to verify.
|
174
|
+
# If not specified, then the request parameters (if any) are searched for the "g-recaptcha-response" value.
|
175
|
+
# remote_ip::
|
176
|
+
# If specified, defines the remote IP of the user that was challenged.
|
177
|
+
# If not specified, then the remote IP from the request (if any) is used.
|
178
|
+
# request::
|
179
|
+
# Specifies the request to use for information.
|
180
|
+
# This must be provided unless :response and :remote_ip are both specified.
|
181
|
+
# This is the default option if an object other than a Hash is provided to #verify.
|
182
|
+
#
|
183
|
+
# Returns true on success, or false on failure.
|
184
|
+
#
|
185
|
+
def self.verify(options = {})
|
186
|
+
return true if temp_lock
|
187
|
+
|
188
|
+
options = { request: options } unless options.is_a?(::Hash)
|
189
|
+
|
190
|
+
model = options[:model]
|
191
|
+
|
192
|
+
response =
|
193
|
+
if model && options[:attribute] && model.respond_to?(options[:attribute])
|
194
|
+
model.send(options[:attribute])
|
195
|
+
else
|
196
|
+
nil
|
197
|
+
end
|
198
|
+
|
199
|
+
remote_ip = nil
|
200
|
+
|
201
|
+
if response.is_a?(::Hash)
|
202
|
+
remote_ip = response[:remote_ip]
|
203
|
+
response = response[:response]
|
204
|
+
end
|
205
|
+
|
206
|
+
# model must respond to the 'errors' message and the result of that must respond to 'add'
|
207
|
+
if !model || !model.respond_to?('errors') || !model.send('errors').respond_to?('add')
|
208
|
+
model = nil
|
209
|
+
end
|
210
|
+
|
211
|
+
response ||= options[:response]
|
212
|
+
remote_ip ||= options[:remote_ip]
|
213
|
+
|
214
|
+
if response.blank? || remote_ip.blank?
|
215
|
+
request = options[:request]
|
216
|
+
raise ArgumentError, 'Either :request must be specified or both :response and :remote_ip must be specified.' unless request
|
217
|
+
response = request.params['g-recaptcha-response']
|
218
|
+
remote_ip = request.respond_to?(:remote_ip) ? request.send(:remote_ip) : ENV['REMOTE_ADDR']
|
219
|
+
end
|
220
|
+
|
221
|
+
if disabled?
|
222
|
+
# In tests or environments where reCAPTCHA is disabled,
|
223
|
+
# the response should be 'disabled' to verify successfully.
|
224
|
+
return response == 'disabled'
|
225
|
+
else
|
226
|
+
begin
|
227
|
+
if proxy.blank?
|
228
|
+
http = Net::HTTP
|
229
|
+
else
|
230
|
+
http = Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password)
|
231
|
+
end
|
232
|
+
|
233
|
+
verify_hash = {
|
234
|
+
secret: private_key,
|
235
|
+
remoteip: remote_ip,
|
236
|
+
response: response
|
237
|
+
}
|
238
|
+
recaptcha = nil
|
239
|
+
Timeout::timeout(5) do
|
240
|
+
uri = URI.parse('https://www.google.com/recaptcha/api/siteverify')
|
241
|
+
http_instance = http.new(uri.host, uri.port)
|
242
|
+
if uri.port == 443
|
243
|
+
http_instance.use_ssl = true
|
244
|
+
end
|
245
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
246
|
+
request.set_form_data(verify_hash)
|
247
|
+
recaptcha = http_instance.request(request)
|
248
|
+
end
|
249
|
+
answer = JSON.parse(recaptcha.body)
|
250
|
+
|
251
|
+
unless answer['success'].to_s.downcase == 'true'
|
252
|
+
if model
|
253
|
+
model.errors.add(options[:attribute] || :base, 'Recaptcha verification failed.')
|
254
|
+
end
|
255
|
+
return false
|
256
|
+
end
|
257
|
+
|
258
|
+
return true
|
259
|
+
rescue Timeout::Error
|
260
|
+
if model
|
261
|
+
model.errors.add(:base, 'Recaptcha unreachable.')
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
false
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
|
271
|
+
##
|
272
|
+
# Contains a collection of onload callbacks for explicit reCAPTCHA fields.
|
273
|
+
#
|
274
|
+
# Used by the Incline::Recaptcha::Tag helper.
|
275
|
+
def self.onload_callbacks
|
276
|
+
# FIXME: Should probably move this to the session.
|
277
|
+
@onload_callbacks ||= []
|
278
|
+
end
|
279
|
+
|
280
|
+
##
|
281
|
+
# Generates a script block to load reCAPTCHA and activate any reCAPTCHA fields.
|
282
|
+
def self.script_block
|
283
|
+
if onload_callbacks.any?
|
284
|
+
ret = "<script type=\"text/javascript\">\n// <![CDATA[\nfunction recaptcha_onload() { "
|
285
|
+
onload_callbacks.each { |onload| ret += CGI::escape_html(onload) + '(); ' }
|
286
|
+
ret += "}\n// ]]>\n</script>\n<script type=\"text/javascript\" src=\"https://www.google.com/recaptcha/api.js?onload=recaptcha_onload&render=explicit\" async defer></script>"
|
287
|
+
|
288
|
+
# clear the cache.
|
289
|
+
onload_callbacks.clear
|
290
|
+
|
291
|
+
ret.html_safe
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
##
|
296
|
+
# Pauses reCAPTCHA validation for the specified block of code.
|
297
|
+
def self.pause_for(&block)
|
298
|
+
# already paused, so just call the block.
|
299
|
+
return block.call if paused?
|
300
|
+
|
301
|
+
# otherwise pause and then call the block.
|
302
|
+
self.temp_lock = true
|
303
|
+
begin
|
304
|
+
return block.call
|
305
|
+
ensure
|
306
|
+
self.temp_lock = false
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
##
|
311
|
+
# Determines if reCAPTCHA validation is currently paused.
|
312
|
+
def self.paused?
|
313
|
+
temp_lock
|
314
|
+
end
|
315
|
+
|
316
|
+
private
|
317
|
+
|
318
|
+
def self.enable_for_testing(pub_key = nil, priv_key = nil)
|
319
|
+
raise 'This method is only valid when testing.' unless Rails.env.test?
|
320
|
+
|
321
|
+
@enabled_for_testing = true
|
322
|
+
@public_key = pub_key
|
323
|
+
@private_key = priv_key
|
324
|
+
begin
|
325
|
+
yield if block_given?
|
326
|
+
ensure
|
327
|
+
@private_key = nil
|
328
|
+
@public_key = nil
|
329
|
+
@enabled_for_testing = false
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
def self.enabled_for_testing?
|
334
|
+
@enabled_for_testing ||= false
|
335
|
+
end
|
336
|
+
|
337
|
+
def self.temp_lock
|
338
|
+
@temp_lock ||= false
|
339
|
+
end
|
340
|
+
|
341
|
+
def self.temp_lock=(bool)
|
342
|
+
@temp_lock = bool
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|
346
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
|
2
|
+
module Incline
|
3
|
+
##
|
4
|
+
# Handles the user management tasks between an authentication system and the database.
|
5
|
+
#
|
6
|
+
# The default authentication system is the database, but other systems are supported.
|
7
|
+
# Out of the box we support LDAP, but the class can be extended to add other functionality.
|
8
|
+
#
|
9
|
+
class UserManager < AuthEngineBase
|
10
|
+
|
11
|
+
##
|
12
|
+
# Creates a new user manager.
|
13
|
+
#
|
14
|
+
# The user manager itself takes no options, however options will be passed to
|
15
|
+
# any registered authentication engines when they are instantiated.
|
16
|
+
#
|
17
|
+
# The options can be used to pre-register engines and provide configuration for them.
|
18
|
+
# The engines will have specific configurations, but the UserManager class recognizes
|
19
|
+
# the 'engines' key.
|
20
|
+
#
|
21
|
+
# {
|
22
|
+
# :engines => {
|
23
|
+
# 'example.com' => {
|
24
|
+
# :engine => MySuperAuthEngine.new(...)
|
25
|
+
# },
|
26
|
+
# 'example.org' => {
|
27
|
+
# :engine => 'incline_ldap/auth_engine',
|
28
|
+
# :config => {
|
29
|
+
# :host => 'ldap.example.org',
|
30
|
+
# :port => 636,
|
31
|
+
# :base_dn => 'DC=ldap,DC=example,DC=org'
|
32
|
+
# }
|
33
|
+
# }
|
34
|
+
# }
|
35
|
+
# }
|
36
|
+
#
|
37
|
+
# When an 'engines' key is processed, the configuration options for the engines are pulled
|
38
|
+
# from the subkeys. Once the processing of the 'engines' key is complete, it will be removed
|
39
|
+
# from the options hash so any engines registered in the future will not receive the extra options.
|
40
|
+
def initialize(options = {})
|
41
|
+
@options = (options || {}).deep_symbolize_keys
|
42
|
+
Incline::User.ensure_admin_exists!
|
43
|
+
|
44
|
+
if @options[:engines].is_a?(::Hash)
|
45
|
+
@options[:engines].each do |domain_name, domain_config|
|
46
|
+
if domain_config[:engine].blank?
|
47
|
+
::Incline::Log::info "Domain #{domain_name} is missing an engine definition and will not be registered."
|
48
|
+
elsif domain_config[:engine].is_a?(::Incline::AuthEngineBase)
|
49
|
+
::Incline::Log::info "Using supplied auth engine for #{domain_name}."
|
50
|
+
register_auth_engine domain_config[:engine], domain_name
|
51
|
+
else
|
52
|
+
engine =
|
53
|
+
begin
|
54
|
+
domain_config[:engine].to_s.classify.constantize
|
55
|
+
rescue NameError
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
if engine
|
60
|
+
engine = engine.new(domain_config[:config] || {})
|
61
|
+
if engine.is_a?(::Incline::AuthEngineBase)
|
62
|
+
::Incline::Log::info "Using newly created auth engine for #{domain_name}."
|
63
|
+
register_auth_engine engine, domain_name
|
64
|
+
else
|
65
|
+
::Incline::Log::warn "Object created for #{domain_name} does not inherit from Incline::AuthEngineBase."
|
66
|
+
end
|
67
|
+
else
|
68
|
+
::Incline::Log::warn "Failed to create auth engine for #{domain_name}."
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
@options.delete(:engines)
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# Attempts to authenticate the user and returns the model on success.
|
80
|
+
def authenticate(email, password, client_ip)
|
81
|
+
return nil unless Incline::EmailValidator.valid?(email)
|
82
|
+
email = email.downcase
|
83
|
+
|
84
|
+
# If an engine is registered for the email domain, then use it.
|
85
|
+
engine = get_auth_engine(email)
|
86
|
+
if engine
|
87
|
+
return engine.authenticate(email, password, client_ip)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Otherwise we will be using the database.
|
91
|
+
user = User.find_by(email: email)
|
92
|
+
if user
|
93
|
+
# user must be enabled and the password must match.
|
94
|
+
unless user.enabled?
|
95
|
+
add_failure_to user, '(DB) account disabled', client_ip
|
96
|
+
return nil
|
97
|
+
end
|
98
|
+
if user.authenticate(password)
|
99
|
+
add_success_to user, '(DB)', client_ip
|
100
|
+
return user
|
101
|
+
else
|
102
|
+
add_failure_to user, '(DB) invalid password', client_ip
|
103
|
+
return nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
add_failure_to email, 'invalid email', client_ip
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Attempts to authenticate the user and returns the model on success.
|
112
|
+
def self.authenticate(email, password, client_ip)
|
113
|
+
default.authenticate email, password, client_ip
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Registers an authentication engine for one or more domains.
|
118
|
+
#
|
119
|
+
# The +engine+ passed in should take an options hash as the only argument to +initialize+
|
120
|
+
# and should provide an +authenticate+ method that takes the +email+, +password+, and
|
121
|
+
# +client_ip+.
|
122
|
+
#
|
123
|
+
# The +authenticate+ method of the engine should return an Incline::User object on success or nil on failure.
|
124
|
+
#
|
125
|
+
# class MyAuthEngine
|
126
|
+
# def initialize(options = {})
|
127
|
+
# ...
|
128
|
+
# end
|
129
|
+
#
|
130
|
+
# def authenticate(email, password, client_ip)
|
131
|
+
# ...
|
132
|
+
# end
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# Incline::UserManager.register_auth_engine(MyAuthEngine, 'example.com', 'example.net', 'example.org')
|
136
|
+
#
|
137
|
+
def register_auth_engine(engine, *domains)
|
138
|
+
unless engine.nil?
|
139
|
+
unless engine.is_a?(::Incline::AuthEngineBase)
|
140
|
+
raise ArgumentError, "The 'engine' parameter must be an instance of an auth engine or a class defining an auth engine." unless engine.is_a?(::Class)
|
141
|
+
engine = engine.new(@options)
|
142
|
+
raise ArgumentError, "The 'engine' parameter must be an instance of an auth engine or a class defining an auth engine." unless engine.is_a?(::Incline::AuthEngineBase)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
domains.map do |dom|
|
146
|
+
dom.to_s.downcase.strip
|
147
|
+
raise ArgumentError, "The domain #{dom.inspect} does not appear to be a valid domain." unless dom =~ /\A[a-z0-9]+(?:[-.][a-z0-9]+)*\.[a-z]+\Z/
|
148
|
+
dom
|
149
|
+
end.each do |dom|
|
150
|
+
auth_engines[dom] = engine
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
##
|
155
|
+
# Registers an authentication engine for one or more domains.
|
156
|
+
#
|
157
|
+
# The +engine+ passed in should take an options hash as the only argument to +initialize+
|
158
|
+
# and should provide an +authenticate+ method that takes the +email+, +password+, and
|
159
|
+
# +client_ip+.
|
160
|
+
#
|
161
|
+
# The +authenticate+ method of the engine should return an Incline::User object on success or nil on failure.
|
162
|
+
def self.register_auth_engine(engine, *domains)
|
163
|
+
default.register_auth_engine(engine, *domains)
|
164
|
+
end
|
165
|
+
|
166
|
+
##
|
167
|
+
# Clears any registered authentication engine for one or more domains.
|
168
|
+
def clear_auth_engine(*domains)
|
169
|
+
register_auth_engine(nil, *domains)
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Clears any registered authentication engine for one or more domains.
|
174
|
+
def self.clear_auth_engine(*domains)
|
175
|
+
default.clear_auth_engine(*domains)
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
def auth_engines
|
181
|
+
@auth_engines ||= { }
|
182
|
+
end
|
183
|
+
|
184
|
+
def get_auth_engine(email)
|
185
|
+
dom = email.partition('@')[2].downcase
|
186
|
+
auth_engines[dom]
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.auth_config
|
190
|
+
@auth_config ||=
|
191
|
+
begin
|
192
|
+
cfg = Rails.root.join('config','auth.yml')
|
193
|
+
if File.exist?(cfg)
|
194
|
+
cfg = YAML.load_file(cfg)
|
195
|
+
if cfg.is_a?(::Hash)
|
196
|
+
cfg = cfg[Rails.env]
|
197
|
+
(cfg || {}).symbolize_keys
|
198
|
+
else
|
199
|
+
{}
|
200
|
+
end
|
201
|
+
else
|
202
|
+
{}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def self.default
|
208
|
+
@default ||= UserManager.new(auth_config)
|
209
|
+
end
|
210
|
+
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
|
2
|
+
module Incline
|
3
|
+
##
|
4
|
+
# Validates a string to ensure it contains a valid email address.
|
5
|
+
#
|
6
|
+
# validates :email_address, 'incline/email' => true
|
7
|
+
#
|
8
|
+
class EmailValidator < ActiveModel::EachValidator
|
9
|
+
|
10
|
+
INTERNAL_DOM_REGEX = '[a-z\d]+(?:-+[a-z\d]+)*(?:\.[a-z\d]+(?:-+[a-z\d]+)*)*\.[a-z]+'
|
11
|
+
private_constant :INTERNAL_DOM_REGEX
|
12
|
+
|
13
|
+
##
|
14
|
+
# This regular expression should validate 99.9% of common email addresses.
|
15
|
+
#
|
16
|
+
# There are some weird rules that it doesn't account for, but they should be rare.
|
17
|
+
#
|
18
|
+
VALID_EMAIL_REGEX = /\A[\w+\-.]+@#{INTERNAL_DOM_REGEX}\z/i
|
19
|
+
|
20
|
+
##
|
21
|
+
# This regular expression should validate any domain.
|
22
|
+
VALID_DOMAIN_REGEX = /\A#{INTERNAL_DOM_REGEX}\z/i
|
23
|
+
|
24
|
+
|
25
|
+
##
|
26
|
+
# Validates attributes to determine if they contain valid email addresses.
|
27
|
+
#
|
28
|
+
# Does not perform an in depth check, but does verify that the format is valid.
|
29
|
+
def validate_each(record, attribute, value)
|
30
|
+
unless value.blank?
|
31
|
+
record.errors[attribute] << (options[:message] || 'is not a valid email address') unless value =~ VALID_EMAIL_REGEX
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Validates that an email address is valid based on format.
|
37
|
+
def self.valid?(email)
|
38
|
+
return false if email.blank?
|
39
|
+
!!(email =~ VALID_EMAIL_REGEX)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'ipaddr'
|
2
|
+
|
3
|
+
module Incline
|
4
|
+
##
|
5
|
+
# Validates a string contains a valid IP address.
|
6
|
+
class IpAddressValidator < ActiveModel::EachValidator
|
7
|
+
|
8
|
+
##
|
9
|
+
# Validates attributes to determine if the values contain valid IP addresses.
|
10
|
+
#
|
11
|
+
# Set the :no_mask option to restrict the IP address to singular addresses only.
|
12
|
+
def validate_each(record, attribute, value)
|
13
|
+
begin
|
14
|
+
unless value.blank?
|
15
|
+
IPAddr.new(value)
|
16
|
+
if options[:no_mask]
|
17
|
+
if value =~ /\//
|
18
|
+
record.errors[attribute] << (options[:message] || 'must not contain a mask')
|
19
|
+
end
|
20
|
+
elsif options[:require_mask]
|
21
|
+
unless value =~ /\//
|
22
|
+
record.errors[attribute] << (options[:message] || 'must contain a mask')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
rescue IPAddr::InvalidAddressError
|
27
|
+
record.errors[attribute] << (options[:message] || 'is not a valid IP address')
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
|
2
|
+
module Incline
|
3
|
+
##
|
4
|
+
# Validates a reCAPTCHA attribute.
|
5
|
+
class RecaptchaValidator < ActiveModel::EachValidator
|
6
|
+
|
7
|
+
##
|
8
|
+
# Validates a reCAPTCHA attribute.
|
9
|
+
#
|
10
|
+
# The value of the attribute should be a hash with two keys: :response, :remote_ip
|
11
|
+
def validate_each(record, attribute, value)
|
12
|
+
# Do NOT raise an error if nil.
|
13
|
+
return if value.blank?
|
14
|
+
|
15
|
+
# Make sure the response only gets processed once.
|
16
|
+
return if value == :verified
|
17
|
+
|
18
|
+
# Automatically skip validation if paused.
|
19
|
+
return if Incline::Recaptcha::paused?
|
20
|
+
|
21
|
+
# If the user form includes the recaptcha field, then something will come in
|
22
|
+
# and then we want to check it.
|
23
|
+
remote_ip, _, response = value.partition('|')
|
24
|
+
if remote_ip.blank? || response.blank?
|
25
|
+
record.errors[:base] << (options[:message] || 'Requires reCAPTCHA challenge to be completed')
|
26
|
+
else
|
27
|
+
if Incline::Recaptcha::verify(response: response, remote_ip: remote_ip)
|
28
|
+
record.send "#{attribute}=", :verified
|
29
|
+
else
|
30
|
+
record.errors[:base] << (options[:message] || 'Invalid response from reCAPTCHA challenge')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
|
2
|
+
module Incline
|
3
|
+
##
|
4
|
+
# Validates a string value to ensure it is a safe name.
|
5
|
+
#
|
6
|
+
# A safe name is one that only contains letters, numbers, and underscore.
|
7
|
+
# It must also start with a letter and cannot end with an underscore.
|
8
|
+
class SafeNameValidator < ActiveModel::EachValidator
|
9
|
+
|
10
|
+
##
|
11
|
+
# Validates a string to ensure it is a safe name.
|
12
|
+
VALID_MASK = /\A[a-z](?:_*[a-z0-9]+)*\z/i
|
13
|
+
|
14
|
+
##
|
15
|
+
# Validates attributes to determine if the values match the requirements of a safe name.
|
16
|
+
def validate_each(record, attribute, value)
|
17
|
+
unless value.blank?
|
18
|
+
unless value =~ VALID_MASK
|
19
|
+
if value =~ /\A[^a-z]/i
|
20
|
+
record.errors[attribute] << (options[:message] || 'must start with a letter')
|
21
|
+
elsif value =~ /_\z/
|
22
|
+
record.errors[attribute] << (options[:message] || 'must not end with an underscore')
|
23
|
+
else
|
24
|
+
record.errors[attribute] << (options[:message] || 'must contain only letters, numbers, and underscore')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|