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
@@ -11,38 +11,45 @@ module CamaleonCms
11
11
  end
12
12
 
13
13
  def post_type_status(status, color = 'default')
14
- "<span class='label label-#{color} label-form'>#{status}</span>"
14
+ content_tag(:span, status, class: "label label-#{color} label-form")
15
15
  end
16
16
 
17
17
  # taxonomies -> (categories || post_tags)
18
- def post_type_list_taxonomy(taxonomies, color = 'primary')
19
- html = ''
20
- taxonomies.decorate.each do |f|
21
- html += "<a class='cama_ajax_request' href='#{cama_admin_post_type_taxonomy_posts_path(@post_type.id,
22
- f.taxonomy, f.id)}'><span class='label label-#{color} label-form'>#{f.the_title}</span></a> "
23
- end
24
- html
18
+ def post_type_list_taxonomy(taxonomies, color = 'primary', post_type = nil)
19
+ # Legacy 2-arg call sites (e.g. overridden admin posts-index views, camaleon-ecommerce) omit
20
+ # post_type and expect the controller's @post_type. Restored from #1178; see regression M23.
21
+ post_type ||= controller.instance_variable_get(:@post_type) if respond_to?(:controller)
22
+ return safe_join([]) if post_type.blank?
23
+
24
+ safe_join(taxonomies.decorate.map do |f|
25
+ link_to(
26
+ cama_admin_post_type_taxonomy_posts_path(post_type.id, f.taxonomy, f.id), class: 'cama_ajax_request'
27
+ ) do
28
+ content_tag(:span, f.the_title, class: "label label-#{color} label-form")
29
+ end
30
+ end, ' ')
25
31
  end
26
32
 
27
33
  # sort array of posts to build post's tree
28
34
  # skip_non_parent_posts: don't include post's where root post doesn't exist
29
35
  # internal control for recursive items
30
- def cama_hierarchy_post_list(posts_list, parent_id = nil, skip_non_parent_posts = false)
36
+ def cama_hierarchy_post_list(posts_list, parent_id = nil, skip_non_parent_posts = false,
37
+ no_parent_accumulator = nil)
38
+ no_parent_accumulator ||= posts_list.clone
31
39
  res = []
32
- @_cama_hierarchy_post_list_no_parent ||= posts_list.clone
33
40
  posts_list.each do |element|
34
41
  next unless element.post_parent.to_s == parent_id.to_s
35
42
 
36
43
  res << element
37
- @_cama_hierarchy_post_list_no_parent.delete_item(element)
38
- res += cama_hierarchy_post_list(posts_list, element.id)
44
+ no_parent_accumulator.delete_item(element)
45
+ res += cama_hierarchy_post_list(posts_list, element.id, false, no_parent_accumulator)
39
46
  end
40
47
 
41
- if !parent_id.present? && !skip_non_parent_posts
42
- @_cama_hierarchy_post_list_no_parent.each do |element|
48
+ if parent_id.blank? && !skip_non_parent_posts
49
+ no_parent_accumulator.each do |element|
43
50
  element.show_title_with_parent = true
44
51
  res << element
45
- res += cama_hierarchy_post_list(posts_list, element.id)
52
+ res += cama_hierarchy_post_list(posts_list, element.id, false, no_parent_accumulator)
46
53
  end
47
54
  end
48
55
  res
@@ -53,24 +60,37 @@ module CamaleonCms
53
60
  def post_type_taxonomy_html_(categories, taxonomy = 'categories', name = 'categories', type = 'checkbox',
54
61
  values = [], class_cat = '', required = false)
55
62
  if categories.count < 1
56
- return t('camaleon_cms.admin.post_type.message.no_created_html',
57
- taxonomy: taxonomy == 'categories' ? t('camaleon_cms.admin.table.categories') : t('camaleon_cms.admin.table.tags')).to_s
63
+ # Interpolate the translated taxonomy label, not the raw 'categories'/'post_tags' slug
64
+ # (the label was computed here and discarded before).
65
+ taxonomy_label = if taxonomy == 'categories'
66
+ t('camaleon_cms.admin.table.categories')
67
+ else
68
+ t('camaleon_cms.admin.table.tags')
69
+ end
70
+ return t('camaleon_cms.admin.post_type.message.no_created_html', taxonomy: taxonomy_label)
58
71
  end
59
72
 
60
- html = "<ul class='#{class_cat}'>"
61
- categories.decorate.each do |f|
62
- html += '<li>'
63
- html += "<label class='class_slug' data-post_link_edit='#{f.the_edit_url}'> "
64
- html += "<input data-error-place='#validation_error_list_#{name}' type='#{type}' name='#{name}[]' #{values.to_i.include?(f.id) ? 'checked' : ''} value='#{f.id}' class = '#{if required
65
- 'required'
66
- end}' />"
67
- html += "#{f.the_title} </label> "
68
- html += post_type_html_inputs(f, 'children', name, type, values, 'children') if f.children.present?
69
- html += '</li>'
70
- end
71
-
72
- html += "</ul><div id='validation_error_list_#{name}'></div>"
73
- html
73
+ content_tag(:ul, class: class_cat) do
74
+ items = categories.decorate.map do |f|
75
+ content_tag(:li) do
76
+ is_checked = Array(values).map(&:to_s).include?(f.id.to_s)
77
+ input_options = {
78
+ class: (required ? 'required' : ''), data: { error_place: "#validation_error_list_#{name}" }
79
+ }
80
+ input_tag = if type == 'radio'
81
+ radio_button_tag("#{name}[]", f.id, is_checked, input_options)
82
+ else
83
+ check_box_tag("#{name}[]", f.id, is_checked, input_options)
84
+ end
85
+ res = content_tag(:label, class: 'class_slug', data: { post_link_edit: f.the_edit_url }) do
86
+ safe_join([input_tag, ' ', f.the_title.to_s, ' '])
87
+ end
88
+ res << post_type_html_inputs(f, 'children', name, type, values, 'children') if f.children.present?
89
+ res
90
+ end
91
+ end
92
+ safe_join(items)
93
+ end + content_tag(:div, '', id: "validation_error_list_#{name}")
74
94
  end
75
95
  end
76
96
  end
@@ -4,31 +4,15 @@ module CamaleonCms
4
4
  # verify if current user is logged in, if not, then return nil
5
5
  # return html link
6
6
  def cama_edit_link(url, title = nil, attrs = {})
7
- return '' unless cama_current_user.present?
7
+ return '' if cama_current_user.blank?
8
8
  return '' unless cama_current_user.admin?
9
9
 
10
10
  attrs = { target: '_blank', style: 'font-size:11px !important;cursor:pointer;' }.merge(attrs)
11
- ActionController::Base.helpers.link_to("&rarr; #{title || ct('edit', default: 'Edit')}".html_safe, url, attrs)
12
- end
13
-
14
- # execute controller action and return response
15
- # NON USED
16
- def cama_requestAction(controller, action, params = {})
17
- controller.class_eval do
18
- def params=(params)
19
- @params = params
20
- end
21
-
22
- def params
23
- @params
24
- end
25
- end
26
- c = controller.new
27
- c.request = @_request
28
- c.response = @_response
29
- c.params = params
30
- c.send(action)
31
- c.response.body
11
+ link_to(
12
+ safe_join(['→ ', title || ct('edit', default: 'Edit')]),
13
+ url,
14
+ attrs
15
+ )
32
16
  end
33
17
 
34
18
  # theme common translation text
@@ -44,26 +28,37 @@ module CamaleonCms
44
28
  I18n.translate("camaleon_cms.common.#{key}", **args)
45
29
  end
46
30
 
47
- # check if current request was for admin panel
31
+ # Public compatibility API for plugins such as camaleon-ecommerce, which use
32
+ # this predicate to switch from visitor pricing/locale behavior to admin mode.
33
+ # This still matters while previewing frontend content from admin, for example
34
+ # a theme preview that renders ecommerce helpers before the request reaches the
35
+ # public frontend controller stack.
48
36
  def cama_is_admin_request?
49
- @cama_i18n_frontend.present?
37
+ respond_to?(:cama_get_i18n_frontend) && cama_get_i18n_frontend.present?
50
38
  end
51
39
 
52
40
  # generate loop categories html sitemap links
53
41
  # this is a helper for sitemap generator to print categories, sub categories and post contents in html list format
54
- def cama_sitemap_cats_generator(cats)
42
+ def cama_sitemap_cats_generator(cats, skip_config = {})
43
+ skip_config = {} unless skip_config.is_a?(Hash)
44
+ # merge! alone is not enough: an on_render_sitemap hook that sets one of these keys to nil
45
+ # overwrites the default with nil and brings back the very nil.include? crash the defaults
46
+ # exist to prevent. compact drops those before they win.
47
+ skip_config = { skip_cat_ids: [], skip_post_ids: [] }.merge!(skip_config.compact)
55
48
  res = []
56
49
  cats.decorate.each do |cat|
57
- next if @r[:skip_cat_ids].include?(cat.id)
50
+ next if skip_config[:skip_cat_ids].include?(cat.id)
58
51
 
59
52
  res_posts = []
60
53
  cat.the_posts.decorate.each do |post|
61
- next if @r[:skip_post_ids].include?(post.id)
54
+ next if skip_config[:skip_post_ids].include?(post.id)
62
55
 
63
- res_posts << "<li><a href='#{post.the_url}'>#{post.the_title}</a></li>"
56
+ post_href = ERB::Util.html_escape(post.the_url)
57
+ res_posts << "<li><a href='#{post_href}'>#{post.the_title}</a></li>"
64
58
  end
65
- res << "<li><h4><a href='#{cat.the_url}'>#{cat.the_title}</a></h4><ul>#{res_posts.join('')}</ul></li>"
66
- res << cama_sitemap_cats_generator(cat.the_categories)
59
+ cat_href = ERB::Util.html_escape(cat.the_url)
60
+ res << "<li><h4><a href='#{cat_href}'>#{cat.the_title}</a></h4><ul>#{res_posts.join('')}</ul></li>"
61
+ res << cama_sitemap_cats_generator(cat.the_categories, skip_config)
67
62
  end
68
63
  res.join('')
69
64
  end
@@ -79,22 +74,19 @@ module CamaleonCms
79
74
  cache
80
75
  end
81
76
 
82
- # =Deprecated
83
- def cama_draw_timer
84
- @_cama_timer ||= Time.current
85
- puts "***************************************** timer: #{((Time.current - @_cama_timer) * 24 * 60 * 60).to_i} (#{caller.first})"
86
- @_cama_timer = Time.current
87
- end
88
-
89
77
  # return normal translation with default value with translation of english
90
78
  def cama_t(key, args = {})
91
- args[:default] = I18n.t(key, **args.dup.merge(locale: :en)) unless args[:default].present?
79
+ args[:default] = I18n.t(key, **args.dup.merge(locale: :en)) if args[:default].blank?
92
80
  I18n.t(key, **args)
93
81
  end
94
82
 
95
83
  # function that converts string into plural format
84
+ # `SafeBuffer#pluralize` returns a plain String, so callers composing the result with other
85
+ # markup lose the safe flag before they can use it. The safeness of the input is propagated --
86
+ # never added, so an unsafe input stays unsafe and no caller becomes less safe than before.
96
87
  def cama_pluralize_text(text)
97
- text.try(:pluralize)
88
+ res = text.try(:pluralize)
89
+ text.try(:html_safe?) ? res.try(:html_safe) : res
98
90
  end
99
91
  end
100
92
  end
@@ -1,78 +1,112 @@
1
1
  module CamaleonCms
2
2
  module CaptchaHelper
3
- # build a captcha image
4
- # len: Number or characters to include in captcha (default 5)
5
- def cama_captcha_build(len = 5)
6
- img = MiniMagick::Image.open(File.join($camaleon_engine_dir.present? ? $camaleon_engine_dir : Rails.root.to_s,
7
- 'lib', 'captcha', "captcha_#{rand(12)}.jpg").to_s)
8
- text = cama_rand_str(len)
9
- session[:cama_captcha] = [] unless session[:cama_captcha].present?
10
- session[:cama_captcha] << text
11
- img.combine_options do |c|
12
- c.gravity 'Center'
13
- c.fill('#FFFFFF')
14
- c.draw "text 0,5 #{text}"
15
- c.font File.join($camaleon_engine_dir.present? ? $camaleon_engine_dir : Rails.root.to_s, 'lib', 'captcha',
16
- 'bumpyroad.ttf')
17
- c.pointsize '30'
18
- end
19
- img
20
- end
3
+ # Challenge/image generation (including the length clamp and its constants) lives in
4
+ # CamaleonCms::CaptchaImageGeneration, shared with the controller-stack
5
+ # CamaleonCms::RuntimeCaptchaImageConcern; verification and the under-attack counters
6
+ # below exist only here.
7
+ include CamaleonCms::CaptchaImageGeneration
21
8
 
22
9
  # build a captcha tag (image with captcha)
23
10
  # img_args: attributes for image_tag
24
11
  # input_args: attributes for input field
25
12
  def cama_captcha_tag(len = 5, img_args = { alt: '' }, input_args = {}, bootstrap_group_mode = false)
26
- unless input_args[:placeholder].present?
13
+ # Symbolize so string-keyed args work: the :placeholder / :style reads below use Symbol keys,
14
+ # so a caller's String key would be missed — the default placeholder would then be added under
15
+ # the Symbol key and render as a duplicate attribute.
16
+ img_args = img_args.to_h.symbolize_keys
17
+ input_args = input_args.to_h.symbolize_keys
18
+ if input_args[:placeholder].blank?
27
19
  input_args[:placeholder] =
28
- I18n.t('camaleon_cms.captcha_placeholder',
29
- default: 'Please enter the text of the image')
20
+ I18n.t('camaleon_cms.captcha_placeholder', default: 'Please enter the text of the image')
30
21
  end
31
- img_args['onclick'] = "this.src = \"#{cama_captcha_url(len: len)}\"+\"&t=\"+(new Date().getTime());"
32
- img = "<img style='cursor: pointer;' src='#{cama_captcha_url(len: len,
33
- t: Time.current.to_i)}' #{img_args.collect do |k, v|
34
- "#{k}='#{v}'"
35
- end.join(' ')} />"
36
- input = "<input type='text' name='captcha' #{input_args.collect { |k, v| "#{k}='#{v}'" }.join(' ')} />"
22
+ img_args[:onclick] = "this.src = \"#{cama_captcha_url(len: len)}\"+\"&t=\"+(new Date().getTime());"
23
+ # Keep a caller-supplied style; the pointer cursor is required for click-to-refresh, so prepend it.
24
+ img_args[:style] = ['cursor: pointer;', img_args[:style].presence].compact.join(' ')
25
+
26
+ helpers = ActionController::Base.helpers
27
+ img = helpers.image_tag(cama_captcha_url(len: len, t: Time.current.to_i), img_args)
28
+ input = helpers.tag(:input, type: 'text', name: 'captcha', **input_args)
29
+
37
30
  if bootstrap_group_mode
38
- "<div class='input-group input-group-captcha'><span class='input-group-btn' style='vertical-align: top;'>#{img}</span>#{input}</div>"
31
+ span = helpers.content_tag(:span, img, class: 'input-group-btn', style: 'vertical-align: top;')
32
+ helpers.content_tag(:div, helpers.safe_join([span, input]), class: 'input-group input-group-captcha')
39
33
  else
40
- "<div class='input-group-captcha'>#{img}#{input}</div>"
34
+ helpers.content_tag(:div, helpers.safe_join([img, input]), class: 'input-group-captcha')
41
35
  end
42
36
  end
43
37
 
44
- # verify captcha value
38
+ # verify captcha value against the single active challenge and consume it on success, so a solved
39
+ # captcha is single-use and a blank submission can never match (H3).
45
40
  def cama_captcha_verified?
46
- (session[:cama_captcha] || []).include?((params[:cama_captcha] || params[:captcha]).to_s.upcase)
41
+ submitted = (params[:cama_captcha] || params[:captcha]).to_s.upcase
42
+ return false if submitted.blank?
43
+ return false unless Array(session[:cama_captcha]).include?(submitted)
44
+
45
+ session.delete(:cama_captcha)
46
+ true
47
47
  end
48
48
 
49
49
  # ************************* captcha in attack helpers ***************************#
50
+ # Failed attempts are counted BOTH per session and per client IP. The per-IP counter lives in
51
+ # Rails.cache and is the server-side signal an attacker cannot reset by dropping the session
52
+ # cookie (audit finding H1); it rolls off CAMA_ATTACK_WINDOW after the last failure.
53
+ CAMA_ATTACK_WINDOW = 15.minutes
54
+
50
55
  # check if the current visitor was submitted 5+ times
51
56
  # key: a string to represent a url or form view
52
57
  # key must be the same as the form "captcha_tags_if_under_attack(key, ...)"
53
58
  def cama_captcha_under_attack?(key)
54
59
  session["cama_captcha_#{key}"] ||= 0
55
- session["cama_captcha_#{key}"].to_i > current_site.get_option('max_try_attack', 5).to_i
60
+ max = current_site.get_option('max_try_attack', 5).to_i
61
+ session["cama_captcha_#{key}"].to_i > max || cama_captcha_attack_ip_count(key) > max
56
62
  end
57
63
 
58
- # verify captcha values if this key is under attack
59
- # key: a string to represent a url or form view
64
+ # per-IP failed-attempt count for this key (0 when the counter is unset). Reads raw so the
65
+ # value round-trips as a bare integer on Redis/Memcached (see cama_captcha_increment_attack).
66
+ # Assumes a request + current_site context, which the admin login flow always has.
67
+ def cama_captcha_attack_ip_count(key)
68
+ Rails.cache.read(cama_captcha_attack_ip_key(key), raw: true).to_i
69
+ end
70
+
71
+ # verify the captcha only when this key is under attack; reports solely whether the
72
+ # current challenge was solved. The attack counter is cleared only by an explicit
73
+ # cama_captcha_reset_attack once the protected action itself succeeds (as the login
74
+ # flows do), so solving a captcha can no longer buy captcha-free attempts for an
75
+ # otherwise failing action. When the key is not under attack, the pending challenge
76
+ # is left unconsumed for whatever form it belongs to.
60
77
  def captcha_verify_if_under_attack(key)
61
- res = cama_captcha_under_attack?(key) ? cama_captcha_verified? : true
62
- session["cama_captcha_#{key}"] = 0 if cama_captcha_verified?
63
- res
78
+ return true unless cama_captcha_under_attack?(key)
79
+
80
+ cama_captcha_verified?
64
81
  end
65
82
 
66
- # increment attempts for key by 1
83
+ # increment attempts for key by 1 (both the per-session and the per-IP counter)
67
84
  def cama_captcha_increment_attack(key)
68
85
  session["cama_captcha_#{key}"] ||= 0
69
86
  session["cama_captcha_#{key}"] = session["cama_captcha_#{key}"].to_i + 1
87
+ # Per-IP counter: an ATOMIC cache increment, not a read-then-write. Concurrent failures from
88
+ # one IP would otherwise race on read+write and lose updates, undercounting a burst and
89
+ # deferring both the captcha gate and the hard lockout. `raw: true` keeps the value a bare
90
+ # integer so Redis/Memcached INCR operates on it (a no-op on Memory/File stores); refreshing
91
+ # the TTL on every increment keeps the rolling window alive during a sustained attack.
92
+ cache_key = cama_captcha_attack_ip_key(key)
93
+ counted = Rails.cache.increment(cache_key, 1, expires_in: CAMA_ATTACK_WINDOW, raw: true)
94
+ # Older FileStore/MemoryStore (Rails < 7.1) return nil for a missing key instead of seeding it;
95
+ # seed it then. This one-time seed's own race is harmless (count 1 vs 2 is far from any
96
+ # threshold) and every subsequent increment is atomic.
97
+ Rails.cache.write(cache_key, 1, expires_in: CAMA_ATTACK_WINDOW, raw: true) if counted.nil?
70
98
  end
71
99
 
72
- # reset the attacks counter for key
100
+ # reset the attacks counter for key (both the per-session and the per-IP counter)
73
101
  # key: a string to represent a url or form view
74
102
  def cama_captcha_reset_attack(key)
75
103
  session["cama_captcha_#{key}"] = 0
104
+ Rails.cache.delete(cama_captcha_attack_ip_key(key))
105
+ end
106
+
107
+ # cache key for the per-IP attack counter, scoped to the site and the form key
108
+ def cama_captcha_attack_ip_key(key)
109
+ "cama_captcha_attack:#{current_site.id}:#{request.remote_ip}:#{key}"
76
110
  end
77
111
 
78
112
  # return a number of attempts for key
@@ -86,19 +120,5 @@ module CamaleonCms
86
120
  def cama_captcha_tags_if_under_attack(key, captcha_parmas = [5, {}, { class: 'form-control required' }])
87
121
  cama_captcha_tag(*captcha_parmas) if cama_captcha_under_attack?(key)
88
122
  end
89
-
90
- private
91
-
92
- # generate random string for captcha
93
- # len: length of characters, default 6
94
- def cama_rand_str(len = 6)
95
- alphabets = [('A'..'Z').to_a].flatten!
96
- alphanumerics = [('A'..'Z').to_a, ('1'..'9').to_a].flatten!
97
- str = alphabets[rand(alphabets.size)]
98
- (len.to_i - 1).times do
99
- str << alphanumerics[rand(alphanumerics.size)]
100
- end
101
- str
102
- end
103
123
  end
104
124
  end
@@ -1,5 +1,7 @@
1
1
  module CamaleonCms
2
2
  module CommentHelper
3
+ LABELS = { 'approved' => 'success', 'pending' => 'warning', 'spam' => 'danger' }.freeze
4
+
3
5
  # return common data to save a new comment
4
6
  # user_id, author, aothor_email, author_ip, approved, :agent
5
7
  def cama_comments_get_common_data
@@ -15,48 +17,92 @@ module CamaleonCms
15
17
 
16
18
  # render as html content all comments recursively
17
19
  # comments: collection of comments
18
- def cama_comments_render_html(comments)
19
- res = ''
20
- labels = { 'approved' => 'success', 'pending' => 'warning', 'spam' => 'danger' }
21
- comments.decorate.each do |comment|
22
- author = comment.the_author
23
- res << "<div class='media'>
24
- <div class='media-left'>
25
- <a href='#{author.the_admin_profile_url}'>#{image_tag author.the_avatar, class: 'media-object',
26
- style: 'width: 64px; height: 64px;'}</a>
27
- </div>
28
- <div class='media-body'>
29
- <h4 class='media-heading'>#{author.the_name} <small>#{comment.the_created_at}</small> <span class='label label-#{labels[comment.approved]} pull-right'>#{t("camaleon_cms.admin.comments.message.#{comment.approved}")}</span></h4>
30
- <div class='comment_content'>#{sanitize comment.content}</div>
31
- <div class='comment_actions'>
32
- <div class='pull-left'>
33
- <a href='#{cama_admin_post_comment_answer_path(@post.id,
34
- comment.id)}' data-comment-id='#{comment.id}' title='#{t('camaleon_cms.admin.comments.tooltip.reply_comment')}' class='btn btn-info reply btn-xs ajax_modal'><span class='fa fa-mail-reply'></span></a>
35
- #{link_to raw('<i class="fa fa-trash-o"></i>'), { action: :destroy, id: comment.id },
36
- method: :delete, data: { confirm: t('camaleon_cms.admin.message.delete') }, class: 'btn btn-danger btn-xs cama_ajax_request', title: t('camaleon_cms.admin.comments.tooltip.delete_comment').to_s}
37
- </div>
38
- <div class='pull-right'>
39
- <a href='#{url_for({ action: :toggle_status, comment_id: comment.id,
40
- s: 'a' })}' title='#{t('camaleon_cms.admin.comments.tooltip.approved_comment')}' class='#{if comment.approved == 'approved'
41
- 'hidden'
42
- end} btn btn-success approve btn-xs cama_ajax_request'><span class='fa fa-thumbs-o-up'></span></a>
43
- <a href='#{url_for({ action: :toggle_status, comment_id: comment.id,
44
- s: 'p' })}' title='#{t('camaleon_cms.admin.comments.tooltip.comment_pending')}' class='#{if comment.approved == 'pending'
45
- 'hidden'
46
- end} btn btn-primary pending btn-xs cama_ajax_request'><span class='fa fa-warning'></span></a>
47
- <a href='#{url_for({ action: :toggle_status, comment_id: comment.id,
48
- s: 's' })}' title='#{t('camaleon_cms.admin.comments.tooltip.comment_spam')}' class='#{if comment.approved == 'spam'
49
- 'hidden'
50
- end} btn btn-danger spam btn-xs cama_ajax_request'><span class='fa fa-bug'></span></a>
51
- </div>
52
- </div>
53
- <hr>
54
- <div class='clearfix'></div>
55
- #{cama_comments_render_html comment.children}
56
- </div>
57
- </div>"
20
+ def cama_comments_render_html(comments, post_id = nil)
21
+ if post_id.nil? && respond_to?(:controller) && controller.respond_to?(:instance_variable_get)
22
+ post_id = controller.instance_variable_get(:@post)&.id
58
23
  end
59
- res
24
+ comments.decorate.map do |comment|
25
+ author = comment.the_author
26
+ content_tag(:div, class: 'media') do
27
+ [
28
+ content_tag(:div, class: 'media-left') do
29
+ link_to(author.the_admin_profile_url) do
30
+ image_tag(author.the_avatar, class: 'media-object', style: 'width: 64px; height: 64px;')
31
+ end
32
+ end,
33
+ content_tag(:div, class: 'media-body') do
34
+ [
35
+ content_tag(:h4, class: 'media-heading') do
36
+ [
37
+ # Escape the author name: User first_name/last_name are no longer sanitized at
38
+ # save time and this heading is marked html_safe below, so a commenter-controlled
39
+ # name would otherwise be injected as raw HTML into the moderator's page.
40
+ ERB::Util.html_escape(author.the_name),
41
+ ' ',
42
+ content_tag(:small, comment.the_created_at),
43
+ ' ',
44
+ content_tag(:span, t("camaleon_cms.admin.comments.message.#{comment.approved}"),
45
+ class: "label label-#{LABELS[comment.approved]} pull-right")
46
+ ].join.html_safe
47
+ end,
48
+ content_tag(:div, sanitize(comment.content), class: 'comment_content'),
49
+ content_tag(:div, class: 'comment_actions') do
50
+ [
51
+ content_tag(:div, class: 'pull-left') do
52
+ [
53
+ link_to(
54
+ cama_admin_post_comment_answer_path(post_id, comment.id),
55
+ data: { comment_id: comment.id },
56
+ title: t('camaleon_cms.admin.comments.tooltip.reply_comment'),
57
+ class: 'btn btn-info reply btn-xs ajax_modal'
58
+ ) { content_tag(:span, '', class: 'fa fa-mail-reply') },
59
+ ' ',
60
+ link_to(
61
+ { action: :destroy, id: comment.id },
62
+ method: :delete,
63
+ data: { confirm: t('camaleon_cms.admin.message.delete') },
64
+ class: 'btn btn-danger btn-xs cama_ajax_request',
65
+ title: t('camaleon_cms.admin.comments.tooltip.delete_comment')
66
+ ) { content_tag(:i, '', class: 'fa fa-trash-o') }
67
+ ].join.html_safe
68
+ end,
69
+ content_tag(:div, class: 'pull-right') do
70
+ [
71
+ link_to(
72
+ url_for({ action: :toggle_status, comment_id: comment.id, s: 'a' }),
73
+ method: :patch,
74
+ title: t('camaleon_cms.admin.comments.tooltip.approved_comment'),
75
+ class: "#{comment.approved == 'approved' ? 'hidden' : ''} " \
76
+ 'btn btn-success approve btn-xs cama_ajax_request'
77
+ ) { content_tag(:span, '', class: 'fa fa-thumbs-o-up') },
78
+ ' ',
79
+ link_to(
80
+ url_for({ action: :toggle_status, comment_id: comment.id, s: 'p' }),
81
+ method: :patch,
82
+ title: t('camaleon_cms.admin.comments.tooltip.comment_pending'),
83
+ class: "#{comment.approved == 'pending' ? 'hidden' : ''} " \
84
+ 'btn btn-primary pending btn-xs cama_ajax_request'
85
+ ) { content_tag(:span, '', class: 'fa fa-warning') },
86
+ ' ',
87
+ link_to(
88
+ url_for({ action: :toggle_status, comment_id: comment.id, s: 's' }),
89
+ method: :patch,
90
+ title: t('camaleon_cms.admin.comments.tooltip.comment_spam'),
91
+ class: "#{comment.approved == 'spam' ? 'hidden' : ''} " \
92
+ 'btn btn-danger spam btn-xs cama_ajax_request'
93
+ ) { content_tag(:span, '', class: 'fa fa-bug') }
94
+ ].join.html_safe
95
+ end
96
+ ].join.html_safe
97
+ end,
98
+ content_tag(:hr),
99
+ content_tag(:div, '', class: 'clearfix'),
100
+ cama_comments_render_html(comment.children, post_id)
101
+ ].join.html_safe
102
+ end
103
+ ].join.html_safe
104
+ end
105
+ end.join('').html_safe
60
106
  end
61
107
  end
62
108
  end
@@ -2,34 +2,41 @@ module CamaleonCms
2
2
  module ContentHelper
3
3
  # initialize content variables
4
4
  def cama_content_init
5
- @_before_content = []
6
- @_after_content = []
5
+ state = cama_content_state
6
+ state[:before_content] = []
7
+ state[:after_content] = []
7
8
  end
8
9
 
9
10
  # prepend content for admin or frontend (after <body>)
10
11
  # sample: cama_content_prepend(<div>my prepend content</div>)
11
12
  def cama_content_prepend(content)
12
- @_before_content << content
13
+ cama_content_state[:before_content] << content
13
14
  end
14
15
 
15
16
  # append content for admin or frontend (before </body>)
16
17
  # sample: cama_content_prepend(<div>my after content</div>)
17
18
  def cama_content_append(content)
18
- @_after_content << content
19
+ cama_content_state[:after_content] << content
19
20
  end
20
21
 
21
22
  # draw all before contents registered by cama_content_prepend
22
23
  def cama_content_before_draw
23
- @_before_content.join('')
24
+ cama_content_state[:before_content].join('')
24
25
  rescue StandardError
25
26
  ''
26
27
  end
27
28
 
28
29
  # draw all after contents registered by cama_content_append
29
30
  def cama_content_after_draw
30
- @_after_content.join('')
31
+ cama_content_state[:after_content].join('')
31
32
  rescue StandardError
32
33
  ''
33
34
  end
35
+
36
+ private
37
+
38
+ def cama_content_state
39
+ CurrentRequest.content_helper_state ||= { before_content: [], after_content: [] }
40
+ end
34
41
  end
35
42
  end
@@ -1,7 +1,7 @@
1
1
  module CamaleonCms
2
2
  module EmailHelper
3
3
  include CamaleonCms::HooksHelper
4
- # send and email
4
+ # send an email
5
5
  # email: email to
6
6
  # subject: Subject of the email
7
7
  # content: content of the email
@@ -20,7 +20,8 @@ module CamaleonCms
20
20
  end
21
21
 
22
22
  # short method of send_email
23
- # args: content='', from=nil, attachs=[], url_base='', current_site, template_name, layout_name, extra_data, format, cc_to
23
+ # args: content='', from=nil, attachs=[], url_base='', current_site, template_name, layout_name, extra_data,
24
+ # format, cc_to
24
25
  def cama_send_email(email_to, subject, args = {})
25
26
  args = { url_base: cama_root_url, current_site: current_site, subject: subject }.merge(args)
26
27
  args[:attachments] = args[:attachs] if args[:attachs].present?
@@ -42,7 +43,10 @@ module CamaleonCms
42
43
 
43
44
  def send_password_reset_email(user_to_send)
44
45
  user_to_send.send_password_reset
45
- reset_url = cama_admin_forgot_url({ h: user_to_send.password_reset_token })
46
+ # Read the column, not the same-named method has_secure_password generates on Rails 7.1+ (which
47
+ # shadows it with a signed token the controller's column lookup never matches). read_attribute
48
+ # returns the stored column on every supported Rails version (6.1–8.1).
49
+ reset_url = cama_admin_forgot_url({ h: user_to_send.read_attribute(:password_reset_token) })
46
50
  extra_data = {
47
51
  url: reset_url,
48
52
  fullname: user_to_send.fullname,