mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
FEATURE: support email attachments
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user