camaleon_cms 2.9.1 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -9
  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 +16 -10
  8. data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
  9. data/app/apps/plugins/front_cache/front_cache_helper.rb +57 -24
  10. data/app/apps/plugins/visibility_post/config/config.json +1 -0
  11. data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
  12. data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
  13. data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
  14. data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
  15. data/app/apps/themes/default/assets/css/style.css.scss +24 -0
  16. data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
  17. data/app/apps/themes/default/default_helper.rb +2 -1
  18. data/app/apps/themes/default/views/category.html.erb +1 -1
  19. data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
  20. data/app/apps/themes/default/views/post_tag.html.erb +1 -1
  21. data/app/apps/themes/default/views/post_type.html.erb +1 -1
  22. data/app/apps/themes/default/views/search.html.erb +1 -1
  23. data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
  24. data/app/apps/themes/new/custom_helper.rb +5 -9
  25. data/app/apps/themes/new/views/category.html.erb +1 -1
  26. data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
  27. data/app/apps/themes/new/views/post_tag.html.erb +1 -1
  28. data/app/apps/themes/new/views/post_type.html.erb +1 -1
  29. data/app/apps/themes/new/views/search.html.erb +1 -1
  30. data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
  31. data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
  32. data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
  33. data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
  34. data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +29 -10
  35. data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
  36. data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +9 -2
  37. data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +4 -3
  38. data/app/controllers/camaleon_cms/admin/appearances/widgets/sidebar_controller.rb +2 -2
  39. data/app/controllers/camaleon_cms/admin/categories_controller.rb +27 -8
  40. data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
  41. data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
  42. data/app/controllers/camaleon_cms/admin/media_controller.rb +84 -19
  43. data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
  44. data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +15 -6
  45. data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
  46. data/app/controllers/camaleon_cms/admin/posts_controller.rb +58 -22
  47. data/app/controllers/camaleon_cms/admin/sessions_controller.rb +238 -45
  48. data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +103 -18
  49. data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +17 -5
  50. data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +33 -7
  51. data/app/controllers/camaleon_cms/admin/settings_controller.rb +11 -6
  52. data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +9 -7
  53. data/app/controllers/camaleon_cms/admin/users_controller.rb +89 -18
  54. data/app/controllers/camaleon_cms/admin_controller.rb +144 -26
  55. data/app/controllers/camaleon_cms/apps/plugins_admin_controller.rb +5 -0
  56. data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
  57. data/app/controllers/camaleon_cms/frontend_controller.rb +140 -70
  58. data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +44 -0
  59. data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
  60. data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
  61. data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
  62. data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
  63. data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
  64. data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
  65. data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
  66. data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
  67. data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
  68. data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
  69. data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
  70. data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
  71. data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
  72. data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
  73. data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
  74. data/app/decorators/camaleon_cms/post_decorator.rb +67 -51
  75. data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
  76. data/app/decorators/camaleon_cms/site_decorator.rb +29 -23
  77. data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
  78. data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
  79. data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
  80. data/app/helpers/camaleon_cms/admin/application_helper.rb +21 -24
  81. data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
  82. data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
  83. data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
  84. data/app/helpers/camaleon_cms/admin/post_type_helper.rb +51 -31
  85. data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
  86. data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
  87. data/app/helpers/camaleon_cms/comment_helper.rb +87 -41
  88. data/app/helpers/camaleon_cms/content_helper.rb +13 -6
  89. data/app/helpers/camaleon_cms/email_helper.rb +7 -3
  90. data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
  91. data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +144 -67
  92. data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
  93. data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
  94. data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
  95. data/app/helpers/camaleon_cms/html_helper.rb +73 -38
  96. data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
  97. data/app/helpers/camaleon_cms/session_helper.rb +195 -39
  98. data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
  99. data/app/helpers/camaleon_cms/site_helper.rb +43 -9
  100. data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
  101. data/app/helpers/camaleon_cms/uploader_helper.rb +19 -422
  102. data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
  103. data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
  104. data/app/models/camaleon_cms/ability.rb +65 -105
  105. data/app/models/camaleon_cms/category.rb +7 -5
  106. data/app/models/camaleon_cms/custom_field.rb +29 -6
  107. data/app/models/camaleon_cms/custom_field_group.rb +75 -21
  108. data/app/models/camaleon_cms/custom_fields_relationship.rb +133 -2
  109. data/app/models/camaleon_cms/media.rb +8 -2
  110. data/app/models/camaleon_cms/meta.rb +2 -2
  111. data/app/models/camaleon_cms/nav_menu.rb +6 -2
  112. data/app/models/camaleon_cms/nav_menu_item.rb +18 -8
  113. data/app/models/camaleon_cms/plugin.rb +4 -3
  114. data/app/models/camaleon_cms/post.rb +113 -27
  115. data/app/models/camaleon_cms/post_comment.rb +10 -6
  116. data/app/models/camaleon_cms/post_default.rb +42 -12
  117. data/app/models/camaleon_cms/post_relationship.rb +6 -4
  118. data/app/models/camaleon_cms/post_tag.rb +4 -3
  119. data/app/models/camaleon_cms/post_type.rb +22 -12
  120. data/app/models/camaleon_cms/site.rb +40 -28
  121. data/app/models/camaleon_cms/term_relationship.rb +10 -7
  122. data/app/models/camaleon_cms/term_taxonomy.rb +51 -5
  123. data/app/models/camaleon_cms/theme.rb +3 -3
  124. data/app/models/camaleon_cms/user.rb +17 -5
  125. data/app/models/camaleon_cms/user_role.rb +77 -4
  126. data/app/models/camaleon_cms/widget/assigned.rb +11 -6
  127. data/app/models/camaleon_cms/widget/main.rb +13 -7
  128. data/app/models/camaleon_cms/widget/sidebar.rb +3 -6
  129. data/app/models/camaleon_record.rb +123 -5
  130. data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +20 -38
  131. data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
  132. data/app/models/concerns/camaleon_cms/custom_fields_read.rb +115 -58
  133. data/app/models/concerns/camaleon_cms/metas.rb +32 -7
  134. data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
  135. data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
  136. data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
  137. data/app/models/current_request.rb +23 -0
  138. data/app/uploaders/camaleon_cms_aws_uploader.rb +39 -11
  139. data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
  140. data/app/uploaders/camaleon_cms_uploader.rb +23 -10
  141. data/app/validators/camaleon_cms/post_uniq_validator.rb +21 -8
  142. data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
  143. data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
  144. data/app/views/camaleon_cms/admin/appearances/nav_menus/_left_menu_items.html.erb +2 -2
  145. data/app/views/camaleon_cms/admin/appearances/widgets/main/form.html.erb +1 -1
  146. data/app/views/camaleon_cms/admin/categories/index.html.erb +3 -3
  147. data/app/views/camaleon_cms/admin/comments/index.html.erb +2 -2
  148. data/app/views/camaleon_cms/admin/comments/list.html.erb +1 -1
  149. data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
  150. data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
  151. data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
  152. data/app/views/camaleon_cms/admin/post_tags/index.html.erb +3 -3
  153. data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +2 -2
  154. data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
  155. data/app/views/camaleon_cms/admin/posts/index.html.erb +6 -6
  156. data/app/views/camaleon_cms/admin/search.html.erb +1 -1
  157. data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
  158. data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
  159. data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
  160. data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
  161. data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +42 -6
  162. data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
  163. data/app/views/camaleon_cms/admin/settings/custom_fields/fields/_select_eval.html.erb +1 -1
  164. data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +5 -4
  165. data/app/views/camaleon_cms/admin/settings/custom_fields/index.html.erb +1 -1
  166. data/app/views/camaleon_cms/admin/settings/post_types/index.html.erb +1 -1
  167. data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
  168. data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
  169. data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
  170. data/app/views/camaleon_cms/admin/settings/sites/index.html.erb +1 -1
  171. data/app/views/camaleon_cms/admin/user_roles/form.html.erb +102 -7
  172. data/app/views/camaleon_cms/admin/user_roles/index.html.erb +1 -1
  173. data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
  174. data/app/views/camaleon_cms/admin/users/index.html.erb +2 -2
  175. data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
  176. data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
  177. data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
  178. data/app/views/layouts/camaleon_cms/admin/_flash_messages.html.erb +2 -2
  179. data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
  180. data/config/initializers/action_view.rb +42 -5
  181. data/config/initializers/active_record_extension.rb +13 -27
  182. data/config/initializers/assets.rb +52 -23
  183. data/config/initializers/custom_initializers.rb +3 -3
  184. data/config/locales/camaleon_cms/admin/ar.yml +7 -2
  185. data/config/locales/camaleon_cms/admin/de.yml +7 -2
  186. data/config/locales/camaleon_cms/admin/en.yml +39 -2
  187. data/config/locales/camaleon_cms/admin/es.yml +7 -2
  188. data/config/locales/camaleon_cms/admin/fr.yml +7 -2
  189. data/config/locales/camaleon_cms/admin/it.yml +7 -2
  190. data/config/locales/camaleon_cms/admin/nl.yml +8 -2
  191. data/config/locales/camaleon_cms/admin/pt-BR.yml +7 -2
  192. data/config/locales/camaleon_cms/admin/pt.yml +7 -2
  193. data/config/locales/camaleon_cms/admin/ru.yml +7 -2
  194. data/config/locales/camaleon_cms/admin/uk.yml +7 -2
  195. data/config/locales/camaleon_cms/admin/zh-CH.yml +7 -2
  196. data/config/routes/admin.rb +33 -15
  197. data/config/routes/frontend.rb +21 -15
  198. data/db/migrate/20150611161134_post_table_into_utf8.rb +14 -14
  199. data/db/migrate/20150926095310_rename_column_posts.rb +3 -3
  200. data/db/migrate/20151212095328_add_confirm_token_to_users.rb +3 -3
  201. data/db/migrate/20160504155652_add_feature_to_posts.rb +1 -1
  202. data/db/migrate/20160504155653_move_first_name_of_users.rb +2 -2
  203. data/db/migrate/20160609121449_add_group_to_custom_field_values.rb +1 -1
  204. data/db/migrate/20161215202255_drop_user_relationship_table.rb +1 -1
  205. data/db/migrate/20180124132318_create_media.rb +1 -1
  206. data/db/migrate/20180704211100_adjust_field_length.rb +1 -1
  207. data/lib/camaleon_cms/assets_precompile.rb +86 -0
  208. data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
  209. data/lib/camaleon_cms/content_security.rb +158 -0
  210. data/lib/camaleon_cms/engine.rb +47 -9
  211. data/lib/camaleon_cms/media_security_headers.rb +27 -0
  212. data/lib/camaleon_cms/setup_token.rb +69 -0
  213. data/lib/camaleon_cms/svg_content_checker.rb +123 -0
  214. data/lib/camaleon_cms/task_reporter.rb +14 -0
  215. data/lib/camaleon_cms/unsafe_markup.rb +207 -0
  216. data/lib/camaleon_cms/uploader_content_security.rb +248 -0
  217. data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
  218. data/lib/camaleon_cms/uploader_path_security.rb +146 -0
  219. data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
  220. data/lib/camaleon_cms/uploader_support.rb +86 -0
  221. data/lib/camaleon_cms/version.rb +1 -1
  222. data/lib/camaleon_cms.rb +11 -0
  223. data/lib/ext/hash.rb +28 -2
  224. data/lib/ext/string.rb +13 -8
  225. data/lib/ext/translator.rb +10 -2
  226. data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
  227. data/lib/generators/camaleon_cms/install_generator.rb +7 -4
  228. data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
  229. data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
  230. data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
  231. data/lib/plugin_routes.rb +412 -368
  232. data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
  233. data/lib/tasks/cross_site_field_groups.rake +74 -0
  234. data/lib/tasks/custom_fields_roles.rake +58 -0
  235. data/lib/tasks/orphaned_comments.rake +42 -0
  236. data/lib/tasks/private_upload_acls.rake +31 -0
  237. data/lib/tasks/site_custom_field_groups.rake +36 -0
  238. data/lib/tasks/unsafe_stored_content.rake +42 -0
  239. data/lib/tasks/unsafe_stored_uploads.rake +70 -0
  240. data/lib/tasks/user_field_groups.rake +27 -0
  241. metadata +185 -7
@@ -3,6 +3,8 @@ module CamaleonCms
3
3
  module Settings
4
4
  class CustomFieldsController < CamaleonCms::Admin::SettingsController
5
5
  add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.custom_fields'), :cama_admin_settings_custom_fields_path
6
+
7
+ before_action :validate_role, only: %i[create update destroy]
6
8
  before_action :set_custom_field_group, only: %i[show edit update destroy]
7
9
  before_action :set_post_data, only: %i[create update]
8
10
 
@@ -59,22 +61,38 @@ module CamaleonCms
59
61
  # reorder custom fields group
60
62
  def reorder
61
63
  params[:values].to_a.each_with_index do |value, index|
62
- current_site.custom_field_groups.find(value).update_column('field_order', index)
64
+ current_site.custom_field_groups.find(value)
65
+ .update_column(:field_order, index) # rubocop:disable Rails/SkipsModelValidations
63
66
  end
64
67
  json = { size: params[:values].size }
65
68
  render json: json
66
69
  end
67
70
 
68
71
  def list
69
- p = params.permit(:post_type, :post_id, categories: [])
70
- args = {}
71
- if p[:post_id].present?
72
- post = @current_site.the_post(p[:post_id].to_i)
73
- post.update_categories(p[:categories])
72
+ p = params.permit(:post_type, :post_id)
73
+ cat_ids = current_site.full_categories.where(id: params[:categories]).pluck(:id)
74
+ if p[:post_id].present? && (post = current_site.the_post(p[:post_id].to_i)).present?
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?
87
+ args = {}
74
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
75
93
  post = CamaleonCms::Post.new
76
- post.taxonomy_id = p[:post_type].to_i
77
- args[:cat_ids] = p[:categories]
94
+ post.taxonomy_id = post_type&.id
95
+ args = { cat_ids: cat_ids }
78
96
  end
79
97
  render partial: 'camaleon_cms/admin/settings/custom_fields/render',
80
98
  locals: { record: post, field_groups: post.get_field_groups(args),
@@ -84,9 +102,58 @@ module CamaleonCms
84
102
  private
85
103
 
86
104
  def set_post_data
87
- @post_data = params.require(:custom_field_group).permit!
88
- @post_data[:object_class], @post_data[:objectid] = @post_data.delete(:assign_group).split(',')
105
+ @post_data = params.require(:custom_field_group).permit(:name, :description, :assign_group, :caption)
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'
89
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'
153
+ end
154
+
155
+ def validate_role
156
+ authorize! :manage, :custom_fields
90
157
  end
91
158
 
92
159
  def set_custom_field_group
@@ -96,21 +163,39 @@ module CamaleonCms
96
163
  redirect_to cama_admin_path
97
164
  end
98
165
 
166
+ def permitted_fields
167
+ return {} if params[:fields].blank?
168
+
169
+ params.require(:fields).permit(params[:fields].keys.index_with do
170
+ %i[id name slug description field_order]
171
+ end).to_h
172
+ end
173
+
174
+ def permitted_field_options
175
+ return {} if params[:field_options].blank?
176
+
177
+ params.require(:field_options).permit(params[:field_options].keys.index_with do
178
+ [:field_key, :multiple, :required, :translate, :default_value, :dimension, :width, :height, :class,
179
+ :placeholder, { default_values: [], multiple_options: %i[title value default] }]
180
+ end).to_h
181
+ end
182
+
99
183
  # return boolean: true if all fields were saved successfully
100
184
  def _save_fields(group)
101
- errors_saved, _all_fields = group.add_fields(params[:fields] ? params.require(:fields).permit! : {},
102
- params[:field_options] ? params.require(:field_options).permit! : {})
185
+ errors_saved, _all_fields = group.add_fields(permitted_fields, permitted_field_options)
103
186
  group.set_option('caption', @caption)
104
187
  if errors_saved.present?
105
- flash[:error] = "<b>#{t('camaleon_cms.errors_found_msg', default: 'Several errors were found, please check.')}</b><br>#{errors_saved.map do |field|
106
- "#{field.name}: " + field.errors.messages.map do |k, v|
107
- "#{k.to_s.titleize}: #{v.join('|')}"
108
- end.join(', ').to_s
109
- end.join('<br>')}"
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
110
196
  else
111
197
  flash[:notice] = t('camaleon_cms.admin.custom_field.message.custom_updated')
112
198
  end
113
- true
114
199
  end
115
200
  end
116
201
  end
@@ -4,6 +4,7 @@ module CamaleonCms
4
4
  class PostTypesController < CamaleonCms::Admin::SettingsController
5
5
  before_action :set_post_type, only: %i[show edit update destroy]
6
6
  before_action :set_data_term, only: %i[create update]
7
+
7
8
  add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.content_groups'), :cama_admin_settings_post_types_path
8
9
 
9
10
  def index
@@ -20,7 +21,7 @@ module CamaleonCms
20
21
 
21
22
  def update
22
23
  if @post_type.update(@data_term)
23
- @post_type.set_field_values(params.require(:field_options).permit!) if params[:field_options].present?
24
+ @post_type.set_field_values(cama_permitted_field_options('PostType')) if params[:field_options].present?
24
25
  hooks_run('updated_post_type', { post_type: @post_type })
25
26
  flash[:notice] = t('camaleon_cms.admin.post_type.message.updated')
26
27
  redirect_to action: :index
@@ -32,7 +33,7 @@ module CamaleonCms
32
33
  def create
33
34
  @post_type = current_site.post_types.new(@data_term)
34
35
  if @post_type.save
35
- @post_type.set_field_values(params.require(:field_options).permit!) if params[:field_options].present?
36
+ @post_type.set_field_values(cama_permitted_field_options('PostType')) if params[:field_options].present?
36
37
  hooks_run('created_post_type', { post_type: @post_type })
37
38
  flash[:notice] = t('camaleon_cms.admin.post_type.message.created')
38
39
  redirect_to action: :index
@@ -49,13 +50,24 @@ module CamaleonCms
49
50
  private
50
51
 
51
52
  def set_data_term
52
- data_term = params.require(:post_type).permit!
53
- data_term[:data_options] = params.require(:meta).permit!
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)
57
+ data_term[:data_options] = params[:meta].present? ? post_type_meta_params : {}
54
58
  @data_term = data_term
55
59
  end
56
60
 
61
+ def post_type_meta_params
62
+ params.require(:meta).permit(:icon, :has_layout, :default_layout, :has_template, :default_template,
63
+ :has_category, :has_single_category, :has_tags, :has_content, :has_summary,
64
+ :has_comments, :has_featured, :has_seo, :has_parent_structure, :has_picture,
65
+ :posts_image_dimension, :posts_thumb_versions, :posts_thumb_size,
66
+ :is_required_picture, :contents_route_format, :default_thumb)
67
+ end
68
+
57
69
  def set_post_type
58
- @post_type = current_site.post_types.find_by_id(params[:id])
70
+ @post_type = current_site.post_types.find_by(id: params[:id])
59
71
  rescue StandardError
60
72
  flash[:error] = t('camaleon_cms.admin.post_type.message.error')
61
73
  redirect_to cama_admin_path
@@ -23,11 +23,11 @@ module CamaleonCms
23
23
 
24
24
  def update
25
25
  tmp = @site.slug
26
- if @site.update(params.require(:site).permit!)
26
+ if @site.update(site_params)
27
27
  save_metas(@site)
28
28
  flash[:notice] = t('camaleon_cms.admin.sites.message.updated')
29
29
  if @site.id == Cama::Site.main_site.id && tmp != @site.slug
30
- redirect_to @site.the_admin_url
30
+ redirect_to(cama_admin_path)
31
31
  else
32
32
  redirect_to action: :index
33
33
  end
@@ -43,12 +43,11 @@ module CamaleonCms
43
43
  end
44
44
 
45
45
  def create
46
- site_data = params.require(:site).permit!
47
- @site = CamaleonCms::Site.new(site_data)
46
+ @site = CamaleonCms::Site.new(site_params)
48
47
  if @site.save
49
48
  save_metas(@site)
50
49
  site_after_install(@site, @site.get_theme_slug)
51
- flash[:notice] = t('camaleon_cms.admin.sites.message.created')
50
+ flash[:notice] = site_created_notice(@site)
52
51
  redirect_to action: :index
53
52
  else
54
53
  new
@@ -63,7 +62,7 @@ module CamaleonCms
63
62
  private
64
63
 
65
64
  def save_metas(site)
66
- return unless params[:metas].present?
65
+ return if params[:metas].blank?
67
66
 
68
67
  params[:metas].each do |meta, val|
69
68
  site.set_meta(meta, val)
@@ -71,7 +70,7 @@ module CamaleonCms
71
70
  end
72
71
 
73
72
  def set_site
74
- @site = CamaleonCms::Site.find_by_id(params[:id]).decorate
73
+ @site = CamaleonCms::Site.find_by(id: params[:id]).decorate
75
74
  rescue StandardError
76
75
  flash[:error] = t('camaleon_cms.admin.sites.message.error')
77
76
  redirect_to cama_admin_path
@@ -84,6 +83,33 @@ module CamaleonCms
84
83
  flash[:error] = t('camaleon_cms.admin.sites.message.unauthorized')
85
84
  redirect_to cama_admin_path
86
85
  end
86
+
87
+ def site_params
88
+ params.require(:site).permit(:name, :slug, :description)
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
87
113
  end
88
114
  end
89
115
  end
@@ -1,8 +1,11 @@
1
1
  module CamaleonCms
2
2
  module Admin
3
3
  class SettingsController < CamaleonCms::AdminController
4
+ include CamaleonCms::Admin::CustomFieldsConcern
5
+
4
6
  before_action :validate_role, except: %i[theme save_theme]
5
7
  before_action :validate_role_theme, only: %i[theme save_theme]
8
+
6
9
  add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.settings')
7
10
 
8
11
  def index
@@ -19,10 +22,10 @@ module CamaleonCms
19
22
  def site_saved
20
23
  @site = current_site
21
24
  cache_slug = @site.slug
22
- if @site.update(params.require(:site).permit!)
25
+ if @site.update(params.require(:site).permit(:name, :slug, :description))
23
26
  @site.set_options(params[:options]) if params[:options].present?
24
27
  @site.set_metas(params[:metas]) if params[:metas].present?
25
- @site.set_field_values(params[:field_options])
28
+ @site.set_field_values(cama_permitted_field_options('Site'))
26
29
  flash[:notice] = t('camaleon_cms.admin.settings.message.site_updated')
27
30
  args = { action: :site }
28
31
  args[:host], args[:port] = @site.get_domain.to_s.split(':') if cache_slug != @site.slug
@@ -59,11 +62,13 @@ module CamaleonCms
59
62
  end
60
63
 
61
64
  def save_theme
62
- current_theme.set_field_values(params[:theme_fields]) if params[:theme_fields].present?
65
+ current_theme.set_field_values(cama_permitted_field_options('Theme', param_key: :theme_fields))
63
66
  current_theme.set_options(params[:theme_option]) if params[:theme_option].present?
64
67
  current_theme.set_metas(params[:theme_meta]) if params[:theme_meta].present?
65
- current_theme.set_field_values(params[:field_options])
66
- hook_run(current_theme.settings, 'on_theme_settings', current_theme) # permit to save extra/custom values by this hook
68
+ current_theme.set_field_values(cama_permitted_field_options('Theme'))
69
+
70
+ # permit saving extra/custom values by this hook
71
+ hook_run(current_theme.settings, 'on_theme_settings', current_theme)
67
72
  flash[:notice] = t('camaleon_cms.admin.message.updated_success', default: 'Theme updated successfully')
68
73
  redirect_to action: :theme
69
74
  end
@@ -74,7 +79,7 @@ module CamaleonCms
74
79
  CamaleonCms::HtmlMailer.sender(params[:email], 'Test', data).deliver_now
75
80
  head :ok
76
81
  rescue StandardError => e
77
- render inline: e.message, status: 502
82
+ render plain: e.message, status: :bad_gateway
78
83
  end
79
84
 
80
85
  private
@@ -20,13 +20,15 @@ module CamaleonCms
20
20
  end
21
21
 
22
22
  def create
23
- user_role_data = params.require(:user_role).permit!
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.set_meta("_post_type_#{current_site.id}",
27
- defined?(params[:rol_values][:post_type]) ? params[:rol_values][:post_type] : {})
28
- @user_role.set_meta("_manager_#{current_site.id}",
29
- defined?(params[:rol_values][:post_type]) ? params[:rol_values][:manager] : {})
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
@@ -40,8 +42,8 @@ module CamaleonCms
40
42
  end
41
43
 
42
44
  def update
43
- if @user_role.update(params.require(:user_role).permit!)
44
- if @user_role.editable?
45
+ if @user_role.update(params.require(:user_role).permit(:name, :slug, :description))
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}",
@@ -1,8 +1,17 @@
1
1
  module CamaleonCms
2
2
  module Admin
3
3
  class UsersController < CamaleonCms::AdminController
4
+ include CamaleonCms::Admin::CustomFieldsConcern
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
+
4
11
  before_action :validate_role, except: %i[profile profile_edit]
12
+
5
13
  add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.users'), :cama_admin_users_url
14
+
6
15
  before_action :set_user, only: %i[show edit update destroy impersonate]
7
16
 
8
17
  def index
@@ -12,7 +21,16 @@ module CamaleonCms
12
21
 
13
22
  def profile
14
23
  add_breadcrumb I18n.t('camaleon_cms.admin.users.profile')
15
- @user = params[:user_id].present? ? current_site.the_user(params[:user_id].to_i).object : cama_current_user.object
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
+
16
34
  edit
17
35
  end
18
36
 
@@ -31,8 +49,8 @@ module CamaleonCms
31
49
  r = { user: @user }
32
50
  hooks_run('user_update', r)
33
51
  if @user.update(user_params)
34
- @user.set_metas(params[:meta]) if params[:meta].present?
35
- @user.set_field_values(params[:field_options])
52
+ @user.set_metas(user_meta_params) if params[:meta].present?
53
+ @user.set_field_values(cama_permitted_field_options(user_field_scope)) if params[:field_options].present?
36
54
  r = { user: @user, message: t('camaleon_cms.admin.users.message.updated'), params: params }
37
55
  hooks_run('user_after_edited', r)
38
56
  flash[:notice] = r[:message]
@@ -50,17 +68,33 @@ module CamaleonCms
50
68
 
51
69
  # update some ajax requests from profile or user form
52
70
  def updated_ajax
53
- @user = current_site.users.find(params[:user_id])
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
+
54
78
  update_session = current_user_is?(@user)
55
79
  attrs = params.require(:password).permit(%i[password password_confirmation])
56
80
  @user.update(password: attrs.require(:password), password_confirmation: attrs.require(:password_confirmation))
57
81
 
58
- return render inline: @user.errors.full_messages.join(', '), status: :unprocessable_entity if @user.errors.any?
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?
59
87
 
60
88
  # keep user logged in when changing their own password
61
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
62
96
  rescue ActionController::ParameterMissing => e
63
- render inline: "ERROR: #{e.class.name}, #{e.message}", status: :bad_request
97
+ render plain: "ERROR: #{e.class.name}, #{e.message}", status: :bad_request
64
98
  end
65
99
 
66
100
  def update_auth_token_in_cookie(token)
@@ -68,7 +102,10 @@ module CamaleonCms
68
102
 
69
103
  current_token = cookie_split_auth_token
70
104
  updated_token = [token, *current_token[1..]]
71
- cookies[:auth_token] = updated_token.join('&')
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('&'))
72
109
  end
73
110
 
74
111
  def current_user_is?(user)
@@ -93,13 +130,12 @@ module CamaleonCms
93
130
  end
94
131
 
95
132
  def create
96
- user_data = params.require(:user).permit!
97
- @user = current_site.users.new(user_data)
133
+ @user = current_site.users.new(user_params)
98
134
  r = { user: @user }
99
135
  hooks_run('user_create', r)
100
136
  if @user.save
101
- @user.set_metas(params[:meta]) if params[:meta].present?
102
- @user.set_field_values(params[:field_options])
137
+ @user.set_metas(user_meta_params) if params[:meta].present?
138
+ @user.set_field_values(cama_permitted_field_options(user_field_scope)) if params[:field_options].present?
103
139
  r = { user: @user }
104
140
  hooks_run('user_created', r)
105
141
  flash[:notice] = t('camaleon_cms.admin.users.message.created')
@@ -130,20 +166,55 @@ module CamaleonCms
130
166
  private
131
167
 
132
168
  def validate_role
133
- (user_id_param.present? && cama_current_user.id.to_s == user_id_param) || authorize!(:manage, :users)
169
+ return if self_target_own_record?
170
+
171
+ authorize! :manage, :users
134
172
  end
135
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
136
186
  def user_id_param
137
- params[:id] || params[:user_id]
187
+ user_id = params[:user_id]
188
+ return params[:id] unless user_id.is_a?(String)
189
+
190
+ user_id
138
191
  end
139
192
 
140
193
  def user_params
141
- parameters = params.require(:user)
142
- if cama_current_user.role_grantor?(@user)
143
- parameters.permit(:username, :email, :role, :first_name, :last_name)
144
- else
145
- parameters.permit(:username, :email, :first_name, :last_name)
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]
146
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)
206
+ p
207
+ end
208
+
209
+ def user_meta_params
210
+ params.require(:meta).permit(:avatar, :slogan)
211
+ end
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
147
218
  end
148
219
 
149
220
  def set_user