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
@@ -24,7 +24,7 @@ module CamaleonCms
24
24
  # update category assignations for this post
25
25
  # remove assignations that no longer exist
26
26
  # add new assignations
27
- # cats: (Array) array of category ids assigned for this post, sample: [1,2,3]
27
+ # cats: (Array) array of category ids assigned for this post, sample: [1, 2, 3]
28
28
  def update_categories(cats = [])
29
29
  rescue_extra_data
30
30
  cats = cats.to_i
@@ -35,7 +35,7 @@ module CamaleonCms
35
35
  news_categories.each do |key|
36
36
  term_relationships.create(term_taxonomy_id: key)
37
37
  end
38
- update_counters('categories')
38
+ update_counts('categories')
39
39
  end
40
40
 
41
41
  # update tags assignations for this post
@@ -53,7 +53,7 @@ module CamaleonCms
53
53
  post_tag = post_type.post_tags.where({ name: f }).first_or_create(slug: f.parameterize)
54
54
  term_relationships.where({ term_taxonomy_id: post_tag.id }).first_or_create
55
55
  end
56
- update_counters('tags')
56
+ update_counts('tags')
57
57
  end
58
58
 
59
59
  # Assign this post for category with id: category_id
@@ -64,43 +64,25 @@ module CamaleonCms
64
64
  categories_id.each do |key|
65
65
  term_relationships.where(term_taxonomy_id: key).first_or_create!
66
66
  end
67
- update_counters('categories')
67
+ update_counts('categories')
68
68
  end
69
69
 
70
- # Assign this post for category with id: category_id
71
- # categories_id: (Array) array of category ids assigned for this post, sample: [1,2,3]
70
+ # Unassign this post from categories with ids: categories_id
71
+ # categories_id: (Array) array of category ids to unassign from this post, sample: [1,2,3]
72
72
  def unassign_category(categories_id)
73
73
  categories_id = [categories_id] if categories_id.is_a?(Integer)
74
+ # a loaded (stale) categories association would hide the removed ids from the
75
+ # counter refresh below — check_default_category loads it on every save
76
+ categories.reset
74
77
  rescue_extra_data
75
78
  term_relationships.where(term_taxonomy_id: categories_id).destroy_all
76
- update_counters('categories')
77
- end
78
-
79
- # Assign new tags to this post
80
- # tags_title: (String) tags name separated by commas, sample: "Tag1,Tag two,tag new"
81
- def assign_tags(tag_titles)
82
- update_counters_before
83
- tags = tag_titles.split(',').strip
84
- tags.each do |t|
85
- post_tag = post_type.post_tags.where(name: t).first_or_create!
86
- term_relationships.where({ term_taxonomy_id: post_tag.id }).first_or_create!
87
- end
88
- update_counters('tags')
89
- end
90
-
91
- # Unassign tags from this post
92
- # tags_title: (String) tags name separated by commas, sample: "Tag1,Tag two,tag new"
93
- def unassign_tags(tag_titles)
94
- update_counters_before
95
- tags = tag_titles.split(',').strip
96
- term_relationships.where({ term_taxonomy_id: post_type.post_tags.where(name: tags).pluck(:id) }).destroy_all
97
- update_counters('tags')
79
+ update_counts('categories')
98
80
  end
99
81
 
100
- # update quantity of posts assigned for tags and categories assigned to this post
82
+ # Update the quantity of posts assigned for tags and categories assigned to this post
101
83
  def update_extra_data
102
84
  rescue_extra_data
103
- update_counters
85
+ update_counts
104
86
  end
105
87
 
106
88
  private
@@ -113,23 +95,23 @@ module CamaleonCms
113
95
  @tags_before = post_tags.pluck(:id) if manage_tags?
114
96
  end
115
97
 
116
- # update quantity of posts assigned for tags and categories assigned to this post
98
+ # Update the quantity of posts assigned for tags and categories assigned to this post
117
99
  # if kind is empty, then this will update both: cats and tags
118
100
  # kind: (string) tags | categories
119
- def update_counters(kind = '')
101
+ def update_counts(kind = '')
120
102
  if ['', 'categories'].include?(kind) && manage_categories?
121
- post_type.full_categories.where(id: (@cats_before + categories.pluck(:id)).uniq).each do |c|
122
- c.update_column('count', c.posts.published.size)
103
+ post_type.full_categories.where(id: (@cats_before + categories.pluck(:id)).uniq).find_each do |c|
104
+ c.update_column(:count, c.posts.published.size) # rubocop:disable Rails/SkipsModelValidations
123
105
  end
124
106
  end
125
107
  return unless ['', 'tags'].include?(kind) && manage_tags?
126
108
 
127
- post_type.post_tags.where(id: (@tags_before + post_tags.pluck(:id)).uniq).each do |tag|
128
- tag.update_column('count', tag.posts.published.size)
109
+ post_type.post_tags.where(id: (@tags_before + post_tags.pluck(:id)).uniq).find_each do |tag|
110
+ tag.update_column(:count, tag.posts.published.size) # rubocop:disable Rails/SkipsModelValidations
129
111
  end
130
112
  end
131
113
 
132
- # save extra data such as: categories and tags assigned to this post
114
+ # Save extra data such as categories and tags assigned to this post
133
115
  def save_extra_data
134
116
  update_categories(data_categories) if manage_categories? && !data_categories.nil?
135
117
  update_tags(data_tags) if manage_tags? && !data_tags.nil?
@@ -139,7 +121,7 @@ module CamaleonCms
139
121
  def check_default_category
140
122
  return unless manage_categories?
141
123
 
142
- assign_category([post_type.default_category.id]) unless categories.present?
124
+ assign_category([post_type.default_category.id]) if categories.blank?
143
125
  end
144
126
  end
145
127
  end
@@ -3,22 +3,35 @@
3
3
  module CamaleonCms
4
4
  module CommonRelationships
5
5
  extend ActiveSupport::Concern
6
+ # rubocop:disable Rails/InverseOf
6
7
 
7
8
  included do
8
- class_name = name.demodulize
9
- has_many :metas, -> { where(object_class: class_name) },
9
+ # Scopes use the demodulized class name (the 2.9.2 contract every install's rows were
10
+ # written under): 'Post', 'Main' for Widget::Main, and 'User' for a host Admin::User.
11
+ # to_s tolerates anonymous subclasses (STI specs build them), whose name is nil.
12
+ object_class_name = name.to_s.demodulize
13
+ has_many :metas, -> { where(object_class: object_class_name) },
10
14
  class_name: 'CamaleonCms::Meta', foreign_key: :objectid, dependent: :destroy
11
15
 
12
- has_many :custom_field_values, -> { where(object_class: class_name) },
16
+ has_many :custom_field_values, -> { where(object_class: object_class_name) },
13
17
  class_name: 'CamaleonCms::CustomFieldsRelationship', foreign_key: :objectid, dependent: :delete_all
14
18
 
15
- has_many :custom_fields, -> { where(object_class: class_name) },
19
+ # No dependent: on the definition associations (2.9.2 behavior). Teardown is owned by
20
+ # CustomFieldsRead#_destroy_custom_field_groups, which runs first and destroys groups WITH
21
+ # callbacks; a dependent here is inert at best, and on hook-less includers (PostComment) a
22
+ # delete_all removed group rows while orphaning their fields and option metas. A :destroy
23
+ # cascade is no alternative: custom_fields matches group rows too (same table, same scope)
24
+ # but its CustomField typing would skip group teardown.
25
+ # rubocop:disable Rails/HasManyOrHasOneDependent
26
+ has_many :custom_fields, -> { where(object_class: object_class_name) },
16
27
  class_name: 'CamaleonCms::CustomField', foreign_key: :objectid
17
28
 
18
29
  # valid only for simple groups and not for complex like: posts, post, ... where the group is for individual or
19
30
  # children groups
20
- has_many :custom_field_groups, -> { where(object_class: class_name) },
31
+ has_many :custom_field_groups, -> { where(object_class: object_class_name) },
21
32
  class_name: 'CamaleonCms::CustomFieldGroup', foreign_key: :objectid
33
+ # rubocop:enable Rails/HasManyOrHasOneDependent
22
34
  end
35
+ # rubocop:enable Rails/InverseOf
23
36
  end
24
37
  end
@@ -35,21 +35,29 @@ module CamaleonCms
35
35
  when 'Post'
36
36
  if term_relationships.empty? && args[:cat_ids].nil?
37
37
  CamaleonCms::CustomFieldGroup.where(
38
- '(objectid = ? AND object_class = ?) OR (objectid = ? AND object_class = ?)', id || -1, class_name, post_type.id, "PostType_#{class_name}"
38
+ '(objectid = ? AND object_class = ?) OR (objectid = ? AND object_class = ?)',
39
+ id || -1, class_name, post_type.id, "PostType_#{class_name}"
39
40
  )
40
41
  else
41
42
  cat_ids = categories.map(&:id)
42
43
  cat_ids += args[:cat_ids] unless args[:cat_ids].nil?
43
44
  cat_ids += CamaleonCms::Category.find(cat_ids).map { |category| _category_parents_ids(category) }.flatten.uniq
44
- CamaleonCms::CustomFieldGroup.where("(objectid = ? AND object_class = ?) OR
45
- (objectid = ? AND object_class = ?) OR
46
- (objectid IN (?) AND object_class = ?)",
47
- id || -1, class_name,
48
- post_type.id, "PostType_#{class_name}",
49
- cat_ids, "Category_#{class_name}")
45
+ CamaleonCms::CustomFieldGroup.where(
46
+ "(objectid = ? AND object_class = ?) OR
47
+ (objectid = ? AND object_class = ?) OR
48
+ (objectid IN (?) AND object_class = ?)",
49
+ id || -1, class_name, post_type.id, "PostType_#{class_name}", cat_ids, "Category_#{class_name}"
50
+ )
50
51
  end
51
52
  when 'NavMenuItem'
52
53
  main_menu.custom_field_groups
54
+ when 'Site'
55
+ # Site overrides `custom_field_groups` to mean "every group this site owns" (foreign key
56
+ # parent_id), unlike the object_class + objectid association every other model receives from
57
+ # CommonRelationships. Scoping by placement here keeps groups meant for post types, themes or
58
+ # menus off the site settings form, where their required fields blocked the submit and their
59
+ # values were discarded on save anyway.
60
+ custom_field_groups.where(object_class: 'Site', objectid: id)
53
61
  when 'PostType'
54
62
  case args[:kind]
55
63
  when 'all'
@@ -69,7 +77,9 @@ module CamaleonCms
69
77
  # return collections CustomFieldGroup
70
78
  # site: site object
71
79
  def get_user_field_groups(site)
72
- site.custom_field_groups.where(object_class: self.class.to_s.parseCamaClass)
80
+ # Demodulized to match the association scope and the placement the settings form stores
81
+ # ('User' for a host Admin::User); parseCamaClass still strips a Draper Decorator suffix.
82
+ site.custom_field_groups.where(object_class: self.class.to_s.parseCamaClass.demodulize)
73
83
  end
74
84
 
75
85
  # get custom field value
@@ -82,7 +92,7 @@ module CamaleonCms
82
92
  rescue StandardError
83
93
  _default
84
94
  end
85
- v.present? ? v : _default
95
+ v.presence || _default
86
96
  end
87
97
  alias get_field get_field_value
88
98
  alias get_field! get_field_value
@@ -117,12 +127,12 @@ module CamaleonCms
117
127
  # puts res[0]['my_slug1'].first ==> "val 1"
118
128
  def get_fields_grouped(field_keys)
119
129
  res = []
120
- custom_field_values.where(custom_field_slug: field_keys).order(group_number: :asc).group_by(&:group_number).each_value do |group_fields|
121
- group = {}
122
- field_keys.each do |field_key|
123
- _tmp = []
124
- group_fields.each { |field| _tmp << field.value if field_key == field.custom_field_slug }
125
- group[field_key] = _tmp if _tmp.present?
130
+ custom_field_values.where(custom_field_slug: field_keys)
131
+ .order(group_number: :asc).group_by(&:group_number).each_value do |group_fields|
132
+ group = field_keys.each_with_object({}) do |field_key, hsh|
133
+ _tmp = group_fields
134
+ .each_with_object([]) { |field, ary| ary << field.value if field_key == field.custom_field_slug }
135
+ hsh[field_key] = _tmp if _tmp.present?
126
136
  end
127
137
  res << group
128
138
  end
@@ -131,11 +141,12 @@ module CamaleonCms
131
141
 
132
142
  # return all values
133
143
  # {key1: "single value", key2: [multiple, values], key3: value4} if include_options = false
134
- # {key1: {values: "single value", options: {a:1, b: 4}}, key2: {values: [multiple, values], options: {a=1, b=2} }} if include_options = true
144
+ # {key1: {values: "single value", options: {a:1, b: 4}}, key2: {values: [multiple, values], options: {a=1, b=2} }}
145
+ # if include_options = true
135
146
  def get_field_values_hash(include_options = false)
136
147
  fields = {}
137
148
  custom_field_values.to_a.uniq.each do |field_value|
138
- custom_field = field_value.custom_fields
149
+ custom_field = field_value.custom_field
139
150
  values = custom_field.values.where(objectid: id).pluck(:value)
140
151
  unless include_options
141
152
  fields[field_value.custom_field_slug] =
@@ -155,13 +166,14 @@ module CamaleonCms
155
166
  # deprecated f attribute
156
167
  def get_fields_object(_f = true)
157
168
  fields = {}
158
- custom_field_values.eager_load(:custom_fields).to_a.uniq.each do |field_value|
159
- custom_field = field_value.custom_fields
169
+ custom_field_values.eager_load(:custom_field).to_a.uniq.each do |field_value|
170
+ custom_field = field_value.custom_field
160
171
  # if custom_field.options[:show_frontend].to_s.to_bool
161
172
  values = custom_field.values.where(objectid: id).pluck(:value)
162
- fields[field_value.custom_field_slug] =
163
- custom_field.attributes.merge(options: custom_field.cama_options,
164
- values: custom_field.cama_options[:multiple].to_s.to_bool ? values : values.first)
173
+ fields[field_value.custom_field_slug] = custom_field.attributes.merge(
174
+ options: custom_field.cama_options,
175
+ values: custom_field.cama_options[:multiple].to_s.to_bool ? values : values.first
176
+ )
165
177
  # end
166
178
  end
167
179
  fields.to_sym
@@ -179,10 +191,11 @@ module CamaleonCms
179
191
  # post_type.add_custom_field_group(values, kind = "Category")
180
192
  # Note 2: If you need add fields for only the Post_type, you have to use options or metas
181
193
  # return: CustomFieldGroup object
182
- # kind: argument only for PostType model: (Post | Category | PostTag), default => Post. If kind = "" this will add group for all post_types
194
+ # kind: argument only for PostType model: (Post | Category | PostTag), default => Post.
195
+ # If kind = "" this will add group for all post_types
183
196
  def add_custom_field_group(values, kind = 'Post')
184
197
  values = values.with_indifferent_access
185
- group = get_field_groups(kind).find_by(slug: values[:slug])
198
+ group = get_field_groups(kind).find_by_slug(values[:slug]) # rubocop:disable Rails/DynamicFindBy
186
199
  unless group
187
200
  site = _cama_get_field_site
188
201
  values[:parent_id] = site.id if site.present?
@@ -202,7 +215,7 @@ module CamaleonCms
202
215
  # more details in add_manual_field(item, options) from custom field groups
203
216
  # kind: argument only for PostType model: (Post | Category | PostTag), default => Post
204
217
  def add_custom_field_to_default_group(item, options, kind = 'Post')
205
- g = get_field_groups(kind).find_by(slug: '_default')
218
+ g = get_field_groups(kind).find_by_slug('_default') # rubocop:disable Rails/DynamicFindBy
206
219
  g ||= add_custom_field_group({ name: 'Default Field Group', slug: '_default' }, kind)
207
220
  g.add_manual_field(item, options)
208
221
  end
@@ -210,13 +223,8 @@ module CamaleonCms
210
223
 
211
224
  # return field object for current model
212
225
  def get_field_object(slug)
213
- CamaleonCms::CustomField.where(
214
- slug: slug,
215
- parent_id: get_field_groups.pluck(:id)
216
- ).first || CamaleonCms::CustomField.where(
217
- slug: slug,
218
- parent_id: get_field_groups({ include_parent: true })
219
- ).first
226
+ CamaleonCms::CustomField.where(slug: slug, parent_id: get_field_groups.pluck(:id)).first ||
227
+ CamaleonCms::CustomField.where(slug: slug, parent_id: get_field_groups({ include_parent: true })).first
220
228
  end
221
229
 
222
230
  # save all fields sent from browser (reservated for browser request)
@@ -226,28 +234,57 @@ module CamaleonCms
226
234
  # "1"=>{ "untitled-text-box"=>{"id"=>"262", "values"=>{"0"=>"33333"}}}
227
235
  # }
228
236
  def set_field_values(datas = {})
229
- return unless datas.present?
237
+ return if datas.blank?
230
238
 
231
239
  ActiveRecord::Base.transaction do
240
+ # A value identical to one already stored is not newly authored, so it must not be re-gated on
241
+ # an unrelated edit (audit M8): the admin form round-trips every value and this method
242
+ # delete/recreates them all, so without this skip a single pre-gate dangerous value would fail
243
+ # the whole save and brick every later edit -- the trap Post#reject_untrusted_dangerous_content
244
+ # avoids via content_changed?. Snapshot the stored pairs before clearing; skip the gate for any
245
+ # recreated value that matches one.
246
+ previously_stored = custom_field_values.pluck(:custom_field_slug, :value).to_set
232
247
  custom_field_values.delete_all
233
248
  datas.each_value do |fields_data|
234
249
  fields_data.each do |field_key, values|
235
- next unless values[:values].present?
250
+ next if values[:values].blank?
251
+
252
+ # Resolve the field id from the trusted slug, not the client-supplied values[:id]: a
253
+ # forged custom_field_id points the row at a different field definition, and the
254
+ # scan-and-reject gate keys off custom_field.options[:field_key] -- so a forged non-gated
255
+ # id would slip markup past the gate for a gated (editor/uri/field_attrs) slug. Fall back
256
+ # to values[:id] only when the slug names no field here (trusted/internal callers that
257
+ # pass slugs outside this object's registered groups; permitted browser payloads never do).
258
+ field_id = get_field_object(field_key)&.id || fallback_field_id_for(field_key) || values[:id]
259
+ group_number = [values[:group_number].to_i, 0].max
236
260
 
237
261
  order_value = -1
238
- (values[:values].is_a?(Hash) || values[:values].is_a?(ActionController::Parameters) ? values[:values].values : values[:values]).each do |value|
239
- custom_field_values.create!({ custom_field_id: values[:id], custom_field_slug: field_key,
240
- value: fix_meta_value(value), term_order: order_value += 1, group_number: values[:group_number] || 0 })
262
+ (
263
+ if values[:values].is_a?(Hash) || values[:values].is_a?(ActionController::Parameters)
264
+ values[:values].values
265
+ else
266
+ values[:values]
267
+ end
268
+ ).each do |value|
269
+ row = custom_field_values.new(
270
+ custom_field_id: field_id, custom_field_slug: field_key,
271
+ value: fix_meta_value(value), term_order: order_value += 1, group_number: group_number
272
+ )
273
+ row.unfiltered_value! if previously_stored.include?([field_key.to_s, row.value])
274
+ row.save!
241
275
  end
242
276
  end
243
277
  end
244
278
  end
245
279
  end
246
280
 
247
- # update new value for field with slug _key
281
+ # Update the value for the field with slug _key
248
282
  # Sample: my_posy.update_field_value('sub_title', 'Test Sub Title')
249
- def update_field_value(_key, value = nil, group_number = 0)
250
- custom_field_values.find_by(custom_field_slug: _key, group_number: group_number)&.update_column('value', value)
283
+ def update_field_value(key, value = nil, group_number = 0)
284
+ # Route through validations so the scan-and-reject gate applies (audit M5): update_column
285
+ # skipped it, letting a dangerous value an untrusted caller supplies reach the frontend verbatim.
286
+ custom_field_values.find_by(custom_field_slug: key, group_number: group_number)
287
+ &.update(value: value)
251
288
  rescue StandardError
252
289
  nil
253
290
  end
@@ -262,48 +299,68 @@ module CamaleonCms
262
299
 
263
300
  # Set custom field values for current model (support for multiple group values)
264
301
  # key: (string required) slug of the custom field
265
- # value: (array | string) array: array of values for multiple values support, string: uniq value for the custom field
302
+ # value: (array | string) array: array of values for multiple values support,
303
+ # string: uniq value for the custom field
266
304
  # args:
267
305
  # field_id: (integer optional) identifier of the custom field
268
306
  # order: order or position of the field value
269
307
  # group_number: number of the group (only for custom field group with is_repeat enabled)
270
- # clear: (boolean, default true) if true, will remove previous values and set these values, if not will append values
308
+ # clear: (boolean, default true) if true, will remove previous values and set these values,
309
+ # if not will append values
271
310
  # return false if the was not saved because there is not present the field with slug: key
272
311
  # sample: my_post.set_field_value('subtitle', 'Sub Title')
273
- # sample: my_post.set_field_value('subtitle', ['Sub Title1', 'Sub Title2']) # set values for a field (for fields that support multiple values)
312
+ # sample: set values for a field (for fields that support multiple values)
313
+ # my_post.set_field_value('subtitle', ['Sub Title1', 'Sub Title2'])
274
314
  # sample: my_post.set_field_value('subtitle', 'Sub Title', {group_number: 1})
275
- # sample: my_post.set_field_value('subtitle', 'Sub Title', {group_number: 1, group_number: 1}) # add field values for fields in group 1
315
+ # sample: add field values for fields in group 1
316
+ # my_post.set_field_value('subtitle', 'Sub Title', {group_number: 1, group_number: 1})
276
317
  def set_field_value(key, value, args = {})
277
318
  args = { order: 0, group_number: 0, field_id: nil, clear: true }.merge!(args)
278
- unless args[:field_id].present?
319
+ if args[:field_id].blank?
279
320
  args[:field_id] = begin
280
321
  get_field_object(key).id
281
322
  rescue StandardError
282
323
  nil
283
324
  end
284
325
  end
285
- raise ArgumentError, "There is no custom field configured for #{key}" unless args[:field_id].present?
326
+ args[:field_id] ||= fallback_field_id_for(key)
286
327
 
287
- if args[:clear]
288
- custom_field_values.where({ custom_field_slug: key,
289
- group_number: args[:group_number] }).delete_all
290
- end
291
- v = { custom_field_id: args[:field_id], custom_field_slug: key, value: fix_meta_value(value),
292
- term_order: args[:order], group_number: args[:group_number] }
293
- if value.is_a?(Array)
294
- value.each do |val|
295
- custom_field_values.create!(v.merge({ value: fix_meta_value(val) }))
328
+ raise ArgumentError, "There is no custom field configured for #{key}" if args[:field_id].blank?
329
+
330
+ v = {
331
+ custom_field_id: args[:field_id], custom_field_slug: key, value: fix_meta_value(value),
332
+ term_order: args[:order], group_number: args[:group_number]
333
+ }
334
+ # Atomic (audit M7): clear the previous value and write the new one in one transaction, so a
335
+ # value the scan-and-reject gate refuses (create! -> RecordInvalid) rolls the delete back and
336
+ # the previously stored value survives instead of being destroyed.
337
+ ActiveRecord::Base.transaction do
338
+ if args[:clear]
339
+ custom_field_values.where({ custom_field_slug: key, group_number: args[:group_number] }).delete_all
340
+ end
341
+ if value.is_a?(Array)
342
+ value.each { |val| custom_field_values.create!(v.merge({ value: fix_meta_value(val) })) }
343
+ else
344
+ custom_field_values.create!(v)
296
345
  end
297
- else
298
- custom_field_values.create!(v)
299
346
  end
300
347
  end
301
348
 
302
349
  private
303
350
 
351
+ def fallback_field_id_for(key)
352
+ return unless self.class.to_s.parseCamaClass == 'Post'
353
+ return unless respond_to?(:post_type_id) && post_type_id.present?
354
+
355
+ group_ids = CamaleonCms::CustomFieldGroup.where(object_class: 'PostType_Post', objectid: post_type_id).pluck(:id)
356
+ return if group_ids.blank?
357
+
358
+ CamaleonCms::CustomField.where(slug: key, parent_id: group_ids).pick(:id)
359
+ end
360
+
304
361
  def fix_meta_value(value)
305
362
  return value.to_json if value.is_a?(ActionController::Parameters)
306
- return JSON.fast_generate(value) if value.is_a?(Array) || value.is_a?(Hash)
363
+ return JSON.generate(value) if value.is_a?(Array) || value.is_a?(Hash)
307
364
 
308
365
  value
309
366
  end
@@ -14,7 +14,25 @@ module CamaleonCms
14
14
  # Add meta with value or Update meta with key: key
15
15
  # return true or false
16
16
  def set_meta(key, value)
17
- metas.where(key: key).update_or_create({ value: fix_meta_value(value) })
17
+ fixed_value = fix_meta_value(value)
18
+
19
+ # Check if the parent object has been saved to the database yet
20
+ if persisted?
21
+ # Safe to use database-driven lookups and updates
22
+ meta_record = metas.find_or_create_by(key: key.to_s)
23
+ meta_record.update(value: fixed_value)
24
+ else
25
+ # In-Memory Fallback: Find an existing unsaved item in the array collection,
26
+ # or build a brand new unsaved record on the association.
27
+ meta_record = metas.find { |m| m.key == key.to_s }
28
+
29
+ if meta_record
30
+ meta_record.value = fixed_value
31
+ else
32
+ metas.build(key: key.to_s, value: fixed_value)
33
+ end
34
+ end
35
+
18
36
  cama_set_cache("meta_#{key}", value)
19
37
  end
20
38
 
@@ -23,7 +41,7 @@ module CamaleonCms
23
41
  def get_meta(key, default = nil)
24
42
  key_str = key.is_a?(Symbol) ? key.to_s : key
25
43
  cama_fetch_cache("meta_#{key_str}") do
26
- option = metas.loaded? ? metas.select { |m| m.key == key }.first : metas.where(key: key_str).first
44
+ option = metas.loaded? ? metas.find { |m| m.key == key_str } : metas.where(key: key_str).first
27
45
  res = ''
28
46
  if option.present?
29
47
  value = begin
@@ -73,12 +91,16 @@ module CamaleonCms
73
91
  # return value for attribute
74
92
  def get_option(key = nil, default = nil, meta_key = '_default')
75
93
  values = cama_options(meta_key)
94
+ return default if key.nil?
95
+
76
96
  key = key.to_sym
77
97
  values.key?(key) && values[key] != '' ? values[key] : default
78
98
  end
79
99
 
80
100
  # delete attribute from configuration
81
101
  def delete_option(key, meta_key = '_default')
102
+ return if key.nil?
103
+
82
104
  values = cama_options(meta_key)
83
105
  key = key.to_sym
84
106
  values.delete(key) if values.key?(key)
@@ -88,7 +110,7 @@ module CamaleonCms
88
110
  # set multiple configurations
89
111
  # h: {ket1: "sdsds", ff: "fdfdfdfd"}
90
112
  def set_options(h = {}, meta_key = '_default')
91
- return unless h.present?
113
+ return if h.blank?
92
114
 
93
115
  data = cama_options(meta_key)
94
116
  PluginRoutes.fixActionParameter(h).to_sym.each do |key, value|
@@ -116,11 +138,14 @@ module CamaleonCms
116
138
  save_metas_options # unless self.changed?
117
139
  end
118
140
 
119
- # save all settings for this post type received in data_options and data_metas attribute (options and metas)
120
- # sample: Site.first.post_types.create({name: "owen", slug: "my_post_type", data_options: { has_category: true, default_layout: "my_layout" }})
141
+ # Save all settings for this post type - received in data_options and data_metas attribute (options and metas)
142
+ # sample:
143
+ # Site.first.post_types.create(
144
+ # {name: "owen", slug: "my_post_type", data_options: { has_category: true, default_layout: "my_layout" }}
145
+ # )
121
146
  def save_metas_options
122
147
  set_multiple_options(data_options)
123
- return unless data_metas.present?
148
+ return if data_metas.blank?
124
149
 
125
150
  data_metas.each do |key, val|
126
151
  set_meta(key, val)
@@ -134,7 +159,7 @@ module CamaleonCms
134
159
  changed_value = if value.is_a?(ActionController::Parameters)
135
160
  value.to_json
136
161
  elsif value.is_a?(Array) || value.is_a?(Hash)
137
- JSON.fast_generate(value)
162
+ JSON.generate(value)
138
163
  else
139
164
  value
140
165
  end
@@ -9,17 +9,11 @@ module CamaleonCms
9
9
  args.each do |attr|
10
10
  next unless new_record? || attribute_changed?(attr)
11
11
 
12
- self[attr] = ActionController::Base.helpers.sanitize(
13
- __send__(attr)&.gsub(CamaleonRecord::TRANSLATION_TAG_HIDE_REGEX, CamaleonRecord::TRANSLATION_TAG_HIDE_MAP)
14
- )&.gsub(CamaleonRecord::TRANSLATION_TAG_RESTORE_REGEX, CamaleonRecord::TRANSLATION_TAG_RESTORE_MAP)
12
+ self[attr] = CamaleonRecord.cama_sanitize_translatable(__send__(attr))
15
13
  end
16
14
  end
17
15
  else
18
- normalizes(*args, with: lambda { |field|
19
- ActionController::Base.helpers.sanitize(
20
- field.gsub(CamaleonRecord::TRANSLATION_TAG_HIDE_REGEX, CamaleonRecord::TRANSLATION_TAG_HIDE_MAP)
21
- ).gsub(CamaleonRecord::TRANSLATION_TAG_RESTORE_REGEX, CamaleonRecord::TRANSLATION_TAG_RESTORE_MAP)
22
- })
16
+ normalizes(*args, with: ->(field) { CamaleonRecord.cama_sanitize_translatable(field) })
23
17
  end
24
18
  end
25
19
  end