mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-30 10:58:31 -05:00
FIX: crop avatars on the server instead of the client
FIX: support for dots in S3 bucket names
This commit is contained in:
parent
bdde331e7a
commit
85d4d3223c
9 changed files with 51 additions and 169 deletions
|
@ -10,24 +10,12 @@ export default Em.Component.extend(UploadMixin, {
|
|||
}.property("uploading"),
|
||||
|
||||
uploadDone(upload) {
|
||||
// in order to be as much responsive as possible, we're cheating a bit here
|
||||
// indeed, the server gives us back the url to the file we've just uploaded
|
||||
// often, this file is not a square, so we need to crop it properly
|
||||
// this will also capture the first frame of animated avatars when they're not allowed
|
||||
Discourse.Utilities.cropAvatar(upload.url).then(avatarTemplate => {
|
||||
// display a warning whenever the image is not a square
|
||||
this.set("imageIsNotASquare", upload.width !== upload.height);
|
||||
|
||||
// set the avatar template to update the image on the client
|
||||
this.set("uploadedAvatarTemplate", avatarTemplate);
|
||||
|
||||
// indicates the users is using an uploaded avatar (must happen after cropping, otherwise
|
||||
// we will attempt to load an invalid avatar and cache a redirect to old one, uploadedAvatarTemplate
|
||||
// trumps over custom_avatar_upload_id)
|
||||
this.set("custom_avatar_upload_id", upload.id);
|
||||
|
||||
// the upload is now done
|
||||
this.sendAction("done");
|
||||
this.setProperties({
|
||||
imageIsNotASquare: upload.width !== upload.height,
|
||||
uploadedAvatarTemplate: upload.url,
|
||||
custom_avatar_upload_id: upload.id,
|
||||
});
|
||||
|
||||
this.sendAction("done");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,10 +1,3 @@
|
|||
/**
|
||||
General utility functions
|
||||
|
||||
@class Utilities
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
Discourse.Utilities = {
|
||||
|
||||
translateSize: function(size) {
|
||||
|
@ -195,22 +188,10 @@ Discourse.Utilities = {
|
|||
return true;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
Determine whether all file extensions are authorized.
|
||||
|
||||
@method authorizesAllExtensions
|
||||
**/
|
||||
authorizesAllExtensions: function() {
|
||||
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0;
|
||||
},
|
||||
|
||||
/**
|
||||
Check the extension of the file against the list of authorized extensions
|
||||
|
||||
@method isAuthorizedUpload
|
||||
@param {File} file The file we want to upload
|
||||
**/
|
||||
isAuthorizedUpload: function(file) {
|
||||
if (file && file.name) {
|
||||
var extensions = _.chain(Discourse.SiteSettings.authorized_extensions.split("|"))
|
||||
|
@ -222,11 +203,6 @@ Discourse.Utilities = {
|
|||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
List the authorized extension for display
|
||||
|
||||
@method authorizedExtensions
|
||||
**/
|
||||
authorizedExtensions: function() {
|
||||
return _.chain(Discourse.SiteSettings.authorized_extensions.split("|"))
|
||||
.reject(function(extension) { return extension.indexOf("*") >= 0; })
|
||||
|
@ -235,12 +211,6 @@ Discourse.Utilities = {
|
|||
.join(", ");
|
||||
},
|
||||
|
||||
/**
|
||||
Get the markdown template for an upload (either an image or an attachment)
|
||||
|
||||
@method getUploadMarkdown
|
||||
@param {Upload} upload The upload we want the markdown from
|
||||
**/
|
||||
getUploadMarkdown: function(upload) {
|
||||
if (Discourse.Utilities.isAnImage(upload.original_filename)) {
|
||||
return '<img src="' + upload.url + '" width="' + upload.width + '" height="' + upload.height + '">';
|
||||
|
@ -249,12 +219,6 @@ Discourse.Utilities = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Check whether the path is refering to an image
|
||||
|
||||
@method isAnImage
|
||||
@param {String} path The path
|
||||
**/
|
||||
isAnImage: function(path) {
|
||||
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path);
|
||||
},
|
||||
|
@ -264,11 +228,6 @@ Discourse.Utilities = {
|
|||
(/(png|jpe?g|gif|bmp|tiff?|svg|webp)/i).test(Discourse.Utilities.authorizedExtensions());
|
||||
},
|
||||
|
||||
/**
|
||||
Determines whether we allow attachments or not
|
||||
|
||||
@method allowsAttachments
|
||||
**/
|
||||
allowsAttachments: function() {
|
||||
return Discourse.Utilities.authorizesAllExtensions() ||
|
||||
!(/((png|jpe?g|gif|bmp|tiff?|svg|webp)(,\s)?)+$/i).test(Discourse.Utilities.authorizedExtensions());
|
||||
|
@ -304,55 +263,6 @@ Discourse.Utilities = {
|
|||
bootbox.alert(I18n.t('post.errors.upload'));
|
||||
},
|
||||
|
||||
/**
|
||||
Crop an image to be used as avatar.
|
||||
Simulate the "centered square thumbnail" generation done server-side.
|
||||
Uses only the first frame of animated gifs when they are disabled.
|
||||
|
||||
@method cropAvatar
|
||||
@param {String} url The url of the avatar
|
||||
@returns {Promise} a promise that will eventually be the cropped avatar.
|
||||
**/
|
||||
cropAvatar: function(url) {
|
||||
var extension = /\.(\w{2,})$/.exec(url)[1];
|
||||
if (Discourse.SiteSettings.allow_animated_avatars && extension === "gif") {
|
||||
// can't crop animated gifs... let the browser stretch the gif
|
||||
return Ember.RSVP.resolve(url);
|
||||
} else {
|
||||
return new Ember.RSVP.Promise(function(resolve) {
|
||||
var image = document.createElement("img");
|
||||
image.crossOrigin = 'Anonymous';
|
||||
// this event will be fired as soon as the image is loaded
|
||||
image.onload = function(e) {
|
||||
var img = e.target;
|
||||
// computes the dimension & position (x, y) of the largest square we can fit in the image
|
||||
var width = img.width, height = img.height, dimension, center, x, y;
|
||||
if (width <= height) {
|
||||
dimension = width;
|
||||
center = height / 2;
|
||||
x = 0;
|
||||
y = center - (dimension / 2);
|
||||
} else {
|
||||
dimension = height;
|
||||
center = width / 2;
|
||||
x = center - (dimension / 2);
|
||||
y = 0;
|
||||
}
|
||||
// set the size of the canvas to the maximum available size for avatars (browser will take care of downsizing the image)
|
||||
var canvas = document.createElement("canvas");
|
||||
var size = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize("huge"));
|
||||
canvas.height = canvas.width = size;
|
||||
// draw the image into the canvas
|
||||
canvas.getContext("2d").drawImage(img, x, y, dimension, dimension, 0, 0, size, size);
|
||||
// retrieve the image from the canvas
|
||||
resolve(canvas.toDataURL("image/" + extension));
|
||||
};
|
||||
// launch the onload event
|
||||
image.src = url;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
defaultHomepage: function() {
|
||||
// the homepage is the first item of the 'top_menu' site setting
|
||||
return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];
|
||||
|
|
|
@ -6,10 +6,6 @@ export default Em.Mixin.create({
|
|||
Em.warn("You should implement `uploadDone`");
|
||||
},
|
||||
|
||||
deleteDone() {
|
||||
Em.warn("You should implement `deleteDone`");
|
||||
},
|
||||
|
||||
_initialize: function() {
|
||||
const $upload = this.$(),
|
||||
csrf = Discourse.Session.currentProp("csrfToken"),
|
||||
|
|
|
@ -8,6 +8,7 @@ class UploadsController < ApplicationController
|
|||
url = params[:url]
|
||||
|
||||
Scheduler::Defer.later("Create Upload") do
|
||||
begin
|
||||
# API can provide a URL
|
||||
if file.nil? && url.present? && is_api?
|
||||
tempfile = FileHelper.download(url, SiteSetting.max_image_size_kb.kilobytes, "discourse-upload-#{type}") rescue nil
|
||||
|
@ -18,6 +19,12 @@ class UploadsController < ApplicationController
|
|||
content_type = file.content_type
|
||||
end
|
||||
|
||||
# when we're dealing with an avatar, crop it to its maximum size
|
||||
if type == "avatar" && FileHelper.is_image?(filename)
|
||||
max = Discourse.avatar_sizes.max
|
||||
OptimizedImage.resize(tempfile.path, tempfile.path, max, max, allow_animation: SiteSetting.allow_animated_avatars)
|
||||
end
|
||||
|
||||
upload = Upload.create_for(current_user.id, tempfile, filename, tempfile.size, content_type: content_type)
|
||||
|
||||
if upload.errors.empty? && current_user.admin?
|
||||
|
@ -32,9 +39,12 @@ class UploadsController < ApplicationController
|
|||
data = upload.errors.empty? ? upload : { errors: upload.errors.values.flatten }
|
||||
|
||||
MessageBus.publish("/uploads/#{type}", data.as_json, user_ids: [current_user.id])
|
||||
|
||||
rescue => e
|
||||
pp e
|
||||
ensure
|
||||
tempfile.try(:close!) rescue nil
|
||||
end
|
||||
end
|
||||
|
||||
# HACK FOR IE9 to prevent the "download dialog"
|
||||
response.headers["Content-Type"] = "text/plain" if request.user_agent =~ /MSIE 9/
|
||||
|
|
|
@ -49,19 +49,14 @@ class UserAvatarsController < ApplicationController
|
|||
protected
|
||||
|
||||
def show_in_site(hostname)
|
||||
size = params[:size].to_i
|
||||
|
||||
|
||||
username = params[:username].to_s
|
||||
return render_dot unless user = User.find_by(username_lower: username.downcase)
|
||||
|
||||
version = params[:version].to_i
|
||||
return render_dot unless version > 0 && user_avatar = user.user_avatar
|
||||
|
||||
# some sanity checks
|
||||
if size < 8 || size > 500
|
||||
return render_dot
|
||||
end
|
||||
size = params[:size].to_i
|
||||
return render_dot if size < 8 || size > 500
|
||||
|
||||
if !Discourse.avatar_sizes.include?(size) && Discourse.store.external?
|
||||
closest = Discourse.avatar_sizes.to_a.min{|a,b| (size-a).abs <=> (size-b).abs}
|
||||
|
|
|
@ -531,7 +531,7 @@ files:
|
|||
enum: 'S3RegionSiteSetting'
|
||||
s3_upload_bucket:
|
||||
default: ''
|
||||
regex: '^[^A-Z._]+$'
|
||||
regex: '^[^A-Z_]+$'
|
||||
s3_cdn_url:
|
||||
default: ''
|
||||
regex: '^https?:\/\/.+[^\/]$'
|
||||
|
@ -770,7 +770,7 @@ backups:
|
|||
s3_backup_bucket:
|
||||
client: false
|
||||
default: ''
|
||||
regex: "^[^\\.]*$"
|
||||
regex: "^[^A-Z_]+$"
|
||||
|
||||
uncategorized:
|
||||
version_checks:
|
||||
|
|
|
@ -82,9 +82,7 @@ module Discourse
|
|||
# Don't cache until we can get a notification from site settings to expire cache
|
||||
set = Set.new(SiteSetting.avatar_sizes.split("|").map(&:to_i))
|
||||
# add retinas which are 2x dpi
|
||||
set.to_a.each do |size|
|
||||
set << size*2
|
||||
end
|
||||
set.to_a.each { |size| set << size * 2 }
|
||||
set
|
||||
end
|
||||
|
||||
|
|
|
@ -49,14 +49,12 @@ class S3Helper
|
|||
private
|
||||
|
||||
def s3_resource
|
||||
opts = {}
|
||||
opts = { region: SiteSetting.s3_region }
|
||||
|
||||
opts = {
|
||||
access_key_id: SiteSetting.s3_access_key_id,
|
||||
secret_access_key: SiteSetting.s3_secret_access_key
|
||||
} unless SiteSetting.s3_use_iam_profile
|
||||
|
||||
opts[:region] = SiteSetting.s3_region unless SiteSetting.s3_region.blank?
|
||||
unless SiteSetting.s3_use_iam_profile
|
||||
opts[:access_key_id] = SiteSetting.s3_access_key_id
|
||||
opts[:secret_access_key] = SiteSetting.s3_secret_access_key
|
||||
end
|
||||
|
||||
Aws::S3::Resource.new(opts)
|
||||
end
|
||||
|
|
|
@ -169,16 +169,3 @@ test("defaultHomepage", function() {
|
|||
Discourse.SiteSettings.top_menu = "latest|top|hot";
|
||||
equal(utils.defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting");
|
||||
});
|
||||
|
||||
module("Discourse.Utilities.cropAvatar with animated avatars", {
|
||||
setup: function() { Discourse.SiteSettings.allow_animated_avatars = true; }
|
||||
});
|
||||
|
||||
asyncTestDiscourse("cropAvatar", function() {
|
||||
expect(1);
|
||||
|
||||
utils.cropAvatar("/path/to/avatar.gif").then(function(avatarTemplate) {
|
||||
equal(avatarTemplate, "/path/to/avatar.gif", "returns the url to the gif when animated gif are enabled");
|
||||
start();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue