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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fc3c9c70942e53b927d67366df21d19a549e0a349e8e87ca08f12977c3c5b08
4
- data.tar.gz: d0b3948628d2c7c5d5efa55e6bc6c2ef1b90413d2a53764ac85ed281eceab630
3
+ metadata.gz: c6940e13676d23d32f7eddfc3cabe9cc6639914fc79f5d53497040dc2987fbfb
4
+ data.tar.gz: 8079bcd83ef28f77f628ad33fbc0bc89f17e2f6ae130868f3d49f112b4cfd1e3
5
5
  SHA512:
6
- metadata.gz: f6795ac94a6097bafa86c8336f209cb395f2b102db44da96d091ad55b5373bb6a1ed5af876f6facb72ba313d6fa803201d0605e2e55b0f699304dab2df470c6b
7
- data.tar.gz: 6045503558fb4aa769b6d3bbd8dfac05357c9e6427134ed2670b3dc08aae94df7b865d69a80f6bdbe950492a439d79345b1f57b94808fbeb78f6acb5868ec90e
6
+ metadata.gz: 99302745a2adbd3a0bce4c9e85153bd6d216f5fd3d6ed67c038b461e465339de41e386b114d1075dd1471fca07c70c72c8492a5e9fcae28bf49fcc3f91c71389
7
+ data.tar.gz: e5ee5c70326b92c29e9ca02be583930d1e2a1bc9a6308cf0bc90a090a29a2c2e4b9417ce9958065561b8d5844146844544d341464c268f2ff40baec939dc4b6c
data/README.md CHANGED
@@ -81,14 +81,13 @@ https://camaleon.website/store/plugins
81
81
  * Image/iFrame Lazy Loader - https://github.com/brian-kephart/camaleon_lazy_loader
82
82
  * Import / Export - https://github.com/owen2345/camaleon_export_import
83
83
  * Lightbox - https://github.com/owen2345/CamaImageLightbox
84
- * Autocomplete - https://github.com/gaelfokou/cama_autocomplete
85
84
  * Permissions for External Menus - https://github.com/owen2345/cama_external_menu
86
85
  * TinyMCE Template Integration - https://github.com/owen2345/Camaleon-Tinymce-Templates
87
86
  * Download Manager - https://github.com/max2320/camaleon-download
88
87
  * OAuth - https://github.com/owen2345/camaleon_oauth
89
- * Visual Editor - Paid Plugin ($) - https://camaleon.website/store/plugins/camaleon_editor
90
- * Spree Commerce Integration - Paid Plugin ($) - https://camaleon.website/store/plugins/camaleon-spree
91
- * Admin AJAX - Paid Plugin ($) - https://camaleon.website/store/plugins/admin_ajax
88
+ * Visual Editor - Paid Plugin ($) - https://camaleon.website/store/plugins/camaleon_editor (Github: https://github.com/owen2345/camaleon_editor)
89
+ * Spree Commerce Integration - Paid Plugin ($) - https://camaleon.website/store/plugins/camaleon-spree (Github: https://github.com/owen2345/camaleon-spree)
90
+ * Admin AJAX - Paid Plugin ($) - https://camaleon.website/store/plugins/admin_ajax (Github: https://github.com/owen2345/admin-ajax)
92
91
  * **See here for a complete Gemfile**: https://github.com/owen2345/camaleon-cms/blob/master/docs/example_gemfile.rb
93
92
 
94
93
  ## Camaleon CMS has many useful frontend Themes such as:
@@ -190,6 +189,13 @@ RAILS_ENV=test bundle exec rake app:db:test:prepare
190
189
 
191
190
  See [docs/security/permissions.md](docs/security/permissions.md) for details on manager permissions, custom fields, and security considerations.
192
191
 
192
+ ## Hooks
193
+
194
+ Plugins and themes extend Camaleon by handling named hooks. See [docs/hooks.md](docs/hooks.md) for the
195
+ mechanism (registration via `config.json`, the mutable `args` payload) and a code-derived inventory of the
196
+ hooks core fires, including the session/auth hooks (`user_before_login`, `user_before_register`,
197
+ `after_login`, …).
198
+
193
199
  ## Contributing
194
200
  * Fork it.
195
201
  * Create a branch (git checkout -b my_feature_branch)
@@ -197,6 +203,13 @@ See [docs/security/permissions.md](docs/security/permissions.md) for details on
197
203
  * Push to the branch (git push origin my_feature_branch)
198
204
  * Create a pull request from your branch into master (Please be sure to provide enough detail for us to understand what this change is doing)
199
205
 
206
+ ## Releasing
207
+
208
+ Releases are cut by the manually-started **Release** GitHub Actions workflow, which builds the gem,
209
+ publishes it to RubyGems, tags the commit and creates the GitHub release. See
210
+ [docs/releasing.md](docs/releasing.md) for the full procedure, what the workflow checks before it
211
+ publishes, and the fully manual fallback.
212
+
200
213
  ## Version History
201
214
 
202
215
  https://camaleon.website/version-history.html
data/Rakefile CHANGED
@@ -4,26 +4,13 @@ rescue LoadError
4
4
  puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
5
  end
6
6
 
7
- require 'rdoc/task'
8
-
9
- RDoc::Task.new(:rdoc) do |rdoc|
10
- rdoc.rdoc_dir = 'rdoc'
11
- rdoc.title = 'CamaleonCms'
12
- rdoc.options << '--line-numbers'
13
- rdoc.rdoc_files.include('README.rdoc')
14
- rdoc.rdoc_files.include('lib/**/*.rb')
15
- end
16
-
17
7
  APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
18
8
  load 'rails/tasks/engine.rake'
19
9
 
20
- load 'rails/tasks/statistics.rake'
21
-
22
- Bundler::GemHelper.install_tasks
23
-
24
- Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each { |f| load f }
10
+ # Deliberately no Bundler::GemHelper.install_tasks: its `rake release` would tag `v<version>` and
11
+ # push a gem that none of the release checks had seen. Releases go through the Release workflow —
12
+ # see docs/releasing.md, which also covers building and publishing by hand.
25
13
 
26
- require 'rspec/core'
27
14
  require 'rspec/core/rake_task'
28
15
 
29
16
  desc 'Run all specs in spec directory (excluding plugin specs)'
@@ -6,12 +6,15 @@ module Plugins
6
6
  end
7
7
 
8
8
  def save_settings
9
- current_site.set_meta('attack_config', { get: { sec: params[:attack][:get_sec], max: params[:attack][:get_max] },
10
- post: { sec: params[:attack][:post_sec] || 20,
11
- max: params[:attack][:post_max] },
12
- msg: params[:attack][:msg],
13
- ban: params[:attack][:ban],
14
- cleared: Time.now })
9
+ current_site.set_meta(
10
+ 'attack_config', {
11
+ get: { sec: params[:attack][:get_sec], max: params[:attack][:get_max] },
12
+ post: { sec: params[:attack][:post_sec] || 20, max: params[:attack][:post_max] },
13
+ msg: params[:attack][:msg],
14
+ ban: params[:attack][:ban],
15
+ cleared: Time.zone.now.iso8601
16
+ }
17
+ )
15
18
  flash[:notice] = t('plugin.attack.messages.settings_saved').to_s
16
19
  redirect_to action: :settings
17
20
  end
@@ -1,14 +1,14 @@
1
1
  module Plugins
2
2
  module Attack
3
3
  module AttackHelper
4
- # here all actions on plugin destroying
4
+ # Here all actions on the plugin are destroyed
5
5
  # plugin: plugin model
6
6
  def attack_on_destroy(_plugin)
7
7
  current_site.attack.destroy_all
8
8
  end
9
9
 
10
- # here all actions on going to active
11
- # you can run sql commands like this:
10
+ # Here all actions on going to active
11
+ # you can run SQL commands like this:
12
12
  # results = ActiveRecord::Base.connection.execute(query);
13
13
  # plugin: plugin model
14
14
  def attack_on_active(_plugin)
@@ -16,15 +16,13 @@ module Plugins
16
16
  post: { sec: 20, max: 5 },
17
17
  msg: I18n.t('plugin.attack.form.request_limit_exceeded').to_s,
18
18
  ban: 5,
19
- cleared: Time.now })
19
+ cleared: Time.zone.now.iso8601 })
20
20
 
21
- unless ActiveRecord::Base.connection.table_exists? 'plugins_attacks'
22
- ActiveRecord::Base.connection.create_table :plugins_attacks do |t|
23
- t.string :path, index: true
24
- t.string :browser_key, index: true
25
- t.belongs_to :site, index: true
26
- t.datetime 'created_at'
27
- end
21
+ ActiveRecord::Base.connection.create_table :plugins_attacks, if_not_exists: true do |t|
22
+ t.string :path, index: true
23
+ t.string :browser_key, index: true
24
+ t.belongs_to :site, index: true
25
+ t.datetime 'created_at'
28
26
  end
29
27
  CamaleonCms::Site.class_eval do
30
28
  has_many :attack, class_name: 'Plugins::Attack::Models::Attack'
@@ -38,9 +36,9 @@ module Plugins
38
36
  end
39
37
 
40
38
  def attack_app_before_load
41
- cache_ban = Rails.cache.read(cama_get_session_id)
39
+ cache_ban = Rails.cache.read(attack_ban_cache_key)
42
40
  if cache_ban.present? # render banned message if it was banned
43
- render html: cache_ban.html_safe, layout: false
41
+ render html: cache_ban.to_s, layout: false
44
42
  return
45
43
  end
46
44
 
@@ -50,44 +48,56 @@ module Plugins
50
48
 
51
49
  private
52
50
 
51
+ # Throttle and ban on the client IP, not the session id (audit finding H2). A cookieless /
52
+ # cookie-rotating request presented a fresh session id every time, so the per-key counter never
53
+ # accumulated: the throttle never tripped and its per-request DB insert ran unbounded. Keying on
54
+ # the IP makes the count accumulate, trip the limit, and stop inserting.
55
+ def attack_client_key
56
+ request.remote_ip.to_s
57
+ end
58
+
59
+ def attack_ban_cache_key
60
+ "plugins_attack_ban:#{attack_client_key}"
61
+ end
62
+
53
63
  def attack_check_request
54
64
  return unless current_site
55
65
 
56
66
  config = current_site.get_meta('attack_config')
57
- q = current_site.attack.where(browser_key: cama_get_session_id, path: attack_request_key)
58
- return unless config.present?
67
+ query = current_site.attack.where(browser_key: attack_client_key, path: attack_request_key)
68
+ return if config.blank?
59
69
 
60
70
  # clear past requests
61
71
  if begin
62
- Time.parse(config[:cleared])
72
+ Time.zone.parse(config[:cleared])
63
73
  rescue StandardError
64
74
  2.hours.ago
65
75
  end < 1.hour.ago
66
76
  current_site.attack.where('plugins_attacks.created_at < ?', 1.hour.ago).delete_all
67
- config[:cleared] = Time.now.to_s
77
+ config[:cleared] = Time.zone.now.iso8601 # Use ISO8601 for reliable parsing
68
78
  current_site.set_meta('attack_config', config)
69
79
  end
70
80
 
71
81
  # post request
72
82
  if request.post? || request.patch?
73
- r = q.where(created_at: config[:post][:sec].to_i.seconds.ago..Time.now)
83
+ r = query.where(created_at: config[:post][:sec].to_i.seconds.ago..Time.zone.now)
74
84
  if r.count > config[:post][:max].to_i
75
- Rails.cache.write(cama_get_session_id, config[:msg], expires_in: config[:ban].to_i.minutes)
76
- # send an email to administrator with request info (ip, browser, if logged then send user info
77
- render html: config[:msg].html_safe
85
+ Rails.cache.write(attack_ban_cache_key, config[:msg], expires_in: config[:ban].to_i.minutes)
86
+ # Email administrator with request info (ip, browser, if logged, then send user info
87
+ render html: config[:msg].to_s
78
88
  return
79
89
  end
80
90
 
81
91
  # get request
82
92
  else
83
- r = q.where(created_at: config[:get][:sec].to_i.seconds.ago..Time.now)
93
+ r = query.where(created_at: config[:get][:sec].to_i.seconds.ago..Time.zone.now)
84
94
  if r.count > config[:get][:max].to_i
85
- Rails.cache.write(cama_get_session_id, config[:msg], expires_in: config[:ban].to_i.minutes)
86
- render html: config[:msg].html_safe
95
+ Rails.cache.write(attack_ban_cache_key, config[:msg], expires_in: config[:ban].to_i.minutes)
96
+ render html: config[:msg].to_s
87
97
  return
88
98
  end
89
99
  end
90
- q.create
100
+ query.create
91
101
  end
92
102
 
93
103
  def attack_request_key(method = nil)
@@ -24,27 +24,34 @@ module Plugins
24
24
  private
25
25
 
26
26
  def plugin_authoring_form_html(post)
27
- "
28
- <div class='form-group'>
29
- <label class='control-label'>#{t('camaleon_cms.admin.table.author')}</label>
30
- <select id='post_user_id' #{if can?(:edit_other,
31
- post.post_type) && (can?(:edit_publish,
32
- post.post_type) || !post.published?)
33
- ''
34
- else
35
- 'disabled'
36
- end} name='post[user_id]' class='form-control select valid' aria-invalid='false'>#{plugin_authoring_authors_list(post)}</select>
37
- </div>
38
- "
27
+ disabled = !(can?(:edit_other, post.post_type) && (can?(:edit_publish, post.post_type) || !post.published?))
28
+
29
+ label = tag.label(t('camaleon_cms.admin.table.author'), class: 'control-label')
30
+ select = content_tag(
31
+ :select,
32
+ safe_join(plugin_authoring_authors_list(post)),
33
+ id: 'post_user_id', name: 'post[user_id]',
34
+ class: 'form-control select valid',
35
+ disabled: disabled,
36
+ 'aria-invalid' => 'false'
37
+ )
38
+
39
+ tag.div(safe_join([label, select]), class: 'form-group')
39
40
  end
40
41
 
41
42
  def plugin_authoring_authors_list(post)
42
43
  author_id = post.new_record? ? cama_current_user.id : post.author.id
43
- ret = ''
44
- current_site.users.where('role <> ?', 'client').order(:username).each do |user|
45
- ret += "<option value='#{user.id}' #{user.id.eql?(author_id) ? 'selected' : ''}>#{user.username.titleize}#{user.fullname.eql?(user.username.titleize) ? '' : " (#{user.fullname})"}</option>"
44
+
45
+ current_site.users.where('role <> ?', 'client').order(:username).map do |user|
46
+ selected = user.id == author_id
47
+ titleized_username = user.username.titleize
48
+ display = if user.fullname == titleized_username
49
+ titleized_username
50
+ else
51
+ "#{titleized_username} (#{user.fullname})"
52
+ end
53
+ tag.option(display, value: user.id, selected: selected)
46
54
  end
47
- ret
48
55
  end
49
56
  end
50
57
  end
@@ -5,19 +5,22 @@ module Plugins
5
5
 
6
6
  def settings
7
7
  @caches = current_site.get_meta('front_cache_elements', { paths: [] })
8
- @caches[:paths] << '' unless @caches[:paths].present?
8
+ @caches[:paths] << '' if @caches[:paths].blank?
9
9
  end
10
10
 
11
11
  def save_settings
12
- current_site.set_meta('front_cache_elements', { paths: (params[:cache][:paths] || []).compact_blank || [],
13
- posts: params[:cache][:posts] || [],
14
- post_types: params[:cache][:post_type] || [],
15
- skip_posts: params[:cache][:skip_posts] || [],
16
- cache_login: params[:cache][:cache_login],
17
- home: params[:cache][:home],
18
- preserve_cache_on_restart: params[:cache][:preserve_cache_on_restart],
19
- invalidate_only: params[:cache][:invalidate_only],
20
- cache_counter: current_site.get_meta('front_cache_elements')[:cache_counter] || 0 })
12
+ current_site.set_meta(
13
+ 'front_cache_elements',
14
+ {
15
+ paths: (params[:cache][:paths] || []).compact_blank || [],
16
+ posts: params[:cache][:posts] || [],
17
+ post_types: params[:cache][:post_type] || [],
18
+ skip_posts: params[:cache][:skip_posts] || [],
19
+ cache_login: params[:cache][:cache_login],
20
+ home: params[:cache][:home],
21
+ preserve_cache_on_restart: params[:cache][:preserve_cache_on_restart]
22
+ }
23
+ )
21
24
  flash[:notice] = t('plugin.front_cache.message.settings_saved').to_s
22
25
  redirect_to action: :settings
23
26
  end
@@ -25,6 +28,7 @@ module Plugins
25
28
  def clean_cache
26
29
  flash[:notice] = t('plugin.front_cache.message.cache_destroyed').to_s
27
30
  front_cache_clean
31
+ front_cache_purge_stored_pages
28
32
  redirect_to(request.referer || '/admin/plugins')
29
33
  end
30
34
  end
@@ -3,7 +3,7 @@ if begin
3
3
  rescue StandardError
4
4
  false
5
5
  end
6
- CamaleonCms::Site.all.each do |site|
6
+ CamaleonCms::Site.find_each do |site|
7
7
  site.set_option('refresh_cache', true)
8
8
  end
9
9
  end
@@ -13,7 +13,6 @@ en:
13
13
  pages_of: 'Pages of'
14
14
  skip_cache_pages: 'Skip Cache Pages'
15
15
  preserve_cache_on_restart: 'Preserve cache on restart'
16
- invalidate_only: 'Invalidate the cache instead of deleting it. (Recommended only if you use custom caching in your views. Not recommended when using FileStore, which is the Rails default cache)'
17
16
  message:
18
17
  please_checkpost_need_cache: 'Please check all posts or post types you need to cache.'
19
18
  enabled_cache_inner_content_logged_users: 'Enable cache for inner content for logged users'
@@ -34,7 +33,6 @@ es:
34
33
  pages_of: 'Página de'
35
34
  skip_cache_pages: 'Saltar página del Caché'
36
35
  preserve_cache_on_restart: 'Preservar caché al reiniciar'
37
- invalidate_only: 'Invalide el caché en lugar de eliminarlo. (Recomendado sólo si utiliza el almacenamiento en caché personalizado en sus vistas No se recomienda cuando se utiliza FileStore, que es la caché predeterminada de Rails)'
38
36
  message:
39
37
  please_checkpost_need_cache: 'Por favor, compruebe todos los mensajes o los tipos de correos que necesita para almacenar en caché.'
40
38
  enabled_cache_inner_content_logged_users: 'Habilitar caché de contenido interno para usuarios registrados'
@@ -56,7 +54,6 @@ nl:
56
54
  pages_of: "Inhoud van"
57
55
  skip_cache_pages: 'Cache overslaan'
58
56
  preserve_cache_on_restart: 'Bewaar cache bij herstarten'
59
- invalidate_only: 'De cache ongeldig maken, in plaats van het te verwijderen. (Alleen aanbevolen als u aangepaste caching gebruikt in uw weergaven. Niet aanbevolen bij het gebruik van FileStore, wat is de standaard cache van de Rails)'
60
57
  message:
61
58
  please_checkpost_need_cache: "Kijk alle berichten of inhoud type's na die je wil cachen."
62
59
  enabled_cache_inner_content_logged_users: 'Activeer cache voor gelogde gebruikers'
@@ -77,7 +74,6 @@ pt-BR:
77
74
  pages_of: 'Páginas de'
78
75
  skip_cache_pages: 'Saltar Cache de Páginas'
79
76
  preserve_cache_on_restart: 'Preserve o cache no reinício'
80
- invalidate_only: 'Invalide o cache em vez de excluí-lo. (Recomendado apenas se você usar cache personalizado em suas visualizações. Não recomendado quando usar FileStore, que é o cache padrão do Rails)'
81
77
  message:
82
78
  please_checkpost_need_cache: 'Por favor verifique todas as publicações ou o tipo do publicação que precisa colocar em cache.'
83
79
  enabled_cache_inner_content_logged_users: 'Permitir cache para conteúdo interno de utilizadores ligados'
@@ -98,7 +94,6 @@ pt-BR:
98
94
  pages_of: 'Páginas de'
99
95
  skip_cache_pages: 'Pular Cache de Páginas'
100
96
  preserve_cache_on_restart: 'Preserve o cache no reinício'
101
- invalidate_only: 'Invalide o cache em vez de excluí-lo. (Recomendado apenas se você usar cache personalizado em suas visualizações. Não recomendado quando usar FileStore, que é o cache padrão do Rails)'
102
97
  message:
103
98
  please_checkpost_need_cache: 'Por favor cheque todos os posts ou o tipo do post que você precisa cachear.'
104
99
  enabled_cache_inner_content_logged_users: 'Permitir cache para conteúdo interno de usuários logados'
@@ -119,7 +114,6 @@ it:
119
114
  pages_of: 'Pagine di'
120
115
  skip_cache_pages: 'Pagine da non cachare'
121
116
  preserve_cache_on_restart: 'Conserva la cache al riavvio'
122
- invalidate_only: 'Invalidate la cache invece di cancellarla. (Consigliato solo se si utilizza la cache personalizzata nelle tue viste. Non è consigliata quando si utilizza FileStore, che è la cache predefinita Rails)'
123
117
  message:
124
118
  please_checkpost_need_cache: 'Per favore, verifica tutti i tipi di messaggi o e-mail che è necessario memorizzare nella cache.'
125
119
  enabled_cache_inner_content_logged_users: 'Attiva il contenuto della cache interna per gli utenti registrati'
@@ -140,7 +134,6 @@ zh-CN:
140
134
  pages_of: '页'
141
135
  skip_cache_pages: '跳过缓存页面'
142
136
  preserve_cache_on_restart: '重新启动时保存缓存'
143
- invalidate_only: '使缓存无效,而不是删除它。 (仅当您在视图中使用自定义缓存时才推荐使用FileStore时,建议不要使用Rails默认缓存)'
144
137
  message:
145
138
  please_checkpost_need_cache: '请检查您需要缓存位内容.'
146
139
  enabled_cache_inner_content_logged_users: '为已登录的用户启用内部内容的缓存'
@@ -1,10 +1,18 @@
1
1
  module Plugins
2
2
  module FrontCache
3
3
  module FrontCacheHelper
4
+ # Upper bound on any stored page's life. Stores whose purge is rescued away (RedisCacheStore
5
+ # and MemCacheStore reject the matcher) would otherwise keep retired or never-revisited
6
+ # entries forever — and Redis's default maxmemory-policy is noeviction, so TTL-less bodies
7
+ # would grow until the shared store refuses writes. An expired entry is an ordinary miss: the
8
+ # page is re-rendered and re-cached.
9
+ FRONT_CACHE_EXPIRATION = 1.week
4
10
  # cache all pages configured in this plugin's settings for public users
5
11
  def front_cache_front_before_load
6
- if current_site.get_option('refresh_cache') # clear cache every restart server unless option checked in settings
7
- front_cache_clean unless current_site.get_meta('front_cache_elements')[:preserve_cache_on_restart]
12
+ # invalidate the site's cached pages on the first request after a (re)start, unless
13
+ # preserve_cache_on_restart is checked in the plugin settings
14
+ if current_site.get_option('refresh_cache')
15
+ front_cache_clean unless (current_site.get_meta('front_cache_elements') || {})[:preserve_cache_on_restart]
8
16
  current_site.set_option('refresh_cache', false)
9
17
  end
10
18
 
@@ -12,28 +20,45 @@ module Plugins
12
20
  return if signin? || Rails.env.development? || Rails.env.test? || !request.get?
13
21
 
14
22
  cache_key = front_cache_plugin_cache_key
15
- @caches = current_site.get_meta('front_cache_elements')
16
- if !flash.keys.present? && front_cache_exist?(cache_key) # recover cache item
23
+ # Fail closed on a missing settings meta: front_cache runs on every frontend request, so an
24
+ # absent front_cache_elements (never seeded, or hand-deleted) must degrade to "cache nothing"
25
+ # rather than raise NoMethodError and 500 the whole public site.
26
+ @caches = current_site.get_meta('front_cache_elements') || {}
27
+ # Single read: the old exist?-then-get pair issued two store reads, and an entry vanishing
28
+ # between them (a concurrent admin purge, TTL expiry) left .gsub running on nil — a
29
+ # visitor-facing 500.
30
+ cached_body = flash.keys.blank? ? front_cache_get(cache_key) : nil
31
+ if cached_body # recover cache item
17
32
  Rails.logger.info "Camaleon CMS - readed cache: #{front_cache_plugin_get_path(cache_key)}"
18
33
  response.headers['PLUGIN_FRONT_CACHE'] = 'TRUE'
19
- args = { data: front_cache_get(cache_key).gsub('{{form_authenticity_token}}', form_authenticity_token) }
34
+ args = { data: cached_body.gsub('{{form_authenticity_token}}', form_authenticity_token) }
20
35
  hooks_run('front_cache_reading_cache', args)
36
+ # rubocop:disable Rails/OutputSafety -- This replays a trusted cached page body that was already rendered by Rails.
21
37
  render html: args[:data].html_safe
38
+ # rubocop:enable Rails/OutputSafety
22
39
  return
23
40
  end
24
41
 
25
42
  @_plugin_do_cache = false
26
- if @caches[:paths].include?(request.original_url) || @caches[:paths].include?(request.path_info) || front_cache_plugin_match_path_patterns?(request.original_url, request.path_info) || (params[:action] == 'index' && params[:controller] == 'camaleon_cms/frontend' && @caches[:home].present?) # cache paths and home page
43
+ # cache paths and home page
44
+ paths = @caches[:paths] || []
45
+ if paths.include?(request.original_url) || paths.include?(request.path_info) ||
46
+ front_cache_plugin_match_path_patterns?(request.original_url, request.path_info) ||
47
+ (params[:action] == 'index' && params[:controller] == 'camaleon_cms/frontend' && @caches[:home].present?)
27
48
  @_plugin_do_cache = true
28
- elsif params[:action] == 'post' && params[:controller] == 'camaleon_cms/frontend' && !params[:draft_id].present?
29
- if (post = current_site.the_posts.find_by_slug(params[:slug]))
30
- post = post.decorate
31
- if post.can_visit? && post.visibility != 'private'
32
- if (@caches[:skip_posts] || []).include?(post.id.to_s)
33
- @_plugin_do_cache = false
34
- elsif (@caches[:post_types] || []).include?(post.post_type_id.to_s) || (@caches[:posts] || []).include?(post.id.to_s)
35
- @_plugin_do_cache = true
36
- end
49
+ elsif params[:action] == 'post' && params[:controller] == 'camaleon_cms/frontend' && params[:draft_id].blank?
50
+ # the_post is a single-record lookup (eager: false) -- no listing preloads for one post
51
+ # Never cache non-public posts. A password-protected post is unlocked per session (visibility_post
52
+ # audit M2), but the page cache is keyed on the URL alone, so caching an unlocked render would
53
+ # serve the protected body to visitors who never entered the password. Private posts are already
54
+ # excluded; password posts must be too.
55
+ if (post = current_site.the_post(params[:slug])) && post.can_visit? && !%w[private
56
+ password].include?(post.visibility)
57
+ if (@caches[:skip_posts] || []).include?(post.id.to_s)
58
+ @_plugin_do_cache = false
59
+ elsif (@caches[:post_types] || []).include?(post.post_type_id.to_s) ||
60
+ (@caches[:posts] || []).include?(post.id.to_s)
61
+ @_plugin_do_cache = true
37
62
  end
38
63
  end
39
64
  end
@@ -43,7 +68,7 @@ module Plugins
43
68
 
44
69
  def front_cache_front_after_load
45
70
  cache_key = front_cache_plugin_cache_key
46
- return unless @_plugin_do_cache && !flash.keys.present?
71
+ return unless @_plugin_do_cache && flash.keys.blank?
47
72
 
48
73
  body =
49
74
  response
@@ -69,8 +94,7 @@ module Plugins
69
94
  post_types: [current_site.post_types.where(slug: 'page').first.id],
70
95
  skip_posts: [],
71
96
  home: true,
72
- cache_login: true,
73
- cache_counter: 0
97
+ cache_login: true
74
98
  }
75
99
  )
76
100
  end
@@ -91,56 +115,104 @@ module Plugins
91
115
  arg[:links] << link_to(t('plugin.front_cache.clean_cache'), admin_plugins_front_cache_clean_path)
92
116
  end
93
117
 
94
- # save as cache all post requests
118
+ # invalidate the page cache on any content-changing request. PUT and DELETE count: a permanent
119
+ # post deletion rides DELETE (Rack::MethodOverride makes request.post? false for
120
+ # _method=delete forms), and without a bump the deleted page kept being served from cache.
95
121
  def front_cache_post_requests
96
- return unless request.post? || request.patch?
122
+ return unless request.post? || request.put? || request.patch? || request.delete?
123
+ # A draft autosave (posts/drafts) writes a private draft_child buffer, never published
124
+ # output, so it must not retire the public page cache — otherwise an open editor's
125
+ # per-minute autosave keeps the whole site's cache from ever warming.
126
+ return if params[:controller].to_s.end_with?('posts/drafts')
97
127
 
98
128
  front_cache_clean
99
129
  end
100
130
 
101
- # clear all frontend cache items
131
+ # invalidate all cached pages of the current site
102
132
  def front_cache_clean
103
- @caches = current_site.get_meta('front_cache_elements')
104
- if @caches[:invalidate_only]
105
- @caches[:cache_counter] += 1
106
- else
107
- Rails.cache.clear
108
- @caches[:cache_counter] = 0
109
- end
110
- current_site.set_meta('front_cache_elements', @caches)
133
+ # Security: never Rails.cache.clear — the store is shared, and clearing it on every POST also
134
+ # destroyed unrelated entries such as the per-IP login brute-force counter (CaptchaHelper),
135
+ # silently defeating it. Bumping the version compared on every page-cache read (ActiveSupport
136
+ # `version:`) retires all of this site's pages on any store, with no store enumeration.
137
+ #
138
+ # The version lives in its own meta key, apart from the front_cache_elements settings hash:
139
+ # save_settings rewrites that hash wholesale from an earlier read, so a version stored inside
140
+ # it could be reverted by a settings save racing a concurrent bump — and a reverted version
141
+ # resurrects a retired generation as servable.
142
+ current_site.set_meta('front_cache_counter', front_cache_version + 1)
111
143
  end
112
144
 
113
145
  private
114
146
 
147
+ # Current page-cache version of the site. `.to_i` covers a site that has never invalidated
148
+ # (missing meta) as version 0 — it must never be nil: an entry written with `version: nil`
149
+ # would match ANY requested version and become immune to invalidation.
150
+ def front_cache_version
151
+ current_site.get_meta('front_cache_counter').to_i
152
+ end
153
+
154
+ # Physically remove every stored page entry of the current site. Only the explicit admin
155
+ # "Clean cache" action calls this — per-request invalidation is the version bump alone, so the
156
+ # request path never pays a store walk. The pattern is ^-anchored on purpose: with a store
157
+ # :namespace, ActiveSupport's key_matcher recognizes only a leading ^ (composing /^ns:<rest>/);
158
+ # any other source becomes /^ns:.*<source>/, where \A can never match, silently deleting
159
+ # nothing. Keys are single-line, so ^ cannot over-match. Best-effort: delete_matched is
160
+ # optional store API (MemCacheStore raises NotImplementedError, RedisCacheStore accepts only
161
+ # glob Strings, third-party stores raise their own classes) and the version bump has already
162
+ # retired the pages, so any store error is logged and swallowed.
163
+ def front_cache_purge_stored_pages
164
+ Rails.cache.delete_matched(%r{^cama_front_cache/#{current_site.id}/})
165
+ rescue StandardError, NotImplementedError => e
166
+ Rails.logger.warn "Camaleon CMS - front_cache purge skipped: #{e.class}: #{e.message}"
167
+ end
168
+
115
169
  def front_cache_exist?(key)
116
- !Rails.cache.read(front_cache_plugin_get_path(key)).nil?
170
+ !front_cache_get(key).nil?
117
171
  end
118
172
 
119
173
  def front_cache_get(key)
120
- Rails.cache.read(front_cache_plugin_get_path(key))
174
+ Rails.cache.read(front_cache_plugin_get_path(key), version: front_cache_version)
121
175
  end
122
176
 
177
+ # One entry per URL: the version is stored inside the entry, so re-caching a page after an
178
+ # invalidation overwrites the retired body in place on every store — storage is bounded at one
179
+ # entry per cached URL instead of one per URL per invalidation.
123
180
  def front_cache_plugin_cache_create(key, content)
124
- Rails.cache.write(front_cache_plugin_get_path(key), content)
181
+ Rails.cache.write(front_cache_plugin_get_path(key), content,
182
+ version: front_cache_version, expires_in: FRONT_CACHE_EXPIRATION)
125
183
  end
126
184
 
127
- # return the physical path of cache directory
185
+ # return the cache key of a stored page
128
186
  # key: (string, optional) the key of the cached page
129
187
  def front_cache_plugin_get_path(key = nil)
130
188
  if key.nil?
131
- "pages/#{@caches[:cache_counter]}/#{current_site.id}"
189
+ "cama_front_cache/#{current_site.id}"
132
190
  else
133
- "pages/#{@caches[:cache_counter]}/#{current_site.id}/#{key}"
191
+ "cama_front_cache/#{current_site.id}/#{key}"
134
192
  end
135
193
  end
136
194
 
137
195
  def front_cache_plugin_match_path_patterns?(key, key2)
138
- @caches[:paths].any? { |path_pattern| key =~ Regexp.new(path_pattern) || key2 =~ Regexp.new(path_pattern) }
196
+ front_cache_compiled_path_patterns.any? { |pattern| key =~ pattern || key2 =~ pattern }
197
+ end
198
+
199
+ # Security (audit Low): compile the admin-configured patterns once per request and skip any that
200
+ # are malformed -- Regexp.new was rebuilt on every request and raised (500) on an invalid pattern
201
+ # (RegexpError) or a non-String pattern reachable via crafted settings params (TypeError).
202
+ def front_cache_compiled_path_patterns
203
+ @front_cache_compiled_path_patterns ||= (@caches[:paths] || []).filter_map do |pattern|
204
+ Regexp.new(pattern)
205
+ rescue RegexpError, TypeError
206
+ nil
207
+ end
139
208
  end
140
209
 
141
210
  def front_cache_plugin_cache_key
142
211
  uri = [request.protocol + request.host_with_port, request.fullpath].join('/')
143
- uri.parameterize
212
+ # Security (audit Low): a lossless key. parameterize lowercased and collapsed every run of
213
+ # non-alphanumerics to '-', so distinct URLs shared one cache entry -- a visitor to one
214
+ # cacheable URL could be served the cached body of another.
215
+ Digest::SHA256.hexdigest(uri)
144
216
  end
145
217
  end
146
218
  end
@@ -61,11 +61,6 @@
61
61
  <%= check_box_tag("cache[preserve_cache_on_restart]", 1, @caches[:preserve_cache_on_restart]) %>
62
62
  </div>
63
63
 
64
- <div class="form-group">
65
- <label><%= t('plugin.front_cache.invalidate_only') %></label><br>
66
- <%= check_box_tag("cache[invalidate_only]", 1, @caches[:invalidate_only]) %>
67
- </div>
68
-
69
64
  <!--<div class="form-group">-->
70
65
  <!--<label><%#= t('plugin.front_cache.message.enabled_cache_inner_content_logged_users') %></label><br>-->
71
66
  <!--<%#= check_box_tag("cache[cache_login]", 1, @caches[:cache_login]) %>-->
@@ -17,6 +17,7 @@
17
17
  "created_post":["plugin_visibility_create_post"],
18
18
  "updated_post":["plugin_visibility_create_post"],
19
19
  "post_the_content": ["plugin_visibility_post_the_content"],
20
+ "post_the_excerpt": ["plugin_visibility_post_the_excerpt"],
20
21
  "filter_post": ["plugin_visibility_filter_post"],
21
22
  "post_can_visit": ["plugin_visibility_can_visit"],
22
23
  "list_post_extra_columns": ["plugin_visibility_extra_columns"]