camaleon_cms 2.9.0 → 2.9.2
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 +12 -5
- data/app/apps/plugins/front_cache/admin_controller.rb +1 -0
- data/app/apps/plugins/front_cache/front_cache_helper.rb +23 -14
- data/app/apps/plugins/visibility_post/visibility_post_helper.rb +1 -1
- data/app/apps/themes/default/views/category.html.erb +1 -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/views/category.html.erb +1 -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/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +22 -5
- data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +4 -2
- data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +3 -3
- data/app/controllers/camaleon_cms/admin/appearances/widgets/sidebar_controller.rb +2 -2
- data/app/controllers/camaleon_cms/admin/categories_controller.rb +9 -5
- data/app/controllers/camaleon_cms/admin/media_controller.rb +18 -5
- data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +7 -4
- data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +1 -1
- data/app/controllers/camaleon_cms/admin/posts_controller.rb +7 -4
- data/app/controllers/camaleon_cms/admin/sessions_controller.rb +2 -2
- data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +33 -11
- data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +13 -4
- data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +7 -4
- data/app/controllers/camaleon_cms/admin/settings_controller.rb +7 -4
- data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +2 -2
- data/app/controllers/camaleon_cms/admin/users_controller.rb +23 -14
- data/app/controllers/camaleon_cms/admin_controller.rb +8 -0
- data/app/controllers/camaleon_cms/apps/plugins_admin_controller.rb +5 -0
- data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +29 -0
- data/app/decorators/camaleon_cms/post_decorator.rb +1 -1
- data/app/decorators/camaleon_cms/user_decorator.rb +1 -1
- data/app/helpers/camaleon_cms/admin/application_helper.rb +17 -17
- data/app/helpers/camaleon_cms/admin/post_type_helper.rb +25 -22
- data/app/helpers/camaleon_cms/comment_helper.rb +74 -40
- data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +1 -1
- data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +7 -7
- data/app/helpers/camaleon_cms/html_helper.rb +15 -1
- data/app/helpers/camaleon_cms/session_helper.rb +13 -1
- data/app/helpers/camaleon_cms/site_helper.rb +16 -3
- data/app/helpers/camaleon_cms/uploader_helper.rb +102 -51
- data/app/models/camaleon_cms/ability.rb +54 -102
- data/app/models/camaleon_cms/category.rb +2 -0
- data/app/models/camaleon_cms/custom_field.rb +14 -0
- data/app/models/camaleon_cms/custom_field_group.rb +38 -1
- data/app/models/camaleon_cms/custom_fields_relationship.rb +1 -1
- data/app/models/camaleon_cms/meta.rb +4 -0
- data/app/models/camaleon_cms/nav_menu.rb +2 -0
- data/app/models/camaleon_cms/nav_menu_item.rb +2 -0
- data/app/models/camaleon_cms/plugin.rb +2 -0
- data/app/models/camaleon_cms/post.rb +1 -1
- data/app/models/camaleon_cms/post_comment.rb +4 -0
- data/app/models/camaleon_cms/post_tag.rb +2 -0
- data/app/models/camaleon_cms/post_type.rb +3 -1
- data/app/models/camaleon_cms/site.rb +2 -0
- data/app/models/camaleon_cms/term_taxonomy.rb +1 -23
- data/app/models/camaleon_cms/theme.rb +2 -0
- data/app/models/camaleon_cms/user_role.rb +13 -0
- data/app/models/camaleon_cms/widget/main.rb +2 -0
- data/app/models/camaleon_cms/widget/sidebar.rb +2 -0
- data/app/models/camaleon_record.rb +40 -0
- data/app/models/concerns/camaleon_cms/custom_fields_read.rb +7 -7
- data/app/models/concerns/camaleon_cms/metas.rb +10 -6
- data/app/models/concerns/camaleon_cms/normalize_attrs.rb +26 -0
- data/app/models/concerns/camaleon_cms/user_methods.rb +6 -2
- data/app/models/current_request.rb +16 -0
- data/app/uploaders/camaleon_cms_aws_uploader.rb +8 -1
- data/app/validators/camaleon_cms/post_uniq_validator.rb +21 -8
- 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 +1 -1
- 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/post_tags/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
- data/app/views/camaleon_cms/admin/posts/index.html.erb +3 -3
- data/app/views/camaleon_cms/admin/search.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +23 -2
- 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 +6 -5
- 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/sites/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/user_roles/form.html.erb +79 -5
- data/app/views/camaleon_cms/admin/user_roles/index.html.erb +1 -1
- data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
- data/app/views/layouts/camaleon_cms/admin/_flash_messages.html.erb +2 -2
- data/config/initializers/custom_initializers.rb +2 -2
- data/config/locales/camaleon_cms/admin/ar.yml +6 -2
- data/config/locales/camaleon_cms/admin/de.yml +6 -2
- data/config/locales/camaleon_cms/admin/en.yml +6 -2
- data/config/locales/camaleon_cms/admin/es.yml +6 -2
- data/config/locales/camaleon_cms/admin/fr.yml +6 -2
- data/config/locales/camaleon_cms/admin/it.yml +6 -2
- data/config/locales/camaleon_cms/admin/nl.yml +7 -2
- data/config/locales/camaleon_cms/admin/pt-BR.yml +6 -2
- data/config/locales/camaleon_cms/admin/pt.yml +6 -2
- data/config/locales/camaleon_cms/admin/ru.yml +6 -2
- data/config/locales/camaleon_cms/admin/uk.yml +6 -2
- data/config/locales/camaleon_cms/admin/zh-CH.yml +6 -2
- 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/version.rb +1 -1
- data/lib/ext/string.rb +3 -3
- data/lib/plugin_routes.rb +6 -6
- data/lib/tasks/custom_fields_roles.rake +56 -0
- metadata +65 -8
|
@@ -140,7 +140,21 @@ module CamaleonCms
|
|
|
140
140
|
|
|
141
141
|
# execute translation for value if this value is like: t(admin.my_text) ==> My Text
|
|
142
142
|
def cama_print_i18n_value(value)
|
|
143
|
-
value
|
|
143
|
+
return value unless value.is_a?(String)
|
|
144
|
+
return value unless value.start_with?('t(') && value.end_with?(')')
|
|
145
|
+
|
|
146
|
+
# Use an exclusive end index to strip the trailing ')' without nil-coercion.
|
|
147
|
+
key = value[2...-1].strip
|
|
148
|
+
# If the expression uses matching single/double quotes, unwrap the key before translation;
|
|
149
|
+
# the quoted form still only accepts simple i18n key characters: a-z, A-Z, 0-9, _, ., and -.
|
|
150
|
+
quoted_key_match = key.match(/\A(['"])([a-zA-Z0-9_.-]+)\1\z/)
|
|
151
|
+
key = quoted_key_match[2] if quoted_key_match
|
|
152
|
+
|
|
153
|
+
# Only translate simple i18n keys so arbitrary Ruby is never evaluated.
|
|
154
|
+
# Allowed chars: a-z, A-Z, 0-9, _, ., and -.
|
|
155
|
+
return value unless key.match?(/\A[a-zA-Z0-9_.-]+\z/)
|
|
156
|
+
|
|
157
|
+
I18n.t(key)
|
|
144
158
|
end
|
|
145
159
|
end
|
|
146
160
|
end
|
|
@@ -26,7 +26,7 @@ module CamaleonCms
|
|
|
26
26
|
if redirect_url.present?
|
|
27
27
|
redirect_to redirect_url
|
|
28
28
|
elsif (return_to = cookies.delete(:return_to)).present?
|
|
29
|
-
redirect_to return_to
|
|
29
|
+
redirect_to safe_redirect_url(return_to) || cama_admin_dashboard_path
|
|
30
30
|
else
|
|
31
31
|
redirect_to cama_admin_dashboard_path
|
|
32
32
|
end
|
|
@@ -167,6 +167,18 @@ module CamaleonCms
|
|
|
167
167
|
|
|
168
168
|
private
|
|
169
169
|
|
|
170
|
+
# validate redirect url to prevent open redirect attacks
|
|
171
|
+
def safe_redirect_url(url)
|
|
172
|
+
return if url.blank?
|
|
173
|
+
|
|
174
|
+
uri = URI.parse(url)
|
|
175
|
+
return if uri.host.present? && uri.host != request.host
|
|
176
|
+
|
|
177
|
+
url
|
|
178
|
+
rescue URI::InvalidURIError
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
170
182
|
# calculate the current user for API
|
|
171
183
|
def cama_calc_api_current_user
|
|
172
184
|
begin
|
|
@@ -2,9 +2,21 @@ module CamaleonCms
|
|
|
2
2
|
module SiteHelper
|
|
3
3
|
# return current site or assign a site as a current site
|
|
4
4
|
def current_site(site = nil)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
if site.present?
|
|
6
|
+
@current_site = site.decorate
|
|
7
|
+
CurrentRequest.site = @current_site
|
|
8
|
+
return @current_site
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
if defined?($current_site)
|
|
12
|
+
CurrentRequest.site = $current_site
|
|
13
|
+
return $current_site
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if defined?(@current_site) && @current_site.present?
|
|
17
|
+
CurrentRequest.site = @current_site
|
|
18
|
+
return @current_site
|
|
19
|
+
end
|
|
8
20
|
|
|
9
21
|
if PluginRoutes.get_sites.size == 1
|
|
10
22
|
site = begin
|
|
@@ -35,6 +47,7 @@ module CamaleonCms
|
|
|
35
47
|
Rails.logger.error 'Camaleon CMS - Please define your current site: $current_site = CamaleonCms::Site.first.decorate or map your domains: https://camaleon.website/documentation/category/139779-examples/how.html'.cama_log_style(:red)
|
|
36
48
|
end
|
|
37
49
|
@current_site = r[:site]
|
|
50
|
+
CurrentRequest.site = @current_site
|
|
38
51
|
end
|
|
39
52
|
|
|
40
53
|
# return current theme model for current site
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
3
6
|
module CamaleonCms
|
|
4
7
|
module UploaderHelper
|
|
5
8
|
UNSAFE_EVENT_PATTERNS = %w[
|
|
@@ -80,21 +83,18 @@ module CamaleonCms
|
|
|
80
83
|
thumb_size: nil
|
|
81
84
|
}.merge!(settings)
|
|
82
85
|
hooks_run('before_upload', settings)
|
|
83
|
-
res = { error: nil }
|
|
84
86
|
|
|
85
87
|
# guard against path traversal
|
|
86
88
|
return { error: 'Invalid file path' } unless cama_uploader.valid_folder_path?(settings[:folder])
|
|
87
89
|
|
|
88
90
|
# formats validations
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
91
|
+
err = validate_file_format_or_error(uploaded_io.path, settings[:formats])
|
|
92
|
+
return err if err
|
|
92
93
|
|
|
93
94
|
# file size validations
|
|
94
95
|
if settings[:maximum] < settings[:file_size]
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return res
|
|
96
|
+
max_size = number_to_human_size(settings[:maximum])
|
|
97
|
+
return { error: "#{ct('file_size_exceeded', default: 'File size exceeded')} (#{max_size})" }
|
|
98
98
|
end
|
|
99
99
|
# save file
|
|
100
100
|
key = File.join(settings[:folder], settings[:filename]).to_s.cama_fix_slash
|
|
@@ -120,9 +120,7 @@ module CamaleonCms
|
|
|
120
120
|
hooks_run('after_upload', settings)
|
|
121
121
|
|
|
122
122
|
# temporal file upload (always put as local for temporal files)
|
|
123
|
-
if settings[:temporal_time] > 0
|
|
124
|
-
CamaleonCmsUploader.delete_block.call(settings, cama_uploader, key)
|
|
125
|
-
end
|
|
123
|
+
CamaleonCmsUploader.delete_block.call(settings, cama_uploader, key) if settings[:temporal_time] > 0
|
|
126
124
|
|
|
127
125
|
res
|
|
128
126
|
end
|
|
@@ -131,9 +129,8 @@ module CamaleonCms
|
|
|
131
129
|
# key: key of the current file
|
|
132
130
|
# the thumbnail will be saved in my_images/my_img.png => my_images/thumb/my_img.png
|
|
133
131
|
def cama_uploader_generate_thumbnail(uploaded_io, key, thumb_size = nil, remove_source = false)
|
|
134
|
-
w = cama_uploader.thumb[:w]
|
|
135
|
-
h = cama_uploader.thumb[:h]
|
|
136
|
-
w, h = thumb_size.split('x') if thumb_size.present?
|
|
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]
|
|
137
134
|
uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
|
|
138
135
|
path_thumb = cama_resize_and_crop(uploaded_io.path, w, h)
|
|
139
136
|
thumb = cama_uploader.add_file(path_thumb, cama_uploader.version_path(key).sub('.svg', '.jpg'), is_thumb: true,
|
|
@@ -187,11 +184,10 @@ module CamaleonCms
|
|
|
187
184
|
# (false => crop the image with this dimension)
|
|
188
185
|
# replace: Boolean (replace current image or create another file)
|
|
189
186
|
def cama_crop_image(file_path, w = nil, h = nil, w_offset = 0, h_offset = 0, resize = false, replace = true)
|
|
190
|
-
force = ''
|
|
191
|
-
force = '!' if w.present? && h.present? && !w.include?('?') && !h.include?('?')
|
|
187
|
+
force = w.present? && h.present? && !w.include?('?') && !h.include?('?') ? '!' : ''
|
|
192
188
|
img = MiniMagick::Image.open(file_path)
|
|
193
|
-
w =
|
|
194
|
-
h =
|
|
189
|
+
w = clamp_to_image_dimension(w, img[:width])
|
|
190
|
+
h = clamp_to_image_dimension(h, img[:height])
|
|
195
191
|
data = { img: img, w: w, h: h, w_offset: w_offset, h_offset: h_offset, resize: resize, replace: replace }
|
|
196
192
|
hooks_run('before_crop_image', data)
|
|
197
193
|
data[:img].combine_options do |i|
|
|
@@ -199,11 +195,8 @@ module CamaleonCms
|
|
|
199
195
|
i.crop "#{w if w.present?}x#{h if h.present?}+#{w_offset}+#{h_offset}#{force}" unless data[:resize]
|
|
200
196
|
end
|
|
201
197
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
ext = File.extname(file_path)
|
|
205
|
-
res = file_path.gsub(ext, "_crop#{ext}")
|
|
206
|
-
end
|
|
198
|
+
ext = File.extname(file_path)
|
|
199
|
+
res = data[:replace] ? file_path : file_path.gsub(ext, "_crop#{ext}")
|
|
207
200
|
data[:img].write res
|
|
208
201
|
res
|
|
209
202
|
end
|
|
@@ -228,8 +221,8 @@ module CamaleonCms
|
|
|
228
221
|
file.sub! '.svg', '.jpg'
|
|
229
222
|
settings[:output_name]&.sub!('.svg', '.jpg')
|
|
230
223
|
end
|
|
231
|
-
w =
|
|
232
|
-
h =
|
|
224
|
+
w = clamp_to_image_dimension(w, img[:width])
|
|
225
|
+
h = clamp_to_image_dimension(h, img[:height])
|
|
233
226
|
w_original = img[:width].to_f
|
|
234
227
|
h_original = img[:height].to_f
|
|
235
228
|
w = w.to_i if w.present?
|
|
@@ -280,41 +273,50 @@ module CamaleonCms
|
|
|
280
273
|
tmp_path = args[:path] || File.join(Rails.public_path, 'tmp', current_site.id.to_s).to_s
|
|
281
274
|
FileUtils.mkdir_p(tmp_path) unless Dir.exist?(tmp_path)
|
|
282
275
|
saved = false
|
|
276
|
+
downloaded_tmp_file = nil
|
|
283
277
|
if uploaded_io.is_a?(String) && uploaded_io.start_with?('data:') # create tmp file using base64 format
|
|
284
278
|
_tmp_name = args[:name]
|
|
285
279
|
return { error: cama_t('camaleon_cms.admin.media.name_required').to_s } unless params[:name].present?
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
280
|
+
|
|
281
|
+
err = validate_file_format_or_error(_tmp_name, args[:formats])
|
|
282
|
+
return err if err
|
|
289
283
|
|
|
290
284
|
path = uploader_verify_name(File.join(tmp_path, _tmp_name))
|
|
291
285
|
File.open(path, 'wb') { |f| f.write(Base64.decode64(uploaded_io.split(';base64,').last)) }
|
|
292
286
|
uploaded_io = File.open(path)
|
|
293
287
|
saved = true
|
|
294
288
|
elsif uploaded_io.is_a?(String) && (uploaded_io.start_with?('http://') || uploaded_io.start_with?('https://'))
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
)
|
|
289
|
+
err = validate_file_format_or_error(uploaded_io, args[:formats])
|
|
290
|
+
return err if err
|
|
298
291
|
|
|
299
292
|
if uploaded_io.include?(current_site.the_url(locale: nil))
|
|
300
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
|
|
301
300
|
end
|
|
302
|
-
_tmp_name = uploaded_io.
|
|
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
|
|
303
306
|
args[:name] = args[:name] || _tmp_name
|
|
304
|
-
uploaded_io = URI(uploaded_io).open
|
|
305
307
|
end
|
|
306
308
|
uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
309
|
+
err = validate_file_format_or_error(_tmp_name || uploaded_io.path, args[:formats])
|
|
310
|
+
return err if err
|
|
310
311
|
|
|
311
|
-
|
|
312
|
+
actual_size = begin
|
|
312
313
|
uploaded_io.size
|
|
313
314
|
rescue StandardError
|
|
314
315
|
File.size(uploaded_io)
|
|
315
316
|
end
|
|
316
|
-
|
|
317
|
-
|
|
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})" }
|
|
318
320
|
end
|
|
319
321
|
|
|
320
322
|
name = args[:name] || uploaded_io&.original_filename || uploaded_io.path.split('/').last
|
|
@@ -323,14 +325,17 @@ module CamaleonCms
|
|
|
323
325
|
File.open(path, 'wb') { |f| f.write(uploaded_io.read) } unless saved
|
|
324
326
|
path = cama_resize_upload(path, args[:dimension]) if args[:dimension].present?
|
|
325
327
|
{ file_path: path, error: nil }
|
|
328
|
+
ensure
|
|
329
|
+
downloaded_tmp_file&.close!
|
|
326
330
|
end
|
|
327
331
|
|
|
328
332
|
# resize image if the format is correct
|
|
329
333
|
# return resized file path
|
|
330
334
|
def cama_resize_upload(image_path, dimension, args = {})
|
|
331
335
|
if cama_uploader.class.validate_file_format(image_path, 'image') && dimension.present?
|
|
332
|
-
|
|
333
|
-
|
|
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',
|
|
334
339
|
replace: true, gravity: :north_east }.merge!(args)
|
|
335
340
|
hooks_run('on_uploader_resize', r)
|
|
336
341
|
image_path = if r[:w].present? && r[:h].present?
|
|
@@ -355,25 +360,20 @@ module CamaleonCms
|
|
|
355
360
|
secret_key: current_site.get_option('filesystem_s3_secret_key'),
|
|
356
361
|
bucket: current_site.get_option('filesystem_s3_bucket_name'),
|
|
357
362
|
cloud_front: current_site.get_option('filesystem_s3_cloudfront'),
|
|
358
|
-
aws_file_upload_settings:
|
|
359
|
-
|
|
360
|
-
}, # permit to add your custom attributes for file_upload https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#upload_file-instance_method
|
|
361
|
-
aws_file_read_settings: lambda { |data, _s3_file|
|
|
362
|
-
data
|
|
363
|
-
} # permit to read custom attributes from aws file and add to file parsed object
|
|
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
|
|
364
365
|
},
|
|
365
366
|
custom_uploader: nil # possibility to use custom file uploader
|
|
366
367
|
}
|
|
367
368
|
hooks_run('on_uploader', args)
|
|
368
369
|
return args[:custom_uploader] if args[:custom_uploader].present?
|
|
369
370
|
|
|
371
|
+
base_args = { current_site: current_site, thumb: args[:thumb] }
|
|
370
372
|
case args[:server]
|
|
371
373
|
when 's3', 'aws'
|
|
372
|
-
CamaleonCmsAwsUploader.new(
|
|
373
|
-
{ current_site: current_site, thumb: args[:thumb], aws_settings: args[:aws_settings] }, self
|
|
374
|
-
)
|
|
374
|
+
CamaleonCmsAwsUploader.new(base_args.merge(aws_settings: args[:aws_settings]), self)
|
|
375
375
|
else
|
|
376
|
-
CamaleonCmsLocalUploader.new(
|
|
376
|
+
CamaleonCmsLocalUploader.new(base_args, self)
|
|
377
377
|
end
|
|
378
378
|
}.call
|
|
379
379
|
end
|
|
@@ -384,19 +384,58 @@ module CamaleonCms
|
|
|
384
384
|
|
|
385
385
|
def slugify_folder(val)
|
|
386
386
|
split_folder = val.split('/')
|
|
387
|
-
split_folder[-1] = slugify(split_folder
|
|
387
|
+
split_folder[-1] = slugify(split_folder[-1])
|
|
388
388
|
split_folder.join('/')
|
|
389
389
|
end
|
|
390
390
|
|
|
391
391
|
private
|
|
392
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
|
+
|
|
393
428
|
def file_content_unsafe?(uploaded_io)
|
|
394
429
|
file = uploaded_io.is_a?(ActionDispatch::Http::UploadedFile) ? uploaded_io.tempfile : uploaded_io
|
|
395
430
|
file_content_unsafe = nil
|
|
396
431
|
|
|
397
432
|
file.set_encoding(Encoding::BINARY) if file.respond_to?(:binmode) && file.respond_to?(:set_encoding)
|
|
398
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).
|
|
399
437
|
file_content = file.read
|
|
438
|
+
file.rewind if file.respond_to?(:rewind)
|
|
400
439
|
SUSPICIOUS_PATTERNS.each do |pattern|
|
|
401
440
|
if file_content =~ pattern
|
|
402
441
|
Rails.logger.info { "Potentially malicious content found: #{pattern.inspect}" }
|
|
@@ -432,5 +471,17 @@ module CamaleonCms
|
|
|
432
471
|
def cama_parse_for_thumb_name(file_path)
|
|
433
472
|
"#{@fog_connection_hook_res[:thumb_folder_name]}/#{File.basename(file_path).parameterize}#{File.extname(file_path)}"
|
|
434
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})" }
|
|
485
|
+
end
|
|
435
486
|
end
|
|
436
487
|
end
|
|
@@ -12,9 +12,16 @@ module CamaleonCms
|
|
|
12
12
|
can :read, :all
|
|
13
13
|
else
|
|
14
14
|
# conditions:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
# Fetch the role record fresh from the database for the current site to
|
|
16
|
+
# ensure up-to-date role meta (avoid stale cached role objects during
|
|
17
|
+
# tests or runtime meta updates).
|
|
18
|
+
current_user_role = if current_site.present?
|
|
19
|
+
current_site.user_roles.where(slug: user.role).first
|
|
20
|
+
else
|
|
21
|
+
user.get_role(current_site)
|
|
22
|
+
end || current_site.user_roles.new
|
|
23
|
+
@roles_manager = current_user_role.get_meta("_manager_#{current_site.id}", {}) || {}
|
|
24
|
+
@roles_post_type = current_user_role.get_meta("_post_type_#{current_site.id}", {}) || {}
|
|
18
25
|
|
|
19
26
|
ids_publish = @roles_post_type[:publish] || []
|
|
20
27
|
ids_edit = @roles_post_type[:edit] || []
|
|
@@ -24,83 +31,45 @@ module CamaleonCms
|
|
|
24
31
|
ids_delete_other = @roles_post_type[:delete_other] || []
|
|
25
32
|
ids_delete_publish = @roles_post_type[:delete_publish] || []
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
safe_can :posts, CamaleonCms::PostType do |pt|
|
|
28
35
|
(ids_edit + ids_edit_other + ids_edit_publish).to_i.include?(pt.id)
|
|
29
|
-
rescue StandardError
|
|
30
|
-
false
|
|
31
36
|
end
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
safe_can :create_post, CamaleonCms::PostType do |pt|
|
|
34
39
|
ids_edit.to_i.include?(pt.id)
|
|
35
|
-
rescue StandardError
|
|
36
|
-
false
|
|
37
40
|
end
|
|
38
|
-
|
|
41
|
+
safe_can :publish_post, CamaleonCms::PostType do |pt|
|
|
39
42
|
ids_publish.to_i.include?(pt.id)
|
|
40
|
-
rescue StandardError
|
|
41
|
-
false
|
|
42
43
|
end
|
|
43
|
-
|
|
44
|
+
safe_can :edit_other, CamaleonCms::PostType do |pt|
|
|
44
45
|
ids_edit_other.to_i.include?(pt.id)
|
|
45
|
-
rescue StandardError
|
|
46
|
-
false
|
|
47
46
|
end
|
|
48
|
-
|
|
47
|
+
safe_can :edit_publish, CamaleonCms::PostType do |pt|
|
|
49
48
|
ids_edit_publish.to_i.include?(pt.id)
|
|
50
|
-
rescue StandardError
|
|
51
|
-
false
|
|
52
49
|
end
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
safe_can :categories, CamaleonCms::PostType do |pt|
|
|
55
52
|
@roles_post_type[:manage_categories].to_i.include?(pt.id)
|
|
56
|
-
rescue StandardError
|
|
57
|
-
false
|
|
58
53
|
end
|
|
59
|
-
|
|
54
|
+
safe_can :post_tags, CamaleonCms::PostType do |pt|
|
|
60
55
|
@roles_post_type[:manage_tags].to_i.include?(pt.id)
|
|
61
|
-
rescue StandardError
|
|
62
|
-
false
|
|
63
56
|
end
|
|
64
57
|
|
|
65
|
-
|
|
58
|
+
safe_can :update, CamaleonCms::Post do |post|
|
|
66
59
|
pt_id = post.post_type.id
|
|
67
60
|
r = false
|
|
68
|
-
r ||=
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
false
|
|
72
|
-
end
|
|
73
|
-
r ||= begin
|
|
74
|
-
ids_edit_publish.to_i.include?(pt_id) && post.published?
|
|
75
|
-
rescue StandardError
|
|
76
|
-
false
|
|
77
|
-
end
|
|
78
|
-
r ||= begin
|
|
79
|
-
ids_edit_other.to_i.include?(pt_id) && post.user_id != user.id
|
|
80
|
-
rescue StandardError
|
|
81
|
-
false
|
|
82
|
-
end
|
|
61
|
+
r ||= ids_edit.to_i.include?(pt_id) && post.user_id == user.id
|
|
62
|
+
r ||= ids_edit_publish.to_i.include?(pt_id) && post.published?
|
|
63
|
+
r ||= ids_edit_other.to_i.include?(pt_id) && post.user_id != user.id
|
|
83
64
|
r
|
|
84
65
|
end
|
|
85
66
|
|
|
86
|
-
|
|
67
|
+
safe_can :destroy, CamaleonCms::Post do |post|
|
|
87
68
|
pt_id = post.post_type.id
|
|
88
69
|
r = false
|
|
89
|
-
r ||=
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
false
|
|
93
|
-
end
|
|
94
|
-
r ||= begin
|
|
95
|
-
ids_delete_publish.to_i.include?(pt_id) && post.published?
|
|
96
|
-
rescue StandardError
|
|
97
|
-
false
|
|
98
|
-
end
|
|
99
|
-
r ||= begin
|
|
100
|
-
ids_delete_other.to_i.include?(pt_id) && post.user_id != user.id
|
|
101
|
-
rescue StandardError
|
|
102
|
-
false
|
|
103
|
-
end
|
|
70
|
+
r ||= ids_delete.to_i.include?(pt_id) && post.user_id == user.id
|
|
71
|
+
r ||= ids_delete_publish.to_i.include?(pt_id) && post.published?
|
|
72
|
+
r ||= ids_delete_other.to_i.include?(pt_id) && post.user_id != user.id
|
|
104
73
|
r
|
|
105
74
|
end
|
|
106
75
|
|
|
@@ -109,59 +78,17 @@ module CamaleonCms
|
|
|
109
78
|
@roles_post_type.each do |k, v|
|
|
110
79
|
next if %w[edit edit_other edit_publish publish manage_categories].include?(k.to_s)
|
|
111
80
|
|
|
112
|
-
|
|
81
|
+
safe_can k.to_sym, CamaleonCms::PostType do |pt|
|
|
113
82
|
v.include?(pt.id.to_s)
|
|
114
|
-
rescue StandardError
|
|
115
|
-
false
|
|
116
83
|
end
|
|
117
84
|
end
|
|
118
85
|
|
|
119
86
|
# others
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
rescue StandardError
|
|
123
|
-
false
|
|
124
|
-
end
|
|
125
|
-
begin
|
|
126
|
-
can :manage, :comments if @roles_manager[:comments]
|
|
127
|
-
rescue StandardError
|
|
128
|
-
false
|
|
129
|
-
end
|
|
130
|
-
# can :manage, :forms if @roles_manager[:forms] rescue false
|
|
131
|
-
begin
|
|
132
|
-
can :manage, :themes if @roles_manager[:themes]
|
|
133
|
-
rescue StandardError
|
|
134
|
-
false
|
|
135
|
-
end
|
|
136
|
-
begin
|
|
137
|
-
can :manage, :widgets if @roles_manager[:widgets]
|
|
138
|
-
rescue StandardError
|
|
139
|
-
false
|
|
140
|
-
end
|
|
141
|
-
begin
|
|
142
|
-
can :manage, :nav_menu if @roles_manager[:nav_menu]
|
|
143
|
-
rescue StandardError
|
|
144
|
-
false
|
|
145
|
-
end
|
|
146
|
-
begin
|
|
147
|
-
can :manage, :plugins if @roles_manager[:plugins]
|
|
148
|
-
rescue StandardError
|
|
149
|
-
false
|
|
150
|
-
end
|
|
151
|
-
begin
|
|
152
|
-
can :manage, :users if @roles_manager[:users]
|
|
153
|
-
rescue StandardError
|
|
154
|
-
false
|
|
155
|
-
end
|
|
156
|
-
begin
|
|
157
|
-
can :manage, :settings if @roles_manager[:settings]
|
|
158
|
-
rescue StandardError
|
|
159
|
-
false
|
|
87
|
+
%i[media comments themes widgets nav_menu plugins users settings custom_fields select_eval].each do |manager_key|
|
|
88
|
+
safe_can :manage, manager_key if @roles_manager[manager_key]
|
|
160
89
|
end
|
|
161
90
|
@roles_manager.try(:each) do |rol_manage_key, val_role|
|
|
162
|
-
|
|
163
|
-
rescue StandardError
|
|
164
|
-
false
|
|
91
|
+
safe_can :manage, rol_manage_key.to_sym if val_role.to_s.cama_true?
|
|
165
92
|
end
|
|
166
93
|
end
|
|
167
94
|
cannot :impersonate, CamaleonCms::User do |u|
|
|
@@ -182,5 +109,30 @@ module CamaleonCms
|
|
|
182
109
|
def cannot?(*args)
|
|
183
110
|
!can?(*args)
|
|
184
111
|
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# Wraps a can rule with block-level exception handling.
|
|
116
|
+
# Blocks are evaluated later during authorization checks (can? calls), so exceptions
|
|
117
|
+
# from accessing post properties, role metadata, etc. must be caught here to fail closed.
|
|
118
|
+
# Non-block calls (e.g., can :manage, :symbol) do not need wrapping; can itself is safe.
|
|
119
|
+
def safe_can(action, subject, &block)
|
|
120
|
+
if block_given?
|
|
121
|
+
can(action, subject) do |resource|
|
|
122
|
+
safely_false { block.call(resource) }
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
# No block: can(action, subject) does not evaluate user logic; safe to call directly.
|
|
126
|
+
can(action, subject)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Fails closed for permission checks when role/post metadata is malformed.
|
|
131
|
+
# Used by safe_can to guard block evaluation during authorization checks.
|
|
132
|
+
def safely_false
|
|
133
|
+
yield
|
|
134
|
+
rescue StandardError
|
|
135
|
+
false
|
|
136
|
+
end
|
|
185
137
|
end
|
|
186
138
|
end
|
|
@@ -25,6 +25,7 @@ module CamaleonCms
|
|
|
25
25
|
unless: ->(o) { o.is_a?(CamaleonCms::CustomFieldGroup) }
|
|
26
26
|
|
|
27
27
|
before_validation :before_validating
|
|
28
|
+
before_update :check_select_eval_authorization
|
|
28
29
|
|
|
29
30
|
private
|
|
30
31
|
|
|
@@ -32,5 +33,18 @@ module CamaleonCms
|
|
|
32
33
|
self.slug ||= name
|
|
33
34
|
self.slug = slug.to_s.parameterize
|
|
34
35
|
end
|
|
36
|
+
|
|
37
|
+
# Prevent unauthorized modification of field_key to select_eval
|
|
38
|
+
def check_select_eval_authorization
|
|
39
|
+
return unless respond_to?(:options) && options.present?
|
|
40
|
+
|
|
41
|
+
# Check if field_key is being changed to select_eval
|
|
42
|
+
return unless options[:field_key] == 'select_eval'
|
|
43
|
+
# Allow if user has explicit permission
|
|
44
|
+
return if can?(:manage, :select_eval)
|
|
45
|
+
|
|
46
|
+
errors.add(:base, 'Not authorized to create or modify select_eval fields')
|
|
47
|
+
throw :abort
|
|
48
|
+
end
|
|
35
49
|
end
|
|
36
50
|
end
|