camaleon_cms 2.9.2 → 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 +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 +15 -10
- data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
- data/app/apps/plugins/front_cache/front_cache_helper.rb +33 -9
- 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 +76 -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 +140 -70
- 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 +66 -50
- 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 +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/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 +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 +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 +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 -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 +52 -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 +66 -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 +20 -38
- data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
- 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 +33 -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 +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 +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 +123 -4
|
@@ -61,7 +61,8 @@ module CamaleonCms
|
|
|
61
61
|
# reorder custom fields group
|
|
62
62
|
def reorder
|
|
63
63
|
params[:values].to_a.each_with_index do |value, index|
|
|
64
|
-
current_site.custom_field_groups.find(value)
|
|
64
|
+
current_site.custom_field_groups.find(value)
|
|
65
|
+
.update_column(:field_order, index) # rubocop:disable Rails/SkipsModelValidations
|
|
65
66
|
end
|
|
66
67
|
json = { size: params[:values].size }
|
|
67
68
|
render json: json
|
|
@@ -71,11 +72,26 @@ module CamaleonCms
|
|
|
71
72
|
p = params.permit(:post_type, :post_id)
|
|
72
73
|
cat_ids = current_site.full_categories.where(id: params[:categories]).pluck(:id)
|
|
73
74
|
if p[:post_id].present? && (post = current_site.the_post(p[:post_id].to_i)).present?
|
|
74
|
-
post
|
|
75
|
+
# The action carries no before_action, so authorize against the resolved post: a caller who
|
|
76
|
+
# cannot update it may neither read its custom-field values (GET) nor rewrite its categories
|
|
77
|
+
# (POST). This scopes an otherwise any-signed-in-user endpoint to the post's own editors.
|
|
78
|
+
authorize! :update, post
|
|
79
|
+
# The category write is state-changing, so it runs only on a POST — the sole verb on this
|
|
80
|
+
# route that protect_from_forgery verifies, because Rails exempts HEAD from CSRF checks
|
|
81
|
+
# just like GET (an `unless request.get?` guard would let a CSRF-exempt HEAD through). A GET
|
|
82
|
+
# or HEAD renders the current fields without mutating the post. Otherwise a bare
|
|
83
|
+
# GET .../custom_fields/list?post_id=N (CSRF through a top-level navigation, which carries
|
|
84
|
+
# the SameSite=Lax auth cookie) wipes the post's categories, because an omitted `categories`
|
|
85
|
+
# param resolves to [] and update_categories then deletes them all (audit finding M6).
|
|
86
|
+
post.update_categories(cat_ids) if request.post?
|
|
75
87
|
args = {}
|
|
76
88
|
else
|
|
89
|
+
# The render-only branch builds a new post of the requested type; gate it on the ability to
|
|
90
|
+
# create posts of that type so the field-group structure is not disclosed to other roles.
|
|
91
|
+
post_type = current_site.the_post_type(p[:post_type].to_i)
|
|
92
|
+
authorize! :create_post, post_type if post_type
|
|
77
93
|
post = CamaleonCms::Post.new
|
|
78
|
-
post.taxonomy_id =
|
|
94
|
+
post.taxonomy_id = post_type&.id
|
|
79
95
|
args = { cat_ids: cat_ids }
|
|
80
96
|
end
|
|
81
97
|
render partial: 'camaleon_cms/admin/settings/custom_fields/render',
|
|
@@ -88,7 +104,52 @@ module CamaleonCms
|
|
|
88
104
|
def set_post_data
|
|
89
105
|
@post_data = params.require(:custom_field_group).permit(:name, :description, :assign_group, :caption)
|
|
90
106
|
@post_data[:object_class], @post_data[:objectid] = @post_data.delete(:assign_group).to_s.split(',')
|
|
107
|
+
# A Site placement has exactly one legal target, and the form only ever offers
|
|
108
|
+
# "Site,<current_site.id>". Pinning it keeps a crafted objectid from producing a group that
|
|
109
|
+
# Site#get_field_groups can never return, which would leave it stranded and unreachable.
|
|
110
|
+
@post_data[:objectid] = current_site.id if @post_data[:object_class] == 'Site'
|
|
91
111
|
@caption = @post_data.delete(:caption)
|
|
112
|
+
reject_foreign_placement unless placement_owned_by_current_site?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A group's placement (object_class + objectid) decides which record's admin page renders it,
|
|
116
|
+
# and those reads are not scoped by the owning site. Without this check a user who can manage
|
|
117
|
+
# custom fields on one site could stamp a group with another site's theme, menu or post type
|
|
118
|
+
# id and have it render there -- invisible in that site's own field group list, which is
|
|
119
|
+
# scoped by parent_id, so its administrators could neither find nor delete it.
|
|
120
|
+
def placement_owned_by_current_site?
|
|
121
|
+
object_class = @post_data[:object_class].to_s
|
|
122
|
+
objectid = @post_data[:objectid].to_s
|
|
123
|
+
return false if object_class.blank? || objectid !~ /\A\d+\z/
|
|
124
|
+
|
|
125
|
+
owned_placement_ids(object_class).include?(objectid.to_i)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Every "other" option the form emits is "<Class>,<current_site.id>" -- Site, the configured
|
|
129
|
+
# user model, and any model contributed through the custom_field_custom_models hook. Keying
|
|
130
|
+
# the default arm on the id rather than on an allow-list of class names admits those hook
|
|
131
|
+
# models without this controller having to know their names.
|
|
132
|
+
def owned_placement_ids(object_class)
|
|
133
|
+
case object_class
|
|
134
|
+
when 'PostType', 'PostType_Post', 'PostType_Category', 'PostType_PostTag'
|
|
135
|
+
current_site.post_types.pluck(:id)
|
|
136
|
+
when 'Theme' then current_site.themes.pluck(:id)
|
|
137
|
+
when 'NavMenu' then current_site.nav_menus.pluck(:id)
|
|
138
|
+
when 'Plugin' then current_site.plugins.pluck(:id)
|
|
139
|
+
when 'Post' then current_site.posts.pluck(:id)
|
|
140
|
+
when 'Category', 'Category_Post' then current_site.full_categories.pluck(:id)
|
|
141
|
+
else [current_site.id]
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def reject_foreign_placement
|
|
146
|
+
@field_group ||= current_site.custom_field_groups.new(@post_data.except(:object_class, :objectid))
|
|
147
|
+
@field_group.errors.add(
|
|
148
|
+
:base,
|
|
149
|
+
t('camaleon_cms.admin.custom_field.message.invalid_placement',
|
|
150
|
+
default: 'Select where to display this group. The selected target does not belong to this site.')
|
|
151
|
+
)
|
|
152
|
+
render 'form'
|
|
92
153
|
end
|
|
93
154
|
|
|
94
155
|
def validate_role
|
|
@@ -103,7 +164,7 @@ module CamaleonCms
|
|
|
103
164
|
end
|
|
104
165
|
|
|
105
166
|
def permitted_fields
|
|
106
|
-
return {}
|
|
167
|
+
return {} if params[:fields].blank?
|
|
107
168
|
|
|
108
169
|
params.require(:fields).permit(params[:fields].keys.index_with do
|
|
109
170
|
%i[id name slug description field_order]
|
|
@@ -111,11 +172,11 @@ module CamaleonCms
|
|
|
111
172
|
end
|
|
112
173
|
|
|
113
174
|
def permitted_field_options
|
|
114
|
-
return {}
|
|
175
|
+
return {} if params[:field_options].blank?
|
|
115
176
|
|
|
116
177
|
params.require(:field_options).permit(params[:field_options].keys.index_with do
|
|
117
|
-
[:field_key, :multiple, :required, :translate, :default_value, :dimension, :width, :height, :class,
|
|
118
|
-
{ default_values: [], multiple_options: %i[title value default] }]
|
|
178
|
+
[:field_key, :multiple, :required, :translate, :default_value, :dimension, :width, :height, :class,
|
|
179
|
+
:placeholder, { default_values: [], multiple_options: %i[title value default] }]
|
|
119
180
|
end).to_h
|
|
120
181
|
end
|
|
121
182
|
|
|
@@ -124,15 +185,17 @@ module CamaleonCms
|
|
|
124
185
|
errors_saved, _all_fields = group.add_fields(permitted_fields, permitted_field_options)
|
|
125
186
|
group.set_option('caption', @caption)
|
|
126
187
|
if errors_saved.present?
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
188
|
+
errors_found_msg = t('camaleon_cms.errors_found_msg', default: 'Several errors were found, please check.')
|
|
189
|
+
errors_saved_all_text = errors_saved.map do |field|
|
|
190
|
+
"#{field.name}: " + field.errors.messages.map do |k, v|
|
|
191
|
+
"#{k.to_s.titleize}: #{v.join('|')}"
|
|
192
|
+
end.join(', ').to_s
|
|
193
|
+
end.join('<br>')
|
|
194
|
+
flash[:error] = "<b>#{errors_found_msg}</b><br>#{errors_saved_all_text}"
|
|
195
|
+
false
|
|
132
196
|
else
|
|
133
197
|
flash[:notice] = t('camaleon_cms.admin.custom_field.message.custom_updated')
|
|
134
198
|
end
|
|
135
|
-
true
|
|
136
199
|
end
|
|
137
200
|
end
|
|
138
201
|
end
|
|
@@ -50,7 +50,10 @@ module CamaleonCms
|
|
|
50
50
|
private
|
|
51
51
|
|
|
52
52
|
def set_data_term
|
|
53
|
-
|
|
53
|
+
# parent_id is the post type's site_id (alias_attribute). It is set from the site association
|
|
54
|
+
# on create and must never be reassigned, so it is not accepted from the request; otherwise a
|
|
55
|
+
# settings manager could re-home the post type onto another site (audit finding H8).
|
|
56
|
+
data_term = params.require(:post_type).permit(:name, :slug, :description)
|
|
54
57
|
data_term[:data_options] = params[:meta].present? ? post_type_meta_params : {}
|
|
55
58
|
@data_term = data_term
|
|
56
59
|
end
|
|
@@ -64,7 +67,7 @@ module CamaleonCms
|
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
def set_post_type
|
|
67
|
-
@post_type = current_site.post_types.
|
|
70
|
+
@post_type = current_site.post_types.find_by(id: params[:id])
|
|
68
71
|
rescue StandardError
|
|
69
72
|
flash[:error] = t('camaleon_cms.admin.post_type.message.error')
|
|
70
73
|
redirect_to cama_admin_path
|
|
@@ -47,7 +47,7 @@ module CamaleonCms
|
|
|
47
47
|
if @site.save
|
|
48
48
|
save_metas(@site)
|
|
49
49
|
site_after_install(@site, @site.get_theme_slug)
|
|
50
|
-
flash[:notice] =
|
|
50
|
+
flash[:notice] = site_created_notice(@site)
|
|
51
51
|
redirect_to action: :index
|
|
52
52
|
else
|
|
53
53
|
new
|
|
@@ -62,7 +62,7 @@ module CamaleonCms
|
|
|
62
62
|
private
|
|
63
63
|
|
|
64
64
|
def save_metas(site)
|
|
65
|
-
return
|
|
65
|
+
return if params[:metas].blank?
|
|
66
66
|
|
|
67
67
|
params[:metas].each do |meta, val|
|
|
68
68
|
site.set_meta(meta, val)
|
|
@@ -70,7 +70,7 @@ module CamaleonCms
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def set_site
|
|
73
|
-
@site = CamaleonCms::Site.
|
|
73
|
+
@site = CamaleonCms::Site.find_by(id: params[:id]).decorate
|
|
74
74
|
rescue StandardError
|
|
75
75
|
flash[:error] = t('camaleon_cms.admin.sites.message.error')
|
|
76
76
|
redirect_to cama_admin_path
|
|
@@ -87,6 +87,29 @@ module CamaleonCms
|
|
|
87
87
|
def site_params
|
|
88
88
|
params.require(:site).permit(:name, :slug, :description)
|
|
89
89
|
end
|
|
90
|
+
|
|
91
|
+
# When creating an additional site provisions a fresh administrator (only when users are not
|
|
92
|
+
# shared across sites), surface its generated password once to the authenticated creator, with
|
|
93
|
+
# a copy control. Otherwise a plain confirmation. See harden-installer-default-admin.
|
|
94
|
+
def site_created_notice(site)
|
|
95
|
+
password = site.generated_admin_password
|
|
96
|
+
return t('camaleon_cms.admin.sites.message.created') if password.blank?
|
|
97
|
+
|
|
98
|
+
# rubocop:disable Style/FormatStringToken -- Rails i18n interpolation requires %{...} syntax
|
|
99
|
+
message = t(
|
|
100
|
+
'camaleon_cms.admin.sites.message.created_with_admin',
|
|
101
|
+
password: password,
|
|
102
|
+
default: 'Site created. New administrator password: %{password}. Change it after signing in.'
|
|
103
|
+
)
|
|
104
|
+
# rubocop:enable Style/FormatStringToken
|
|
105
|
+
copy = helpers.tag.button(
|
|
106
|
+
t('camaleon_cms.admin.installer.copy', default: 'Copy'),
|
|
107
|
+
type: 'button',
|
|
108
|
+
class: 'btn btn-xs btn-default',
|
|
109
|
+
onclick: "navigator.clipboard&&navigator.clipboard.writeText('#{password}')"
|
|
110
|
+
)
|
|
111
|
+
helpers.safe_join([message, ' ', copy])
|
|
112
|
+
end
|
|
90
113
|
end
|
|
91
114
|
end
|
|
92
115
|
end
|
|
@@ -62,11 +62,13 @@ module CamaleonCms
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def save_theme
|
|
65
|
-
current_theme.set_field_values(
|
|
65
|
+
current_theme.set_field_values(cama_permitted_field_options('Theme', param_key: :theme_fields))
|
|
66
66
|
current_theme.set_options(params[:theme_option]) if params[:theme_option].present?
|
|
67
67
|
current_theme.set_metas(params[:theme_meta]) if params[:theme_meta].present?
|
|
68
68
|
current_theme.set_field_values(cama_permitted_field_options('Theme'))
|
|
69
|
-
|
|
69
|
+
|
|
70
|
+
# permit saving extra/custom values by this hook
|
|
71
|
+
hook_run(current_theme.settings, 'on_theme_settings', current_theme)
|
|
70
72
|
flash[:notice] = t('camaleon_cms.admin.message.updated_success', default: 'Theme updated successfully')
|
|
71
73
|
redirect_to action: :theme
|
|
72
74
|
end
|
|
@@ -77,7 +79,7 @@ module CamaleonCms
|
|
|
77
79
|
CamaleonCms::HtmlMailer.sender(params[:email], 'Test', data).deliver_now
|
|
78
80
|
head :ok
|
|
79
81
|
rescue StandardError => e
|
|
80
|
-
render plain: e.message, status:
|
|
82
|
+
render plain: e.message, status: :bad_gateway
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
private
|
|
@@ -23,10 +23,12 @@ module CamaleonCms
|
|
|
23
23
|
user_role_data = params.require(:user_role).permit(:name, :slug, :description)
|
|
24
24
|
@user_role = current_site.user_roles.new(user_role_data)
|
|
25
25
|
if @user_role.save
|
|
26
|
-
@user_role.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
if @user_role.permissions_editable?
|
|
27
|
+
@user_role.set_meta("_post_type_#{current_site.id}",
|
|
28
|
+
defined?(params[:rol_values][:post_type]) ? params[:rol_values][:post_type] : {})
|
|
29
|
+
@user_role.set_meta("_manager_#{current_site.id}",
|
|
30
|
+
defined?(params[:rol_values][:post_type]) ? params[:rol_values][:manager] : {})
|
|
31
|
+
end
|
|
30
32
|
flash[:notice] = t('camaleon_cms.admin.users.message.rol_created')
|
|
31
33
|
redirect_to action: :edit, id: @user_role.id
|
|
32
34
|
else
|
|
@@ -41,7 +43,7 @@ module CamaleonCms
|
|
|
41
43
|
|
|
42
44
|
def update
|
|
43
45
|
if @user_role.update(params.require(:user_role).permit(:name, :slug, :description))
|
|
44
|
-
if @user_role.
|
|
46
|
+
if @user_role.permissions_editable?
|
|
45
47
|
@user_role.set_meta("_post_type_#{current_site.id}",
|
|
46
48
|
defined?(params[:rol_values][:post_type]) ? params[:rol_values][:post_type] : {})
|
|
47
49
|
@user_role.set_meta("_manager_#{current_site.id}",
|
|
@@ -3,6 +3,11 @@ module CamaleonCms
|
|
|
3
3
|
class UsersController < CamaleonCms::AdminController
|
|
4
4
|
include CamaleonCms::Admin::CustomFieldsConcern
|
|
5
5
|
|
|
6
|
+
# Member actions resolve a single target user, so a caller acting on their own record is
|
|
7
|
+
# legitimately exempt from :manage, :users. The collection actions (index/new/create) resolve
|
|
8
|
+
# no such target, so a self-referential ?user_id= must not exempt them from the capability check.
|
|
9
|
+
SELF_TARGET_ACTIONS = %w[show edit update destroy impersonate updated_ajax].freeze
|
|
10
|
+
|
|
6
11
|
before_action :validate_role, except: %i[profile profile_edit]
|
|
7
12
|
|
|
8
13
|
add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.users'), :cama_admin_users_url
|
|
@@ -16,7 +21,16 @@ module CamaleonCms
|
|
|
16
21
|
|
|
17
22
|
def profile
|
|
18
23
|
add_breadcrumb I18n.t('camaleon_cms.admin.users.profile')
|
|
19
|
-
|
|
24
|
+
user_id = params[:user_id]
|
|
25
|
+
# Authorize from the parameter before loading, so a denied caller cannot tell
|
|
26
|
+
# whether the requested user exists from the shape of the response.
|
|
27
|
+
authorize! :manage, :users if user_id.present? && user_id.to_i != cama_current_user.id
|
|
28
|
+
@user = user_id.present? ? current_site.the_user(user_id.to_i)&.object : cama_current_user.object
|
|
29
|
+
if @user.blank?
|
|
30
|
+
flash[:error] = t('camaleon_cms.admin.users.message.error')
|
|
31
|
+
return redirect_to(cama_admin_path)
|
|
32
|
+
end
|
|
33
|
+
|
|
20
34
|
edit
|
|
21
35
|
end
|
|
22
36
|
|
|
@@ -36,7 +50,7 @@ module CamaleonCms
|
|
|
36
50
|
hooks_run('user_update', r)
|
|
37
51
|
if @user.update(user_params)
|
|
38
52
|
@user.set_metas(user_meta_params) if params[:meta].present?
|
|
39
|
-
@user.set_field_values(cama_permitted_field_options(
|
|
53
|
+
@user.set_field_values(cama_permitted_field_options(user_field_scope)) if params[:field_options].present?
|
|
40
54
|
r = { user: @user, message: t('camaleon_cms.admin.users.message.updated'), params: params }
|
|
41
55
|
hooks_run('user_after_edited', r)
|
|
42
56
|
flash[:notice] = r[:message]
|
|
@@ -54,17 +68,33 @@ module CamaleonCms
|
|
|
54
68
|
|
|
55
69
|
# update some ajax requests from profile or user form
|
|
56
70
|
def updated_ajax
|
|
57
|
-
@user = current_site.users.find(
|
|
71
|
+
@user = current_site.users.find(user_id_param)
|
|
72
|
+
# Only an admin may reset an admin's password; a `:manage, :users` holder who could would sign in
|
|
73
|
+
# as that admin (H10). Self-service and non-admin targets are unaffected.
|
|
74
|
+
unless cama_current_user.may_edit_credentials?(@user)
|
|
75
|
+
return render plain: t('camaleon_cms.admin.users.message.error'), status: :forbidden
|
|
76
|
+
end
|
|
77
|
+
|
|
58
78
|
update_session = current_user_is?(@user)
|
|
59
79
|
attrs = params.require(:password).permit(%i[password password_confirmation])
|
|
60
80
|
@user.update(password: attrs.require(:password), password_confirmation: attrs.require(:password_confirmation))
|
|
61
81
|
|
|
62
|
-
return render
|
|
82
|
+
return render plain: @user.errors.full_messages.join(', '), status: :unprocessable_entity if @user.errors.any?
|
|
83
|
+
|
|
84
|
+
# A newly provisioned admin (see harden-installer-default-admin) owes a password change; clearing
|
|
85
|
+
# the marker on a real change lets them past the enforce_password_change gate.
|
|
86
|
+
@user.delete_meta('must_change_password') if @user.saved_change_to_password_digest?
|
|
63
87
|
|
|
64
88
|
# keep user logged in when changing their own password
|
|
65
89
|
update_auth_token_in_cookie @user.auth_token if update_session && @user.saved_change_to_password_digest?
|
|
90
|
+
rescue ActiveRecord::RecordNotFound
|
|
91
|
+
# The other failure paths of this action answer with a status and a short text body, so an
|
|
92
|
+
# unresolvable target does too rather than falling through to the framework's HTML error
|
|
93
|
+
# page. Catch this class only, never StandardError, so a genuine lookup failure still
|
|
94
|
+
# surfaces instead of being reported as a missing user.
|
|
95
|
+
render plain: t('camaleon_cms.admin.users.message.error'), status: :not_found
|
|
66
96
|
rescue ActionController::ParameterMissing => e
|
|
67
|
-
render
|
|
97
|
+
render plain: "ERROR: #{e.class.name}, #{e.message}", status: :bad_request
|
|
68
98
|
end
|
|
69
99
|
|
|
70
100
|
def update_auth_token_in_cookie(token)
|
|
@@ -72,7 +102,10 @@ module CamaleonCms
|
|
|
72
102
|
|
|
73
103
|
current_token = cookie_split_auth_token
|
|
74
104
|
updated_token = [token, *current_token[1..]]
|
|
75
|
-
|
|
105
|
+
# Route through the shared hardened-cookie options (audit M3/M4): a bare assignment here
|
|
106
|
+
# re-issued the auth cookie without HttpOnly/Secure/domain/expiry when a user changed their
|
|
107
|
+
# own password, undoing the M3 hardening for that session.
|
|
108
|
+
cookies[:auth_token] = cama_auth_cookie_options(updated_token.join('&'))
|
|
76
109
|
end
|
|
77
110
|
|
|
78
111
|
def current_user_is?(user)
|
|
@@ -102,7 +135,7 @@ module CamaleonCms
|
|
|
102
135
|
hooks_run('user_create', r)
|
|
103
136
|
if @user.save
|
|
104
137
|
@user.set_metas(user_meta_params) if params[:meta].present?
|
|
105
|
-
@user.set_field_values(cama_permitted_field_options(
|
|
138
|
+
@user.set_field_values(cama_permitted_field_options(user_field_scope)) if params[:field_options].present?
|
|
106
139
|
r = { user: @user }
|
|
107
140
|
hooks_run('user_created', r)
|
|
108
141
|
flash[:notice] = t('camaleon_cms.admin.users.message.created')
|
|
@@ -133,16 +166,43 @@ module CamaleonCms
|
|
|
133
166
|
private
|
|
134
167
|
|
|
135
168
|
def validate_role
|
|
136
|
-
|
|
169
|
+
return if self_target_own_record?
|
|
170
|
+
|
|
171
|
+
authorize! :manage, :users
|
|
137
172
|
end
|
|
138
173
|
|
|
174
|
+
# The self-exemption applies only to member actions that resolve a single target user; a
|
|
175
|
+
# collection action (index/new/create) has no such target, so a self-referential ?user_id=
|
|
176
|
+
# never exempts it (audit finding H7).
|
|
177
|
+
def self_target_own_record?
|
|
178
|
+
return false unless SELF_TARGET_ACTIONS.include?(action_name)
|
|
179
|
+
|
|
180
|
+
user_id = user_id_param
|
|
181
|
+
user_id.present? && cama_current_user.id.to_s == user_id.to_s
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Only a scalar user_id participates in target resolution (?user_id[]= would
|
|
185
|
+
# crash the record lookup and flip authorization); otherwise the route id wins
|
|
139
186
|
def user_id_param
|
|
140
|
-
|
|
187
|
+
user_id = params[:user_id]
|
|
188
|
+
return params[:id] unless user_id.is_a?(String)
|
|
189
|
+
|
|
190
|
+
user_id
|
|
141
191
|
end
|
|
142
192
|
|
|
143
193
|
def user_params
|
|
144
|
-
|
|
145
|
-
|
|
194
|
+
fields = %i[username email first_name last_name password password_confirmation]
|
|
195
|
+
# Only an admin may change an admin's password or recovery identifiers (email/username); drop them
|
|
196
|
+
# for anyone else editing an admin, the same fail-safe the role permit below uses (H10).
|
|
197
|
+
unless cama_current_user.may_edit_credentials?(@user)
|
|
198
|
+
fields -= %i[username email password password_confirmation]
|
|
199
|
+
end
|
|
200
|
+
p = params.require(:user).permit(*fields)
|
|
201
|
+
# role is a scalar slug; coerce anything else (e.g. a nested user[role][x] param) to nil so it is
|
|
202
|
+
# never mass-assigned — assigning an unpermitted nested Parameters would raise and 500 the request.
|
|
203
|
+
requested_role = params[:user][:role]
|
|
204
|
+
requested_role = nil unless requested_role.is_a?(String)
|
|
205
|
+
p[:role] = requested_role if requested_role.present? && cama_current_user.role_grantor?(@user, requested_role)
|
|
146
206
|
p
|
|
147
207
|
end
|
|
148
208
|
|
|
@@ -150,6 +210,13 @@ module CamaleonCms
|
|
|
150
210
|
params.require(:meta).permit(:avatar, :slogan)
|
|
151
211
|
end
|
|
152
212
|
|
|
213
|
+
# Allowed field slugs must be keyed on the demodulized placement name the settings form
|
|
214
|
+
# emits and get_user_field_groups queries; keyed on a hardcoded 'User' the lookup finds no
|
|
215
|
+
# groups for a host user model that demodulizes to another name, discarding every value.
|
|
216
|
+
def user_field_scope
|
|
217
|
+
PluginRoutes.get_user_class_name.demodulize
|
|
218
|
+
end
|
|
219
|
+
|
|
153
220
|
def set_user
|
|
154
221
|
@user = current_site.users.find(user_id_param)
|
|
155
222
|
rescue StandardError
|
|
@@ -4,11 +4,25 @@ module CamaleonCms
|
|
|
4
4
|
flash[:error] = "Error: #{exception.message}"
|
|
5
5
|
redirect_to cama_admin_dashboard_path
|
|
6
6
|
end
|
|
7
|
-
|
|
7
|
+
# Security (scan-and-reject policy, audit M17): custom-field values save after their parent
|
|
8
|
+
# through `custom_field_values.create!`, so a value the gate refuses arrives here as
|
|
9
|
+
# RecordInvalid. Surface the refusal as a flash error naming the field instead of a 500; the
|
|
10
|
+
# parent's own attributes were already saved, only the refused value rows are rolled back.
|
|
11
|
+
rescue_from ActiveRecord::RecordInvalid do |exception|
|
|
12
|
+
raise exception unless exception.record.is_a?(CamaleonCms::CustomFieldsRelationship)
|
|
13
|
+
|
|
14
|
+
flash[:error] = exception.record.errors.full_messages.to_sentence
|
|
15
|
+
redirect_back fallback_location: cama_admin_dashboard_path
|
|
16
|
+
end
|
|
8
17
|
# layout 'camaleon_cms/admin'
|
|
18
|
+
# Admin responses are user-specific and must never be served from a browser or shared cache;
|
|
19
|
+
# without this a stale render (e.g. an editor that failed to initialize on a cold boot) can
|
|
20
|
+
# reappear after a restart until a manual reload. Set first so it applies to auth redirects too.
|
|
21
|
+
before_action :cama_prevent_response_caching
|
|
9
22
|
before_action :cama_authenticate
|
|
10
23
|
before_action :keep_request_attrs
|
|
11
24
|
before_action :admin_init_actions
|
|
25
|
+
before_action :enforce_password_change
|
|
12
26
|
before_action :admin_logged_actions
|
|
13
27
|
before_action :admin_before_hooks
|
|
14
28
|
after_action :admin_after_hooks
|
|
@@ -16,6 +30,7 @@ module CamaleonCms
|
|
|
16
30
|
params[:cama_ajax_request].present? ? 'camaleon_cms/admin/_ajax' : 'camaleon_cms/admin'
|
|
17
31
|
}
|
|
18
32
|
add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.dashboard', default: 'Dashboard'), :cama_admin_path
|
|
33
|
+
helper_method :cama_get_i18n_frontend
|
|
19
34
|
|
|
20
35
|
# render admin dashboard
|
|
21
36
|
def index
|
|
@@ -42,46 +57,95 @@ module CamaleonCms
|
|
|
42
57
|
# render search results
|
|
43
58
|
# receive params[:q]
|
|
44
59
|
# receive params[:kind]: define de type of the results type (content|category|tag) => default content
|
|
45
|
-
# if this is
|
|
60
|
+
# if this is receiving a param[:ajax], then will render only results view
|
|
46
61
|
def search
|
|
47
62
|
add_breadcrumb I18n.t('camaleon_cms.admin.button.search')
|
|
48
|
-
params[:kind] = 'content'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
params[:kind] = 'content' if params[:kind].blank?
|
|
64
|
+
# A crafted ?q[]=x / ?q[a]=b arrives as an Array / Parameters, which has no #downcase; treat any
|
|
65
|
+
# non-String q as an empty query instead of 500ing.
|
|
66
|
+
params[:q] = params[:q].is_a?(String) ? params[:q].downcase : ''
|
|
67
|
+
# Security (audit 2026-08-11 M12): the action had no authorization and no status filter, so any
|
|
68
|
+
# admin-area user (e.g. a client with no content rights) could enumerate every post title/slug in
|
|
69
|
+
# every status -- draft, pending, private, trash -- plus post types, categories and tags they
|
|
70
|
+
# cannot access. Scope every kind to the post types the caller may manage: content and post types
|
|
71
|
+
# by :posts, categories and tags by their own :categories / :post_tags abilities (the roles UI
|
|
72
|
+
# grants manage_categories / manage_tags independently of any post edit right).
|
|
73
|
+
table_name = case params[:kind]
|
|
74
|
+
when 'post_type'
|
|
75
|
+
base_query = current_site.post_types.where(id: cama_admin_searchable_post_type_ids)
|
|
76
|
+
Cama::PostType.table_name
|
|
77
|
+
when 'category'
|
|
78
|
+
pt_ids = cama_admin_searchable_post_type_ids(:categories)
|
|
79
|
+
# A category's post type lives in post_type_id (the status column) at every nesting
|
|
80
|
+
# level; parent_id points at the parent *category* for children, so filtering by it
|
|
81
|
+
# would drop every nested category.
|
|
82
|
+
base_query = current_site.full_categories.where(post_type_id: pt_ids)
|
|
83
|
+
Cama::Category.table_name
|
|
84
|
+
when 'tag'
|
|
85
|
+
pt_ids = cama_admin_searchable_post_type_ids(:post_tags)
|
|
86
|
+
base_query = current_site.post_tags.where(parent_id: pt_ids)
|
|
87
|
+
Cama::PostTag.table_name
|
|
88
|
+
else
|
|
89
|
+
base_query = cama_admin_searchable_posts
|
|
90
|
+
Cama::Post.table_name
|
|
91
|
+
end
|
|
92
|
+
@items = base_query.where(
|
|
93
|
+
items_sql_by_name(table_name), "%#{params[:q]}%", "%#{params[:q]}%", "%#{params[:q]}%"
|
|
94
|
+
)
|
|
95
|
+
|
|
68
96
|
@items = @items.paginate(page: params[:page], per_page: current_site.admin_per_page)
|
|
69
97
|
end
|
|
70
98
|
|
|
99
|
+
# Decorators use this helper while rendering admin pages to keep public URLs in
|
|
100
|
+
# the site's frontend language instead of the admin interface language.
|
|
101
|
+
def cama_get_i18n_frontend
|
|
102
|
+
@cama_i18n_frontend
|
|
103
|
+
end
|
|
104
|
+
|
|
71
105
|
private
|
|
72
106
|
|
|
107
|
+
# Send `Cache-Control: no-store` on every admin response so no admin page is ever cached by the
|
|
108
|
+
# browser or an intermediary. Admin pages are per-user and dynamic; caching them risks a stale
|
|
109
|
+
# render being reused (the cold-boot blank-editor symptom is one such case).
|
|
110
|
+
def cama_prevent_response_caching
|
|
111
|
+
response.headers['Cache-Control'] = 'no-store'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Actions reachable while an administrator still owes a password change: the profile screen, its
|
|
115
|
+
# submit, and the AJAX password-change endpoint. Everything else in the admin panel is redirected
|
|
116
|
+
# to the change-password screen until the marker is cleared. Sign-out lives on SessionsController
|
|
117
|
+
# (not an AdminController subclass), so it is out of this gate's reach and always reachable.
|
|
118
|
+
PASSWORD_CHANGE_EXEMPT = { 'camaleon_cms/admin/users' => %w[profile profile_edit update updated_ajax] }.freeze
|
|
119
|
+
|
|
120
|
+
# Force a newly provisioned administrator (minted with a generated password, see
|
|
121
|
+
# harden-installer-default-admin) to set their own password before using the admin panel.
|
|
122
|
+
def enforce_password_change
|
|
123
|
+
return if cama_current_user.blank?
|
|
124
|
+
return if cama_current_user.get_meta('must_change_password').blank?
|
|
125
|
+
return if PASSWORD_CHANGE_EXEMPT[controller_path]&.include?(action_name)
|
|
126
|
+
|
|
127
|
+
flash[:alert] = t('camaleon_cms.admin.users.message.must_change_password',
|
|
128
|
+
default: 'Please choose a new password before continuing.')
|
|
129
|
+
redirect_to cama_admin_profile_edit_path
|
|
130
|
+
end
|
|
131
|
+
|
|
73
132
|
# initialize all vars and methods for admin panel
|
|
74
133
|
def admin_init_actions
|
|
75
134
|
I18n.locale = current_site.get_admin_language
|
|
76
|
-
@_admin_menus
|
|
135
|
+
# Alias the legacy @_admin_menus ivar onto the live menu store so a hook using the WordPress-style
|
|
136
|
+
# `@_admin_menus.delete('comments')` removal idiom mutates the same hash admin_menu_draw reads. The
|
|
137
|
+
# menu-insert methods mutate in place (.replace), so this reference stays valid. Regression M19.
|
|
138
|
+
@_admin_menus = CurrentRequest.admin_menu_items = {}
|
|
77
139
|
@_admin_breadcrumb = []
|
|
78
140
|
@_extra_models_for_fields = []
|
|
141
|
+
# Cache the site's frontend language for decorators and plugins that still
|
|
142
|
+
# need to distinguish admin requests from frontend visitor requests.
|
|
79
143
|
@cama_i18n_frontend = current_site.get_languages.first
|
|
80
144
|
end
|
|
81
145
|
|
|
82
146
|
# trigger hooks for admin panel before admin load
|
|
83
147
|
def admin_before_hooks
|
|
84
|
-
|
|
148
|
+
run_hook_lifecycle('admin_before_load')
|
|
85
149
|
end
|
|
86
150
|
|
|
87
151
|
# Set cama_current_user and current_site in CurrentRequest so models can access the current context.
|
|
@@ -93,11 +157,57 @@ module CamaleonCms
|
|
|
93
157
|
|
|
94
158
|
# trigger hooks for admin panel after admin load
|
|
95
159
|
def admin_after_hooks
|
|
96
|
-
|
|
160
|
+
run_hook_lifecycle('admin_after_load')
|
|
97
161
|
end
|
|
98
162
|
|
|
99
163
|
def admin_logged_actions
|
|
100
|
-
admin_menus_add_commons if !request.xhr? ||
|
|
164
|
+
admin_menus_add_commons if !request.xhr? || params[:cama_ajax_request].blank? # initialize admin sidebar menus
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Post types on which the caller holds `action`: :posts for content and the post-type kind,
|
|
168
|
+
# :categories / :post_tags for the taxonomy kinds. Admins hold every ability on every type, so
|
|
169
|
+
# skip the per-record sweep (and the redundant IN filter it would feed) for them.
|
|
170
|
+
def cama_admin_searchable_post_type_ids(action = :posts)
|
|
171
|
+
return current_site.post_types.ids if can?(:manage, :all)
|
|
172
|
+
|
|
173
|
+
cama_admin_searchable_post_types(action).map(&:id)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def cama_admin_searchable_post_types(action)
|
|
177
|
+
current_site.post_types.select { |pt| can?(action, pt) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Posts the caller may see in admin search: the accessible post types narrowed by the shared
|
|
181
|
+
# admin visibility rule. edit_other implies :posts, so the visibility sweep only rechecks the
|
|
182
|
+
# already-loaded listable subset; admins skip the scoping entirely.
|
|
183
|
+
def cama_admin_searchable_posts
|
|
184
|
+
return current_site.posts if can?(:manage, :all)
|
|
185
|
+
|
|
186
|
+
listable = cama_admin_searchable_post_types(:posts)
|
|
187
|
+
cama_admin_visible_posts(current_site.posts.where(post_type_id: listable.map(&:id)), listable)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Restrict `scope` to the posts the caller may see in admin listings across `post_types`: every
|
|
191
|
+
# post on the types where they hold edit_other, only their own elsewhere. Admins see everything.
|
|
192
|
+
# Single source of the visibility rule for PostsController#index (per post type) and admin search
|
|
193
|
+
# (across types) -- these drifting apart is how the M12 disclosure shipped, so change it here, not
|
|
194
|
+
# at a call site.
|
|
195
|
+
def cama_admin_visible_posts(scope, post_types)
|
|
196
|
+
return scope if can?(:manage, :all)
|
|
197
|
+
|
|
198
|
+
edit_other_ids = post_types.select { |pt| can?(:edit_other, pt) }.map(&:id)
|
|
199
|
+
return scope if edit_other_ids.length == post_types.length
|
|
200
|
+
|
|
201
|
+
scope.where(post_type_id: edit_other_ids).or(scope.where(user_id: cama_current_user.id))
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def items_sql_by_name(table_name)
|
|
205
|
+
lower_name = "LOWER(#{table_name}"
|
|
206
|
+
if table_name == Cama::Post.table_name
|
|
207
|
+
return "#{lower_name}.title) LIKE ? OR #{lower_name}.slug) LIKE ? OR #{lower_name}.content_filtered) LIKE ?"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
"#{lower_name}.name) LIKE ? OR #{lower_name}.slug) LIKE ? OR #{lower_name}.description) LIKE ?"
|
|
101
211
|
end
|
|
102
212
|
end
|
|
103
213
|
end
|