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
|
@@ -3,7 +3,6 @@ module CamaleonCms
|
|
|
3
3
|
module Appearances
|
|
4
4
|
class NavMenusController < CamaleonCms::AdminController
|
|
5
5
|
include CamaleonCms::Admin::CustomFieldsConcern
|
|
6
|
-
include CamaleonCms::Frontend::NavMenuHelper
|
|
7
6
|
|
|
8
7
|
add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.appearance')
|
|
9
8
|
add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.menus')
|
|
@@ -11,9 +10,9 @@ module CamaleonCms
|
|
|
11
10
|
|
|
12
11
|
def index
|
|
13
12
|
@nav_menu = if params[:id].present?
|
|
14
|
-
current_site.nav_menus.
|
|
13
|
+
current_site.nav_menus.find_by(id: params[:id])
|
|
15
14
|
elsif params[:slug].present?
|
|
16
|
-
current_site.nav_menus.find_by_slug(params[:slug])
|
|
15
|
+
current_site.nav_menus.find_by_slug(params[:slug]) # rubocop:disable Rails/DynamicFindBy
|
|
17
16
|
else
|
|
18
17
|
current_site.nav_menus.first
|
|
19
18
|
end
|
|
@@ -91,7 +90,10 @@ module CamaleonCms
|
|
|
91
90
|
# update the reorder of items
|
|
92
91
|
def reorder_items(items = nil, parent_id = nil, is_root = true)
|
|
93
92
|
items = params[:items] if items.nil?
|
|
94
|
-
|
|
93
|
+
# Security (audit M10): resolve the destination menu through the current site (like
|
|
94
|
+
# #add_items) -- a raw params[:nav_menu_id] let an item be re-homed under another site's
|
|
95
|
+
# nav menu. Nested calls pass an explicit parent item id, so only the root needs scoping.
|
|
96
|
+
parent_id = current_site.nav_menus.find(params[:nav_menu_id]).id if parent_id.nil?
|
|
95
97
|
items.each do |index, _item|
|
|
96
98
|
item = current_site.nav_menu_items.find(_item['id'])
|
|
97
99
|
item.update(parent_id: parent_id, term_order: index)
|
|
@@ -113,7 +115,7 @@ module CamaleonCms
|
|
|
113
115
|
|
|
114
116
|
if params[:custom_items].present? # custom menu items
|
|
115
117
|
params[:custom_items].each_value do |custom_item|
|
|
116
|
-
type = custom_item['kind'].
|
|
118
|
+
type = custom_item['kind'].presence || 'external'
|
|
117
119
|
items << @nav_menu.append_menu_item({ label: custom_item['label'], link: custom_item['url'], type: type })
|
|
118
120
|
end
|
|
119
121
|
end
|
|
@@ -146,7 +148,7 @@ module CamaleonCms
|
|
|
146
148
|
# Only permit external menu options that match registered custom field slug
|
|
147
149
|
def permitted_external_options(external_params = nil)
|
|
148
150
|
opts = external_params ? external_params[:options] : params[:options]
|
|
149
|
-
return {}
|
|
151
|
+
return {} if opts.blank?
|
|
150
152
|
|
|
151
153
|
allowed_keys = cama_custom_field_allowed_slugs('NavMenuItem')
|
|
152
154
|
return {} if allowed_keys.blank?
|
|
@@ -7,9 +7,8 @@ module CamaleonCms
|
|
|
7
7
|
add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.appearance')
|
|
8
8
|
def index
|
|
9
9
|
add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.themes')
|
|
10
|
-
PluginRoutes.reload
|
|
11
10
|
authorize! :manage, :themes
|
|
12
|
-
return
|
|
11
|
+
return if params[:set].blank?
|
|
13
12
|
|
|
14
13
|
site_install_theme(params[:set])
|
|
15
14
|
flash.now[:notice] = t('camaleon_cms.admin.themes.message.updated')
|
|
@@ -18,9 +17,13 @@ module CamaleonCms
|
|
|
18
17
|
|
|
19
18
|
def load_data
|
|
20
19
|
file = Rails.root.join('app', 'apps', 'themes', current_site.get_theme_slug, 'data.json')
|
|
21
|
-
@messages = load_file_content_to_db(
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
@messages = load_file_content_to_db(
|
|
21
|
+
file,
|
|
22
|
+
{
|
|
23
|
+
post_types: 1, clear_post_type: 1, nav_menus: 1, clear_nav_menus: 1, slider_basic: 1,
|
|
24
|
+
clear_slider_basic: 1, theme_import: 1
|
|
25
|
+
}
|
|
26
|
+
)
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
def preview
|
|
@@ -17,8 +17,13 @@ module CamaleonCms
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def update
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
# sidebar_id (post_parent) and widget_id (visibility) identify the tenant-scoped sidebar and
|
|
21
|
+
# widget this assignment belongs to; they are set from @sidebar/@widget on create and must
|
|
22
|
+
# not be reassigned from the request, or the assignment could be re-pointed into another
|
|
23
|
+
# site's sidebar or widget (audit finding H9). Reordering has its own current-site-scoped
|
|
24
|
+
# action (sidebar#reorder).
|
|
25
|
+
if @assigned.update(params.require(:assign).permit(:title, :content, :item_order))
|
|
26
|
+
@assigned.set_field_values(cama_permitted_field_options('Main'))
|
|
22
27
|
flash[:notice] = t('camaleon_cms.admin.widgets.assign.updated')
|
|
23
28
|
else
|
|
24
29
|
flash[:error] = t('camaleon_cms.admin.widgets.assign.error_updated')
|
|
@@ -22,7 +22,8 @@ module CamaleonCms
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def create
|
|
25
|
-
@widget = current_site
|
|
25
|
+
@widget = current_site
|
|
26
|
+
.widgets.new(params.require(:widget_main).permit(:name, :slug, :description, :excerpt, :renderer))
|
|
26
27
|
@widget.status = 'simple'
|
|
27
28
|
if @widget.save!
|
|
28
29
|
flash[:notice] = t('camaleon_cms.admin.widgets.message.created')
|
|
@@ -7,6 +7,7 @@ module CamaleonCms
|
|
|
7
7
|
|
|
8
8
|
before_action :set_post_type
|
|
9
9
|
before_action :set_category, only: %w[show edit update destroy]
|
|
10
|
+
before_action :validate_category_parent, only: %w[create update]
|
|
10
11
|
|
|
11
12
|
def index
|
|
12
13
|
@categories = @post_type.categories
|
|
@@ -51,8 +52,11 @@ module CamaleonCms
|
|
|
51
52
|
|
|
52
53
|
# return html category list used to reload categories list in post editor form
|
|
53
54
|
def list
|
|
54
|
-
render inline: post_type_html_inputs(
|
|
55
|
-
|
|
55
|
+
render inline: post_type_html_inputs(
|
|
56
|
+
@post_type, 'categories', 'categories',
|
|
57
|
+
@post_type.get_option('has_single_category', false) ? 'radio' : 'checkbox',
|
|
58
|
+
params[:categories] || [], 'categorychecklist', true
|
|
59
|
+
)
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
def destroy
|
|
@@ -62,10 +66,21 @@ module CamaleonCms
|
|
|
62
66
|
|
|
63
67
|
private
|
|
64
68
|
|
|
69
|
+
# The parent dropdown offers the post type itself or one of its own categories. A parent_id naming
|
|
70
|
+
# anything else would re-home the category under another post type or site, so it is rejected
|
|
71
|
+
# (audit finding H8). full_categories is scoped to this post type's site and id.
|
|
72
|
+
def validate_category_parent
|
|
73
|
+
submitted = params.dig(:category, :parent_id)
|
|
74
|
+
return if submitted.blank? || submitted.to_s == @post_type.id.to_s
|
|
75
|
+
return if @post_type.full_categories.exists?(id: submitted)
|
|
76
|
+
|
|
77
|
+
raise CanCan::AccessDenied, t('camaleon_cms.admin.post_type.message.error')
|
|
78
|
+
end
|
|
79
|
+
|
|
65
80
|
# define parent post type
|
|
66
81
|
def set_post_type
|
|
67
82
|
begin
|
|
68
|
-
@post_type = current_site.post_types.
|
|
83
|
+
@post_type = current_site.post_types.find_by(id: params[:post_type_id]).decorate
|
|
69
84
|
rescue StandardError
|
|
70
85
|
flash[:error] = t('camaleon_cms.admin.request_error_message')
|
|
71
86
|
redirect_to cama_admin_path, { error: 'Error Post Type' }
|
|
@@ -76,7 +91,7 @@ module CamaleonCms
|
|
|
76
91
|
end
|
|
77
92
|
|
|
78
93
|
def set_category
|
|
79
|
-
@category = @post_type.categories.
|
|
94
|
+
@category = @post_type.categories.find_by(id: params[:id])
|
|
80
95
|
render_404 unless @category
|
|
81
96
|
rescue StandardError
|
|
82
97
|
flash[:error] = t('camaleon_cms.admin.post_type.message.error')
|
|
@@ -2,7 +2,6 @@ require 'will_paginate/array'
|
|
|
2
2
|
module CamaleonCms
|
|
3
3
|
module Admin
|
|
4
4
|
class CommentsController < CamaleonCms::AdminController
|
|
5
|
-
include CamaleonCms::CommentHelper
|
|
6
5
|
add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.comments'), :cama_admin_comments_url
|
|
7
6
|
before_action :validate_role
|
|
8
7
|
before_action :set_post, except: :list
|
|
@@ -5,9 +5,13 @@ module CamaleonCms
|
|
|
5
5
|
skip_before_action :cama_before_actions
|
|
6
6
|
skip_after_action :cama_after_actions
|
|
7
7
|
before_action :installer_verification, except: :welcome
|
|
8
|
+
before_action :require_setup_token, only: :save
|
|
8
9
|
layout 'camaleon_cms/login'
|
|
9
10
|
|
|
10
11
|
def index
|
|
12
|
+
# Ensure the setup token exists (and is logged) so a remote operator can read it and enter it
|
|
13
|
+
# below. Local installs are exempt (require_setup_token), so no token is generated for them.
|
|
14
|
+
CamaleonCms::SetupToken.value if CamaleonCms::SetupToken.required? && !request.local?
|
|
11
15
|
@site ||= CamaleonCms::Site.new
|
|
12
16
|
@site.slug = request.original_url.to_s.parse_domain
|
|
13
17
|
render 'form'
|
|
@@ -17,6 +21,10 @@ module CamaleonCms
|
|
|
17
21
|
@site = CamaleonCms::Site.new(params[:site].permit(:slug, :name)).decorate
|
|
18
22
|
if @site.save
|
|
19
23
|
site_after_install(@site, params[:theme])
|
|
24
|
+
CamaleonCms::SetupToken.clear! # single-use: the installer is closed now that a site exists
|
|
25
|
+
# Carry the generated admin password to the welcome page for a single, session-scoped display.
|
|
26
|
+
flash[:generated_admin_password] = @site.generated_admin_password
|
|
27
|
+
session[:cama_installer_welcome] = true
|
|
20
28
|
flash[:notice] = t('camaleon_cms.admin.sites.message.created')
|
|
21
29
|
redirect_to action: :welcome
|
|
22
30
|
else
|
|
@@ -24,11 +32,34 @@ module CamaleonCms
|
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
|
|
27
|
-
def welcome
|
|
35
|
+
def welcome
|
|
36
|
+
# Only the operator who just completed setup may see the credentials; the marker is one-time.
|
|
37
|
+
return redirect_to cama_admin_login_url unless session.delete(:cama_installer_welcome)
|
|
38
|
+
|
|
39
|
+
@generated_admin_password = flash[:generated_admin_password]
|
|
40
|
+
end
|
|
28
41
|
|
|
29
42
|
def installer_verification
|
|
30
43
|
redirect_to cama_root_url unless CamaleonCms::Site.count == 0
|
|
31
44
|
end
|
|
45
|
+
|
|
46
|
+
# Refuse to provision unless the request carries the setup token (readable only from the server's
|
|
47
|
+
# environment or filesystem). Only enforced while no site exists; afterwards installer_verification
|
|
48
|
+
# has already closed the installer.
|
|
49
|
+
def require_setup_token
|
|
50
|
+
return unless CamaleonCms::SetupToken.required?
|
|
51
|
+
# Loopback requests (a local console/dev install, or an SSH tunnel) already prove host access,
|
|
52
|
+
# so the token is required only for remote requests. request.local? is true only when the
|
|
53
|
+
# computed client IP is loopback, so a real remote client behind a proxy that forwards
|
|
54
|
+
# X-Forwarded-For is still gated. See harden-installer-default-admin.
|
|
55
|
+
return if request.local?
|
|
56
|
+
return if CamaleonCms::SetupToken.valid?(params[:setup_token])
|
|
57
|
+
|
|
58
|
+
flash[:error] = t('camaleon_cms.admin.installer.invalid_setup_token',
|
|
59
|
+
default: 'A valid setup token is required to install. See the server log or ' \
|
|
60
|
+
'tmp/camaleon_setup_token.')
|
|
61
|
+
redirect_to cama_admin_installers_path
|
|
62
|
+
end
|
|
32
63
|
end
|
|
33
64
|
end
|
|
34
65
|
end
|
|
@@ -3,25 +3,69 @@ module CamaleonCms
|
|
|
3
3
|
module Admin
|
|
4
4
|
class MediaController < CamaleonCms::AdminController
|
|
5
5
|
skip_before_action :admin_logged_actions, except: %i[index download_private_file], raise: false
|
|
6
|
-
|
|
6
|
+
# Security (audit M6/M7): upload writes files (and, via saved_avatar-style flows, model state),
|
|
7
|
+
# so it must not skip CSRF. The multipart uploader (jQuery uploadFile) posts outside jquery_ujs'
|
|
8
|
+
# ajax prefilter, so it carries the token as an authenticity_token form field instead (see
|
|
9
|
+
# _media_manager.js). The url-upload path posts through media#actions, already CSRF-protected.
|
|
7
10
|
before_action :init_media_vars, except: :download_private_file
|
|
8
11
|
before_action :verify_media_authorization
|
|
9
12
|
|
|
10
13
|
# render media section
|
|
11
14
|
def index
|
|
12
15
|
@show_file_actions = true
|
|
13
|
-
@files = @tree.paginate(page: params[:page], per_page: 100)
|
|
16
|
+
@files = cama_uploader.cama_prepare_browser_page(@tree.paginate(page: params[:page], per_page: 100))
|
|
14
17
|
@next_page = @files.current_page < @files.total_pages ? @files.current_page + 1 : nil
|
|
15
18
|
add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.media')
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
# crop a image to save as a new file
|
|
22
|
+
#
|
|
23
|
+
# Unlike crop_url (which is URL-only and prefixes the site URL onto relative
|
|
24
|
+
# inputs), crop accepts three cp_img_path shapes: an http(s) URL, a data:
|
|
25
|
+
# URI, or a server filesystem path (see the cama_tmp_upload examples). Only
|
|
26
|
+
# http(s) URLs are validated here; data: URIs and filesystem paths pass
|
|
27
|
+
# through to cama_tmp_upload, whose cama_canonical_upload_path guard confines
|
|
28
|
+
# them to the public/tmp roots. A bare relative path is therefore treated as
|
|
29
|
+
# a filesystem path, not prefixed with the site URL — intentional divergence.
|
|
19
30
|
def crop
|
|
20
|
-
|
|
31
|
+
cp_img_path = params[:cp_img_path].to_s
|
|
32
|
+
if (url_error = cama_upload_url_error(cp_img_path))
|
|
33
|
+
return render(plain: helpers.sanitize(url_error))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Security (object-level authorization): setting another user's avatar is a user-management
|
|
37
|
+
# write, so gate a non-self saved_avatar target on :manage, :users -- the same capability
|
|
38
|
+
# UsersController#update already requires. Decide from the parameter, before resolving the
|
|
39
|
+
# target (so a denied caller cannot use the response as a same-site existence oracle) and
|
|
40
|
+
# before the upload/crop (so no file work happens on a denied request). A caller may always
|
|
41
|
+
# set their own avatar with the media permission the endpoint already requires.
|
|
42
|
+
# (Mirrors UsersController#profile's self-vs-other gate. The .to_s compare and the blank-
|
|
43
|
+
# saved_avatar skip -- a plain crop, not a user write -- are intentional and fail-closed; do
|
|
44
|
+
# not "align" this to users_controller's .to_i, which coerces non-canonical ids.)
|
|
45
|
+
if params[:saved_avatar].present? && params[:saved_avatar].to_s != cama_current_user.id.to_s
|
|
46
|
+
authorize! :manage, :users
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
tmp = cama_tmp_upload(cp_img_path, formats: params[:formats], name: params[:name])
|
|
50
|
+
return render(plain: helpers.sanitize(tmp[:error])) if tmp[:error].present?
|
|
51
|
+
|
|
52
|
+
path_image = tmp[:file_path]
|
|
21
53
|
crop_path = cama_crop_image(path_image, params[:ic_w], params[:ic_h], params[:ic_x], params[:ic_y])
|
|
22
54
|
res = upload_file(crop_path, { remove_source: true })
|
|
23
|
-
|
|
24
|
-
|
|
55
|
+
# A failed upload returns { error: ... } with no url. Surface it like the action's other
|
|
56
|
+
# error paths instead of writing a nil avatar meta and rendering an empty 200 body (which
|
|
57
|
+
# let the avatar flow store "").
|
|
58
|
+
return render(plain: helpers.sanitize(res[:error])) if res[:error].present?
|
|
59
|
+
|
|
60
|
+
# Security (audit Low): resolve the avatar target through the current site and nil-safely.
|
|
61
|
+
# The object-level gate above (:manage, :users for a non-self target) is the primary control
|
|
62
|
+
# that stops a media manager from writing another user's avatar; this site-scoping is defense
|
|
63
|
+
# in depth -- for an authorized :manage,:users holder a foreign id is a no-op here, where an
|
|
64
|
+
# unscoped User.find would reach across sites and 500 on a bad id.
|
|
65
|
+
if params[:saved_avatar].present?
|
|
66
|
+
current_site.users.find_by(id: params[:saved_avatar])&.set_meta('avatar', res['url'])
|
|
67
|
+
end
|
|
68
|
+
render plain: res['url'].to_s
|
|
25
69
|
end
|
|
26
70
|
|
|
27
71
|
# download private files
|
|
@@ -42,13 +86,13 @@ module CamaleonCms
|
|
|
42
86
|
def ajax
|
|
43
87
|
@show_file_actions = true if current_site.get_option('file_actions_in_modals') == 'yes'
|
|
44
88
|
@tree = cama_uploader.search(params[:search]) if params[:search].present?
|
|
45
|
-
@files = @tree.paginate(page: params[:page], per_page: 100)
|
|
89
|
+
@files = cama_uploader.cama_prepare_browser_page(@tree.paginate(page: params[:page], per_page: 100))
|
|
46
90
|
@next_page = @files.current_page < @files.total_pages ? @files.current_page + 1 : nil
|
|
47
91
|
if params[:partial].present?
|
|
48
92
|
render json: { next_page: @next_page,
|
|
49
93
|
html: render_to_string(partial: 'render_file_item', locals: { files: @files }) }
|
|
50
|
-
|
|
51
|
-
render 'index', layout: false
|
|
94
|
+
elsif params[:partial].blank?
|
|
95
|
+
render 'index', layout: false
|
|
52
96
|
end
|
|
53
97
|
end
|
|
54
98
|
|
|
@@ -68,9 +112,9 @@ module CamaleonCms
|
|
|
68
112
|
when 'crop_url'
|
|
69
113
|
user_url = params[:url].to_s
|
|
70
114
|
user_url = "#{current_site.the_url(locale: nil)}#{user_url}" unless user_url.start_with?('data:', 'http')
|
|
71
|
-
|
|
72
|
-
r = if
|
|
73
|
-
{ error:
|
|
115
|
+
url_error = cama_upload_url_error(user_url)
|
|
116
|
+
r = if url_error
|
|
117
|
+
{ error: url_error }
|
|
74
118
|
else
|
|
75
119
|
cama_tmp_upload(user_url, formats: params[:formats], name: params[:name])
|
|
76
120
|
end
|
|
@@ -93,9 +137,17 @@ module CamaleonCms
|
|
|
93
137
|
params[:dimension] = nil if params[:skip_auto_crop].present?
|
|
94
138
|
f = { error: 'File not found.' }
|
|
95
139
|
if params[:file_upload].present?
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
140
|
+
if params[:file_upload].is_a?(String) && (url_error = cama_upload_url_error(params[:file_upload]))
|
|
141
|
+
return render plain: helpers.sanitize(url_error)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
f = upload_file(
|
|
145
|
+
params[:file_upload],
|
|
146
|
+
{
|
|
147
|
+
folder: params[:folder], dimension: params['dimension'], formats: params[:formats],
|
|
148
|
+
versions: params[:versions], thumb_size: params[:thumb_size]
|
|
149
|
+
}.merge!(settings)
|
|
150
|
+
)
|
|
99
151
|
end
|
|
100
152
|
|
|
101
153
|
if f[:error].present?
|
|
@@ -3,9 +3,7 @@ module CamaleonCms
|
|
|
3
3
|
class PluginsController < CamaleonCms::AdminController
|
|
4
4
|
before_action :validate_role
|
|
5
5
|
add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.plugins')
|
|
6
|
-
def index
|
|
7
|
-
PluginRoutes.reload
|
|
8
|
-
end
|
|
6
|
+
def index; end
|
|
9
7
|
|
|
10
8
|
def toggle
|
|
11
9
|
status = params[:status].to_bool
|
|
@@ -30,7 +30,7 @@ module CamaleonCms
|
|
|
30
30
|
def update
|
|
31
31
|
args = { post_tag: @post_tag, post_type: @post_type }
|
|
32
32
|
hooks_run('before_update_post_tag', args)
|
|
33
|
-
if @post_tag.update(
|
|
33
|
+
if @post_tag.update(post_tag_params)
|
|
34
34
|
@post_tag.set_options(params[:meta]) if params[:meta].present?
|
|
35
35
|
@post_tag.set_field_values(cama_permitted_field_options('PostType_PostTag'))
|
|
36
36
|
hooks_run('after_update_post_tag', args)
|
|
@@ -43,7 +43,7 @@ module CamaleonCms
|
|
|
43
43
|
|
|
44
44
|
# render post tag create form
|
|
45
45
|
def create
|
|
46
|
-
@post_tag = @post_type.post_tags.new(
|
|
46
|
+
@post_tag = @post_type.post_tags.new(post_tag_params)
|
|
47
47
|
args = { post_tag: @post_tag, post_type: @post_type }
|
|
48
48
|
hooks_run('before_create_post_tag', args)
|
|
49
49
|
if @post_tag.save
|
|
@@ -73,15 +73,21 @@ module CamaleonCms
|
|
|
73
73
|
|
|
74
74
|
private
|
|
75
75
|
|
|
76
|
+
# parent_id is the owning post type's id, set from the @post_type association on create; it is not
|
|
77
|
+
# accepted from the request so a tag cannot be moved under another post type or site (H8).
|
|
78
|
+
def post_tag_params
|
|
79
|
+
params.require(:post_tag).permit(:name, :slug, :description)
|
|
80
|
+
end
|
|
81
|
+
|
|
76
82
|
def set_post_type
|
|
77
|
-
@post_type = current_site.post_types.
|
|
83
|
+
@post_type = current_site.post_types.find_by(id: params[:post_type_id]).decorate
|
|
78
84
|
authorize! :post_tags, @post_type
|
|
79
85
|
add_breadcrumb @post_type.the_title, @post_type.the_admin_url
|
|
80
86
|
add_breadcrumb t('camaleon_cms.admin.post_type.post_tags'), url_for({ action: :index })
|
|
81
87
|
end
|
|
82
88
|
|
|
83
89
|
def set_post_tag
|
|
84
|
-
@post_tag = @post_type.post_tags.
|
|
90
|
+
@post_tag = @post_type.post_tags.find_by(id: params[:id])
|
|
85
91
|
rescue StandardError
|
|
86
92
|
flash[:error] = t('camaleon_cms.admin.post_type.message.error')
|
|
87
93
|
redirect_to cama_admin_path
|
|
@@ -5,22 +5,37 @@ module CamaleonCms
|
|
|
5
5
|
before_action :set_post_data_params, only: %i[create update]
|
|
6
6
|
|
|
7
7
|
def index
|
|
8
|
+
# Security (audit 2026-08-11 M12): this action had no authorization, so any signed-in user
|
|
9
|
+
# could read a post type's JSON. Gate it on the same permission the post listing requires.
|
|
10
|
+
authorize! :posts, @post_type
|
|
8
11
|
render json: @post_type
|
|
9
12
|
end
|
|
10
13
|
|
|
11
14
|
def create
|
|
12
|
-
if
|
|
13
|
-
|
|
15
|
+
if @draft_parent_post.present?
|
|
16
|
+
# A draft buffer is an artifact of editing its parent post — authorize that post,
|
|
17
|
+
# and only ever touch the current user's own buffer
|
|
18
|
+
authorize! :update, @draft_parent_post
|
|
19
|
+
@post_draft = @post_type.posts.drafts.where(post_parent: @draft_parent_post.id,
|
|
20
|
+
user_id: cama_current_user.id).first
|
|
14
21
|
if @post_draft.present?
|
|
15
22
|
@post_draft.set_option('draft_status', @post_draft.status)
|
|
16
23
|
@post_draft.attributes = @post_data
|
|
17
24
|
end
|
|
25
|
+
else
|
|
26
|
+
authorize! :create_post, @post_type
|
|
27
|
+
end
|
|
28
|
+
if @post_draft.blank?
|
|
29
|
+
@post_draft = @post_type.posts.new(@post_data)
|
|
30
|
+
@post_draft.user_id = cama_current_user.id
|
|
18
31
|
end
|
|
19
|
-
@post_draft = @post_type.posts.new(@post_data) unless @post_draft.present?
|
|
20
32
|
r = { post: @post_draft, post_type: @post_type }
|
|
21
33
|
hooks_run('create_post_draft', r)
|
|
22
34
|
if @post_draft.save(validate: false)
|
|
23
|
-
|
|
35
|
+
# Security (audit M8): confine field values to slugs actually registered on the post type,
|
|
36
|
+
# like PostsController#save_post_with_fields -- raw params[:field_options] let a caller
|
|
37
|
+
# write custom_field_values with attacker-chosen slugs/ids/group numbers.
|
|
38
|
+
@post_draft.set_params(params[:meta], cama_permitted_field_options('PostType_Post'), @post_data[:keywords])
|
|
24
39
|
msg = { draft: { id: @post_draft.id },
|
|
25
40
|
_drafts_path: cama_admin_post_type_draft_path(@post_type.id, @post_draft) }
|
|
26
41
|
r = { post: @post_draft, post_type: @post_type }
|
|
@@ -33,18 +48,25 @@ module CamaleonCms
|
|
|
33
48
|
end
|
|
34
49
|
|
|
35
50
|
def update
|
|
36
|
-
@post_draft =
|
|
51
|
+
@post_draft = @post_type.posts.drafts.where(user_id: cama_current_user.id).find(params[:id])
|
|
52
|
+
authorize! :update, @post_draft.parent || @post_draft
|
|
37
53
|
@post_draft.attributes = @post_data
|
|
38
54
|
r = { post: @post_draft, post_type: @post_type }
|
|
39
55
|
hooks_run('update_post_draft', r)
|
|
40
56
|
if @post_draft.save(validate: false)
|
|
41
|
-
|
|
57
|
+
# Security (audit M8): confine field values to the post type's registered slugs (see #create).
|
|
58
|
+
@post_draft.set_params(params[:meta], cama_permitted_field_options('PostType_Post'), params[:options])
|
|
42
59
|
hooks_run('updated_post_draft', { post: @post_draft, post_type: @post_type })
|
|
43
60
|
msg = { draft: { id: @post_draft.id } }
|
|
44
61
|
else
|
|
45
62
|
msg = { error: @post_draft.errors.full_messages }
|
|
46
63
|
end
|
|
47
64
|
render json: msg
|
|
65
|
+
rescue ActiveRecord::RecordNotFound
|
|
66
|
+
# Another user's buffer answers exactly like a nonexistent draft id (same rescue
|
|
67
|
+
# behavior as PostsController#set_post) — no existence oracle for foreign buffers
|
|
68
|
+
flash[:error] = t('camaleon_cms.admin.post.message.error', post_type: @post_type.decorate.the_title)
|
|
69
|
+
redirect_to cama_admin_path
|
|
48
70
|
end
|
|
49
71
|
|
|
50
72
|
def destroy; end
|
|
@@ -52,12 +74,19 @@ module CamaleonCms
|
|
|
52
74
|
private
|
|
53
75
|
|
|
54
76
|
def set_post_data_params
|
|
55
|
-
post_data = params
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
post_data = params
|
|
78
|
+
.require(:post).permit(
|
|
79
|
+
:title, :slug, :content, :excerpt, :status, :comment_status, :visibility,
|
|
80
|
+
:visibility_value, :post_order, :published_at
|
|
81
|
+
).to_h
|
|
82
|
+
post_data.delete(:created_at) if params[:post][:created_at].blank?
|
|
83
|
+
post_data.delete(:updated_at) if params[:post][:updated_at].blank?
|
|
58
84
|
post_data[:status] = 'draft_child'
|
|
59
|
-
|
|
60
|
-
|
|
85
|
+
if action_name == 'create'
|
|
86
|
+
@draft_parent_post = current_site.posts.find_by(id: params[:post_id]) if params[:post_id].present?
|
|
87
|
+
# post_parent is create-only and always overwritten — never client-writable via post[post_parent]
|
|
88
|
+
post_data[:post_parent] = @draft_parent_post&.id
|
|
89
|
+
end
|
|
61
90
|
post_data[:data_tags] = params[:tags].to_s
|
|
62
91
|
post_data[:data_categories] = params[:categories] || []
|
|
63
92
|
@post_data = post_data
|