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
@@ -100,8 +100,12 @@ en:
100
100
  custom_group_support_multiple: Support multiple groups
101
101
  message:
102
102
  custom_group_error: 'Error custom field group'
103
+ value_rejected_html: "The '%{slug}' field contains HTML that is not allowed for your role (for example scripts, event handlers or embeds). Remove it, or ask an administrator for the unfiltered-content permission."
104
+ value_rejected_uri: "The '%{slug}' field contains a script-capable URL (javascript:/data:), which is not allowed."
105
+ value_too_large: "The '%{slug}' field is too large to store; please shorten it."
103
106
  custom_created: 'The custom field group has been created.'
104
107
  custom_updated: 'The custom field group has been updated.'
108
+ invalid_placement: 'Select where to display this group. The selected target does not belong to this site.'
105
109
  fields:
106
110
  text_box: 'Text Box'
107
111
  private_file: Private File
@@ -170,11 +174,19 @@ en:
170
174
  subject_email: 'Reset your access'
171
175
  send_mail_succes: 'Send email reset success.'
172
176
  send_mail_error: 'Not found email address.'
177
+ password_reset_requested: 'If an account matches that email, a password reset link has been sent.'
173
178
  invalid_caption: 'Invalid captcha'
174
179
  email_not_validated: 'Email has not been validated.'
175
180
  confirm_email_token_incorrect: 'URL incorrect'
176
181
  confirm_email_token_expired: 'Confirm email URL expired'
177
182
  confirm_email_success: 'Confirm email success!'
183
+ reauth_failed: 'Incorrect password. Please try again.'
184
+ too_many_attempts: 'Too many failed attempts. Please try again later.'
185
+ back_to_parent:
186
+ title: 'Return to your account'
187
+ instructions: 'You are currently working as %{impersonated}. Enter the password for your own account to return to it, or log out completely.'
188
+ submit: 'Return to my account'
189
+ logout: 'Log out completely'
178
190
  personal_info: 'Personal info'
179
191
  please_login: 'Please login'
180
192
  reset_password: 'reset password'
@@ -200,6 +212,8 @@ en:
200
212
  out: 'Out'
201
213
  logout: 'Logout'
202
214
  logout_html: 'Log <strong>Out</strong>?'
215
+ confirm_title: 'Log out?'
216
+ confirm_instructions: 'You are about to end your session. Continue?'
203
217
  message:
204
218
  closed: 'Session Closed'
205
219
  media:
@@ -273,6 +287,9 @@ en:
273
287
  new_site: 'New Site'
274
288
  edit_site: 'Edit Site'
275
289
  update: 'Update'
290
+ shortcodes:
291
+ message:
292
+ rejected: "This content contains a shortcode that your role is not allowed to publish. Remove the shortcode, or ask an administrator for the shortcode-content permission."
276
293
  post:
277
294
  add_tag: 'Add a Tag'
278
295
  clone: 'Clone'
@@ -281,6 +298,8 @@ en:
281
298
  label_required: 'Required Label'
282
299
  placeholder_required: 'Required Placeholder'
283
300
  message:
301
+ content_rejected: 'contains HTML that is not allowed for your role (for example scripts, event handlers or embeds). Remove it, or ask an administrator for the unfiltered-content permission.'
302
+ content_too_large: 'is too large to store; please shorten it.'
284
303
  created: 'The %{post_type} has been created.'
285
304
  updated: 'The %{post_type} has been updated.'
286
305
  deleted: 'The %{post_type} has been removed.'
@@ -487,10 +506,24 @@ en:
487
506
  message:
488
507
  error: 'Not found site'
489
508
  created: 'The site has been created.'
509
+ created_with_admin: 'Site created. New administrator password: %{password}. Change it after signing in.'
490
510
  updated: 'The site has been updated.'
491
511
  deleted: 'Correct Deleted'
492
512
  enter_key_domain: 'Enter a key or a domain'
493
513
  enter_key_domain_empty: 'Enter a key (site2) or a domain (my_domain.com). Key will be the name of your subdomain.'
514
+ installer:
515
+ setup_token: 'Setup token'
516
+ setup_token_hint: 'Required for remote installations; leave blank when installing locally. Printed in the server log and saved to tmp/camaleon_setup_token on first run.'
517
+ invalid_setup_token: 'A valid setup token is required to install. See the server log or tmp/camaleon_setup_token.'
518
+ welcome_title: 'Your site was created successfully.'
519
+ visit_frontend: 'Visit Frontend:'
520
+ visit_admin: 'Visit Admin panel'
521
+ here: 'Here.'
522
+ user_label: 'User:'
523
+ password_label: 'Password:'
524
+ copy: 'Copy'
525
+ copied: 'Copied!'
526
+ must_change_notice: 'Save this password now — it is shown only once. You will be asked to change it when you first sign in.'
494
527
  signin:
495
528
  message:
496
529
  enter_username_password: 'You must enter a username and/or password'
@@ -628,6 +661,7 @@ en:
628
661
  created: 'The user has been created.'
629
662
  updated: 'The user has been updated.'
630
663
  deleted: 'The user has been deleted.'
664
+ must_change_password: 'Please choose a new password before continuing.'
631
665
  incorrect_old_password: 'Incorrect old password'
632
666
  rol_error: 'Not found group user.'
633
667
  rol_created: 'The user role has been created.'
@@ -710,6 +744,8 @@ en:
710
744
  max: 'Please enter a value less than or equal to {0}.'
711
745
  min: 'Please enter a value greater than or equal to {0}.'
712
746
  username_alphanumeric: 'Username needs to start with an alphanumeric character'
747
+ https_only_url: 'Only HTTPS URLs are allowed'
748
+ path_traversal: 'URL path contains path traversal sequences'
713
749
  widgets:
714
750
  create_widget: 'Create Widget'
715
751
  create_sidebar: 'Create Sidebar'
@@ -125,6 +125,7 @@ es:
125
125
  message:
126
126
  success: 'Bienvenido!!!'
127
127
  fail: 'Usuario o contraseña incorrectos'
128
+ too_many_attempts: 'Demasiados intentos fallidos. Vuelva a intentarlo más tarde.'
128
129
  forgot_url_incorrect: 'URL incorrecta'
129
130
  forgot_expired: 'Restablecer caducado.'
130
131
  hello: 'Hola'
@@ -128,6 +128,7 @@ fr:
128
128
  message:
129
129
  success: 'Bienvenue !!!'
130
130
  fail: 'Nom ou mot de passe incorrect'
131
+ too_many_attempts: 'Trop de tentatives échouées. Veuillez réessayer plus tard.'
131
132
  forgot_url_incorrect: 'URL incorrecte'
132
133
  forgot_expired: 'Réinitialisation expirée.'
133
134
  hello: 'Bonjour'
@@ -135,6 +135,7 @@ it:
135
135
  message:
136
136
  success: 'Benvenuto!!!'
137
137
  fail: 'Username o Password errati'
138
+ too_many_attempts: 'Troppi tentativi falliti. Riprova più tardi.'
138
139
  forgot_url_incorrect: 'URL errato'
139
140
  forgot_expired: 'Reset scaduto.'
140
141
  hello: 'Ciao'
@@ -126,6 +126,7 @@ nl:
126
126
  message:
127
127
  success: 'Welkom!!!'
128
128
  fail: 'Gebruikersnaam of wachtwoord incorrect'
129
+ too_many_attempts: 'Te veel mislukte pogingen. Probeer het later opnieuw.'
129
130
  forgot_url_incorrect: 'URL incorrect'
130
131
  forgot_expired: 'Reset verlopen.'
131
132
  hello: 'Hallo'
@@ -126,6 +126,7 @@ pt-BR:
126
126
  message:
127
127
  success: 'Bem-vindo!!!'
128
128
  fail: 'Usuário ou senha incorreta'
129
+ too_many_attempts: 'Muitas tentativas malsucedidas. Tente novamente mais tarde.'
129
130
  forgot_url_incorrect: 'URL incorreta'
130
131
  forgot_expired: 'Expirou o prazo para redefinir a senha.'
131
132
  hello: 'Olá'
@@ -124,6 +124,7 @@ pt:
124
124
  message:
125
125
  success: 'Benvindo!!!'
126
126
  fail: 'Utilizador ou password incorreta'
127
+ too_many_attempts: 'Demasiadas tentativas falhadas. Tente novamente mais tarde.'
127
128
  forgot_url_incorrect: 'Endereço incorreto'
128
129
  forgot_expired: 'Expirou o prazo para redefinir a password.'
129
130
  hello: 'Olá'
@@ -137,6 +137,7 @@ ru:
137
137
  message:
138
138
  success: 'Добро пожаловать!!!'
139
139
  fail: 'Неверное имя пользователя или пароль'
140
+ too_many_attempts: 'Слишком много неудачных попыток. Повторите попытку позже.'
140
141
  forgot_url_incorrect: 'Неверный URL'
141
142
  forgot_expired: 'Сброс истек.'
142
143
  hello: 'Привет'
@@ -134,6 +134,7 @@ uk:
134
134
  message:
135
135
  success: 'Ласкаво просимо !!!'
136
136
  fail: "Неправильне ім'я користувача або пароль"
137
+ too_many_attempts: 'Забагато невдалих спроб. Спробуйте пізніше.'
137
138
  forgot_url_incorrect: 'Неправильний URL'
138
139
  forgot_expired: 'Скидання закінчилось.'
139
140
  hello: 'Привіт'
@@ -143,6 +143,7 @@ zh-CN:
143
143
  message:
144
144
  success: '欢迎登录!!!'
145
145
  fail: '用户名或密码不正确'
146
+ too_many_attempts: '尝试次数过多,请稍后再试。'
146
147
  forgot_url_incorrect: '链接错误'
147
148
  forgot_expired: '重置链接已过期。'
148
149
  hello: '您好'
@@ -8,17 +8,22 @@ Rails.application.routes.draw do
8
8
  get 'search'
9
9
  get 'login' => 'sessions#login'
10
10
  post 'login' => 'sessions#login_post'
11
- get 'logout' => 'sessions#logout'
11
+ # Security (audit M6): logout changes state, so it acts only over POST. The GET renders a
12
+ # confirmation page instead of 404ing -- frontend themes across the ecosystem link this
13
+ # path -- and the impersonation flow keeps its redirect (see SessionsController#logout).
14
+ match 'logout' => 'sessions#logout', via: %i[get post]
15
+ match 'back_to_parent' => 'sessions#back_to_parent', via: %i[get post]
12
16
  match 'forgot' => 'sessions#forgot', via: %i[get post patch]
13
- match 'confirm_email' => 'sessions#confirm_email', via: %i[get post path]
17
+ match 'confirm_email' => 'sessions#confirm_email', via: %i[get post patch]
14
18
  match 'register' => 'sessions#register', via: %i[get post patch]
15
19
  match 'api/:method', action: :api, via: %i[get post], as: :api
16
20
 
17
21
  resources :post_type, as: :post_type do
18
22
  resources :posts, controller: 'posts' do
19
23
  # resources :comments
20
- get :trash
21
- get :restore
24
+ # Security (audit M6): state-changing member actions must not ride GET links.
25
+ patch :trash
26
+ patch :restore
22
27
  collection do
23
28
  match 'ajax', via: %i[get post patch]
24
29
  end
@@ -41,7 +46,8 @@ Rails.application.routes.draw do
41
46
  match 'profile/edit' => 'users#profile_edit', via: %i[get post patch]
42
47
  resources :users, controller: 'users' do
43
48
  patch 'updated_ajax'
44
- get :impersonate, on: :member
49
+ # Security (audit M6): forces a session switch -- the highest-value CSRF target here.
50
+ post :impersonate, on: :member
45
51
  end
46
52
 
47
53
  resources :user_roles, controller: 'user_roles' do
@@ -53,11 +59,11 @@ Rails.application.routes.draw do
53
59
  collection do
54
60
  post 'get_items/:key', action: :get_items, as: :get_items
55
61
  post 'reorder'
56
- get 'list'
62
+ match 'list', via: %i[get post]
57
63
  end
58
64
  end
59
65
  get 'site'
60
- get 'test_email'
66
+ post 'test_email' # Security (audit M6): sends mail; must not be a CSRF-able GET
61
67
  get 'theme'
62
68
  post 'save_theme'
63
69
  get 'languages'
@@ -73,25 +79,33 @@ Rails.application.routes.draw do
73
79
  resources :comments, controller: 'comments' do
74
80
  get 'answer'
75
81
  post 'save_answer'
76
- get 'toggle_status'
82
+ patch 'toggle_status' # Security (audit M6): flips moderation state
77
83
  end
78
84
  end
79
85
 
80
86
  namespace :appearances do
81
- match 'widgets', via: %i[get delete]
87
+ # Security (audit M6): this legacy widgets surface predates the widgets/{main,sidebar,
88
+ # assign} controllers (its target controller is long gone) but still declared delete
89
+ # endpoints reachable over CSRF-exempt GET. The non-GET verbs keep the paths and helpers
90
+ # routable for any external binding.
91
+ delete 'widgets'
82
92
  match 'widgets_save', via: %i[post patch]
83
- match 'widget_delete', via: %i[get patch]
93
+ patch 'widget_delete'
84
94
  get 'render_form'
85
95
 
86
96
  resources :themes, only: [:index] do
87
97
  collection do
88
98
  get 'preview'
89
99
  # match "settings", via: [:get, :post, :patch]
90
- match 'load_data', via: %i[get post patch]
100
+ # Security (audit M6): load_data clears and re-imports post types, nav menus and sliders
101
+ # -- a state change that must not ride a CSRF-exempt GET link.
102
+ match 'load_data', via: %i[post patch]
91
103
  end
92
104
  end
93
105
  resources :nav_menus, except: :show do
94
- get 'item_delete/:id' => :delete_menu_item, as: :delete_menu_item
106
+ # Security (audit M6): destroys a menu item -- must not ride a CSRF-exempt GET. The
107
+ # admin JS sends a token-bearing DELETE (nav_menu.js).
108
+ delete 'item_delete/:id' => :delete_menu_item, as: :delete_menu_item
95
109
  get 'custom_settings/:id' => :custom_settings, as: :custom_settings
96
110
  post 'save_custom_settings/:id' => :save_custom_settings, as: :save_custom_settings
97
111
  get 'edit_menu_item/:id' => :edit_menu_item, as: :edit_menu_item
@@ -114,8 +128,10 @@ Rails.application.routes.draw do
114
128
  end
115
129
 
116
130
  resources :plugins, only: %i[index destroy] do
117
- get 'toggle', on: :collection
118
- get 'upgrade'
131
+ # Security (audit M6): activating/deactivating and upgrading a plugin run install and
132
+ # upgrade hooks -- state changes, not reads.
133
+ patch 'toggle', on: :collection
134
+ post 'upgrade'
119
135
  end
120
136
 
121
137
  # installer
@@ -125,7 +141,9 @@ Rails.application.routes.draw do
125
141
  end
126
142
 
127
143
  resources :media, only: [:index] do
128
- match 'crop', via: :all, on: :collection
144
+ # Security (audit M6): crop writes a cropped upload and can rewrite a user avatar
145
+ # (saved_avatar); via: :all admitted every verb, the CSRF-exempt GET/HEAD included.
146
+ post 'crop', on: :collection
129
147
  get 'ajax', on: :collection
130
148
  get 'download_private_file', on: :collection
131
149
  post 'upload', on: :collection
@@ -29,11 +29,12 @@ Rails.application.routes.draw do
29
29
  multilingual_segment = PluginRoutes.all_translations('routes.category', default: 'category')
30
30
  request.params[:category_id].match(/[0-9]+/) && request.params[:label_cat].in?(multilingual_segment)
31
31
  }
32
- get ':post_type_title/:label_cat/:category_id-:title/:slug' => :post, as: 'post_of_category_post_type', constraints: lambda { |request|
33
- multilingual_segment = PluginRoutes.all_translations('routes.category', default: 'category')
34
- request.params[:post_type_title].match(/^(?!(#{PluginRoutes.all_locales})$)[\w.-]+$/) &&
35
- request.params[:category_id].match(/[0-9]+/) && request.params[:label_cat].in?(multilingual_segment)
36
- }
32
+ get ':post_type_title/:label_cat/:category_id-:title/:slug' => :post, as: 'post_of_category_post_type',
33
+ constraints: lambda { |request|
34
+ multilingual_segment = PluginRoutes.all_translations('routes.category', default: 'category')
35
+ request.params[:post_type_title].match(/^(?!(#{PluginRoutes.all_locales})$)[\w.-]+$/) &&
36
+ request.params[:category_id].match(/[0-9]+/) && request.params[:label_cat].in?(multilingual_segment)
37
+ }
37
38
  get ':label/:post_tag_id-:title' => :post_tag, as: 'post_tag', constraints: lambda { |request|
38
39
  multilingual_segment = PluginRoutes.all_translations('routes.tag', default: 'tag')
39
40
  request.params[:post_tag_id].match(/[0-9]+/) && request.params[:label].in?(multilingual_segment)
@@ -44,12 +45,14 @@ Rails.application.routes.draw do
44
45
  }
45
46
  get ':label/:post_tag_slug' => :post_tag, as: 'post_tag_simple', constraints: lambda { |request|
46
47
  multilingual_segment = PluginRoutes.all_translations('routes.tag', default: 'tag')
47
- request.params[:post_tag_slug].match(%r{[a-zA-Z0-9_=\s\-/]+}) && request.params[:label].in?(multilingual_segment)
48
- }
49
- get ':label/:user_id-:user_name' => :profile, as: :profile, defaults: { label: 'profile' }, constraints: lambda { |request|
50
- multilingual_segment = PluginRoutes.all_translations('routes.profile', default: 'profile') | %w[profile]
51
- request.params[:user_id].match(/[0-9]+/) && request.params[:label].in?(multilingual_segment)
48
+ request.params[:post_tag_slug].match(%r{[a-zA-Z0-9_=\s\-/]+}) &&
49
+ request.params[:label].in?(multilingual_segment)
52
50
  }
51
+ get ':label/:user_id-:user_name' => :profile, as: :profile, defaults: { label: 'profile' },
52
+ constraints: lambda { |request|
53
+ multilingual_segment = PluginRoutes.all_translations('routes.profile', default: 'profile') | %w[profile]
54
+ request.params[:user_id].match(/[0-9]+/) && request.params[:label].in?(multilingual_segment)
55
+ }
53
56
  get ':label' => :search, as: :search, defaults: { label: 'search' }, constraints: lambda { |request|
54
57
  multilingual_segment = PluginRoutes.all_translations('routes.search', default: 'search') | %w[search]
55
58
  request.params[:label].in?(multilingual_segment)
@@ -71,11 +74,14 @@ Rails.application.routes.draw do
71
74
  # post types
72
75
  controller 'camaleon_cms/frontend' do
73
76
  PluginRoutes.get_sites.each do |s|
74
- s.post_types.pluck(:slug, :id).each do |pt_slug, pt_id|
75
- get ':post_type_slug' => :post_type, as: "post_type_#{pt_id}", post_type_id: pt_id, constraints: lambda { |request|
76
- multilingual_segment = PluginRoutes.all_translations("routes.post_types.#{pt_slug}", default: pt_slug)
77
- request.params[:post_type_slug].in?(multilingual_segment)
78
- }
77
+ # get_sites eager-loads :post_types, so map in memory instead of a per-site pluck query.
78
+ s.post_types.map { |pt| [pt.slug, pt.id] }.each do |pt_slug, pt_id|
79
+ get ':post_type_slug' => :post_type, as: "post_type_#{pt_id}", post_type_id: pt_id,
80
+ constraints: lambda { |request|
81
+ multilingual_segment =
82
+ PluginRoutes.all_translations("routes.post_types.#{pt_slug}", default: pt_slug)
83
+ request.params[:post_type_slug].in?(multilingual_segment)
84
+ }
79
85
  end
80
86
  end
81
87
  end
@@ -0,0 +1,86 @@
1
+ module CamaleonCms
2
+ # Helper used by config/initializers/assets.rb to declare plugin/theme assets as
3
+ # precompiled.
4
+ #
5
+ # Plugin and theme assets can live in three different layouts:
6
+ # * host app: <app>/app/apps/{plugins,themes}/<key>/assets/...
7
+ # * the camaleon_cms gem: <gem>/app/apps/{plugins,themes}/<key>/assets/...
8
+ # * separate "gem_mode" gems: <gem>/app/assets/<type>/{plugins,themes}/<key>/...
9
+ # (e.g. cama_contact_form ships app/assets/javascripts/plugins/cama_contact_form/...)
10
+ #
11
+ # Rails' default Sprockets precompile rules only cover the app/assets top level, so none
12
+ # of these are declared as precompiled. On modern Sprockets (>= 4) with
13
+ # `config.assets.unknown_asset_fallback = false`, requesting such an asset (e.g. via
14
+ # `javascript_include_tag "plugins/visibility_post/assets/js/form.js"`) raises
15
+ # `Sprockets::Rails::Helper::AssetNotPrecompiledError` unless it is declared.
16
+ #
17
+ # We enumerate the concrete asset files from the *real* Sprockets load paths and declare
18
+ # their exact logical paths as strings. Strings are the canonical, Sprockets-version
19
+ # agnostic form (work on both 3.x and 4.x). Scanning `config.assets.paths` (instead of a
20
+ # couple of hardcoded directories) ensures host, gem-bundled and separately gem-packaged
21
+ # (gem_mode) plugins/themes are all covered, matching the reach of the old precompile
22
+ # `proc` this replaced. Because the declaration is computed from the on-disk files, every
23
+ # installed plugin/theme is declared regardless of whether it is currently active, so
24
+ # switching themes / enabling-disabling plugins (DB-only operations) never requires a
25
+ # recompile.
26
+ module AssetsPrecompile
27
+ PRECOMPILE_EXTENSIONS = /\.(css|js|svg|ttf|woff|woff2|eot|otf|swf|pdf|png|jpe?g|gif|ico|mp3|mp4|webm|ogg|webp)\z/i
28
+ PRECOMPILE_CONTENT_TYPES = %r{(javascript|image/|audio|video|font)}
29
+
30
+ module_function
31
+
32
+ # path: logical asset path relative to an asset load path, e.g.
33
+ # "plugins/visibility_post/assets/js/form.js", "themes/my_theme/assets/css/app.css"
34
+ # or "plugins/cama_contact_form/admin_editor.js" (gem_mode layout).
35
+ # Returns true when the asset must be precompiled.
36
+ def match?(path)
37
+ dirname = File.dirname(path)
38
+ return false unless dirname.start_with?('plugins/', 'themes/')
39
+
40
+ name = File.basename(path)
41
+ return false if name.start_with?('_') || path.include?('/views/')
42
+
43
+ PRECOMPILE_EXTENSIONS.match?(path) || content_type_for(name).scan(PRECOMPILE_CONTENT_TYPES).any?
44
+ end
45
+
46
+ def content_type_for(name)
47
+ MIME::Types.type_for(name).first.content_type
48
+ rescue StandardError
49
+ ''
50
+ end
51
+
52
+ # Enumerate the logical paths of every plugin/theme asset found under the given asset
53
+ # roots. For each root, only the `plugins/` and `themes/` subtrees are scanned, and each
54
+ # file is converted to its logical path (relative to the root) and filtered by `match?`.
55
+ def logical_paths(asset_roots = default_asset_roots)
56
+ asset_roots.flat_map do |root|
57
+ root = root.to_s
58
+ next [] if root.empty? || !Dir.exist?(root)
59
+
60
+ prefix = "#{root.chomp('/')}/"
61
+ Dir.glob(File.join(root, '{plugins,themes}', '**', '*')).filter_map do |file|
62
+ next unless File.file?(file)
63
+
64
+ logical = file.delete_prefix(prefix)
65
+ logical if match?(logical)
66
+ end
67
+ end.uniq
68
+ end
69
+
70
+ # Asset roots to scan: the configured Sprockets load paths (which cover host,
71
+ # gem-bundled and gem_mode plugin/theme assets), plus the host and camaleon_cms gem
72
+ # `app/apps` folders as a fallback in case the asset paths are not yet populated when
73
+ # this is called during initialization.
74
+ def default_asset_roots
75
+ roots = []
76
+ if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
77
+ roots.concat(Array(Rails.application.config.assets.paths).map(&:to_s))
78
+ end
79
+ roots << Rails.root.join('app/apps').to_s if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
80
+ # rubocop:disable Style/GlobalVars
81
+ roots << File.join($camaleon_engine_dir, 'app', 'apps') if defined?($camaleon_engine_dir) && $camaleon_engine_dir
82
+ # rubocop:enable Style/GlobalVars
83
+ roots.uniq
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CamaleonCms
4
+ # Captcha challenge/image generation shared by the two entry points that expose it:
5
+ # CamaleonCms::RuntimeCaptchaImageConcern (the controller stack serving GET /captcha) and
6
+ # CamaleonCms::CaptchaHelper (views and the runtime helper surface). Both include this
7
+ # module, so a captcha hardening fix cannot land in one entry point and silently miss the
8
+ # copy that actually runs. Parity between the entry points is guarded by
9
+ # spec/lib/camaleon_cms/captcha_implementation_parity_spec.rb.
10
+ module CaptchaImageGeneration
11
+ # The requested length is clamped so an attacker-supplied ?len= can neither make the
12
+ # worker build an arbitrarily large challenge string for ImageMagick to draw (H4
13
+ # resource exhaustion) nor shrink the answer space to a brute-forceable size (H3).
14
+ CAPTCHA_MIN_LENGTH = 4
15
+ CAPTCHA_MAX_LENGTH = 8
16
+ CAPTCHA_DEFAULT_LENGTH = 5
17
+
18
+ # build a captcha image
19
+ # @param [Integer, nil] len Number of characters to include in captcha (default: 5)
20
+ # @return [MiniMagick::Image]
21
+ def cama_captcha_build(len = CAPTCHA_DEFAULT_LENGTH)
22
+ img = MiniMagick::Image.open(resolve_captcha_file("captcha_#{rand(12)}.jpg"))
23
+ text = cama_rand_str(cama_captcha_length(len))
24
+ # Single active challenge: replace, never accumulate. An append-only list let any previously
25
+ # issued answer keep verifying, which (with a shrinkable length) made the captcha bypassable (H3).
26
+ session[:cama_captcha] = [text]
27
+ img.combine_options do |c|
28
+ c.gravity('Center')
29
+ c.fill('#FFFFFF')
30
+ c.draw("text 0,5 #{text}")
31
+ c.font(resolve_captcha_file('bumpyroad.ttf'))
32
+ c.pointsize('30')
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # Clamp a requested captcha length into the safe range; a non-numeric or absent value
39
+ # falls back to the default.
40
+ def cama_captcha_length(len)
41
+ return CAPTCHA_DEFAULT_LENGTH unless len.to_s.match?(/\A\d+\z/)
42
+
43
+ len.to_i.clamp(CAPTCHA_MIN_LENGTH, CAPTCHA_MAX_LENGTH)
44
+ end
45
+
46
+ def resolve_captcha_file(filename)
47
+ base_dir = $camaleon_engine_dir.presence || Rails.root.to_s # rubocop:disable Style/GlobalVars
48
+ File.join(base_dir, 'lib', 'captcha', filename)
49
+ end
50
+
51
+ # generate random string for captcha
52
+ # len: length of characters, default 6
53
+ def cama_rand_str(len = 6)
54
+ alphabets = [('A'..'Z').to_a].flatten!
55
+ alphanumerics = [('A'..'Z').to_a, ('1'..'9').to_a].flatten!
56
+ # Security (audit Low): draw from a CSPRNG. Kernel#rand is a Mersenne-Twister PRNG whose future
57
+ # output is predictable from observed samples, so a seen challenge weakened the next one.
58
+ str = alphabets[SecureRandom.random_number(alphabets.size)]
59
+ (len.to_i - 1).times do
60
+ str << alphanumerics[SecureRandom.random_number(alphanumerics.size)]
61
+ end
62
+ str
63
+ end
64
+ end
65
+ end