diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js
index 71dbe19522c..200b3bbb923 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js
+++ b/app/assets/javascripts/discourse/lib/utilities.js
@@ -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;
}
}
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index e501d4a0e90..02cb0c6cb0d 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -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
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 104938562fd..24a3d9443ae 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -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(
diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb
index 47f22ea5b91..078dcfa482f 100644
--- a/app/jobs/regular/pull_hotlinked_images.rb
+++ b/app/jobs/regular/pull_hotlinked_images.rb
@@ -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, "")
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
diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb
index b17414de20d..eacd0958821 100644
--- a/app/jobs/scheduled/poll_mailbox.rb
+++ b/app/jobs/scheduled/poll_mailbox.rb
@@ -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)
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index a7945e170aa..2ed664a5e33 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -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
diff --git a/app/models/upload.rb b/app/models/upload.rb
index d4733e63a05..a2c5917d8da 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 834ef33391c..bf74bb2639d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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)
diff --git a/app/services/uri_adapter.rb b/app/services/uri_adapter.rb
deleted file mode 100644
index 09cfc1a1823..00000000000
--- a/app/services/uri_adapter.rb
+++ /dev/null
@@ -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
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 9856f1e4644..ac7c7c886c6 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1444,6 +1444,7 @@ en:
edit_reason: "We have downloaded copies of the remote images"
unauthorized: "Sorry, the file you are trying to upload is not authorized (authorized extensions: %{authorized_extensions})."
pasted_image_filename: "Pasted image"
+ store_failure: "Failed to store upload #%{upload_id} for user #%{user_id}."
attachments:
too_large: "Sorry, the file you are trying to upload is too big (maximum size is %{max_size_kb}%kb)."
images:
diff --git a/config/routes.rb b/config/routes.rb
index 9a1df2aa7db..89fd083787b 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -187,7 +187,7 @@ Discourse::Application.routes.draw do
get "users/:username/preferences/username" => "users#preferences", constraints: {username: USERNAME_ROUTE_FORMAT}
put "users/:username/preferences/username" => "users#username", constraints: {username: USERNAME_ROUTE_FORMAT}
get "users/:username/avatar(/:size)" => "users#avatar", constraints: {username: USERNAME_ROUTE_FORMAT} # LEGACY ROUTE
- post "users/:username/preferences/avatar" => "users#upload_avatar", constraints: {username: USERNAME_ROUTE_FORMAT}
+ post "users/:username/preferences/avatar" => "users#upload_avatar", constraints: {username: USERNAME_ROUTE_FORMAT} # LEGACY ROUTE
post "users/:username/preferences/user_image" => "users#upload_user_image", constraints: {username: USERNAME_ROUTE_FORMAT}
put "users/:username/preferences/avatar/toggle" => "users#toggle_avatar", constraints: {username: USERNAME_ROUTE_FORMAT}
put "users/:username/preferences/profile_background/clear" => "users#clear_profile_background", constraints: {username: USERNAME_ROUTE_FORMAT}
diff --git a/lib/avatar_upload_service.rb b/lib/avatar_upload_service.rb
index 4a19d49da3c..860b7a7d55f 100644
--- a/lib/avatar_upload_service.rb
+++ b/lib/avatar_upload_service.rb
@@ -1,43 +1,23 @@
+require_dependency "file_helper"
+
class AvatarUploadService
attr_accessor :source
- attr_reader :filesize, :file
+ attr_reader :filesize, :filename, :file
def initialize(file, source)
@source = source
- @file , @filesize = construct(file)
+ @file, @filename, @filesize = construct(file)
end
def construct(file)
case source
when :url
- build_from_url(file)
+ tmp = FileHelper.download(file, SiteSetting.max_image_size_kb.kilobytes, "discourse-avatar")
+ [tmp, File.basename(URI.parse(file).path), File.size(tmp)]
when :image
- [file, File.size(file.tempfile)]
+ [file.tempfile, file.original_filename, File.size(file.tempfile)]
end
end
- private
-
- def build_from_url(url)
- temp = ::UriAdapter.new(url)
- return temp.build_uploaded_file, temp.file_size
- end
-
-end
-
-class AvatarUploadPolicy
-
- def initialize(avatar)
- @avatar = avatar
- end
-
- def max_size_kb
- SiteSetting.max_image_size_kb.kilobytes
- end
-
- def too_big?
- @avatar.filesize > max_size_kb
- end
-
end
diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb
index 30eecb07667..ca28308fddf 100644
--- a/lib/email/receiver.rb
+++ b/lib/email/receiver.rb
@@ -3,8 +3,11 @@
#
module Email
+
class Receiver
+ include ActionView::Helpers::NumberHelper
+
class ProcessingError < StandardError; end
class EmailUnparsableError < ProcessingError; end
class EmptyEmailError < ProcessingError; end
@@ -18,28 +21,11 @@ module Email
@raw = raw
end
- def is_in_email?
- @allow_strangers = false
- if SiteSetting.email_in and SiteSetting.email_in_address == @message.to.first
- @category_id = SiteSetting.email_in_category.to_i
- return true
- end
-
- category = Category.find_by_email(@message.to.first)
- return false if not category
-
- @category_id = category.id
- @allow_strangers = category.email_in_allow_strangers
- return true
-
- end
-
def process
raise EmptyEmailError if @raw.blank?
@message = Mail.new(@raw)
-
# First remove the known discourse stuff.
parse_body
raise EmptyEmailError if @body.blank?
@@ -48,18 +34,17 @@ module Email
@body = EmailReplyParser.read(@body).visible_text.force_encoding('UTF-8')
discourse_email_parser
-
raise EmailUnparsableError if @body.blank?
if is_in_email?
@user = User.find_by_email(@message.from.first)
- if @user.blank? and @allow_strangers
+ if @user.blank? && @allow_strangers
wrap_body_in_quote
@user = Discourse.system_user
end
raise UserNotFoundError if @user.blank?
- raise UserNotSufficientTrustLevelError.new @user if not @user.has_trust_level?(TrustLevel.levels[SiteSetting.email_in_min_trust.to_i])
+ raise UserNotSufficientTrustLevelError.new @user unless @user.has_trust_level?(TrustLevel.levels[SiteSetting.email_in_min_trust.to_i])
create_new_topic
else
@@ -81,12 +66,6 @@ module Email
private
- def wrap_body_in_quote
- @body = "[quote=\"#{@message.from.first}\"]
-#{@body}
-[/quote]"
- end
-
def parse_body
html = nil
@@ -102,7 +81,7 @@ module Email
if @message.content_type =~ /text\/html/
if defined? @message.charset
- html = @message.body.decoded.force_encoding(@message.charset).encode("UTF-8").to_s
+ html = @message.body.decoded.force_encoding(@message.charset).encode("UTF-8").to_s
else
html = @message.body.to_s
end
@@ -127,12 +106,11 @@ module Email
# If we have an HTML message, strip the markup
doc = Nokogiri::HTML(html)
- # Blackberry is annoying in that it only provides HTML. We can easily
- # extract it though
+ # Blackberry is annoying in that it only provides HTML. We can easily extract it though
content = doc.at("#BB10_response_div")
return content.text if content.present?
- return doc.xpath("//text()").text
+ doc.xpath("//text()").text
end
def discourse_email_parser
@@ -154,35 +132,91 @@ module Email
@body.strip!
end
- def create_reply
- # Try to post the body as a reply
- creator = PostCreator.new(email_log.user,
- raw: @body,
- topic_id: @email_log.topic_id,
- reply_to_post_number: @email_log.post.post_number,
- cooking_options: {traditional_markdown_linebreaks: true})
+ def is_in_email?
+ @allow_strangers = false
- creator.create
+ if SiteSetting.email_in && SiteSetting.email_in_address == @message.to.first
+ @category_id = SiteSetting.email_in_category.to_i
+ return true
+ end
+
+ category = Category.find_by_email(@message.to.first)
+ return false unless category
+
+ @category_id = category.id
+ @allow_strangers = category.email_in_allow_strangers
+
+ true
+ end
+
+ def wrap_body_in_quote
+ @body = "[quote=\"#{@message.from.first}\"]
+#{@body}
+[/quote]"
+ end
+
+ def create_reply
+ create_post_with_attachments(email_log.user, @body, @email_log.topic_id, @email_log.post.post_number)
end
def create_new_topic
- # Try to post the body as a reply
- topic_creator = TopicCreator.new(@user,
- Guardian.new(@user),
- category: @category_id,
- title: @message.subject)
+ topic = TopicCreator.new(
+ @user,
+ Guardian.new(@user),
+ category: @category_id,
+ title: @message.subject,
+ ).create
- topic = topic_creator.create
- post_creator = PostCreator.new(@user,
- raw: @body,
- topic_id: topic.id,
- cooking_options: {traditional_markdown_linebreaks: true})
+ post = create_post_with_attachments(@user, @body, topic.id)
- post_creator.create
- EmailLog.create(email_type: "topic_via_incoming_email",
- to_address: @message.to.first,
- topic_id: topic.id, user_id: @user.id)
- topic
+ EmailLog.create(
+ email_type: "topic_via_incoming_email",
+ to_address: @message.to.first,
+ topic_id: topic.id,
+ user_id: @user.id,
+ )
+
+ post
+ end
+
+ def create_post_with_attachments(user, raw, topic_id, reply_to_post_number=nil)
+ options = {
+ raw: raw,
+ topic_id: topic_id,
+ cooking_options: { traditional_markdown_linebreaks: true },
+ }
+ options[:reply_to_post_number] = reply_to_post_number if reply_to_post_number
+
+ # deal with attachments
+ @message.attachments.each do |attachment|
+ tmp = Tempfile.new("discourse-email-attachment")
+ begin
+ # read attachment
+ File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded }
+ # create the upload for the user
+ upload = Upload.create_for(user.id, tmp, attachment.filename, File.size(tmp))
+ if upload && upload.errors.empty?
+ # TODO: should use the same code as the client to insert attachments
+ raw << "\n#{attachment_markdown(upload)}\n"
+ end
+ ensure
+ tmp.close!
+ end
+
+ end
+
+ create_post(user, options)
+ end
+
+ def attachment_markdown(upload)if FileHelper.is_image?(upload.original_filename)
+ "
"
+ else
+ "#{upload.original_filename} (#{number_to_human_size(upload.filesize)})"
+ end
+ end
+
+ def create_post(user, options)
+ PostCreator.new(user, options).create
end
end
diff --git a/lib/file_helper.rb b/lib/file_helper.rb
new file mode 100644
index 00000000000..77fc67500b7
--- /dev/null
+++ b/lib/file_helper.rb
@@ -0,0 +1,34 @@
+class FileHelper
+
+ def self.is_image?(filename)
+ filename =~ images_regexp
+ end
+
+ def self.download(url, max_file_size, tmp_file_name)
+ raise Discourse::InvalidParameters unless url =~ /^https?:\/\//
+
+ extension = File.extname(URI.parse(url).path)
+ tmp = Tempfile.new([tmp_file_name, extension])
+
+ File.open(tmp.path, "wb") do |f|
+ avatar = open(url, "rb", read_timeout: 5)
+ while f.size <= max_file_size && data = avatar.read(max_file_size)
+ f.write(data)
+ end
+ avatar.close!
+ end
+
+ tmp
+ end
+
+ private
+
+ def self.images
+ @@images ||= Set.new ["jpg", "jpeg", "png", "gif", "tif", "tiff", "bmp"]
+ end
+
+ def self.images_regexp
+ @@images_regexp ||= /\.(#{images.to_a.join("|").gsub(".", "\.")})$/i
+ end
+
+end
diff --git a/lib/file_store/local_store.rb b/lib/file_store/local_store.rb
index da43b48b0a0..78fd8755c45 100644
--- a/lib/file_store/local_store.rb
+++ b/lib/file_store/local_store.rb
@@ -62,8 +62,8 @@ module FileStore
private
def get_path_for_upload(file, upload)
- unique_sha1 = Digest::SHA1.hexdigest("#{Time.now.to_s}#{file.original_filename}")[0..15]
- extension = File.extname(file.original_filename)
+ unique_sha1 = Digest::SHA1.hexdigest("#{Time.now.to_s}#{upload.original_filename}")[0..15]
+ extension = File.extname(upload.original_filename)
clean_name = "#{unique_sha1}#{extension}"
# path
"#{relative_base_url}/#{upload.id}/#{clean_name}"
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index 832f7def6c7..75686b9f196 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -10,8 +10,7 @@ module PrettyText
def t(key, opts)
str = I18n.t("js." + key)
if opts
- # TODO: server localisation has no parity with client
- # should be fixed
+ # TODO: server localisation has no parity with client should be fixed
str = str.dup
opts.each do |k,v|
str.gsub!("{{#{k}}}", v)
@@ -31,7 +30,7 @@ module PrettyText
def is_username_valid(username)
return false unless username
username = username.downcase
- return User.exec_sql('select 1 from users where username_lower = ?', username).values.length == 1
+ return User.exec_sql('SELECT 1 FROM users WHERE username_lower = ?', username).values.length == 1
end
end
@@ -53,12 +52,13 @@ module PrettyText
ctx["helpers"] = Helpers.new
ctx_load(ctx,
- "vendor/assets/javascripts/md5.js",
- "vendor/assets/javascripts/lodash.js",
- "vendor/assets/javascripts/Markdown.Converter.js",
- "lib/headless-ember.js",
- "vendor/assets/javascripts/rsvp.js",
- Rails.configuration.ember.handlebars_location)
+ "vendor/assets/javascripts/md5.js",
+ "vendor/assets/javascripts/lodash.js",
+ "vendor/assets/javascripts/Markdown.Converter.js",
+ "lib/headless-ember.js",
+ "vendor/assets/javascripts/rsvp.js",
+ Rails.configuration.ember.handlebars_location
+ )
ctx.eval("var Discourse = {}; Discourse.SiteSettings = {};")
ctx.eval("var window = {}; window.devicePixelRatio = 2;") # hack to make code think stuff is retina
@@ -67,12 +67,13 @@ module PrettyText
decorate_context(ctx)
ctx_load(ctx,
- "vendor/assets/javascripts/better_markdown.js",
- "app/assets/javascripts/defer/html-sanitizer-bundle.js",
- "app/assets/javascripts/discourse/dialects/dialect.js",
- "app/assets/javascripts/discourse/lib/utilities.js",
- "app/assets/javascripts/discourse/lib/html.js",
- "app/assets/javascripts/discourse/lib/markdown.js")
+ "vendor/assets/javascripts/better_markdown.js",
+ "app/assets/javascripts/defer/html-sanitizer-bundle.js",
+ "app/assets/javascripts/discourse/dialects/dialect.js",
+ "app/assets/javascripts/discourse/lib/utilities.js",
+ "app/assets/javascripts/discourse/lib/html.js",
+ "app/assets/javascripts/discourse/lib/markdown.js"
+ )
Dir["#{Rails.root}/app/assets/javascripts/discourse/dialects/**.js"].each do |dialect|
unless dialect =~ /\/dialect\.js$/
@@ -111,6 +112,7 @@ module PrettyText
return @ctx if @ctx
@ctx = create_new_context
end
+
@ctx
end
diff --git a/lib/validators/upload_validator.rb b/lib/validators/upload_validator.rb
new file mode 100644
index 00000000000..4ba0710d176
--- /dev/null
+++ b/lib/validators/upload_validator.rb
@@ -0,0 +1,80 @@
+require_dependency "file_helper"
+
+module Validators; end
+
+class Validators::UploadValidator < ActiveModel::Validator
+
+ def validate(upload)
+ extension = File.extname(upload.original_filename)[1..-1]
+
+ if is_authorized?(upload, extension)
+ if FileHelper.is_image?(upload.original_filename)
+ authorized_image_extension(upload, extension)
+ maximum_image_file_size(upload)
+ else
+ authorized_attachment_extension(upload, extension)
+ maximum_attachment_file_size(upload)
+ end
+ end
+ end
+
+ def is_authorized?(upload, extension)
+ authorized_extensions(upload, extension, authorized_uploads)
+ end
+
+ def authorized_image_extension(upload, extension)
+ authorized_extensions(upload, extension, authorized_images)
+ end
+
+ def maximum_image_file_size(upload)
+ maximum_file_size(upload, "image")
+ end
+
+ def authorized_attachment_extension(upload, extension)
+ authorized_extensions(upload, extension, authorized_attachments)
+ end
+
+ def maximum_attachment_file_size(upload)
+ maximum_file_size(upload, "attachment")
+ end
+
+ private
+
+ def authorized_uploads
+ authorized_uploads = Set.new
+
+ SiteSetting.authorized_extensions
+ .tr(" ", "")
+ .split("|")
+ .each do |extension|
+ authorized_uploads << (extension.start_with?(".") ? extension[1..-1] : extension)
+ end
+
+ authorized_uploads
+ end
+
+ def authorized_images
+ @authorized_images ||= (authorized_uploads & FileHelper.images)
+ end
+
+ def authorized_attachments
+ @authorized_attachments ||= (authorized_uploads - FileHelper.images)
+ end
+
+ def authorized_extensions(upload, extension, extensions)
+ unless authorized = extensions.include?(extension)
+ message = I18n.t("upload.unauthorized", authorized_extensions: extensions.to_a.join(", "))
+ upload.errors.add(:original_filename, message)
+ end
+ authorized
+ end
+
+ def maximum_file_size(upload, type)
+ max_size_kb = SiteSetting.send("max_#{type}_size_kb").kilobytes
+ if upload.filesize > max_size_kb
+ message = I18n.t("upload.#{type}s.too_large", max_size_kb: max_size_kb)
+ upload.errors.add(:filesize, message)
+ end
+ end
+
+end
diff --git a/spec/components/avatar_upload_service_spec.rb b/spec/components/avatar_upload_service_spec.rb
index 6248470bb9c..c515bbb3450 100644
--- a/spec/components/avatar_upload_service_spec.rb
+++ b/spec/components/avatar_upload_service_spec.rb
@@ -2,11 +2,11 @@ require "spec_helper"
require "avatar_upload_service"
describe AvatarUploadService do
+
+ let(:logo) { File.new("#{Rails.root}/spec/fixtures/images/logo.png") }
+
let(:file) do
- ActionDispatch::Http::UploadedFile.new({
- filename: 'logo.png',
- tempfile: File.new("#{Rails.root}/spec/fixtures/images/logo.png")
- })
+ ActionDispatch::Http::UploadedFile.new({ filename: 'logo.png', tempfile: logo })
end
let(:url) { "http://cdn.discourse.org/assets/logo.png" }
@@ -16,49 +16,41 @@ describe AvatarUploadService do
let(:avatar_file) { AvatarUploadService.new(file, :image) }
it "should have a filesize" do
- expect(avatar_file.filesize).to eq(2290)
+ avatar_file.filesize.should == 2290
+ end
+
+ it "should have a filename" do
+ avatar_file.filename.should == "logo.png"
+ end
+
+ it "should have a file" do
+ avatar_file.file.should == file.tempfile
end
it "should have a source as 'image'" do
- expect(avatar_file.source).to eq(:image)
- end
-
- it "is an instance of File class" do
- file = avatar_file.file
- expect(file.tempfile).to be_instance_of File
- end
-
- it "returns the file object built from File" do
- file = avatar_file.file
- file.should be_instance_of(ActionDispatch::Http::UploadedFile)
- file.original_filename.should == "logo.png"
+ avatar_file.source.should == :image
end
end
context "when file is in the form of a URL" do
let(:avatar_file) { AvatarUploadService.new(url, :url) }
- before :each do
- UriAdapter.any_instance.stubs(:open).returns StringIO.new(fixture_file("images/logo.png"))
- end
+ before { FileHelper.stubs(:download).returns(logo) }
it "should have a filesize" do
- expect(avatar_file.filesize).to eq(2290)
+ avatar_file.filesize.should == 2290
+ end
+
+ it "should have a filename" do
+ avatar_file.filename.should == "logo.png"
+ end
+
+ it "should have a file" do
+ avatar_file.file.should == logo
end
it "should have a source as 'url'" do
- expect(avatar_file.source).to eq(:url)
- end
-
- it "is an instance of Tempfile class" do
- file = avatar_file.file
- expect(file.tempfile).to be_instance_of Tempfile
- end
-
- it "returns the file object built from URL" do
- file = avatar_file.file
- file.should be_instance_of(ActionDispatch::Http::UploadedFile)
- file.original_filename.should == "logo.png"
+ avatar_file.source.should == :url
end
end
end
diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb
index ec674739b56..93c13a213fb 100644
--- a/spec/components/cooked_post_processor_spec.rb
+++ b/spec/components/cooked_post_processor_spec.rb
@@ -94,8 +94,8 @@ describe CookedPostProcessor do
it "generates overlay information" do
cpp.post_process_images
- cpp.html.should match_html '