camaleon_cms 2.9.1 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -9
  3. data/Rakefile +3 -16
  4. data/app/apps/plugins/attack/admin_controller.rb +9 -6
  5. data/app/apps/plugins/attack/attack_helper.rb +35 -25
  6. data/app/apps/plugins/authoring_post/authoring_post_helper.rb +23 -16
  7. data/app/apps/plugins/front_cache/admin_controller.rb +16 -10
  8. data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
  9. data/app/apps/plugins/front_cache/front_cache_helper.rb +57 -24
  10. data/app/apps/plugins/visibility_post/config/config.json +1 -0
  11. data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
  12. data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
  13. data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
  14. data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
  15. data/app/apps/themes/default/assets/css/style.css.scss +24 -0
  16. data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
  17. data/app/apps/themes/default/default_helper.rb +2 -1
  18. data/app/apps/themes/default/views/category.html.erb +1 -1
  19. data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
  20. data/app/apps/themes/default/views/post_tag.html.erb +1 -1
  21. data/app/apps/themes/default/views/post_type.html.erb +1 -1
  22. data/app/apps/themes/default/views/search.html.erb +1 -1
  23. data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
  24. data/app/apps/themes/new/custom_helper.rb +5 -9
  25. data/app/apps/themes/new/views/category.html.erb +1 -1
  26. data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
  27. data/app/apps/themes/new/views/post_tag.html.erb +1 -1
  28. data/app/apps/themes/new/views/post_type.html.erb +1 -1
  29. data/app/apps/themes/new/views/search.html.erb +1 -1
  30. data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
  31. data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
  32. data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
  33. data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
  34. data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +29 -10
  35. data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
  36. data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +9 -2
  37. data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +4 -3
  38. data/app/controllers/camaleon_cms/admin/appearances/widgets/sidebar_controller.rb +2 -2
  39. data/app/controllers/camaleon_cms/admin/categories_controller.rb +27 -8
  40. data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
  41. data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
  42. data/app/controllers/camaleon_cms/admin/media_controller.rb +84 -19
  43. data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
  44. data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +15 -6
  45. data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
  46. data/app/controllers/camaleon_cms/admin/posts_controller.rb +58 -22
  47. data/app/controllers/camaleon_cms/admin/sessions_controller.rb +238 -45
  48. data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +103 -18
  49. data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +17 -5
  50. data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +33 -7
  51. data/app/controllers/camaleon_cms/admin/settings_controller.rb +11 -6
  52. data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +9 -7
  53. data/app/controllers/camaleon_cms/admin/users_controller.rb +89 -18
  54. data/app/controllers/camaleon_cms/admin_controller.rb +144 -26
  55. data/app/controllers/camaleon_cms/apps/plugins_admin_controller.rb +5 -0
  56. data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
  57. data/app/controllers/camaleon_cms/frontend_controller.rb +140 -70
  58. data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +44 -0
  59. data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
  60. data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
  61. data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
  62. data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
  63. data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
  64. data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
  65. data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
  66. data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
  67. data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
  68. data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
  69. data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
  70. data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
  71. data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
  72. data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
  73. data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
  74. data/app/decorators/camaleon_cms/post_decorator.rb +67 -51
  75. data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
  76. data/app/decorators/camaleon_cms/site_decorator.rb +29 -23
  77. data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
  78. data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
  79. data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
  80. data/app/helpers/camaleon_cms/admin/application_helper.rb +21 -24
  81. data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
  82. data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
  83. data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
  84. data/app/helpers/camaleon_cms/admin/post_type_helper.rb +51 -31
  85. data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
  86. data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
  87. data/app/helpers/camaleon_cms/comment_helper.rb +87 -41
  88. data/app/helpers/camaleon_cms/content_helper.rb +13 -6
  89. data/app/helpers/camaleon_cms/email_helper.rb +7 -3
  90. data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
  91. data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +144 -67
  92. data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
  93. data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
  94. data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
  95. data/app/helpers/camaleon_cms/html_helper.rb +73 -38
  96. data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
  97. data/app/helpers/camaleon_cms/session_helper.rb +195 -39
  98. data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
  99. data/app/helpers/camaleon_cms/site_helper.rb +43 -9
  100. data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
  101. data/app/helpers/camaleon_cms/uploader_helper.rb +19 -422
  102. data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
  103. data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
  104. data/app/models/camaleon_cms/ability.rb +65 -105
  105. data/app/models/camaleon_cms/category.rb +7 -5
  106. data/app/models/camaleon_cms/custom_field.rb +29 -6
  107. data/app/models/camaleon_cms/custom_field_group.rb +75 -21
  108. data/app/models/camaleon_cms/custom_fields_relationship.rb +133 -2
  109. data/app/models/camaleon_cms/media.rb +8 -2
  110. data/app/models/camaleon_cms/meta.rb +2 -2
  111. data/app/models/camaleon_cms/nav_menu.rb +6 -2
  112. data/app/models/camaleon_cms/nav_menu_item.rb +18 -8
  113. data/app/models/camaleon_cms/plugin.rb +4 -3
  114. data/app/models/camaleon_cms/post.rb +113 -27
  115. data/app/models/camaleon_cms/post_comment.rb +10 -6
  116. data/app/models/camaleon_cms/post_default.rb +42 -12
  117. data/app/models/camaleon_cms/post_relationship.rb +6 -4
  118. data/app/models/camaleon_cms/post_tag.rb +4 -3
  119. data/app/models/camaleon_cms/post_type.rb +22 -12
  120. data/app/models/camaleon_cms/site.rb +40 -28
  121. data/app/models/camaleon_cms/term_relationship.rb +10 -7
  122. data/app/models/camaleon_cms/term_taxonomy.rb +51 -5
  123. data/app/models/camaleon_cms/theme.rb +3 -3
  124. data/app/models/camaleon_cms/user.rb +17 -5
  125. data/app/models/camaleon_cms/user_role.rb +77 -4
  126. data/app/models/camaleon_cms/widget/assigned.rb +11 -6
  127. data/app/models/camaleon_cms/widget/main.rb +13 -7
  128. data/app/models/camaleon_cms/widget/sidebar.rb +3 -6
  129. data/app/models/camaleon_record.rb +123 -5
  130. data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +20 -38
  131. data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
  132. data/app/models/concerns/camaleon_cms/custom_fields_read.rb +115 -58
  133. data/app/models/concerns/camaleon_cms/metas.rb +32 -7
  134. data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
  135. data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
  136. data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
  137. data/app/models/current_request.rb +23 -0
  138. data/app/uploaders/camaleon_cms_aws_uploader.rb +39 -11
  139. data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
  140. data/app/uploaders/camaleon_cms_uploader.rb +23 -10
  141. data/app/validators/camaleon_cms/post_uniq_validator.rb +21 -8
  142. data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
  143. data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
  144. data/app/views/camaleon_cms/admin/appearances/nav_menus/_left_menu_items.html.erb +2 -2
  145. data/app/views/camaleon_cms/admin/appearances/widgets/main/form.html.erb +1 -1
  146. data/app/views/camaleon_cms/admin/categories/index.html.erb +3 -3
  147. data/app/views/camaleon_cms/admin/comments/index.html.erb +2 -2
  148. data/app/views/camaleon_cms/admin/comments/list.html.erb +1 -1
  149. data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
  150. data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
  151. data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
  152. data/app/views/camaleon_cms/admin/post_tags/index.html.erb +3 -3
  153. data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +2 -2
  154. data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
  155. data/app/views/camaleon_cms/admin/posts/index.html.erb +6 -6
  156. data/app/views/camaleon_cms/admin/search.html.erb +1 -1
  157. data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
  158. data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
  159. data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
  160. data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
  161. data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +42 -6
  162. data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
  163. data/app/views/camaleon_cms/admin/settings/custom_fields/fields/_select_eval.html.erb +1 -1
  164. data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +5 -4
  165. data/app/views/camaleon_cms/admin/settings/custom_fields/index.html.erb +1 -1
  166. data/app/views/camaleon_cms/admin/settings/post_types/index.html.erb +1 -1
  167. data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
  168. data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
  169. data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
  170. data/app/views/camaleon_cms/admin/settings/sites/index.html.erb +1 -1
  171. data/app/views/camaleon_cms/admin/user_roles/form.html.erb +102 -7
  172. data/app/views/camaleon_cms/admin/user_roles/index.html.erb +1 -1
  173. data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
  174. data/app/views/camaleon_cms/admin/users/index.html.erb +2 -2
  175. data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
  176. data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
  177. data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
  178. data/app/views/layouts/camaleon_cms/admin/_flash_messages.html.erb +2 -2
  179. data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
  180. data/config/initializers/action_view.rb +42 -5
  181. data/config/initializers/active_record_extension.rb +13 -27
  182. data/config/initializers/assets.rb +52 -23
  183. data/config/initializers/custom_initializers.rb +3 -3
  184. data/config/locales/camaleon_cms/admin/ar.yml +7 -2
  185. data/config/locales/camaleon_cms/admin/de.yml +7 -2
  186. data/config/locales/camaleon_cms/admin/en.yml +39 -2
  187. data/config/locales/camaleon_cms/admin/es.yml +7 -2
  188. data/config/locales/camaleon_cms/admin/fr.yml +7 -2
  189. data/config/locales/camaleon_cms/admin/it.yml +7 -2
  190. data/config/locales/camaleon_cms/admin/nl.yml +8 -2
  191. data/config/locales/camaleon_cms/admin/pt-BR.yml +7 -2
  192. data/config/locales/camaleon_cms/admin/pt.yml +7 -2
  193. data/config/locales/camaleon_cms/admin/ru.yml +7 -2
  194. data/config/locales/camaleon_cms/admin/uk.yml +7 -2
  195. data/config/locales/camaleon_cms/admin/zh-CH.yml +7 -2
  196. data/config/routes/admin.rb +33 -15
  197. data/config/routes/frontend.rb +21 -15
  198. data/db/migrate/20150611161134_post_table_into_utf8.rb +14 -14
  199. data/db/migrate/20150926095310_rename_column_posts.rb +3 -3
  200. data/db/migrate/20151212095328_add_confirm_token_to_users.rb +3 -3
  201. data/db/migrate/20160504155652_add_feature_to_posts.rb +1 -1
  202. data/db/migrate/20160504155653_move_first_name_of_users.rb +2 -2
  203. data/db/migrate/20160609121449_add_group_to_custom_field_values.rb +1 -1
  204. data/db/migrate/20161215202255_drop_user_relationship_table.rb +1 -1
  205. data/db/migrate/20180124132318_create_media.rb +1 -1
  206. data/db/migrate/20180704211100_adjust_field_length.rb +1 -1
  207. data/lib/camaleon_cms/assets_precompile.rb +86 -0
  208. data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
  209. data/lib/camaleon_cms/content_security.rb +158 -0
  210. data/lib/camaleon_cms/engine.rb +47 -9
  211. data/lib/camaleon_cms/media_security_headers.rb +27 -0
  212. data/lib/camaleon_cms/setup_token.rb +69 -0
  213. data/lib/camaleon_cms/svg_content_checker.rb +123 -0
  214. data/lib/camaleon_cms/task_reporter.rb +14 -0
  215. data/lib/camaleon_cms/unsafe_markup.rb +207 -0
  216. data/lib/camaleon_cms/uploader_content_security.rb +248 -0
  217. data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
  218. data/lib/camaleon_cms/uploader_path_security.rb +146 -0
  219. data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
  220. data/lib/camaleon_cms/uploader_support.rb +86 -0
  221. data/lib/camaleon_cms/version.rb +1 -1
  222. data/lib/camaleon_cms.rb +11 -0
  223. data/lib/ext/hash.rb +28 -2
  224. data/lib/ext/string.rb +13 -8
  225. data/lib/ext/translator.rb +10 -2
  226. data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
  227. data/lib/generators/camaleon_cms/install_generator.rb +7 -4
  228. data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
  229. data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
  230. data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
  231. data/lib/plugin_routes.rb +412 -368
  232. data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
  233. data/lib/tasks/cross_site_field_groups.rake +74 -0
  234. data/lib/tasks/custom_fields_roles.rake +58 -0
  235. data/lib/tasks/orphaned_comments.rake +42 -0
  236. data/lib/tasks/private_upload_acls.rake +31 -0
  237. data/lib/tasks/site_custom_field_groups.rake +36 -0
  238. data/lib/tasks/unsafe_stored_content.rake +42 -0
  239. data/lib/tasks/unsafe_stored_uploads.rake +70 -0
  240. data/lib/tasks/user_field_groups.rake +27 -0
  241. metadata +185 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6aace403e18b38c945faca659af2196ed66f130ed61e7b5454626137c59331ce
4
- data.tar.gz: f104182791714153a963e03ccc23dec4720cd9e7528ab378b0facf33e351e2b4
3
+ metadata.gz: e0997f6bbf586f6f6c5618c97814c0a291fa98bcb54adbd7736a0449f60edc33
4
+ data.tar.gz: e73b0849f7b21ffc7bd7510d71c328706b022cfefaf738194836b9e332e4f062
5
5
  SHA512:
6
- metadata.gz: 28708ab09b20a2a0c78ae3f104e2a38c5560725596bc9e70e935fb3384230f0ef006523d9f56db6e3c4ee5238a066ca6e1d6ea31e710623c8b9c9b8777380fb2
7
- data.tar.gz: c7fe66386eb9b15fd988afc2ad366a1ffd16d82f923219c10b61e590e5eda7f27b66413da745cb9eca8c736284500f35d9bbd41f6c39dd99828164ba8c308544
6
+ metadata.gz: 74c4843293dd8fbfa585cc784834866be63a7e330b7c9d21ecbbeb9a9e6753168d5c2502705db0fdecee9f89b2ff3b3d102e699c41384fb398f6332be98464cf
7
+ data.tar.gz: f152511e4e5022003ea2003a275cd677912cce73ac55bb9dbc8528094b6ddcf529b445627ba3750c4bbcebf5412b7107049c43daa0f3e3f5390539d2ba6c4eff
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:
@@ -173,15 +172,29 @@ Owen Peredo Diaz
173
172
  https://camaleon.website/license.html
174
173
 
175
174
  ## Testing
176
- * Init DB
175
+ > **Note:** Since this project is a gem, Rails commands (e.g., `rails routes`, `zeitwerk:check`) MUST be run from the `spec/dummy` folder. Use subshells to ensure you return to root: `(cd spec/dummy && bin/rails ...)`
176
+
177
+ - Init DB
178
+
177
179
  ```bash
178
180
  RAILS_ENV=test bundle exec rake app:db:migrate
179
181
  RAILS_ENV=test bundle exec rake app:db:test:prepare
180
182
  ```
181
- * Run tests
182
- ```bash
183
- bundle exec rspec
184
- ```
183
+
184
+ - Run the tests: `bin/rspec`
185
+ - If the `rspec` binstub is not present, generate it: `bundle binstubs rspec-core`
186
+ - And then still run `bin/rspec`
187
+
188
+ ## Permissions (Manager roles)
189
+
190
+ See [docs/security/permissions.md](docs/security/permissions.md) for details on manager permissions, custom fields, and security considerations.
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`, …).
185
198
 
186
199
  ## Contributing
187
200
  * Fork it.
@@ -190,6 +203,13 @@ bundle exec rspec
190
203
  * Push to the branch (git push origin my_feature_branch)
191
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)
192
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
+
193
213
  ## Version History
194
214
 
195
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
@@ -2,21 +2,27 @@ module Plugins
2
2
  module FrontCache
3
3
  class AdminController < CamaleonCms::Apps::PluginsAdminController
4
4
  include Plugins::FrontCache::FrontCacheHelper
5
+
5
6
  def settings
6
7
  @caches = current_site.get_meta('front_cache_elements', { paths: [] })
7
- @caches[:paths] << '' unless @caches[:paths].present?
8
+ @caches[:paths] << '' if @caches[:paths].blank?
8
9
  end
9
10
 
10
11
  def save_settings
11
- current_site.set_meta('front_cache_elements', { paths: (params[:cache][:paths] || []).compact_blank || [],
12
- posts: params[:cache][:posts] || [],
13
- post_types: params[:cache][:post_type] || [],
14
- skip_posts: params[:cache][:skip_posts] || [],
15
- cache_login: params[:cache][:cache_login],
16
- home: params[:cache][:home],
17
- preserve_cache_on_restart: params[:cache][:preserve_cache_on_restart],
18
- invalidate_only: params[:cache][:invalidate_only],
19
- 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
+ invalidate_only: params[:cache][:invalidate_only],
23
+ cache_counter: current_site.get_meta('front_cache_elements')[:cache_counter] || 0
24
+ }
25
+ )
20
26
  flash[:notice] = t('plugin.front_cache.message.settings_saved').to_s
21
27
  redirect_to action: :settings
22
28
  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
@@ -1,37 +1,47 @@
1
1
  module Plugins
2
2
  module FrontCache
3
3
  module FrontCacheHelper
4
- # save as cache all pages configured on settings of this plugin for public users
4
+ # cache all pages configured in this plugin's settings for public users
5
5
  def front_cache_front_before_load
6
6
  if current_site.get_option('refresh_cache') # clear cache every restart server unless option checked in settings
7
7
  front_cache_clean unless current_site.get_meta('front_cache_elements')[:preserve_cache_on_restart]
8
8
  current_site.set_option('refresh_cache', false)
9
9
  end
10
10
 
11
- # avoid cache if current visitor is logged in or development
11
+ # avoid cache if the current visitor is logged in, or we're in the development or test environment
12
12
  return if signin? || Rails.env.development? || Rails.env.test? || !request.get?
13
13
 
14
14
  cache_key = front_cache_plugin_cache_key
15
15
  @caches = current_site.get_meta('front_cache_elements')
16
- if !flash.keys.present? && front_cache_exist?(cache_key) # recover cache item
16
+ if flash.keys.blank? && front_cache_exist?(cache_key) # recover cache item
17
17
  Rails.logger.info "Camaleon CMS - readed cache: #{front_cache_plugin_get_path(cache_key)}"
18
18
  response.headers['PLUGIN_FRONT_CACHE'] = 'TRUE'
19
19
  args = { data: front_cache_get(cache_key).gsub('{{form_authenticity_token}}', form_authenticity_token) }
20
20
  hooks_run('front_cache_reading_cache', args)
21
+ # rubocop:disable Rails/OutputSafety -- This replays a trusted cached page body that was already rendered by Rails.
21
22
  render html: args[:data].html_safe
23
+ # rubocop:enable Rails/OutputSafety
22
24
  return
23
25
  end
24
26
 
25
27
  @_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
28
+ # cache paths and home page
29
+ if @caches[:paths].include?(request.original_url) || @caches[:paths].include?(request.path_info) ||
30
+ front_cache_plugin_match_path_patterns?(request.original_url, request.path_info) ||
31
+ (params[:action] == 'index' && params[:controller] == 'camaleon_cms/frontend' && @caches[:home].present?)
27
32
  @_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]))
33
+ elsif params[:action] == 'post' && params[:controller] == 'camaleon_cms/frontend' && params[:draft_id].blank?
34
+ if (post = current_site.the_posts.find_by_slug(params[:slug])) # rubocop:disable Rails/DynamicFindBy
30
35
  post = post.decorate
31
- if post.can_visit? && post.visibility != 'private'
36
+ # Never cache non-public posts. A password-protected post is unlocked per session (visibility_post
37
+ # audit M2), but the page cache is keyed on the URL alone, so caching an unlocked render would
38
+ # serve the protected body to visitors who never entered the password. Private posts are already
39
+ # excluded; password posts must be too.
40
+ if post.can_visit? && !%w[private password].include?(post.visibility)
32
41
  if (@caches[:skip_posts] || []).include?(post.id.to_s)
33
42
  @_plugin_do_cache = false
34
- elsif (@caches[:post_types] || []).include?(post.post_type_id.to_s) || (@caches[:posts] || []).include?(post.id.to_s)
43
+ elsif (@caches[:post_types] || []).include?(post.post_type_id.to_s) ||
44
+ (@caches[:posts] || []).include?(post.id.to_s)
35
45
  @_plugin_do_cache = true
36
46
  end
37
47
  end
@@ -43,11 +53,15 @@ module Plugins
43
53
 
44
54
  def front_cache_front_after_load
45
55
  cache_key = front_cache_plugin_cache_key
46
- return unless @_plugin_do_cache && !flash.keys.present?
47
-
48
- args = { data: response.body
49
- .gsub(/csrf-token" content="(.*?)"/, 'csrf-token" content="{{form_authenticity_token}}"')
50
- .gsub(/name="authenticity_token" value="(.*?)"/, 'name="authenticity_token" value="{{form_authenticity_token}}"') }
56
+ return unless @_plugin_do_cache && flash.keys.blank?
57
+
58
+ body =
59
+ response
60
+ .body.gsub(/csrf-token" content="(.*?)"/, 'csrf-token" content="{{form_authenticity_token}}"')
61
+ .gsub(
62
+ /name="authenticity_token" value="(.*?)"/, 'name="authenticity_token" value="{{form_authenticity_token}}"'
63
+ )
64
+ args = { data: body }
51
65
  hooks_run('front_cache_writing_cache', args)
52
66
  front_cache_plugin_cache_create(cache_key, args[:data])
53
67
  Rails.logger.info "Camaleon CMS - cache saved as: #{front_cache_plugin_get_path(cache_key)}"
@@ -57,22 +71,27 @@ module Plugins
57
71
  def front_cache_on_active(_plugin)
58
72
  return if current_site.get_meta('front_cache_elements', nil).present?
59
73
 
60
- current_site.set_meta('front_cache_elements', { paths: [],
61
- posts: [],
62
- post_types: [current_site.post_types.where(slug: 'page').first.id],
63
- skip_posts: [],
64
- home: true,
65
- cache_login: true,
66
- cache_counter: 0 })
74
+ current_site.set_meta(
75
+ 'front_cache_elements',
76
+ {
77
+ paths: [],
78
+ posts: [],
79
+ post_types: [current_site.post_types.where(slug: 'page').first.id],
80
+ skip_posts: [],
81
+ home: true,
82
+ cache_login: true,
83
+ cache_counter: 0
84
+ }
85
+ )
67
86
  end
68
87
 
69
88
  # on uninstall plugin
70
- def front_cache_on_inactive(plugin)
89
+ def front_cache_on_inactive(_plugin)
71
90
  # current_site.delete_meta("front_cache_elements")
72
91
  end
73
92
 
74
93
  # cache actions (for logged users)
75
- def front_cache_on_render(args); end
94
+ def front_cache_on_render(_args); end
76
95
 
77
96
  # expire cache for a page after comment registered or updated
78
97
  def front_cache_before_load; end
@@ -126,12 +145,26 @@ module Plugins
126
145
  end
127
146
 
128
147
  def front_cache_plugin_match_path_patterns?(key, key2)
129
- @caches[:paths].any? { |path_pattern| key =~ Regexp.new(path_pattern) || key2 =~ Regexp.new(path_pattern) }
148
+ front_cache_compiled_path_patterns.any? { |pattern| key =~ pattern || key2 =~ pattern }
149
+ end
150
+
151
+ # Security (audit Low): compile the admin-configured patterns once per request and skip any that
152
+ # are malformed -- Regexp.new was rebuilt on every request and raised (500) on an invalid pattern
153
+ # (RegexpError) or a non-String pattern reachable via crafted settings params (TypeError).
154
+ def front_cache_compiled_path_patterns
155
+ @front_cache_compiled_path_patterns ||= (@caches[:paths] || []).filter_map do |pattern|
156
+ Regexp.new(pattern)
157
+ rescue RegexpError, TypeError
158
+ nil
159
+ end
130
160
  end
131
161
 
132
162
  def front_cache_plugin_cache_key
133
163
  uri = [request.protocol + request.host_with_port, request.fullpath].join('/')
134
- uri.parameterize
164
+ # Security (audit Low): a lossless key. parameterize lowercased and collapsed every run of
165
+ # non-alphanumerics to '-', so distinct URLs shared one cache entry -- a visitor to one
166
+ # cacheable URL could be served the cached body of another.
167
+ Digest::SHA256.hexdigest(uri)
135
168
  end
136
169
  end
137
170
  end
@@ -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"]
@@ -0,0 +1 @@
1
+ post 'unlock/:post_id' => 'front#unlock', as: :unlock, constraints: { post_id: /[0-9]+/ }
@@ -0,0 +1,23 @@
1
+ module Plugins
2
+ module VisibilityPost
3
+ class FrontController < CamaleonCms::Apps::PluginsFrontController
4
+ # Security (audit M2): unlocks a password-protected post for the current session. Replaces
5
+ # the old GET `post_password` parameter flow, which put the password in URLs, logs and
6
+ # referrers and compared it with `==`. The password travels in a POST body, is compared in
7
+ # constant time, and the unlocked state is a session-side marker -- never the password.
8
+ def unlock
9
+ post = current_site.the_posts.where(id: params[:post_id]).first
10
+ return page_not_found if post.blank? || post.visibility != 'password'
11
+
12
+ submitted = params[:post_password].to_s
13
+ if submitted.present? && ActiveSupport::SecurityUtils.secure_compare(submitted, post.visibility_value.to_s)
14
+ session[VisibilityPostHelper::SESSION_UNLOCKED_KEY] =
15
+ Array(session[VisibilityPostHelper::SESSION_UNLOCKED_KEY]) | [post.id]
16
+ else
17
+ flash[:cama_visibility_post_error] = true
18
+ end
19
+ redirect_to post.decorate.the_url(as_path: true)
20
+ end
21
+ end
22
+ end
23
+ end