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
@@ -1,30 +1,45 @@
1
1
  module CamaleonCms
2
2
  class Site < CamaleonCms::TermTaxonomy
3
+ normalize_attrs(:description)
4
+
3
5
  include CamaleonCms::SiteDefaultSettings
4
6
 
5
7
  # attrs: [name, description, slug]
6
8
  attr_accessor :site_domain
7
9
 
8
- default_scope { where(taxonomy: :site).reorder(term_group: :desc) }
10
+ # Transient (never persisted): when default_settings auto-provisions the
11
+ # administrator, it stashes the randomly generated plaintext password here so
12
+ # the provisioning controller can surface it once. nil when no admin was minted
13
+ # (e.g. a shared admin already existed). See harden-installer-default-admin.
14
+ attr_accessor :generated_admin_password
9
15
 
10
- has_many :post_types, class_name: 'CamaleonCms::PostType', foreign_key: :parent_id, dependent: :destroy
16
+ has_many :post_types, class_name: 'CamaleonCms::PostType', foreign_key: :parent_id, dependent: :destroy,
17
+ inverse_of: :site
11
18
  has_many :nav_menus, class_name: 'CamaleonCms::NavMenu', foreign_key: :parent_id, dependent: :destroy,
12
19
  inverse_of: :site
13
- has_many :nav_menu_items, class_name: 'CamaleonCms::NavMenuItem', foreign_key: :term_group
14
- has_many :widgets, class_name: 'CamaleonCms::Widget::Main', foreign_key: :parent_id, dependent: :destroy
15
- has_many :sidebars, class_name: 'CamaleonCms::Widget::Sidebar', foreign_key: :parent_id, dependent: :destroy
16
- has_many :user_roles_rel, class_name: 'CamaleonCms::UserRole', foreign_key: :parent_id, dependent: :destroy
20
+ # inverse_of: false — this joins by term_group (the site id), while NavMenuItem#parent
21
+ # reads parent_id (the menu id); an inverse here overwrites items' real menu parent.
22
+ has_many :nav_menu_items, class_name: 'CamaleonCms::NavMenuItem', foreign_key: :term_group, inverse_of: false,
23
+ dependent: :destroy
24
+ has_many :widgets, class_name: 'CamaleonCms::Widget::Main', foreign_key: :parent_id, dependent: :destroy,
25
+ inverse_of: :site
26
+ has_many :sidebars, class_name: 'CamaleonCms::Widget::Sidebar', foreign_key: :parent_id, dependent: :destroy,
27
+ inverse_of: :site
28
+ has_many :user_roles_rel, class_name: 'CamaleonCms::UserRole', foreign_key: :parent_id, dependent: :destroy,
29
+ inverse_of: :site
17
30
  has_many :custom_field_groups, class_name: 'CamaleonCms::CustomFieldGroup', foreign_key: :parent_id,
18
- dependent: :destroy
19
- has_many :term_taxonomies, class_name: 'CamaleonCms::TermTaxonomy', foreign_key: :parent_id
31
+ dependent: :destroy, inverse_of: :parent
32
+ has_many :term_taxonomies, class_name: 'CamaleonCms::TermTaxonomy', foreign_key: :parent_id, inverse_of: :parent,
33
+ dependent: :destroy
20
34
 
21
35
  has_many :posts, through: :post_types, source: :posts
22
- has_many :plugins, class_name: 'CamaleonCms::Plugin', foreign_key: :parent_id, dependent: :destroy
23
- has_many :themes, class_name: 'CamaleonCms::Theme', foreign_key: :parent_id, dependent: :destroy
24
- has_many :public_media, -> { where(is_public: true) },
25
- class_name: 'CamaleonCms::Media', foreign_key: :site_id, dependent: :destroy
26
- has_many :private_media, -> { where(is_public: false) },
27
- class_name: 'CamaleonCms::Media', foreign_key: :site_id, dependent: :destroy
36
+ has_many :plugins, class_name: 'CamaleonCms::Plugin', foreign_key: :parent_id, dependent: :destroy,
37
+ inverse_of: :site
38
+ has_many :themes, class_name: 'CamaleonCms::Theme', foreign_key: :parent_id, dependent: :destroy, inverse_of: :site
39
+ has_many :public_media, -> { where(is_public: true) }, class_name: 'CamaleonCms::Media', foreign_key: :site_id,
40
+ dependent: :destroy, inverse_of: :site
41
+ has_many :private_media, -> { where(is_public: false) }, class_name: 'CamaleonCms::Media', foreign_key: :site_id,
42
+ dependent: :destroy, inverse_of: :site
28
43
 
29
44
  after_create :default_settings
30
45
  after_create :set_default_user_roles
@@ -33,7 +48,7 @@ module CamaleonCms
33
48
  before_destroy :destroy_site
34
49
  after_destroy :refresh_routes
35
50
 
36
- validates_uniqueness_of :slug, scope: :taxonomy
51
+ validates :slug, uniqueness: { scope: :taxonomy }
37
52
 
38
53
  # all user roles for this site
39
54
  def user_roles
@@ -77,7 +92,7 @@ module CamaleonCms
77
92
  options[:_admin_theme] || 'en'
78
93
  end
79
94
 
80
- # set current admin language for this site
95
+ # Set the current admin language for this site
81
96
  def set_admin_language(language)
82
97
  set_option('_admin_theme', language)
83
98
  end
@@ -147,11 +162,9 @@ module CamaleonCms
147
162
 
148
163
  # list all users of current site
149
164
  def users
150
- if PluginRoutes.system_info['users_share_sites']
151
- CamaleonCms::User.all
152
- else
153
- CamaleonCms::User.where(site_id: id)
154
- end
165
+ return CamaleonCms::User.all if PluginRoutes.system_info['users_share_sites']
166
+
167
+ CamaleonCms::User.where(site_id: id)
155
168
  end
156
169
  alias users_include_admins users
157
170
 
@@ -174,15 +187,13 @@ module CamaleonCms
174
187
  def get_valid_post_slug(slug, post_id = nil)
175
188
  slugs = slug.translations
176
189
  if slugs.present?
177
- slugs.each do |k, v|
178
- slugs[k] = get_valid_post_slug(v)
179
- end
190
+ slugs.each { |k, v| slugs[k] = get_valid_post_slug(v) }
180
191
  slugs.to_translate
181
192
  else
182
193
  res = slug
183
194
  (1..9999).each do |i|
184
- p = posts.find_by_slug(res)
185
- break if !p.present? || (p.present? && p.id == post_id)
195
+ p = posts.find_by_slug(res) # rubocop:disable Rails/DynamicFindBy
196
+ break if p.blank? || (p.present? && p.id == post_id)
186
197
 
187
198
  res = "#{slug}-#{i}"
188
199
  end
@@ -209,7 +220,7 @@ module CamaleonCms
209
220
  # if the anonymous user not exist, will create one
210
221
  def get_anonymous_user
211
222
  user = users.where(username: 'anonymous').first
212
- unless user.present?
223
+ if user.blank?
213
224
  pass = "anonymous#{rand(9999)}"
214
225
  user = users.create({ email: 'anonymous_user@local.com', username: 'anonymous', password: pass,
215
226
  password_confirmation: pass, first_name: 'Anonymous' })
@@ -219,7 +230,8 @@ module CamaleonCms
219
230
 
220
231
  # return the domain for current site
221
232
  # sample: mysite.com | sample.mysite.com
222
- # also, you can define custom domain for this site by: my_site.site_domain = 'my_site.com' # used for sites with different domains to call from console or task
233
+ # also, you can define custom domain for this site by: my_site.site_domain = 'my_site.com'
234
+ # used for sites with different domains to call from console or task
223
235
  def get_domain
224
236
  @site_domain || (if main_site?
225
237
  slug
@@ -1,23 +1,26 @@
1
1
  module CamaleonCms
2
2
  class TermRelationship < CamaleonRecord
3
3
  self.table_name = "#{PluginRoutes.static_system_info['db_prefix']}term_relationships"
4
+
4
5
  default_scope -> { order(term_order: :asc) }
5
6
 
6
- belongs_to :term_taxonomy, inverse_of: :term_relationships, required: false
7
- belongs_to :object, lambda {
8
- order("#{CamaleonCms::Post.table_name}.id DESC")
9
- }, class_name: 'CamaleonCms::Post', foreign_key: :objectid, inverse_of: :term_relationships, required: false
7
+ belongs_to :term_taxonomy, inverse_of: :term_relationships, optional: true
8
+ belongs_to :object, -> { order("#{CamaleonCms::Post.table_name}.id DESC") },
9
+ class_name: 'CamaleonCms::Post', foreign_key: :objectid, inverse_of: :term_relationships,
10
+ optional: true
10
11
 
11
- # callbacks
12
12
  after_create :update_count
13
13
  before_destroy :update_count
14
14
 
15
15
  private
16
16
 
17
- # update counter of post published items
17
+ # Update the counter of post-published items
18
18
  # TODO verify counter
19
19
  def update_count
20
- term_taxonomy.update_column('count', term_taxonomy.posts.published.size) if term_taxonomy&.try(:posts)
20
+ return unless term_taxonomy&.try(:posts)
21
+
22
+ term_taxonomy
23
+ .update_column(:count, term_taxonomy.posts.published.size) # rubocop:disable Rails/SkipsModelValidations
21
24
  end
22
25
  end
23
26
  end
@@ -14,11 +14,56 @@ module CamaleonCms
14
14
  end
15
15
 
16
16
  self.table_name = "#{PluginRoutes.static_system_info['db_prefix']}term_taxonomy"
17
- # attr_accessible :taxonomy, :description, :parent_id, :count, :name, :slug, :term_group, :status, :term_order, :user_id
17
+ # attr_accessible :taxonomy, :description, :parent_id, :count, :name, :slug, :term_group, :status, :term_order,
18
+ # :user_id
18
19
  # attr_accessible :data_options
19
20
  # attr_accessible :data_metas
20
21
 
21
- normalize_attrs(:name, :description)
22
+ self.inheritance_column = :taxonomy
23
+
24
+ def self.sti_name
25
+ name.demodulize.underscore
26
+ end
27
+
28
+ def self.polymorphic_name
29
+ name.demodulize
30
+ end
31
+
32
+ def self.find_sti_class(type_name)
33
+ # Handle explicit exceptions where database string doesn't match namespacing layout
34
+ case type_name.to_s
35
+ when 'widget'
36
+ return CamaleonCms::Widget::Main
37
+ when 'sidebar'
38
+ return CamaleonCms::Widget::Sidebar
39
+ end
40
+
41
+ # Standard conversion for "site" -> "CamaleonCms::Site"
42
+ # or "nav_menu_item" -> "CamaleonCms::NavMenuItem"
43
+ klass = begin
44
+ "CamaleonCms::#{type_name.camelize}".constantize
45
+ rescue NameError
46
+ nil
47
+ end
48
+ # The ancestry guard keeps a value like "meta" from instantiating an unrelated
49
+ # CamaleonCms class against the term_taxonomy table.
50
+ return klass if klass && klass <= base_class
51
+
52
+ # Runtime scan across loaded taxonomy models.
53
+ found = CamaleonCms::TermTaxonomy.descendants.find { |k| k.sti_name == type_name.to_s }
54
+ return found if found
55
+
56
+ # `super` resolves fully-qualified external classes (plugin STI outside the CamaleonCms
57
+ # namespace) and enforces ancestry — the scan above only sees classes already loaded, so
58
+ # without this an unloaded plugin taxonomy silently degraded to the base class. Same
59
+ # fallback PostDefault.find_sti_class keeps. Values it cannot place load as the base class,
60
+ # as every row did before native STI.
61
+ begin
62
+ super
63
+ rescue ActiveRecord::SubclassNotFound
64
+ base_class
65
+ end
66
+ end
22
67
 
23
68
  # callbacks
24
69
  before_validation :before_validating
@@ -29,11 +74,12 @@ module CamaleonCms
29
74
  validates_with CamaleonCms::UniqValidator
30
75
 
31
76
  # relations
32
- has_many :term_relationships, class_name: 'CamaleonCms::TermRelationship', foreign_key: :term_taxonomy_id,
77
+ has_many :term_relationships, class_name: 'CamaleonCms::TermRelationship',
33
78
  dependent: :destroy
34
79
  # has_many :posts, foreign_key: :objectid, through: :term_relationships, :source => :objects
35
- belongs_to :parent, class_name: 'CamaleonCms::TermTaxonomy', foreign_key: :parent_id, required: false
36
- belongs_to :owner, class_name: CamaManager.get_user_class_name, foreign_key: :user_id, required: false
80
+ belongs_to :parent, class_name: 'CamaleonCms::TermTaxonomy', optional: true
81
+ belongs_to :owner, class_name: CamaManager.get_user_class_name.to_s, foreign_key: :user_id, optional: true,
82
+ inverse_of: false
37
83
 
38
84
  # return all children taxonomy
39
85
  # sample: sub categories of a category
@@ -1,10 +1,10 @@
1
1
  module CamaleonCms
2
2
  class Theme < CamaleonCms::TermTaxonomy
3
+ normalize_attrs(:description)
4
+
3
5
  # attrs:
4
6
  # slug => plugin key
5
- belongs_to :site, class_name: 'CamaleonCms::Site', foreign_key: :parent_id, required: false
6
-
7
- default_scope { where(taxonomy: :theme) }
7
+ belongs_to :site, class_name: 'CamaleonCms::Site', foreign_key: :parent_id, optional: true, inverse_of: :themes
8
8
 
9
9
  before_validation :fix_name
10
10
  before_destroy :destroy_custom_fields
@@ -1,22 +1,34 @@
1
- unless PluginRoutes.static_system_info['user_model'].present?
1
+ if PluginRoutes.static_system_info['user_model'].blank?
2
2
  module CamaleonCms
3
3
  class User < CamaleonRecord
4
4
  include CamaleonCms::UserMethods
5
5
 
6
- self.table_name = PluginRoutes.static_system_info['cama_users_db_table'] || "#{PluginRoutes.static_system_info['db_prefix']}users"
6
+ self.table_name = PluginRoutes.static_system_info['cama_users_db_table'] ||
7
+ "#{PluginRoutes.static_system_info['db_prefix']}users"
7
8
 
8
9
  default_scope { order(role: :asc) }
9
10
 
11
+ # No `dependent:` — a deleted user's widgets keep rendering; only the owner is gone.
12
+ has_many :widgets, class_name: 'CamaleonCms::Widget::Main', # rubocop:disable Rails/HasManyOrHasOneDependent
13
+ inverse_of: :owner
14
+
10
15
  validates :username, presence: true
11
- validates :email, presence: true, format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i } # , :unless => Proc.new { |a| a.auth_social.present? }
16
+ # The following might be continued wit: , :unless => Proc.new { |a| a.auth_social.present? }
17
+ validates :email, presence: true, format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i }
12
18
  has_secure_password
19
+ # Security (audit 2026-08-11 M15): has_secure_password only validates presence (on create) and the
20
+ # 72-byte bcrypt maximum, so a trivially short password was accepted. Enforce a minimum length
21
+ # whenever a password is actually set (create, reset, change). allow_blank leaves an update that
22
+ # submits no password untouched (a blank field is not a change), and lets has_secure_password's own
23
+ # presence check own the empty-on-create case.
24
+ validates :password, length: { minimum: 8 }, allow_blank: true
13
25
 
14
26
  def self.find_by_email(email)
15
- where(['lower(email) = ?', email.to_s.downcase]).take
27
+ find_by(['lower(email) = ?', email.to_s.downcase])
16
28
  end
17
29
 
18
30
  def self.find_by_username(username)
19
- where(['lower(username) = ?', username.to_s.downcase]).take
31
+ find_by(['lower(username) = ?', username.to_s.downcase])
20
32
  end
21
33
  end
22
34
  end
@@ -1,10 +1,15 @@
1
1
  module CamaleonCms
2
2
  class UserRole < CamaleonCms::TermTaxonomy
3
- after_destroy :set_users_as_cilent
3
+ normalize_attrs(:description)
4
4
 
5
- default_scope { where(taxonomy: :user_roles) }
5
+ def self.sti_name
6
+ 'user_roles'
7
+ end
6
8
 
7
- belongs_to :site, class_name: 'CamaleonCms::Site', foreign_key: :parent_id, required: false
9
+ after_destroy :set_users_as_cilent
10
+
11
+ belongs_to :site, class_name: 'CamaleonCms::Site', foreign_key: :parent_id, optional: true,
12
+ inverse_of: :user_roles_rel
8
13
 
9
14
  def roles_post_type
10
15
  get_meta('_post_type')
@@ -18,6 +23,23 @@ module CamaleonCms
18
23
  term_group.nil?
19
24
  end
20
25
 
26
+ # Users in a role slugged `admin` are administrators: `User#admin?` tests exactly this, and
27
+ # `Ability#initialize` answers `can :manage, :all` for them and returns before this role's
28
+ # `_post_type_`/`_manager_` metas are read. Those metas therefore describe nothing — every
29
+ # permission is held whatever they say.
30
+ def grants_all_permissions?
31
+ slug == 'admin'
32
+ end
33
+
34
+ # Whether the permission checkboxes mean anything. False for the default admin role, which is not
35
+ # editable at all, and false for any other role slugged `admin`, whose permissions cannot be
36
+ # withheld from its users. The editor renders them locked and the controller declines to write
37
+ # them, so the two cannot disagree — and a locked checkbox, which the browser does not submit,
38
+ # can never silently clear what is stored.
39
+ def permissions_editable?
40
+ editable? && !grants_all_permissions?
41
+ end
42
+
21
43
  ROLES = {
22
44
  post_type: [
23
45
  {
@@ -79,6 +101,14 @@ module CamaleonCms
79
101
  label: I18n.t('camaleon_cms.admin.users.roles_values.html_manage_tags').to_s,
80
102
  color: 'warning',
81
103
  description: I18n.t('camaleon_cms.admin.users.tool_tip.manage_tags').to_s
104
+ },
105
+ {
106
+ key: 'post_content_unfiltered_html',
107
+ label: I18n.t('camaleon_cms.admin.users.roles_values.post_content_unfiltered_html',
108
+ default: 'Allow unfiltered HTML in post content').to_s,
109
+ color: 'danger',
110
+ description: I18n.t('camaleon_cms.admin.users.tool_tip.post_content_unfiltered_html',
111
+ default: 'Permit users with this role to save raw/unfiltered HTML in post content').to_s
82
112
  }
83
113
  ],
84
114
  manager: [
@@ -122,10 +152,53 @@ module CamaleonCms
122
152
  label: I18n.t('camaleon_cms.admin.sidebar.settings').to_s,
123
153
  description: I18n.t('camaleon_cms.admin.users.tool_tip.settings').to_s
124
154
  },
155
+ {
156
+ key: 'custom_fields',
157
+ label: I18n.t('camaleon_cms.admin.sidebar.custom_fields', default: 'Custom Fields').to_s,
158
+ description: I18n.t('camaleon_cms.admin.users.tool_tip.custom_fields',
159
+ default: 'Manage custom field groups and fields, including the select_eval type.').to_s
160
+ },
161
+ {
162
+ key: 'select_eval',
163
+ label: I18n.t('camaleon_cms.admin.custom_field.select_eval', default: 'Select Eval').to_s,
164
+ description: I18n.t(
165
+ 'camaleon_cms.admin.users.tool_tip.select_eval',
166
+ default: 'Allow toggling and using select_eval custom fields in the admin UI.'
167
+ ).to_s
168
+ },
125
169
  {
126
170
  key: 'theme_settings',
127
171
  label: I18n.t('camaleon_cms.admin.settings.theme_setting', default: 'Theme Settings').to_s,
128
172
  description: I18n.t('camaleon_cms.admin.users.tool_tip.themes').to_s
173
+ },
174
+ {
175
+ key: 'contact_form_unfiltered_html',
176
+ label: I18n.t('camaleon_cms.admin.users.roles_values.contact_form_unfiltered_html',
177
+ default: 'Allow unfiltered HTML in contact forms').to_s,
178
+ color: 'danger',
179
+ description: I18n.t(
180
+ 'camaleon_cms.admin.users.tool_tip.contact_form_unfiltered_html',
181
+ default: 'Permit users with this role to save raw/unfiltered HTML anywhere in a contact form: the ' \
182
+ 'markup wrapping a form, field labels, descriptions, templates, CSS classes, default ' \
183
+ 'values, custom attributes, option labels and response messages. Without it, a save ' \
184
+ 'carrying such content is refused rather than cleaned up. Distinct from the per-post-type ' \
185
+ '"Allow unfiltered HTML in post content" permission.'
186
+ ).to_s
187
+ },
188
+ {
189
+ key: 'media_unfiltered_upload',
190
+ label: I18n.t('camaleon_cms.admin.users.roles_values.media_unfiltered_upload',
191
+ default: 'Allow unscanned media uploads').to_s,
192
+ color: 'danger',
193
+ description: I18n.t(
194
+ 'camaleon_cms.admin.users.tool_tip.media_unfiltered_upload',
195
+ default: 'Permit users with this role to upload files without the malicious-content scan. Without ' \
196
+ 'it, every upload is scanned whatever its source, and a file carrying scripts, event ' \
197
+ 'handlers, blocked elements or blocked URI schemes is refused rather than cleaned up. ' \
198
+ 'Uploaded files are served from the site origin, so this grants the ability to publish ' \
199
+ 'active content there. Site-wide, unlike the per-post-type "Allow unfiltered HTML in post ' \
200
+ 'content" permission.'
201
+ ).to_s
129
202
  }
130
203
  ]
131
204
  }.freeze
@@ -133,7 +206,7 @@ module CamaleonCms
133
206
  private
134
207
 
135
208
  def set_users_as_cilent
136
- site.users.where(role: slug).update_all(role: 'client')
209
+ site.users.where(role: slug).update_all(role: 'client') # rubocop:disable Rails/SkipsModelValidations
137
210
  end
138
211
  end
139
212
  end
@@ -1,7 +1,12 @@
1
1
  module CamaleonCms
2
2
  module Widget
3
3
  class Assigned < CamaleonCms::PostDefault
4
- default_scope -> { where(post_class: name).order(:taxonomy_id) }
4
+ default_scope -> { order(:taxonomy_id) }
5
+
6
+ def self.type_condition(table = arel_table)
7
+ super.or(table[inheritance_column].eq(name))
8
+ end
9
+
5
10
  # post_parent: sidebar_id
6
11
  # visibility: widget_id
7
12
  # comment_count: item_order
@@ -12,11 +17,11 @@ module CamaleonCms
12
17
 
13
18
  # attr_accessible :widget_id, :sidebar_id, :item_order
14
19
 
15
- has_many :metas, lambda {
16
- where(object_class: 'Widget::Assigned')
17
- }, class_name: 'CamaleonCms::Meta', foreign_key: :objectid, dependent: :destroy
18
- belongs_to :sidebar, class_name: 'CamaleonCms::Widget::Sidebar', foreign_key: :post_parent
19
- belongs_to :widget, class_name: 'CamaleonCms::Widget::Main', foreign_key: :visibility
20
+ belongs_to :sidebar, class_name: 'CamaleonCms::Widget::Sidebar', foreign_key: :post_parent, inverse_of: :assigned,
21
+ optional: true
22
+ belongs_to :widget, class_name: 'CamaleonCms::Widget::Main', foreign_key: :visibility, inverse_of: :assigned,
23
+ optional: true
24
+
20
25
  after_initialize :fix_slug2
21
26
  before_create :set_order
22
27
 
@@ -1,7 +1,10 @@
1
1
  module CamaleonCms
2
2
  module Widget
3
3
  class Main < CamaleonCms::TermTaxonomy
4
- default_scope { where(taxonomy: :widget) }
4
+ def self.sti_name
5
+ 'widget'
6
+ end
7
+
5
8
  # attr_accessible :excerpt, :renderer
6
9
  # name: "title"
7
10
  # description: "content for this"
@@ -10,14 +13,17 @@ module CamaleonCms
10
13
  # excerpt: string for message
11
14
  # renderer: string (path to the template for render this widget)
12
15
 
13
- has_many :metas, lambda {
14
- where(object_class: 'Widget::Main')
15
- }, class_name: 'CamaleonCms::Meta', foreign_key: :objectid, dependent: :destroy
16
- belongs_to :owner, class_name: CamaManager.get_user_class_name, foreign_key: :user_id
17
- belongs_to :site, class_name: 'CamaleonCms::Site', foreign_key: :parent_id
16
+ # inverse_of: false — custom `user_model` classes get only UserMethods, which
17
+ # defines no `widgets` collection the inverse could point at.
18
+ belongs_to :owner, class_name: CamaManager.get_user_class_name.to_s, foreign_key: :user_id, inverse_of: false,
19
+ optional: true
20
+ belongs_to :site, class_name: 'CamaleonCms::Site', foreign_key: :parent_id, inverse_of: :widgets, optional: true
21
+
22
+ has_many :assigned, class_name: 'CamaleonCms::Widget::Assigned', foreign_key: :visibility, dependent: :destroy,
23
+ inverse_of: :widget
18
24
 
19
- has_many :assigned, class_name: 'CamaleonCms::Widget::Assigned', foreign_key: :visibility, dependent: :destroy
20
25
  before_save :check_excerpt
26
+
21
27
  def is_simple?
22
28
  status == 'simple'
23
29
  end
@@ -1,13 +1,10 @@
1
1
  module CamaleonCms
2
2
  module Widget
3
3
  class Sidebar < CamaleonCms::TermTaxonomy
4
- default_scope { where(taxonomy: :sidebar) }
4
+ normalize_attrs(:description)
5
5
 
6
- has_many :metas, lambda {
7
- where(object_class: 'Widget::Sidebar')
8
- }, class_name: 'CamaleonCms::Meta', foreign_key: :objectid, dependent: :destroy
9
- has_many :assigned, foreign_key: :post_parent, dependent: :destroy
10
- belongs_to :site, class_name: 'CamaleonCms::Site', foreign_key: :parent_id
6
+ has_many :assigned, foreign_key: :post_parent, dependent: :destroy, inverse_of: :sidebar
7
+ belongs_to :site, class_name: 'CamaleonCms::Site', foreign_key: :parent_id, inverse_of: :sidebars, optional: true
11
8
 
12
9
  # scopes
13
10
  scope :default_sidebar, -> { where(slug: 'default-sidebar') }
@@ -1,16 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class CamaleonRecord < ActiveRecord::Base
4
- TRANSLATION_TAG_HIDE_MAP = { '<!--' => '!--', '-->' => '--!' }.freeze
3
+ class CamaleonRecord < ActiveRecord::Base # rubocop:disable Rails/ApplicationRecord
4
+ # Sentinels used to shield HTML comment delimiters (the translation locale markers such as `<!--:en-->`)
5
+ # from ActionController's sanitize(), which strips HTML comments. Private-Use-Area code points are used so
6
+ # that plain text typed by a user can never be rewritten into a comment delimiter after sanitization: the
7
+ # previous `!--`/`--!` tokens collided with ordinary text (e.g. "Read more !--"), so the restore pass would
8
+ # inject stray `<!--`/`-->` into the output. The hide map also strips any raw sentinel characters supplied
9
+ # in the input, so they cannot be smuggled through to become delimiters on restore.
10
+ TRANSLATION_TAG_HIDE_SENTINELS = { open: "\u{E000}", close: "\u{E001}" }.freeze
11
+ TRANSLATION_TAG_HIDE_MAP = {
12
+ '<!--' => TRANSLATION_TAG_HIDE_SENTINELS[:open],
13
+ '-->' => TRANSLATION_TAG_HIDE_SENTINELS[:close],
14
+ TRANSLATION_TAG_HIDE_SENTINELS[:open] => '',
15
+ TRANSLATION_TAG_HIDE_SENTINELS[:close] => ''
16
+ }.freeze
5
17
  TRANSLATION_TAG_HIDE_REGEX = Regexp.new(TRANSLATION_TAG_HIDE_MAP.keys.map { |x| Regexp.escape(x) }.join('|')).freeze
6
- TRANSLATION_TAG_RESTORE_MAP = { '--!' => '-->', '!--' => '<!--' }.freeze
18
+ TRANSLATION_TAG_RESTORE_MAP = {
19
+ TRANSLATION_TAG_HIDE_SENTINELS[:open] => '<!--',
20
+ TRANSLATION_TAG_HIDE_SENTINELS[:close] => '-->'
21
+ }.freeze
7
22
  TRANSLATION_TAG_RESTORE_REGEX =
8
23
  Regexp.new(TRANSLATION_TAG_RESTORE_MAP.keys.map { |x| Regexp.escape(x) }.join('|')).freeze
9
24
 
10
- include ActiveRecordExtras::Relation
11
-
12
25
  self.abstract_class = true
13
26
 
27
+ # Sanitize a value with ActionController's sanitize() while preserving translation locale markers
28
+ # (<!--:xx-->). Shared by Post#sanitize_content and the NormalizeAttrs concern so the transform lives in
29
+ # one place. Returns nil unchanged. `tags:`/`attributes:` default to nil, i.e. the sanitizer's own
30
+ # allowlist, so NormalizeAttrs and other callers are unaffected; Post#content passes a widened list.
31
+ def self.cama_sanitize_translatable(value, tags: nil, attributes: nil)
32
+ return value if value.nil?
33
+
34
+ options = {}
35
+ options[:tags] = tags if tags
36
+ options[:attributes] = attributes if attributes
37
+ ActionController::Base.helpers.sanitize(
38
+ value.to_s.gsub(TRANSLATION_TAG_HIDE_REGEX, TRANSLATION_TAG_HIDE_MAP), **options
39
+ ).gsub(TRANSLATION_TAG_RESTORE_REGEX, TRANSLATION_TAG_RESTORE_MAP)
40
+ end
41
+
42
+ def self.polymorphic_name
43
+ return super unless name.to_s.start_with?('CamaleonCms::')
44
+
45
+ name.demodulize
46
+ end
47
+
48
+ def self.polymorphic_class_for(name)
49
+ super
50
+ rescue NameError
51
+ legacy_class = legacy_camaleon_polymorphic_class(name)
52
+ return legacy_class if legacy_class
53
+ return nil if legacy_polymorphic_marker?(name)
54
+
55
+ raise
56
+ end
57
+
14
58
  # save cache value for this key
15
59
  def cama_set_cache(key, val)
16
60
  @cama_cache_vars ||= {}
@@ -20,6 +64,8 @@ class CamaleonRecord < ActiveRecord::Base
20
64
 
21
65
  # remove cache value for this key
22
66
  def cama_remove_cache(key)
67
+ return unless @cama_cache_vars
68
+
23
69
  @cama_cache_vars.delete(cama_build_cache_key(key))
24
70
  end
25
71
 
@@ -49,4 +95,76 @@ class CamaleonRecord < ActiveRecord::Base
49
95
  def cama_build_cache_key(key)
50
96
  _key = "cama_cache_#{self.class.name}_#{id}_#{key}"
51
97
  end
98
+
99
+ # Return the current user for this thread/request context.
100
+ # Uses ActiveSupport::CurrentAttributes (CurrentRequest.user)
101
+ def current_user
102
+ return @current_user if defined?(@current_user)
103
+
104
+ @current_user = CurrentRequest.user
105
+ end
106
+
107
+ # Authorization helpers that delegate to central Ability (CanCan)
108
+ def can?(*args)
109
+ # Return false if no user or site context (e.g., background jobs, console)
110
+ return false if current_user.nil? || current_site.nil?
111
+
112
+ ability.can?(*args)
113
+ end
114
+
115
+ def ability
116
+ # Memoize Ability per request to avoid repeated DB queries and object instantiation.
117
+ # In tests that modify role meta mid-request, call reset_ability to invalidate cache.
118
+ @ability ||= CamaleonCms::Ability.new(current_user, current_site)
119
+ end
120
+
121
+ # Reset cached ability instance (useful in tests when role meta changes)
122
+ def reset_ability
123
+ @ability = nil
124
+ end
125
+
126
+ # current_site memoized from the CurrentRequest.site
127
+ def current_site
128
+ return @current_site if defined?(@current_site) && @current_site.present?
129
+
130
+ @current_site = CurrentRequest.site
131
+ end
132
+
133
+ def self.legacy_camaleon_polymorphic_class(name)
134
+ class_name = name.to_s
135
+ return if class_name.blank?
136
+
137
+ candidates = []
138
+
139
+ unless class_name.include?('::')
140
+ candidates << "CamaleonCms::#{class_name}"
141
+
142
+ if class_name.include?('_')
143
+ base_name = class_name.split('_').first
144
+ candidates << "CamaleonCms::#{base_name}" if base_name.present?
145
+ end
146
+
147
+ candidates << CamaManager.get_user_class_name.to_s if class_name == 'User'
148
+ end
149
+
150
+ candidates << "CamaleonCms::#{class_name}" if class_name.include?('::') && !class_name.start_with?('CamaleonCms::')
151
+
152
+ candidates.uniq.each do |candidate|
153
+ next unless valid_constant_name?(candidate)
154
+
155
+ klass = candidate.safe_constantize
156
+ return klass if klass
157
+ end
158
+
159
+ nil
160
+ end
161
+ private_class_method def self.legacy_polymorphic_marker?(name)
162
+ name.to_s.start_with?('_')
163
+ end
164
+
165
+ private_class_method def self.valid_constant_name?(name)
166
+ name.to_s.match?(/\A[A-Z]\w*(::[A-Z]\w*)*\z/)
167
+ end
168
+
169
+ private_class_method :legacy_camaleon_polymorphic_class
52
170
  end