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.
- checksums.yaml +4 -4
- data/README.md +17 -4
- 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 +14 -10
- data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
- data/app/apps/plugins/front_cache/config/locales/translation.yml +0 -7
- data/app/apps/plugins/front_cache/front_cache_helper.rb +109 -37
- data/app/apps/plugins/front_cache/views/admin/settings.html.erb +0 -5
- 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/layouts/index.html.erb +3 -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/layouts/_header.html.erb +3 -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 +8 -6
- data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
- data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +7 -2
- data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +2 -1
- data/app/controllers/camaleon_cms/admin/categories_controller.rb +19 -4
- 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 +66 -14
- data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
- data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +10 -4
- data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
- data/app/controllers/camaleon_cms/admin/posts_controller.rb +55 -22
- data/app/controllers/camaleon_cms/admin/sessions_controller.rb +237 -44
- data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +80 -13
- data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +5 -2
- data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +26 -3
- data/app/controllers/camaleon_cms/admin/settings_controller.rb +5 -3
- data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +7 -5
- data/app/controllers/camaleon_cms/admin/users_controller.rb +78 -11
- data/app/controllers/camaleon_cms/admin_controller.rb +136 -26
- data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
- data/app/controllers/camaleon_cms/frontend_controller.rb +146 -74
- data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +20 -5
- 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 +68 -50
- data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
- data/app/decorators/camaleon_cms/site_decorator.rb +33 -25
- 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 +5 -8
- 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 +34 -17
- 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 +19 -7
- 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/application_helper.rb +6 -1
- data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
- data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +143 -66
- 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 +58 -37
- data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
- data/app/helpers/camaleon_cms/session_helper.rb +188 -44
- data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
- data/app/helpers/camaleon_cms/site_helper.rb +34 -13
- data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
- data/app/helpers/camaleon_cms/uploader_helper.rb +19 -473
- 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 +15 -7
- data/app/models/camaleon_cms/category.rb +6 -6
- data/app/models/camaleon_cms/custom_field.rb +15 -6
- data/app/models/camaleon_cms/custom_field_group.rb +39 -22
- data/app/models/camaleon_cms/custom_fields_relationship.rb +140 -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 +5 -3
- data/app/models/camaleon_cms/nav_menu_item.rb +16 -8
- data/app/models/camaleon_cms/plugin.rb +3 -4
- data/app/models/camaleon_cms/post.rb +137 -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 -5
- data/app/models/camaleon_cms/post_type.rb +21 -13
- data/app/models/camaleon_cms/site.rb +39 -29
- data/app/models/camaleon_cms/term_relationship.rb +10 -7
- data/app/models/camaleon_cms/term_taxonomy.rb +58 -4
- data/app/models/camaleon_cms/theme.rb +2 -4
- data/app/models/camaleon_cms/user.rb +17 -5
- data/app/models/camaleon_cms/user_role.rb +81 -6
- data/app/models/camaleon_cms/widget/assigned.rb +11 -6
- data/app/models/camaleon_cms/widget/main.rb +12 -8
- data/app/models/camaleon_cms/widget/sidebar.rb +3 -8
- data/app/models/camaleon_record.rb +89 -5
- data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +36 -42
- data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
- data/app/models/concerns/camaleon_cms/content_shortcode_gate.rb +73 -0
- data/app/models/concerns/camaleon_cms/custom_fields_read.rb +111 -54
- data/app/models/concerns/camaleon_cms/metas.rb +31 -6
- 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 +8 -1
- data/app/uploaders/camaleon_cms_aws_uploader.rb +31 -10
- 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 +5 -4
- 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/categories/index.html.erb +2 -2
- 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 +2 -2
- data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
- data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
- data/app/views/camaleon_cms/admin/posts/index.html.erb +5 -5
- 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 +19 -4
- 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/form.html.erb +4 -3
- 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/user_roles/form.html.erb +24 -3
- data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
- data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
- 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/_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 +1 -1
- data/config/locales/camaleon_cms/admin/ar.yml +1 -0
- data/config/locales/camaleon_cms/admin/de.yml +1 -0
- data/config/locales/camaleon_cms/admin/en.yml +36 -0
- data/config/locales/camaleon_cms/admin/es.yml +1 -0
- data/config/locales/camaleon_cms/admin/fr.yml +1 -0
- data/config/locales/camaleon_cms/admin/it.yml +1 -0
- data/config/locales/camaleon_cms/admin/nl.yml +1 -0
- data/config/locales/camaleon_cms/admin/pt-BR.yml +1 -0
- data/config/locales/camaleon_cms/admin/pt.yml +1 -0
- data/config/locales/camaleon_cms/admin/ru.yml +1 -0
- data/config/locales/camaleon_cms/admin/uk.yml +1 -0
- data/config/locales/camaleon_cms/admin/zh-CH.yml +1 -0
- data/config/routes/admin.rb +33 -15
- data/config/routes/frontend.rb +21 -15
- 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 +45 -21
- data/lib/camaleon_cms/media_security_headers.rb +27 -0
- data/lib/camaleon_cms/setup_token.rb +69 -0
- data/lib/camaleon_cms/shortcode_registry.rb +102 -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 +23 -0
- data/lib/ext/hash.rb +28 -2
- data/lib/ext/string.rb +10 -5
- 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 +15 -13
- 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 +125 -4
|
@@ -1,487 +1,33 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'tempfile'
|
|
5
|
-
|
|
6
3
|
module CamaleonCms
|
|
4
|
+
# Uploader entry point for views, ActiveJobs, and any standalone object that includes
|
|
5
|
+
# it (see config/initializers/custom_initializers.rb). The implementation lives in the
|
|
6
|
+
# shared modules below, which CamaleonCms::RuntimeUploaderConcern includes as well, so
|
|
7
|
+
# the two entry points cannot drift.
|
|
7
8
|
module UploaderHelper
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
onstalled onstorage onsuspend ontimeupdate ontoggle onunloadonsubmit onvolumechange onwaiting onwheel
|
|
14
|
-
].map { |pattern| /#{pattern}\w*\s*=/i }.freeze
|
|
15
|
-
|
|
16
|
-
SUSPICIOUS_PATTERNS = (UNSAFE_EVENT_PATTERNS + [
|
|
17
|
-
/<script[\s>]/i, # Script tags
|
|
18
|
-
/javascript:/i, # JavaScript in href/src attributes
|
|
19
|
-
/<iframe[\s>]/i, # Iframes
|
|
20
|
-
/<object[\s>]/i, # Object tags
|
|
21
|
-
/<embed[\s>]/i, # Embed tags
|
|
22
|
-
/<base[\s>]/i, # Base tags (can be used to manipulate URLs)
|
|
23
|
-
/data:/i # data: URLs (which can include scripts)
|
|
24
|
-
]).freeze
|
|
25
|
-
|
|
9
|
+
include UploaderContentSecurity
|
|
10
|
+
include UploaderPathSecurity
|
|
11
|
+
include UploaderPipeline
|
|
12
|
+
include UploaderImageProcessing
|
|
13
|
+
include UploaderSupport
|
|
26
14
|
include ActionView::Helpers::NumberHelper
|
|
27
15
|
include CamaleonCms::CamaleonHelper
|
|
28
16
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# dimension: dimension for the image (sample: 30x30 | x30 | 30x | 300x300?)
|
|
36
|
-
# formats: extensions permitted, sample: jpg,png,... or generic: images | videos | audios | documents (default *)
|
|
37
|
-
# remove_source: Boolean (delete source file after saved if this is true, default false)
|
|
38
|
-
# same_name: Boolean (save the file with the same name if defined true, else search for a non used name)
|
|
39
|
-
# versions: (String) Create additional multiple versions of the image uploaded,
|
|
40
|
-
# sample: '300x300,505x350' ==> Will create two extra images with these dimensions
|
|
41
|
-
# sample "test.png", versions: '200x200,450x450' will generate: thumb/test-png_200x200.png, test-png_450x450.png
|
|
42
|
-
# thumb_size: String (redefine the dimensions of the thumbnail, sample: '100x100' ==> only for images)
|
|
43
|
-
# temporal_time: if great than 0 seconds, then this file will expire (removed) in that time (default: 0)
|
|
44
|
-
# To manage jobs, please check https://edgeguides.rubyonrails.org/active_job_basics.html
|
|
45
|
-
# Note: if you are using temporal_time, you will need to copy the file to another directory later
|
|
46
|
-
# sample: upload_file(params[:my_file], {formats: "images", folder: "temporal"})
|
|
47
|
-
# sample: upload_file(params[:my_file], {formats: "jpg,png,gif,mp3,mp4", temporal_time: 10.minutes, maximum: 10.megabytes})
|
|
48
|
-
def upload_file(uploaded_io, settings = {})
|
|
49
|
-
cached_name = uploaded_io.is_a?(ActionDispatch::Http::UploadedFile) ? uploaded_io.original_filename : nil
|
|
50
|
-
return { error: 'File is empty', file: nil, size: nil } unless uploaded_io.present?
|
|
51
|
-
|
|
52
|
-
if uploaded_io.is_a?(String) && uploaded_io.match(%r{^https?://}).present? # download url file
|
|
53
|
-
tmp = cama_tmp_upload(uploaded_io)
|
|
54
|
-
return tmp if tmp[:error].present?
|
|
55
|
-
|
|
56
|
-
settings[:remove_source] = true
|
|
57
|
-
uploaded_io = tmp[:file_path]
|
|
58
|
-
end
|
|
59
|
-
uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
|
|
60
|
-
if settings[:dimension].present?
|
|
61
|
-
uploaded_io = File.open(cama_resize_upload(uploaded_io.path, settings[:dimension]))
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
return { error: 'Potentially malicious content found!' } if file_content_unsafe?(uploaded_io)
|
|
65
|
-
|
|
66
|
-
settings = settings.to_sym
|
|
67
|
-
settings[:uploaded_io] = uploaded_io
|
|
68
|
-
settings = {
|
|
69
|
-
folder: '',
|
|
70
|
-
maximum: current_site.get_option('filesystem_max_size', 100).to_f.megabytes,
|
|
71
|
-
formats: '*',
|
|
72
|
-
generate_thumb: true,
|
|
73
|
-
temporal_time: 0,
|
|
74
|
-
filename: begin
|
|
75
|
-
cached_name || uploaded_io.original_filename
|
|
76
|
-
rescue StandardError
|
|
77
|
-
uploaded_io.path.split('/').last
|
|
78
|
-
end.cama_fix_filename,
|
|
79
|
-
file_size: File.size(uploaded_io.to_io),
|
|
80
|
-
remove_source: false,
|
|
81
|
-
same_name: false,
|
|
82
|
-
versions: '',
|
|
83
|
-
thumb_size: nil
|
|
84
|
-
}.merge!(settings)
|
|
85
|
-
hooks_run('before_upload', settings)
|
|
86
|
-
|
|
87
|
-
# guard against path traversal
|
|
88
|
-
return { error: 'Invalid file path' } unless cama_uploader.valid_folder_path?(settings[:folder])
|
|
89
|
-
|
|
90
|
-
# formats validations
|
|
91
|
-
err = validate_file_format_or_error(uploaded_io.path, settings[:formats])
|
|
92
|
-
return err if err
|
|
93
|
-
|
|
94
|
-
# file size validations
|
|
95
|
-
if settings[:maximum] < settings[:file_size]
|
|
96
|
-
max_size = number_to_human_size(settings[:maximum])
|
|
97
|
-
return { error: "#{ct('file_size_exceeded', default: 'File size exceeded')} (#{max_size})" }
|
|
98
|
-
end
|
|
99
|
-
# save file
|
|
100
|
-
key = File.join(settings[:folder], settings[:filename]).to_s.cama_fix_slash
|
|
101
|
-
res = cama_uploader.add_file(settings[:uploaded_io], key, { same_name: settings[:same_name] })
|
|
102
|
-
|
|
103
|
-
# generate image versions
|
|
104
|
-
if res['file_type'] == 'image'
|
|
105
|
-
settings[:versions].to_s.gsub(' ', '').split(',').each do |v|
|
|
106
|
-
version_path = cama_resize_upload(settings[:uploaded_io].path, v, { replace: false })
|
|
107
|
-
cama_uploader.add_file(version_path, cama_uploader.version_path(res['key'], v), is_thumb: true,
|
|
108
|
-
same_name: true)
|
|
109
|
-
FileUtils.rm_f(version_path)
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# generate thumb
|
|
114
|
-
if settings[:generate_thumb] && res['thumb'].present?
|
|
115
|
-
cama_uploader_generate_thumbnail(uploaded_io.path, res['key'], settings[:thumb_size],
|
|
116
|
-
settings[:remove_source])
|
|
117
|
-
end
|
|
118
|
-
FileUtils.rm_f(uploaded_io.path) if settings[:remove_source] && File.exist?(uploaded_io.path)
|
|
119
|
-
|
|
120
|
-
hooks_run('after_upload', settings)
|
|
121
|
-
|
|
122
|
-
# temporal file upload (always put as local for temporal files)
|
|
123
|
-
CamaleonCmsUploader.delete_block.call(settings, cama_uploader, key) if settings[:temporal_time] > 0
|
|
124
|
-
|
|
125
|
-
res
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# generate thumbnail of a existent image
|
|
129
|
-
# key: key of the current file
|
|
130
|
-
# the thumbnail will be saved in my_images/my_img.png => my_images/thumb/my_img.png
|
|
131
|
-
def cama_uploader_generate_thumbnail(uploaded_io, key, thumb_size = nil, remove_source = false)
|
|
132
|
-
w = thumb_size.present? ? thumb_size.split('x')[0] : cama_uploader.thumb[:w]
|
|
133
|
-
h = thumb_size.present? ? thumb_size.split('x')[1] : cama_uploader.thumb[:h]
|
|
134
|
-
uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
|
|
135
|
-
path_thumb = cama_resize_and_crop(uploaded_io.path, w, h)
|
|
136
|
-
thumb = cama_uploader.add_file(path_thumb, cama_uploader.version_path(key).sub('.svg', '.jpg'), is_thumb: true,
|
|
137
|
-
same_name: true)
|
|
138
|
-
FileUtils.rm_f(path_thumb) if remove_source
|
|
139
|
-
thumb
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# helper to find an available filename for file_path in that directory
|
|
143
|
-
# sample: uploader_verify_name("/var/www/my_image.jpg")
|
|
144
|
-
# return "/var/www/my_image_1.jpg" => if "/var/www/my_image.jpg" exist
|
|
145
|
-
# return "/var/www/my_image.jpg" => if "/var/www/my_image.jpg" doesn't exist
|
|
146
|
-
def uploader_verify_name(file_path)
|
|
147
|
-
dir = File.dirname(file_path)
|
|
148
|
-
filename = File.basename(file_path).to_s.cama_fix_filename
|
|
149
|
-
files = Dir.entries(dir)
|
|
150
|
-
if files.include?(filename)
|
|
151
|
-
i = 1
|
|
152
|
-
_filename = filename
|
|
153
|
-
while files.include?(_filename)
|
|
154
|
-
_filename = "#{File.basename(filename, File.extname(filename))}_#{i}#{File.extname(filename)}"
|
|
155
|
-
i += 1
|
|
156
|
-
end
|
|
157
|
-
filename = _filename
|
|
158
|
-
end
|
|
159
|
-
"#{File.dirname(file_path)}/#{filename}"
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# convert downloaded file path into public url
|
|
163
|
-
def cama_file_path_to_url(file_path)
|
|
164
|
-
file_path.sub(Rails.public_path.to_s, begin
|
|
165
|
-
root_url
|
|
166
|
-
rescue StandardError
|
|
167
|
-
cama_root_url
|
|
168
|
-
end)
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
# convert public url to file path
|
|
172
|
-
def cama_url_to_file_path(url)
|
|
173
|
-
File.join(Rails.public_path, URI(url.to_s).path)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
# crop and image and saved as imagename_crop.ext
|
|
177
|
-
# file: file path
|
|
178
|
-
# w: new width
|
|
179
|
-
# h: new height
|
|
180
|
-
# w_offset: left offset
|
|
181
|
-
# w_offset: top offset
|
|
182
|
-
# resize: true/false
|
|
183
|
-
# (true => resize the image to this dimension)
|
|
184
|
-
# (false => crop the image with this dimension)
|
|
185
|
-
# replace: Boolean (replace current image or create another file)
|
|
186
|
-
def cama_crop_image(file_path, w = nil, h = nil, w_offset = 0, h_offset = 0, resize = false, replace = true)
|
|
187
|
-
force = w.present? && h.present? && !w.include?('?') && !h.include?('?') ? '!' : ''
|
|
188
|
-
img = MiniMagick::Image.open(file_path)
|
|
189
|
-
w = clamp_to_image_dimension(w, img[:width])
|
|
190
|
-
h = clamp_to_image_dimension(h, img[:height])
|
|
191
|
-
data = { img: img, w: w, h: h, w_offset: w_offset, h_offset: h_offset, resize: resize, replace: replace }
|
|
192
|
-
hooks_run('before_crop_image', data)
|
|
193
|
-
data[:img].combine_options do |i|
|
|
194
|
-
i.resize("#{w if w.present?}x#{h if h.present?}#{force}") if data[:resize]
|
|
195
|
-
i.crop "#{w if w.present?}x#{h if h.present?}+#{w_offset}+#{h_offset}#{force}" unless data[:resize]
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
ext = File.extname(file_path)
|
|
199
|
-
res = data[:replace] ? file_path : file_path.gsub(ext, "_crop#{ext}")
|
|
200
|
-
data[:img].write res
|
|
201
|
-
res
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
# resize and crop a file
|
|
205
|
-
# SVGs are converted to JPEGs for editing
|
|
206
|
-
# Params:
|
|
207
|
-
# file: (String) File path
|
|
208
|
-
# w: (Integer) width
|
|
209
|
-
# h: (Integer) height
|
|
210
|
-
# settings:
|
|
211
|
-
# gravity: (Sym, default :north_east) Crop position: :north_west, :north, :north_east, :east, :south_east, :south, :south_west, :west, :center
|
|
212
|
-
# overwrite: (Boolean, default true) true for overwrite current image with resized resolutions, false: create other file called with prefix "crop_"
|
|
213
|
-
# output_name: (String, default prefixd name with crop_), permit to define the output name of the thumbnail if overwrite = true
|
|
214
|
-
# Return: (String) file path where saved this cropped
|
|
215
|
-
# sample: cama_resize_and_crop(my_file, 200, 200, {gravity: :north_east, overwrite: false})
|
|
216
|
-
def cama_resize_and_crop(file, w, h, settings = {})
|
|
217
|
-
settings = { gravity: :north_east, overwrite: true, output_name: +'' }.merge!(settings)
|
|
218
|
-
img = MiniMagick::Image.open(file)
|
|
219
|
-
if file.end_with? '.svg'
|
|
220
|
-
img.format 'jpg'
|
|
221
|
-
file.sub! '.svg', '.jpg'
|
|
222
|
-
settings[:output_name]&.sub!('.svg', '.jpg')
|
|
223
|
-
end
|
|
224
|
-
w = clamp_to_image_dimension(w, img[:width])
|
|
225
|
-
h = clamp_to_image_dimension(h, img[:height])
|
|
226
|
-
w_original = img[:width].to_f
|
|
227
|
-
h_original = img[:height].to_f
|
|
228
|
-
w = w.to_i if w.present?
|
|
229
|
-
h = h.to_i if h.present?
|
|
230
|
-
|
|
231
|
-
# check proportions
|
|
232
|
-
if w_original * h < h_original * w
|
|
233
|
-
op_resize = "#{w.to_i}x"
|
|
234
|
-
w_result = w
|
|
235
|
-
h_result = (h_original * w / w_original)
|
|
236
|
-
else
|
|
237
|
-
op_resize = "x#{h.to_i}"
|
|
238
|
-
w_result = (w_original * h / h_original)
|
|
239
|
-
h_result = h
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
w_offset, h_offset = cama_crop_offsets_by_gravity(settings[:gravity], [w_result, h_result], [w, h])
|
|
243
|
-
data = { img: img, w: w, h: h, w_offset: w_offset, h_offset: h_offset, op_resize: op_resize, settings: settings }
|
|
244
|
-
hooks_run('before_resize_crop', data)
|
|
245
|
-
data[:img].combine_options do |i|
|
|
246
|
-
i.resize(data[:op_resize])
|
|
247
|
-
i.gravity(settings[:gravity])
|
|
248
|
-
i.crop "#{data[:w].to_i}x#{data[:h].to_i}+#{data[:w_offset]}+#{data[:h_offset]}!"
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
if settings[:overwrite]
|
|
252
|
-
data[:img].write(file.sub('.svg', '.jpg'))
|
|
253
|
-
elsif settings[:output_name].present?
|
|
254
|
-
data[:img].write(file = File.join(File.dirname(file), settings[:output_name]).to_s)
|
|
255
|
-
else
|
|
256
|
-
data[:img].write(file = uploader_verify_name(File.join(File.dirname(file),
|
|
257
|
-
"crop_#{File.basename(file.sub('.svg', '.jpg'))}")))
|
|
258
|
-
end
|
|
259
|
-
file
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
# upload tmp file
|
|
263
|
-
# support for url and local path
|
|
264
|
-
# sample:
|
|
265
|
-
# cama_tmp_upload('https://camaleon.website/media/132/logo2.png') ==> /var/rails/my_project/public/tmp/1/logo2.png
|
|
266
|
-
# cama_tmp_upload('/var/www/media/132/logo 2.png') ==> /var/rails/my_project/public/tmp/1/logo-2.png
|
|
267
|
-
# accept args:
|
|
268
|
-
# name: to indicate the name to use, sample: cama_tmp_upload('/var/www/media/132/logo 2.png', {name: 'owen.png', formats: 'images'})
|
|
269
|
-
# formats: extensions permitted, sample: jpg,png,... or generic: images | videos | audios | documents (default *)
|
|
270
|
-
# dimension: 20x30
|
|
271
|
-
# return: {file_path, error}
|
|
272
|
-
def cama_tmp_upload(uploaded_io, args = {})
|
|
273
|
-
tmp_path = args[:path] || File.join(Rails.public_path, 'tmp', current_site.id.to_s).to_s
|
|
274
|
-
FileUtils.mkdir_p(tmp_path) unless Dir.exist?(tmp_path)
|
|
275
|
-
saved = false
|
|
276
|
-
downloaded_tmp_file = nil
|
|
277
|
-
if uploaded_io.is_a?(String) && uploaded_io.start_with?('data:') # create tmp file using base64 format
|
|
278
|
-
_tmp_name = args[:name]
|
|
279
|
-
return { error: cama_t('camaleon_cms.admin.media.name_required').to_s } unless params[:name].present?
|
|
280
|
-
|
|
281
|
-
err = validate_file_format_or_error(_tmp_name, args[:formats])
|
|
282
|
-
return err if err
|
|
283
|
-
|
|
284
|
-
path = uploader_verify_name(File.join(tmp_path, _tmp_name))
|
|
285
|
-
File.open(path, 'wb') { |f| f.write(Base64.decode64(uploaded_io.split(';base64,').last)) }
|
|
286
|
-
uploaded_io = File.open(path)
|
|
287
|
-
saved = true
|
|
288
|
-
elsif uploaded_io.is_a?(String) && (uploaded_io.start_with?('http://') || uploaded_io.start_with?('https://'))
|
|
289
|
-
err = validate_file_format_or_error(uploaded_io, args[:formats])
|
|
290
|
-
return err if err
|
|
291
|
-
|
|
292
|
-
if uploaded_io.include?(current_site.the_url(locale: nil))
|
|
293
|
-
uploaded_io = File.join(Rails.public_path, uploaded_io.sub(current_site.the_url(locale: nil), '')).to_s
|
|
294
|
-
else
|
|
295
|
-
remote_file = cama_download_remote_file(uploaded_io)
|
|
296
|
-
return remote_file if remote_file[:error].present?
|
|
297
|
-
|
|
298
|
-
downloaded_tmp_file = remote_file[:file]
|
|
299
|
-
uploaded_io = downloaded_tmp_file
|
|
300
|
-
end
|
|
301
|
-
_tmp_name = if uploaded_io.is_a?(String)
|
|
302
|
-
uploaded_io.split('/').last.split('?').first
|
|
303
|
-
else
|
|
304
|
-
uploaded_io.path.split('/').last
|
|
305
|
-
end
|
|
306
|
-
args[:name] = args[:name] || _tmp_name
|
|
307
|
-
end
|
|
308
|
-
uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
|
|
309
|
-
err = validate_file_format_or_error(_tmp_name || uploaded_io.path, args[:formats])
|
|
310
|
-
return err if err
|
|
311
|
-
|
|
312
|
-
actual_size = begin
|
|
313
|
-
uploaded_io.size
|
|
314
|
-
rescue StandardError
|
|
315
|
-
File.size(uploaded_io)
|
|
316
|
-
end
|
|
317
|
-
if args[:maximum].present? && args[:maximum] < actual_size
|
|
318
|
-
max_size = number_to_human_size(args[:maximum])
|
|
319
|
-
return { error: "#{ct('file_size_exceeded', default: 'File size exceeded')} (#{max_size})" }
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
name = args[:name] || uploaded_io&.original_filename || uploaded_io.path.split('/').last
|
|
323
|
-
name = "#{File.basename(name, File.extname(name)).parameterize}#{File.extname(name)}"
|
|
324
|
-
path ||= uploader_verify_name(File.join(tmp_path, name))
|
|
325
|
-
File.open(path, 'wb') { |f| f.write(uploaded_io.read) } unless saved
|
|
326
|
-
path = cama_resize_upload(path, args[:dimension]) if args[:dimension].present?
|
|
327
|
-
{ file_path: path, error: nil }
|
|
328
|
-
ensure
|
|
329
|
-
downloaded_tmp_file&.close!
|
|
330
|
-
end
|
|
331
|
-
|
|
332
|
-
# resize image if the format is correct
|
|
333
|
-
# return resized file path
|
|
334
|
-
def cama_resize_upload(image_path, dimension, args = {})
|
|
335
|
-
if cama_uploader.class.validate_file_format(image_path, 'image') && dimension.present?
|
|
336
|
-
dim_parts = dimension.split('x')
|
|
337
|
-
r = { file: image_path, w: dim_parts[0], h: dim_parts[1], w_offset: 0, h_offset: 0,
|
|
338
|
-
resize: !dim_parts[2] || dim_parts[2] == 'resize',
|
|
339
|
-
replace: true, gravity: :north_east }.merge!(args)
|
|
340
|
-
hooks_run('on_uploader_resize', r)
|
|
341
|
-
image_path = if r[:w].present? && r[:h].present?
|
|
342
|
-
cama_resize_and_crop(r[:file], r[:w], r[:h], { overwrite: r[:replace], gravity: r[:gravity] })
|
|
343
|
-
else
|
|
344
|
-
cama_crop_image(r[:file], r[:w], r[:h], r[:w_offset], r[:h_offset], r[:resize], r[:replace])
|
|
345
|
-
end
|
|
346
|
-
end
|
|
347
|
-
image_path
|
|
348
|
-
end
|
|
349
|
-
|
|
350
|
-
# return the current uploader
|
|
351
|
-
def cama_uploader
|
|
352
|
-
@cama_uploader ||= lambda {
|
|
353
|
-
thumb = current_site.get_option('filesystem_thumb_size', '100x100').split('x')
|
|
354
|
-
args = {
|
|
355
|
-
server: current_site.get_option('filesystem_type', 'local').downcase,
|
|
356
|
-
thumb: { w: thumb[0], h: thumb[1] },
|
|
357
|
-
aws_settings: {
|
|
358
|
-
region: current_site.get_option('filesystem_region', 'us-west-2'),
|
|
359
|
-
access_key: current_site.get_option('filesystem_s3_access_key'),
|
|
360
|
-
secret_key: current_site.get_option('filesystem_s3_secret_key'),
|
|
361
|
-
bucket: current_site.get_option('filesystem_s3_bucket_name'),
|
|
362
|
-
cloud_front: current_site.get_option('filesystem_s3_cloudfront'),
|
|
363
|
-
aws_file_upload_settings: ->(settings) { settings }, # permit to add your custom attributes for file_upload https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#upload_file-instance_method
|
|
364
|
-
aws_file_read_settings: ->(data, _s3_file) { data } # permit to read custom attributes from aws file and add to file parsed object
|
|
365
|
-
},
|
|
366
|
-
custom_uploader: nil # possibility to use custom file uploader
|
|
367
|
-
}
|
|
368
|
-
hooks_run('on_uploader', args)
|
|
369
|
-
return args[:custom_uploader] if args[:custom_uploader].present?
|
|
370
|
-
|
|
371
|
-
base_args = { current_site: current_site, thumb: args[:thumb] }
|
|
372
|
-
case args[:server]
|
|
373
|
-
when 's3', 'aws'
|
|
374
|
-
CamaleonCmsAwsUploader.new(base_args.merge(aws_settings: args[:aws_settings]), self)
|
|
375
|
-
else
|
|
376
|
-
CamaleonCmsLocalUploader.new(base_args, self)
|
|
377
|
-
end
|
|
378
|
-
}.call
|
|
17
|
+
# Message seam overrides (see CamaleonCms::UploaderPipeline). This entry point runs
|
|
18
|
+
# where `ct` and `cama_t` exist, so upload errors keep going through them: `ct` fires
|
|
19
|
+
# the `on_translation` hook, letting plugins override the text. The pipeline defaults
|
|
20
|
+
# go straight to I18n, which is all a controller can do.
|
|
21
|
+
def cama_uploader_ct(key, args = {})
|
|
22
|
+
ct(key, args)
|
|
379
23
|
end
|
|
380
24
|
|
|
381
|
-
def
|
|
382
|
-
|
|
25
|
+
def cama_uploader_t(key, args = {})
|
|
26
|
+
cama_t(key, args)
|
|
383
27
|
end
|
|
384
28
|
|
|
385
|
-
def
|
|
386
|
-
|
|
387
|
-
split_folder[-1] = slugify(split_folder[-1])
|
|
388
|
-
split_folder.join('/')
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
private
|
|
392
|
-
|
|
393
|
-
# Download remote files with SSRF guardrails: validate host/IP and reject redirects.
|
|
394
|
-
# NOTE: UserUrlValidator.validate is intentionally called here even though the
|
|
395
|
-
# crop_url controller path may have already validated the URL — this ensures
|
|
396
|
-
# every caller of cama_tmp_upload is protected (defense-in-depth).
|
|
397
|
-
def cama_download_remote_file(url)
|
|
398
|
-
validation_result = UserUrlValidator.validate(url)
|
|
399
|
-
return { error: validation_result.join(', ') } if validation_result.is_a?(Array)
|
|
400
|
-
|
|
401
|
-
uri = URI.parse(url)
|
|
402
|
-
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
403
|
-
http.open_timeout = 10
|
|
404
|
-
http.read_timeout = 10
|
|
405
|
-
http.request(Net::HTTP::Get.new(uri.request_uri.presence || '/'))
|
|
406
|
-
end
|
|
407
|
-
|
|
408
|
-
return { error: 'Redirects are not allowed for remote uploads.' } if response.is_a?(Net::HTTPRedirection)
|
|
409
|
-
|
|
410
|
-
return { error: "Unable to download remote file (HTTP #{response.code})." } unless response.is_a?(Net::HTTPSuccess)
|
|
411
|
-
|
|
412
|
-
# Enforce the site's maximum upload size to prevent memory exhaustion from oversized responses.
|
|
413
|
-
max_bytes = current_site.get_option('filesystem_max_size', 100).to_f.megabytes
|
|
414
|
-
body = response.body
|
|
415
|
-
if body.bytesize > max_bytes
|
|
416
|
-
return { error: "Remote file too large (max #{ActiveSupport::NumberHelper.number_to_human_size(max_bytes)})." }
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
ext = File.extname(uri.path.to_s)
|
|
420
|
-
tempfile = Tempfile.new(['cama-upload-url', ext], binmode: true)
|
|
421
|
-
tempfile.write(body)
|
|
422
|
-
tempfile.rewind
|
|
423
|
-
{ file: tempfile, error: nil }
|
|
424
|
-
rescue StandardError => e
|
|
425
|
-
{ error: "Unable to download remote file: #{ERB::Util.html_escape(e.message)}" }
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
def file_content_unsafe?(uploaded_io)
|
|
429
|
-
file = uploaded_io.is_a?(ActionDispatch::Http::UploadedFile) ? uploaded_io.tempfile : uploaded_io
|
|
430
|
-
file_content_unsafe = nil
|
|
431
|
-
|
|
432
|
-
file.set_encoding(Encoding::BINARY) if file.respond_to?(:binmode) && file.respond_to?(:set_encoding)
|
|
433
|
-
|
|
434
|
-
# Read the file for pattern scanning, then rewind so subsequent consumers
|
|
435
|
-
# (e.g. upload handlers) can read the full content. Failing to rewind
|
|
436
|
-
# resulted in 0-byte uploads when the file was a Tempfile (see report).
|
|
437
|
-
file_content = file.read
|
|
438
|
-
file.rewind if file.respond_to?(:rewind)
|
|
439
|
-
SUSPICIOUS_PATTERNS.each do |pattern|
|
|
440
|
-
if file_content =~ pattern
|
|
441
|
-
Rails.logger.info { "Potentially malicious content found: #{pattern.inspect}" }
|
|
442
|
-
break file_content_unsafe = pattern.inspect
|
|
443
|
-
end
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
file_content_unsafe
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
# helper for resize and crop method
|
|
450
|
-
def cama_crop_offsets_by_gravity(gravity, original_dimensions, cropped_dimensions)
|
|
451
|
-
original_width, original_height = original_dimensions
|
|
452
|
-
cropped_width, cropped_height = cropped_dimensions
|
|
453
|
-
|
|
454
|
-
vertical_offset = case gravity
|
|
455
|
-
when :north_west, :north, :north_east then 0
|
|
456
|
-
when :center, :east, :west then [((original_height - cropped_height) / 2.0).to_i, 0].max
|
|
457
|
-
when :south_west, :south, :south_east then (original_height - cropped_height).to_i
|
|
458
|
-
end
|
|
459
|
-
|
|
460
|
-
horizontal_offset = case gravity
|
|
461
|
-
when :north_west, :west, :south_west then 0
|
|
462
|
-
when :center, :north, :south then [((original_width - cropped_width) / 2.0).to_i, 0].max
|
|
463
|
-
when :north_east, :east, :south_east then (original_width - cropped_width).to_i
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
[horizontal_offset, vertical_offset]
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
# convert file path into thumb path format
|
|
470
|
-
# return the image name into thumb format: owewen.png into thumb/owen-png.png
|
|
471
|
-
def cama_parse_for_thumb_name(file_path)
|
|
472
|
-
"#{@fog_connection_hook_res[:thumb_folder_name]}/#{File.basename(file_path).parameterize}#{File.extname(file_path)}"
|
|
473
|
-
end
|
|
474
|
-
|
|
475
|
-
def clamp_to_image_dimension(value, img_size)
|
|
476
|
-
return value unless value.present? && value.to_s.include?('?')
|
|
477
|
-
|
|
478
|
-
img_size.to_f > value.sub('?', '').to_i ? value.sub('?', '') : img_size
|
|
479
|
-
end
|
|
480
|
-
|
|
481
|
-
def validate_file_format_or_error(file, formats)
|
|
482
|
-
return if cama_uploader.class.validate_file_format(file, formats)
|
|
483
|
-
|
|
484
|
-
{ error: "#{ct('file_format_error')} (#{formats})" }
|
|
29
|
+
def cama_uploader_human_size(bytes)
|
|
30
|
+
number_to_human_size(bytes)
|
|
485
31
|
end
|
|
486
32
|
end
|
|
487
33
|
end
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
module CamaleonCms
|
|
2
2
|
module UserRolesHelper
|
|
3
3
|
def cama_get_roles_values
|
|
4
|
-
@_cache_cama_get_roles_values ||= lambda
|
|
4
|
+
@_cache_cama_get_roles_values ||= lambda do
|
|
5
5
|
roles_list = CamaleonCms::UserRole::ROLES
|
|
6
|
-
# permit
|
|
7
|
-
# sample:
|
|
6
|
+
# permit adding custom roles to be listed in editing roles form
|
|
7
|
+
# sample:
|
|
8
|
+
# args[:roles_list][:manager] << {key: 'my_role_key', label: "my_custom_permission", description: "lorem ipsum"}
|
|
8
9
|
# authorize! :manage, :my_role_key
|
|
9
10
|
args = { roles_list: roles_list }
|
|
10
11
|
hooks_run('available_user_roles_list', args)
|
|
11
12
|
args[:roles_list]
|
|
12
|
-
|
|
13
|
+
end.call
|
|
13
14
|
end
|
|
14
15
|
end
|
|
15
16
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module CamaleonCms
|
|
2
|
-
class HtmlMailer < ActionMailer::Base
|
|
2
|
+
class HtmlMailer < ActionMailer::Base # rubocop:disable Rails/ApplicationMailer
|
|
3
3
|
include CamaleonCms::SiteHelper
|
|
4
4
|
include CamaleonCms::HooksHelper
|
|
5
5
|
include CamaleonCms::PluginsHelper
|
|
@@ -23,7 +23,7 @@ module CamaleonCms
|
|
|
23
23
|
layout_name: 'camaleon_cms/mailer',
|
|
24
24
|
format: 'html'
|
|
25
25
|
}.merge(data)
|
|
26
|
-
data[:cc_to] = [data[:cc_to]] if data[:cc_to].is_a?(String) ||
|
|
26
|
+
data[:cc_to] = [data[:cc_to]] if data[:cc_to].is_a?(String) || data[:cc_to].blank?
|
|
27
27
|
|
|
28
28
|
mail_data = { to: email, subject: subject }
|
|
29
29
|
if @current_site.get_option('mailer_enabled') == 1
|
|
@@ -72,7 +72,7 @@ module CamaleonCms
|
|
|
72
72
|
end
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
layout = data[:layout_name].
|
|
75
|
+
layout = data[:layout_name].presence || false
|
|
76
76
|
if data[:template_name].present? # render email with template
|
|
77
77
|
if data[:format] == 'html'
|
|
78
78
|
mail(mail_data) do |format|
|
|
@@ -92,7 +92,7 @@ module CamaleonCms
|
|
|
92
92
|
mail(mail_data) { |format| format.html { render inline: @html, layout: layout } } if data[:format] == 'html'
|
|
93
93
|
mail(mail_data) { |format| format.text { render inline: @html, layout: layout } } if data[:format] == 'txt'
|
|
94
94
|
end
|
|
95
|
-
mail(mail_data)
|
|
95
|
+
mail(mail_data) if data[:format].blank?
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
private
|
|
@@ -54,6 +54,10 @@ module CamaleonCms
|
|
|
54
54
|
safe_can :post_tags, CamaleonCms::PostType do |pt|
|
|
55
55
|
@roles_post_type[:manage_tags].to_i.include?(pt.id)
|
|
56
56
|
end
|
|
57
|
+
safe_can :post_content_unfiltered_html, CamaleonCms::PostType do |pt|
|
|
58
|
+
ids_unfiltered = @roles_post_type[:post_content_unfiltered_html] || []
|
|
59
|
+
ids_unfiltered.to_i.include?(pt.id)
|
|
60
|
+
end
|
|
57
61
|
|
|
58
62
|
safe_can :update, CamaleonCms::Post do |post|
|
|
59
63
|
pt_id = post.post_type.id
|
|
@@ -84,12 +88,7 @@ module CamaleonCms
|
|
|
84
88
|
end
|
|
85
89
|
|
|
86
90
|
# others
|
|
87
|
-
|
|
88
|
-
safe_can :manage, manager_key if @roles_manager[manager_key]
|
|
89
|
-
end
|
|
90
|
-
@roles_manager.try(:each) do |rol_manage_key, val_role|
|
|
91
|
-
safe_can :manage, rol_manage_key.to_sym if val_role.to_s.cama_true?
|
|
92
|
-
end
|
|
91
|
+
define_manage_rules
|
|
93
92
|
end
|
|
94
93
|
cannot :impersonate, CamaleonCms::User do |u|
|
|
95
94
|
u.id == user.id
|
|
@@ -119,7 +118,7 @@ module CamaleonCms
|
|
|
119
118
|
def safe_can(action, subject, &block)
|
|
120
119
|
if block_given?
|
|
121
120
|
can(action, subject) do |resource|
|
|
122
|
-
safely_false {
|
|
121
|
+
safely_false { yield(resource) }
|
|
123
122
|
end
|
|
124
123
|
else
|
|
125
124
|
# No block: can(action, subject) does not evaluate user logic; safe to call directly.
|
|
@@ -134,5 +133,14 @@ module CamaleonCms
|
|
|
134
133
|
rescue StandardError
|
|
135
134
|
false
|
|
136
135
|
end
|
|
136
|
+
|
|
137
|
+
def define_manage_rules
|
|
138
|
+
%i[media comments themes widgets nav_menu plugins users settings custom_fields select_eval].each do |resource|
|
|
139
|
+
safe_can :manage, resource if @roles_manager[resource]
|
|
140
|
+
end
|
|
141
|
+
@roles_manager.each do |rol_manage_key, val_role|
|
|
142
|
+
safe_can :manage, rol_manage_key.to_sym if val_role.to_s.cama_true?
|
|
143
|
+
end
|
|
144
|
+
end
|
|
137
145
|
end
|
|
138
146
|
end
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
module CamaleonCms
|
|
2
2
|
class Category < CamaleonCms::TermTaxonomy
|
|
3
|
-
normalize_attrs(:
|
|
3
|
+
normalize_attrs(:description)
|
|
4
4
|
|
|
5
5
|
alias_attribute :site_id, :term_group
|
|
6
6
|
alias_attribute :post_type_id, :status
|
|
7
7
|
|
|
8
|
-
default_scope { where(taxonomy: :category) }
|
|
9
8
|
scope :no_empty, -> { where('count > 0') } # return all categories that contains at least one post
|
|
10
9
|
scope :empty, -> { where(count: [0, nil]) } # return all categories that does not contain any post
|
|
11
10
|
# scope :parents, -> { where("term_taxonomy.parent_id IS NULL") }
|
|
12
11
|
|
|
13
12
|
has_many :posts, foreign_key: :objectid, through: :term_relationships, source: :object
|
|
14
|
-
has_many :children, class_name: 'CamaleonCms::Category', foreign_key: :parent_id, dependent: :destroy
|
|
15
|
-
|
|
13
|
+
has_many :children, class_name: 'CamaleonCms::Category', foreign_key: :parent_id, dependent: :destroy,
|
|
14
|
+
inverse_of: :parent
|
|
15
|
+
belongs_to :parent, class_name: 'CamaleonCms::Category', optional: true
|
|
16
16
|
belongs_to :post_type_parent, class_name: 'CamaleonCms::PostType', foreign_key: :parent_id,
|
|
17
|
-
inverse_of: :categories,
|
|
18
|
-
belongs_to :site,
|
|
17
|
+
inverse_of: :categories, optional: true
|
|
18
|
+
belongs_to :site, optional: true
|
|
19
19
|
|
|
20
20
|
before_save :set_site
|
|
21
21
|
before_destroy :set_posts_in_default_category
|
|
@@ -14,15 +14,24 @@ module CamaleonCms
|
|
|
14
14
|
# status: nil -> visible on list group fields
|
|
15
15
|
# attr_accessible :object_class, :objectid, :description, :parent_id, :count, :name, :slug,
|
|
16
16
|
# :field_order, :status, :is_repeat
|
|
17
|
+
|
|
18
|
+
# Scope metas by object_class. Meta rows are keyed by (objectid, object_class), so a custom
|
|
19
|
+
# field whose numeric id collides with another model's id (e.g. a Post) would otherwise read
|
|
20
|
+
# the wrong "_default" meta and lose its field_key. See regression: TinyMCE editor field
|
|
21
|
+
# rendered as a plain text_box on Theme settings.
|
|
22
|
+
# rubocop:disable Rails/InverseOf
|
|
17
23
|
has_many :metas, -> { where(object_class: 'CustomField') }, foreign_key: :objectid, dependent: :destroy
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
belongs_to :
|
|
24
|
+
# rubocop:enable Rails/InverseOf
|
|
25
|
+
has_many :values, class_name: 'CamaleonCms::CustomFieldsRelationship', dependent: :destroy
|
|
26
|
+
|
|
27
|
+
belongs_to :custom_field_group, class_name: 'CamaleonCms::CustomFieldGroup', foreign_key: :parent_id,
|
|
28
|
+
optional: true, inverse_of: :fields
|
|
29
|
+
belongs_to :parent, class_name: 'CamaleonCms::CustomField', optional: true
|
|
30
|
+
belongs_to :owner, polymorphic: true, foreign_key: :objectid, foreign_type: :object_class, optional: true
|
|
22
31
|
|
|
23
32
|
validates :name, :object_class, presence: true
|
|
24
|
-
|
|
25
|
-
unless: ->(o) { o.is_a?(CamaleonCms::CustomFieldGroup) }
|
|
33
|
+
validates :slug, uniqueness: { scope: %i[parent_id object_class],
|
|
34
|
+
unless: ->(o) { o.is_a?(CamaleonCms::CustomFieldGroup) } }
|
|
26
35
|
|
|
27
36
|
before_validation :before_validating
|
|
28
37
|
before_update :check_select_eval_authorization
|