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
@@ -236,10 +236,13 @@ function cama_init_post(obj) {
236
236
  return false;
237
237
  });
238
238
 
239
- /* Disabled until fix to reload only category fields and not all
239
+ /* Disabled until fix to reload only category fields and not all.
240
+ NOTE (M6): custom_fields#list performs its category write only on a CSRF-verified POST, so
241
+ this must stay $.post when re-enabled (jquery_ujs attaches the CSRF token) — with $.get the
242
+ fields would still render but the post's categories would silently not be saved.
240
243
  $form.on("change", ".list-categories input", function () {
241
244
  showLoading();
242
- $.get(
245
+ $.post(
243
246
  $form.find("#post_add_new_category").data('fields-reload-url'), {
244
247
  categories: $form.find("#post_right_bar .list-categories input[name='categories[]']:checked").map(function(i, el){ return $(this).val(); }).get(),
245
248
  post_id: post_id
@@ -111,9 +111,20 @@ $(function() {
111
111
  return false
112
112
 
113
113
  showLoading()
114
- $.get(link.attr('href'), function() {
115
- link.closest('.dd-item').remove()
116
- hideLoading()
114
+ // Security (audit M6): the route is DELETE-only. jquery_ujs' ajax prefilter attaches the
115
+ // X-CSRF-Token header to every same-origin request (it gates on crossDomain, not the verb), so
116
+ // this DELETE carries the token.
117
+ $.ajax({
118
+ url: link.attr('href'),
119
+ type: 'DELETE',
120
+ success() {
121
+ link.closest('.dd-item').remove()
122
+ },
123
+ // Hide the overlay on every outcome -- a failed DELETE (e.g. a stale CSRF token after the
124
+ // session rotated) would otherwise leave the full-screen spinner blocking the admin.
125
+ complete() {
126
+ hideLoading()
127
+ }
117
128
  })
118
129
  return false
119
130
  })
@@ -143,6 +143,11 @@ window.cama_init_media = function(mediaPanel) {
143
143
  const customFileData = function() {
144
144
  const r = window.cama_media_get_custom_params()
145
145
  r.skip_auto_crop = true
146
+ // Security (audit M7): the uploadFile plugin posts multipart data outside the UJS ajax
147
+ // prefilter, so the CSRF token must ride as a form field (media#upload no longer skips it).
148
+ // Read the meta tag directly so this does not depend on any UJS library being loaded.
149
+ const csrfMeta = document.querySelector('meta[name="csrf-token"]')
150
+ r.authenticity_token = csrfMeta && csrfMeta.getAttribute('content')
146
151
  return r
147
152
  }
148
153
 
@@ -2,7 +2,7 @@ module CamaleonCms
2
2
  module Admin
3
3
  module Appearances
4
4
  class NavMenusController < CamaleonCms::AdminController
5
- include CamaleonCms::Frontend::NavMenuHelper
5
+ include CamaleonCms::Admin::CustomFieldsConcern
6
6
 
7
7
  add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.appearance')
8
8
  add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.menus')
@@ -10,9 +10,9 @@ module CamaleonCms
10
10
 
11
11
  def index
12
12
  @nav_menu = if params[:id].present?
13
- current_site.nav_menus.find_by_id(params[:id])
13
+ current_site.nav_menus.find_by(id: params[:id])
14
14
  elsif params[:slug].present?
15
- current_site.nav_menus.find_by_slug(params[:slug])
15
+ current_site.nav_menus.find_by_slug(params[:slug]) # rubocop:disable Rails/DynamicFindBy
16
16
  else
17
17
  current_site.nav_menus.first
18
18
  end
@@ -27,7 +27,7 @@ module CamaleonCms
27
27
  end
28
28
 
29
29
  def create
30
- nav_menu = current_site.nav_menus.new(params.require(:nav_menu).permit!)
30
+ nav_menu = current_site.nav_menus.new(nav_menu_params)
31
31
  nav_menu.save
32
32
  flash[:notice] = t('.created_menu', default: 'Created Menu')
33
33
  redirect_to action: :index, id: nav_menu.id
@@ -35,7 +35,7 @@ module CamaleonCms
35
35
 
36
36
  def update
37
37
  nav_menu = current_site.nav_menus.find(params[:id])
38
- nav_menu.update(params.require(:nav_menu).permit!)
38
+ nav_menu.update(nav_menu_params)
39
39
  flash[:notice] = t('.updated_menu', default: 'Menu updated')
40
40
  redirect_to action: :index, id: nav_menu.id
41
41
  end
@@ -59,7 +59,7 @@ module CamaleonCms
59
59
 
60
60
  def save_custom_settings
61
61
  @nav_menu_item = current_site.nav_menu_items.find(params[:id])
62
- @nav_menu_item.set_field_values(params.require(:field_options).permit!)
62
+ @nav_menu_item.set_field_values(cama_permitted_field_options('NavMenuItem'))
63
63
  head :ok
64
64
  end
65
65
 
@@ -77,7 +77,7 @@ module CamaleonCms
77
77
  @nav_menu = current_site.nav_menus.find(params[:nav_menu_id])
78
78
  item = current_site.nav_menu_items.find(params[:id])
79
79
  item.update_menu_item(parse_external_menu(params))
80
- item.set_options(params.require(:options).permit!) if params[:options].present?
80
+ item.set_options(permitted_external_options) if params[:options].present?
81
81
  render partial: 'menu_items', locals: { items: [item], nav_menu: @nav_menu }
82
82
  end
83
83
 
@@ -90,7 +90,10 @@ module CamaleonCms
90
90
  # update the reorder of items
91
91
  def reorder_items(items = nil, parent_id = nil, is_root = true)
92
92
  items = params[:items] if items.nil?
93
- 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?
94
97
  items.each do |index, _item|
95
98
  item = current_site.nav_menu_items.find(_item['id'])
96
99
  item.update(parent_id: parent_id, term_order: index)
@@ -106,13 +109,13 @@ module CamaleonCms
106
109
  external_params = params[:external]
107
110
  if external_params.present?
108
111
  external_item = @nav_menu.append_menu_item(parse_external_menu(external_params))
109
- external_item.set_options(external_params.require(:options).permit!) if external_params[:options].present?
112
+ external_item.set_options(permitted_external_options(external_params)) if external_params[:options].present?
110
113
  items << external_item
111
114
  end
112
115
 
113
116
  if params[:custom_items].present? # custom menu items
114
117
  params[:custom_items].each_value do |custom_item|
115
- type = custom_item['kind'].present? ? custom_item['kind'] : 'external'
118
+ type = custom_item['kind'].presence || 'external'
116
119
  items << @nav_menu.append_menu_item({ label: custom_item['label'], link: custom_item['url'], type: type })
117
120
  end
118
121
  end
@@ -136,6 +139,22 @@ module CamaleonCms
136
139
  def parse_external_menu(_params)
137
140
  { label: _params[:external_label], link: _params[:external_url], type: 'external', target: _params[:target] }
138
141
  end
142
+
143
+ # Strong parameters for nav_menu
144
+ def nav_menu_params
145
+ params.require(:nav_menu).permit(:name, :slug)
146
+ end
147
+
148
+ # Only permit external menu options that match registered custom field slug
149
+ def permitted_external_options(external_params = nil)
150
+ opts = external_params ? external_params[:options] : params[:options]
151
+ return {} if opts.blank?
152
+
153
+ allowed_keys = cama_custom_field_allowed_slugs('NavMenuItem')
154
+ return {} if allowed_keys.blank?
155
+
156
+ opts.permit(*allowed_keys).to_h
157
+ end
139
158
  end
140
159
  end
141
160
  end
@@ -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
@@ -3,6 +3,8 @@ module CamaleonCms
3
3
  module Appearances
4
4
  module Widgets
5
5
  class AssignController < CamaleonCms::AdminController
6
+ include CamaleonCms::Admin::CustomFieldsConcern
7
+
6
8
  before_action :check_permission_role
7
9
  before_action :find_sidebar
8
10
  before_action :find_assigned_sidebar, only: %i[update destroy]
@@ -15,8 +17,13 @@ module CamaleonCms
15
17
  end
16
18
 
17
19
  def update
18
- if @assigned.update(params.require(:assign).permit!)
19
- @assigned.set_field_values(params[:field_options])
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'))
20
27
  flash[:notice] = t('camaleon_cms.admin.widgets.assign.updated')
21
28
  else
22
29
  flash[:error] = t('camaleon_cms.admin.widgets.assign.error_updated')
@@ -22,8 +22,9 @@ module CamaleonCms
22
22
  end
23
23
 
24
24
  def create
25
- params[:widget_main][:status] = 'simple'
26
- @widget = current_site.widgets.new(params.require(:widget_main).permit!)
25
+ @widget = current_site
26
+ .widgets.new(params.require(:widget_main).permit(:name, :slug, :description, :excerpt, :renderer))
27
+ @widget.status = 'simple'
27
28
  if @widget.save!
28
29
  flash[:notice] = t('camaleon_cms.admin.widgets.message.created')
29
30
  else
@@ -33,7 +34,7 @@ module CamaleonCms
33
34
  end
34
35
 
35
36
  def update
36
- if @widget.update!(params.require(:widget_main).permit!)
37
+ if @widget.update!(params.require(:widget_main).permit(:name, :slug, :description, :excerpt, :renderer))
37
38
  flash[:notice] = t('camaleon_cms.admin.widgets.message.updated')
38
39
  else
39
40
  flash[:error] = t('camaleon_cms.admin.widgets.message.error_updated')
@@ -12,7 +12,7 @@ module CamaleonCms
12
12
  end
13
13
 
14
14
  def create
15
- @sidebar = current_site.sidebars.new(params.require(:widget_sidebar).permit!)
15
+ @sidebar = current_site.sidebars.new(params.require(:widget_sidebar).permit(:name, :slug, :description))
16
16
  if @sidebar.save
17
17
  flash[:notice] = t('camaleon_cms.admin.widgets.sidebar.created')
18
18
  else
@@ -26,7 +26,7 @@ module CamaleonCms
26
26
  end
27
27
 
28
28
  def update
29
- if @sidebar.update(params.require(:widget_sidebar).permit!)
29
+ if @sidebar.update(params.require(:widget_sidebar).permit(:name, :slug, :description))
30
30
  flash[:notice] = t('camaleon_cms.admin.widgets.sidebar.updated')
31
31
  else
32
32
  flash[:error] = t('camaleon_cms.admin.widgets.sidebar.error_updated')
@@ -1,9 +1,13 @@
1
1
  module CamaleonCms
2
2
  module Admin
3
3
  class CategoriesController < CamaleonCms::AdminController
4
+ include CamaleonCms::Admin::CustomFieldsConcern
5
+
4
6
  add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.contents')
7
+
5
8
  before_action :set_post_type
6
9
  before_action :set_category, only: %w[show edit update destroy]
10
+ before_action :validate_category_parent, only: %w[create update]
7
11
 
8
12
  def index
9
13
  @categories = @post_type.categories
@@ -20,9 +24,9 @@ module CamaleonCms
20
24
  def update
21
25
  hooks_run('update_category', { category: @category, post_type: @post_type })
22
26
 
23
- if @category.update(params.require(:category).permit!)
27
+ if @category.update(params.require(:category).permit(:name, :slug, :description, :parent_id))
24
28
  @category.set_options(params[:meta])
25
- @category.set_field_values(params[:field_options])
29
+ @category.set_field_values(cama_permitted_field_options('PostType_Category'))
26
30
  hooks_run('updated_category', { category: @category, post_type: @post_type })
27
31
  flash[:notice] = t('camaleon_cms.admin.post_type.message.updated')
28
32
  redirect_to action: :index
@@ -32,12 +36,12 @@ module CamaleonCms
32
36
  end
33
37
 
34
38
  def create
35
- @category = @post_type.categories.new(params.require(:category).permit!)
39
+ @category = @post_type.categories.new(params.require(:category).permit(:name, :slug, :description, :parent_id))
36
40
  hooks_run('create_category', { category: @category, post_type: @post_type })
37
41
 
38
42
  if @category.save
39
43
  @category.set_options(params[:meta])
40
- @category.set_field_values(params[:field_options])
44
+ @category.set_field_values(cama_permitted_field_options('PostType_Category'))
41
45
  hooks_run('created_category', { category: @category, post_type: @post_type })
42
46
  flash[:notice] = t('camaleon_cms.admin.post_type.message.created')
43
47
  redirect_to action: :index
@@ -48,8 +52,11 @@ module CamaleonCms
48
52
 
49
53
  # return html category list used to reload categories list in post editor form
50
54
  def list
51
- render inline: post_type_html_inputs(@post_type, 'categories', 'categories',
52
- @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
+ )
53
60
  end
54
61
 
55
62
  def destroy
@@ -59,10 +66,21 @@ module CamaleonCms
59
66
 
60
67
  private
61
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
+
62
80
  # define parent post type
63
81
  def set_post_type
64
82
  begin
65
- @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
66
84
  rescue StandardError
67
85
  flash[:error] = t('camaleon_cms.admin.request_error_message')
68
86
  redirect_to cama_admin_path, { error: 'Error Post Type' }
@@ -73,7 +91,8 @@ module CamaleonCms
73
91
  end
74
92
 
75
93
  def set_category
76
- @category = CamaleonCms::Category.find_by_id(params[:id])
94
+ @category = @post_type.categories.find_by(id: params[:id])
95
+ render_404 unless @category
77
96
  rescue StandardError
78
97
  flash[:error] = t('camaleon_cms.admin.post_type.message.error')
79
98
  redirect_to cama_admin_path
@@ -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,55 +3,101 @@ 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
11
+ before_action :verify_media_authorization
8
12
 
9
13
  # render media section
10
14
  def index
11
- authorize! :manage, :media
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
28
72
  def download_private_file
29
73
  cama_uploader.enable_private_mode!
30
74
 
31
- file = cama_uploader.fetch_file("private/#{params[:file]}")
75
+ sanitize_private_filename!
76
+ return render(plain: 'Invalid file', status: :forbidden) unless @private_file_path
77
+
78
+ fetched = cama_uploader.fetch_file(@private_file_path)
32
79
 
33
- return render plain: helpers.sanitize(file[:error]) if file.is_a?(Hash) && file[:error].present?
80
+ return render plain: helpers.sanitize(fetched[:error]) if fetched.is_a?(Hash) && fetched[:error].present?
34
81
 
35
- send_file file, disposition: 'inline'
82
+ send_file fetched, disposition: 'inline'
36
83
  end
37
84
 
38
85
  # render media for modal content
39
86
  def ajax
40
87
  @show_file_actions = true if current_site.get_option('file_actions_in_modals') == 'yes'
41
88
  @tree = cama_uploader.search(params[:search]) if params[:search].present?
42
- @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))
43
90
  @next_page = @files.current_page < @files.total_pages ? @files.current_page + 1 : nil
44
91
  if params[:partial].present?
45
92
  render json: { next_page: @next_page,
46
93
  html: render_to_string(partial: 'render_file_item', locals: { files: @files }) }
47
- else
48
- render 'index', layout: false unless params[:partial].present?
94
+ elsif params[:partial].blank?
95
+ render 'index', layout: false
49
96
  end
50
97
  end
51
98
 
52
99
  # do background actions in fog
53
100
  def actions
54
- authorize! :manage, :media if params[:media_action] != 'crop_url'
55
101
  params[:folder] = params[:folder].gsub('//', '/') if params[:folder].present?
56
102
 
57
103
  case params[:media_action]
@@ -66,9 +112,9 @@ module CamaleonCms
66
112
  when 'crop_url'
67
113
  user_url = params[:url].to_s
68
114
  user_url = "#{current_site.the_url(locale: nil)}#{user_url}" unless user_url.start_with?('data:', 'http')
69
- url_validation_result = UserUrlValidator.validate(user_url)
70
- r = if url_validation_result.is_a?(Array)
71
- { error: url_validation_result.join(', ') }
115
+ url_error = cama_upload_url_error(user_url)
116
+ r = if url_error
117
+ { error: url_error }
72
118
  else
73
119
  cama_tmp_upload(user_url, formats: params[:formats], name: params[:name])
74
120
  end
@@ -91,9 +137,17 @@ module CamaleonCms
91
137
  params[:dimension] = nil if params[:skip_auto_crop].present?
92
138
  f = { error: 'File not found.' }
93
139
  if params[:file_upload].present?
94
- f = upload_file(params[:file_upload],
95
- { folder: params[:folder], dimension: params['dimension'], formats: params[:formats], versions: params[:versions],
96
- 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
+ )
97
151
  end
98
152
 
99
153
  if f[:error].present?
@@ -105,6 +159,17 @@ module CamaleonCms
105
159
 
106
160
  private
107
161
 
162
+ def verify_media_authorization
163
+ authorize! :manage, :media
164
+ end
165
+
166
+ # Sanitizes the private file parameter to prevent path traversal.
167
+ # Sets @private_file_path if valid, nil otherwise.
168
+ def sanitize_private_filename!
169
+ name = File.basename(params[:file].to_s)
170
+ @private_file_path = ("private/#{name}" if name.present? && name == params[:file].to_s)
171
+ end
172
+
108
173
  # init basic media variables
109
174
  def init_media_vars
110
175
  # @cama_uploader = CamaleonCmsLocalUploader.new({current_site: current_site, private: true})
@@ -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
@@ -1,7 +1,10 @@
1
1
  module CamaleonCms
2
2
  module Admin
3
3
  class PostTagsController < CamaleonCms::AdminController
4
+ include CamaleonCms::Admin::CustomFieldsConcern
5
+
4
6
  add_breadcrumb I18n.t('camaleon_cms.admin.sidebar.contents')
7
+
5
8
  before_action :set_post_type
6
9
  before_action :set_post_tag, only: %w[show edit update destroy]
7
10
 
@@ -27,9 +30,9 @@ module CamaleonCms
27
30
  def update
28
31
  args = { post_tag: @post_tag, post_type: @post_type }
29
32
  hooks_run('before_update_post_tag', args)
30
- if @post_tag.update(params.require(:post_tag).permit!)
33
+ if @post_tag.update(post_tag_params)
31
34
  @post_tag.set_options(params[:meta]) if params[:meta].present?
32
- @post_tag.set_field_values(params[:field_options])
35
+ @post_tag.set_field_values(cama_permitted_field_options('PostType_PostTag'))
33
36
  hooks_run('after_update_post_tag', args)
34
37
  flash[:notice] = t('camaleon_cms.admin.post_type.message.updated')
35
38
  redirect_to action: :index
@@ -40,12 +43,12 @@ module CamaleonCms
40
43
 
41
44
  # render post tag create form
42
45
  def create
43
- @post_tag = @post_type.post_tags.new(params.require(:post_tag).permit!)
46
+ @post_tag = @post_type.post_tags.new(post_tag_params)
44
47
  args = { post_tag: @post_tag, post_type: @post_type }
45
48
  hooks_run('before_create_post_tag', args)
46
49
  if @post_tag.save
47
50
  @post_tag.set_options(params[:meta]) if params[:meta].present?
48
- @post_tag.set_field_values(params[:field_options])
51
+ @post_tag.set_field_values(cama_permitted_field_options('PostType_PostTag'))
49
52
  hooks_run('after_create_post_tag', args)
50
53
  flash[:notice] = t('camaleon_cms.admin.post_type.message.created')
51
54
  redirect_to action: :index
@@ -70,15 +73,21 @@ module CamaleonCms
70
73
 
71
74
  private
72
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
+
73
82
  def set_post_type
74
- @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
75
84
  authorize! :post_tags, @post_type
76
85
  add_breadcrumb @post_type.the_title, @post_type.the_admin_url
77
86
  add_breadcrumb t('camaleon_cms.admin.post_type.post_tags'), url_for({ action: :index })
78
87
  end
79
88
 
80
89
  def set_post_tag
81
- @post_tag = @post_type.post_tags.find_by_id(params[:id])
90
+ @post_tag = @post_type.post_tags.find_by(id: params[:id])
82
91
  rescue StandardError
83
92
  flash[:error] = t('camaleon_cms.admin.post_type.message.error')
84
93
  redirect_to cama_admin_path