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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -5
  3. data/app/apps/plugins/front_cache/admin_controller.rb +1 -0
  4. data/app/apps/plugins/front_cache/front_cache_helper.rb +23 -14
  5. data/app/apps/plugins/visibility_post/visibility_post_helper.rb +1 -1
  6. data/app/apps/themes/default/views/category.html.erb +1 -1
  7. data/app/apps/themes/default/views/post_tag.html.erb +1 -1
  8. data/app/apps/themes/default/views/post_type.html.erb +1 -1
  9. data/app/apps/themes/default/views/search.html.erb +1 -1
  10. data/app/apps/themes/new/views/category.html.erb +1 -1
  11. data/app/apps/themes/new/views/post_tag.html.erb +1 -1
  12. data/app/apps/themes/new/views/post_type.html.erb +1 -1
  13. data/app/apps/themes/new/views/search.html.erb +1 -1
  14. data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +22 -5
  15. data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +4 -2
  16. data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +3 -3
  17. data/app/controllers/camaleon_cms/admin/appearances/widgets/sidebar_controller.rb +2 -2
  18. data/app/controllers/camaleon_cms/admin/categories_controller.rb +9 -5
  19. data/app/controllers/camaleon_cms/admin/media_controller.rb +18 -5
  20. data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +7 -4
  21. data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +1 -1
  22. data/app/controllers/camaleon_cms/admin/posts_controller.rb +7 -4
  23. data/app/controllers/camaleon_cms/admin/sessions_controller.rb +2 -2
  24. data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +33 -11
  25. data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +13 -4
  26. data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +7 -4
  27. data/app/controllers/camaleon_cms/admin/settings_controller.rb +7 -4
  28. data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +2 -2
  29. data/app/controllers/camaleon_cms/admin/users_controller.rb +23 -14
  30. data/app/controllers/camaleon_cms/admin_controller.rb +8 -0
  31. data/app/controllers/camaleon_cms/apps/plugins_admin_controller.rb +5 -0
  32. data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +29 -0
  33. data/app/decorators/camaleon_cms/post_decorator.rb +1 -1
  34. data/app/decorators/camaleon_cms/user_decorator.rb +1 -1
  35. data/app/helpers/camaleon_cms/admin/application_helper.rb +17 -17
  36. data/app/helpers/camaleon_cms/admin/post_type_helper.rb +25 -22
  37. data/app/helpers/camaleon_cms/comment_helper.rb +74 -40
  38. data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +1 -1
  39. data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +7 -7
  40. data/app/helpers/camaleon_cms/html_helper.rb +15 -1
  41. data/app/helpers/camaleon_cms/session_helper.rb +13 -1
  42. data/app/helpers/camaleon_cms/site_helper.rb +16 -3
  43. data/app/helpers/camaleon_cms/uploader_helper.rb +102 -51
  44. data/app/models/camaleon_cms/ability.rb +54 -102
  45. data/app/models/camaleon_cms/category.rb +2 -0
  46. data/app/models/camaleon_cms/custom_field.rb +14 -0
  47. data/app/models/camaleon_cms/custom_field_group.rb +38 -1
  48. data/app/models/camaleon_cms/custom_fields_relationship.rb +1 -1
  49. data/app/models/camaleon_cms/meta.rb +4 -0
  50. data/app/models/camaleon_cms/nav_menu.rb +2 -0
  51. data/app/models/camaleon_cms/nav_menu_item.rb +2 -0
  52. data/app/models/camaleon_cms/plugin.rb +2 -0
  53. data/app/models/camaleon_cms/post.rb +1 -1
  54. data/app/models/camaleon_cms/post_comment.rb +4 -0
  55. data/app/models/camaleon_cms/post_tag.rb +2 -0
  56. data/app/models/camaleon_cms/post_type.rb +3 -1
  57. data/app/models/camaleon_cms/site.rb +2 -0
  58. data/app/models/camaleon_cms/term_taxonomy.rb +1 -23
  59. data/app/models/camaleon_cms/theme.rb +2 -0
  60. data/app/models/camaleon_cms/user_role.rb +13 -0
  61. data/app/models/camaleon_cms/widget/main.rb +2 -0
  62. data/app/models/camaleon_cms/widget/sidebar.rb +2 -0
  63. data/app/models/camaleon_record.rb +40 -0
  64. data/app/models/concerns/camaleon_cms/custom_fields_read.rb +7 -7
  65. data/app/models/concerns/camaleon_cms/metas.rb +10 -6
  66. data/app/models/concerns/camaleon_cms/normalize_attrs.rb +26 -0
  67. data/app/models/concerns/camaleon_cms/user_methods.rb +6 -2
  68. data/app/models/current_request.rb +16 -0
  69. data/app/uploaders/camaleon_cms_aws_uploader.rb +8 -1
  70. data/app/validators/camaleon_cms/post_uniq_validator.rb +21 -8
  71. data/app/views/camaleon_cms/admin/appearances/nav_menus/_left_menu_items.html.erb +2 -2
  72. data/app/views/camaleon_cms/admin/appearances/widgets/main/form.html.erb +1 -1
  73. data/app/views/camaleon_cms/admin/categories/index.html.erb +1 -1
  74. data/app/views/camaleon_cms/admin/comments/index.html.erb +2 -2
  75. data/app/views/camaleon_cms/admin/comments/list.html.erb +1 -1
  76. data/app/views/camaleon_cms/admin/post_tags/index.html.erb +1 -1
  77. data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
  78. data/app/views/camaleon_cms/admin/posts/index.html.erb +3 -3
  79. data/app/views/camaleon_cms/admin/search.html.erb +1 -1
  80. data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +23 -2
  81. data/app/views/camaleon_cms/admin/settings/custom_fields/fields/_select_eval.html.erb +1 -1
  82. data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +6 -5
  83. data/app/views/camaleon_cms/admin/settings/custom_fields/index.html.erb +1 -1
  84. data/app/views/camaleon_cms/admin/settings/post_types/index.html.erb +1 -1
  85. data/app/views/camaleon_cms/admin/settings/sites/index.html.erb +1 -1
  86. data/app/views/camaleon_cms/admin/user_roles/form.html.erb +79 -5
  87. data/app/views/camaleon_cms/admin/user_roles/index.html.erb +1 -1
  88. data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
  89. data/app/views/layouts/camaleon_cms/admin/_flash_messages.html.erb +2 -2
  90. data/config/initializers/custom_initializers.rb +2 -2
  91. data/config/locales/camaleon_cms/admin/ar.yml +6 -2
  92. data/config/locales/camaleon_cms/admin/de.yml +6 -2
  93. data/config/locales/camaleon_cms/admin/en.yml +6 -2
  94. data/config/locales/camaleon_cms/admin/es.yml +6 -2
  95. data/config/locales/camaleon_cms/admin/fr.yml +6 -2
  96. data/config/locales/camaleon_cms/admin/it.yml +6 -2
  97. data/config/locales/camaleon_cms/admin/nl.yml +7 -2
  98. data/config/locales/camaleon_cms/admin/pt-BR.yml +6 -2
  99. data/config/locales/camaleon_cms/admin/pt.yml +6 -2
  100. data/config/locales/camaleon_cms/admin/ru.yml +6 -2
  101. data/config/locales/camaleon_cms/admin/uk.yml +6 -2
  102. data/config/locales/camaleon_cms/admin/zh-CH.yml +6 -2
  103. data/db/migrate/20150611161134_post_table_into_utf8.rb +14 -14
  104. data/db/migrate/20150926095310_rename_column_posts.rb +3 -3
  105. data/db/migrate/20151212095328_add_confirm_token_to_users.rb +3 -3
  106. data/db/migrate/20160504155652_add_feature_to_posts.rb +1 -1
  107. data/db/migrate/20160504155653_move_first_name_of_users.rb +2 -2
  108. data/db/migrate/20160609121449_add_group_to_custom_field_values.rb +1 -1
  109. data/db/migrate/20161215202255_drop_user_relationship_table.rb +1 -1
  110. data/db/migrate/20180124132318_create_media.rb +1 -1
  111. data/db/migrate/20180704211100_adjust_field_length.rb +1 -1
  112. data/lib/camaleon_cms/version.rb +1 -1
  113. data/lib/ext/string.rb +3 -3
  114. data/lib/plugin_routes.rb +6 -6
  115. data/lib/tasks/custom_fields_roles.rake +56 -0
  116. 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.start_with?('t(') ? eval(value.sub('t(', 'I18n.t(')) : 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
- @current_site = site.decorate if site.present?
6
- return $current_site if defined?($current_site)
7
- return @current_site if defined?(@current_site)
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
- return { error: "#{ct('file_format_error')} (#{settings[:formats]})" } unless cama_uploader.class.validate_file_format(
90
- uploaded_io.path, settings[:formats]
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
- res[:error] =
96
- "#{ct('file_size_exceeded', default: 'File size exceeded')} (#{number_to_human_size(settings[:maximum])})"
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 = img[:width].to_f > w.sub('?', '').to_i ? w.sub('?', '') : img[:width] if w.present? && w.to_s.include?('?')
194
- h = img[:height].to_f > h.sub('?', '').to_i ? h.sub('?', '') : img[:height] if h.present? && h.to_s.include?('?')
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
- res = file_path
203
- unless data[:replace]
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 = img[:width].to_f > w.sub('?', '').to_i ? w.sub('?', '') : img[:width] if w.present? && w.to_s.include?('?')
232
- h = img[:height].to_f > h.sub('?', '').to_i ? h.sub('?', '') : img[:height] if h.present? && h.to_s.include?('?')
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
- return { error: "#{ct('file_format_error')} (#{args[:formats]})" } unless cama_uploader.class.validate_file_format(
287
- _tmp_name, args[:formats]
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
- return { error: "#{ct('file_format_error')} (#{args[:formats]})" } unless cama_uploader.class.validate_file_format(
296
- uploaded_io, args[:formats]
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.split('/').last.split('?').first
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
- return { error: "#{ct('file_format_error')} (#{args[:formats]})" } unless cama_uploader.class.validate_file_format(
308
- _tmp_name || uploaded_io.path, args[:formats]
309
- )
309
+ err = validate_file_format_or_error(_tmp_name || uploaded_io.path, args[:formats])
310
+ return err if err
310
311
 
311
- if args[:maximum].present? && args[:maximum] < begin
312
+ actual_size = begin
312
313
  uploaded_io.size
313
314
  rescue StandardError
314
315
  File.size(uploaded_io)
315
316
  end
316
- return { error: "#{ct('file_size_exceeded',
317
- default: 'File size exceeded')} (#{number_to_human_size(args[:maximum])})" }
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
- r = { file: image_path, w: dimension.split('x')[0], h: dimension.split('x')[1], w_offset: 0, h_offset: 0,
333
- resize: !dimension.split('x')[2] || dimension.split('x')[2] == 'resize',
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: lambda { |settings|
359
- settings
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({ current_site: current_site, thumb: args[:thumb] }, self)
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.last)
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
- current_user_role = user.get_role(current_site) || current_site.user_roles.new
16
- @roles_manager ||= current_user_role.get_meta("_manager_#{current_site.id}", {}) || {}
17
- @roles_post_type ||= current_user_role.get_meta("_post_type_#{current_site.id}", {}) || {}
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
- can :posts, CamaleonCms::PostType do |pt|
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
- can :create_post, CamaleonCms::PostType do |pt|
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
- can :publish_post, CamaleonCms::PostType do |pt|
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
- can :edit_other, CamaleonCms::PostType do |pt|
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
- can :edit_publish, CamaleonCms::PostType do |pt|
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
- can :categories, CamaleonCms::PostType do |pt|
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
- can :post_tags, CamaleonCms::PostType do |pt|
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
- can :update, CamaleonCms::Post do |post|
58
+ safe_can :update, CamaleonCms::Post do |post|
66
59
  pt_id = post.post_type.id
67
60
  r = false
68
- r ||= begin
69
- ids_edit.to_i.include?(pt_id) && post.user_id == user.id
70
- rescue StandardError
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
- can :destroy, CamaleonCms::Post do |post|
67
+ safe_can :destroy, CamaleonCms::Post do |post|
87
68
  pt_id = post.post_type.id
88
69
  r = false
89
- r ||= begin
90
- ids_delete.to_i.include?(pt_id) && post.user_id == user.id
91
- rescue StandardError
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
- can k.to_sym, CamaleonCms::PostType do |pt|
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
- begin
121
- can :manage, :media if @roles_manager[:media]
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
- can :manage, rol_manage_key.to_sym if val_role.to_s.cama_true?
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
@@ -1,5 +1,7 @@
1
1
  module CamaleonCms
2
2
  class Category < CamaleonCms::TermTaxonomy
3
+ normalize_attrs(:name, :description)
4
+
3
5
  alias_attribute :site_id, :term_group
4
6
  alias_attribute :post_type_id, :status
5
7
 
@@ -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