2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2025-09-07 12:02:53 +08:00

REFACTOR: upload workflow creation into UploadCreator

- Automatically convert large-ish PNG/BMP to JPEG
- Updated fast_image to latest version
This commit is contained in:
Régis Hanol 2017-05-11 00:16:57 +02:00
parent a5c4ddd334
commit 9641d2413d
27 changed files with 391 additions and 483 deletions

View file

@ -61,8 +61,7 @@ gem 'fast_xs'
gem 'fast_xor' gem 'fast_xor'
# while we sort out https://github.com/sdsykes/fastimage/pull/46 gem 'fastimage', '2.1.0'
gem 'discourse_fastimage', '2.0.3', require: 'fastimage'
gem 'aws-sdk', require: false gem 'aws-sdk', require: false
gem 'excon', require: false gem 'excon', require: false
gem 'unf', require: false gem 'unf', require: false

View file

@ -78,7 +78,6 @@ GEM
diff-lcs (1.3) diff-lcs (1.3)
discourse-qunit-rails (0.0.9) discourse-qunit-rails (0.0.9)
railties railties
discourse_fastimage (2.0.3)
discourse_image_optim (0.24.4) discourse_image_optim (0.24.4)
exifr (~> 1.2, >= 1.2.2) exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0) fspath (~> 3.0)
@ -114,6 +113,7 @@ GEM
rake rake
rake-compiler rake-compiler
fast_xs (0.8.0) fast_xs (0.8.0)
fastimage (2.1.0)
ffi (1.9.18) ffi (1.9.18)
flamegraph (0.9.5) flamegraph (0.9.5)
foreman (0.82.0) foreman (0.82.0)
@ -402,7 +402,6 @@ DEPENDENCIES
byebug byebug
certified certified
discourse-qunit-rails discourse-qunit-rails
discourse_fastimage (= 2.0.3)
discourse_image_optim discourse_image_optim
email_reply_trimmer (= 0.1.6) email_reply_trimmer (= 0.1.6)
ember-handlebars-template (= 0.7.5) ember-handlebars-template (= 0.7.5)
@ -415,6 +414,7 @@ DEPENDENCIES
fast_blank fast_blank
fast_xor fast_xor
fast_xs fast_xs
fastimage (= 2.1.0)
flamegraph flamegraph
foreman foreman
gc_tracer gc_tracer

View file

@ -1,3 +1,5 @@
require_dependency 'upload_creator'
class Admin::EmojisController < Admin::AdminController class Admin::EmojisController < Admin::AdminController
def index def index
@ -14,13 +16,11 @@ class Admin::EmojisController < Admin::AdminController
.gsub(/_{2,}/, '_') .gsub(/_{2,}/, '_')
.downcase .downcase
upload = Upload.create_for( upload = UploadCreator.new(
current_user.id,
file.tempfile, file.tempfile,
file.original_filename, file.original_filename,
File.size(file.tempfile.path), type: 'custom_emoji'
image_type: 'custom_emoji' ).create_for(current_user.id)
)
data = data =
if upload.persisted? if upload.persisted?

View file

@ -1,3 +1,5 @@
require_dependency 'upload_creator'
class Admin::ThemesController < Admin::AdminController class Admin::ThemesController < Admin::AdminController
skip_before_filter :check_xhr, only: [:show, :preview] skip_before_filter :check_xhr, only: [:show, :preview]
@ -5,27 +7,23 @@ class Admin::ThemesController < Admin::AdminController
def preview def preview
@theme = Theme.find(params[:id]) @theme = Theme.find(params[:id])
redirect_to path("/"), flash: {preview_theme_key: @theme.key} redirect_to path("/"), flash: { preview_theme_key: @theme.key }
end end
def upload_asset def upload_asset
path = params[:file].path path = params[:file].path
File.open(path) do |file| File.open(path) do |file|
upload = Upload.create_for(current_user.id, filename = params[:file]&.original_filename || File.basename(path)
file, upload = UploadCreator.new(file, filename, for_theme: true).create_for(current_user.id)
params[:file]&.original_filename || File.basename(path),
File.size(path),
for_theme: true)
if upload.errors.count > 0 if upload.errors.count > 0
render json: upload.errors, status: :unprocessable_entity render json: upload.errors, status: :unprocessable_entity
else else
render json: {upload_id: upload.id}, status: :created render json: { upload_id: upload.id }, status: :created
end end
end end
end end
def import def import
@theme = nil @theme = nil
if params[:theme] if params[:theme]
json = JSON::parse(params[:theme].read) json = JSON::parse(params[:theme].read)
@ -48,7 +46,6 @@ class Admin::ThemesController < Admin::AdminController
else else
render json: @theme.errors, status: :unprocessable_entity render json: @theme.errors, status: :unprocessable_entity
end end
end end
def index def index
@ -206,7 +203,6 @@ class Admin::ThemesController < Admin::AdminController
end end
def set_fields def set_fields
return unless fields = theme_params[:theme_fields] return unless fields = theme_params[:theme_fields]
fields.each do |field| fields.each do |field|

View file

@ -1,3 +1,5 @@
require_dependency 'upload_creator'
class UploadsController < ApplicationController class UploadsController < ApplicationController
before_filter :ensure_logged_in, except: [:show] before_filter :ensure_logged_in, except: [:show]
skip_before_filter :preload_json, :check_xhr, :redirect_to_login_if_required, only: [:show] skip_before_filter :preload_json, :check_xhr, :redirect_to_login_if_required, only: [:show]
@ -5,26 +7,25 @@ class UploadsController < ApplicationController
def create def create
type = params.require(:type) type = params.require(:type)
raise Discourse::InvalidAccess.new unless type =~ /^[a-z\-\_]{1,100}$/ raise Discourse::InvalidAccess.new unless Upload::UPLOAD_TYPES.include?(type)
file = params[:file] || params[:files].try(:first) if type == "avatar" && (SiteSetting.sso_overrides_avatar || !SiteSetting.allow_uploaded_avatars)
url = params[:url] return render json: failed_json, status: 422
client_id = params[:client_id]
synchronous = (current_user.staff? || is_api?) && params[:synchronous]
if type == "avatar"
if SiteSetting.sso_overrides_avatar || !SiteSetting.allow_uploaded_avatars
return render json: failed_json, status: 422
end
end end
if synchronous url = params[:url]
data = create_upload(type, file, url) file = params[:file] || params[:files]&.first
if params[:synchronous] && (current_user.staff? || is_api?)
data = create_upload(file, url, type)
render json: data.as_json render json: data.as_json
else else
Scheduler::Defer.later("Create Upload") do Scheduler::Defer.later("Create Upload") do
data = create_upload(type, file, url) begin
MessageBus.publish("/uploads/#{type}", data.as_json, client_ids: [client_id]) data = create_upload(file, url, type)
ensure
MessageBus.publish("/uploads/#{type}", (data || {}).as_json, client_ids: [params[:client_id]])
end
end end
render json: success_json render json: success_json
end end
@ -58,86 +59,31 @@ class UploadsController < ApplicationController
render nothing: true, status: 404 render nothing: true, status: 404
end end
def create_upload(type, file, url) def create_upload(file, url, type)
begin if file.nil?
maximum_upload_size = [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes if url.present? && is_api?
maximum_upload_size = [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes
# ensure we have a file tempfile = FileHelper.download(url, maximum_upload_size, "discourse-upload-#{type}") rescue nil
if file.nil? filename = File.basename(URI.parse(url).path)
# API can provide a URL
if url.present? && is_api?
tempfile = FileHelper.download(url, maximum_upload_size, "discourse-upload-#{type}") rescue nil
filename = File.basename(URI.parse(url).path)
end
else
tempfile = file.tempfile
filename = file.original_filename
content_type = file.content_type
end end
return { errors: I18n.t("upload.file_missing") } if tempfile.nil?
# convert pasted images to HQ jpegs
if filename == "image.png" && SiteSetting.convert_pasted_images_to_hq_jpg
jpeg_path = "#{File.dirname(tempfile.path)}/image.jpg"
OptimizedImage.ensure_safe_paths!(tempfile.path, jpeg_path)
Discourse::Utils.execute_command('convert', tempfile.path, '-quality', SiteSetting.convert_pasted_images_quality.to_s, jpeg_path)
# only change the format of the image when JPG is at least 5% smaller
if File.size(jpeg_path) < File.size(tempfile.path) * 0.95
filename = "image.jpg"
content_type = "image/jpeg"
tempfile = File.open(jpeg_path)
else
File.delete(jpeg_path) rescue nil
end
end
# allow users to upload large images that will be automatically reduced to allowed size
max_image_size_kb = SiteSetting.max_image_size_kb.kilobytes
if max_image_size_kb > 0 && FileHelper.is_image?(filename)
if File.size(tempfile.path) >= max_image_size_kb && Upload.should_optimize?(tempfile.path)
attempt = 2
allow_animation = type == "avatar" ? SiteSetting.allow_animated_avatars : SiteSetting.allow_animated_thumbnails
while attempt > 0
downsized_size = File.size(tempfile.path)
break if downsized_size < max_image_size_kb
image_info = FastImage.new(tempfile.path) rescue nil
w, h = *(image_info.try(:size) || [0, 0])
break if w == 0 || h == 0
downsize_ratio = best_downsize_ratio(downsized_size, max_image_size_kb)
dimensions = "#{(w * downsize_ratio).floor}x#{(h * downsize_ratio).floor}"
OptimizedImage.downsize(tempfile.path, tempfile.path, dimensions, filename: filename, allow_animation: allow_animation)
attempt -= 1
end
end
end
upload = Upload.create_for(current_user.id, tempfile, filename, File.size(tempfile.path), content_type: content_type, image_type: type)
if upload.errors.empty? && current_user.admin?
retain_hours = params[:retain_hours].to_i
upload.update_columns(retain_hours: retain_hours) if retain_hours > 0
end
if upload.errors.empty? && FileHelper.is_image?(filename)
Jobs.enqueue(:create_thumbnails, upload_id: upload.id, type: type, user_id: params[:user_id])
end
upload.errors.empty? ? upload : { errors: upload.errors.values.flatten }
ensure
tempfile.try(:close!) rescue nil
end
end
def best_downsize_ratio(downsized_size, max_image_size)
if downsized_size / 9 > max_image_size
0.3
elsif downsized_size / 3 > max_image_size
0.6
else else
0.8 tempfile = file.tempfile
filename = file.original_filename
content_type = file.content_type
end end
return { errors: [I18n.t("upload.file_missing")] } if tempfile.nil?
upload = UploadCreator.new(tempfile, filename, type: type, content_type: content_type).create_for(current_user.id)
if upload.errors.empty? && current_user.admin?
retain_hours = params[:retain_hours].to_i
upload.update_columns(retain_hours: retain_hours) if retain_hours > 0
end
upload.errors.empty? ? upload : { errors: upload.errors.values.flatten }
ensure
tempfile&.close! rescue nil
end end
end end

View file

@ -1,3 +1,5 @@
require_dependency 'upload_creator'
module Jobs module Jobs
class MigrateCustomEmojis < Jobs::Onceoff class MigrateCustomEmojis < Jobs::Onceoff
def execute_onceoff(args) def execute_onceoff(args)
@ -7,13 +9,11 @@ module Jobs
name = File.basename(path, File.extname(path)) name = File.basename(path, File.extname(path))
File.open(path) do |file| File.open(path) do |file|
upload = Upload.create_for( upload = UploadCreator.new(
Discourse.system_user.id,
file, file,
File.basename(path), File.basename(path),
file.size, type: 'custom_emoji'
image_type: 'custom_emoji' ).create_for(Discourse.system_user.id)
)
if upload.persisted? if upload.persisted?
custom_emoji = CustomEmoji.new(name: name, upload: upload) custom_emoji = CustomEmoji.new(name: name, upload: upload)

View file

@ -0,0 +1,20 @@
module Jobs
class CreateAvatarThumbnails < Jobs::Base
def execute(args)
upload_id = args[:upload_id]
raise Discourse::InvalidParameters.new(:upload_id) if upload_id.blank?
upload = Upload.find(upload_id)
user = User.find(args[:user_id] || upload.user_id)
Discourse.avatar_sizes.each do |size|
OptimizedImage.create_for(upload, size, size, filename: upload.original_filename, allow_animation: SiteSetting.allow_animated_avatars)
end
end
end
end

View file

@ -1,37 +0,0 @@
module Jobs
class CreateThumbnails < Jobs::Base
def execute(args)
type = args[:type]
upload_id = args[:upload_id]
raise Discourse::InvalidParameters.new(:type) if type.blank?
raise Discourse::InvalidParameters.new(:upload_id) if upload_id.blank?
# only need to generate thumbnails for avatars
return if type != "avatar"
upload = Upload.find(upload_id)
user_id = args[:user_id] || upload.user_id
user = User.find(user_id)
self.send("create_thumbnails_for_#{type}", upload, user)
end
def create_thumbnails_for_avatar(upload, user)
Discourse.avatar_sizes.each do |size|
OptimizedImage.create_for(
upload,
size,
size,
filename: upload.original_filename,
allow_animation: SiteSetting.allow_animated_avatars
)
end
end
end
end

View file

@ -1,5 +1,6 @@
require_dependency 'url_helper' require_dependency 'url_helper'
require_dependency 'file_helper' require_dependency 'file_helper'
require_dependency 'upload_creator'
module Jobs module Jobs
@ -41,7 +42,7 @@ module Jobs
if hotlinked if hotlinked
if File.size(hotlinked.path) <= @max_size if File.size(hotlinked.path) <= @max_size
filename = File.basename(URI.parse(src).path) filename = File.basename(URI.parse(src).path)
upload = Upload.create_for(post.user_id, hotlinked, filename, File.size(hotlinked.path), { origin: src }) upload = UploadCreator.new(hotlinked, filename, origin: src).create_for(post.user_id)
downloaded_urls[src] = upload.url downloaded_urls[src] = upload.url
else else
Rails.logger.info("Failed to pull hotlinked image for post: #{post_id}: #{src} - Image is bigger than #{@max_size}") Rails.logger.info("Failed to pull hotlinked image for post: #{post_id}: #{src} - Image is bigger than #{@max_size}")

View file

@ -178,7 +178,7 @@ class OptimizedImage < ActiveRecord::Base
#{from}[0] #{from}[0]
-gravity center -gravity center
-background transparent -background transparent
-resize #{dimensions}#{!!opts[:force_aspect_ratio] ? "\\!" : "\\>"} -resize #{dimensions}
-profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')} -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')}
#{to} #{to}
} }

View file

@ -1,4 +1,5 @@
require_dependency 'git_importer' require_dependency 'git_importer'
require_dependency 'upload_creator'
class RemoteTheme < ActiveRecord::Base class RemoteTheme < ActiveRecord::Base
@ -50,7 +51,7 @@ class RemoteTheme < ActiveRecord::Base
theme_info["assets"]&.each do |name, relative_path| theme_info["assets"]&.each do |name, relative_path|
if path = importer.real_path(relative_path) if path = importer.real_path(relative_path)
upload = Upload.create_for(theme.user_id, File.open(path), File.basename(relative_path), File.size(path), for_theme: true) upload = UploadCreator.new(File.open(path), File.basename(relative_path), for_theme: true).create_for(theme.user_id)
theme.set_field(target: :common, name: name, type: :theme_upload_var, upload_id: upload.id) theme.set_field(target: :common, name: name, type: :theme_upload_var, upload_id: upload.id)
end end
end end

View file

@ -1,5 +1,4 @@
require "digest/sha1" require "digest/sha1"
require_dependency "image_sizer"
require_dependency "file_helper" require_dependency "file_helper"
require_dependency "url_helper" require_dependency "url_helper"
require_dependency "db_helper" require_dependency "db_helper"
@ -22,6 +21,9 @@ class Upload < ActiveRecord::Base
validates_with ::Validators::UploadValidator validates_with ::Validators::UploadValidator
CROPPED_TYPES ||= %w{avatar card_background custom_emoji profile_background}
UPLOAD_TYPES ||= CROPPED_TYPES + %w{composer}
def thumbnail(width = self.width, height = self.height) def thumbnail(width = self.width, height = self.height)
optimized_images.find_by(width: width, height: height) optimized_images.find_by(width: width, height: height)
end end
@ -57,198 +59,10 @@ class Upload < ActiveRecord::Base
File.extname(original_filename) File.extname(original_filename)
end end
# list of image types that will be cropped
CROPPED_IMAGE_TYPES ||= %w{
avatar
profile_background
card_background
custom_emoji
}
WHITELISTED_SVG_ELEMENTS ||= %w{
circle
clippath
defs
ellipse
g
line
linearGradient
path
polygon
polyline
radialGradient
rect
stop
svg
text
textpath
tref
tspan
use
}
def self.generate_digest(path) def self.generate_digest(path)
Digest::SHA1.file(path).hexdigest Digest::SHA1.file(path).hexdigest
end end
def self.svg_whitelist_xpath
@@svg_whitelist_xpath ||= "//*[#{WHITELISTED_SVG_ELEMENTS.map { |e| "name()!='#{e}'" }.join(" and ") }]"
end
# options
# - content_type
# - origin (url)
# - image_type ("avatar", "profile_background", "card_background", "custom_emoji")
# - is_attachment_for_group_message (boolean)
# - for_theme (boolean)
def self.create_for(user_id, file, filename, filesize, options = {})
upload = Upload.new
DistributedMutex.synchronize("upload_#{user_id}_#{filename}") do
# do some work on images
if FileHelper.is_image?(filename) && is_actual_image?(file)
# retrieve image info
w, h = FastImage.size(file) || [0, 0]
if filename[/\.svg$/i]
# whitelist svg elements
doc = Nokogiri::XML(file)
doc.xpath(svg_whitelist_xpath).remove
File.write(file.path, doc.to_s)
file.rewind
else
if w * h >= SiteSetting.max_image_megapixels * 1_000_000
upload.errors.add(:base, I18n.t("upload.images.larger_than_x_megapixels", max_image_megapixels: SiteSetting.max_image_megapixels))
return upload
end
# fix orientation first
fix_image_orientation(file.path) if should_optimize?(file.path, [w, h])
end
# default size
width, height = ImageSizer.resize(w, h)
# make sure we're at the beginning of the file (both FastImage and Nokogiri move the pointer)
file.rewind
# crop images depending on their type
if CROPPED_IMAGE_TYPES.include?(options[:image_type])
allow_animation = SiteSetting.allow_animated_thumbnails
max_pixel_ratio = Discourse::PIXEL_RATIOS.max
case options[:image_type]
when "avatar"
allow_animation = SiteSetting.allow_animated_avatars
width = height = Discourse.avatar_sizes.max
OptimizedImage.resize(file.path, file.path, width, height, filename: filename, allow_animation: allow_animation)
when "profile_background"
max_width = 850 * max_pixel_ratio
width, height = ImageSizer.resize(w, h, max_width: max_width, max_height: max_width)
OptimizedImage.downsize(file.path, file.path, "#{width}x#{height}", filename: filename, allow_animation: allow_animation)
when "card_background"
max_width = 590 * max_pixel_ratio
width, height = ImageSizer.resize(w, h, max_width: max_width, max_height: max_width)
OptimizedImage.downsize(file.path, file.path, "#{width}x#{height}", filename: filename, allow_animation: allow_animation)
when "custom_emoji"
OptimizedImage.downsize(file.path, file.path, "100x100", filename: filename, allow_animation: allow_animation)
end
end
# optimize image (except GIFs, SVGs and large PNGs)
if should_optimize?(file.path, [w, h])
begin
ImageOptim.new.optimize_image!(file.path)
rescue ImageOptim::Worker::TimeoutExceeded
# Don't optimize if it takes too long
Rails.logger.warn("ImageOptim timed out while optimizing #{filename}")
end
# update the file size
filesize = File.size(file.path)
end
end
# compute the sha of the file
sha1 = Upload.generate_digest(file)
# do we already have that upload?
upload = find_by(sha1: sha1)
# make sure the previous upload has not failed
if upload && (upload.url.blank? || is_dimensionless_image?(filename, upload.width, upload.height))
upload.destroy
upload = nil
end
# return the previous upload if any
return upload unless upload.nil?
# create the upload otherwise
upload = Upload.new
upload.user_id = user_id
upload.original_filename = filename
upload.filesize = filesize
upload.sha1 = sha1
upload.url = ""
upload.width = width
upload.height = height
upload.origin = options[:origin][0...1000] if options[:origin]
if options[:is_attachment_for_group_message]
upload.is_attachment_for_group_message = true
end
if options[:for_theme]
upload.for_theme = true
end
if is_dimensionless_image?(filename, upload.width, upload.height)
upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
return upload
end
return upload unless upload.save
# store the file and update its url
File.open(file.path) do |f|
url = Discourse.store.store_upload(f, upload, options[:content_type])
if url.present?
upload.url = url
upload.save
else
upload.errors.add(:url, I18n.t("upload.store_failure", { upload_id: upload.id, user_id: user_id }))
end
end
upload
end
end
def self.is_actual_image?(file)
# due to ImageMagick CVE-20163714, use FastImage to check the magic bytes
# cf. https://meta.discourse.org/t/imagemagick-cve-2016-3714/43624
FastImage.size(file, raise_on_failure: true)
rescue
false
end
LARGE_PNG_SIZE ||= 3.megabytes
def self.should_optimize?(path, dimensions = nil)
# don't optimize GIFs or SVGs
return false if path =~ /\.(gif|svg)$/i
return true if path !~ /\.png$/i
dimensions ||= (FastImage.size(path) || [0, 0])
w, h = dimensions
# don't optimize large PNGs
w > 0 && h > 0 && w * h < LARGE_PNG_SIZE
end
def self.is_dimensionless_image?(filename, width, height)
FileHelper.is_image?(filename) && (width.blank? || width == 0 || height.blank? || height == 0)
end
def self.get_from_url(url) def self.get_from_url(url)
return if url.blank? return if url.blank?
# we store relative urls, so we need to remove any host/cdn # we store relative urls, so we need to remove any host/cdn
@ -263,10 +77,6 @@ class Upload < ActiveRecord::Base
Upload.find_by(url: url) Upload.find_by(url: url)
end end
def self.fix_image_orientation(path)
Discourse::Utils.execute_command('convert', path, '-auto-orient', path)
end
def self.migrate_to_new_scheme(limit=nil) def self.migrate_to_new_scheme(limit=nil)
problems = [] problems = []

View file

@ -1,4 +1,5 @@
require_dependency 'letter_avatar' require_dependency 'letter_avatar'
require_dependency 'upload_creator'
class UserAvatar < ActiveRecord::Base class UserAvatar < ActiveRecord::Base
belongs_to :user belongs_to :user
@ -20,7 +21,7 @@ class UserAvatar < ActiveRecord::Base
max = Discourse.avatar_sizes.max max = Discourse.avatar_sizes.max
gravatar_url = "http://www.gravatar.com/avatar/#{email_hash}.png?s=#{max}&d=404" gravatar_url = "http://www.gravatar.com/avatar/#{email_hash}.png?s=#{max}&d=404"
tempfile = FileHelper.download(gravatar_url, SiteSetting.max_image_size_kb.kilobytes, "gravatar") tempfile = FileHelper.download(gravatar_url, SiteSetting.max_image_size_kb.kilobytes, "gravatar")
upload = Upload.create_for(user_id, tempfile, 'gravatar.png', File.size(tempfile.path), origin: gravatar_url, image_type: "avatar") upload = UploadCreator.new(tempfile, 'gravatar.png', origin: gravatar_url, type: "avatar").create_for(user_id)
if gravatar_upload_id != upload.id if gravatar_upload_id != upload.id
gravatar_upload.try(:destroy!) rescue nil gravatar_upload.try(:destroy!) rescue nil
@ -65,7 +66,7 @@ class UserAvatar < ActiveRecord::Base
ext = FastImage.type(tempfile).to_s ext = FastImage.type(tempfile).to_s
tempfile.rewind tempfile.rewind
upload = Upload.create_for(user.id, tempfile, "external-avatar." + ext, File.size(tempfile.path), origin: avatar_url, image_type: "avatar") upload = UploadCreator.new(tempfile, "external-avatar." + ext, origin: avatar_url, type: "avatar").create_for(user.id)
user.create_user_avatar unless user.user_avatar user.create_user_avatar unless user.user_avatar

View file

@ -1179,8 +1179,7 @@ en:
allow_all_attachments_for_group_messages: "Allow all email attachments for group messages." allow_all_attachments_for_group_messages: "Allow all email attachments for group messages."
convert_pasted_images_to_hq_jpg: "Convert pasted images to high-quality JPG files." png_to_jpg_quality: "Quality of the converted JPG file (1 is lowest quality, 99 is best quality, 100 to disable)."
convert_pasted_images_quality: "Quality of the converted JPG file (1 is lowest quality, 100 is best quality)."
enable_flash_video_onebox: "Enable embedding of swf and flv (Adobe Flash) links in oneboxes. WARNING: may introduce security risks." enable_flash_video_onebox: "Enable embedding of swf and flv (Adobe Flash) links in oneboxes. WARNING: may introduce security risks."
@ -2721,12 +2720,15 @@ en:
deleted: 'deleted' deleted: 'deleted'
image: "image"
upload: upload:
edit_reason: "downloaded local copies of images" edit_reason: "downloaded local copies of images"
unauthorized: "Sorry, the file you are trying to upload is not authorized (authorized extensions: %{authorized_extensions})." unauthorized: "Sorry, the file you are trying to upload is not authorized (authorized extensions: %{authorized_extensions})."
pasted_image_filename: "Pasted image" pasted_image_filename: "Pasted image"
store_failure: "Failed to store upload #%{upload_id} for user #%{user_id}." store_failure: "Failed to store upload #%{upload_id} for user #%{user_id}."
file_missing: "Sorry, you must provide a file to upload." file_missing: "Sorry, you must provide a file to upload."
empty: "Sorry, but the file you provided is empty."
attachments: attachments:
too_large: "Sorry, the file you are trying to upload is too big (maximum size is %{max_size_kb}KB)." too_large: "Sorry, the file you are trying to upload is too big (maximum size is %{max_size_kb}KB)."
images: images:

View file

@ -794,8 +794,7 @@ files:
regex: '^((https?:)?\/)?\/.+[^\/]' regex: '^((https?:)?\/)?\/.+[^\/]'
shadowed_by_global: true shadowed_by_global: true
allow_all_attachments_for_group_messages: false allow_all_attachments_for_group_messages: false
convert_pasted_images_to_hq_jpg: true png_to_jpg_quality:
convert_pasted_images_quality:
default: 95 default: 95
min: 1 min: 1
max: 100 max: 100

View file

@ -2,6 +2,7 @@ require "digest"
require_dependency "new_post_manager" require_dependency "new_post_manager"
require_dependency "post_action_creator" require_dependency "post_action_creator"
require_dependency "html_to_markdown" require_dependency "html_to_markdown"
require_dependency "upload_creator"
module Email module Email
@ -570,7 +571,7 @@ module Email
File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded }
# create the upload for the user # create the upload for the user
opts = { is_attachment_for_group_message: options[:is_group_message] } opts = { is_attachment_for_group_message: options[:is_group_message] }
upload = Upload.create_for(options[:user].id, tmp, attachment.filename, tmp.size, opts) upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(options[:user].id)
if upload && upload.errors.empty? if upload && upload.errors.empty?
# try to inline images # try to inline images
if attachment.content_type.start_with?("image/") if attachment.content_type.start_with?("image/")

View file

@ -143,7 +143,7 @@ def migrate_from_s3
if filename = guess_filename(url, post.raw) if filename = guess_filename(url, post.raw)
puts "FILENAME: #{filename}" puts "FILENAME: #{filename}"
file = FileHelper.download("http:#{url}", 20.megabytes, "from_s3", true) file = FileHelper.download("http:#{url}", 20.megabytes, "from_s3", true)
if upload = Upload.create_for(post.user_id || -1, file, filename, File.size(file)) if upload = UploadCreator.new(file, filename, File.size(file)).create_for(post.user_id || -1)
post.raw = post.raw.gsub(/(https?:)?#{Regexp.escape(url)}/, upload.url) post.raw = post.raw.gsub(/(https?:)?#{Regexp.escape(url)}/, upload.url)
post.save post.save
post.rebake! post.rebake!
@ -433,7 +433,7 @@ def recover_from_tombstone
if File.exists?(tombstone_path) if File.exists?(tombstone_path)
File.open(tombstone_path) do |file| File.open(tombstone_path) do |file|
new_upload = Upload.create_for(Discourse::SYSTEM_USER_ID, file, File.basename(url), File.size(file)) new_upload = UploadCreator.new(file, File.basename(url), File.size(file)).create_for(Discourse::SYSTEM_USER_ID)
if new_upload.persisted? if new_upload.persisted?
printf "Restored into #{new_upload.url}\n" printf "Restored into #{new_upload.url}\n"

255
lib/upload_creator.rb Normal file
View file

@ -0,0 +1,255 @@
require "fastimage"
require_dependency "image_sizer"
class UploadCreator
TYPES_CONVERTED_TO_JPEG ||= %i{bmp png}
WHITELISTED_SVG_ELEMENTS ||= %w{
circle clippath defs ellipse g line linearGradient path polygon polyline
radialGradient rect stop svg text textpath tref tspan use
}
# Available options
# - type (string)
# - content_type (string)
# - origin (string)
# - is_attachment_for_group_message (boolean)
# - for_theme (boolean)
def initialize(file, filename, opts = {})
@upload = Upload.new
@file = file
@filename = filename
@opts = opts
end
def create_for(user_id)
if filesize <= 0
@upload.errors.add(:base, I18n.t("upload.empty"))
return @upload
end
DistributedMutex.synchronize("upload_#{user_id}_#{@filename}") do
if FileHelper.is_image?(@filename)
extract_image_info!
return @upload if @upload.errors.present?
if @filename[/\.svg$/i]
whitelist_svg!
else
convert_to_jpeg! if should_convert_to_jpeg?
downsize! if should_downsize?
return @upload if is_still_too_big?
fix_orientation! if should_fix_orientation?
crop! if should_crop?
optimize! if should_optimize?
end
end
# compute the sha of the file
sha1 = Upload.generate_digest(@file)
# do we already have that upload?
@upload = Upload.find_by(sha1: sha1)
# make sure the previous upload has not failed
if @upload && @upload.url.blank?
@upload.destroy
@upload = nil
end
# return the previous upload if any
return @upload unless @upload.nil?
# create the upload otherwise
@upload = Upload.new
@upload.user_id = user_id
@upload.original_filename = @filename
@upload.filesize = filesize
@upload.sha1 = sha1
@upload.url = ""
@upload.origin = @opts[:origin][0...1000] if @opts[:origin]
if FileHelper.is_image?(@filename)
@upload.width, @upload.height = ImageSizer.resize(*@image_info.size)
end
if @opts[:is_attachment_for_group_message]
@upload.is_attachment_for_group_message = true
end
if @opts[:for_theme]
@upload.for_theme = true
end
return @upload unless @upload.save
# store the file and update its url
File.open(@file.path) do |f|
url = Discourse.store.store_upload(f, @upload, @opts[:content_type])
if url.present?
@upload.url = url
@upload.save
else
@upload.errors.add(:url, I18n.t("upload.store_failure", upload_id: @upload.id, user_id: user_id))
end
end
if @upload.errors.empty? && FileHelper.is_image?(@filename) && @opts[:type] == "avatar"
Jobs.enqueue(:create_avatar_thumbnails, upload_id: @upload.id, user_id: user_id)
end
@upload
end
ensure
@file.close! rescue nil
end
def extract_image_info!
@image_info = FastImage.new(@file) rescue nil
@file.rewind
if @image_info.nil?
@upload.errors.add(:base, I18n.t("upload.images.not_supported_or_corrupted"))
elsif filesize <= 0
@upload.errors.add(:base, I18n.t("upload.empty"))
elsif pixels == 0
@upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
end
end
def should_convert_to_jpeg?
TYPES_CONVERTED_TO_JPEG.include?(@image_info.type) &&
@image_info.size.min > 720 &&
SiteSetting.png_to_jpg_quality < 100
end
def convert_to_jpeg!
jpeg_tempfile = Tempfile.new(["image", ".jpg"])
OptimizedImage.ensure_safe_paths!(@file.path, jpeg_tempfile.path)
Discourse::Utils.execute_command('convert', @file.path, '-quality', SiteSetting.png_to_jpg_quality.to_s, jpeg_tempfile.path)
# keep the JPEG if it's at least 15% smaller
if File.size(jpeg_tempfile.path) < filesize * 0.85
@image_info = FastImage.new(jpeg_tempfile)
@file = jpeg_tempfile
@filename = (File.basename(@filename, ".*").presence || I18n.t("image").presence || "image") + ".jpg"
@opts[:content_type] = "image/jpeg"
else
jpeg_tempfile.close! rescue nil
end
end
def should_downsize?
max_image_size > 0 && filesize >= max_image_size
end
def downsize!
3.times do
original_size = filesize
downsized_pixels = [pixels, max_image_pixels].min / 2
OptimizedImage.downsize(@file.path, @file.path, "#{downsized_pixels}@", filename: @filename, allow_animation: allow_animation)
extract_image_info!
return if filesize >= original_size || pixels == 0 || !should_downsize?
end
end
def is_still_too_big?
if max_image_pixels > 0 && pixels >= max_image_pixels
@upload.errors.add(:base, I18n.t("upload.images.larger_than_x_megapixels", max_image_megapixels: SiteSetting.max_image_megapixels))
true
elsif max_image_size > 0 && filesize >= max_image_size
@upload.errors.add(:base, I18n.t("upload.images.too_large", max_size_kb: SiteSetting.max_image_size_kb))
true
else
false
end
end
def whitelist_svg!
doc = Nokogiri::XML(@file)
doc.xpath(svg_whitelist_xpath).remove
File.write(@file.path, doc.to_s)
@file.rewind
end
def should_crop?
Upload::CROPPED_TYPES.include?(@opts[:type])
end
def crop!
max_pixel_ratio = Discourse::PIXEL_RATIOS.max
case @opts[:type]
when "avatar"
width = height = Discourse.avatar_sizes.max
OptimizedImage.resize(@file.path, @file.path, width, height, filename: @filename, allow_animation: allow_animation)
when "profile_background"
max_width = 850 * max_pixel_ratio
width, height = ImageSizer.resize(@image_info.size[0], @image_info.size[1], max_width: max_width, max_height: max_width)
OptimizedImage.downsize(@file.path, @file.path, "#{width}x#{height}\\>", filename: @filename, allow_animation: allow_animation)
when "card_background"
max_width = 590 * max_pixel_ratio
width, height = ImageSizer.resize(@image_info.size[0], @image_info.size[1], max_width: max_width, max_height: max_width)
OptimizedImage.downsize(@file.path, @file.path, "#{width}x#{height}\\>", filename: @filename, allow_animation: allow_animation)
when "custom_emoji"
OptimizedImage.downsize(@file.path, @file.path, "100x100\\>", filename: @filename, allow_animation: allow_animation)
end
end
def should_fix_orientation?
# orientation is between 1 and 8, 1 being the default
# cf. http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
@image_info.orientation.to_i > 1
end
def fix_orientation!
OptimizedImage.ensure_safe_paths!(@file.path)
Discourse::Utils.execute_command('convert', @file.path, '-auto-orient', @file.path)
end
def should_optimize?
# GIF is too slow (plus, we'll soon be converting them to MP4)
# Optimizing SVG is useless
return false if @file.path =~ /\.(gif|svg)$/i
# Safeguard for large PNGs
return pixels < 2_000_000 if @file.path =~ /\.png/i
# Everything else is fine!
true
end
def optimize!
OptimizedImage.ensure_safe_paths!(@file.path)
ImageOptim.new.optimize_image!(@file.path)
rescue ImageOptim::Worker::TimeoutExceeded
Rails.logger.warn("ImageOptim timed out while optimizing #{@filename}")
end
def filesize
File.size?(@file.path).to_i
end
def max_image_size
@@max_image_size ||= SiteSetting.max_image_size_kb.kilobytes
end
def max_image_pixels
@@max_image_pixels ||= SiteSetting.max_image_megapixels * 1_000_000
end
def pixels
@image_info.size&.reduce(:*).to_i
end
def allow_animation
@@allow_animation ||= @opts[:type] == "avatar" ? SiteSetting.allow_animated_avatars : SiteSetting.allow_animated_thumbnails
end
def svg_whitelist_xpath
@@svg_whitelist_xpath ||= "//*[#{WHITELISTED_SVG_ELEMENTS.map { |e| "name()!='#{e}'" }.join(" and ") }]"
end
end

View file

@ -15,7 +15,7 @@ module ImportScripts
src.close src.close
tmp.rewind tmp.rewind
Upload.create_for(user_id, tmp, source_filename, tmp.size) UploadCreator.new(tmp, source_filename).create_for(user_id)
rescue => e rescue => e
Rails.logger.error("Failed to create upload: #{e}") Rails.logger.error("Failed to create upload: #{e}")
nil nil

View file

@ -144,7 +144,7 @@ class ImportScripts::Lithium < ImportScripts::Base
file.write(picture["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8")) file.write(picture["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
file.rewind file.rewind
upload = Upload.create_for(imported_user.id, file, picture["filename"], file.size) upload = UploadCreator.new(file, picture["filename"]).create_for(imported_user.id)
return if !upload.persisted? return if !upload.persisted?
@ -173,7 +173,7 @@ class ImportScripts::Lithium < ImportScripts::Base
file.write(background["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8")) file.write(background["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
file.rewind file.rewind
upload = Upload.create_for(imported_user.id, file, background["filename"], file.size) upload = UploadCreator.new(file, background["filename"]).create_for(imported_user.id)
return if !upload.persisted? return if !upload.persisted?
@ -807,7 +807,7 @@ SQL
if image if image
File.open(image) do |file| File.open(image) do |file|
upload = Upload.create_for(user_id, file, "image." + (image =~ /.png$/ ? "png": "jpg"), File.size(image)) upload = UploadCreator.new(file, "image." + (image.ends_with?(".png") ? "png" : "jpg")).create_for(user_id)
l["src"] = upload.url l["src"] = upload.url
end end
else else

View file

@ -439,7 +439,7 @@ p end
# read attachment # read attachment
File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded }
# create the upload for the user # create the upload for the user
upload = Upload.create_for(user_id_from_imported_user_id(from_email) || Discourse::SYSTEM_USER_ID, tmp, attachment.filename, tmp.size ) upload = UploadCreator.new(tmp, attachment.filename).create_for(user_id_from_imported_user_id(from_email) || Discourse::SYSTEM_USER_ID)
if upload && upload.errors.empty? if upload && upload.errors.empty?
raw << "\n\n#{receiver.attachment_markdown(upload)}\n\n" raw << "\n\n#{receiver.attachment_markdown(upload)}\n\n"
end end
@ -530,7 +530,7 @@ p end
# read attachment # read attachment
File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded }
# create the upload for the user # create the upload for the user
upload = Upload.create_for(user_id_from_imported_user_id(from_email) || Discourse::SYSTEM_USER_ID, tmp, attachment.filename, tmp.size ) upload = UploadCreator.new(tmp, attachment.filename).create_for(user_id_from_imported_user_id(from_email) || Discourse::SYSTEM_USER_ID)
if upload && upload.errors.empty? if upload && upload.errors.empty?
raw << "\n\n#{receiver.attachment_markdown(upload)}\n\n" raw << "\n\n#{receiver.attachment_markdown(upload)}\n\n"
end end

View file

@ -101,7 +101,7 @@ class ImportScripts::Sfn < ImportScripts::Base
avatar.write(user["avatar"].encode("ASCII-8BIT").force_encoding("UTF-8")) avatar.write(user["avatar"].encode("ASCII-8BIT").force_encoding("UTF-8"))
avatar.rewind avatar.rewind
upload = Upload.create_for(newuser.id, avatar, "avatar.jpg", avatar.size) upload = UploadCreator.new(avatar, "avatar.jpg").create_for(newuser.id)
if upload.persisted? if upload.persisted?
newuser.create_user_avatar newuser.create_user_avatar
newuser.user_avatar.update(custom_upload_id: upload.id) newuser.user_avatar.update(custom_upload_id: upload.id)

View file

@ -201,7 +201,7 @@ EOM
file.write(picture["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8")) file.write(picture["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
file.rewind file.rewind
upload = Upload.create_for(imported_user.id, file, picture["filename"], file.size) upload = UploadCreator.new(file, picture["filename"]).create_for(imported_user.id)
return if !upload.persisted? return if !upload.persisted?
@ -231,7 +231,7 @@ EOM
file.write(background["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8")) file.write(background["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
file.rewind file.rewind
upload = Upload.create_for(imported_user.id, file, background["filename"], file.size) upload = UploadCreator.new(file, background["filename"]).create_for(imported_user.id)
return if !upload.persisted? return if !upload.persisted?

View file

@ -123,7 +123,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
file = Tempfile.new("profile-picture") file = Tempfile.new("profile-picture")
file.write(picture["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8")) file.write(picture["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
file.rewind file.rewind
upload = Upload.create_for(imported_user.id, file, picture["filename"], file.size) upload = UploadCreator.new(file, picture["filename"]).create_for(imported_user.id)
else else
filename = File.join(AVATAR_DIR, picture['filename']) filename = File.join(AVATAR_DIR, picture['filename'])
unless File.exists?(filename) unless File.exists?(filename)
@ -160,7 +160,7 @@ class ImportScripts::VBulletin < ImportScripts::Base
file.write(background["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8")) file.write(background["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8"))
file.rewind file.rewind
upload = Upload.create_for(imported_user.id, file, background["filename"], file.size) upload = UploadCreator.new(file, background["filename"]).create_for(imported_user.id)
return if !upload.persisted? return if !upload.persisted?

View file

@ -48,7 +48,7 @@ describe UploadsController do
end end
it 'is successful with an image' do it 'is successful with an image' do
Jobs.expects(:enqueue).with(:create_thumbnails, anything) Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything)
message = MessageBus.track_publish do message = MessageBus.track_publish do
xhr :post, :create, file: logo, type: "avatar" xhr :post, :create, file: logo, type: "avatar"
@ -78,7 +78,7 @@ describe UploadsController do
SiteSetting.authorized_extensions = "*" SiteSetting.authorized_extensions = "*"
controller.stubs(:is_api?).returns(true) controller.stubs(:is_api?).returns(true)
Jobs.expects(:enqueue).with(:create_thumbnails, anything) Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything)
stub_request(:get, "http://example.com/image.png").to_return(body: File.read('spec/fixtures/images/logo.png')) stub_request(:get, "http://example.com/image.png").to_return(body: File.read('spec/fixtures/images/logo.png'))
@ -92,7 +92,7 @@ describe UploadsController do
it 'correctly sets retain_hours for admins' do it 'correctly sets retain_hours for admins' do
log_in :admin log_in :admin
Jobs.expects(:enqueue).with(:create_thumbnails, anything) Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never
message = MessageBus.track_publish do message = MessageBus.track_publish do
xhr :post, :create, file: logo, retain_hours: 100, type: "profile_background" xhr :post, :create, file: logo, retain_hours: 100, type: "profile_background"
@ -110,7 +110,7 @@ describe UploadsController do
end.first end.first
expect(response.status).to eq 200 expect(response.status).to eq 200
expect(message.data["errors"]).to eq(I18n.t("upload.file_missing")) expect(message.data["errors"]).to contain_exactly(I18n.t("upload.file_missing"))
end end
it 'properly returns errors' do it 'properly returns errors' do
@ -139,7 +139,7 @@ describe UploadsController do
end end
it 'returns an error when it could not determine the dimensions of an image' do it 'returns an error when it could not determine the dimensions of an image' do
Jobs.expects(:enqueue).with(:create_thumbnails, anything).never Jobs.expects(:enqueue).with(:create_avatar_thumbnails, anything).never
message = MessageBus.track_publish do message = MessageBus.track_publish do
xhr :post, :create, file: fake_jpg, type: "composer" xhr :post, :create, file: fake_jpg, type: "composer"
@ -148,8 +148,7 @@ describe UploadsController do
expect(response.status).to eq 200 expect(response.status).to eq 200
expect(message.channel).to eq("/uploads/composer") expect(message.channel).to eq("/uploads/composer")
expect(message.data["errors"]).to be expect(message.data["errors"]).to contain_exactly(I18n.t("upload.images.size_not_found"))
expect(message.data["errors"][0]).to eq(I18n.t("upload.images.size_not_found"))
end end
end end

View file

@ -171,7 +171,7 @@ HTML
it 'can handle uploads based of ThemeField' do it 'can handle uploads based of ThemeField' do
theme = Theme.new(name: 'theme', user_id: -1) theme = Theme.new(name: 'theme', user_id: -1)
upload = Upload.create_for(-1, image, "logo.png", File.size(image)) upload = UploadCreator.new(image, "logo.png").create_for(-1)
theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var) theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var)
theme.set_field(target: :common, name: :scss, value: 'body {background-image: url($logo)}') theme.set_field(target: :common, name: :scss, value: 'body {background-image: url($logo)}')
theme.save! theme.save!

View file

@ -46,91 +46,6 @@ describe Upload do
end end
context "#create_for" do
before do
Upload.stubs(:fix_image_orientation)
ImageOptim.any_instance.stubs(:optimize_image!)
end
it "does not create another upload if it already exists" do
Upload.expects(:find_by).with(sha1: image_sha1).returns(upload)
Upload.expects(:save).never
expect(Upload.create_for(user_id, image, image_filename, image_filesize)).to eq(upload)
end
it "ensures images isn't huge before processing it" do
Upload.expects(:fix_image_orientation).never
upload = Upload.create_for(user_id, huge_image, huge_image_filename, huge_image_filesize)
expect(upload.errors.size).to be > 0
end
it "fix image orientation" do
Upload.expects(:fix_image_orientation).with(image.path)
Upload.create_for(user_id, image, image_filename, image_filesize)
end
it "computes width & height for images" do
ImageSizer.expects(:resize)
Upload.create_for(user_id, image, image_filename, image_filesize)
end
it "does not compute width & height for non-image" do
FastImage.any_instance.expects(:size).never
upload = Upload.create_for(user_id, attachment, attachment_filename, attachment_filesize)
expect(upload.errors.size).to be > 0
end
it "generates an error when the image is too large" do
SiteSetting.stubs(:max_image_size_kb).returns(1)
upload = Upload.create_for(user_id, image, image_filename, image_filesize)
expect(upload.errors.size).to be > 0
end
it "generates an error when the attachment is too large" do
SiteSetting.stubs(:max_attachment_size_kb).returns(1)
upload = Upload.create_for(user_id, attachment, attachment_filename, attachment_filesize)
expect(upload.errors.size).to be > 0
end
it "saves proper information" do
store = {}
Discourse.expects(:store).returns(store)
store.expects(:store_upload).returns(url)
upload = Upload.create_for(user_id, image, image_filename, image_filesize)
expect(upload.user_id).to eq(user_id)
expect(upload.original_filename).to eq(image_filename)
expect(upload.filesize).to eq(image_filesize)
expect(upload.width).to eq(244)
expect(upload.height).to eq(66)
expect(upload.url).to eq(url)
end
context "when svg is authorized" do
before { SiteSetting.stubs(:authorized_extensions).returns("svg") }
it "consider SVG as an image" do
store = {}
Discourse.expects(:store).returns(store)
store.expects(:store_upload).returns(url)
upload = Upload.create_for(user_id, image_svg, image_svg_filename, image_svg_filesize)
expect(upload.user_id).to eq(user_id)
expect(upload.original_filename).to eq(image_svg_filename)
expect(upload.filesize).to eq(image_svg_filesize)
expect(upload.width).to eq(100)
expect(upload.height).to eq(50)
expect(upload.url).to eq(url)
end
end
end
context ".get_from_url" do context ".get_from_url" do
let(:url) { "/uploads/default/original/3X/1/0/10f73034616a796dfd70177dc54b6def44c4ba6f.png" } let(:url) { "/uploads/default/original/3X/1/0/10f73034616a796dfd70177dc54b6def44c4ba6f.png" }
let(:upload) { Fabricate(:upload, url: url) } let(:upload) { Fabricate(:upload, url: url) }