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
@@ -5,21 +5,24 @@ module CamaleonCms
5
5
  add_flash_types :notice
6
6
  add_flash_types :info
7
7
 
8
+ # Controller-context helper surface (ct, cama_t, cama_cache_fetch, ...): plugin hook
9
+ # functions run on the controller and actions like FrontendController#search call these.
8
10
  include CamaleonCms::CamaleonHelper
9
- include CamaleonCms::SessionHelper
10
- include CamaleonCms::SiteHelper
11
- include CamaleonCms::HtmlHelper
12
- include CamaleonCms::UserRolesHelper
13
- include CamaleonCms::ShortCodeHelper
14
- include CamaleonCms::PluginsHelper
15
- include CamaleonCms::ThemeHelper
16
- include CamaleonCms::HooksHelper
17
- include CamaleonCms::ContentHelper
18
- include CamaleonCms::CaptchaHelper
19
- include CamaleonCms::UploaderHelper
20
- include CamaleonCms::EmailHelper
11
+ include CamaleonCms::SessionRuntimeConcern
12
+ include CamaleonCms::RequestContextConcern
13
+ include CamaleonCms::HookLifecycleConcern
14
+ include CamaleonCms::RuntimeShortcodeThemeConcern
15
+ include CamaleonCms::RuntimeHtmlContentConcern
16
+ include CamaleonCms::RuntimeAdminMenuConcern
17
+ include CamaleonCms::RuntimeCaptchaImageConcern
18
+ include CamaleonCms::RuntimeUploaderConcern
21
19
  include Mobu::DetectMobile
20
+ delegate :tag, :content_tag, :safe_join, :image_tag, :link_to, :sanitize, to: :helpers
22
21
 
22
+ # Preserve backwards-compatible plugin helper inclusion: any helper module a
23
+ # plugin declares in its config.json is mixed in here so plugins can call
24
+ # those methods from controllers (not only from views). Documented in
25
+ # `docs/ai/plans/helper-concern-architecture-refactor.md`.
23
26
  PluginRoutes.all_helpers.each { |h| include h.constantize }
24
27
 
25
28
  before_action :cama_site_check_existence, except: [:captcha]
@@ -35,13 +38,24 @@ module CamaleonCms
35
38
 
36
39
  # show page error
37
40
  def render_error(status = 404, exception = nil, message = '')
38
- Rails.logger.debug "Camaleon CMS - 404 url: #{begin
39
- request.original_url
40
- rescue StandardError
41
- nil
42
- end} ==> message: #{exception.message if exception.present?} ==> #{params[:error_msg]} ==> #{caller.inspect}"
43
- @message = "#{message} #{params[:error_msg] || (exception.present? ? "#{exception.message}<br><br>#{caller.inspect}" : '')}"
44
- @message = '' if Rails.env == 'production'
41
+ error_msg = params[:error_msg]
42
+ Rails.logger.debug do
43
+ original_url = begin
44
+ request.original_url
45
+ rescue StandardError
46
+ nil
47
+ end
48
+ 'Camaleon CMS - 404 url: ' \
49
+ "#{original_url} ==> message: #{exception&.message} ==> #{error_msg} ==> #{caller.inspect}"
50
+ end
51
+
52
+ @message = if Rails.env.production?
53
+ ''
54
+ else
55
+ "#{message} " \
56
+ "#{error_msg || (exception.present? ? "#{exception.message}<br><br>#{caller.inspect}" : '')}"
57
+ end
58
+
45
59
  respond_to do |format|
46
60
  format.html { render "camaleon_cms/#{status}", status: status }
47
61
  format.any { head status }
@@ -69,18 +83,9 @@ module CamaleonCms
69
83
  # initializing before and after contents
70
84
  cama_content_init
71
85
 
72
- @_hooks_skip = []
73
- # trigger all hooks before_load_app
74
- hooks_run('app_before_load')
75
-
76
- request.env.except!('HTTP_X_FORWARDED_HOST') if request.env['HTTP_X_FORWARDED_HOST'] # just drop the variable
77
-
78
- views_dir = 'app/apps/'
79
- prepend_view_path(File.join($camaleon_engine_dir, views_dir).to_s)
80
- prepend_view_path(Rails.root.join(views_dir).to_s)
81
-
82
- CamaleonCms::PostDefault.current_user = cama_current_user
83
- CamaleonCms::PostDefault.current_site = current_site
86
+ initialize_hook_skip_list
87
+ run_app_before_load_hooks
88
+ configure_runtime_request_context
84
89
  end
85
90
 
86
91
  # initialize ability for current user
@@ -89,67 +94,7 @@ module CamaleonCms
89
94
  end
90
95
 
91
96
  def cama_after_actions
92
- # trigger all actions app after load
93
- hooks_run('app_after_load')
94
- end
95
-
96
- # redirect to sessions login form when the session was expired.
97
- def auth_session_error
98
- redirect_to cama_root_path
99
- end
100
-
101
- # check if current site exist, if not, this will be redirected to main domain
102
- # Also, check current site status
103
- def cama_site_check_existence
104
- if !current_site.present?
105
- if Cama::Site.main_site.present?
106
- url = Cama::Site.main_site.decorate.the_url
107
- # TODO: Remove this condition when Rails 6.x won't be supported
108
- if Rails.gem_version >= Gem::Version.new('7.0.0')
109
- redirect_to url, allow_other_host: true
110
- else
111
- redirect_to url
112
- end
113
- else
114
- redirect_to cama_admin_installers_path
115
- end
116
- elsif (cama_current_user.present? && !cama_current_user.admin?) || !cama_current_user.present?
117
- # inactive page control
118
- if current_site.is_inactive?
119
- if request.original_url.to_s.match(%r{\A#{current_site.the_url}admin(/|\z)})
120
- if cama_current_user.present?
121
- cama_logout_user
122
- flash[:error] = 'Site is Inactive'
123
- end
124
- else
125
- p = current_site.posts.find_by_id(current_site.get_option('page_inactive')).try(:decorate)
126
- if p
127
- redirect_to(p.the_url) unless params == { 'controller' => 'camaleon_cms/frontend', 'action' => 'post',
128
- 'slug' => p.the_slug }
129
- else
130
- render html: 'This site was inactivated. Please contact to administrator.'
131
- end
132
- end
133
- end
134
-
135
- # maintenance page and IP's control
136
- if current_site.is_maintenance? && !current_site.get_option('maintenance_ips',
137
- '').split(',').include?(request.remote_ip)
138
- p = current_site.posts.find_by_id(current_site.get_option('page_maintenance')).try(:decorate)
139
- if p
140
- redirect_to(p.the_url) if params != { 'controller' => 'camaleon_cms/frontend', 'action' => 'post',
141
- 'slug' => p.the_slug }
142
- else
143
- render html: 'This site is in maintenance mode. Please contact to administrator.'
144
- end
145
- end
146
- end
147
- end
148
-
149
- unless ApplicationController.method_defined?(:current_user)
150
- def current_user
151
- cama_current_user
152
- end
97
+ run_app_after_load_hooks
153
98
  end
154
99
  end
155
100
  end
@@ -1,22 +1,22 @@
1
1
  module CamaleonCms
2
2
  class FrontendController < CamaleonCms::CamaleonController
3
3
  before_action :init_frontent
4
+ include CamaleonCms::FrontendVisitedStateConcern
4
5
  include CamaleonCms::FrontendConcern
5
6
  include CamaleonCms::Frontend::ApplicationHelper
6
7
  layout proc { |controller|
7
- args = {
8
- layout: (params[:cama_ajax_request].present? ? 'cama_ajax' : PluginRoutes.static_system_info['default_layout']), controller: controller
9
- }
10
- hooks_run('front_default_layout', args)
11
- args[:layout]
12
- }
8
+ args = {
9
+ layout: (params[:cama_ajax_request].present? ? 'cama_ajax' : PluginRoutes.static_system_info['default_layout']),
10
+ controller: controller
11
+ }
12
+ hooks_run('front_default_layout', args)
13
+ args[:layout]
14
+ }
13
15
  before_action :before_hooks
14
16
  after_action :after_hooks
15
- # rescue_from ActiveRecord::RecordNotFound, with: :page_not_found
16
-
17
17
  # home page for frontend
18
18
  def index
19
- @cama_visited_home = true
19
+ mark_frontend_home_visited
20
20
  if @_site_options[:home_page].present?
21
21
  render_post(@_site_options[:home_page].to_i)
22
22
  else
@@ -29,28 +29,38 @@ module CamaleonCms
29
29
  # render category list
30
30
  def category
31
31
  begin
32
- @category ||= current_site.the_full_categories.find_by_slug(params[:category_slug]).decorate if params[:category_slug].present?
32
+ if params[:category_slug].present?
33
+ @category ||= current_site.the_full_categories.find_by_slug(params[:category_slug]).decorate # rubocop:disable Rails/DynamicFindBy
34
+ end
33
35
  @category ||= current_site.the_full_categories.find(params[:category_id]).decorate
34
36
  @post_type = @category.the_post_type
35
37
  rescue StandardError
36
38
  return page_not_found
37
39
  end
38
- @cama_visited_category = @category
40
+ mark_frontend_category_visited(@category)
39
41
  @children = @category.children.no_empty.decorate
40
42
  @posts = @category.the_posts.paginate(page: params[:page],
41
- per_page: current_site.front_per_page).eager_load(:metas)
42
- r_file = lookup_context.template_exists?("category_#{@category.the_slug}") ? "category_#{@category.the_slug}" : nil # specific template category with specific slug within a posttype
43
- unless r_file.present?
44
- r_file = lookup_context.template_exists?("post_types/#{@post_type.the_slug}/category") ? "post_types/#{@post_type.the_slug}/category" : nil
43
+ per_page: current_site.front_per_page)
44
+ category_slug = @category.the_slug
45
+
46
+ # specific template category with specific slug within a post-type
47
+ r_file = lookup_context.template_exists?("category_#{category_slug}") ? "category_#{category_slug}" : nil
48
+ post_type_slug = "post_types/#{@post_type.the_slug}"
49
+ if r_file.blank?
50
+ r_file = lookup_context.template_exists?("#{post_type_slug}/category") ? "#{post_type_slug}/category" : nil
45
51
  end
46
- unless r_file.present?
47
- r_file = lookup_context.template_exists?("categories/#{@category.the_slug}") ? "categories/#{@category.the_slug}" : 'category'
52
+ if r_file.blank?
53
+ r_file = if lookup_context.template_exists?("categories/#{category_slug}")
54
+ "categories/#{category_slug}"
55
+ else
56
+ 'category'
57
+ end
48
58
  end
49
59
 
50
- layout_ = if lookup_context.template_exists?("layouts/post_types/#{@post_type.the_slug}/category")
51
- "post_types/#{@post_type.the_slug}/category"
52
- elsif lookup_context.template_exists?("layouts/categories/#{@category.the_slug}")
53
- "categories/#{@category.the_slug}"
60
+ layout_ = if lookup_context.template_exists?("layouts/#{post_type_slug}/category")
61
+ "#{post_type_slug}/category"
62
+ elsif lookup_context.template_exists?("layouts/categories/#{category_slug}")
63
+ "categories/#{category_slug}"
54
64
  end
55
65
  r = { category: @category, layout: layout_, render: r_file }
56
66
  hooks_run('on_render_category', r)
@@ -60,18 +70,20 @@ module CamaleonCms
60
70
  # render contents from post type
61
71
  def post_type
62
72
  begin
63
- @post_type = current_site.post_types.find_by_slug(params[:post_type_slug]).decorate
73
+ @post_type = current_site.post_types.find_by_slug(params[:post_type_slug]).decorate # rubocop:disable Rails/DynamicFindBy
64
74
  rescue StandardError
65
75
  return page_not_found
66
76
  end
67
77
  @object = @post_type
68
- @cama_visited_post_type = @post_type
78
+ CurrentRequest.frontend_object = @object
79
+ mark_frontend_post_type_visited(@post_type)
69
80
  @posts = @post_type.the_posts.paginate(page: params[:page],
70
- per_page: current_site.front_per_page).eager_load(:metas)
81
+ per_page: current_site.front_per_page)
71
82
  @categories = @post_type.categories.no_empty.eager_load(:metas).decorate
72
83
  @post_tags = @post_type.post_tags.eager_load(:metas)
73
- r_file = lookup_context.template_exists?("post_types/#{@post_type.the_slug}") ? "post_types/#{@post_type.the_slug}" : 'post_type'
74
- layout_ = lookup_context.template_exists?("layouts/post_types/#{@post_type.the_slug}") ? "post_types/#{@post_type.the_slug}" : nil
84
+ post_type_slug = "post_types/#{@post_type.the_slug}"
85
+ r_file = lookup_context.template_exists?(post_type_slug) ? post_type_slug : 'post_type'
86
+ layout_ = lookup_context.template_exists?("layouts/#{post_type_slug}") ? post_type_slug : nil
75
87
  r = { post_type: @post_type, layout: layout_, render: r_file }
76
88
  hooks_run('on_render_post_type', r)
77
89
  render r[:render], (!r[:layout].nil? ? { layout: r[:layout] } : {})
@@ -81,7 +93,7 @@ module CamaleonCms
81
93
  def post_tag
82
94
  begin
83
95
  @post_tag = if params[:post_tag_slug].present?
84
- current_site.post_tags.find_by_slug(params[:post_tag_slug]).decorate
96
+ current_site.post_tags.find_by_slug(params[:post_tag_slug]).decorate # rubocop:disable Rails/DynamicFindBy
85
97
  else
86
98
  current_site.post_tags.find(params[:post_tag_id]).decorate
87
99
  end
@@ -90,10 +102,12 @@ module CamaleonCms
90
102
  return page_not_found
91
103
  end
92
104
  @object = @post_tag
93
- @cama_visited_tag = @post_tag
105
+ CurrentRequest.frontend_object = @object
106
+ mark_frontend_tag_visited(@post_tag)
94
107
  @posts = @post_tag.the_posts.paginate(page: params[:page],
95
- per_page: current_site.front_per_page).eager_load(:metas)
96
- r_file = lookup_context.template_exists?("post_types/#{@post_type.the_slug}/post_tag") ? "post_types/#{@post_type.the_slug}/post_tag" : 'post_tag'
108
+ per_page: current_site.front_per_page)
109
+ slug_post_tag = "post_types/#{@post_type.the_slug}/post_tag"
110
+ r_file = lookup_context.template_exists?(slug_post_tag) ? slug_post_tag : 'post_tag'
97
111
  layout_ = lookup_context.template_exists?('layouts/post_tag') ? 'post_tag' : nil
98
112
  r = { post_tag: @post_tag, layout: layout_, render: r_file }
99
113
  hooks_run('on_render_post_tag', r)
@@ -103,19 +117,17 @@ module CamaleonCms
103
117
  # search contents
104
118
  def search
105
119
  breadcrumb_add(ct('search'))
106
- items = params[:post_type_slugs].present? ? current_site.the_posts(params[:post_type_slugs].split(',')) : current_site.the_posts
107
- @cama_visited_search = true
120
+ post_type_slugs = params[:post_type_slugs]
121
+ items = post_type_slugs.present? ? current_site.the_posts(post_type_slugs.split(',')) : current_site.the_posts
122
+ mark_frontend_search_visited
108
123
  @param_search = params[:q]
109
124
  layout_ = lookup_context.template_exists?('layouts/search') ? 'search' : nil
110
125
  r = { layout: layout_, render: 'search', posts: nil }
111
126
  hooks_run('on_render_search', r)
112
- params[:q] = (params[:q] || '').downcase
113
- @posts = if !r[:posts].nil?
114
- r[:posts]
115
- else
116
- items.where('LOWER(title) LIKE ? OR LOWER(content_filtered) LIKE ?',
117
- "%#{params[:q]}%", "%#{params[:q]}%")
118
- end
127
+ q_params = params[:q].to_s.downcase
128
+ params[:q] = q_params
129
+ @posts = r[:posts] ||
130
+ items.where('LOWER(title) LIKE ? OR LOWER(content_filtered) LIKE ?', "%#{q_params}%", "%#{q_params}%")
119
131
  @posts_size = @posts.size
120
132
  @posts = @posts.paginate(page: params[:page], per_page: current_site.front_per_page)
121
133
  render r[:render], (!r[:layout].nil? ? { layout: r[:layout] } : {})
@@ -124,7 +136,7 @@ module CamaleonCms
124
136
  # ajax requests
125
137
  def ajax
126
138
  r = { render_file: nil, render_text: '', layout: nil }
127
- @cama_visited_ajax = true
139
+ mark_frontend_ajax_visited
128
140
  hooks_run('on_ajax', r)
129
141
  if r[:render_file]
130
142
  render r[:render_file], (!r[:layout].nil? ? { layout: r[:layout] } : {})
@@ -150,7 +162,8 @@ module CamaleonCms
150
162
  return page_not_found
151
163
  end
152
164
  @object = @user
153
- @cama_visited_profile = true
165
+ CurrentRequest.frontend_object = @object
166
+ mark_frontend_profile_visited(@user)
154
167
  layout_ = lookup_context.template_exists?('layouts/profile') ? 'profile' : nil
155
168
  r = { user: @user, layout: layout_, render: 'profile' }
156
169
  hooks_run('on_render_profile', r)
@@ -168,6 +181,7 @@ module CamaleonCms
168
181
  def draft_render
169
182
  post_draft = current_site.posts.drafts.find(params[:draft_id])
170
183
  @object = post_draft
184
+ CurrentRequest.frontend_object = @object
171
185
 
172
186
  # let a hook override the ability for certain roles see drafts
173
187
  args = { permitted: false }
@@ -184,25 +198,30 @@ module CamaleonCms
184
198
  # post_or_slug_or_id: slug_post | id post | post object
185
199
  # from_url: true/false => true (true, permit eval hooks "on_render_post")
186
200
  def render_post(post_or_slug_or_id, from_url = false, status = nil, force_visit = false)
201
+ # eager: false -- rendering one post must not pay the listing preloads (with_eager)
202
+ visible_posts = verify_front_visibility(current_site.posts, eager: false)
187
203
  @post = case post_or_slug_or_id
188
204
  when String # slug
189
- current_site.the_posts.find_by_slug(post_or_slug_or_id)
205
+ # find_by_slug is multi-language aware (matches localized slugs like
206
+ # "<!--:en-->sample-post<!--:-->..."), unlike find_by(slug:)
207
+ visible_posts.find_by_slug(post_or_slug_or_id) # rubocop:disable Rails/DynamicFindBy
190
208
  when Integer # id
191
- current_site.the_posts.where(id: post_or_slug_or_id).first
209
+ visible_posts.where(id: post_or_slug_or_id).first
192
210
  else # model
193
211
  post_or_slug_or_id
194
212
  end
195
213
 
196
214
  @post = @post.try(:decorate)
197
- if !@post.present? || !(force_visit || @post.can_visit?)
198
- if params[:format] == 'html' || !params[:format].present?
215
+ if @post.blank? || !(force_visit || @post.can_visit?)
216
+ if params[:format] == 'html' || params[:format].blank?
199
217
  page_not_found
200
218
  else
201
- head 404
219
+ head :not_found
202
220
  end
203
221
  else
204
222
  @object = @post
205
- @cama_visited_post = @post
223
+ CurrentRequest.frontend_object = @object
224
+ mark_frontend_post_visited(@post)
206
225
  @post_type = @post.the_post_type
207
226
  @comments = @post.the_comments
208
227
  @categories = @post.the_categories
@@ -213,10 +232,11 @@ module CamaleonCms
213
232
  rescue StandardError
214
233
  nil
215
234
  end
235
+ post_template = @post.get_template(@post_type)
216
236
  r_file = if lookup_context.template_exists?("page_#{@post.id}")
217
237
  "page_#{@post.id}"
218
- elsif @post.get_template(@post_type).present? && lookup_context.template_exists?(@post.get_template(@post_type))
219
- @post.get_template(@post_type)
238
+ elsif post_template.present? && lookup_context.template_exists?(post_template)
239
+ post_template
220
240
  elsif home_page.present? && @post.id.to_s == home_page
221
241
  'index'
222
242
  elsif lookup_context.template_exists?("post_types/#{@post_type.the_slug}/single")
@@ -261,17 +281,54 @@ module CamaleonCms
261
281
  # if url hasn't a locale, then it will use default locale set on application.rb
262
282
  def init_frontent
263
283
  # preview theme initializing
264
- @_current_theme = current_site.themes.where(slug: params[:ccc_theme_preview]).first_or_create!.decorate if cama_sign_in? && params[:ccc_theme_preview].present? && can?(
265
- :manage, :themes
266
- )
284
+ previewing_theme = cama_sign_in? && params[:ccc_theme_preview].present? && can?(:manage, :themes)
285
+ if previewing_theme
286
+ preview_theme = current_site.themes.where(slug: params[:ccc_theme_preview]).first_or_create!.decorate
287
+ @_current_theme = preview_theme
288
+ CurrentRequest.frontend_current_theme = preview_theme
289
+ ensure_preview_site_defaults
290
+ end
267
291
 
268
292
  @_site_options = current_site.options
269
- session[:cama_current_language] = params[:cama_set_language].to_sym if params[:cama_set_language].present?
293
+ # Only a scalar language param participates in locale resolution — a non-scalar
294
+ # (?locale[]= / ?cama_set_language[]=) would crash to_sym and 500 the request
295
+ set_language = params[:cama_set_language]
296
+ session[:cama_current_language] = set_language.to_sym if set_language.is_a?(String) && set_language.present?
270
297
  session[:cama_current_language] = nil if current_site.get_languages.exclude?(session[:cama_current_language])
271
- I18n.locale = params[:locale] || session[:cama_current_language] || current_site.get_languages.first
272
- return page_not_found unless current_site.get_languages.include?(I18n.locale.to_sym)
298
+ requested_locale = params[:locale] if params[:locale].is_a?(String)
299
+ I18n.locale = requested_locale || session[:cama_current_language] || current_site.get_languages.first
300
+ # The availability gate renders the site's 404 (directly or via a custom error_404
301
+ # post), so the theme lookup prefixes must already be registered when it fires
302
+ configure_frontend_lookup_prefixes
303
+ theme_init
304
+ return if current_site.get_languages.include?(I18n.locale.to_sym)
305
+
306
+ # The unoffered locale must not style the 404 page or leak into its links
307
+ I18n.locale = current_site.get_languages.first
308
+ page_not_found
309
+ end
310
+
311
+ # initialize hooks before to execute action
312
+ def before_hooks
313
+ hooks_run('front_before_load')
314
+ end
315
+
316
+ # initialize hooks after executed action
317
+ def after_hooks
318
+ hooks_run('front_after_load')
319
+ end
320
+
321
+ # define default options for url helpers
322
+ # control for default locale
323
+ def default_url_options(options = {})
324
+ return options if current_site.get_languages.first.to_s == I18n.locale.to_s
273
325
 
274
- # define render paths
326
+ { locale: I18n.locale }.merge!(options)
327
+ rescue StandardError
328
+ options
329
+ end
330
+
331
+ def configure_frontend_lookup_prefixes
275
332
  lookup_context.prefixes.delete('frontend')
276
333
  lookup_context.prefixes.delete('application')
277
334
  lookup_context.prefixes.delete('camaleon_cms/frontend')
@@ -282,36 +339,51 @@ module CamaleonCms
282
339
  t =~ %r{themes/(.*)/views}i || t == 'camaleon_cms/default_theme' || t == "themes/#{current_site.id}/views"
283
340
  end
284
341
 
285
- lookup_context.prefixes.append("themes/#{current_site.id}/views") if Dir.exist?(Rails.root.join('app', 'apps',
286
- 'themes', current_site.id.to_s).to_s)
342
+ # Per-site theme override: views placed under app/apps/themes/<site_id>/views
343
+ # take precedence over the active theme's views (it is appended first).
344
+ if Dir.exist?(Rails.root.join('app', 'apps', 'themes', current_site.id.to_s).to_s)
345
+ lookup_context.prefixes.append("themes/#{current_site.id}/views")
346
+ end
287
347
  lookup_context.prefixes.append("themes/#{current_theme.slug}/views")
288
348
  lookup_context.prefixes.append('camaleon_cms/default_theme')
289
349
 
290
350
  lookup_context.prefixes = lookup_context.prefixes.uniq
291
351
  lookup_context.use_camaleon_partial_prefixes = true
292
- theme_init
293
352
  end
294
353
 
295
- # initialize hooks before to execute action
296
- def before_hooks
297
- hooks_run('front_before_load')
354
+ def ensure_preview_site_defaults
355
+ preview_required_menu_slugs.each do |slug|
356
+ current_site.nav_menus.where(slug: slug).first_or_create! do |menu|
357
+ menu.name = slug.to_s.humanize
358
+ end
359
+ end
298
360
  end
299
361
 
300
- # initialize hooks after executed action
301
- def after_hooks
302
- hooks_run('front_after_load')
362
+ def preview_required_menu_slugs
363
+ slugs = ['main_menu']
364
+ slugs.concat(extract_theme_menu_slugs(current_theme.settings['path']))
365
+ slugs.uniq
303
366
  end
304
367
 
305
- # define default options for url helpers
306
- # control for default locale
307
- def default_url_options(options = {})
308
- if current_site.get_languages.first.to_s == I18n.locale.to_s
309
- options
310
- else
311
- { locale: I18n.locale }.merge options
368
+ def extract_theme_menu_slugs(theme_path)
369
+ return [] if theme_path.blank?
370
+
371
+ views_path = File.join(theme_path, 'views')
372
+ return [] unless Dir.exist?(views_path)
373
+
374
+ Dir.glob(File.join(views_path, '**', '*.erb')).flat_map do |file_path|
375
+ extract_menu_slugs_from_template(File.read(file_path))
376
+ end.uniq
377
+ end
378
+
379
+ def extract_menu_slugs_from_template(content)
380
+ slugs = []
381
+ slugs.concat(content.scan(/nav_menus\.where\(\s*slug:\s*'([^']+)'\s*\)/).flatten)
382
+ slugs.concat(content.scan(/nav_menus\.where\(\s*slug:\s*"([^"]+)"\s*\)/).flatten)
383
+ content.scan(/nav_menus\.where\(\s*slug:\s*\[([^\]]+)\]\s*\)/).flatten.each do |array_content|
384
+ slugs.concat(array_content.scan(/['"]([^'"]+)['"]/).flatten)
312
385
  end
313
- rescue StandardError
314
- options
386
+ slugs
315
387
  end
316
388
  end
317
389
  end
@@ -5,19 +5,34 @@ module CamaleonCms
5
5
 
6
6
  private
7
7
 
8
- # Only permit field_options that match registered custom field slugs
9
- def cama_permitted_field_options(object_class)
10
- return {} unless params[:field_options].present?
8
+ # Only permit field values whose slug is registered under object_class. param_key selects
9
+ # which request param carries the payload, so sibling params (the theme form's
10
+ # theme_fields) get the same allowed-slugs permit as the default field_options.
11
+ def cama_permitted_field_options(object_class, param_key: :field_options)
12
+ field_options = params[param_key]
13
+ # A non-hash payload (a scalar `field_options=foo`, or an array) carries no groups to permit,
14
+ # so treat it as empty instead of calling #keys/#permit on it. Guarding here rather than
15
+ # `params.require` avoids a NoMethodError -> 500 an authenticated caller could trigger with a
16
+ # malformed param (the same crash every set_field_values caller shared).
17
+ return {} unless field_options.is_a?(ActionController::Parameters)
11
18
 
12
19
  allowed_keys = cama_custom_field_allowed_slugs(object_class)
13
20
  return {} if allowed_keys.blank?
14
21
 
15
- field_options = params.require(:field_options)
16
- field_options.permit(field_options.keys.select { |k| k.to_s =~ /\A\d+\z/ }.index_with do
22
+ permitted = field_options.permit(field_options.keys.select { |k| k.to_s =~ /\A\d+\z/ }.index_with do
17
23
  allowed_keys.index_with { [:id, :group_number, { values: {} }] }
18
24
  end).to_h
25
+ # Drop groups left empty after filtering. set_field_values deletes every existing value
26
+ # before writing, so handing it a non-blank-but-empty payload (a group whose submitted
27
+ # slugs were all unregistered) would wipe the object's stored values and write nothing.
28
+ permitted.reject { |_group, fields| fields.blank? }
19
29
  end
20
30
 
31
+ # The allow-list covers fields registered directly under object_class. For a post
32
+ # ('PostType_Post') this intentionally spans only the post type's own groups -- not per-post
33
+ # ('Post') or category-inherited ('Category_Post') groups the edit form also renders. It
34
+ # matches PostsController#save_post_with_fields, so the main post save and drafts confine field
35
+ # writes identically; values for those sibling scopes are deliberately not written this way.
21
36
  def cama_custom_field_allowed_slugs(object_class)
22
37
  CamaleonCms::CustomField.where(
23
38
  parent_id: CamaleonCms::CustomField.where(object_class: object_class).select(:id),