camaleon_cms 2.9.2 → 2.9.3

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