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.
Files changed (216) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -4
  3. data/Rakefile +3 -16
  4. data/app/apps/plugins/attack/admin_controller.rb +9 -6
  5. data/app/apps/plugins/attack/attack_helper.rb +35 -25
  6. data/app/apps/plugins/authoring_post/authoring_post_helper.rb +23 -16
  7. data/app/apps/plugins/front_cache/admin_controller.rb +14 -10
  8. data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
  9. data/app/apps/plugins/front_cache/config/locales/translation.yml +0 -7
  10. data/app/apps/plugins/front_cache/front_cache_helper.rb +109 -37
  11. data/app/apps/plugins/front_cache/views/admin/settings.html.erb +0 -5
  12. data/app/apps/plugins/visibility_post/config/config.json +1 -0
  13. data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
  14. data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
  15. data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
  16. data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
  17. data/app/apps/themes/default/assets/css/style.css.scss +24 -0
  18. data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
  19. data/app/apps/themes/default/default_helper.rb +2 -1
  20. data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
  21. data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
  22. data/app/apps/themes/new/custom_helper.rb +5 -9
  23. data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
  24. data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
  25. data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
  26. data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
  27. data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
  28. data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +8 -6
  29. data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
  30. data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +7 -2
  31. data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +2 -1
  32. data/app/controllers/camaleon_cms/admin/categories_controller.rb +19 -4
  33. data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
  34. data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
  35. data/app/controllers/camaleon_cms/admin/media_controller.rb +66 -14
  36. data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
  37. data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +10 -4
  38. data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
  39. data/app/controllers/camaleon_cms/admin/posts_controller.rb +55 -22
  40. data/app/controllers/camaleon_cms/admin/sessions_controller.rb +237 -44
  41. data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +80 -13
  42. data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +5 -2
  43. data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +26 -3
  44. data/app/controllers/camaleon_cms/admin/settings_controller.rb +5 -3
  45. data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +7 -5
  46. data/app/controllers/camaleon_cms/admin/users_controller.rb +78 -11
  47. data/app/controllers/camaleon_cms/admin_controller.rb +136 -26
  48. data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
  49. data/app/controllers/camaleon_cms/frontend_controller.rb +146 -74
  50. data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +20 -5
  51. data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
  52. data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
  53. data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
  54. data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
  55. data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
  56. data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
  57. data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
  58. data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
  59. data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
  60. data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
  61. data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
  62. data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
  63. data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
  64. data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
  65. data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
  66. data/app/decorators/camaleon_cms/post_decorator.rb +68 -50
  67. data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
  68. data/app/decorators/camaleon_cms/site_decorator.rb +33 -25
  69. data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
  70. data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
  71. data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
  72. data/app/helpers/camaleon_cms/admin/application_helper.rb +5 -8
  73. data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
  74. data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
  75. data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
  76. data/app/helpers/camaleon_cms/admin/post_type_helper.rb +34 -17
  77. data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
  78. data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
  79. data/app/helpers/camaleon_cms/comment_helper.rb +19 -7
  80. data/app/helpers/camaleon_cms/content_helper.rb +13 -6
  81. data/app/helpers/camaleon_cms/email_helper.rb +7 -3
  82. data/app/helpers/camaleon_cms/frontend/application_helper.rb +6 -1
  83. data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
  84. data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +143 -66
  85. data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
  86. data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
  87. data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
  88. data/app/helpers/camaleon_cms/html_helper.rb +58 -37
  89. data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
  90. data/app/helpers/camaleon_cms/session_helper.rb +188 -44
  91. data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
  92. data/app/helpers/camaleon_cms/site_helper.rb +34 -13
  93. data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
  94. data/app/helpers/camaleon_cms/uploader_helper.rb +19 -473
  95. data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
  96. data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
  97. data/app/models/camaleon_cms/ability.rb +15 -7
  98. data/app/models/camaleon_cms/category.rb +6 -6
  99. data/app/models/camaleon_cms/custom_field.rb +15 -6
  100. data/app/models/camaleon_cms/custom_field_group.rb +39 -22
  101. data/app/models/camaleon_cms/custom_fields_relationship.rb +140 -2
  102. data/app/models/camaleon_cms/media.rb +8 -2
  103. data/app/models/camaleon_cms/meta.rb +2 -2
  104. data/app/models/camaleon_cms/nav_menu.rb +5 -3
  105. data/app/models/camaleon_cms/nav_menu_item.rb +16 -8
  106. data/app/models/camaleon_cms/plugin.rb +3 -4
  107. data/app/models/camaleon_cms/post.rb +137 -27
  108. data/app/models/camaleon_cms/post_comment.rb +10 -6
  109. data/app/models/camaleon_cms/post_default.rb +42 -12
  110. data/app/models/camaleon_cms/post_relationship.rb +6 -4
  111. data/app/models/camaleon_cms/post_tag.rb +4 -5
  112. data/app/models/camaleon_cms/post_type.rb +21 -13
  113. data/app/models/camaleon_cms/site.rb +39 -29
  114. data/app/models/camaleon_cms/term_relationship.rb +10 -7
  115. data/app/models/camaleon_cms/term_taxonomy.rb +58 -4
  116. data/app/models/camaleon_cms/theme.rb +2 -4
  117. data/app/models/camaleon_cms/user.rb +17 -5
  118. data/app/models/camaleon_cms/user_role.rb +81 -6
  119. data/app/models/camaleon_cms/widget/assigned.rb +11 -6
  120. data/app/models/camaleon_cms/widget/main.rb +12 -8
  121. data/app/models/camaleon_cms/widget/sidebar.rb +3 -8
  122. data/app/models/camaleon_record.rb +89 -5
  123. data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +36 -42
  124. data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
  125. data/app/models/concerns/camaleon_cms/content_shortcode_gate.rb +73 -0
  126. data/app/models/concerns/camaleon_cms/custom_fields_read.rb +111 -54
  127. data/app/models/concerns/camaleon_cms/metas.rb +31 -6
  128. data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
  129. data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
  130. data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
  131. data/app/models/current_request.rb +8 -1
  132. data/app/uploaders/camaleon_cms_aws_uploader.rb +31 -10
  133. data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
  134. data/app/uploaders/camaleon_cms_uploader.rb +23 -10
  135. data/app/validators/camaleon_cms/post_uniq_validator.rb +5 -4
  136. data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
  137. data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
  138. data/app/views/camaleon_cms/admin/categories/index.html.erb +2 -2
  139. data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
  140. data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
  141. data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
  142. data/app/views/camaleon_cms/admin/post_tags/index.html.erb +2 -2
  143. data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
  144. data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
  145. data/app/views/camaleon_cms/admin/posts/index.html.erb +5 -5
  146. data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
  147. data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
  148. data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
  149. data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
  150. data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +19 -4
  151. data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
  152. data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +4 -3
  153. data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
  154. data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
  155. data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
  156. data/app/views/camaleon_cms/admin/user_roles/form.html.erb +24 -3
  157. data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
  158. data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
  159. data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
  160. data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
  161. data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
  162. data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
  163. data/config/initializers/action_view.rb +42 -5
  164. data/config/initializers/active_record_extension.rb +13 -27
  165. data/config/initializers/assets.rb +52 -23
  166. data/config/initializers/custom_initializers.rb +1 -1
  167. data/config/locales/camaleon_cms/admin/ar.yml +1 -0
  168. data/config/locales/camaleon_cms/admin/de.yml +1 -0
  169. data/config/locales/camaleon_cms/admin/en.yml +36 -0
  170. data/config/locales/camaleon_cms/admin/es.yml +1 -0
  171. data/config/locales/camaleon_cms/admin/fr.yml +1 -0
  172. data/config/locales/camaleon_cms/admin/it.yml +1 -0
  173. data/config/locales/camaleon_cms/admin/nl.yml +1 -0
  174. data/config/locales/camaleon_cms/admin/pt-BR.yml +1 -0
  175. data/config/locales/camaleon_cms/admin/pt.yml +1 -0
  176. data/config/locales/camaleon_cms/admin/ru.yml +1 -0
  177. data/config/locales/camaleon_cms/admin/uk.yml +1 -0
  178. data/config/locales/camaleon_cms/admin/zh-CH.yml +1 -0
  179. data/config/routes/admin.rb +33 -15
  180. data/config/routes/frontend.rb +21 -15
  181. data/lib/camaleon_cms/assets_precompile.rb +86 -0
  182. data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
  183. data/lib/camaleon_cms/content_security.rb +158 -0
  184. data/lib/camaleon_cms/engine.rb +45 -21
  185. data/lib/camaleon_cms/media_security_headers.rb +27 -0
  186. data/lib/camaleon_cms/setup_token.rb +69 -0
  187. data/lib/camaleon_cms/shortcode_registry.rb +102 -0
  188. data/lib/camaleon_cms/svg_content_checker.rb +123 -0
  189. data/lib/camaleon_cms/task_reporter.rb +14 -0
  190. data/lib/camaleon_cms/unsafe_markup.rb +207 -0
  191. data/lib/camaleon_cms/uploader_content_security.rb +248 -0
  192. data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
  193. data/lib/camaleon_cms/uploader_path_security.rb +146 -0
  194. data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
  195. data/lib/camaleon_cms/uploader_support.rb +86 -0
  196. data/lib/camaleon_cms/version.rb +1 -1
  197. data/lib/camaleon_cms.rb +23 -0
  198. data/lib/ext/hash.rb +28 -2
  199. data/lib/ext/string.rb +10 -5
  200. data/lib/ext/translator.rb +10 -2
  201. data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
  202. data/lib/generators/camaleon_cms/install_generator.rb +7 -4
  203. data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
  204. data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
  205. data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
  206. data/lib/plugin_routes.rb +412 -368
  207. data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
  208. data/lib/tasks/cross_site_field_groups.rake +74 -0
  209. data/lib/tasks/custom_fields_roles.rake +15 -13
  210. data/lib/tasks/orphaned_comments.rake +42 -0
  211. data/lib/tasks/private_upload_acls.rake +31 -0
  212. data/lib/tasks/site_custom_field_groups.rake +36 -0
  213. data/lib/tasks/unsafe_stored_content.rake +42 -0
  214. data/lib/tasks/unsafe_stored_uploads.rake +70 -0
  215. data/lib/tasks/user_field_groups.rake +27 -0
  216. 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.find_by_id(params[:id])
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
- parent_id = params[:nav_menu_id] if parent_id.nil?
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'].present? ? custom_item['kind'] : 'external'
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 {} unless opts.present?
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 unless params[:set].present?
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(file,
22
- { post_types: 1, clear_post_type: 1, nav_menus: 1, clear_nav_menus: 1, slider_basic: 1, clear_slider_basic: 1,
23
- theme_import: 1 })
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
- if @assigned.update(params.require(:assign).permit(:title, :content, :widget_id, :sidebar_id, :item_order))
21
- @assigned.set_field_values(cama_permitted_field_options('Widget::Main'))
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.widgets.new(params.require(:widget_main).permit(:name, :slug, :description, :excerpt, :renderer))
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(@post_type, 'categories', 'categories',
55
- @post_type.get_option('has_single_category', false) ? 'radio' : 'checkbox', params[:categories] || [], 'categorychecklist', true)
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.find_by_id(params[:post_type_id]).decorate
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.find_by_id(params[:id])
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; end
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
- skip_before_action :verify_authenticity_token, only: :upload, raise: false
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
- path_image = cama_tmp_upload(params[:cp_img_path])[:file_path]
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
- CamaleonCms::User.find(params[:saved_avatar]).set_meta('avatar', res['url']) if params[:saved_avatar].present?
24
- render html: res['url'].html_safe
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
- else
51
- render 'index', layout: false unless params[:partial].present?
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
- url_validation_result = UserUrlValidator.validate(user_url)
72
- r = if url_validation_result.is_a?(Array)
73
- { error: url_validation_result.join(', ') }
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
- f = upload_file(params[:file_upload],
97
- { folder: params[:folder], dimension: params['dimension'], formats: params[:formats], versions: params[:versions],
98
- thumb_size: params[:thumb_size] }.merge(settings))
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(params.require(:post_tag).permit(:name, :slug, :description, :parent_id))
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(params.require(:post_tag).permit(:name, :slug, :description, :parent_id))
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.find_by_id(params[:post_type_id]).decorate
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.find_by_id(params[:id])
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 params[:post_id].present?
13
- @post_draft = CamaleonCms::Post.drafts.where(post_parent: params[:post_id]).first
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
- @post_draft.set_params(params[:meta], params[:field_options], @post_data[:keywords])
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 = CamaleonCms::Post.drafts.find(params[:id])
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
- @post_draft.set_params(params[:meta], params[:field_options], params[:options])
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.require(:post).permit(:title, :slug, :content, :excerpt, :status, :comment_status, :post_parent, :visibility, :visibility_value, :post_order, :published_at).to_h
56
- post_data.delete(:created_at) unless params[:post][:created_at].present?
57
- post_data.delete(:updated_at) unless params[:post][:updated_at].present?
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
- post_data[:post_parent] = params[:post_id]
60
- post_data[:user_id] = cama_current_user.id unless post_data[:user_id].present?
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