Module: CamaleonCms::UploaderPipeline
- Included in:
- RuntimeUploaderConcern, UploaderHelper
- Defined in:
- lib/camaleon_cms/uploader_pipeline.rb
Overview
Staging and persistence pipeline shared by the two uploader entry points: CamaleonCms::RuntimeUploaderConcern (controllers) and CamaleonCms::UploaderHelper (views, ActiveJobs, standalone objects). Both include this module, so a fix to upload staging or persistence cannot land in one entry point and not the other.
Nothing here may read request state such as params: UploaderHelper is documented
in config/initializers/custom_initializers.rb as includable from an ActiveJob, where
no request exists. Context differences go through the message seam at the bottom.
Instance Method Summary collapse
-
#cama_tmp_upload(uploaded_io, args = {}) ⇒ Object
upload tmp file support for url and local path sample: cama_tmp_upload('https://camaleon.website/media/132/logo2.png') ==> /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', 'owen.png', formats: 'images') formats: extensions permitted, sample: jpg,png,...
-
#cama_uploader_ct(key, args = {}) ⇒ Object
Message seam.
- #cama_uploader_human_size(bytes) ⇒ Object
- #cama_uploader_t(key, args = {}) ⇒ Object
-
#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,...
Instance Method Details
#cama_tmp_upload(uploaded_io, args = {}) ⇒ Object
upload tmp file support for url and local path sample: cama_tmp_upload('https://camaleon.website/media/132/logo2.png') ==> /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
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/camaleon_cms/uploader_pipeline.rb', line 150 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) # Default to the site limit so the size guard below actually applies: callers # such as crop/crop_url pass no :maximum, which left it dead code. args[:maximum] ||= current_site.get_option('filesystem_max_size', 100).to_f.megabytes saved = false downloaded_tmp_file = nil staged_path = nil if uploaded_io.is_a?(String) && uploaded_io.start_with?('data:') # create tmp file using base64 format path, err = cama_stage_data_uri(uploaded_io, args, tmp_path) return err if err staged_path = path _tmp_name = File.basename(args[:name].to_s) uploaded_io = File.open(path) saved = true elsif uploaded_io.is_a?(String) && uploaded_io.start_with?('http://', 'https://') err = validate_file_format_or_error(uploaded_io, args[:formats]) return err if err if same_site_url?(uploaded_io, current_site) uploaded_io = File.join(Rails.public_path, site_url_path(uploaded_io, current_site)).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 if uploaded_io.is_a?(String) = cama_canonical_upload_path(uploaded_io, extra_roots: cama_extra_upload_roots(args)) return { error: 'Invalid file path' } unless uploaded_io = 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 err = cama_size_limit_error(actual_size, args[:maximum]) return err if err 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)) unless saved # Same rule as the data: branch above -- read, scan, and only then write, so no # source can land unscanned in public/tmp. The scan is keyed on `name`, the output # filename, because that is the extension the web server will serve the bytes under; # only the uploader's permission decides whether it runs. content = uploaded_io.read if !cama_trusted_for_unfiltered_upload? && content_unsafe?(content, filename: name) return { error: 'Potentially malicious content found!' } end File.open(path, 'wb') { |f| f.write(content) } staged_path = path end path = cama_resize_upload(path, args[:dimension]) if args[:dimension].present? { file_path: path, error: nil } rescue StandardError # A raised error leaves no half-written file behind in the served staging dir. cama_purge_staged_file(staged_path, tmp_path) raise ensure downloaded_tmp_file&.close! end |
#cama_uploader_ct(key, args = {}) ⇒ Object
Message seam. Rendering user-facing upload errors differs by execution context, so the pipeline never calls a translator directly.
ct runs the on_translation hook that lets plugins override the text, which a
shared I18n.t call would silently drop. CamaleonCms::UploaderHelper overrides all
three to route through ct / cama_t / number_to_human_size (it includes
CamaleonHelper itself). CamaleonCms::RuntimeUploaderConcern overrides only
cama_uploader_ct, routing through ct when its host has one — true for
CamaleonCms::CamaleonController since #1223 restored CamaleonHelper there. The
defaults below are what remains: t/human_size on the concern path, and all
three for concern hosts without ct (ActiveJobs, standalone objects).
242 243 244 |
# File 'lib/camaleon_cms/uploader_pipeline.rb', line 242 def cama_uploader_ct(key, args = {}) I18n.t("camaleon_cms.common.#{key}", **args) end |
#cama_uploader_human_size(bytes) ⇒ Object
250 251 252 |
# File 'lib/camaleon_cms/uploader_pipeline.rb', line 250 def cama_uploader_human_size(bytes) ActiveSupport::NumberHelper.number_to_human_size(bytes) end |
#cama_uploader_t(key, args = {}) ⇒ Object
246 247 248 |
# File 'lib/camaleon_cms/uploader_pipeline.rb', line 246 def cama_uploader_t(key, args = {}) I18n.t(key, **args) 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)
36 37 38 39 40 41 42 43 44 45 46 47 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 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/camaleon_cms/uploader_pipeline.rb', line 36 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 } if uploaded_io.blank? 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 if uploaded_io.is_a?(String) = cama_canonical_upload_path(uploaded_io, extra_roots: cama_extra_upload_roots(settings)) return { error: 'Invalid file path' } unless uploaded_io = 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 # Permission first: a trusted upload never reads the file in order to scan it. if !cama_trusted_for_unfiltered_upload? && file_content_unsafe?(uploaded_io) return cama_upload_failure({ error: 'Potentially malicious content found!' }, uploaded_io, settings) end settings = settings.to_h.deep_symbolize_keys 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) settings[:formats] = '*' if settings[:formats].nil? settings[:folder] = '' if settings[:folder].nil? # e.g. crop_url passes no folder io_before_hook = settings[:uploaded_io] hooks_run('before_upload', settings) # A before_upload handler may rebind settings[:uploaded_io] to bytes the top-of-method # scan never saw (e.g. an image optimizer rewriting an SVG). For an untrusted uploader, # re-scan the substituted IO so a handler cannot launder a blocked payload past the scan. # Keyed on object identity: an unchanged IO was already scanned, and a permission-holder # is exempt exactly as above. if !settings[:uploaded_io].equal?(io_before_hook) && !cama_trusted_for_unfiltered_upload? && file_content_unsafe?(settings[:uploaded_io]) return cama_upload_failure({ error: 'Potentially malicious content found!' }, settings[:uploaded_io], settings) end # guard against path traversal unless cama_uploader.valid_folder_path?(settings[:folder]) return cama_upload_failure({ error: 'Invalid file path' }, uploaded_io, settings) end # formats validations err = validate_file_format_or_error(uploaded_io.path, settings[:formats]) return cama_upload_failure(err, uploaded_io, settings) if err # file size validations err = cama_size_limit_error(settings[:file_size], settings[:maximum]) return cama_upload_failure(err, uploaded_io, settings) if err # 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.delete(' ').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 |