camaleon_cms 2.9.2 → 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 +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 +15 -10
- data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
- data/app/apps/plugins/front_cache/front_cache_helper.rb +33 -9
- 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 +76 -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 +140 -70
- 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 +66 -50
- 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 +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/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 +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 +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 +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 -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 +52 -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 +66 -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 +20 -38
- data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
- 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 +33 -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 +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 +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 +123 -4
|
@@ -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
|
|
@@ -12,5 +12,12 @@
|
|
|
12
12
|
# Note: Rails automatically resets CurrentAttributes between requests, ensuring no leakage.
|
|
13
13
|
# In specs, call CurrentRequest.reset in before/after hooks to avoid cross-test pollution.
|
|
14
14
|
class CurrentRequest < ActiveSupport::CurrentAttributes
|
|
15
|
-
attribute :user, :site
|
|
15
|
+
attribute :user, :user_resolved, :site, :content_helper_state, :hooks_helper_state, :html_helper_state,
|
|
16
|
+
:theme_helper_state,
|
|
17
|
+
:frontend_object, :frontend_seo_settings, :frontend_current_theme, :frontend_site_current_path,
|
|
18
|
+
:frontend_visited_home, :frontend_visited_post, :frontend_visited_ajax, :frontend_visited_search,
|
|
19
|
+
:frontend_visited_post_type, :frontend_visited_tag, :frontend_visited_category,
|
|
20
|
+
:frontend_visited_profile, :frontend_user,
|
|
21
|
+
:admin_menu_items, :custom_field_elements, :extra_models_for_fields,
|
|
22
|
+
:shortcodes, :shortcodes_template, :shortcodes_descr
|
|
16
23
|
end
|
|
@@ -72,9 +72,7 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
72
72
|
'dimension' => ''
|
|
73
73
|
}.with_indifferent_access
|
|
74
74
|
if res['file_type'] == 'image' && File.extname(res['name']).downcase != '.gif'
|
|
75
|
-
res['thumb'] =
|
|
76
|
-
version_path(res['url']).sub('.svg',
|
|
77
|
-
'.jpg')
|
|
75
|
+
res['thumb'] = version_path(res['url']).sub(SVG_EXT_PATTERN, '.jpg')
|
|
78
76
|
end
|
|
79
77
|
res['key'] = File.join(res['folder_path'], res['name'])
|
|
80
78
|
@aws_settings[:aws_file_read_settings].call(res, s3_file)
|
|
@@ -100,14 +98,20 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
100
98
|
end
|
|
101
99
|
|
|
102
100
|
s3_file = bucket.object(key.slice(1..-1))
|
|
101
|
+
# Security (audit 2026-08-11 M5): a private-mode upload must be owner-only. Storing it 'public-read'
|
|
102
|
+
# (like a public file) left it world-readable at a guessable s3://bucket/private/<name> URL,
|
|
103
|
+
# bypassing the download_private_file gate. That gate fetches via the authenticated S3 API, so a
|
|
104
|
+
# 'private' ACL does not affect legitimate serving.
|
|
105
|
+
acl = is_private_uploader? ? 'private' : 'public-read'
|
|
103
106
|
s3_file.upload_file(
|
|
104
|
-
uploaded_io_or_file_path.is_a?(String) ? uploaded_io_or_file_path : uploaded_io_or_file_path.path,
|
|
107
|
+
uploaded_io_or_file_path.is_a?(String) ? uploaded_io_or_file_path : uploaded_io_or_file_path.path,
|
|
108
|
+
@aws_settings[:aws_file_upload_settings].call({ acl: acl })
|
|
105
109
|
)
|
|
106
110
|
res = cache_item(file_parse(s3_file)) unless args[:is_thumb]
|
|
107
111
|
res
|
|
108
112
|
end
|
|
109
113
|
|
|
110
|
-
#
|
|
114
|
+
# Add a new folder to AWS with :key
|
|
111
115
|
def add_folder(key)
|
|
112
116
|
return { error: 'Invalid folder path' } unless valid_folder_path?(key)
|
|
113
117
|
|
|
@@ -118,17 +122,17 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
118
122
|
cache_item(file_parse(s3_file))
|
|
119
123
|
end
|
|
120
124
|
|
|
121
|
-
#
|
|
125
|
+
# Delete a folder in AWS with :key
|
|
122
126
|
def delete_folder(key)
|
|
123
127
|
return { error: 'Invalid folder path' } unless valid_folder_path?(key)
|
|
124
128
|
|
|
125
129
|
key = "#{@aws_settings['inner_folder']}/#{key}" if @aws_settings['inner_folder'].present?
|
|
126
130
|
key = key.cama_fix_media_key
|
|
127
131
|
bucket.objects(prefix: key.slice(1..-1) << '/').delete
|
|
128
|
-
get_media_collection.
|
|
132
|
+
get_media_collection.by_key(key).take.destroy
|
|
129
133
|
end
|
|
130
134
|
|
|
131
|
-
#
|
|
135
|
+
# Delete the `:key` file in AWS
|
|
132
136
|
def delete_file(key)
|
|
133
137
|
return { error: 'Invalid file path' } unless valid_folder_path?(key)
|
|
134
138
|
|
|
@@ -140,10 +144,27 @@ class CamaleonCmsAwsUploader < CamaleonCmsUploader
|
|
|
140
144
|
''
|
|
141
145
|
end
|
|
142
146
|
@instance.hooks_run('after_delete', key)
|
|
143
|
-
get_media_collection.
|
|
147
|
+
get_media_collection.by_key(key).take.destroy
|
|
144
148
|
end
|
|
145
149
|
|
|
146
|
-
#
|
|
150
|
+
# Security (audit 2026-08-11 M5 follow-up): uploads stored before the private-ACL fix kept a
|
|
151
|
+
# world-readable ACL under the private prefix. Sweep them back to owner-only and return the count.
|
|
152
|
+
# The prefix mirrors setup_private_folder -- <inner_folder>/private/ -- normalized exactly like
|
|
153
|
+
# add_file keys (leading slash added by cama_fix_media_key, then stripped for S3), with a trailing
|
|
154
|
+
# slash so a sibling folder such as private_x is not swept.
|
|
155
|
+
def repair_private_acls!
|
|
156
|
+
inner_folder = @aws_settings['inner_folder'].to_s
|
|
157
|
+
inner_folder = "#{inner_folder}/#{PRIVATE_DIRECTORY}" unless is_private_uploader?
|
|
158
|
+
prefix = "#{inner_folder.cama_fix_media_key.slice(1..-1)}/"
|
|
159
|
+
repaired = 0
|
|
160
|
+
bucket.objects(prefix: prefix).each do |object|
|
|
161
|
+
object.acl.put(acl: 'private')
|
|
162
|
+
repaired += 1
|
|
163
|
+
end
|
|
164
|
+
repaired
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Initialize a bucket with AWS configurations
|
|
147
168
|
# return: (AWS Bucket object)
|
|
148
169
|
def bucket
|
|
149
170
|
@bucket ||= lambda {
|
|
@@ -2,13 +2,13 @@ class CamaleonCmsLocalUploader < CamaleonCmsUploader
|
|
|
2
2
|
def after_initialize
|
|
3
3
|
@root_folder = @current_site.upload_directory
|
|
4
4
|
|
|
5
|
-
FileUtils.mkdir_p(@root_folder)
|
|
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
|
|
@@ -5,7 +5,7 @@ module CamaleonCms
|
|
|
5
5
|
|
|
6
6
|
slug_array = record.slug.to_s.translations_array
|
|
7
7
|
ptype = record.post_type
|
|
8
|
-
return
|
|
8
|
+
return if ptype.blank? # only for posts that belongs to a post type model
|
|
9
9
|
|
|
10
10
|
post_table = CamaleonCms::Post.table_name
|
|
11
11
|
|
|
@@ -27,7 +27,7 @@ module CamaleonCms
|
|
|
27
27
|
.where.not(id: record.id)
|
|
28
28
|
.where.not(status: %i[draft draft_child trash])
|
|
29
29
|
unless posts.empty?
|
|
30
|
-
|
|
30
|
+
message =
|
|
31
31
|
if slug_array.size > 1
|
|
32
32
|
"#{I18n.t('camaleon_cms.admin.post.message.requires_different_slug')}: #{posts.pluck(:slug).map do |slug|
|
|
33
33
|
record.slug.to_s.translations.map do |lng, r_slug|
|
|
@@ -37,14 +37,15 @@ module CamaleonCms
|
|
|
37
37
|
else
|
|
38
38
|
"#{I18n.t('camaleon_cms.admin.post.message.requires_different_slug')}: #{record.slug} "
|
|
39
39
|
end
|
|
40
|
+
record.errors.add(:base, message)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
# avoid recursive page parent
|
|
43
44
|
return unless record.post_parent.present? && ptype.manage_hierarchy? &&
|
|
44
45
|
record.parents.cama_pluck(:id).include?(record.id)
|
|
45
46
|
|
|
46
|
-
record.errors
|
|
47
|
-
|
|
47
|
+
record.errors.add(:base, I18n.t('camaleon_cms.admin.post.message.recursive_hierarchy',
|
|
48
|
+
default: 'Parent Post Recursive'))
|
|
48
49
|
end
|
|
49
50
|
end
|
|
50
51
|
end
|
|
@@ -11,7 +11,7 @@ module CamaleonCms
|
|
|
11
11
|
|
|
12
12
|
return unless slug_exists
|
|
13
13
|
|
|
14
|
-
record.errors
|
|
14
|
+
record.errors.add(:base, I18n.t('camaleon_cms.admin.post.message.requires_different_slug').to_s)
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
17
|
end
|