Module: CamaleonCms::UploaderHelper

Includes:
ActionView::Helpers::NumberHelper, CamaleonHelper
Included in:
CamaleonController
Defined in:
app/helpers/camaleon_cms/uploader_helper.rb

Constant Summary collapse

UNSAFE_EVENT_PATTERNS =
%w[
  onabort onafter onbefore onblur oncanplay onchange onclick oncontextmenu oncopy oncuechange oncut ondblclick
  ondrag ondrop ondurationchange onended onerror onfocus onhashchange oninvalid oninput onkey onload onmessage
  onmouse ononline onoffline onpagehide onpageshow onpage onpaste onpause onplay onpopstate onprogress
  onpropertychange onratechange onreadystatechange onreset onresize onscroll onsearch onseek onselect onshow
  onstalled onstorage onsuspend ontimeupdate ontoggle onunloadonsubmit onvolumechange onwaiting onwheel
].map { |pattern| /#{pattern}\w*\s*=/i }.freeze
SUSPICIOUS_PATTERNS =
(UNSAFE_EVENT_PATTERNS + [
  /<script[\s>]/i,  # Script tags
  /javascript:/i,   # JavaScript in href/src attributes
  /<iframe[\s>]/i,  # Iframes
  /<object[\s>]/i,  # Object tags
  /<embed[\s>]/i,   # Embed tags
  /<base[\s>]/i,    # Base tags (can be used to manipulate URLs)
  /data:/i          # data: URLs (which can include scripts)
]).freeze

Instance Method Summary collapse

Methods included from CamaleonHelper

#cama_cache_fetch, #cama_draw_timer, #cama_edit_link, #cama_is_admin_request?, #cama_pluralize_text, #cama_requestAction, #cama_sitemap_cats_generator, #cama_t, #ct

Instance Method Details

#cama_crop_image(file_path, w = nil, h = nil, w_offset = 0, h_offset = 0, resize = false, replace = true) ⇒ Object

crop and image and saved as imagename_crop.ext file: file path w: new width h: new height w_offset: left offset w_offset: top offset resize: true/false

(true => resize the image to this dimension)
(false => crop the image with this dimension)

replace: Boolean (replace current image or create another file)



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 186

def cama_crop_image(file_path, w = nil, h = nil, w_offset = 0, h_offset = 0, resize = false, replace = true)
  force = w.present? && h.present? && !w.include?('?') && !h.include?('?') ? '!' : ''
  img = MiniMagick::Image.open(file_path)
  w = clamp_to_image_dimension(w, img[:width])
  h = clamp_to_image_dimension(h, img[:height])
  data = { img: img, w: w, h: h, w_offset: w_offset, h_offset: h_offset, resize: resize, replace: replace }
  hooks_run('before_crop_image', data)
  data[:img].combine_options do |i|
    i.resize("#{w if w.present?}x#{h if h.present?}#{force}") if data[:resize]
    i.crop "#{w if w.present?}x#{h if h.present?}+#{w_offset}+#{h_offset}#{force}" unless data[:resize]
  end

  ext = File.extname(file_path)
  res = data[:replace] ? file_path : file_path.gsub(ext, "_crop#{ext}")
  data[:img].write res
  res
end

#cama_file_path_to_url(file_path) ⇒ Object

convert downloaded file path into public url



163
164
165
166
167
168
169
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 163

def cama_file_path_to_url(file_path)
  file_path.sub(Rails.public_path.to_s, begin
    root_url
  rescue StandardError
    cama_root_url
  end)
end

#cama_resize_and_crop(file, w, h, settings = {}) ⇒ Object

resize and crop a file SVGs are converted to JPEGs for editing Params:

file: (String) File path
w: (Integer) width
h: (Integer) height
settings:
  gravity: (Sym, default :north_east) Crop position: :north_west, :north, :north_east, :east, :south_east, :south, :south_west, :west, :center
  overwrite: (Boolean, default true) true for overwrite current image with resized resolutions, false: create other file called with prefix "crop_"
  output_name: (String, default prefixd name with crop_), permit to define the output name of the thumbnail if overwrite = true

Return: (String) file path where saved this cropped sample: cama_resize_and_crop(my_file, 200, 200, :north_east, overwrite: false)



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 216

def cama_resize_and_crop(file, w, h, settings = {})
  settings = { gravity: :north_east, overwrite: true, output_name: +'' }.merge!(settings)
  img = MiniMagick::Image.open(file)
  if file.end_with? '.svg'
    img.format 'jpg'
    file.sub! '.svg', '.jpg'
    settings[:output_name]&.sub!('.svg', '.jpg')
  end
  w = clamp_to_image_dimension(w, img[:width])
  h = clamp_to_image_dimension(h, img[:height])
  w_original = img[:width].to_f
  h_original = img[:height].to_f
  w = w.to_i if w.present?
  h = h.to_i if h.present?

  # check proportions
  if w_original * h < h_original * w
    op_resize = "#{w.to_i}x"
    w_result = w
    h_result = (h_original * w / w_original)
  else
    op_resize = "x#{h.to_i}"
    w_result = (w_original * h / h_original)
    h_result = h
  end

  w_offset, h_offset = cama_crop_offsets_by_gravity(settings[:gravity], [w_result, h_result], [w, h])
  data = { img: img, w: w, h: h, w_offset: w_offset, h_offset: h_offset, op_resize: op_resize, settings: settings }
  hooks_run('before_resize_crop', data)
  data[:img].combine_options do |i|
    i.resize(data[:op_resize])
    i.gravity(settings[:gravity])
    i.crop "#{data[:w].to_i}x#{data[:h].to_i}+#{data[:w_offset]}+#{data[:h_offset]}!"
  end

  if settings[:overwrite]
    data[:img].write(file.sub('.svg', '.jpg'))
  elsif settings[:output_name].present?
    data[:img].write(file = File.join(File.dirname(file), settings[:output_name]).to_s)
  else
    data[:img].write(file = uploader_verify_name(File.join(File.dirname(file),
                                                           "crop_#{File.basename(file.sub('.svg', '.jpg'))}")))
  end
  file
end

#cama_resize_upload(image_path, dimension, args = {}) ⇒ Object

resize image if the format is correct return resized file path



334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 334

def cama_resize_upload(image_path, dimension, args = {})
  if cama_uploader.class.validate_file_format(image_path, 'image') && dimension.present?
    dim_parts = dimension.split('x')
    r = { file: image_path, w: dim_parts[0], h: dim_parts[1], w_offset: 0, h_offset: 0,
          resize: !dim_parts[2] || dim_parts[2] == 'resize',
          replace: true, gravity: :north_east }.merge!(args)
    hooks_run('on_uploader_resize', r)
    image_path = if r[:w].present? && r[:h].present?
                   cama_resize_and_crop(r[:file], r[:w], r[:h], { overwrite: r[:replace], gravity: r[:gravity] })
                 else
                   cama_crop_image(r[:file], r[:w], r[:h], r[:w_offset], r[:h_offset], r[:resize], r[:replace])
                 end
  end
  image_path
end

#cama_tmp_upload(uploaded_io, args = {}) ⇒ Object

upload tmp file support for url and local path sample: cama_tmp_upload(‘’) ==> /var/rails/my_project/public/tmp/1/logo2.png cama_tmp_upload(‘/var/www/media/132/logo 2.png’) ==> /var/rails/my_project/public/tmp/1/logo-2.png accept args:

name: to indicate the name to use, sample: cama_tmp_upload('/var/www/media/132/logo 2.png', {name: 'owen.png', formats: 'images'})
formats: extensions permitted, sample: jpg,png,... or generic: images | videos | audios | documents (default *)
dimension: 20x30

return: error



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 272

def cama_tmp_upload(uploaded_io, args = {})
  tmp_path = args[:path] || File.join(Rails.public_path, 'tmp', current_site.id.to_s).to_s
  FileUtils.mkdir_p(tmp_path) unless Dir.exist?(tmp_path)
  saved = false
  downloaded_tmp_file = nil
  if uploaded_io.is_a?(String) && uploaded_io.start_with?('data:') # create tmp file using base64 format
    _tmp_name = args[:name]
    return { error: cama_t('camaleon_cms.admin.media.name_required').to_s } unless params[:name].present?

    err = validate_file_format_or_error(_tmp_name, args[:formats])
    return err if err

    path = uploader_verify_name(File.join(tmp_path, _tmp_name))
    File.open(path, 'wb') { |f| f.write(Base64.decode64(uploaded_io.split(';base64,').last)) }
    uploaded_io = File.open(path)
    saved = true
  elsif uploaded_io.is_a?(String) && (uploaded_io.start_with?('http://') || uploaded_io.start_with?('https://'))
    err = validate_file_format_or_error(uploaded_io, args[:formats])
    return err if err

    if uploaded_io.include?(current_site.the_url(locale: nil))
      uploaded_io = File.join(Rails.public_path, uploaded_io.sub(current_site.the_url(locale: nil), '')).to_s
    else
      remote_file = cama_download_remote_file(uploaded_io)
      return remote_file if remote_file[:error].present?

      downloaded_tmp_file = remote_file[:file]
      uploaded_io = downloaded_tmp_file
    end
    _tmp_name = if uploaded_io.is_a?(String)
                  uploaded_io.split('/').last.split('?').first
                else
                  uploaded_io.path.split('/').last
                end
    args[:name] = args[:name] || _tmp_name
  end
  uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
  err = validate_file_format_or_error(_tmp_name || uploaded_io.path, args[:formats])
  return err if err

  actual_size = begin
    uploaded_io.size
  rescue StandardError
    File.size(uploaded_io)
  end
  if args[:maximum].present? && args[:maximum] < actual_size
    max_size = number_to_human_size(args[:maximum])
    return { error: "#{ct('file_size_exceeded', default: 'File size exceeded')} (#{max_size})" }
  end

  name = args[:name] || uploaded_io&.original_filename || uploaded_io.path.split('/').last
  name = "#{File.basename(name, File.extname(name)).parameterize}#{File.extname(name)}"
  path ||= uploader_verify_name(File.join(tmp_path, name))
  File.open(path, 'wb') { |f| f.write(uploaded_io.read) } unless saved
  path = cama_resize_upload(path, args[:dimension]) if args[:dimension].present?
  { file_path: path, error: nil }
ensure
  downloaded_tmp_file&.close!
end

#cama_uploaderObject

return the current uploader



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 351

def cama_uploader
  @cama_uploader ||= lambda {
    thumb = current_site.get_option('filesystem_thumb_size', '100x100').split('x')
    args = {
      server: current_site.get_option('filesystem_type', 'local').downcase,
      thumb: { w: thumb[0], h: thumb[1] },
      aws_settings: {
        region: current_site.get_option('filesystem_region', 'us-west-2'),
        access_key: current_site.get_option('filesystem_s3_access_key'),
        secret_key: current_site.get_option('filesystem_s3_secret_key'),
        bucket: current_site.get_option('filesystem_s3_bucket_name'),
        cloud_front: current_site.get_option('filesystem_s3_cloudfront'),
        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
        aws_file_read_settings: ->(data, _s3_file) { data } # permit to read custom attributes from aws file and add to file parsed object
      },
      custom_uploader: nil # possibility to use custom file uploader
    }
    hooks_run('on_uploader', args)
    return args[:custom_uploader] if args[:custom_uploader].present?

    base_args = { current_site: current_site, thumb: args[:thumb] }
    case args[:server]
    when 's3', 'aws'
      CamaleonCmsAwsUploader.new(base_args.merge(aws_settings: args[:aws_settings]), self)
    else
      CamaleonCmsLocalUploader.new(base_args, self)
    end
  }.call
end

#cama_uploader_generate_thumbnail(uploaded_io, key, thumb_size = nil, remove_source = false) ⇒ Object

generate thumbnail of a existent image key: key of the current file the thumbnail will be saved in my_images/my_img.png => my_images/thumb/my_img.png



131
132
133
134
135
136
137
138
139
140
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 131

def cama_uploader_generate_thumbnail(uploaded_io, key, thumb_size = nil, remove_source = false)
  w = thumb_size.present? ? thumb_size.split('x')[0] : cama_uploader.thumb[:w]
  h = thumb_size.present? ? thumb_size.split('x')[1] : cama_uploader.thumb[:h]
  uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
  path_thumb = cama_resize_and_crop(uploaded_io.path, w, h)
  thumb = cama_uploader.add_file(path_thumb, cama_uploader.version_path(key).sub('.svg', '.jpg'), is_thumb: true,
                                                                                                  same_name: true)
  FileUtils.rm_f(path_thumb) if remove_source
  thumb
end

#cama_url_to_file_path(url) ⇒ Object

convert public url to file path



172
173
174
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 172

def cama_url_to_file_path(url)
  File.join(Rails.public_path, URI(url.to_s).path)
end

#slugify(val) ⇒ Object



381
382
383
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 381

def slugify(val)
  val.to_s.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
end

#slugify_folder(val) ⇒ Object



385
386
387
388
389
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 385

def slugify_folder(val)
  split_folder = val.split('/')
  split_folder[-1] = slugify(split_folder[-1])
  split_folder.join('/')
end

#upload_file(uploaded_io, settings = {}) ⇒ Object

upload a file into server settings:

folder: Directory where the file will be saved (default: "")
  sample: temporal => will save in /rails_path/public/temporal
generate_thumb: true, # generate thumb image if this is image format (default true)
maximum: maximum bytes permitted to upload (default: 1000MG)
dimension: dimension for the image (sample: 30x30 | x30 | 30x | 300x300?)
formats: extensions permitted, sample: jpg,png,... or generic: images | videos | audios | documents (default *)
remove_source: Boolean (delete source file after saved if this is true, default false)
same_name: Boolean (save the file with the same name if defined true, else search for a non used name)
versions: (String) Create additional multiple versions of the image uploaded,
  sample: '300x300,505x350' ==> Will create two extra images with these dimensions
  sample "test.png", versions: '200x200,450x450' will generate: thumb/test-png_200x200.png, test-png_450x450.png
thumb_size: String (redefine the dimensions of the thumbnail, sample: '100x100' ==> only for images)
temporal_time: if great than 0 seconds, then this file will expire (removed) in that time (default: 0)
  To manage jobs, please check https://edgeguides.rubyonrails.org/active_job_basics.html
  Note: if you are using temporal_time, you will need to copy the file to another directory later

sample: upload_file(params, “images”, folder: “temporal”) sample: upload_file(params, “jpg,png,gif,mp3,mp4”, temporal_time: 10.minutes, maximum: 10.megabytes)



48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 48

def upload_file(uploaded_io, settings = {})
  cached_name = uploaded_io.is_a?(ActionDispatch::Http::UploadedFile) ? uploaded_io.original_filename : nil
  return { error: 'File is empty', file: nil, size: nil } unless uploaded_io.present?

  if uploaded_io.is_a?(String) && uploaded_io.match(%r{^https?://}).present? # download url file
    tmp = cama_tmp_upload(uploaded_io)
    return tmp if tmp[:error].present?

    settings[:remove_source] = true
    uploaded_io = tmp[:file_path]
  end
  uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
  if settings[:dimension].present?
    uploaded_io = File.open(cama_resize_upload(uploaded_io.path, settings[:dimension]))
  end

  return { error: 'Potentially malicious content found!' } if file_content_unsafe?(uploaded_io)

  settings = settings.to_sym
  settings[:uploaded_io] = uploaded_io
  settings = {
    folder: '',
    maximum: current_site.get_option('filesystem_max_size', 100).to_f.megabytes,
    formats: '*',
    generate_thumb: true,
    temporal_time: 0,
    filename: begin
      cached_name || uploaded_io.original_filename
    rescue StandardError
      uploaded_io.path.split('/').last
    end.cama_fix_filename,
    file_size: File.size(uploaded_io.to_io),
    remove_source: false,
    same_name: false,
    versions: '',
    thumb_size: nil
  }.merge!(settings)
  hooks_run('before_upload', settings)

  # guard against path traversal
  return { error: 'Invalid file path' } unless cama_uploader.valid_folder_path?(settings[:folder])

  # formats validations
  err = validate_file_format_or_error(uploaded_io.path, settings[:formats])
  return err if err

  # file size validations
  if settings[:maximum] < settings[:file_size]
    max_size = number_to_human_size(settings[:maximum])
    return { error: "#{ct('file_size_exceeded', default: 'File size exceeded')} (#{max_size})" }
  end
  # save file
  key = File.join(settings[:folder], settings[:filename]).to_s.cama_fix_slash
  res = cama_uploader.add_file(settings[:uploaded_io], key, { same_name: settings[:same_name] })

  # generate image versions
  if res['file_type'] == 'image'
    settings[:versions].to_s.gsub(' ', '').split(',').each do |v|
      version_path = cama_resize_upload(settings[:uploaded_io].path, v, { replace: false })
      cama_uploader.add_file(version_path, cama_uploader.version_path(res['key'], v), is_thumb: true,
                                                                                      same_name: true)
      FileUtils.rm_f(version_path)
    end
  end

  # generate thumb
  if settings[:generate_thumb] && res['thumb'].present?
    cama_uploader_generate_thumbnail(uploaded_io.path, res['key'], settings[:thumb_size],
                                     settings[:remove_source])
  end
  FileUtils.rm_f(uploaded_io.path) if settings[:remove_source] && File.exist?(uploaded_io.path)

  hooks_run('after_upload', settings)

  # temporal file upload (always put as local for temporal files)
  CamaleonCmsUploader.delete_block.call(settings, cama_uploader, key) if settings[:temporal_time] > 0

  res
end

#uploader_verify_name(file_path) ⇒ Object

helper to find an available filename for file_path in that directory sample: uploader_verify_name(“/var/www/my_image.jpg”)

return "/var/www/my_image_1.jpg" => if "/var/www/my_image.jpg" exist
return "/var/www/my_image.jpg" => if "/var/www/my_image.jpg" doesn't exist


146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'app/helpers/camaleon_cms/uploader_helper.rb', line 146

def uploader_verify_name(file_path)
  dir = File.dirname(file_path)
  filename = File.basename(file_path).to_s.cama_fix_filename
  files = Dir.entries(dir)
  if files.include?(filename)
    i = 1
    _filename = filename
    while files.include?(_filename)
      _filename = "#{File.basename(filename, File.extname(filename))}_#{i}#{File.extname(filename)}"
      i += 1
    end
    filename = _filename
  end
  "#{File.dirname(file_path)}/#{filename}"
end