FEATURE: support email attachments

This commit is contained in:
Régis Hanol
2014-04-14 22:55:57 +02:00
parent ed6e2b1d79
commit 2505d18aa9
29 changed files with 432 additions and 538 deletions

View File

@@ -261,15 +261,16 @@ Discourse.Utilities = {
switch (data.jqXHR.status) {
// cancel from the user
case 0: return;
// entity too large, usually returned from the web server
case 413:
var maxSizeKB = Discourse.SiteSettings.max_image_size_kb;
bootbox.alert(I18n.t('post.errors.image_too_large', { max_size_kb: maxSizeKB }));
return;
// the error message is provided by the server
case 415: // media type not authorized
case 422: // there has been an error on the server (mostly due to FastImage)
bootbox.alert(data.jqXHR.responseText);
case 422:
bootbox.alert(data.jqXHR.responseJSON.join("\n"));
return;
}
}

View File

@@ -5,45 +5,29 @@ class UploadsController < ApplicationController
def create
file = params[:file] || params[:files].first
# check if the extension is allowed
unless SiteSetting.authorized_upload?(file)
text = I18n.t("upload.unauthorized", authorized_extensions: SiteSetting.authorized_extensions.gsub("|", ", "))
return render status: 415, text: text
end
# check the file size (note: this might also be done in the web server)
filesize = File.size(file.tempfile)
type = SiteSetting.authorized_image?(file) ? "image" : "attachment"
max_size_kb = SiteSetting.send("max_#{type}_size_kb").kilobytes
return render status: 413, text: I18n.t("upload.#{type}s.too_large", max_size_kb: max_size_kb) if filesize > max_size_kb
upload = Upload.create_for(current_user.id, file.tempfile, file.original_filename, filesize)
upload = Upload.create_for(current_user.id, file, filesize)
render_serialized(upload, UploadSerializer, root: false)
rescue FastImage::ImageFetchFailure
render status: 422, text: I18n.t("upload.images.fetch_failure")
rescue FastImage::UnknownImageType
render status: 422, text: I18n.t("upload.images.unknown_image_type")
rescue FastImage::SizeNotFound
render status: 422, text: I18n.t("upload.images.size_not_found")
if upload.errors.empty?
render_serialized(upload, UploadSerializer, root: false)
else
render status: 422, text: upload.errors.full_messages
end
end
def show
RailsMultisite::ConnectionManagement.with_connection(params[:site]) do |db|
return render nothing: true, status: 404 unless Discourse.store.internal?
id = params[:id].to_i
url = request.fullpath
# the "url" parameter is here to prevent people from scanning the uploads using the id
upload = Upload.where(id: id, url: url).first
return render nothing: true, status: 404 unless upload
send_file(Discourse.store.path_for(upload), filename: upload.original_filename)
if upload = Upload.where(id: id, url: url).first
send_file(Discourse.store.path_for(upload), filename: upload.original_filename)
else
render nothing: true, status: 404
end
end
end

View File

@@ -307,14 +307,13 @@ class UsersController < ApplicationController
size = 128 if size > 128
size
end
# LEGACY: used by the API
def upload_avatar
params[:user_image_type] = "avatar"
upload_user_image
end
def upload_user_image
params.require(:user_image_type)
user = fetch_user_from_params
@@ -322,39 +321,24 @@ class UsersController < ApplicationController
file = params[:file] || params[:files].first
# Only allow url uploading for API users
# TODO: Does not protect from huge uploads
# https://github.com/discourse/discourse/pull/1512
# check the file size (note: this might also be done in the web server)
img = build_user_image_from(file)
upload_policy = AvatarUploadPolicy.new(img)
if upload_policy.too_big?
return render status: 413, text: I18n.t("upload.images.too_large",
max_size_kb: upload_policy.max_size_kb)
begin
image = build_user_image_from(file)
rescue Discourse::InvalidParameters
return render status: 422, text: I18n.t("upload.images.unknown_image_type")
end
raise FastImage::UnknownImageType unless SiteSetting.authorized_image?(img.file)
upload_type = params[:user_image_type]
if upload_type == "avatar"
upload_avatar_for(user, img)
elsif upload_type == "profile_background"
upload_profile_background_for(user, img)
upload = Upload.create_for(user.id, image.file, image.filename, image.filesize)
if upload.errors.empty?
case params[:user_image_type]
when "avatar"
upload_avatar_for(user, upload)
when "profile_background"
upload_profile_background_for(user, upload)
end
else
render status: 422, text: ""
render status: 422, text: upload.errors.full_messages
end
rescue Discourse::InvalidParameters
render status: 422, text: I18n.t("upload.images.unknown_image_type")
rescue FastImage::ImageFetchFailure
render status: 422, text: I18n.t("upload.images.fetch_failure")
rescue FastImage::UnknownImageType
render status: 422, text: I18n.t("upload.images.unknown_image_type")
rescue FastImage::SizeNotFound
render status: 422, text: I18n.t("upload.images.size_not_found")
end
def toggle_avatar
@@ -367,21 +351,23 @@ class UsersController < ApplicationController
render nothing: true
end
def clear_profile_background
user = fetch_user_from_params
guardian.ensure_can_edit!(user)
user.profile_background = ""
user.save!
render nothing: true
end
def destroy
@user = fetch_user_from_params
guardian.ensure_can_delete_user!(@user)
UserDestroyer.new(current_user).destroy(@user, {delete_posts: true, context: params[:context]})
render json: success_json
end
@@ -403,31 +389,28 @@ class UsersController < ApplicationController
def build_user_image_from(file)
source = if file.is_a?(String)
is_api? ? :url : (raise FastImage::UnknownImageType)
is_api? ? :url : (raise Discourse::InvalidParameters)
else
:image
end
AvatarUploadService.new(file, source)
end
def upload_avatar_for(user, avatar)
upload = Upload.create_for(user.id, avatar.file, avatar.filesize)
def upload_avatar_for(user, upload)
user.upload_avatar(upload)
Jobs.enqueue(:generate_avatars, user_id: user.id, upload_id: upload.id)
render json: { url: upload.url, width: upload.width, height: upload.height }
end
def upload_profile_background_for(user, background)
upload = Upload.create_for(user.id, background.file, background.filesize)
user.profile_background = upload.url
user.save!
# TODO: maybe add a resize job here
def upload_profile_background_for(user, upload)
user.upload_profile_background(upload)
# TODO: add a resize job here
render json: { url: upload.url, width: upload.width, height: upload.height }
end
def respond_to_suspicious_request
if suspicious?(params)
render(

View File

@@ -1,4 +1,5 @@
require_dependency 'url_helper'
require_dependency 'file_helper'
module Jobs
@@ -30,14 +31,13 @@ module Jobs
begin
# have we already downloaded that file?
if !downloaded_urls.include?(src)
hotlinked = download(src)
hotlinked = FileHelper.download(src, @max_size, "discourse-hotlinked") rescue Discourse::InvalidParameters
if hotlinked.try(:size) <= @max_size
filename = File.basename(URI.parse(src).path)
file = ActionDispatch::Http::UploadedFile.new(tempfile: hotlinked, filename: filename)
upload = Upload.create_for(post.user_id, file, hotlinked.size, src)
upload = Upload.create_for(post.user_id, hotlinked, filename, hotlinked.size, src)
downloaded_urls[src] = upload.url
else
puts "Failed to pull hotlinked image: #{src} - Image is bigger than #{@max_size}"
Rails.logger.error("Failed to pull hotlinked image: #{src} - Image is bigger than #{@max_size}")
end
end
# have we successfully downloaded that file?
@@ -59,7 +59,7 @@ module Jobs
raw.gsub!(src, "<img src='#{url}'>")
end
rescue => e
puts "Failed to pull hotlinked image: #{src}\n" + e.message + "\n" + e.backtrace.join("\n")
Rails.logger.error("Failed to pull hotlinked image: #{src}\n" + e.message + "\n" + e.backtrace.join("\n"))
ensure
# close & delete the temp file
hotlinked && hotlinked.close!
@@ -87,22 +87,6 @@ module Jobs
!src.start_with?(Discourse.asset_host || Discourse.base_url_no_prefix)
end
def download(url)
return if @max_size <= 0
extension = File.extname(URI.parse(url).path)
tmp = Tempfile.new(["discourse-hotlinked", extension])
File.open(tmp.path, "wb") do |f|
hotlinked = open(url, "rb", read_timeout: 5)
while f.size <= @max_size && data = hotlinked.read(@max_size)
f.write(data)
end
hotlinked.close!
end
tmp
end
end
end

View File

@@ -49,6 +49,7 @@ module Jobs
handle_mail(mail)
end
end
pop.finish
end
rescue Net::POPAuthenticationError => e
# inform admins about the error (1 message per hour to prevent too much SPAM)

View File

@@ -72,28 +72,6 @@ class SiteSetting < ActiveRecord::Base
.first
end
def self.authorized_uploads
authorized_extensions.tr(" ", "")
.split("|")
.map { |extension| (extension.start_with?(".") ? extension[1..-1] : extension).gsub(".", "\.") }
end
def self.authorized_upload?(file)
authorized_uploads.count > 0 && file.original_filename =~ /\.(#{authorized_uploads.join("|")})$/i
end
def self.images
@images ||= Set.new ["jpg", "jpeg", "png", "gif", "tif", "tiff", "bmp"]
end
def self.authorized_images
authorized_uploads.select { |extension| images.include?(extension) }
end
def self.authorized_image?(file)
authorized_images.count > 0 && file.original_filename =~ /\.(#{authorized_images.join("|")})$/i
end
def self.scheme
use_https? ? "https" : "http"
end

View File

@@ -1,5 +1,7 @@
require "digest/sha1"
require "image_sizer"
require_dependency "image_sizer"
require_dependency "file_helper"
require_dependency "validators/upload_validator"
class Upload < ActiveRecord::Base
belongs_to :user
@@ -12,6 +14,8 @@ class Upload < ActiveRecord::Base
validates_presence_of :filesize
validates_presence_of :original_filename
validates_with ::Validators::UploadValidator
def thumbnail(width = self.width, height = self.height)
optimized_images.where(width: width, height: height).first
end
@@ -42,9 +46,9 @@ class Upload < ActiveRecord::Base
File.extname(original_filename)
end
def self.create_for(user_id, file, filesize, origin = nil)
def self.create_for(user_id, file, filename, filesize, origin = nil)
# compute the sha
sha1 = Digest::SHA1.file(file.tempfile).hexdigest
sha1 = Digest::SHA1.file(file).hexdigest
# check if the file has already been uploaded
upload = Upload.where(sha1: sha1).first
# delete the previously uploaded file if there's been an error
@@ -54,37 +58,50 @@ class Upload < ActiveRecord::Base
end
# create the upload
unless upload
# deal with width & height for images
if SiteSetting.authorized_image?(file)
# retrieve image info
image_info = FastImage.new(file.tempfile, raise_on_failure: true)
# compute image aspect ratio
width, height = ImageSizer.resize(*image_info.size)
# make sure we're at the beginning of the file (FastImage is moving the pointer)
file.rewind
end
# trim the origin if any
origin = origin[0...1000] if origin
# create a db record (so we can use the id)
upload = Upload.create!(
# initialize a new upload
upload = Upload.new(
user_id: user_id,
original_filename: file.original_filename,
original_filename: filename,
filesize: filesize,
sha1: sha1,
url: "",
width: width,
height: height,
origin: origin,
url: ""
)
# trim the origin if any
upload.origin = origin[0...1000] if origin
# deal with width & height for images
if FileHelper.is_image?(filename)
begin
# retrieve image info
image_info = FastImage.new(file, raise_on_failure: true)
# compute image aspect ratio
upload.width, upload.height = ImageSizer.resize(*image_info.size)
# make sure we're at the beginning of the file (FastImage moves the pointer)
file.rewind
rescue FastImage::ImageFetchFailure
upload.errors.add(:base, I18n.t("upload.images.fetch_failure"))
rescue FastImage::UnknownImageType
upload.errors.add(:base, I18n.t("upload.images.unknown_image_type"))
rescue FastImage::SizeNotFound
upload.errors.add(:base, I18n.t("upload.images.size_not_found"))
end
return upload unless upload.errors.empty?
end
# create a db record (so we can use the id)
return upload unless upload.save
# store the file and update its url
url = Discourse.store.store_upload(file, upload)
if url.present?
upload.url = url
upload.save
else
Rails.logger.error("Failed to store upload ##{upload.id} for user ##{user_id}")
upload.errors.add(:url, I18n.t("upload.store_failure", { upload_id: upload.id, user_id: user_id }))
end
end
# return the uploaded file
upload
end

View File

@@ -527,13 +527,18 @@ class User < ActiveRecord::Base
created_at > 1.day.ago
end
def upload_avatar(avatar)
def upload_avatar(upload)
self.uploaded_avatar_template = nil
self.uploaded_avatar = avatar
self.uploaded_avatar = upload
self.use_uploaded_avatar = true
self.save!
end
def upload_profile_background(upload)
self.profile_background = upload.url
self.save!
end
def generate_api_key(created_by)
if api_key.present?
api_key.regenerate!(created_by)

View File

@@ -1,64 +0,0 @@
# For converting urls to files
class UriAdapter
attr_reader :target, :content, :tempfile, :original_filename
def initialize(target)
raise Discourse::InvalidParameters unless target =~ /^https?:\/\//
@target = Addressable::URI.parse(target)
@original_filename = ::File.basename(@target.path)
@content = download_content
@tempfile = TempfileFactory.new.generate(@original_filename)
end
def download_content
open(target.normalize)
end
def copy_to_tempfile(src)
while data = src.read(16.kilobytes)
tempfile.write(data)
end
src.close
tempfile.rewind
tempfile
end
def file_size
content.size
end
def build_uploaded_file
return if SiteSetting.max_image_size_kb.kilobytes < file_size
copy_to_tempfile(content)
content_type = content.content_type if content.respond_to?(:content_type)
content_type ||= "text/html"
ActionDispatch::Http::UploadedFile.new( tempfile: tempfile,
filename: original_filename,
type: content_type
)
end
end
# From https://github.com/thoughtbot/paperclip/blob/master/lib/paperclip/tempfile_factory.rb
class TempfileFactory
ILLEGAL_FILENAME_CHARACTERS = /^~/
def generate(name)
@name = name
file = Tempfile.new([basename, extension])
file.binmode
file
end
def extension
File.extname(@name)
end
def basename
File.basename(@name, extension).gsub(ILLEGAL_FILENAME_CHARACTERS, '_')
end
end