camaleon_cms 2.9.1 → 2.9.3
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.
- checksums.yaml +4 -4
- data/README.md +29 -9
- data/Rakefile +3 -16
- data/app/apps/plugins/attack/admin_controller.rb +9 -6
- data/app/apps/plugins/attack/attack_helper.rb +35 -25
- data/app/apps/plugins/authoring_post/authoring_post_helper.rb +23 -16
- data/app/apps/plugins/front_cache/admin_controller.rb +16 -10
- data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
- data/app/apps/plugins/front_cache/front_cache_helper.rb +57 -24
- data/app/apps/plugins/visibility_post/config/config.json +1 -0
- data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
- data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
- data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
- data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
- data/app/apps/themes/default/assets/css/style.css.scss +24 -0
- data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
- data/app/apps/themes/default/default_helper.rb +2 -1
- data/app/apps/themes/default/views/category.html.erb +1 -1
- data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
- data/app/apps/themes/default/views/post_tag.html.erb +1 -1
- data/app/apps/themes/default/views/post_type.html.erb +1 -1
- data/app/apps/themes/default/views/search.html.erb +1 -1
- data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
- data/app/apps/themes/new/custom_helper.rb +5 -9
- data/app/apps/themes/new/views/category.html.erb +1 -1
- data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
- data/app/apps/themes/new/views/post_tag.html.erb +1 -1
- data/app/apps/themes/new/views/post_type.html.erb +1 -1
- data/app/apps/themes/new/views/search.html.erb +1 -1
- data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
- data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
- data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
- data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
- data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +29 -10
- data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
- data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +9 -2
- data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +4 -3
- data/app/controllers/camaleon_cms/admin/appearances/widgets/sidebar_controller.rb +2 -2
- data/app/controllers/camaleon_cms/admin/categories_controller.rb +27 -8
- data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
- data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
- data/app/controllers/camaleon_cms/admin/media_controller.rb +84 -19
- data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
- data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +15 -6
- data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
- data/app/controllers/camaleon_cms/admin/posts_controller.rb +58 -22
- data/app/controllers/camaleon_cms/admin/sessions_controller.rb +238 -45
- data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +103 -18
- data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +17 -5
- data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +33 -7
- data/app/controllers/camaleon_cms/admin/settings_controller.rb +11 -6
- data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +9 -7
- data/app/controllers/camaleon_cms/admin/users_controller.rb +89 -18
- data/app/controllers/camaleon_cms/admin_controller.rb +144 -26
- data/app/controllers/camaleon_cms/apps/plugins_admin_controller.rb +5 -0
- data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
- data/app/controllers/camaleon_cms/frontend_controller.rb +140 -70
- data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +44 -0
- data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
- data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
- data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
- data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
- data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
- data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
- data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
- data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
- data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
- data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
- data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
- data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
- data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
- data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
- data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
- data/app/decorators/camaleon_cms/post_decorator.rb +67 -51
- data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
- data/app/decorators/camaleon_cms/site_decorator.rb +29 -23
- data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
- data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
- data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
- data/app/helpers/camaleon_cms/admin/application_helper.rb +21 -24
- data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
- data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
- data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
- data/app/helpers/camaleon_cms/admin/post_type_helper.rb +51 -31
- data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
- data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
- data/app/helpers/camaleon_cms/comment_helper.rb +87 -41
- data/app/helpers/camaleon_cms/content_helper.rb +13 -6
- data/app/helpers/camaleon_cms/email_helper.rb +7 -3
- data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
- data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +144 -67
- data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
- data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
- data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
- data/app/helpers/camaleon_cms/html_helper.rb +73 -38
- data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
- data/app/helpers/camaleon_cms/session_helper.rb +195 -39
- data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
- data/app/helpers/camaleon_cms/site_helper.rb +43 -9
- data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
- data/app/helpers/camaleon_cms/uploader_helper.rb +19 -422
- data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
- data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
- data/app/models/camaleon_cms/ability.rb +65 -105
- data/app/models/camaleon_cms/category.rb +7 -5
- data/app/models/camaleon_cms/custom_field.rb +29 -6
- data/app/models/camaleon_cms/custom_field_group.rb +75 -21
- data/app/models/camaleon_cms/custom_fields_relationship.rb +133 -2
- data/app/models/camaleon_cms/media.rb +8 -2
- data/app/models/camaleon_cms/meta.rb +2 -2
- data/app/models/camaleon_cms/nav_menu.rb +6 -2
- data/app/models/camaleon_cms/nav_menu_item.rb +18 -8
- data/app/models/camaleon_cms/plugin.rb +4 -3
- data/app/models/camaleon_cms/post.rb +113 -27
- data/app/models/camaleon_cms/post_comment.rb +10 -6
- data/app/models/camaleon_cms/post_default.rb +42 -12
- data/app/models/camaleon_cms/post_relationship.rb +6 -4
- data/app/models/camaleon_cms/post_tag.rb +4 -3
- data/app/models/camaleon_cms/post_type.rb +22 -12
- data/app/models/camaleon_cms/site.rb +40 -28
- data/app/models/camaleon_cms/term_relationship.rb +10 -7
- data/app/models/camaleon_cms/term_taxonomy.rb +51 -5
- data/app/models/camaleon_cms/theme.rb +3 -3
- data/app/models/camaleon_cms/user.rb +17 -5
- data/app/models/camaleon_cms/user_role.rb +77 -4
- data/app/models/camaleon_cms/widget/assigned.rb +11 -6
- data/app/models/camaleon_cms/widget/main.rb +13 -7
- data/app/models/camaleon_cms/widget/sidebar.rb +3 -6
- data/app/models/camaleon_record.rb +123 -5
- data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +20 -38
- data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
- data/app/models/concerns/camaleon_cms/custom_fields_read.rb +115 -58
- data/app/models/concerns/camaleon_cms/metas.rb +32 -7
- data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
- data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
- data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
- data/app/models/current_request.rb +23 -0
- data/app/uploaders/camaleon_cms_aws_uploader.rb +39 -11
- data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
- data/app/uploaders/camaleon_cms_uploader.rb +23 -10
- data/app/validators/camaleon_cms/post_uniq_validator.rb +21 -8
- data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
- data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
- data/app/views/camaleon_cms/admin/appearances/nav_menus/_left_menu_items.html.erb +2 -2
- data/app/views/camaleon_cms/admin/appearances/widgets/main/form.html.erb +1 -1
- data/app/views/camaleon_cms/admin/categories/index.html.erb +3 -3
- data/app/views/camaleon_cms/admin/comments/index.html.erb +2 -2
- data/app/views/camaleon_cms/admin/comments/list.html.erb +1 -1
- data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
- data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
- data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
- data/app/views/camaleon_cms/admin/post_tags/index.html.erb +3 -3
- data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +2 -2
- data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
- data/app/views/camaleon_cms/admin/posts/index.html.erb +6 -6
- data/app/views/camaleon_cms/admin/search.html.erb +1 -1
- data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
- data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
- data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +42 -6
- data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/fields/_select_eval.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +5 -4
- data/app/views/camaleon_cms/admin/settings/custom_fields/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/post_types/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
- data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/sites/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/user_roles/form.html.erb +102 -7
- data/app/views/camaleon_cms/admin/user_roles/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
- data/app/views/camaleon_cms/admin/users/index.html.erb +2 -2
- data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
- data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
- data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
- data/app/views/layouts/camaleon_cms/admin/_flash_messages.html.erb +2 -2
- data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
- data/config/initializers/action_view.rb +42 -5
- data/config/initializers/active_record_extension.rb +13 -27
- data/config/initializers/assets.rb +52 -23
- data/config/initializers/custom_initializers.rb +3 -3
- data/config/locales/camaleon_cms/admin/ar.yml +7 -2
- data/config/locales/camaleon_cms/admin/de.yml +7 -2
- data/config/locales/camaleon_cms/admin/en.yml +39 -2
- data/config/locales/camaleon_cms/admin/es.yml +7 -2
- data/config/locales/camaleon_cms/admin/fr.yml +7 -2
- data/config/locales/camaleon_cms/admin/it.yml +7 -2
- data/config/locales/camaleon_cms/admin/nl.yml +8 -2
- data/config/locales/camaleon_cms/admin/pt-BR.yml +7 -2
- data/config/locales/camaleon_cms/admin/pt.yml +7 -2
- data/config/locales/camaleon_cms/admin/ru.yml +7 -2
- data/config/locales/camaleon_cms/admin/uk.yml +7 -2
- data/config/locales/camaleon_cms/admin/zh-CH.yml +7 -2
- data/config/routes/admin.rb +33 -15
- data/config/routes/frontend.rb +21 -15
- data/db/migrate/20150611161134_post_table_into_utf8.rb +14 -14
- data/db/migrate/20150926095310_rename_column_posts.rb +3 -3
- data/db/migrate/20151212095328_add_confirm_token_to_users.rb +3 -3
- data/db/migrate/20160504155652_add_feature_to_posts.rb +1 -1
- data/db/migrate/20160504155653_move_first_name_of_users.rb +2 -2
- data/db/migrate/20160609121449_add_group_to_custom_field_values.rb +1 -1
- data/db/migrate/20161215202255_drop_user_relationship_table.rb +1 -1
- data/db/migrate/20180124132318_create_media.rb +1 -1
- data/db/migrate/20180704211100_adjust_field_length.rb +1 -1
- data/lib/camaleon_cms/assets_precompile.rb +86 -0
- data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
- data/lib/camaleon_cms/content_security.rb +158 -0
- data/lib/camaleon_cms/engine.rb +47 -9
- data/lib/camaleon_cms/media_security_headers.rb +27 -0
- data/lib/camaleon_cms/setup_token.rb +69 -0
- data/lib/camaleon_cms/svg_content_checker.rb +123 -0
- data/lib/camaleon_cms/task_reporter.rb +14 -0
- data/lib/camaleon_cms/unsafe_markup.rb +207 -0
- data/lib/camaleon_cms/uploader_content_security.rb +248 -0
- data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
- data/lib/camaleon_cms/uploader_path_security.rb +146 -0
- data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
- data/lib/camaleon_cms/uploader_support.rb +86 -0
- data/lib/camaleon_cms/version.rb +1 -1
- data/lib/camaleon_cms.rb +11 -0
- data/lib/ext/hash.rb +28 -2
- data/lib/ext/string.rb +13 -8
- data/lib/ext/translator.rb +10 -2
- data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
- data/lib/generators/camaleon_cms/install_generator.rb +7 -4
- data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
- data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
- data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
- data/lib/plugin_routes.rb +412 -368
- data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
- data/lib/tasks/cross_site_field_groups.rake +74 -0
- data/lib/tasks/custom_fields_roles.rake +58 -0
- data/lib/tasks/orphaned_comments.rake +42 -0
- data/lib/tasks/private_upload_acls.rake +31 -0
- data/lib/tasks/site_custom_field_groups.rake +36 -0
- data/lib/tasks/unsafe_stored_content.rake +42 -0
- data/lib/tasks/unsafe_stored_uploads.rake +70 -0
- data/lib/tasks/user_field_groups.rake +27 -0
- metadata +185 -7
|
@@ -6,10 +6,22 @@ module CamaleonCms
|
|
|
6
6
|
# redirect_url (default nil): after initialized the session, this will be redirected to
|
|
7
7
|
# "redirect_url" if defined
|
|
8
8
|
# it doesn't redirect if redirect_url === false
|
|
9
|
-
# return to previous page if defined the cookie['return_to'] or login
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
# return to previous page if defined the cookie['return_to'], or login
|
|
10
|
+
# url received extra param: return_to=https://mysite.com
|
|
11
|
+
# allow_external (default false): follow redirect_url even when it points off-site, for a caller that
|
|
12
|
+
# has vetted the destination (e.g. an after_login hook doing SSO/payment). http(s) only; a configured
|
|
13
|
+
# allowlist (redirect_allowed_hosts option / safe_redirect_hosts hook) is honored regardless of it.
|
|
14
|
+
def login_user(user, remember_me = false, redirect_url = nil, rotate_session: true, allow_external: false)
|
|
15
|
+
# Rotate the session on a genuine sign-in (H6): a fresh session drops any state left behind by a
|
|
16
|
+
# previous occupant of a shared browser — notably an impersonation parent_auth_token, which
|
|
17
|
+
# session_back_to_parent would otherwise restore on logout, escalating the new user to admin.
|
|
18
|
+
# Impersonation manages that token itself and passes rotate_session: false so this does not wipe it.
|
|
19
|
+
reset_session if rotate_session
|
|
20
|
+
|
|
21
|
+
# Security (audit 2026-08-11 M3): the auth cookie is HttpOnly (JavaScript/XSS cannot read the
|
|
22
|
+
# bearer token) and Secure over an SSL request (never sent in the clear). The three components are
|
|
23
|
+
# joined into one string so the split-based reader and the impersonation stash keep their shape.
|
|
24
|
+
c = cama_auth_cookie_options([user.auth_token, request.user_agent, request.ip].join('&'))
|
|
13
25
|
c[:expires] = 1.month.from_now if remember_me
|
|
14
26
|
|
|
15
27
|
# fix to overwrite a cookie
|
|
@@ -24,9 +36,13 @@ module CamaleonCms
|
|
|
24
36
|
return if redirect_url == false
|
|
25
37
|
|
|
26
38
|
if redirect_url.present?
|
|
27
|
-
|
|
39
|
+
# Host-check the explicit redirect_url too: after_login hooks and downstream plugins pass
|
|
40
|
+
# caller-controlled destinations here (e.g. a return_to cookie), so it gets the same open-redirect
|
|
41
|
+
# guard as the return_to cookie branch below. allow_external lets such a hook opt a vetted off-site
|
|
42
|
+
# destination (SSO/payment) past the same-host rule; it stays http(s)-only either way.
|
|
43
|
+
cama_safe_redirect(redirect_url, cama_admin_dashboard_path, allow_external: allow_external)
|
|
28
44
|
elsif (return_to = cookies.delete(:return_to)).present?
|
|
29
|
-
|
|
45
|
+
cama_safe_redirect(return_to, cama_admin_dashboard_path)
|
|
30
46
|
else
|
|
31
47
|
redirect_to cama_admin_dashboard_path
|
|
32
48
|
end
|
|
@@ -35,13 +51,14 @@ module CamaleonCms
|
|
|
35
51
|
# login a user using username and password
|
|
36
52
|
# return boolean: true => authenticated, false => authentication failed
|
|
37
53
|
def login_user_with_password(username, password)
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
# Custom class finder: usernames are stored downcased, so the lookup must
|
|
55
|
+
# lower-case both sides regardless of DB collation.
|
|
56
|
+
user = current_site.users.find_by_username(username) # rubocop:disable Rails/DynamicFindBy
|
|
57
|
+
r = { user: user, params: params, password: password, captcha_validate: true }
|
|
40
58
|
hooks_run('user_before_login', r)
|
|
41
|
-
|
|
59
|
+
user&.authenticate(password)
|
|
42
60
|
end
|
|
43
61
|
|
|
44
|
-
##
|
|
45
62
|
# User registration.
|
|
46
63
|
#
|
|
47
64
|
# user_data must contain:
|
|
@@ -50,22 +67,42 @@ module CamaleonCms
|
|
|
50
67
|
# - username
|
|
51
68
|
# - password
|
|
52
69
|
# - password_confirmation
|
|
53
|
-
|
|
54
70
|
def cama_register_user(user_data, meta)
|
|
55
|
-
|
|
56
|
-
r = { user: @user, params: params }
|
|
57
|
-
hook_run('user_before_register', r)
|
|
71
|
+
user = current_site.users.new(user_data)
|
|
58
72
|
|
|
73
|
+
# Gate on the register captcha before running any hook, so user_before_register does not fire for
|
|
74
|
+
# attempts that fail the captcha. This diverges from user_before_login, which runs its hook even on a
|
|
75
|
+
# failed captcha and passes the result as r[:captcha_validate]; user_before_register does neither, so
|
|
76
|
+
# the "parity with user_before_login" below is limited to the stop_process veto. Verifying here also
|
|
77
|
+
# consumes the single-use captcha before the hook, so a later veto burns a solved challenge —
|
|
78
|
+
# harmless, since the re-rendered form issues a fresh one.
|
|
59
79
|
if current_site.security_user_register_captcha_enabled? && !cama_captcha_verified?
|
|
60
|
-
{ result: false, type: :captcha_error,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
80
|
+
return { result: false, type: :captcha_error,
|
|
81
|
+
message: t('camaleon_cms.admin.users.message.error_captcha'), user: user }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
r = { user: user, params: params, stop_process: false }
|
|
85
|
+
# Broadcast (hooks_run), not hook_run: hook_run(target, name, …) takes the app as its first arg, so
|
|
86
|
+
# hook_run('user_before_register', r) treated the name as a plugin and silently no-op'd — the hook
|
|
87
|
+
# never fired. Match the sibling user_before_login/user_after_register broadcasts.
|
|
88
|
+
hooks_run('user_before_register', r)
|
|
89
|
+
# A handler may veto the registration by setting r[:stop_process] (parity with user_before_login):
|
|
90
|
+
# stop here without saving. The handler may surface its own reason (add to r[:user].errors, or
|
|
91
|
+
# render/redirect); otherwise the controller shows a generic error.
|
|
92
|
+
return { result: false, type: :stopped, user: user } if r[:stop_process]
|
|
93
|
+
|
|
94
|
+
if user.save
|
|
95
|
+
user.set_metas(meta)
|
|
96
|
+
message = if current_site.need_validate_email?
|
|
97
|
+
t('camaleon_cms.admin.users.message.created_pending_validate_email')
|
|
98
|
+
else
|
|
99
|
+
t('camaleon_cms.admin.users.message.created')
|
|
100
|
+
end
|
|
101
|
+
r = { user: user, message: message, redirect_url: cama_admin_login_path }
|
|
65
102
|
hooks_run('user_after_register', r)
|
|
66
|
-
{ result: true, message: r[:message], redirect_url: r[:redirect_url] }
|
|
103
|
+
{ result: true, message: r[:message], redirect_url: r[:redirect_url], user: user }
|
|
67
104
|
else
|
|
68
|
-
{ result: false, type: :no_saved }
|
|
105
|
+
{ result: false, type: :no_saved, user: user }
|
|
69
106
|
end
|
|
70
107
|
end
|
|
71
108
|
|
|
@@ -79,55 +116,87 @@ module CamaleonCms
|
|
|
79
116
|
def session_switch_user(user, redirect_url = nil)
|
|
80
117
|
return unless cama_sign_in?
|
|
81
118
|
|
|
82
|
-
session
|
|
83
|
-
login_user
|
|
119
|
+
# Start impersonation from a clean session (see login_user), then stash the admin's auth cookie.
|
|
120
|
+
# The stash happens AFTER the reset and login_user is told not to rotate again, so the token
|
|
121
|
+
# session_back_to_parent restores later survives this rotation (H6).
|
|
122
|
+
parent_auth_token = cookies[:auth_token]
|
|
123
|
+
reset_session
|
|
124
|
+
session[:parent_auth_token] = parent_auth_token
|
|
125
|
+
login_user(user, false, redirect_url, rotate_session: false)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The admin who started the current impersonation, resolved from the stashed parent auth token
|
|
129
|
+
# (same lookup as cama_current_user). Returns nil if there is no active impersonation or the stash
|
|
130
|
+
# no longer resolves to a user (e.g. the admin rotated their token by changing their password).
|
|
131
|
+
# Callers MUST re-authenticate this user before session_back_to_parent (H6 residual): the
|
|
132
|
+
# impersonated session an admin holds is indistinguishable from one they abandon, so the admin's
|
|
133
|
+
# password is the only proof that the person returning is the admin and not a later occupant.
|
|
134
|
+
def cama_impersonation_parent_user
|
|
135
|
+
token = session[:parent_auth_token].to_s.split('&').first
|
|
136
|
+
return if token.blank?
|
|
137
|
+
|
|
138
|
+
current_site.users_include_admins.find_by(auth_token: token)
|
|
84
139
|
end
|
|
85
140
|
|
|
86
141
|
# switch current session into parent session called by session_switch_user
|
|
87
142
|
# after returned into parent session, this will be redirected to redirect_url or admin dashboard
|
|
143
|
+
# SECURITY: only call this once the parent admin has been re-authenticated (see
|
|
144
|
+
# Admin::SessionsController#back_to_parent and cama_impersonation_parent_user).
|
|
88
145
|
def session_back_to_parent(redirect_url = nil)
|
|
89
146
|
return unless cama_sign_in? && session[:parent_auth_token].present?
|
|
90
147
|
|
|
91
|
-
cookies[:auth_token] = session[:parent_auth_token]
|
|
148
|
+
cookies[:auth_token] = cama_auth_cookie_options(session[:parent_auth_token])
|
|
92
149
|
session.delete(:parent_auth_token)
|
|
93
150
|
redirect_to (redirect_url || cama_admin_dashboard_path), notice: 'Welcome back!'
|
|
94
151
|
end
|
|
95
152
|
|
|
96
153
|
# logout current user
|
|
97
154
|
def cama_logout_user
|
|
155
|
+
# Security (audit 2026-08-11 M3): rotate the server-side token so a cookie copied before logout
|
|
156
|
+
# cannot be replayed afterwards. The token is per-user, so this also ends the user's other sessions.
|
|
157
|
+
# Audit M14: skip the rotation during impersonation -- cama_current_user is then the impersonated
|
|
158
|
+
# user, not the admin ending the session, and rotating their token would log the innocent user
|
|
159
|
+
# out of all their own devices. session_back_to_parent is the intended exit from impersonation.
|
|
160
|
+
cama_current_user&.cama_reset_auth_token! if session[:parent_auth_token].blank?
|
|
98
161
|
cookies.delete(:auth_token, domain: :all)
|
|
99
162
|
cookies.delete(:auth_token, domain: nil)
|
|
100
163
|
c_data = { value: nil, expires: 24.hours.ago }
|
|
101
164
|
c_data[:domain] = :all if PluginRoutes.system_info['users_share_sites'].present? && CamaleonCms::Site.count > 1
|
|
102
165
|
cookies[:auth_token] = c_data
|
|
103
|
-
|
|
104
|
-
|
|
166
|
+
CurrentRequest.user = nil
|
|
167
|
+
CurrentRequest.user_resolved = true # the cookie is gone; don't re-resolve to a stale user
|
|
168
|
+
# Drop all server-side session state on logout (H6): a lingering impersonation parent_auth_token
|
|
169
|
+
# must not survive to be restored into a later session on a shared browser.
|
|
170
|
+
reset_session
|
|
171
|
+
cama_safe_redirect(params[:return_to], cama_admin_login_path,
|
|
172
|
+
notice: t('camaleon_cms.admin.logout.message.closed'))
|
|
105
173
|
end
|
|
106
174
|
|
|
107
|
-
#
|
|
175
|
+
# Check if the current user is already signed
|
|
108
176
|
def cama_sign_in?
|
|
109
177
|
!cama_current_user.nil?
|
|
110
178
|
end
|
|
111
179
|
|
|
112
180
|
alias signin? cama_sign_in?
|
|
113
181
|
|
|
114
|
-
# return the role for current user
|
|
182
|
+
# return the role for the current user
|
|
115
183
|
# if not logged in, then return 'public'
|
|
116
184
|
def cama_current_role
|
|
117
185
|
current_site.visitor_role
|
|
118
186
|
end
|
|
119
187
|
|
|
120
|
-
# return current user logged in
|
|
188
|
+
# return the current user logged in
|
|
121
189
|
def cama_current_user
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
@cama_current_user = cama_calc_api_current_user
|
|
126
|
-
return @cama_current_user if @cama_current_user
|
|
190
|
+
# Honor an externally-set user, and memoize a nil resolution via user_resolved so a signed-out
|
|
191
|
+
# request (or a present-but-stale auth cookie) is not re-resolved on every call (regression M16).
|
|
192
|
+
return CurrentRequest.user if CurrentRequest.user || CurrentRequest.user_resolved
|
|
127
193
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
194
|
+
CurrentRequest.user_resolved = true
|
|
195
|
+
user = cama_calc_api_current_user # api current user...
|
|
196
|
+
if user.nil? && cookie_auth_token_complete?
|
|
197
|
+
user = current_site.users_include_admins.find_by(auth_token: user_auth_token_from_cookie).try(:decorate)
|
|
198
|
+
end
|
|
199
|
+
CurrentRequest.user = user
|
|
131
200
|
end
|
|
132
201
|
|
|
133
202
|
def cookie_auth_token_complete?
|
|
@@ -159,7 +228,7 @@ module CamaleonCms
|
|
|
159
228
|
|
|
160
229
|
# return the session id
|
|
161
230
|
def cama_get_session_id
|
|
162
|
-
session[:autor] = 'Owen Peredo Diaz'
|
|
231
|
+
session[:autor] = 'Owen Peredo Diaz' if request.session_options[:id].blank?
|
|
163
232
|
id = request.session_options[:id]
|
|
164
233
|
id = id.public_id if id.instance_of?(::Rack::Session::SessionId)
|
|
165
234
|
id
|
|
@@ -167,6 +236,93 @@ module CamaleonCms
|
|
|
167
236
|
|
|
168
237
|
private
|
|
169
238
|
|
|
239
|
+
# Options for the auth cookie (M3): HttpOnly so JavaScript cannot read the bearer token, Secure over
|
|
240
|
+
# an SSL request so it is never sent in the clear, a 24h default lifetime, and the shared-domain
|
|
241
|
+
# scope when sites share users. Callers override :expires (remember-me) as needed.
|
|
242
|
+
def cama_auth_cookie_options(value)
|
|
243
|
+
opts = { value: value, httponly: true, secure: request.ssl?, expires: 24.hours.from_now }
|
|
244
|
+
opts[:domain] = :all if PluginRoutes.system_info['users_share_sites'].present? && CamaleonCms::Site.count > 1
|
|
245
|
+
opts
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# validate redirect url to prevent open redirect attacks: relative, or same-host over http(s), only —
|
|
249
|
+
# unless the off-site host is explicitly trusted (see cama_redirect_allowed_hosts) or the caller vouches
|
|
250
|
+
# for it (allow_external, for hook-set SSO/payment destinations). Host compared case-insensitively
|
|
251
|
+
# (RFC 3986); a same-host absolute URL is emitted with the host in the request's canonical case, so
|
|
252
|
+
# Rails' own case-sensitive open-redirect protection stays active without tripping on it.
|
|
253
|
+
def safe_redirect_url(url, allow_external: false)
|
|
254
|
+
return if url.blank?
|
|
255
|
+
|
|
256
|
+
uri = URI.parse(url)
|
|
257
|
+
if uri.host.present?
|
|
258
|
+
# A non-HTTP scheme carrying a host (javascript://host/..., data://host/...) parses with a host but
|
|
259
|
+
# is not a real navigation — it would execute if rendered into an href/JS sink. Reject it before any
|
|
260
|
+
# host decision, so neither the allowlist nor allow_external can let such a scheme through.
|
|
261
|
+
return unless uri.scheme.blank? || %w[http https].include?(uri.scheme.downcase)
|
|
262
|
+
|
|
263
|
+
if uri.host.casecmp?(request.host)
|
|
264
|
+
uri.host = request.host
|
|
265
|
+
return uri.to_s
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Off-host: followed only for an admin/plugin-vetted host, or when the caller explicitly vouches for
|
|
269
|
+
# this destination (allow_external — a hook doing SSO/payment). Never for a caller-controlled
|
|
270
|
+
# return_to, which always passes allow_external: false.
|
|
271
|
+
return uri.to_s if allow_external || cama_redirect_host_allowed?(uri.host)
|
|
272
|
+
|
|
273
|
+
return
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# A blank parsed host does not mean same-origin: a scheme (https:evil.com, javascript:...) or a
|
|
277
|
+
# slash/backslash-led form (///evil.com, /\evil.com, and their %2f/%5c encodings) still sends a
|
|
278
|
+
# browser off-site. Follow only a genuine same-origin path — one leading "/" then a normal path
|
|
279
|
+
# character (audit: the ///evil.com open redirect).
|
|
280
|
+
return unless uri.scheme.blank? && url.start_with?('/') && !url.match?(%r{\A/(?:[/\\]|%2f|%5c)}i)
|
|
281
|
+
|
|
282
|
+
url
|
|
283
|
+
rescue URI::InvalidURIError
|
|
284
|
+
nil
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Redirect to a destination vetted by safe_redirect_url, or to `fallback` when it is dropped. A vetted
|
|
288
|
+
# off-host destination (allowlisted or vouched via allow_external) is emitted with allow_other_host so
|
|
289
|
+
# Rails' backstop does not reject it; a same-host destination keeps that backstop as a second layer.
|
|
290
|
+
def cama_safe_redirect(url, fallback, allow_external: false, **opts)
|
|
291
|
+
target = safe_redirect_url(url, allow_external: allow_external)
|
|
292
|
+
return redirect_to(fallback, **opts) if target.blank?
|
|
293
|
+
|
|
294
|
+
opts[:allow_other_host] = true if cama_redirect_off_host?(target) && cama_supports_allow_other_host?
|
|
295
|
+
redirect_to(target, **opts)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Admin- and plugin-declared allowlist of off-site hosts that redirects may target, in addition to the
|
|
299
|
+
# request host. Sourced from the `redirect_allowed_hosts` site option (comma-separated) and the
|
|
300
|
+
# `safe_redirect_hosts` hook (plugins append their provider host to r[:hosts]). Empty by default — the
|
|
301
|
+
# strict same-host policy — so an off-host redirect is only ever reachable once a host is explicitly
|
|
302
|
+
# trusted. Only consulted for an off-host destination, so same-host/relative redirects skip it.
|
|
303
|
+
def cama_redirect_allowed_hosts
|
|
304
|
+
r = { hosts: current_site.get_option('redirect_allowed_hosts', '').to_s.split(',') }
|
|
305
|
+
hooks_run('safe_redirect_hosts', r)
|
|
306
|
+
Array(r[:hosts]).map { |h| h.to_s.strip }.reject(&:blank?)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def cama_redirect_host_allowed?(host)
|
|
310
|
+
cama_redirect_allowed_hosts.any? { |allowed| allowed.casecmp?(host) }
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def cama_redirect_off_host?(url)
|
|
314
|
+
host = URI.parse(url).host
|
|
315
|
+
host.present? && !host.casecmp?(request.host)
|
|
316
|
+
rescue URI::InvalidURIError
|
|
317
|
+
false
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# allow_other_host is a Rails 7.0+ redirect_to option; on the 6.1 floor there is no open-redirect
|
|
321
|
+
# backstop to satisfy, so an off-host redirect there just works without it.
|
|
322
|
+
def cama_supports_allow_other_host?
|
|
323
|
+
Rails::VERSION::MAJOR >= 7
|
|
324
|
+
end
|
|
325
|
+
|
|
170
326
|
# calculate the current user for API
|
|
171
327
|
def cama_calc_api_current_user
|
|
172
328
|
begin
|
|
@@ -178,7 +334,7 @@ module CamaleonCms
|
|
|
178
334
|
end
|
|
179
335
|
return unless doorkeeper_token
|
|
180
336
|
|
|
181
|
-
current_site.users_include_admins.
|
|
337
|
+
current_site.users_include_admins.find_by(id: doorkeeper_token.resource_owner_id).try(:decorate)
|
|
182
338
|
end
|
|
183
339
|
end
|
|
184
340
|
end
|