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
@@ -4,10 +4,20 @@ module CamaleonCms
4
4
  # default structure for each new site
5
5
  def default_settings
6
6
  default_post_type = [
7
- { name: 'Post', description: 'Posts',
8
- options: { has_category: true, has_tags: true, not_deleted: true, has_summary: true, has_content: true, has_comments: true, has_picture: true, has_template: true } },
9
- { name: 'Page', description: 'Pages',
10
- options: { has_category: false, has_tags: false, not_deleted: true, has_summary: false, has_content: true, has_comments: false, has_picture: true, has_template: true, has_layout: true } }
7
+ {
8
+ name: 'Post', description: 'Posts',
9
+ options: {
10
+ has_category: true, has_tags: true, not_deleted: true, has_summary: true, has_content: true,
11
+ has_comments: true, has_picture: true, has_template: true
12
+ }
13
+ },
14
+ {
15
+ name: 'Page', description: 'Pages',
16
+ options: {
17
+ has_category: false, has_tags: false, not_deleted: true, has_summary: false, has_content: true,
18
+ has_comments: false, has_picture: true, has_template: true, has_layout: true
19
+ }
20
+ }
11
21
  ]
12
22
  default_post_type.each do |pt|
13
23
  post_types.create({ name: pt[:name], slug: pt[:name].to_s.parameterize, description: pt[:description],
@@ -17,20 +27,101 @@ module CamaleonCms
17
27
  # nav menus
18
28
  @nav_menu = nav_menus.new({ name: 'Main Menu', slug: 'main_menu' })
19
29
  if @nav_menu.save
20
- post_types.all.each do |pt|
30
+ post_types.find_each do |pt|
21
31
  if pt.slug == 'post'
22
32
  title = 'Sample Post'
23
33
  slug = 'sample-post'
24
- content = '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pharetra ut augue in posuere. Nulla non malesuada dui. Sed egestas tortor ut purus tempor sodales. Duis non sollicitudin nulla, quis mollis neque. Integer sit amet augue ac neque varius auctor. Vestibulum malesuada leo leo, at semper libero efficitur nec. Etiam semper nisi ac nisi ullamcorper, sed tincidunt purus elementum. Mauris ac congue nibh. Quisque pretium eget leo nec suscipit. </p> <p> Vestibulum ultrices orci ut congue interdum. Morbi dolor nunc, imperdiet vel risus semper, tempor dapibus urna. Phasellus luctus pharetra enim quis volutpat. Integer tristique urna nec malesuada ullamcorper. Curabitur dictum, lectus id ultrices rhoncus, ante neque auctor erat, ut sodales nisi odio sit amet lorem. In hac habitasse platea dictumst. Quisque orci orci, hendrerit at luctus tristique, lobortis in diam. Curabitur ligula enim, rhoncus ut vestibulum a, consequat sit amet nisi. Aliquam bibendum fringilla ultrices. Aliquam erat volutpat. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In justo mi, congue in rhoncus lobortis, facilisis in est. Nam et rhoncus purus. </p> <p> Sed sagittis auctor lectus at rutrum. Morbi ultricies felis mi, ut scelerisque augue facilisis eu. In molestie quam ex. Quisque ut sapien sed odio tempus imperdiet. In id accumsan massa. Morbi quis nunc ullamcorper, interdum enim eu, finibus purus. Vestibulum ac fermentum augue, at tempus ante. Aliquam ultrices, purus ut porttitor gravida, dui augue dignissim massa, ac tempor ante dolor at arcu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Suspendisse placerat risus est, eget varius mi ultricies in. Duis non odio ut felis dapibus eleifend. In fringilla enim lobortis placerat efficitur. </p> <p> Nulla sodales faucibus urna, quis viverra dolor facilisis sollicitudin. Aenean ac egestas nibh. Nam non tortor eget nibh scelerisque fermentum. Etiam ornare, nunc ut luctus mollis, ante dolor consectetur augue, non scelerisque odio est a nulla. Nullam cursus egestas nulla, nec commodo nibh suscipit ut. Mauris ut felis sem. Aenean at mi at nisi dictum blandit sit amet at erat. Etiam eget lobortis tellus. Curabitur in commodo arcu, at vehicula tortor. </p>'
34
+ content_parts = []
35
+ content_parts << helpers.content_tag(
36
+ :p, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer pharetra ut augue in posuere. ' \
37
+ 'Nulla non malesuada dui. Sed egestas tortor ut purus tempor sodales. ' \
38
+ 'Duis non sollicitudin nulla, quis mollis neque. Integer sit amet augue ac neque varius auctor. ' \
39
+ 'Vestibulum malesuada leo leo, at semper libero efficitur nec. ' \
40
+ 'Etiam semper nisi ac nisi ullamcorper, sed tincidunt purus elementum. ' \
41
+ 'Mauris ac congue nibh. Quisque pretium eget leo nec suscipit.'
42
+ )
43
+ content_parts << helpers.content_tag(
44
+ :p, 'Vestibulum ultrices orci ut congue interdum. ' \
45
+ 'Morbi dolor nunc, imperdiet vel risus semper, tempor dapibus urna. ' \
46
+ 'Phasellus luctus pharetra enim quis volutpat. Integer tristique urna nec malesuada ullamcorper. ' \
47
+ 'Curabitur dictum, lectus id ultrices rhoncus, ante neque auctor erat, ' \
48
+ 'ut sodales nisi odio sit amet lorem. In hac habitasse platea dictumst. ' \
49
+ 'Quisque orci orci, hendrerit at luctus tristique, lobortis in diam. ' \
50
+ 'Curabitur ligula enim, rhoncus ut vestibulum a, consequat sit amet nisi. ' \
51
+ 'Aliquam bibendum fringilla ultrices. Aliquam erat volutpat. ' \
52
+ 'Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; ' \
53
+ 'In justo mi, congue in rhoncus lobortis, facilisis in est. Nam et rhoncus purus.'
54
+ )
55
+ content_parts << helpers.content_tag(
56
+ :p, 'Sed sagittis auctor lectus at rutrum. ' \
57
+ 'Morbi ultricies felis mi, ut scelerisque augue facilisis eu. In molestie quam ex. ' \
58
+ 'Quisque ut sapien sed odio tempus imperdiet. In id accumsan massa. ' \
59
+ 'Morbi quis nunc ullamcorper, interdum enim eu, finibus purus. ' \
60
+ 'Vestibulum ac fermentum augue, at tempus ante. ' \
61
+ 'Aliquam ultrices, purus ut porttitor gravida, dui augue dignissim massa, ' \
62
+ 'ac tempor ante dolor at arcu. ' \
63
+ 'Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. ' \
64
+ 'Suspendisse placerat risus est, eget varius mi ultricies in. ' \
65
+ 'Duis non odio ut felis dapibus eleifend. In fringilla enim lobortis placerat efficitur.'
66
+ )
67
+ content_parts << helpers.content_tag(
68
+ :p, 'Nulla sodales faucibus urna, quis viverra dolor facilisis sollicitudin. Aenean ac egestas nibh. ' \
69
+ 'Nam non tortor eget nibh scelerisque fermentum. ' \
70
+ 'Etiam ornare, nunc ut luctus mollis, ante dolor consectetur augue, ' \
71
+ 'non scelerisque odio est a nulla. Nullam cursus egestas nulla, nec commodo nibh suscipit ut. ' \
72
+ 'Mauris ut felis sem. Aenean at mi at nisi dictum blandit sit amet at erat. ' \
73
+ 'Etiam eget lobortis tellus. Curabitur in commodo arcu, at vehicula tortor.'
74
+ )
75
+ content = helpers.safe_join(content_parts)
25
76
  else
26
77
  title = 'Welcome'
27
78
  slug = 'welcome'
28
- content = "<p style='text-align: center;'><img width='155' height='155' src='https://camaleon.website/media/132/logo2.png' alt='logo' /></p><p><strong>Camaleon CMS</strong>&nbsp;is a free and open-source tool and a fexible content management system (CMS) based on <a href='https://rubyonrails.org'>Ruby on Rails</a>.</p> <p>With Camaleon you can do the following:</p> <ul> <li>Create instantly a lot of sites&nbsp;in the same installation</li> <li>Manage your content information in several languages</li> <li>Extend current functionality by&nbsp;plugins (MVC structure and no more echo or prints anywhere)</li> <li>Create or install different themes for each site</li> <li>Create your own structure without coding anything (adapt Camaleon as you want&nbsp;and not you for Camaleon)</li> <li>Create your store and start to sell your products using our plugins</li> <li>Avoid web attacks</li> <li>Compare the speed and enjoy the speed of your new Camaleon site</li> <li>Customize or create your themes for mobile support</li> <li>Support&nbsp;more visitors at the same time</li> <li>Manage your information with a panel like wordpress&nbsp;</li> <li>All urls are oriented for SEO</li> <li>Multiples roles of users</li> </ul>"
79
+ welcome_parts = []
80
+ logo_img = helpers
81
+ .image_tag('https://camaleon.website/media/132/logo2.png', width: 155, height: 155, alt: 'logo')
82
+ welcome_parts << helpers.content_tag(:p, logo_img, style: 'text-align: center;')
83
+ cms_strong = helpers.content_tag(:strong, 'Camaleon CMS')
84
+ rails_link = helpers.link_to('Ruby on Rails', 'https://rubyonrails.org')
85
+ part2_text = helpers.safe_join(
86
+ [
87
+ cms_strong,
88
+ ' is a free and open-source tool and a fexible content management system (CMS) based on ',
89
+ rails_link
90
+ ]
91
+ )
92
+ welcome_parts << helpers.content_tag(:p, part2_text)
93
+ welcome_parts << helpers.content_tag(:p, 'With Camaleon you can do the following:')
94
+ li_texts = [
95
+ 'Create instantly a lot of sites in the same installation',
96
+ 'Manage your content information in several languages',
97
+ 'Extend current functionality by plugins (MVC structure and no more echo or prints anywhere)',
98
+ 'Create or install different themes for each site',
99
+ 'Create your own structure without coding anything (adapt Camaleon as you want and not you for Camaleon)',
100
+ 'Create your store and start to sell your products using our plugins',
101
+ 'Avoid web attacks',
102
+ 'Compare the speed and enjoy the speed of your new Camaleon site',
103
+ 'Customize or create your themes for mobile support',
104
+ 'Support more visitors at the same time',
105
+ 'Manage your information with a panel like wordpress ',
106
+ 'All urls are oriented for SEO',
107
+ 'Multiples roles of users'
108
+ ]
109
+ li_tags = li_texts.map { |text| helpers.content_tag(:li, text) }
110
+ welcome_parts << helpers.content_tag(:ul, helpers.safe_join(li_tags))
111
+ content = helpers.safe_join(welcome_parts)
29
112
  end
30
113
  user = users.admin_scope.first
31
- unless user.present?
32
- user = users.admin_scope.create({ email: 'admin@local.com', username: 'admin', password: 'admin123',
33
- password_confirmation: 'admin123', first_name: 'Administrator' })
114
+ if user.blank?
115
+ # Never mint a known default password. Generate a random one, force a change on first
116
+ # login, and stash the plaintext transiently so the provisioning controller can surface
117
+ # it once. See harden-installer-default-admin.
118
+ generated_password = SecureRandom.alphanumeric(16)
119
+ user = users.admin_scope.create({ email: 'admin@local.com', username: 'admin',
120
+ password: generated_password,
121
+ password_confirmation: generated_password,
122
+ first_name: 'Administrator' })
123
+ user.set_meta('must_change_password', true)
124
+ self.generated_admin_password = generated_password
34
125
  end
35
126
  post = pt.add_post({ title: title, slug: slug, content: content, user_id: user.id, status: 'published' })
36
127
  @nav_menu.append_menu_item({ label: title, type: 'post', link: post.id })
@@ -60,18 +151,26 @@ module CamaleonCms
60
151
  if post_type.present?
61
152
  d = user_role.get_meta("_post_type_#{id}", {})
62
153
  CamaleonCms::UserRole::ROLES[:post_type].each do |value|
154
+ # post_content_unfiltered_html lets a role store unsanitized HTML in post content; it must not be
155
+ # granted to the default Editor role (only admins, via `can :manage, :all`, are trusted for it).
156
+ next if value[:key].to_s == 'post_content_unfiltered_html'
157
+
63
158
  value_old = d[value[:key].to_sym] || []
64
159
  d[value[:key].to_sym] = value_old + [post_type.id]
65
160
  end
66
161
  else
67
162
  pts = post_types.all.pluck(:id)
68
- CamaleonCms::UserRole::ROLES[:post_type].each { |value| d[value[:key]] = pts }
163
+ CamaleonCms::UserRole::ROLES[:post_type].each do |value|
164
+ next if value[:key].to_s == 'post_content_unfiltered_html'
165
+
166
+ d[value[:key]] = pts
167
+ end
69
168
  end
70
169
  user_role.set_meta("_post_type_#{id}", d || {})
71
170
  end
72
171
 
73
- user_role = user_roles.where({ slug: 'contributor' }).first_or_create({ name: 'Contributor',
74
- description: 'Contributor Role' })
172
+ user_role = user_roles.where({ slug: 'contributor' })
173
+ .first_or_create({ name: 'Contributor', description: 'Contributor Role' })
75
174
  if user_role.valid?
76
175
  d = {}
77
176
  if post_type.present?
@@ -89,13 +188,18 @@ module CamaleonCms
89
188
 
90
189
  return if post_type.present?
91
190
 
92
- user_role = user_roles.where({ slug: 'client',
93
- term_group: -1 }).first_or_create({ name: 'Client',
94
- description: 'Default roles client' })
191
+ user_role = user_roles.where({ slug: 'client', term_group: -1 })
192
+ .first_or_create({ name: 'Client', description: 'Default roles client' })
95
193
  return unless user_role.valid?
96
194
 
97
195
  user_role.set_meta("_post_type_#{id}", {})
98
196
  user_role.set_meta("_manager_#{id}", {})
99
197
  end
198
+
199
+ private
200
+
201
+ def helpers
202
+ ActionController::Base.helpers
203
+ end
100
204
  end
101
205
  end
@@ -8,12 +8,20 @@ module CamaleonCms
8
8
 
9
9
  extend CamaleonCms::NormalizeAttrs
10
10
 
11
- validates_uniqueness_of :username, scope: [:site_id], case_sensitive: false,
12
- message: I18n.t('camaleon_cms.admin.users.message.requires_different_username', default: 'Requires different username')
13
- validates_uniqueness_of :email, scope: [:site_id], case_sensitive: false,
14
- message: I18n.t('camaleon_cms.admin.users.message.requires_different_email', default: 'Requires different email')
15
-
16
- normalize_attrs(:first_name, :last_name, :username)
11
+ validates(
12
+ :username, uniqueness: { scope: [:site_id], case_sensitive: false,
13
+ message: I18n.t(
14
+ 'camaleon_cms.admin.users.message.requires_different_username',
15
+ default: 'Requires different username'
16
+ ) }
17
+ )
18
+ validates(
19
+ :email, uniqueness: { scope: [:site_id], case_sensitive: false,
20
+ message: I18n.t(
21
+ 'camaleon_cms.admin.users.message.requires_different_email',
22
+ default: 'Requires different email'
23
+ ) }
24
+ )
17
25
 
18
26
  # callbacks
19
27
  before_validation :cama_before_validation
@@ -24,8 +32,12 @@ module CamaleonCms
24
32
  before_update { generate_token :auth_token if will_save_change_to_password_digest? }
25
33
 
26
34
  # relations
27
- has_many :all_posts, class_name: 'CamaleonCms::Post', foreign_key: :user_id
28
- has_many :all_comments, class_name: 'CamaleonCms::PostComment'
35
+ has_many :all_posts, class_name: 'CamaleonCms::Post', foreign_key: :user_id, inverse_of: :owner,
36
+ dependent: :nullify
37
+ # No `dependent:` — the rows must still carry this user_id when the
38
+ # `after_destroy :reassign_comments` callback moves them to the anonymous user.
39
+ has_many :all_comments, class_name: 'CamaleonCms::PostComment' # rubocop:disable Rails/HasManyOrHasOneDependent
40
+
29
41
  belongs_to :site, class_name: 'CamaleonCms::Site', optional: true
30
42
 
31
43
  # scopes
@@ -64,9 +76,9 @@ module CamaleonCms
64
76
  @_user_role ||= site.user_roles.where(slug: role).first
65
77
  end
66
78
 
67
- # assign a new site for current user
79
+ # Assign a new site for the current user
68
80
  def assign_site(site)
69
- update_column(:site_id, site.id)
81
+ update_column(:site_id, site.id) # rubocop:disable Rails/SkipsModelValidations
70
82
  end
71
83
 
72
84
  def sites
@@ -93,6 +105,14 @@ module CamaleonCms
93
105
  end
94
106
  end
95
107
 
108
+ # Rotate the auth token and persist it (audit 2026-08-11 M3): called on logout so a cookie copied
109
+ # before logout can no longer authenticate. Skips validations/callbacks -- it only replaces the
110
+ # token column and must work even for a record that would not otherwise pass validation.
111
+ def cama_reset_auth_token!
112
+ generate_token(:auth_token)
113
+ update_column(:auth_token, auth_token) # rubocop:disable Rails/SkipsModelValidations
114
+ end
115
+
96
116
  def send_password_reset
97
117
  generate_token(:password_reset_token)
98
118
  self.password_reset_sent_at = Time.zone.now
@@ -126,20 +146,25 @@ module CamaleonCms
126
146
  all_posts.each do |p|
127
147
  s = p.post_type.site
128
148
  u = s.users.admin_scope.where.not(id: id).first
129
- next unless u.present?
149
+ next if u.blank?
130
150
 
131
- p.update_column(:user_id, u.id)
132
- p.comments.where(user_id: id).each do |c|
133
- c.update_column(:user_id, u.id)
134
- end
151
+ p.update_column(:user_id, u.id) # rubocop:disable Rails/SkipsModelValidations
152
+ p.comments.where(user_id: id).update_all(user_id: u.id) # rubocop:disable Rails/SkipsModelValidations
135
153
  end
136
154
  end
137
155
 
156
+ # Until the dependent: :nullify on all_comments was removed this loop never ran — the
157
+ # association was already empty by the time after_destroy fired — so the unguarded chain
158
+ # below was never exercised. It runs now, and a comment whose post, post type or site cannot
159
+ # be resolved would raise inside after_destroy and roll the whole user deletion back. Such a
160
+ # comment is skipped instead; `rake camaleon_cms:reassign_orphaned_comments` reports the same
161
+ # rows, and it already guarded the identical chain.
138
162
  def reassign_comments
139
- all_comments.includes(post: { post_type: :site }).each do |comment|
140
- site = comment.post.post_type.site
141
- user = site.get_anonymous_user
142
- comment.update_column(:user_id, user.id)
163
+ all_comments.includes(post: { post_type: :site }).find_each do |comment|
164
+ site = comment.post&.post_type&.site
165
+ next if site.nil?
166
+
167
+ comment.update_column(:user_id, site.get_anonymous_user.id) # rubocop:disable Rails/SkipsModelValidations
143
168
  end
144
169
  end
145
170
  end
@@ -12,5 +12,12 @@
12
12
  # Note: Rails automatically resets CurrentAttributes between requests, ensuring no leakage.
13
13
  # In specs, call CurrentRequest.reset in before/after hooks to avoid cross-test pollution.
14
14
  class CurrentRequest < ActiveSupport::CurrentAttributes
15
- attribute :user, :site
15
+ attribute :user, :user_resolved, :site, :content_helper_state, :hooks_helper_state, :html_helper_state,
16
+ :theme_helper_state,
17
+ :frontend_object, :frontend_seo_settings, :frontend_current_theme, :frontend_site_current_path,
18
+ :frontend_visited_home, :frontend_visited_post, :frontend_visited_ajax, :frontend_visited_search,
19
+ :frontend_visited_post_type, :frontend_visited_tag, :frontend_visited_category,
20
+ :frontend_visited_profile, :frontend_user,
21
+ :admin_menu_items, :custom_field_elements, :extra_models_for_fields,
22
+ :shortcodes, :shortcodes_template, :shortcodes_descr
16
23
  end
@@ -72,9 +72,7 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
72
72
  'dimension' => ''
73
73
  }.with_indifferent_access
74
74
  if res['file_type'] == 'image' && File.extname(res['name']).downcase != '.gif'
75
- res['thumb'] =
76
- version_path(res['url']).sub('.svg',
77
- '.jpg')
75
+ res['thumb'] = version_path(res['url']).sub(SVG_EXT_PATTERN, '.jpg')
78
76
  end
79
77
  res['key'] = File.join(res['folder_path'], res['name'])
80
78
  @aws_settings[:aws_file_read_settings].call(res, s3_file)
@@ -100,14 +98,20 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
100
98
  end
101
99
 
102
100
  s3_file = bucket.object(key.slice(1..-1))
101
+ # Security (audit 2026-08-11 M5): a private-mode upload must be owner-only. Storing it 'public-read'
102
+ # (like a public file) left it world-readable at a guessable s3://bucket/private/<name> URL,
103
+ # bypassing the download_private_file gate. That gate fetches via the authenticated S3 API, so a
104
+ # 'private' ACL does not affect legitimate serving.
105
+ acl = is_private_uploader? ? 'private' : 'public-read'
103
106
  s3_file.upload_file(
104
- uploaded_io_or_file_path.is_a?(String) ? uploaded_io_or_file_path : uploaded_io_or_file_path.path, @aws_settings[:aws_file_upload_settings].call({ acl: 'public-read' })
107
+ uploaded_io_or_file_path.is_a?(String) ? uploaded_io_or_file_path : uploaded_io_or_file_path.path,
108
+ @aws_settings[:aws_file_upload_settings].call({ acl: acl })
105
109
  )
106
110
  res = cache_item(file_parse(s3_file)) unless args[:is_thumb]
107
111
  res
108
112
  end
109
113
 
110
- # add new folder to AWS with :key
114
+ # Add a new folder to AWS with :key
111
115
  def add_folder(key)
112
116
  return { error: 'Invalid folder path' } unless valid_folder_path?(key)
113
117
 
@@ -118,17 +122,17 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
118
122
  cache_item(file_parse(s3_file))
119
123
  end
120
124
 
121
- # delete a folder in AWS with :key
125
+ # Delete a folder in AWS with :key
122
126
  def delete_folder(key)
123
127
  return { error: 'Invalid folder path' } unless valid_folder_path?(key)
124
128
 
125
129
  key = "#{@aws_settings['inner_folder']}/#{key}" if @aws_settings['inner_folder'].present?
126
130
  key = key.cama_fix_media_key
127
131
  bucket.objects(prefix: key.slice(1..-1) << '/').delete
128
- get_media_collection.find_by_key(key).take.destroy
132
+ get_media_collection.by_key(key).take.destroy
129
133
  end
130
134
 
131
- # delete a file in AWS with :key
135
+ # Delete the `:key` file in AWS
132
136
  def delete_file(key)
133
137
  return { error: 'Invalid file path' } unless valid_folder_path?(key)
134
138
 
@@ -140,10 +144,27 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
140
144
  ''
141
145
  end
142
146
  @instance.hooks_run('after_delete', key)
143
- get_media_collection.find_by_key(key).take.destroy
147
+ get_media_collection.by_key(key).take.destroy
144
148
  end
145
149
 
146
- # initialize a bucket with AWS configurations
150
+ # Security (audit 2026-08-11 M5 follow-up): uploads stored before the private-ACL fix kept a
151
+ # world-readable ACL under the private prefix. Sweep them back to owner-only and return the count.
152
+ # The prefix mirrors setup_private_folder -- <inner_folder>/private/ -- normalized exactly like
153
+ # add_file keys (leading slash added by cama_fix_media_key, then stripped for S3), with a trailing
154
+ # slash so a sibling folder such as private_x is not swept.
155
+ def repair_private_acls!
156
+ inner_folder = @aws_settings['inner_folder'].to_s
157
+ inner_folder = "#{inner_folder}/#{PRIVATE_DIRECTORY}" unless is_private_uploader?
158
+ prefix = "#{inner_folder.cama_fix_media_key.slice(1..-1)}/"
159
+ repaired = 0
160
+ bucket.objects(prefix: prefix).each do |object|
161
+ object.acl.put(acl: 'private')
162
+ repaired += 1
163
+ end
164
+ repaired
165
+ end
166
+
167
+ # Initialize a bucket with AWS configurations
147
168
  # return: (AWS Bucket object)
148
169
  def bucket
149
170
  @bucket ||= lambda {
@@ -2,13 +2,13 @@ class CamaleonCmsLocalUploader < CamaleonCmsUploader
2
2
  def after_initialize
3
3
  @root_folder = @current_site.upload_directory
4
4
 
5
- FileUtils.mkdir_p(@root_folder) unless Dir.exist?(@root_folder)
5
+ FileUtils.mkdir_p(@root_folder)
6
6
  end
7
7
 
8
8
  def setup_private_folder
9
9
  @root_folder = Rails.root.join(self.class::PRIVATE_DIRECTORY).to_s
10
10
 
11
- FileUtils.mkdir_p(@root_folder) unless Dir.exist?(@root_folder)
11
+ FileUtils.mkdir_p(@root_folder)
12
12
  end
13
13
 
14
14
  def browser_files(prefix = '/', _objects = {})
@@ -32,6 +32,17 @@ class CamaleonCmsLocalUploader < CamaleonCmsUploader
32
32
  { error: 'File not found' }
33
33
  end
34
34
 
35
+ # Repairs legacy thumbnail URLs (see #cama_compat_legacy_thumb) for one rendered page of
36
+ # media items. The cache (media DB records) may still hold thumb URLs computed from the
37
+ # source extension (sample: ".jpg") while the on-disk thumbnail is a legacy ".png"; without
38
+ # this the admin media browser would render 404 thumbnails for such files. Applied by the
39
+ # media controller to the paginated page only — #objects stays a lazy relation so the browser
40
+ # paginates at the database instead of materializing (and stat-ing) the whole folder.
41
+ def cama_prepare_browser_page(items)
42
+ items.each { |item| cama_fix_legacy_thumb_item(item) }
43
+ items
44
+ end
45
+
35
46
  def file_parse(key)
36
47
  file_path = File.join(@root_folder, key)
37
48
  url_path = file_path.sub(Rails.root.join('public').to_s, '')
@@ -41,16 +52,13 @@ class CamaleonCmsLocalUploader < CamaleonCmsUploader
41
52
  'folder_path' => File.dirname(key),
42
53
  'url' => if is_dir
43
54
  ''
55
+ elsif is_private_uploader?
56
+ url_path.sub("#{@root_folder}/", '')
44
57
  else
45
- (if is_private_uploader?
46
- url_path.sub("#{@root_folder}/",
47
- '')
48
- else
49
- File.join(
50
- @current_site.decorate.the_url(as_path: true, locale: false,
51
- skip_relative_url_root: true), url_path
52
- )
53
- end)
58
+ File.join(
59
+ @current_site.decorate.the_url(as_path: true, locale: false,
60
+ skip_relative_url_root: true), url_path
61
+ )
54
62
  end,
55
63
  'is_folder' => is_dir,
56
64
  'file_size' => is_dir ? 0 : File.size(file_path).round(2),
@@ -60,11 +68,16 @@ class CamaleonCmsLocalUploader < CamaleonCmsUploader
60
68
  }.with_indifferent_access
61
69
  res['key'] = File.join(res['folder_path'], res['name'])
62
70
  if res['file_type'] == 'image' && File.extname(file_path).downcase != '.gif'
63
- res['thumb'] =
64
- (is_private_uploader? ? "/admin/media/download_private_file?file=#{version_path(key).slice(1..-1)}" : version_path(res['url']))
71
+ res['thumb'] = if is_private_uploader?
72
+ "/admin/media/download_private_file?file=#{version_path(key).slice(1..-1)}"
73
+ else
74
+ version_path(res['url'])
75
+ end
65
76
  end
66
77
  if res['file_type'] == 'image'
67
- res['thumb'].sub! '.svg', '.jpg'
78
+ res['thumb'].sub!(SVG_EXT_PATTERN, '.jpg')
79
+ res['thumb'] = cama_compat_legacy_thumb(res['thumb'], version_path(key).sub(SVG_EXT_PATTERN, '.jpg'),
80
+ res['url'])
68
81
  im = MiniMagick::Image.open(file_path)
69
82
  res['dimension'] = begin
70
83
  "#{im[:width]}x#{im[:height]}"
@@ -109,27 +122,71 @@ class CamaleonCmsLocalUploader < CamaleonCmsUploader
109
122
  f
110
123
  end
111
124
 
112
- # remove an existent folder
125
+ # Remove an existent folder
113
126
  def delete_folder(key)
114
127
  return { error: 'Invalid folder path' } if key.include?('..')
115
128
 
116
129
  folder = File.join(@root_folder, key)
117
130
  FileUtils.rm_rf(folder) if Dir.exist? folder
118
- get_media_collection.find_by_key(key).take.destroy
131
+ get_media_collection.by_key(key).take.destroy
119
132
  end
120
133
 
121
- # remove an existent file
134
+ # Remove an existent file
122
135
  def delete_file(key)
123
136
  return { error: 'Invalid file path' } if key.include?('..')
124
137
 
125
138
  file = File.join(@root_folder, key)
126
139
  FileUtils.rm(file) if File.exist? file
127
140
  @instance.hooks_run('after_delete', key)
128
- get_media_collection.find_by_key(key).take.destroy
141
+ get_media_collection.by_key(key).take.destroy
129
142
  end
130
143
 
131
- # convert a real file path into file key
144
+ # Convert a real file path into a file key
132
145
  def parse_key(file_path)
133
146
  file_path.sub(@root_folder, '').cama_fix_media_key
134
147
  end
148
+
149
+ private
150
+
151
+ # Applies the legacy-thumbnail fallback (see #cama_compat_legacy_thumb) to a
152
+ # single cached media item in place, so the admin media browser renders the
153
+ # on-disk ".png" thumbnail instead of a 404ing ".jpg" one. No-op for folders,
154
+ # non-image files and items without a thumb.
155
+ # item: (CamaleonCms::Media | Hash) cached media object
156
+ def cama_fix_legacy_thumb_item(item)
157
+ return if item['is_folder'] || item['file_type'] != 'image' || item['thumb'].blank?
158
+
159
+ thumb_key = version_path(File.join(item['folder_path'].to_s, item['name'].to_s))
160
+ item['thumb'] = cama_compat_legacy_thumb(item['thumb'], thumb_key, item['url'])
161
+ end
162
+
163
+ # Backwards-compatibility for legacy thumbnails.
164
+ # Older Camaleon releases stored raster thumbnails as PNG regardless of the
165
+ # source extension (sample: "photo.jpg" => "thumb/photo-jpg.png"). The thumb
166
+ # URL is normally derived from the source extension, so for such legacy files
167
+ # the computed (sample: ".jpg") thumb URL would 404. Resolution order when the
168
+ # computed thumb is missing on disk:
169
+ # 1. a ".png" sibling exists => point the thumb at the PNG;
170
+ # 2. no thumbnail exists at all (sample: ".ico" favicons are never
171
+ # thumbnailed) => fall back to the original file url so the browser does
172
+ # not request a 404 thumb (the original lives one level above /thumb).
173
+ # Otherwise the computed thumb url is returned unchanged (new uploads, PNG
174
+ # sources and S3 are unaffected).
175
+ # thumb_url: (String) computed public/private thumb url
176
+ # thumb_key: (String) computed thumb media key (relative to @root_folder)
177
+ # original_url: (String) url of the original (non-thumb) file
178
+ def cama_compat_legacy_thumb(thumb_url, thumb_key, original_url = nil)
179
+ return thumb_url if thumb_url.blank?
180
+
181
+ ext = File.extname(thumb_key)
182
+ return thumb_url if ext.blank?
183
+ return thumb_url if file_exists?(File.join(@root_folder, thumb_key))
184
+
185
+ unless ext.casecmp?('.png')
186
+ png_key = thumb_key.sub(/#{Regexp.escape(ext)}\z/, '.png')
187
+ return thumb_url.sub(/#{Regexp.escape(ext)}\z/, '.png') if file_exists?(File.join(@root_folder, png_key))
188
+ end
189
+
190
+ original_url.presence || thumb_url
191
+ end
135
192
  end
@@ -1,5 +1,8 @@
1
1
  class CamaleonCmsUploader
2
2
  PRIVATE_DIRECTORY = 'private'.freeze
3
+ # Case-insensitive terminal-`.svg` matcher, borrowed from the thumb/crop rename convention it
4
+ # must stay in lockstep with (defined beside that convention in UploaderImageProcessing).
5
+ SVG_EXT_PATTERN = CamaleonCms::UploaderImageProcessing::SVG_EXT_PATTERN
3
6
 
4
7
  attr_accessor :thumb
5
8
 
@@ -25,10 +28,11 @@ class CamaleonCmsUploader
25
28
  ''].include?(prefix)
26
29
  get_media_collection.where(folder_path: '/')
27
30
  else
28
- get_media_collection.find_by_key(prefix).take.try(:items)
31
+ get_media_collection.by_key(prefix).take.try(:items)
29
32
  end
30
- # Private hook to recover custom files to include in current list where data can be modified to add custom{files, folders}
31
- # Note: this hooks doesn't have access to public vars like params. requests, ...
33
+ # Private hook to recover custom files to include into the current list where
34
+ # data can be modified to add custom{files, folders}.
35
+ # Note: these hooks don't have access to public vars like params. requests, ...
32
36
  if @instance
33
37
  args = { data: res, prefix: prefix }
34
38
  @instance.hooks_run('uploader_list_objects', args)
@@ -37,6 +41,15 @@ class CamaleonCmsUploader
37
41
  res
38
42
  end
39
43
 
44
+ # Post-process one *rendered page* of media items for display. Base and S3 need nothing;
45
+ # the local uploader overrides this to repair legacy thumbnail URLs (see its override), so
46
+ # that per-image filesystem check runs over the paginated page rather than the whole folder.
47
+ # Kept out of #objects on purpose: #objects must stay a lazy relation so the media browser
48
+ # paginates at the database.
49
+ def cama_prepare_browser_page(items)
50
+ items
51
+ end
52
+
40
53
  # clean cached of files structure saved into DB
41
54
  def clear_cache
42
55
  get_media_collection.destroy_all
@@ -94,7 +107,7 @@ class CamaleonCmsUploader
94
107
  # support for multiples formats, sample: image,audio
95
108
  def self.get_file_format_extensions(format)
96
109
  res = []
97
- format.downcase.gsub(' ', '').split(',').each do |f|
110
+ format.downcase.delete(' ').split(',').each do |f|
98
111
  res << case f
99
112
  when 'image', 'images'
100
113
  'jpg,jpeg,png,gif,bmp,ico,svg'
@@ -116,12 +129,12 @@ class CamaleonCmsUploader
116
129
  # verify permitted formats (return boolean true | false)
117
130
  # true: if format is accepted
118
131
  # false: if format is not accepted
119
- # sample: validate_file_format('/var/www/myfile.xls', 'image,audio,docx,xls') => return true if the file extension is in formats
132
+ # sample: validate_file_format('/var/www/myfile.xls', 'image,audio,docx,xls') => return true if the file extension
133
+ # is in formats
120
134
  def self.validate_file_format(key, valid_formats = '*')
121
- return true if valid_formats == '*' || !valid_formats.present?
135
+ return true if valid_formats == '*' || valid_formats.blank?
122
136
 
123
- valid_formats = valid_formats.gsub(' ',
124
- '').downcase.split(',') + get_file_format_extensions(valid_formats).split(',')
137
+ valid_formats = valid_formats.delete(' ').downcase.split(',') + get_file_format_extensions(valid_formats).split(',')
125
138
  valid_formats.include?(File.extname(key).sub('.', '').split('?').first.try(:downcase))
126
139
  end
127
140
 
@@ -136,10 +149,10 @@ class CamaleonCmsUploader
136
149
  # sample: search_new_key("my_file/file.txt")
137
150
  def search_new_key(key)
138
151
  _key = key
139
- if get_media_collection.find_by_key(key).any?
152
+ if get_media_collection.by_key(key).any?
140
153
  (1..999).each do |i|
141
154
  _key = key.cama_add_postfix_file_name("_#{i}")
142
- break unless get_media_collection.find_by_key(_key).any?
155
+ break unless get_media_collection.by_key(_key).any?
143
156
  end
144
157
  end
145
158
  _key