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
@@ -10,7 +10,8 @@ namespace :camaleon_cms do
10
10
  include Rails.application.routes.url_helpers
11
11
  $current_site = CamaleonCms::Site.find(ENV['site_id'].to_i)
12
12
  cama_uploader_init_connection
13
- @fog_connection_bucket_dir.files.all.each do |file|
13
+ # TODO: The `fog` gem isn't used anymore - this whole task should be refactored
14
+ @fog_connection_bucket_dir.files.all.each do |file| # rubocop:disable Rails/FindEach
14
15
  puts file.inspect
15
16
  cama_uploader_generate_thumbnail(file.key, file.key, '')
16
17
  end
@@ -0,0 +1,74 @@
1
+ namespace :camaleon_cms do
2
+ # A custom field group carries tenancy in `parent_id` (which site owns and administers it) and
3
+ # placement in `object_class` + `objectid` (which record's admin page displays it). Placement reads
4
+ # are not scoped by site, so a group whose two disagree renders on the site owning the placement
5
+ # target while staying invisible in that site's own field group list, which is tenancy-scoped.
6
+ #
7
+ # Re-home rather than delete: a mismatch is not proof of an injection -- a plugin stamping
8
+ # parent_id from the wrong site produces the same shape -- and deleting would take the group's
9
+ # field definitions and their values with it. Setting parent_id to the site that already renders
10
+ # the group restores the invariant and surfaces it where an administrator can judge and remove it.
11
+ desc 'Re-home custom field groups whose owning site differs from the site owning their placement'
12
+ task rehome_cross_site_field_groups: :environment do
13
+ report = CamaleonCms::TaskReporter
14
+ report.call 'Re-homing cross-site custom field groups...'
15
+ rehomed_count = 0
16
+ skipped_count = 0
17
+
18
+ CamaleonCms::CustomFieldGroup.unscoped.where.not(object_class: '_fields').find_each do |group|
19
+ site_id = cama_placement_site_id(group)
20
+
21
+ if site_id.blank?
22
+ report.call " Skipped group id=#{group.id} slug=#{group.slug} " \
23
+ "(#{group.object_class}/#{group.objectid} does not resolve to a site)"
24
+ skipped_count += 1
25
+ next
26
+ end
27
+
28
+ next if site_id == group.parent_id
29
+
30
+ begin
31
+ report.call "✓ Re-homed group id=#{group.id} slug=#{group.slug} " \
32
+ "from site_id=#{group.parent_id} to site_id=#{site_id} " \
33
+ "(placement #{group.object_class}/#{group.objectid})"
34
+ group.update_column(:parent_id, site_id) # rubocop:disable Rails/SkipsModelValidations
35
+ rehomed_count += 1
36
+ rescue StandardError => e
37
+ report.call "✗ Failed to re-home group id=#{group.id}: #{e.message}"
38
+ skipped_count += 1
39
+ end
40
+ end
41
+
42
+ report.call "\nSummary:"
43
+ report.call " Re-homed: #{rehomed_count} field groups"
44
+ report.call " Skipped: #{skipped_count} field groups"
45
+ report.call "\nDone. Review the field group list of any site that gained a group."
46
+ end
47
+ end
48
+
49
+ # Resolve the site owning a group's placement target, or nil when it cannot be determined.
50
+ # Best-effort by design: a placement naming a record that no longer exists is left alone rather
51
+ # than guessed at or deleted.
52
+ def cama_placement_site_id(group)
53
+ objectid = group.objectid
54
+ return nil if objectid.blank?
55
+
56
+ case group.object_class
57
+ when 'PostType', 'PostType_Post', 'PostType_Category', 'PostType_PostTag'
58
+ CamaleonCms::PostType.find_by(id: objectid)&.parent_id
59
+ when 'Theme' then CamaleonCms::Theme.find_by(id: objectid)&.parent_id
60
+ when 'Plugin' then CamaleonCms::Plugin.find_by(id: objectid)&.parent_id
61
+ when 'NavMenu' then CamaleonCms::NavMenu.find_by(id: objectid)&.parent_id
62
+ when 'Widget::Main' then CamaleonCms::Widget::Main.find_by(id: objectid)&.parent_id
63
+ when 'Post' then CamaleonCms::Post.find_by(id: objectid)&.post_type&.parent_id
64
+ when 'Category', 'Category_Post' then CamaleonCms::Category.find_by(id: objectid)&.post_type_parent&.parent_id
65
+ when 'PostTag' then CamaleonCms::PostTag.find_by(id: objectid)&.post_type&.parent_id
66
+ else
67
+ # Site, the configured user model, and models registered through the custom_field_custom_models
68
+ # hook are all placed with the site's own id, so the placement target is the site itself.
69
+ CamaleonCms::Site.find_by(id: objectid)&.id
70
+ end
71
+ rescue StandardError => e
72
+ CamaleonCms::TaskReporter.call " Could not resolve placement for group id=#{group.id}: #{e.message}"
73
+ nil
74
+ end
@@ -0,0 +1,58 @@
1
+ namespace :camaleon_cms do
2
+ desc 'Backfill user roles to include custom_fields manager permission'
3
+ task backfill_custom_fields_permission: :environment do
4
+ report = CamaleonCms::TaskReporter
5
+ report.call 'Backfilling custom_fields manager permission for existing user roles...'
6
+ CamaleonCms::UserRole.find_each do |role|
7
+ key = "_manager_#{role.parent_id}"
8
+ begin
9
+ current_role = role.get_meta(key)
10
+ # if the role already has settings/managers, skip; otherwise add custom_fields => 1
11
+ if current_role.blank? || (!current_role.is_a?(Hash) || current_role['custom_fields'].blank?)
12
+ current_role = (current_role.is_a?(Hash) ? current_role : {}).merge!('custom_fields' => 1)
13
+ role.set_meta(key, current_role)
14
+ report.call "Updated role=#{role.slug} site_id=#{role.parent_id}"
15
+ else
16
+ report.call "Skipped role=#{role.slug} site_id=#{role.parent_id} (already has custom_fields)"
17
+ end
18
+ rescue StandardError => e
19
+ report.call "Failed to update role=#{role.slug}: #{e.message}"
20
+ end
21
+ end
22
+ report.call 'Done.'
23
+ end
24
+
25
+ desc 'Backfill admin user roles to include select_eval permission'
26
+ task backfill_select_eval_permission: :environment do
27
+ report = CamaleonCms::TaskReporter
28
+ report.call 'Backfilling select_eval permission for admin roles...'
29
+ updated_count = 0
30
+ skipped_count = 0
31
+
32
+ CamaleonCms::UserRole.where(slug: 'admin', term_group: -1).find_each do |role|
33
+ site_id = role.parent_id
34
+ key = "_manager_#{site_id}"
35
+ begin
36
+ current_meta = role.get_meta(key, {})
37
+
38
+ # Only update if role doesn't already have select_eval
39
+ if !current_meta[:select_eval]
40
+ updated_meta = current_meta.merge(select_eval: 1)
41
+ role.set_meta(key, updated_meta)
42
+ report.call "✓ Updated admin role site_id=#{site_id}"
43
+ updated_count += 1
44
+ else
45
+ report.call " Skipped admin role site_id=#{site_id} (already has select_eval)"
46
+ skipped_count += 1
47
+ end
48
+ rescue StandardError => e
49
+ report.call "✗ Failed to update admin role site_id=#{site_id}: #{e.message}"
50
+ end
51
+ end
52
+
53
+ report.call "\nSummary:"
54
+ report.call " Updated: #{updated_count} admin roles"
55
+ report.call " Skipped: #{skipped_count} admin roles"
56
+ report.call "\nDone! All admin roles now have select_eval permission."
57
+ end
58
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :camaleon_cms do
4
+ desc 'Reassign comments whose user no longer exists to the owning site\'s anonymous user'
5
+ task reassign_orphaned_comments: :environment do
6
+ user_table = CamaManager.get_user_class_name.constantize.table_name
7
+ comment_table = CamaleonCms::PostComment.table_name
8
+
9
+ # `user_id IS NULL` on its own does not mean "orphaned": guest comments are a first-class
10
+ # shape — user_id is nullable and the row carries author/author_email/author_url instead —
11
+ # and they have always had a null user_id. The regression nulled the user_id of *registered*
12
+ # users' comments, and those never carry an author string, which is exactly why
13
+ # PostCommentDecorator#the_author_name falls back to the user's name. Requiring a blank
14
+ # author therefore separates the rows this task exists to repair from the ones it must not
15
+ # touch. Rows with a dangling (non-null, non-existent) user_id are repaired regardless.
16
+ orphans = CamaleonCms::PostComment
17
+ .where("(#{comment_table}.user_id IS NULL AND " \
18
+ "(#{comment_table}.author IS NULL OR #{comment_table}.author = '')) " \
19
+ "OR (#{comment_table}.user_id IS NOT NULL AND #{comment_table}.user_id NOT IN " \
20
+ "(SELECT id FROM #{user_table}))")
21
+ reassigned = 0
22
+ skipped = 0
23
+
24
+ orphans.find_each do |comment|
25
+ site = comment.post&.post_type&.site
26
+ if site.nil?
27
+ skipped += 1
28
+ next
29
+ end
30
+
31
+ comment.update_column(:user_id, site.get_anonymous_user.id) # rubocop:disable Rails/SkipsModelValidations
32
+ reassigned += 1
33
+ end
34
+
35
+ summary = "camaleon_cms:reassign_orphaned_comments reassigned #{reassigned} comment(s)" \
36
+ "#{skipped > 0 ? ", skipped #{skipped} without a resolvable site" : ''}"
37
+ Rails.logger.info(summary)
38
+ # An operator runs this from a terminal, where Rails.logger writes to a file they are not
39
+ # watching. Without this the task looks like it did nothing.
40
+ puts summary
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :camaleon_cms do
4
+ desc 'Re-apply the owner-only ACL to S3 objects under each AWS site\'s private upload prefix ' \
5
+ '(uploads stored before the private-ACL fix were world-readable). ' \
6
+ 'Set CAMA_S3_INNER_FOLDER for setups whose uploader hook configures an inner_folder.'
7
+ task repair_private_upload_acls: :environment do
8
+ inner_folder = ENV['CAMA_S3_INNER_FOLDER'].to_s
9
+ repaired_total = 0
10
+ swept_sites = 0
11
+
12
+ CamaleonCms::Site.find_each do |site_record|
13
+ site = site_record.decorate
14
+ next unless %w[aws s3].include?(site.get_option('filesystem_type', 'local').to_s.downcase)
15
+
16
+ uploader = CamaleonCmsAwsUploader.new({ current_site: site,
17
+ aws_settings: { 'inner_folder' => inner_folder } }, nil)
18
+ repaired = uploader.repair_private_acls!
19
+ swept_sites += 1
20
+ repaired_total += repaired
21
+ puts " #{site.the_slug}: #{repaired} object(s) re-ACLed"
22
+ end
23
+
24
+ summary = "camaleon_cms:repair_private_upload_acls re-ACLed #{repaired_total} object(s) " \
25
+ "across #{swept_sites} AWS site(s)"
26
+ Rails.logger.info(summary)
27
+ # An operator runs this from a terminal, where Rails.logger writes to a file they are not
28
+ # watching. Without this the task looks like it did nothing.
29
+ puts summary
30
+ end
31
+ end
@@ -0,0 +1,36 @@
1
+ namespace :camaleon_cms do
2
+ # Site#get_field_groups matches a group's placement (object_class + objectid) instead of its
3
+ # tenancy (parent_id). `objectid` has no presence validation, so a group could be persisted with
4
+ # object_class 'Site' and a NULL objectid by bypassing add_custom_field_group. Those rows are
5
+ # invisible on the site settings page under the placement-scoped read, so give them the objectid
6
+ # they should always have had: the site that owns them.
7
+ desc 'Backfill objectid for site custom field groups that were saved without one'
8
+ task backfill_site_field_group_objectid: :environment do
9
+ report = CamaleonCms::TaskReporter
10
+ report.call 'Backfilling objectid for site custom field groups...'
11
+ updated_count = 0
12
+ skipped_count = 0
13
+
14
+ CamaleonCms::CustomField.unscoped.where(object_class: 'Site', objectid: nil).find_each do |group|
15
+ if group.parent_id.blank?
16
+ report.call " Skipped group id=#{group.id} slug=#{group.slug} (no owning site)"
17
+ skipped_count += 1
18
+ next
19
+ end
20
+
21
+ begin
22
+ group.update_column(:objectid, group.parent_id) # rubocop:disable Rails/SkipsModelValidations
23
+ report.call "✓ Updated group id=#{group.id} slug=#{group.slug} site_id=#{group.parent_id}"
24
+ updated_count += 1
25
+ rescue StandardError => e
26
+ report.call "✗ Failed to update group id=#{group.id}: #{e.message}"
27
+ skipped_count += 1
28
+ end
29
+ end
30
+
31
+ report.call "\nSummary:"
32
+ report.call " Updated: #{updated_count} field groups"
33
+ report.call " Skipped: #{skipped_count} field groups"
34
+ report.call "\nDone."
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :camaleon_cms do
4
+ namespace :security do
5
+ # The scan-and-reject gates refuse dangerous content at save time but never rewrite what is
6
+ # already stored (nothing is ever rewritten under this model). This task lists the stored
7
+ # content that would fail today's gates -- post content and gated custom-field values -- so an
8
+ # operator can review and clean it up by hand. Read-only: it changes nothing.
9
+ desc 'List stored post content and custom-field values that would fail the scan-and-reject gates'
10
+ task scan_content: :environment do
11
+ report = CamaleonCms::TaskReporter
12
+ report.call 'Scanning stored content against the scan-and-reject gates (read-only)...'
13
+ flagged = 0
14
+
15
+ CamaleonCms::Post.unscoped.where.not(content: [nil, '']).find_each do |post|
16
+ next unless CamaleonCms::UnsafeMarkup.unsafe_html?(
17
+ post.content, tags: CamaleonCms::Post::CONTENT_ALLOWED_TAGS,
18
+ attributes: CamaleonCms::Post::CONTENT_ALLOWED_ATTRIBUTES
19
+ )
20
+
21
+ flagged += 1
22
+ report.call "✗ Post id=#{post.id} (#{post.post_class}) '#{post.slug.to_s.truncate(60)}': " \
23
+ 'content would be rejected'
24
+ end
25
+
26
+ # Reuse the model's own gate dispatch (which covers editor, field_attrs and URI field types),
27
+ # so the scan can never diverge from what the save-time validation would refuse.
28
+ CamaleonCms::CustomFieldsRelationship.unscoped.where.not(value: [nil, ''])
29
+ .includes(:custom_field).find_each do |row|
30
+ field_key = row.custom_field&.options&.[](:field_key).to_s
31
+ next unless CamaleonCms::CustomFieldsRelationship.gate_rejection_reason(field_key, row.value)
32
+
33
+ flagged += 1
34
+ report.call "✗ Custom-field value id=#{row.id} field='#{row.custom_field_slug}' " \
35
+ "(#{field_key}) on #{row.object_class} ##{row.objectid}: value would be rejected"
36
+ end
37
+
38
+ report.call "Done. #{flagged} stored item(s) would be rejected by today's gates."
39
+ report.call 'Nothing was modified; review and clean up the listed items by hand.'
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :camaleon_cms do
4
+ namespace :security do
5
+ # The upload rules refuse dangerous content at upload time but never re-examine what is already
6
+ # stored (nothing is ever rewritten under this model). This task lists the stored files today's
7
+ # rules would refuse, so an operator can review them by hand. Read-only: it changes nothing.
8
+ #
9
+ # The companion for authored content is camaleon_cms:security:scan_content.
10
+ desc 'List stored media files that would fail the upload scan rules'
11
+ task scan_uploads: :environment do
12
+ report = CamaleonCms::TaskReporter
13
+ # Reuse the real scanner rather than reimplementing its rules, so the report can never
14
+ # disagree with what an upload of the same bytes would be told today.
15
+ scanner = Class.new { include CamaleonCms::UploaderContentSecurity }.new
16
+ flagged = 0
17
+ scanned = 0
18
+
19
+ report.call 'Scanning stored media against the upload scan rules (read-only)...'
20
+
21
+ # Scans every file under `root`, reporting those today's rules would refuse. `label` names the
22
+ # location and `base` anchors the reported path. Closes over the counters so the public and
23
+ # private roots share one pass rather than duplicating the scan logic (and risking drift).
24
+ # FNM_DOTMATCH so dotfiles are scanned: a basename that is entirely an extension (`.svg`, `.js`)
25
+ # is exactly what the scanner fail-closes on. `.`/`..` are directory entries File.file? drops.
26
+ scan_tree = lambda do |root, base, label|
27
+ next unless root.directory?
28
+
29
+ Dir.glob(root.join('**', '*'), File::FNM_DOTMATCH).sort.each do |path|
30
+ next unless File.file?(path)
31
+
32
+ scanned += 1
33
+ relative = Pathname.new(path).relative_path_from(base)
34
+ reason = begin
35
+ scanner.content_unsafe?(File.binread(path), filename: File.basename(path))
36
+ rescue StandardError => e
37
+ # A file the scan cannot read at all is reported rather than skipped: an unreadable
38
+ # entry is exactly what an operator wants to know about, and swallowing it would hand
39
+ # them a false all-clear.
40
+ "unreadable (#{e.class})"
41
+ end
42
+ next unless reason
43
+
44
+ flagged += 1
45
+ report.call "✗ #{label} /#{relative}: would be rejected (#{reason})"
46
+ end
47
+ end
48
+
49
+ CamaleonCms::Site.unscoped.find_each do |site|
50
+ # Site#upload_directory honours the `media_slug_folder` config, which stores uploads under
51
+ # media/<slug> instead of media/<id>. Hardcoding the id scanned nothing on a slug-configured
52
+ # install and printed a clean "0 would be rejected" -- a false all-clear.
53
+ scan_tree.call(Pathname.new(site.upload_directory), Rails.public_path, "Site #{site.id}")
54
+ end
55
+
56
+ # Private media is scanned at upload time exactly like public media, so files stored before
57
+ # these rules apply are equally worth surfacing. It lives outside the public root, under its
58
+ # own directory, so it is scanned separately and reported relative to the app root.
59
+ scan_tree.call(Rails.root.join(CamaleonCmsUploader::PRIVATE_DIRECTORY), Rails.root, 'Private media')
60
+
61
+ report.call "Done. #{scanned} stored file(s) scanned, #{flagged} would be rejected by today's rules."
62
+ report.call 'Nothing was modified.'
63
+ if flagged > 0
64
+ report.call 'A finding is not by itself evidence of compromise: these rules are newer than ' \
65
+ 'the files, so content uploaded legitimately under the previous rules is ' \
66
+ 'listed too. Review each one before acting.'
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :camaleon_cms do
4
+ # Through 2.9.2 the custom-fields settings form emitted the raw `user_model` config string as
5
+ # the users placement option, so a namespaced host user model (e.g. 'Admin::User') stored its
6
+ # user field groups under the qualified name — which the demodulized association scope and the
7
+ # 2.9.2 allowed-slugs save filter cannot see. Re-key those group placements to the demodulized
8
+ # contract. Only the group rows move: their fields hang off parent_id under '_fields', group
9
+ # metas key off 'CustomFieldGroup', and value rows were always written under the demodulized
10
+ # name. Keyed to the current config: a host that renamed its user model after writing groups
11
+ # should temporarily restore the historical value while running the task.
12
+ desc 'Re-key user custom field groups stored under a namespaced user_model name'
13
+ task demodulize_user_field_groups: :environment do
14
+ configured = PluginRoutes.static_system_info['user_model'].presence.to_s
15
+ demodulized = configured.demodulize
16
+
17
+ if configured.blank? || configured == demodulized
18
+ puts 'Nothing to re-key: the configured user model is not namespaced.'
19
+ next
20
+ end
21
+
22
+ count = CamaleonCms::CustomField.unscoped
23
+ .where(object_class: configured)
24
+ .update_all(object_class: demodulized) # rubocop:disable Rails/SkipsModelValidations
25
+ puts "Re-keyed #{count} user field group(s) from '#{configured}' to '#{demodulized}'."
26
+ end
27
+ end