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
@@ -0,0 +1,143 @@
1
+ # rubocop:disable Rails/OutputSafety
2
+ module CamaleonCms
3
+ module RuntimeHtmlContentConcern
4
+ extend ActiveSupport::Concern
5
+
6
+ # Single source of truth for the cama_content_* surface (prepend/append/draw).
7
+ # Including it here keeps those methods reachable from controllers (e.g. plugin
8
+ # hooks running in controller context call cama_content_append) while the shared
9
+ # view helper stays ivar-free.
10
+ include CamaleonCms::ContentHelper
11
+
12
+ def cama_html_helpers_init
13
+ state = camaleon_html_helper_state
14
+ state[:pre_assets_content] = []
15
+ state[:assets_libraries] = {}
16
+ state[:assets_content] = []
17
+ end
18
+
19
+ def cama_load_libraries(*keys)
20
+ state = camaleon_html_helper_state
21
+ keys.each do |key|
22
+ library = cama_assets_libraries[key.to_sym]
23
+ state[:assets_libraries][key.to_sym] = library if library.present?
24
+ end
25
+ end
26
+ alias add_asset_library cama_load_libraries
27
+
28
+ def append_asset_libraries(libraries)
29
+ state = camaleon_html_helper_state
30
+ libraries.each do |key, library|
31
+ state[:assets_libraries][key.to_sym] = if state[:assets_libraries].include?(key)
32
+ state[:assets_libraries][key.to_sym].merge(library)
33
+ else
34
+ library
35
+ end
36
+ end
37
+ end
38
+ alias cama_load_custom_assets append_asset_libraries
39
+
40
+ def append_asset_content(content)
41
+ camaleon_html_helper_state[:assets_content] << content
42
+ end
43
+
44
+ def append_pre_asset_content(content)
45
+ camaleon_html_helper_state[:pre_assets_content] << content
46
+ end
47
+
48
+ def cama_draw_pre_asset_contents
49
+ camaleon_html_helper_state[:pre_assets_content].join('').html_safe
50
+ end
51
+
52
+ def cama_draw_custom_assets
53
+ state = camaleon_html_helper_state
54
+ cama_html_helpers_init if state[:assets_libraries].blank?
55
+ libs = []
56
+ state[:assets_libraries].each_value do |assets|
57
+ libs += assets[:css] if assets[:css].present?
58
+ end
59
+ stylesheets = libs.uniq
60
+ css = helpers.stylesheet_link_tag(*stylesheets, media: 'all')
61
+
62
+ libs = []
63
+ state[:assets_libraries].each_value do |assets|
64
+ libs += assets[:js] if assets[:js].present?
65
+ end
66
+ javascripts = libs.uniq
67
+ js = helpers.javascript_include_tag(*javascripts)
68
+
69
+ args = { stylesheets: stylesheets, javascripts: javascripts, js_html: js, css_html: css }
70
+ hooks_run('draw_custom_assets', args)
71
+ trusted_fragments = [args[:css_html], args[:js_html], *state[:assets_content]].filter_map do |fragment|
72
+ next if fragment.blank?
73
+
74
+ fragment.is_a?(ActiveSupport::SafeBuffer) ? fragment : fragment.to_s.html_safe
75
+ end
76
+
77
+ helpers.safe_join(trusted_fragments, "\n")
78
+ end
79
+
80
+ def theme_init
81
+ breadcrumb_items = []
82
+ camaleon_theme_state[:front_breadcrumb] = breadcrumb_items
83
+ # back-compat ivar: themes/plugins may read @_front_breadcrumb directly.
84
+ # Owned here (controller concern) so the shared view helper stays ivar-free.
85
+ @_front_breadcrumb = breadcrumb_items
86
+ end
87
+
88
+ def breadcrumb_add(label, url = '', prepend = false)
89
+ breadcrumb_items = nav_menu_breadcrumb_items
90
+ if prepend
91
+ breadcrumb_items.unshift([label, url])
92
+ else
93
+ breadcrumb_items << [label, url]
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def camaleon_html_helper_state
100
+ state = CurrentRequest.html_helper_state ||= {}
101
+ state[:pre_assets_content] ||= []
102
+ state[:assets_libraries] ||= {}
103
+ state[:assets_content] ||= []
104
+ state
105
+ end
106
+
107
+ def cama_assets_libraries
108
+ state = camaleon_html_helper_state
109
+ return state[:cama_assets_libraries] if state[:cama_assets_libraries].present?
110
+
111
+ libs = {}
112
+ libs[:colorpicker] =
113
+ { js: ['camaleon_cms/admin/bootstrap-colorpicker'], css: ['camaleon_cms/admin/colorpicker'] }
114
+ libs[:datepicker] = { js: [] }
115
+ libs[:datetimepicker] = { js: [], css: [] }
116
+ libs[:tinymce] =
117
+ { js: %w[camaleon_cms/admin/tinymce/tinymce.min camaleon_cms/admin/tinymce/plugins/filemanager/plugin.min],
118
+ css: ['camaleon_cms/admin/tinymce/skins/lightgray/content.min'] }
119
+ libs[:form_ajax] = { js: ['camaleon_cms/admin/form/jquery.form'] }
120
+ libs[:cropper] = {}
121
+ libs[:post] =
122
+ { js: %w[camaleon_cms/admin/jquery.tagsinput.min camaleon_cms/admin/post],
123
+ css: ['camaleon_cms/admin/jquery.tagsinput'] }
124
+ libs[:multiselect] = { js: ['camaleon_cms/admin/bootstrap-select.js'] }
125
+ libs[:validate] = { js: ['camaleon_cms/admin/jquery.validate'] }
126
+ libs[:nav_menu] =
127
+ { css: ['camaleon_cms/admin/nestable/jquery.nestable'],
128
+ js: %w[camaleon_cms/admin/jquery.nestable camaleon_cms/admin/nav_menu] }
129
+ libs[:admin_intro] =
130
+ { js: ['camaleon_cms/admin/introjs/intro.min'], css: ['camaleon_cms/admin/introjs/introjs.min'] }
131
+ state[:cama_assets_libraries] = libs
132
+ end
133
+
134
+ def camaleon_theme_state
135
+ CurrentRequest.theme_helper_state ||= {}
136
+ end
137
+
138
+ def nav_menu_breadcrumb_items
139
+ camaleon_theme_state[:front_breadcrumb] ||= []
140
+ end
141
+ end
142
+ end
143
+ # rubocop:enable Rails/OutputSafety
@@ -0,0 +1,246 @@
1
+ # rubocop:disable Layout/LineLength
2
+ module CamaleonCms
3
+ module RuntimeShortcodeThemeConcern
4
+ extend ActiveSupport::Concern
5
+
6
+ # Theme asset/view helpers are owned by CamaleonCms::ThemeHelper (single
7
+ # source of truth shared with views). Including it here exposes them on the
8
+ # controller runtime stack so plugin/theme hooks executed in controller
9
+ # context (e.g. `on_install_theme`) can call `theme_asset`, `theme_view`,
10
+ # `theme_asset_path`, etc. ThemeHelper is ivar-free, so it is safe to mix in.
11
+ include CamaleonCms::ThemeHelper
12
+
13
+ def shortcodes_init
14
+ CurrentRequest.shortcodes = []
15
+ CurrentRequest.shortcodes_template = {}
16
+ CurrentRequest.shortcodes_descr = {}
17
+
18
+ shortcode_add('widget', nil, "Renderize the widget content in this place.
19
+ Don't forget to create and copy the shortcode of your widgets in appearance -> widgets
20
+ Sample: [widget widget_key]")
21
+
22
+ shortcode_add(
23
+ 'load_libraries',
24
+ lambda do |attrs, args|
25
+ return args[:shortcode] if attrs.blank?
26
+
27
+ cama_load_libraries(*attrs['data'].to_s.split(','))
28
+ ''
29
+ end,
30
+ "Permit to load libraries on demand, sample: [load_libraries data='datepicker,tinymce']"
31
+ )
32
+ shortcode_add(
33
+ 'asset',
34
+ lambda do |attrs, args|
35
+ return args[:shortcode] if attrs.blank?
36
+
37
+ url = shortcode_asset_reference(attrs['file'], as_path: attrs['as_path'].present?)
38
+ if attrs['image'].present?
39
+ ActionController::Base.helpers.image_tag(url, class: attrs['class'], style: attrs['style'])
40
+ else
41
+ url
42
+ end
43
+ end,
44
+ "Permit to generate an asset url (
45
+ add file='' asset file path,
46
+ add as_path='true' to generate only the path and not the full url,
47
+ add class='my_class' to setup image class,
48
+ add style='height: 100px; width: 200px;...' to setup image style,
49
+ add image='true' to generate the image tag with this url),
50
+ sample: <img src=\"[asset as_path='true' file='themes/my_theme/assets/img/signature.png']\" /> or [asset image='true' file='themes/my_theme/assets/img/signature.png' style='height: 50px;']"
51
+ )
52
+
53
+ shortcode_add(
54
+ 'data',
55
+ ->(attrs, args) { attrs.present? ? cama_shortcode_data(attrs, args) : args[:shortcode] },
56
+ "Permit to generate specific data of a post.
57
+ Attributes:
58
+ object: (String, defaut post) Model name: post | posttype | category | posttag | site | user |theme | navmenu
59
+ id: (Integer) Post id
60
+ key: (String) Post slug
61
+ field: (String) Custom field key, you can add render_field='true' to render field as html element, also you can add index=2 to indicate the value in position 2 for multitple values
62
+ attrs: (String) attribute name
63
+ post: title | created_at | excerpt | url | link | thumb | updated_at | author_name | author_url | content
64
+ posttype: title | created_at | excerpt | url | link | thumb | updated_at
65
+ category: title | created_at | excerpt | url | link | thumb | updated_at
66
+ posttag: title | created_at | excerpt | url | link | thumb | updated_at
67
+ site: title | created_at | excerpt | url | link | thumb | updated_at
68
+ user: title | created_at | excerpt | url | link | thumb | updated_at
69
+ theme: title | created_at | excerpt | url | link | thumb | updated_at
70
+ navmenu: title | created_at | excerpt | url | link | thumb | updated_at
71
+ Note: If id and key is not present, then will be rendered for current model
72
+ Sample:
73
+ [data id='122' attr='title'] ==> return the title of post with id = 122
74
+ [data key='contact' attr='url'] ==> return the url of post with slug = contact
75
+ [data key='contact' attr='link'] ==> return the link with title as text of the link of post with slug = contact
76
+ [data object='site' attr='url'] ==> return the url of currrent site
77
+ [data key='page' object='posttype' attr='url'] ==> return the url of post_type with slug = page
78
+ [data field=icon index=2] ==> return the second value (multiple values) for this custom field with key=icon of current object
79
+ [data key='contact' field='sub_title'] ==> return the value of the custom_field with key=sub_title registered for post.slug = contact
80
+ [data object='site' field='sub_title'] ==> return the value of the custom_field with key=sub_title registered for current_site"
81
+ )
82
+ end
83
+
84
+ def shortcode_add(key, template = nil, descr = '')
85
+ shortcode_keys << key
86
+ CurrentRequest.shortcodes_template = shortcode_templates.merge({ key.to_s => template }) if template.present?
87
+ shortcode_descriptions[key] = descr if descr.present?
88
+ end
89
+
90
+ def shortcode_keys
91
+ CurrentRequest.shortcodes ||= []
92
+ end
93
+
94
+ def shortcode_templates
95
+ CurrentRequest.shortcodes_template ||= {}
96
+ end
97
+
98
+ def shortcode_descriptions
99
+ CurrentRequest.shortcodes_descr ||= {}
100
+ end
101
+
102
+ def shortcode_asset_reference(file, as_path: false)
103
+ return if file.blank?
104
+
105
+ # Defensive ordering: assign the locals the rescue branch uses before everything else, so it
106
+ # can never see them nil. The only call that raises AssetNotFound is the asset helper below —
107
+ # resolve_shortcode_theme_asset never does (regex, string interpolation and File.exist? only).
108
+ helper = ActionController::Base.helpers
109
+ method_name = as_path ? :asset_path : :asset_url
110
+ file = resolve_shortcode_theme_asset(file)
111
+ helper.public_send(method_name, file)
112
+ rescue Sprockets::Rails::Helper::AssetNotFound
113
+ helper.public_send(method_name, file, skip_pipeline: true)
114
+ end
115
+
116
+ def resolve_shortcode_theme_asset(file)
117
+ # Tolerate both `themes/<slug>/assets/...` and `/themes/<slug>/assets/...` — the latter is
118
+ # what Rails' asset helpers typically emit.
119
+ match = file.to_s.match(%r{\A/?themes/[^/]+/assets/(?<asset>.+)\z})
120
+ return file unless match
121
+
122
+ can_resolve_theme_assets = respond_to?(:current_theme) &&
123
+ respond_to?(:theme_asset_path) &&
124
+ respond_to?(:theme_asset_file_path)
125
+ return file unless can_resolve_theme_assets
126
+ return file if current_theme.blank?
127
+
128
+ asset = match[:asset]
129
+ remapped_file = theme_asset_path(asset)
130
+ return file unless remapped_file.present? && remapped_file != file
131
+ return file unless File.exist?(theme_asset_file_path(asset))
132
+
133
+ remapped_file
134
+ end
135
+
136
+ def cama_shortcode_data(attrs, args)
137
+ res = args[:shortcode]
138
+ object = (attrs['object'].presence || 'post').downcase
139
+ attr = attrs['attr'] || 'title'
140
+ model = if attrs['id'].present? || attrs['key'].present?
141
+ cama_shortcode_model_parser(object, attrs)
142
+ else
143
+ args[:owner]
144
+ end
145
+ return res if model.blank?
146
+
147
+ if attrs['field'].present?
148
+ field = model.get_field_object(attrs['field'])
149
+ if attrs['render_field'].present?
150
+ return render_to_string(template: "custom_fields/#{field.options['field_key']}", layout: false,
151
+ locals: { object: model, field: field, field_key: attrs['field'], attibutes: attrs })
152
+ end
153
+
154
+ res = if attrs['index']
155
+ begin
156
+ model.the_fields(attrs['field'])[attrs['index'].to_i - 1]
157
+ rescue StandardError
158
+ ''
159
+ end
160
+ else
161
+ model.the_field(attrs['field'])
162
+ end
163
+ return res
164
+ else
165
+ case attr
166
+ when 'title'
167
+ res = model.the_title
168
+ when 'created_at'
169
+ res = model.the_created_at
170
+ when 'updated_at'
171
+ res = model.the_updated_at
172
+ when 'excerpt'
173
+ res = begin
174
+ model.the_excerpt
175
+ rescue StandardError
176
+ ''
177
+ end
178
+ when 'url'
179
+ res = begin
180
+ model.the_url
181
+ rescue StandardError
182
+ ''
183
+ end
184
+ when 'link'
185
+ res = begin
186
+ model.the_link
187
+ rescue StandardError
188
+ ''
189
+ end
190
+ when 'thumb'
191
+ res = case object
192
+ when 'site'
193
+ model.the_logo
194
+ when 'user'
195
+ model.the_avatar
196
+ else
197
+ model.the_thumb_url
198
+ end
199
+ else
200
+ case object
201
+ when 'post'
202
+ case attr
203
+ when 'content'
204
+ res = model.try(:the_content)
205
+ when 'author_name'
206
+ res = model.try(:the_author).try(:the_name)
207
+ when 'author_url'
208
+ res = model.try(:the_author).try(:the_name)
209
+ end
210
+ end
211
+ end
212
+ end
213
+ res
214
+ end
215
+
216
+ def cama_shortcode_model_parser(object, attrs)
217
+ model = nil
218
+ case object.downcase
219
+ when 'post'
220
+ model = current_site.the_post(attrs['id'].to_i) if attrs['id'].present?
221
+ model = current_site.the_post(attrs['key'].to_s) if attrs['key'].present?
222
+ when 'posttype'
223
+ model = current_site.the_post_type(attrs['id'].to_i) if attrs['id'].present?
224
+ model = current_site.the_post_type(attrs['key'].to_s) if attrs['key'].present?
225
+ when 'category'
226
+ model = current_site.the_category(attrs['id'].to_i) if attrs['id'].present?
227
+ model = current_site.the_category(attrs['key'].to_s) if attrs['key'].present?
228
+ when 'posttag'
229
+ model = current_site.the_tag(attrs['id'].to_i) if attrs['id'].present?
230
+ model = current_site.the_tag(attrs['key'].to_s) if attrs['key'].present?
231
+ when 'site'
232
+ model = current_site
233
+ when 'theme'
234
+ model = current_theme
235
+ when 'navmenu'
236
+ model = current_site.nav_menu_items.find(attrs['id']) if attrs['id'].present?
237
+ model = current_site.nav_menu_items.find_by_slug(attrs['key']) if attrs['key'].present? # rubocop:disable Rails/DynamicFindBy
238
+ when 'user'
239
+ model = current_site.the_user(attrs['id'].to_i) if attrs['id'].present?
240
+ model = current_site.the_user(attrs['key'].to_s) if attrs['key'].present?
241
+ end
242
+ model
243
+ end
244
+ end
245
+ end
246
+ # rubocop:enable Layout/LineLength
@@ -0,0 +1,13 @@
1
+ module CamaleonCms
2
+ module RuntimeStateConcern
3
+ extend ActiveSupport::Concern
4
+
5
+ include CamaleonCms::RuntimeShortcodeThemeConcern
6
+ include CamaleonCms::RuntimeHtmlContentConcern
7
+ include CamaleonCms::RuntimeAdminMenuConcern
8
+ include CamaleonCms::RuntimeCaptchaImageConcern
9
+ include CamaleonCms::RuntimeUploaderConcern
10
+
11
+ delegate :tag, :content_tag, :safe_join, :image_tag, :link_to, :sanitize, to: :helpers
12
+ end
13
+ end
@@ -0,0 +1,54 @@
1
+ module CamaleonCms
2
+ # Uploader entry point for controllers (mixed into CamaleonCms::CamaleonController and
3
+ # re-exported through RuntimeStateConcern). The implementation lives in the shared
4
+ # modules below, which CamaleonCms::UploaderHelper includes as well, so the two entry
5
+ # points cannot drift. Only cama_upload_url_error is controller-specific.
6
+ module RuntimeUploaderConcern
7
+ extend ActiveSupport::Concern
8
+
9
+ include UploaderContentSecurity
10
+ include UploaderPathSecurity
11
+ include UploaderPipeline
12
+ include UploaderImageProcessing
13
+ include UploaderSupport
14
+
15
+ private
16
+
17
+ # Route upload messages through `ct` when the host provides it — the media controllers do,
18
+ # since #1223 restored CamaleonHelper (and its `ct`) to the controller chain — so the
19
+ # on_translation hook that lets plugins override upload error text runs on the controller
20
+ # path too. Non-controller includers (ActiveJobs, standalone objects) have no `ct` and keep
21
+ # the pipeline's I18n default via super. The default (no hook) text is identical either way:
22
+ # `ct`'s default arm is I18n.translate("camaleon_cms.common.#{key}", **args).
23
+ def cama_uploader_ct(key, args = {})
24
+ return ct(key, args) if respond_to?(:ct, true)
25
+
26
+ super
27
+ end
28
+
29
+ # Validates a user-supplied upload URL and returns nil when it is acceptable,
30
+ # or an error String to render. Only http(s) URLs are validated here; other
31
+ # inputs (local paths, data: URIs) are handled by the sink guards in
32
+ # cama_tmp_upload/upload_file.
33
+ #
34
+ # Same-site URLs are mapped to a local file and read directly (no network
35
+ # request), so they only need the static path-traversal check — not DNS
36
+ # resolution, SSRF/private-IP blocking, or HTML-tag sanitizing. Applying those
37
+ # to a same-site URL wrongly rejects legitimate values: hosts that resolve to a
38
+ # loopback/private IP (localhost, Docker, intranet), non-resolvable dev hosts,
39
+ # and multi-parameter query strings (the "&" is escaped by sanitize). Remote
40
+ # URLs, which are actually fetched, still get the full SSRF validation.
41
+ def cama_upload_url_error(url)
42
+ return unless url.to_s.start_with?('http://', 'https://')
43
+
44
+ result = if same_site_url?(url, current_site)
45
+ UserUrlValidator.validate(url, reject_path_traversal: true, resolve: false,
46
+ enforce_sanitizing: false, enforce_user: false,
47
+ allow_localhost: true, allow_local_network: true)
48
+ else
49
+ UserUrlValidator.validate(url, reject_path_traversal: true)
50
+ end
51
+ result.join(', ') if result.is_a?(Array)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ module CamaleonCms
2
+ # Wires the captcha helper into the runtime controller stack. Tag building,
3
+ # verification and the under-attack session counters live only in
4
+ # CamaleonCms::CaptchaHelper (shared with views); challenge/image generation
5
+ # lives in CamaleonCms::CaptchaImageGeneration, which both the helper and the
6
+ # controller-stack RuntimeCaptchaImageConcern include (parity-guarded), so the
7
+ # controller and view contexts cannot drift apart.
8
+ #
9
+ # CaptchaHelper is ivar-free (it only touches session/params/current_site and
10
+ # ActionController::Base.helpers), so it is safe to mix into the controller.
11
+ # Including it here restores `cama_captcha_tag`/`cama_captcha_build` on the
12
+ # controller runtime stack so shortcodes/forms rendered in controller context
13
+ # (e.g. the contact-form plugin) can call them.
14
+ module SessionCaptchaRuntimeConcern
15
+ extend ActiveSupport::Concern
16
+
17
+ include CamaleonCms::CaptchaHelper
18
+ end
19
+ end
@@ -0,0 +1,40 @@
1
+ module CamaleonCms
2
+ # Wires the session/auth and email helpers into the runtime controller stack.
3
+ # Behaviour lives in the helper modules (single source of truth) so view and
4
+ # controller contexts cannot drift apart.
5
+ module SessionRuntimeConcern
6
+ extend ActiveSupport::Concern
7
+
8
+ include CamaleonCms::SessionHelper
9
+ include CamaleonCms::EmailHelper
10
+ include CamaleonCms::SessionCaptchaRuntimeConcern
11
+
12
+ # When the host application does not define `current_user`, fall back to
13
+ # Camaleon's own user lookup so `helper_method :current_user` keeps working.
14
+ included do
15
+ define_method(:current_user) { cama_current_user } unless ApplicationController.method_defined?(:current_user)
16
+ end
17
+
18
+ # Back-compat: expose the affected user record as the `@user` controller ivar
19
+ # for legacy templates/plugins. The helper methods themselves stay ivar-free;
20
+ # these controller-concern overrides own the instance-variable bridging so the
21
+ # shared view helpers never pollute the view context.
22
+ def login_user_with_password(username, password)
23
+ result = super
24
+ @user = current_site.users.find_by(username: username)
25
+ result
26
+ end
27
+
28
+ def cama_register_user(user_data, meta)
29
+ result = super
30
+ @user = result[:user] if result.is_a?(Hash) && result.key?(:user)
31
+ result
32
+ end
33
+
34
+ # Redirect to login when the session has expired. Lives here because it is a
35
+ # controller-only redirect concern (not a view helper).
36
+ def auth_session_error
37
+ redirect_to cama_root_path
38
+ end
39
+ end
40
+ end
@@ -15,7 +15,7 @@ module CamaleonCms
15
15
  # return the keywords for this model
16
16
  def the_keywords
17
17
  k = object.get_option('keywords', '')
18
- k = h.current_site.the_keywords if object.class.name != 'CamaleonCms::Site' && !k.present?
18
+ k = h.current_site.the_keywords if object.class.name != 'CamaleonCms::Site' && k.blank?
19
19
  k.to_s.translate(get_locale)
20
20
  end
21
21
 
@@ -50,22 +50,25 @@ module CamaleonCms
50
50
  @_deco_locale = locale.to_sym
51
51
  end
52
52
 
53
- # get the locale for current decorator
53
+ # Admin requests can render frontend URLs from the dashboard, so prefer the
54
+ # cached frontend locale before falling back to the current I18n locale.
54
55
  def get_locale(locale = nil)
55
- locale || @_deco_locale || begin
56
+ frontend_locale = begin
56
57
  h.cama_get_i18n_frontend
57
- rescue StandardError
58
+ rescue NoMethodError
58
59
  nil
59
- end || I18n.locale
60
+ end
61
+ locale || @_deco_locale || frontend_locale || I18n.locale
60
62
  end
61
63
 
62
64
  # return the current locale prefixed to add in frontend routes
63
65
  def _calc_locale(_l)
64
- _l = (_l || @_deco_locale || begin
66
+ frontend_locale = begin
65
67
  h.cama_get_i18n_frontend
66
- rescue StandardError
68
+ rescue NoMethodError
67
69
  nil
68
- end || I18n.locale).to_s
70
+ end
71
+ _l = (_l || @_deco_locale || frontend_locale || I18n.locale).to_s
69
72
  "_#{_l}"
70
73
  end
71
74
  end
@@ -19,7 +19,8 @@ module CamaleonCms
19
19
  h.edit_cama_admin_post_type_category_url(object.post_type.id, object)
20
20
  end
21
21
 
22
- # return all children categories for the current category (active_record) filtered by permissions + hidden posts + roles + etc...
22
+ # return all children categories for the current
23
+ # category (active_record) filtered by permissions + hidden posts + roles + etc...
23
24
  # in return object, you can add custom where's or pagination like here:
24
25
  # https://edgeguides.rubyonrails.org/active_record_querying.html
25
26
  def the_categories
@@ -30,7 +31,7 @@ module CamaleonCms
30
31
  def the_category(slug_or_id)
31
32
  return object.categories.where(id: slug_or_id).first if slug_or_id.is_a?(Integer)
32
33
 
33
- object.categories.find_by_slug(slug_or_id) if slug_or_id.is_a?(String)
34
+ object.categories.find_by_slug(slug_or_id) if slug_or_id.is_a?(String) # rubocop:disable Rails/DynamicFindBy
34
35
  end
35
36
 
36
37
  # ---------------------
@@ -7,9 +7,9 @@ module CamaleonCms
7
7
  h.l(object.created_at, format: format.to_sym)
8
8
  end
9
9
 
10
- # return owner of this comment
10
+ # return owner of this comment, or nil for rows whose user is missing
11
11
  def the_user
12
- object.user.decorate
12
+ object.user&.decorate
13
13
  end
14
14
  alias the_author the_user
15
15
 
@@ -26,15 +26,23 @@ module CamaleonCms
26
26
  end
27
27
 
28
28
  def the_author_name
29
- object.author.presence || object.user.full_name
29
+ # `fullname` is the model method; `full_name` never existed, so this fallback
30
+ # crashed for any comment stored without an author string.
31
+ object.author.presence || object.user&.fullname
30
32
  end
31
33
 
32
34
  def the_author_email
33
- object.author_email.presence || object.user.email
35
+ object.author_email.presence || object.user&.email
34
36
  end
35
37
 
38
+ # A missing user reads the same as the anonymous one: there is no profile to link to.
39
+ # Its three siblings above were made nil-safe for orphaned rows; this one was missed, so it
40
+ # still raised NoMethodError on exactly the rows they were fixed to tolerate.
36
41
  def the_author_url
37
- object.author_url.presence || (object.user.username == 'anonymous' ? '' : object.user.decorate.the_url)
42
+ return object.author_url if object.author_url.present?
43
+ return '' if object.user.blank? || object.user.username == 'anonymous'
44
+
45
+ object.user.decorate.the_url
38
46
  end
39
47
  end
40
48
  end