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.
- checksums.yaml +4 -4
- data/README.md +29 -9
- data/Rakefile +3 -16
- data/app/apps/plugins/attack/admin_controller.rb +9 -6
- data/app/apps/plugins/attack/attack_helper.rb +35 -25
- data/app/apps/plugins/authoring_post/authoring_post_helper.rb +23 -16
- data/app/apps/plugins/front_cache/admin_controller.rb +16 -10
- data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
- data/app/apps/plugins/front_cache/front_cache_helper.rb +57 -24
- data/app/apps/plugins/visibility_post/config/config.json +1 -0
- data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
- data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
- data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
- data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
- data/app/apps/themes/default/assets/css/style.css.scss +24 -0
- data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
- data/app/apps/themes/default/default_helper.rb +2 -1
- data/app/apps/themes/default/views/category.html.erb +1 -1
- data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
- data/app/apps/themes/default/views/post_tag.html.erb +1 -1
- data/app/apps/themes/default/views/post_type.html.erb +1 -1
- data/app/apps/themes/default/views/search.html.erb +1 -1
- data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
- data/app/apps/themes/new/custom_helper.rb +5 -9
- data/app/apps/themes/new/views/category.html.erb +1 -1
- data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
- data/app/apps/themes/new/views/post_tag.html.erb +1 -1
- data/app/apps/themes/new/views/post_type.html.erb +1 -1
- data/app/apps/themes/new/views/search.html.erb +1 -1
- data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
- data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
- data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
- data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
- data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +29 -10
- data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
- data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +9 -2
- data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +4 -3
- data/app/controllers/camaleon_cms/admin/appearances/widgets/sidebar_controller.rb +2 -2
- data/app/controllers/camaleon_cms/admin/categories_controller.rb +27 -8
- data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
- data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
- data/app/controllers/camaleon_cms/admin/media_controller.rb +84 -19
- data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
- data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +15 -6
- data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
- data/app/controllers/camaleon_cms/admin/posts_controller.rb +58 -22
- data/app/controllers/camaleon_cms/admin/sessions_controller.rb +238 -45
- data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +103 -18
- data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +17 -5
- data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +33 -7
- data/app/controllers/camaleon_cms/admin/settings_controller.rb +11 -6
- data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +9 -7
- data/app/controllers/camaleon_cms/admin/users_controller.rb +89 -18
- data/app/controllers/camaleon_cms/admin_controller.rb +144 -26
- data/app/controllers/camaleon_cms/apps/plugins_admin_controller.rb +5 -0
- data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
- data/app/controllers/camaleon_cms/frontend_controller.rb +140 -70
- data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +44 -0
- data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
- data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
- data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
- data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
- data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
- data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
- data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
- data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
- data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
- data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
- data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
- data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
- data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
- data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
- data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
- data/app/decorators/camaleon_cms/post_decorator.rb +67 -51
- data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
- data/app/decorators/camaleon_cms/site_decorator.rb +29 -23
- data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
- data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
- data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
- data/app/helpers/camaleon_cms/admin/application_helper.rb +21 -24
- data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
- data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
- data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
- data/app/helpers/camaleon_cms/admin/post_type_helper.rb +51 -31
- data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
- data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
- data/app/helpers/camaleon_cms/comment_helper.rb +87 -41
- data/app/helpers/camaleon_cms/content_helper.rb +13 -6
- data/app/helpers/camaleon_cms/email_helper.rb +7 -3
- data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
- data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +144 -67
- data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
- data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
- data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
- data/app/helpers/camaleon_cms/html_helper.rb +73 -38
- data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
- data/app/helpers/camaleon_cms/session_helper.rb +195 -39
- data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
- data/app/helpers/camaleon_cms/site_helper.rb +43 -9
- data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
- data/app/helpers/camaleon_cms/uploader_helper.rb +19 -422
- data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
- data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
- data/app/models/camaleon_cms/ability.rb +65 -105
- data/app/models/camaleon_cms/category.rb +7 -5
- data/app/models/camaleon_cms/custom_field.rb +29 -6
- data/app/models/camaleon_cms/custom_field_group.rb +75 -21
- data/app/models/camaleon_cms/custom_fields_relationship.rb +133 -2
- data/app/models/camaleon_cms/media.rb +8 -2
- data/app/models/camaleon_cms/meta.rb +2 -2
- data/app/models/camaleon_cms/nav_menu.rb +6 -2
- data/app/models/camaleon_cms/nav_menu_item.rb +18 -8
- data/app/models/camaleon_cms/plugin.rb +4 -3
- data/app/models/camaleon_cms/post.rb +113 -27
- data/app/models/camaleon_cms/post_comment.rb +10 -6
- data/app/models/camaleon_cms/post_default.rb +42 -12
- data/app/models/camaleon_cms/post_relationship.rb +6 -4
- data/app/models/camaleon_cms/post_tag.rb +4 -3
- data/app/models/camaleon_cms/post_type.rb +22 -12
- data/app/models/camaleon_cms/site.rb +40 -28
- data/app/models/camaleon_cms/term_relationship.rb +10 -7
- data/app/models/camaleon_cms/term_taxonomy.rb +51 -5
- data/app/models/camaleon_cms/theme.rb +3 -3
- data/app/models/camaleon_cms/user.rb +17 -5
- data/app/models/camaleon_cms/user_role.rb +77 -4
- data/app/models/camaleon_cms/widget/assigned.rb +11 -6
- data/app/models/camaleon_cms/widget/main.rb +13 -7
- data/app/models/camaleon_cms/widget/sidebar.rb +3 -6
- data/app/models/camaleon_record.rb +123 -5
- data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +20 -38
- data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
- data/app/models/concerns/camaleon_cms/custom_fields_read.rb +115 -58
- data/app/models/concerns/camaleon_cms/metas.rb +32 -7
- data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
- data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
- data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
- data/app/models/current_request.rb +23 -0
- data/app/uploaders/camaleon_cms_aws_uploader.rb +39 -11
- data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
- data/app/uploaders/camaleon_cms_uploader.rb +23 -10
- data/app/validators/camaleon_cms/post_uniq_validator.rb +21 -8
- data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
- data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
- data/app/views/camaleon_cms/admin/appearances/nav_menus/_left_menu_items.html.erb +2 -2
- data/app/views/camaleon_cms/admin/appearances/widgets/main/form.html.erb +1 -1
- data/app/views/camaleon_cms/admin/categories/index.html.erb +3 -3
- data/app/views/camaleon_cms/admin/comments/index.html.erb +2 -2
- data/app/views/camaleon_cms/admin/comments/list.html.erb +1 -1
- data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
- data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
- data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
- data/app/views/camaleon_cms/admin/post_tags/index.html.erb +3 -3
- data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +2 -2
- data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
- data/app/views/camaleon_cms/admin/posts/index.html.erb +6 -6
- data/app/views/camaleon_cms/admin/search.html.erb +1 -1
- data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
- data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
- data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +42 -6
- data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/fields/_select_eval.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +5 -4
- data/app/views/camaleon_cms/admin/settings/custom_fields/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/post_types/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
- data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/sites/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/user_roles/form.html.erb +102 -7
- data/app/views/camaleon_cms/admin/user_roles/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
- data/app/views/camaleon_cms/admin/users/index.html.erb +2 -2
- data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
- data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
- data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
- data/app/views/layouts/camaleon_cms/admin/_flash_messages.html.erb +2 -2
- data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
- data/config/initializers/action_view.rb +42 -5
- data/config/initializers/active_record_extension.rb +13 -27
- data/config/initializers/assets.rb +52 -23
- data/config/initializers/custom_initializers.rb +3 -3
- data/config/locales/camaleon_cms/admin/ar.yml +7 -2
- data/config/locales/camaleon_cms/admin/de.yml +7 -2
- data/config/locales/camaleon_cms/admin/en.yml +39 -2
- data/config/locales/camaleon_cms/admin/es.yml +7 -2
- data/config/locales/camaleon_cms/admin/fr.yml +7 -2
- data/config/locales/camaleon_cms/admin/it.yml +7 -2
- data/config/locales/camaleon_cms/admin/nl.yml +8 -2
- data/config/locales/camaleon_cms/admin/pt-BR.yml +7 -2
- data/config/locales/camaleon_cms/admin/pt.yml +7 -2
- data/config/locales/camaleon_cms/admin/ru.yml +7 -2
- data/config/locales/camaleon_cms/admin/uk.yml +7 -2
- data/config/locales/camaleon_cms/admin/zh-CH.yml +7 -2
- data/config/routes/admin.rb +33 -15
- data/config/routes/frontend.rb +21 -15
- data/db/migrate/20150611161134_post_table_into_utf8.rb +14 -14
- data/db/migrate/20150926095310_rename_column_posts.rb +3 -3
- data/db/migrate/20151212095328_add_confirm_token_to_users.rb +3 -3
- data/db/migrate/20160504155652_add_feature_to_posts.rb +1 -1
- data/db/migrate/20160504155653_move_first_name_of_users.rb +2 -2
- data/db/migrate/20160609121449_add_group_to_custom_field_values.rb +1 -1
- data/db/migrate/20161215202255_drop_user_relationship_table.rb +1 -1
- data/db/migrate/20180124132318_create_media.rb +1 -1
- data/db/migrate/20180704211100_adjust_field_length.rb +1 -1
- data/lib/camaleon_cms/assets_precompile.rb +86 -0
- data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
- data/lib/camaleon_cms/content_security.rb +158 -0
- data/lib/camaleon_cms/engine.rb +47 -9
- data/lib/camaleon_cms/media_security_headers.rb +27 -0
- data/lib/camaleon_cms/setup_token.rb +69 -0
- data/lib/camaleon_cms/svg_content_checker.rb +123 -0
- data/lib/camaleon_cms/task_reporter.rb +14 -0
- data/lib/camaleon_cms/unsafe_markup.rb +207 -0
- data/lib/camaleon_cms/uploader_content_security.rb +248 -0
- data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
- data/lib/camaleon_cms/uploader_path_security.rb +146 -0
- data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
- data/lib/camaleon_cms/uploader_support.rb +86 -0
- data/lib/camaleon_cms/version.rb +1 -1
- data/lib/camaleon_cms.rb +11 -0
- data/lib/ext/hash.rb +28 -2
- data/lib/ext/string.rb +13 -8
- data/lib/ext/translator.rb +10 -2
- data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
- data/lib/generators/camaleon_cms/install_generator.rb +7 -4
- data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
- data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
- data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
- data/lib/plugin_routes.rb +412 -368
- data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
- data/lib/tasks/cross_site_field_groups.rake +74 -0
- data/lib/tasks/custom_fields_roles.rake +58 -0
- data/lib/tasks/orphaned_comments.rake +42 -0
- data/lib/tasks/private_upload_acls.rake +31 -0
- data/lib/tasks/site_custom_field_groups.rake +36 -0
- data/lib/tasks/unsafe_stored_content.rake +42 -0
- data/lib/tasks/unsafe_stored_uploads.rake +70 -0
- data/lib/tasks/user_field_groups.rake +27 -0
- metadata +185 -7
|
@@ -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
|
-
{
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
30
|
+
post_types.find_each do |pt|
|
|
21
31
|
if pt.slug == 'post'
|
|
22
32
|
title = 'Sample Post'
|
|
23
33
|
slug = 'sample-post'
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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' })
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
|
149
|
+
next if u.blank?
|
|
130
150
|
|
|
131
|
-
p.update_column(:user_id, u.id)
|
|
132
|
-
p.comments.where(user_id: id).
|
|
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 }).
|
|
140
|
-
site = comment.post
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# CurrentRequest holds per-request state (user, site) accessible to models
|
|
2
|
+
# via ActiveSupport::CurrentAttributes (thread-safe, resets after each request).
|
|
3
|
+
#
|
|
4
|
+
# Usage in controllers:
|
|
5
|
+
# CurrentRequest.user = cama_current_user
|
|
6
|
+
# CurrentRequest.site = current_site
|
|
7
|
+
#
|
|
8
|
+
# Usage in models:
|
|
9
|
+
# current_user # delegates to CurrentRequest.user via CamaleonRecord
|
|
10
|
+
# current_site # delegates to CurrentRequest.site via CamaleonRecord
|
|
11
|
+
#
|
|
12
|
+
# Note: Rails automatically resets CurrentAttributes between requests, ensuring no leakage.
|
|
13
|
+
# In specs, call CurrentRequest.reset in before/after hooks to avoid cross-test pollution.
|
|
14
|
+
class CurrentRequest < ActiveSupport::CurrentAttributes
|
|
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
|
|
23
|
+
end
|
|
@@ -37,6 +37,8 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
def fetch_file(file_name)
|
|
40
|
+
return { error: 'Invalid file path' } unless valid_folder_path?(file_name)
|
|
41
|
+
|
|
40
42
|
return file_name if file_exists?(file_name)
|
|
41
43
|
|
|
42
44
|
return file_name if bucket.object(file_name).download_file(file_name) && file_exists?(file_name)
|
|
@@ -70,9 +72,7 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
70
72
|
'dimension' => ''
|
|
71
73
|
}.with_indifferent_access
|
|
72
74
|
if res['file_type'] == 'image' && File.extname(res['name']).downcase != '.gif'
|
|
73
|
-
res['thumb'] =
|
|
74
|
-
version_path(res['url']).sub('.svg',
|
|
75
|
-
'.jpg')
|
|
75
|
+
res['thumb'] = version_path(res['url']).sub(SVG_EXT_PATTERN, '.jpg')
|
|
76
76
|
end
|
|
77
77
|
res['key'] = File.join(res['folder_path'], res['name'])
|
|
78
78
|
@aws_settings[:aws_file_read_settings].call(res, s3_file)
|
|
@@ -84,8 +84,9 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
84
84
|
# - same_name: false => avoid to overwrite an existent file with same key and search for an available key
|
|
85
85
|
# - is_thumb: true => if this file is a thumbnail of an uploaded file
|
|
86
86
|
def add_file(uploaded_io_or_file_path, key, args = {})
|
|
87
|
+
return { error: 'Invalid file path' } unless valid_folder_path?(key)
|
|
88
|
+
|
|
87
89
|
args = { same_name: false, is_thumb: false }.merge(args)
|
|
88
|
-
res = nil
|
|
89
90
|
key = "#{@aws_settings['inner_folder']}/#{key}" if @aws_settings['inner_folder'].present? && !args[:is_thumb]
|
|
90
91
|
key = key.cama_fix_media_key
|
|
91
92
|
key = search_new_key(key) unless args[:same_name]
|
|
@@ -97,14 +98,20 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
97
98
|
end
|
|
98
99
|
|
|
99
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'
|
|
100
106
|
s3_file.upload_file(
|
|
101
|
-
uploaded_io_or_file_path.is_a?(String) ? uploaded_io_or_file_path : uploaded_io_or_file_path.path,
|
|
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 })
|
|
102
109
|
)
|
|
103
110
|
res = cache_item(file_parse(s3_file)) unless args[:is_thumb]
|
|
104
111
|
res
|
|
105
112
|
end
|
|
106
113
|
|
|
107
|
-
#
|
|
114
|
+
# Add a new folder to AWS with :key
|
|
108
115
|
def add_folder(key)
|
|
109
116
|
return { error: 'Invalid folder path' } unless valid_folder_path?(key)
|
|
110
117
|
|
|
@@ -115,16 +122,20 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
115
122
|
cache_item(file_parse(s3_file))
|
|
116
123
|
end
|
|
117
124
|
|
|
118
|
-
#
|
|
125
|
+
# Delete a folder in AWS with :key
|
|
119
126
|
def delete_folder(key)
|
|
127
|
+
return { error: 'Invalid folder path' } unless valid_folder_path?(key)
|
|
128
|
+
|
|
120
129
|
key = "#{@aws_settings['inner_folder']}/#{key}" if @aws_settings['inner_folder'].present?
|
|
121
130
|
key = key.cama_fix_media_key
|
|
122
131
|
bucket.objects(prefix: key.slice(1..-1) << '/').delete
|
|
123
|
-
get_media_collection.
|
|
132
|
+
get_media_collection.by_key(key).take.destroy
|
|
124
133
|
end
|
|
125
134
|
|
|
126
|
-
#
|
|
135
|
+
# Delete the `:key` file in AWS
|
|
127
136
|
def delete_file(key)
|
|
137
|
+
return { error: 'Invalid file path' } unless valid_folder_path?(key)
|
|
138
|
+
|
|
128
139
|
key = "#{@aws_settings['inner_folder']}/#{key}" if @aws_settings['inner_folder'].present?
|
|
129
140
|
key = key.cama_fix_media_key
|
|
130
141
|
begin
|
|
@@ -133,10 +144,27 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
133
144
|
''
|
|
134
145
|
end
|
|
135
146
|
@instance.hooks_run('after_delete', key)
|
|
136
|
-
get_media_collection.
|
|
147
|
+
get_media_collection.by_key(key).take.destroy
|
|
148
|
+
end
|
|
149
|
+
|
|
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
|
|
137
165
|
end
|
|
138
166
|
|
|
139
|
-
#
|
|
167
|
+
# Initialize a bucket with AWS configurations
|
|
140
168
|
# return: (AWS Bucket object)
|
|
141
169
|
def bucket
|
|
142
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)
|
|
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)
|
|
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
|
-
(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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!
|
|
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
|
-
#
|
|
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.
|
|
131
|
+
get_media_collection.by_key(key).take.destroy
|
|
119
132
|
end
|
|
120
133
|
|
|
121
|
-
#
|
|
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.
|
|
141
|
+
get_media_collection.by_key(key).take.destroy
|
|
129
142
|
end
|
|
130
143
|
|
|
131
|
-
#
|
|
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.
|
|
31
|
+
get_media_collection.by_key(prefix).take.try(:items)
|
|
29
32
|
end
|
|
30
|
-
# Private hook to recover custom files to include
|
|
31
|
-
#
|
|
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.
|
|
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
|
|
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 == '*' ||
|
|
135
|
+
return true if valid_formats == '*' || valid_formats.blank?
|
|
122
136
|
|
|
123
|
-
valid_formats = valid_formats.
|
|
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.
|
|
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.
|
|
155
|
+
break unless get_media_collection.by_key(_key).any?
|
|
143
156
|
end
|
|
144
157
|
end
|
|
145
158
|
_key
|