camaleon_cms 2.9.2 → 2.9.4

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 (216) 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 +14 -10
  8. data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
  9. data/app/apps/plugins/front_cache/config/locales/translation.yml +0 -7
  10. data/app/apps/plugins/front_cache/front_cache_helper.rb +109 -37
  11. data/app/apps/plugins/front_cache/views/admin/settings.html.erb +0 -5
  12. data/app/apps/plugins/visibility_post/config/config.json +1 -0
  13. data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
  14. data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
  15. data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
  16. data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
  17. data/app/apps/themes/default/assets/css/style.css.scss +24 -0
  18. data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
  19. data/app/apps/themes/default/default_helper.rb +2 -1
  20. data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
  21. data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
  22. data/app/apps/themes/new/custom_helper.rb +5 -9
  23. data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
  24. data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
  25. data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
  26. data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
  27. data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
  28. data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +8 -6
  29. data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
  30. data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +7 -2
  31. data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +2 -1
  32. data/app/controllers/camaleon_cms/admin/categories_controller.rb +19 -4
  33. data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
  34. data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
  35. data/app/controllers/camaleon_cms/admin/media_controller.rb +66 -14
  36. data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
  37. data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +10 -4
  38. data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
  39. data/app/controllers/camaleon_cms/admin/posts_controller.rb +55 -22
  40. data/app/controllers/camaleon_cms/admin/sessions_controller.rb +237 -44
  41. data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +80 -13
  42. data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +5 -2
  43. data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +26 -3
  44. data/app/controllers/camaleon_cms/admin/settings_controller.rb +5 -3
  45. data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +7 -5
  46. data/app/controllers/camaleon_cms/admin/users_controller.rb +78 -11
  47. data/app/controllers/camaleon_cms/admin_controller.rb +136 -26
  48. data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
  49. data/app/controllers/camaleon_cms/frontend_controller.rb +146 -74
  50. data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +20 -5
  51. data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
  52. data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
  53. data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
  54. data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
  55. data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
  56. data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
  57. data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
  58. data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
  59. data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
  60. data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
  61. data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
  62. data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
  63. data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
  64. data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
  65. data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
  66. data/app/decorators/camaleon_cms/post_decorator.rb +68 -50
  67. data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
  68. data/app/decorators/camaleon_cms/site_decorator.rb +33 -25
  69. data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
  70. data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
  71. data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
  72. data/app/helpers/camaleon_cms/admin/application_helper.rb +5 -8
  73. data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
  74. data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
  75. data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
  76. data/app/helpers/camaleon_cms/admin/post_type_helper.rb +34 -17
  77. data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
  78. data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
  79. data/app/helpers/camaleon_cms/comment_helper.rb +19 -7
  80. data/app/helpers/camaleon_cms/content_helper.rb +13 -6
  81. data/app/helpers/camaleon_cms/email_helper.rb +7 -3
  82. data/app/helpers/camaleon_cms/frontend/application_helper.rb +6 -1
  83. data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
  84. data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +143 -66
  85. data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
  86. data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
  87. data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
  88. data/app/helpers/camaleon_cms/html_helper.rb +58 -37
  89. data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
  90. data/app/helpers/camaleon_cms/session_helper.rb +188 -44
  91. data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
  92. data/app/helpers/camaleon_cms/site_helper.rb +34 -13
  93. data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
  94. data/app/helpers/camaleon_cms/uploader_helper.rb +19 -473
  95. data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
  96. data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
  97. data/app/models/camaleon_cms/ability.rb +15 -7
  98. data/app/models/camaleon_cms/category.rb +6 -6
  99. data/app/models/camaleon_cms/custom_field.rb +15 -6
  100. data/app/models/camaleon_cms/custom_field_group.rb +39 -22
  101. data/app/models/camaleon_cms/custom_fields_relationship.rb +140 -2
  102. data/app/models/camaleon_cms/media.rb +8 -2
  103. data/app/models/camaleon_cms/meta.rb +2 -2
  104. data/app/models/camaleon_cms/nav_menu.rb +5 -3
  105. data/app/models/camaleon_cms/nav_menu_item.rb +16 -8
  106. data/app/models/camaleon_cms/plugin.rb +3 -4
  107. data/app/models/camaleon_cms/post.rb +137 -27
  108. data/app/models/camaleon_cms/post_comment.rb +10 -6
  109. data/app/models/camaleon_cms/post_default.rb +42 -12
  110. data/app/models/camaleon_cms/post_relationship.rb +6 -4
  111. data/app/models/camaleon_cms/post_tag.rb +4 -5
  112. data/app/models/camaleon_cms/post_type.rb +21 -13
  113. data/app/models/camaleon_cms/site.rb +39 -29
  114. data/app/models/camaleon_cms/term_relationship.rb +10 -7
  115. data/app/models/camaleon_cms/term_taxonomy.rb +58 -4
  116. data/app/models/camaleon_cms/theme.rb +2 -4
  117. data/app/models/camaleon_cms/user.rb +17 -5
  118. data/app/models/camaleon_cms/user_role.rb +81 -6
  119. data/app/models/camaleon_cms/widget/assigned.rb +11 -6
  120. data/app/models/camaleon_cms/widget/main.rb +12 -8
  121. data/app/models/camaleon_cms/widget/sidebar.rb +3 -8
  122. data/app/models/camaleon_record.rb +89 -5
  123. data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +36 -42
  124. data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
  125. data/app/models/concerns/camaleon_cms/content_shortcode_gate.rb +73 -0
  126. data/app/models/concerns/camaleon_cms/custom_fields_read.rb +111 -54
  127. data/app/models/concerns/camaleon_cms/metas.rb +31 -6
  128. data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
  129. data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
  130. data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
  131. data/app/models/current_request.rb +8 -1
  132. data/app/uploaders/camaleon_cms_aws_uploader.rb +31 -10
  133. data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
  134. data/app/uploaders/camaleon_cms_uploader.rb +23 -10
  135. data/app/validators/camaleon_cms/post_uniq_validator.rb +5 -4
  136. data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
  137. data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
  138. data/app/views/camaleon_cms/admin/categories/index.html.erb +2 -2
  139. data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
  140. data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
  141. data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
  142. data/app/views/camaleon_cms/admin/post_tags/index.html.erb +2 -2
  143. data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
  144. data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
  145. data/app/views/camaleon_cms/admin/posts/index.html.erb +5 -5
  146. data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
  147. data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
  148. data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
  149. data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
  150. data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +19 -4
  151. data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
  152. data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +4 -3
  153. data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
  154. data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
  155. data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
  156. data/app/views/camaleon_cms/admin/user_roles/form.html.erb +24 -3
  157. data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
  158. data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
  159. data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
  160. data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
  161. data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
  162. data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
  163. data/config/initializers/action_view.rb +42 -5
  164. data/config/initializers/active_record_extension.rb +13 -27
  165. data/config/initializers/assets.rb +52 -23
  166. data/config/initializers/custom_initializers.rb +1 -1
  167. data/config/locales/camaleon_cms/admin/ar.yml +1 -0
  168. data/config/locales/camaleon_cms/admin/de.yml +1 -0
  169. data/config/locales/camaleon_cms/admin/en.yml +36 -0
  170. data/config/locales/camaleon_cms/admin/es.yml +1 -0
  171. data/config/locales/camaleon_cms/admin/fr.yml +1 -0
  172. data/config/locales/camaleon_cms/admin/it.yml +1 -0
  173. data/config/locales/camaleon_cms/admin/nl.yml +1 -0
  174. data/config/locales/camaleon_cms/admin/pt-BR.yml +1 -0
  175. data/config/locales/camaleon_cms/admin/pt.yml +1 -0
  176. data/config/locales/camaleon_cms/admin/ru.yml +1 -0
  177. data/config/locales/camaleon_cms/admin/uk.yml +1 -0
  178. data/config/locales/camaleon_cms/admin/zh-CH.yml +1 -0
  179. data/config/routes/admin.rb +33 -15
  180. data/config/routes/frontend.rb +21 -15
  181. data/lib/camaleon_cms/assets_precompile.rb +86 -0
  182. data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
  183. data/lib/camaleon_cms/content_security.rb +158 -0
  184. data/lib/camaleon_cms/engine.rb +45 -21
  185. data/lib/camaleon_cms/media_security_headers.rb +27 -0
  186. data/lib/camaleon_cms/setup_token.rb +69 -0
  187. data/lib/camaleon_cms/shortcode_registry.rb +102 -0
  188. data/lib/camaleon_cms/svg_content_checker.rb +123 -0
  189. data/lib/camaleon_cms/task_reporter.rb +14 -0
  190. data/lib/camaleon_cms/unsafe_markup.rb +207 -0
  191. data/lib/camaleon_cms/uploader_content_security.rb +248 -0
  192. data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
  193. data/lib/camaleon_cms/uploader_path_security.rb +146 -0
  194. data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
  195. data/lib/camaleon_cms/uploader_support.rb +86 -0
  196. data/lib/camaleon_cms/version.rb +1 -1
  197. data/lib/camaleon_cms.rb +23 -0
  198. data/lib/ext/hash.rb +28 -2
  199. data/lib/ext/string.rb +10 -5
  200. data/lib/ext/translator.rb +10 -2
  201. data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
  202. data/lib/generators/camaleon_cms/install_generator.rb +7 -4
  203. data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
  204. data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
  205. data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
  206. data/lib/plugin_routes.rb +412 -368
  207. data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
  208. data/lib/tasks/cross_site_field_groups.rake +74 -0
  209. data/lib/tasks/custom_fields_roles.rake +15 -13
  210. data/lib/tasks/orphaned_comments.rake +42 -0
  211. data/lib/tasks/private_upload_acls.rake +31 -0
  212. data/lib/tasks/site_custom_field_groups.rake +36 -0
  213. data/lib/tasks/unsafe_stored_content.rake +42 -0
  214. data/lib/tasks/unsafe_stored_uploads.rake +70 -0
  215. data/lib/tasks/user_field_groups.rake +27 -0
  216. metadata +125 -4
@@ -24,18 +24,22 @@ 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
- cats = cats.to_i
31
- old_categories = categories.pluck("#{CamaleonCms::TermTaxonomy.table_name}.id")
30
+ # uniq: duplicate ids (e.g. a crafted `categories[]=5&categories[]=5`) would otherwise create
31
+ # duplicate term_relationships rows, which duplicate the post in category/tag listings and
32
+ # inflate will_paginate's total_entries (there is no unique index on the join table)
33
+ cats = cats.to_i.uniq
34
+ # reuse the snapshot rescue_extra_data just took instead of plucking the same set again
35
+ old_categories = manage_categories? ? @cats_before : categories.pluck("#{CamaleonCms::TermTaxonomy.table_name}.id")
32
36
  delete_categories = old_categories - cats
33
37
  news_categories = cats - old_categories
34
38
  term_relationships.where('term_taxonomy_id in (?)', delete_categories).destroy_all if delete_categories.present?
35
39
  news_categories.each do |key|
36
40
  term_relationships.create(term_taxonomy_id: key)
37
41
  end
38
- update_counters('categories')
42
+ update_counts('categories')
39
43
  end
40
44
 
41
45
  # update tags assignations for this post
@@ -53,7 +57,7 @@ module CamaleonCms
53
57
  post_tag = post_type.post_tags.where({ name: f }).first_or_create(slug: f.parameterize)
54
58
  term_relationships.where({ term_taxonomy_id: post_tag.id }).first_or_create
55
59
  end
56
- update_counters('tags')
60
+ update_counts('tags')
57
61
  end
58
62
 
59
63
  # Assign this post for category with id: category_id
@@ -64,72 +68,62 @@ module CamaleonCms
64
68
  categories_id.each do |key|
65
69
  term_relationships.where(term_taxonomy_id: key).first_or_create!
66
70
  end
67
- update_counters('categories')
71
+ update_counts('categories')
68
72
  end
69
73
 
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]
74
+ # Unassign this post from categories with ids: categories_id
75
+ # categories_id: (Array) array of category ids to unassign from this post, sample: [1,2,3]
72
76
  def unassign_category(categories_id)
73
77
  categories_id = [categories_id] if categories_id.is_a?(Integer)
74
78
  rescue_extra_data
75
79
  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')
80
+ update_counts('categories')
89
81
  end
90
82
 
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')
98
- end
99
-
100
- # update quantity of posts assigned for tags and categories assigned to this post
83
+ # Update the quantity of posts assigned for tags and categories assigned to this post
101
84
  def update_extra_data
102
85
  rescue_extra_data
103
- update_counters
86
+ update_counts
104
87
  end
105
88
 
106
89
  private
107
90
 
108
- @cats_before = []
109
- @tags_before = []
110
91
  # save as a cache previous categories and tags assigned for this post
111
92
  def rescue_extra_data
93
+ # Every writer mutates term_relationships directly, which does NOT invalidate an already-loaded
94
+ # categories/post_tags/term_relationships proxy -- and the_post preloads categories/post_tags via
95
+ # with_eager, so the proxy is routinely loaded before a write. Reset here, the single point every
96
+ # writer (and update_extra_data) calls first, so the snapshots below and every later
97
+ # pluck/empty?/count read the database rather than a stale in-memory target.
98
+ reset_term_proxies
112
99
  @cats_before = categories.pluck(:id) if manage_categories?
113
100
  @tags_before = post_tags.pluck(:id) if manage_tags?
114
101
  end
115
102
 
116
- # update quantity of posts assigned for tags and categories assigned to this post
103
+ # drop any loaded category/tag/term_relationship cache so a write starts from DB truth
104
+ def reset_term_proxies
105
+ categories.reset
106
+ post_tags.reset
107
+ term_relationships.reset
108
+ end
109
+
110
+ # Update the quantity of posts assigned for tags and categories assigned to this post
117
111
  # if kind is empty, then this will update both: cats and tags
118
112
  # kind: (string) tags | categories
119
- def update_counters(kind = '')
113
+ def update_counts(kind = '')
120
114
  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)
115
+ post_type.full_categories.where(id: (@cats_before + categories.pluck(:id)).uniq).find_each do |c|
116
+ c.update_column(:count, c.posts.published.size) # rubocop:disable Rails/SkipsModelValidations
123
117
  end
124
118
  end
125
119
  return unless ['', 'tags'].include?(kind) && manage_tags?
126
120
 
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)
121
+ post_type.post_tags.where(id: (@tags_before + post_tags.pluck(:id)).uniq).find_each do |tag|
122
+ tag.update_column(:count, tag.posts.published.size) # rubocop:disable Rails/SkipsModelValidations
129
123
  end
130
124
  end
131
125
 
132
- # save extra data such as: categories and tags assigned to this post
126
+ # Save extra data such as categories and tags assigned to this post
133
127
  def save_extra_data
134
128
  update_categories(data_categories) if manage_categories? && !data_categories.nil?
135
129
  update_tags(data_tags) if manage_tags? && !data_tags.nil?
@@ -139,7 +133,7 @@ module CamaleonCms
139
133
  def check_default_category
140
134
  return unless manage_categories?
141
135
 
142
- assign_category([post_type.default_category.id]) unless categories.present?
136
+ assign_category([post_type.default_category.id]) if categories.blank?
143
137
  end
144
138
  end
145
139
  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
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamaleonCms
4
+ # Save-time authorization gate for authored content that `do_shortcode` later expands (post
5
+ # content, custom-field values, taxonomy/widget descriptions). A registered shortcode makes
6
+ # theme/plugin code emit arbitrary HTML/JS at render, which the content scan cannot judge, so
7
+ # authorship is GATED behind the default-off `content_shortcodes` manager permission rather than
8
+ # filtered. The content is never escaped, stripped or rewritten -- a save carrying a shortcode is
9
+ # refused for an untrusted author, and stored verbatim for a trusted one.
10
+ #
11
+ # Conforms to `security-capability-gating` (fail-closed) and the content-shortcode-gating
12
+ # capability. A model gates an attribute with `gate_content_shortcodes :attr`; a newly added
13
+ # surface that `do_shortcode` expands MUST adopt this gate (asserted by the coverage spec).
14
+ module ContentShortcodeGate
15
+ extend ActiveSupport::Concern
16
+
17
+ class_methods do
18
+ # Gate the named attribute(s) on create/update. Each attribute is scanned in its RAW stored
19
+ # form -- which contains every locale of a translatable value at once, so a shortcode in any
20
+ # locale is caught -- and, if a registered shortcode is present and the actor is a
21
+ # non-administrator lacking `content_shortcodes`, the save is refused with a base error.
22
+ def gate_content_shortcodes(*attributes, on: %i[create update])
23
+ gated = attributes.map(&:to_sym)
24
+ validate(on: on) { cama_reject_unauthorized_shortcodes(gated) }
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def cama_reject_unauthorized_shortcodes(attributes)
31
+ return unless attributes.any? { |attr| cama_shortcode_attr_gated?(attr) }
32
+ return if cama_trusted_for_shortcodes?
33
+
34
+ errors.add(:base, cama_shortcode_rejection_message)
35
+ end
36
+
37
+ # An attribute needs the gate only when it (a) changed on this save (or the record is new) -- so
38
+ # a title-only edit never re-gates pre-existing shortcode content, mirroring
39
+ # Post#reject_untrusted_dangerous_content -- and (b) actually contains a registered shortcode.
40
+ def cama_shortcode_attr_gated?(attr)
41
+ return false unless new_record? || cama_gate_attribute_changed?(attr)
42
+
43
+ value = self[attr]
44
+ value.present? && CamaleonCms::ShortcodeRegistry.content_has_shortcode?(value.to_s)
45
+ end
46
+
47
+ def cama_gate_attribute_changed?(attr)
48
+ respond_to?("#{attr}_changed?") ? public_send("#{attr}_changed?") : attribute_changed?(attr.to_s)
49
+ end
50
+
51
+ # Fail closed (the gate applies) without a request context or on any evaluation error, per
52
+ # security-capability-gating: a background job, rake task or console is untrusted. Administrators
53
+ # always pass; a non-administrator passes only while holding the `content_shortcodes` manager
54
+ # permission.
55
+ def cama_trusted_for_shortcodes?
56
+ user = CurrentRequest.user
57
+ site = CurrentRequest.site
58
+ return false if user.blank? || site.blank?
59
+ return true if user.admin?
60
+
61
+ CamaleonCms::Ability.new(user, site).can?(:manage, :content_shortcodes)
62
+ rescue StandardError
63
+ false
64
+ end
65
+
66
+ # Only en.yml carries this key while the process locale follows the current admin/site language
67
+ # -- fall back to English rather than emit "translation missing".
68
+ def cama_shortcode_rejection_message
69
+ key = 'camaleon_cms.admin.shortcodes.message.rejected'
70
+ I18n.t(key, default: I18n.t(key, locale: :en))
71
+ end
72
+ end
73
+ 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,7 +141,8 @@ 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|
@@ -159,9 +170,10 @@ module CamaleonCms
159
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,45 +299,65 @@ 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
363
  return JSON.generate(value) if value.is_a?(Array) || value.is_a?(Hash)
@@ -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)
@@ -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