camaleon_cms 2.9.2 → 2.9.4
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 +17 -4
- 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 +14 -10
- data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
- data/app/apps/plugins/front_cache/config/locales/translation.yml +0 -7
- data/app/apps/plugins/front_cache/front_cache_helper.rb +109 -37
- data/app/apps/plugins/front_cache/views/admin/settings.html.erb +0 -5
- 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/layouts/index.html.erb +3 -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/layouts/_header.html.erb +3 -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 +8 -6
- data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
- data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +7 -2
- data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +2 -1
- data/app/controllers/camaleon_cms/admin/categories_controller.rb +19 -4
- 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 +66 -14
- data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
- data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +10 -4
- data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
- data/app/controllers/camaleon_cms/admin/posts_controller.rb +55 -22
- data/app/controllers/camaleon_cms/admin/sessions_controller.rb +237 -44
- data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +80 -13
- data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +5 -2
- data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +26 -3
- data/app/controllers/camaleon_cms/admin/settings_controller.rb +5 -3
- data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +7 -5
- data/app/controllers/camaleon_cms/admin/users_controller.rb +78 -11
- data/app/controllers/camaleon_cms/admin_controller.rb +136 -26
- data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
- data/app/controllers/camaleon_cms/frontend_controller.rb +146 -74
- data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +20 -5
- 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 +68 -50
- data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
- data/app/decorators/camaleon_cms/site_decorator.rb +33 -25
- 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 +5 -8
- 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 +34 -17
- 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 +19 -7
- 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/application_helper.rb +6 -1
- data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
- data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +143 -66
- 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 +58 -37
- data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
- data/app/helpers/camaleon_cms/session_helper.rb +188 -44
- data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
- data/app/helpers/camaleon_cms/site_helper.rb +34 -13
- data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
- data/app/helpers/camaleon_cms/uploader_helper.rb +19 -473
- 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 +15 -7
- data/app/models/camaleon_cms/category.rb +6 -6
- data/app/models/camaleon_cms/custom_field.rb +15 -6
- data/app/models/camaleon_cms/custom_field_group.rb +39 -22
- data/app/models/camaleon_cms/custom_fields_relationship.rb +140 -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 +5 -3
- data/app/models/camaleon_cms/nav_menu_item.rb +16 -8
- data/app/models/camaleon_cms/plugin.rb +3 -4
- data/app/models/camaleon_cms/post.rb +137 -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 -5
- data/app/models/camaleon_cms/post_type.rb +21 -13
- data/app/models/camaleon_cms/site.rb +39 -29
- data/app/models/camaleon_cms/term_relationship.rb +10 -7
- data/app/models/camaleon_cms/term_taxonomy.rb +58 -4
- data/app/models/camaleon_cms/theme.rb +2 -4
- data/app/models/camaleon_cms/user.rb +17 -5
- data/app/models/camaleon_cms/user_role.rb +81 -6
- data/app/models/camaleon_cms/widget/assigned.rb +11 -6
- data/app/models/camaleon_cms/widget/main.rb +12 -8
- data/app/models/camaleon_cms/widget/sidebar.rb +3 -8
- data/app/models/camaleon_record.rb +89 -5
- data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +36 -42
- data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
- data/app/models/concerns/camaleon_cms/content_shortcode_gate.rb +73 -0
- data/app/models/concerns/camaleon_cms/custom_fields_read.rb +111 -54
- data/app/models/concerns/camaleon_cms/metas.rb +31 -6
- 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 +8 -1
- data/app/uploaders/camaleon_cms_aws_uploader.rb +31 -10
- 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 +5 -4
- 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/categories/index.html.erb +2 -2
- 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 +2 -2
- data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
- data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
- data/app/views/camaleon_cms/admin/posts/index.html.erb +5 -5
- 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 +19 -4
- 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/form.html.erb +4 -3
- 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/user_roles/form.html.erb +24 -3
- data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
- data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
- 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/_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 +1 -1
- data/config/locales/camaleon_cms/admin/ar.yml +1 -0
- data/config/locales/camaleon_cms/admin/de.yml +1 -0
- data/config/locales/camaleon_cms/admin/en.yml +36 -0
- data/config/locales/camaleon_cms/admin/es.yml +1 -0
- data/config/locales/camaleon_cms/admin/fr.yml +1 -0
- data/config/locales/camaleon_cms/admin/it.yml +1 -0
- data/config/locales/camaleon_cms/admin/nl.yml +1 -0
- data/config/locales/camaleon_cms/admin/pt-BR.yml +1 -0
- data/config/locales/camaleon_cms/admin/pt.yml +1 -0
- data/config/locales/camaleon_cms/admin/ru.yml +1 -0
- data/config/locales/camaleon_cms/admin/uk.yml +1 -0
- data/config/locales/camaleon_cms/admin/zh-CH.yml +1 -0
- data/config/routes/admin.rb +33 -15
- data/config/routes/frontend.rb +21 -15
- 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 +45 -21
- data/lib/camaleon_cms/media_security_headers.rb +27 -0
- data/lib/camaleon_cms/setup_token.rb +69 -0
- data/lib/camaleon_cms/shortcode_registry.rb +102 -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 +23 -0
- data/lib/ext/hash.rb +28 -2
- data/lib/ext/string.rb +10 -5
- 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 +15 -13
- 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 +125 -4
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
module CamaleonCms
|
|
2
2
|
module HtmlHelper
|
|
3
3
|
def cama_html_helpers_init
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
state = camaleon_html_helper_state
|
|
5
|
+
state[:pre_assets_content] = [] # Assets contents before the libraries import
|
|
6
|
+
state[:assets_libraries] = {}
|
|
7
|
+
state[:assets_content] = []
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
# register a new asset library to be included on demand calling by: cama_load_libraries(...)
|
|
@@ -12,36 +13,47 @@ module CamaleonCms
|
|
|
12
13
|
def cama_assets_library_register(key, assets = {})
|
|
13
14
|
key = key.to_sym
|
|
14
15
|
cama_assets_libraries
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
state = camaleon_html_helper_state
|
|
17
|
+
state[:cama_assets_libraries][key] = { css: [], js: [] } if state[:cama_assets_libraries][key].blank?
|
|
18
|
+
state[:cama_assets_libraries][key][:css] += assets[:css] if assets[:css].present?
|
|
19
|
+
state[:cama_assets_libraries][key][:js] += assets[:js] if assets[:js].present?
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
# enable to load admin or registered libraries (colorpicker, datepicker, tinymce, form_ajax, cropper)
|
|
21
23
|
# sample: add_asset_library("datepicker", "colorpicker")
|
|
22
24
|
# This will add this assets library in the admin head or in a custom place by calling: cama_draw_custom_assets()
|
|
23
25
|
def cama_load_libraries(*keys)
|
|
26
|
+
state = camaleon_html_helper_state
|
|
24
27
|
keys.each do |key|
|
|
25
28
|
library = cama_assets_libraries[key.to_sym]
|
|
26
|
-
|
|
29
|
+
state[:assets_libraries][key.to_sym] = library if library.present?
|
|
27
30
|
end
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
alias add_asset_library cama_load_libraries
|
|
31
34
|
|
|
32
|
-
# add custom asset libraries (js, css or both) for the current request, also you can add extra css or js files for
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
+
# add custom asset libraries (js, css or both) for the current request, also you can add extra css or js files for
|
|
36
|
+
# existent libraries
|
|
37
|
+
# sample: (add a new library)
|
|
38
|
+
# append_asset_libraries(
|
|
39
|
+
# {
|
|
40
|
+
# "my_library_key"=> {
|
|
41
|
+
# js: [plugin_asset("js/my_js"), "plugins/myplugin/assets/js/my_js2"],
|
|
42
|
+
# css: [plugin_asset("css/my_css"), "plugins/myplugin/assets/css/my_css2"]
|
|
43
|
+
# }
|
|
44
|
+
# }
|
|
45
|
+
# )
|
|
35
46
|
# sample: (update existent library)
|
|
36
47
|
# append_asset_libraries({"colorpicker"=>{js: [plugin_asset("js/my_custom_js")] } })
|
|
37
48
|
# return nil
|
|
38
49
|
def append_asset_libraries(libraries)
|
|
50
|
+
state = camaleon_html_helper_state
|
|
39
51
|
libraries.each do |key, library|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
52
|
+
state[:assets_libraries][key.to_sym] = if state[:assets_libraries].include?(key)
|
|
53
|
+
state[:assets_libraries][key.to_sym].merge(library)
|
|
54
|
+
else
|
|
55
|
+
library
|
|
56
|
+
end
|
|
45
57
|
end
|
|
46
58
|
end
|
|
47
59
|
alias cama_load_custom_assets append_asset_libraries
|
|
@@ -51,7 +63,7 @@ module CamaleonCms
|
|
|
51
63
|
# content may be: <style>a{color: red;}</style>
|
|
52
64
|
# this will be printed with <%raw cama_draw_custom_assets %>
|
|
53
65
|
def append_asset_content(content)
|
|
54
|
-
|
|
66
|
+
camaleon_html_helper_state[:assets_content] << content
|
|
55
67
|
end
|
|
56
68
|
|
|
57
69
|
# add pre asset content into custom assets
|
|
@@ -59,27 +71,30 @@ module CamaleonCms
|
|
|
59
71
|
# content may be: <style>a{color: red;}</style>
|
|
60
72
|
# this will be printed before assets_library with <%raw cama_draw_pre_asset_contents %>
|
|
61
73
|
def append_pre_asset_content(content)
|
|
62
|
-
|
|
74
|
+
camaleon_html_helper_state[:pre_assets_content] << content
|
|
63
75
|
end
|
|
64
76
|
|
|
65
77
|
# return all scripts to be executed before import the js libraries(cama_draw_custom_assets)
|
|
66
78
|
def cama_draw_pre_asset_contents
|
|
67
|
-
|
|
79
|
+
# rubocop:disable Rails/OutputSafety -- Callers append trusted script/style fragments that must render as markup.
|
|
80
|
+
camaleon_html_helper_state[:pre_assets_content].join('').html_safe
|
|
81
|
+
# rubocop:enable Rails/OutputSafety
|
|
68
82
|
end
|
|
69
83
|
|
|
70
84
|
# return all js libraries added [aa.js, bb,js, ..]
|
|
71
85
|
# def get_assets_js
|
|
72
86
|
def cama_draw_custom_assets
|
|
73
|
-
|
|
87
|
+
state = camaleon_html_helper_state
|
|
88
|
+
cama_html_helpers_init if state[:assets_libraries].blank?
|
|
74
89
|
libs = []
|
|
75
|
-
|
|
90
|
+
state[:assets_libraries].each_value do |assets|
|
|
76
91
|
libs += assets[:css] if assets[:css].present?
|
|
77
92
|
end
|
|
78
93
|
stylesheets = libs.uniq
|
|
79
94
|
css = stylesheet_link_tag(*stylesheets, media: 'all')
|
|
80
95
|
|
|
81
96
|
libs = []
|
|
82
|
-
|
|
97
|
+
state[:assets_libraries].each_value do |assets|
|
|
83
98
|
libs += assets[:js] if assets[:js].present?
|
|
84
99
|
end
|
|
85
100
|
javascripts = libs.uniq
|
|
@@ -87,33 +102,31 @@ module CamaleonCms
|
|
|
87
102
|
|
|
88
103
|
args = { stylesheets: stylesheets, javascripts: javascripts, js_html: js, css_html: css }
|
|
89
104
|
hooks_run('draw_custom_assets', args)
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
# rubocop:disable Rails/OutputSafety -- Asset helper output and appended fragments are trusted framework/plugin markup.
|
|
106
|
+
trusted_fragments = [args[:css_html], args[:js_html], *state[:assets_content]].filter_map do |fragment|
|
|
107
|
+
next if fragment.blank?
|
|
92
108
|
|
|
93
|
-
|
|
94
|
-
# each level is prefixed with -
|
|
95
|
-
# level: internal recursive control
|
|
96
|
-
def cama_get_options_html_from_items(terms, level = 0)
|
|
97
|
-
options = []
|
|
98
|
-
terms.all.each do |term|
|
|
99
|
-
options << [('—' * level) + term.name, term.id] unless @term.id == term.id
|
|
100
|
-
children = term.children
|
|
101
|
-
options += cama_get_options_html_from_items(children, level + 1) unless children.empty?
|
|
109
|
+
fragment.is_a?(ActiveSupport::SafeBuffer) ? fragment : fragment.to_s.html_safe
|
|
102
110
|
end
|
|
103
|
-
|
|
111
|
+
# rubocop:enable Rails/OutputSafety
|
|
112
|
+
|
|
113
|
+
safe_join(trusted_fragments, "\n")
|
|
104
114
|
end
|
|
105
115
|
|
|
106
|
-
# create
|
|
116
|
+
# create an HTML tooltip to include anywhere
|
|
107
117
|
# text: text of the tooltip
|
|
108
118
|
# location: location of the tooltip (left | right | top |bottom)
|
|
109
119
|
def cama_html_tooltip(text = 'Tooltip', location = 'left')
|
|
110
|
-
|
|
120
|
+
tag.a(href: 'javascript:;', title: text, data: { toggle: 'tooltip', placement: location }) do
|
|
121
|
+
tag.i(class: 'fa fa-info-circle')
|
|
122
|
+
end
|
|
111
123
|
end
|
|
112
124
|
|
|
113
125
|
private
|
|
114
126
|
|
|
115
127
|
def cama_assets_libraries
|
|
116
|
-
|
|
128
|
+
state = camaleon_html_helper_state
|
|
129
|
+
return state[:cama_assets_libraries] if state[:cama_assets_libraries].present?
|
|
117
130
|
|
|
118
131
|
libs = {}
|
|
119
132
|
libs[:colorpicker] =
|
|
@@ -135,7 +148,15 @@ module CamaleonCms
|
|
|
135
148
|
js: %w[camaleon_cms/admin/jquery.nestable camaleon_cms/admin/nav_menu] }
|
|
136
149
|
libs[:admin_intro] =
|
|
137
150
|
{ js: ['camaleon_cms/admin/introjs/intro.min'], css: ['camaleon_cms/admin/introjs/introjs.min'] }
|
|
138
|
-
|
|
151
|
+
state[:cama_assets_libraries] = libs
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def camaleon_html_helper_state
|
|
155
|
+
state = CurrentRequest.html_helper_state ||= {}
|
|
156
|
+
state[:pre_assets_content] ||= []
|
|
157
|
+
state[:assets_libraries] ||= {}
|
|
158
|
+
state[:assets_content] ||= []
|
|
159
|
+
state
|
|
139
160
|
end
|
|
140
161
|
|
|
141
162
|
# execute translation for value if this value is like: t(admin.my_text) ==> My Text
|
|
@@ -2,21 +2,19 @@ module CamaleonCms
|
|
|
2
2
|
module PluginsHelper
|
|
3
3
|
include CamaleonCms::SiteHelper
|
|
4
4
|
# load all plugins + theme installed for current site
|
|
5
|
-
# METHOD IGNORED (is a partial solution to avoid
|
|
6
|
-
# this method
|
|
5
|
+
# METHOD IGNORED (is a partial solution to avoid loading helpers and cache it for all sites)
|
|
6
|
+
# this method tries to load helpers for each request without caching
|
|
7
7
|
def plugins_initialize(klass = nil)
|
|
8
8
|
mod = Module.new
|
|
9
9
|
PluginRoutes.enabled_apps(current_site).each do |plugin|
|
|
10
|
-
next if
|
|
10
|
+
next if plugin.blank? || plugin['helpers'].blank?
|
|
11
11
|
|
|
12
|
-
plugin['helpers'].each
|
|
13
|
-
mod.send :include, h.constantize
|
|
14
|
-
end
|
|
12
|
+
plugin['helpers'].each { |h| mod.send :include, h.constantize }
|
|
15
13
|
end
|
|
16
14
|
(klass || self).send :extend, mod
|
|
17
15
|
end
|
|
18
16
|
|
|
19
|
-
# upgrade installed plugin in current site for a new version
|
|
17
|
+
# upgrade the installed plugin in the current site for a new version
|
|
20
18
|
# plugin_key: key of the plugin
|
|
21
19
|
# trigger hook "on_upgrade"
|
|
22
20
|
# return model of the plugin
|
|
@@ -29,7 +27,7 @@ module CamaleonCms
|
|
|
29
27
|
plugin_model
|
|
30
28
|
end
|
|
31
29
|
|
|
32
|
-
# install a plugin for current site
|
|
30
|
+
# install a plugin for the current site
|
|
33
31
|
# plugin_key: key of the plugin
|
|
34
32
|
# return model of the plugin
|
|
35
33
|
def plugin_install(plugin_key)
|
|
@@ -50,7 +48,7 @@ module CamaleonCms
|
|
|
50
48
|
end
|
|
51
49
|
end
|
|
52
50
|
|
|
53
|
-
# uninstall a plugin from current site
|
|
51
|
+
# uninstall a plugin from the current site
|
|
54
52
|
# plugin_key: key of the plugin
|
|
55
53
|
# return model of the plugin
|
|
56
54
|
def plugin_uninstall(plugin_key)
|
|
@@ -66,7 +64,7 @@ module CamaleonCms
|
|
|
66
64
|
plugin_model
|
|
67
65
|
end
|
|
68
66
|
|
|
69
|
-
# remove a plugin from current site
|
|
67
|
+
# remove a plugin from the current site
|
|
70
68
|
# plugin_key: key of the plugin
|
|
71
69
|
# return model of the plugin removed
|
|
72
70
|
# DEPRECATED: PLUGINS AND THEMES CANNOT BE DESTROYED
|
|
@@ -105,8 +103,10 @@ module CamaleonCms
|
|
|
105
103
|
# return plugin full asset path
|
|
106
104
|
# plugin_key: plugin name
|
|
107
105
|
# asset: (String) asset name
|
|
108
|
-
# sample: <script src="<%= plugin_asset_path("admin.js") %>"></script> =>
|
|
109
|
-
#
|
|
106
|
+
# sample: <script src="<%= plugin_asset_path("admin.js") %>"></script> =>
|
|
107
|
+
# /assets/plugins/my_plugin/admin-54505620f.js
|
|
108
|
+
# sample: <script src="<%= plugin_asset_path("admin.js", 'my_plugin') %>"></script> =>
|
|
109
|
+
# /assets/plugins/my_plugin/admin-54505620f.js
|
|
110
110
|
def plugin_asset_path(asset, plugin_key = nil)
|
|
111
111
|
if plugin_key.present? && plugin_key.include?('/')
|
|
112
112
|
return plugin_asset_url(plugin_key,
|
|
@@ -123,11 +123,12 @@ module CamaleonCms
|
|
|
123
123
|
alias plugin_asset plugin_asset_path
|
|
124
124
|
alias plugin_gem_asset plugin_asset_path
|
|
125
125
|
|
|
126
|
-
# return the full url for asset of current plugin:
|
|
126
|
+
# return the full url for asset of the current plugin:
|
|
127
127
|
# asset: (String) asset name
|
|
128
128
|
# plugin_key: (optional) plugin name, default (current plugin caller to this function)
|
|
129
129
|
# sample:
|
|
130
|
-
# plugin_asset_url("css/main.css") =>
|
|
130
|
+
# plugin_asset_url("css/main.css") =>
|
|
131
|
+
# return: https://myhost.com/assets/plugins/my_plugin/assets/css/main-54505620f.css
|
|
131
132
|
def plugin_asset_url(asset, plugin_key = nil)
|
|
132
133
|
key = plugin_key || self_plugin_key(1)
|
|
133
134
|
p = PluginRoutes.plugin_info(key)['gem_mode'] ? "plugins/#{key}/#{asset}" : "plugins/#{key}/assets/#{asset}"
|
|
@@ -138,22 +139,22 @@ module CamaleonCms
|
|
|
138
139
|
end
|
|
139
140
|
end
|
|
140
141
|
|
|
141
|
-
#
|
|
142
|
+
# Autoload all helpers of this plugin
|
|
142
143
|
def plugin_load_helpers(plugin)
|
|
143
|
-
return if
|
|
144
|
+
return if plugin.blank? || plugin['helpers'].blank?
|
|
144
145
|
|
|
145
146
|
plugin['helpers'].each do |h|
|
|
146
147
|
next if self.class.include?(h.constantize)
|
|
147
148
|
|
|
148
|
-
class_eval
|
|
149
|
-
include h.constantize
|
|
150
|
-
end
|
|
149
|
+
class_eval { include h.constantize }
|
|
151
150
|
rescue StandardError => e
|
|
152
|
-
Rails.logger.debug
|
|
151
|
+
Rails.logger.debug do
|
|
152
|
+
"Camaleon CMS - App loading error for #{h}: #{e.message}. Please check the plugins and themes presence"
|
|
153
|
+
end
|
|
153
154
|
end
|
|
154
155
|
end
|
|
155
156
|
|
|
156
|
-
# return plugin key for current plugin file (helper|controller|view)
|
|
157
|
+
# return the plugin key for the current plugin file (helper|controller|view)
|
|
157
158
|
# index: internal control (ignored)
|
|
158
159
|
def self_plugin_key(index = 0)
|
|
159
160
|
f = caller[index]
|
|
@@ -175,9 +176,7 @@ module CamaleonCms
|
|
|
175
176
|
# return the plugin model for current site calculated according to the file caller location
|
|
176
177
|
def current_plugin(plugin_key = nil)
|
|
177
178
|
_key = plugin_key || self_plugin_key(1)
|
|
178
|
-
cama_cache_fetch("current_plugin_#{_key}")
|
|
179
|
-
current_site.get_plugin(_key)
|
|
180
|
-
end
|
|
179
|
+
cama_cache_fetch("current_plugin_#{_key}") { current_site.get_plugin(_key) }
|
|
181
180
|
end
|
|
182
181
|
end
|
|
183
182
|
end
|
|
@@ -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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
user = current_site.users.new(user_data)
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
|
193
|
+
|
|
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,18 +236,93 @@ module CamaleonCms
|
|
|
167
236
|
|
|
168
237
|
private
|
|
169
238
|
|
|
170
|
-
#
|
|
171
|
-
|
|
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)
|
|
172
254
|
return if url.blank?
|
|
173
255
|
|
|
174
256
|
uri = URI.parse(url)
|
|
175
|
-
|
|
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)
|
|
176
281
|
|
|
177
282
|
url
|
|
178
283
|
rescue URI::InvalidURIError
|
|
179
284
|
nil
|
|
180
285
|
end
|
|
181
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
|
+
|
|
182
326
|
# calculate the current user for API
|
|
183
327
|
def cama_calc_api_current_user
|
|
184
328
|
begin
|
|
@@ -190,7 +334,7 @@ module CamaleonCms
|
|
|
190
334
|
end
|
|
191
335
|
return unless doorkeeper_token
|
|
192
336
|
|
|
193
|
-
current_site.users_include_admins.
|
|
337
|
+
current_site.users_include_admins.find_by(id: doorkeeper_token.resource_owner_id).try(:decorate)
|
|
194
338
|
end
|
|
195
339
|
end
|
|
196
340
|
end
|