Add size limit for uploading user avatars and realm icons.

- Add settings parameter for max realm icon size.
- Add settings parameter for max user avatar size.
- Add checking file size to avatar and icon
  uploading views.
- Transfer file size limit parameter to frontend.
- Add tests.
This commit is contained in:
K.Kanakhin
2017-03-06 11:22:28 +06:00
committed by Tim Abbott
parent 01129c1ab9
commit 1cb0f8dc41
10 changed files with 53 additions and 9 deletions

View File

@@ -72,7 +72,8 @@ exports.build_user_avatar_widget = function (upload_function) {
get_file_input, get_file_input,
$("#user_avatar_file_input_error").expectOne(), $("#user_avatar_file_input_error").expectOne(),
$("#user_avatar_upload_button").expectOne(), $("#user_avatar_upload_button").expectOne(),
upload_function upload_function,
page_params.max_avatar_file_size
); );
}; };

View File

@@ -24,7 +24,8 @@ var realm_icon = (function () {
get_file_input, get_file_input,
$("#realm_icon_file_input_error").expectOne(), $("#realm_icon_file_input_error").expectOne(),
$("#realm_icon_upload_button").expectOne(), $("#realm_icon_upload_button").expectOne(),
upload_function upload_function,
page_params.max_icon_file_size
); );
}; };

View File

@@ -2,6 +2,8 @@ var upload_widget = (function () {
var exports = {}; var exports = {};
var default_max_file_size = 5;
function is_image_format(file) { function is_image_format(file) {
var type = file.type; var type = file.type;
if (!type) { if (!type) {
@@ -22,8 +24,11 @@ var upload_widget = (function () {
file_name_field, // jQuery object to show file name file_name_field, // jQuery object to show file name
input_error, // jQuery object for error text input_error, // jQuery object for error text
clear_button, // jQuery button to clear last upload choice clear_button, // jQuery button to clear last upload choice
upload_button // jQuery button to open file dialog upload_button, // jQuery button to open file dialog
max_file_upload_size
) { ) {
// default value of max upladed file size
max_file_upload_size = max_file_upload_size || default_max_file_size;
function accept(file) { function accept(file) {
file_name_field.text(file.name); file_name_field.text(file.name);
@@ -61,8 +66,10 @@ var upload_widget = (function () {
input_error.hide(); input_error.hide();
} else if (e.target.files.length === 1) { } else if (e.target.files.length === 1) {
var file = e.target.files[0]; var file = e.target.files[0];
if (file.size > 5 * 1024 * 1024) { if (file.size > max_file_upload_size * 1024 * 1024) {
input_error.text(i18n.t('File size must be < 5Mb.')); input_error.text(i18n.t('File size must be < __max_file_size__Mb.', {
max_file_size: max_file_upload_size,
}));
input_error.show(); input_error.show();
clear(); clear();
} else if (!is_image_format(file)) { } else if (!is_image_format(file)) {
@@ -103,7 +110,11 @@ var upload_widget = (function () {
get_file_input, // function returns a jQuery file input object get_file_input, // function returns a jQuery file input object
input_error, // jQuery object for error text input_error, // jQuery object for error text
upload_button, // jQuery button to open file dialog upload_button, // jQuery button to open file dialog
upload_function) { upload_function,
max_file_upload_size
) {
// default value of max upladed file size
max_file_upload_size = max_file_upload_size || default_max_file_size;
function accept() { function accept() {
input_error.hide(); input_error.hide();
@@ -131,8 +142,10 @@ var upload_widget = (function () {
input_error.hide(); input_error.hide();
} else if (e.target.files.length === 1) { } else if (e.target.files.length === 1) {
var file = e.target.files[0]; var file = e.target.files[0];
if (file.size > 5 * 1024 * 1024) { if (file.size > max_file_upload_size * 1024 * 1024) {
input_error.text(i18n.t('File size must be < 5Mb.')); input_error.text(i18n.t('File size must be < __max_file_size__Mb.', {
max_file_size: max_file_upload_size,
}));
input_error.show(); input_error.show();
clear(); clear();
} else if (!is_image_format(file)) { } else if (!is_image_format(file)) {

View File

@@ -106,6 +106,7 @@ def fetch_initial_state_data(user_profile, event_types, queue_id,
state['realm_icon_url'] = realm_icon_url(user_profile.realm) state['realm_icon_url'] = realm_icon_url(user_profile.realm)
state['realm_icon_source'] = user_profile.realm.icon_source state['realm_icon_source'] = user_profile.realm.icon_source
state['realm_email_changes_disabled'] = user_profile.realm.email_changes_disabled state['realm_email_changes_disabled'] = user_profile.realm.email_changes_disabled
state['max_icon_file_size'] = settings.MAX_ICON_FILE_SIZE
if want('realm_domain'): if want('realm_domain'):
state['realm_domain'] = user_profile.realm.domain state['realm_domain'] = user_profile.realm.domain

View File

@@ -521,6 +521,14 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
self.assertEqual(user_profile.avatar_source, UserProfile.AVATAR_FROM_GRAVATAR) self.assertEqual(user_profile.avatar_source, UserProfile.AVATAR_FROM_GRAVATAR)
self.assertEqual(user_profile.avatar_version, 2) self.assertEqual(user_profile.avatar_version, 2)
def test_avatar_upload_file_size_error(self):
# type: () -> None
self.login("hamlet@zulip.com")
with get_test_image_file(self.correct_files[0][0]) as fp:
with self.settings(MAX_AVATAR_FILE_SIZE=0):
result = self.client_put_multipart("/json/users/me/avatar", {'file': fp})
self.assert_json_error(result, "Uploaded file is larger than the allowed limit of 0 MB")
def tearDown(self): def tearDown(self):
# type: () -> None # type: () -> None
destroy_uploads() destroy_uploads()
@@ -650,7 +658,6 @@ class RealmIconTest(UploadSerializeMixin, ZulipTestCase):
def test_realm_icon_version(self): def test_realm_icon_version(self):
# type: () -> None # type: () -> None
self.login("iago@zulip.com") self.login("iago@zulip.com")
realm = get_realm('zulip') realm = get_realm('zulip')
icon_version = realm.icon_version icon_version = realm.icon_version
@@ -660,6 +667,14 @@ class RealmIconTest(UploadSerializeMixin, ZulipTestCase):
realm = get_realm('zulip') realm = get_realm('zulip')
self.assertEqual(realm.icon_version, icon_version + 1) self.assertEqual(realm.icon_version, icon_version + 1)
def test_realm_icon_upload_file_size_error(self):
# type: () -> None
self.login("iago@zulip.com")
with get_test_image_file(self.correct_files[0][0]) as fp:
with self.settings(MAX_ICON_FILE_SIZE=0):
result = self.client_put_multipart("/json/realm/icon", {'file': fp})
self.assert_json_error(result, "Uploaded file is larger than the allowed limit of 0 MB")
def tearDown(self): def tearDown(self):
# type: () -> None # type: () -> None
destroy_uploads() destroy_uploads()

View File

@@ -1972,6 +1972,8 @@ class HomeTest(ZulipTestCase):
"left_side_userlist", "left_side_userlist",
"login_page", "login_page",
"mandatory_topics", "mandatory_topics",
"max_avatar_file_size",
"max_icon_file_size",
"max_message_id", "max_message_id",
"maxfilesize", "maxfilesize",
"muted_topics", "muted_topics",

View File

@@ -204,6 +204,7 @@ def home_real(request):
login_page = settings.HOME_NOT_LOGGED_IN, login_page = settings.HOME_NOT_LOGGED_IN,
server_uri = settings.SERVER_URI, server_uri = settings.SERVER_URI,
maxfilesize = settings.MAX_FILE_UPLOAD_SIZE, maxfilesize = settings.MAX_FILE_UPLOAD_SIZE,
max_avatar_file_size = settings.MAX_AVATAR_FILE_SIZE,
server_generation = settings.SERVER_GENERATION, server_generation = settings.SERVER_GENERATION,
use_websockets = settings.USE_WEBSOCKETS, use_websockets = settings.USE_WEBSOCKETS,
save_stacktraces = settings.SAVE_FRONTEND_STACKTRACES, save_stacktraces = settings.SAVE_FRONTEND_STACKTRACES,
@@ -284,6 +285,7 @@ def home_real(request):
'emoji_alt_code', 'emoji_alt_code',
'last_event_id', 'last_event_id',
'left_side_userlist', 'left_side_userlist',
'max_icon_file_size',
'max_message_id', 'max_message_id',
'muted_topics', 'muted_topics',
'realm_add_emoji_by_admins_only', 'realm_add_emoji_by_admins_only',

View File

@@ -1,3 +1,4 @@
from django.conf import settings
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.http import HttpResponse, HttpRequest from django.http import HttpResponse, HttpRequest
@@ -18,6 +19,9 @@ def upload_icon(request, user_profile):
return json_error(_("You must upload exactly one icon.")) return json_error(_("You must upload exactly one icon."))
icon_file = list(request.FILES.values())[0] icon_file = list(request.FILES.values())[0]
if ((settings.MAX_ICON_FILE_SIZE * 1024 * 1024) < icon_file.size):
return json_error(_("Uploaded file is larger than the allowed limit of %s MB") % (
settings.MAX_ICON_FILE_SIZE))
upload_icon_image(icon_file, user_profile) upload_icon_image(icon_file, user_profile)
do_change_icon_source(user_profile.realm, user_profile.realm.ICON_UPLOADED) do_change_icon_source(user_profile.realm, user_profile.realm.ICON_UPLOADED)
icon_url = realm_icon_url(user_profile.realm) icon_url = realm_icon_url(user_profile.realm)

View File

@@ -268,6 +268,9 @@ def set_avatar_backend(request, user_profile):
return json_error(_("You must upload exactly one avatar.")) return json_error(_("You must upload exactly one avatar."))
user_file = list(request.FILES.values())[0] user_file = list(request.FILES.values())[0]
if ((settings.MAX_AVATAR_FILE_SIZE * 1024 * 1024) < user_file.size):
return json_error(_("Uploaded file is larger than the allowed limit of %s MB") % (
settings.MAX_AVATAR_FILE_SIZE))
upload_avatar_image(user_file, user_profile, user_profile) upload_avatar_image(user_file, user_profile, user_profile)
do_change_avatar_fields(user_profile, UserProfile.AVATAR_FROM_USER) do_change_avatar_fields(user_profile, UserProfile.AVATAR_FROM_USER)
user_avatar_url = avatar_url(user_profile) user_avatar_url = avatar_url(user_profile)

View File

@@ -116,6 +116,8 @@ DEFAULT_SETTINGS = {'TWITTER_CONSUMER_KEY': '',
'S3_AVATAR_BUCKET': '', 'S3_AVATAR_BUCKET': '',
'LOCAL_UPLOADS_DIR': None, 'LOCAL_UPLOADS_DIR': None,
'MAX_FILE_UPLOAD_SIZE': 25, 'MAX_FILE_UPLOAD_SIZE': 25,
'MAX_AVATAR_FILE_SIZE': 5,
'MAX_ICON_FILE_SIZE': 5,
'ERROR_REPORTING': True, 'ERROR_REPORTING': True,
'BROWSER_ERROR_REPORTING': False, 'BROWSER_ERROR_REPORTING': False,
'STAGING_ERROR_NOTIFICATIONS': False, 'STAGING_ERROR_NOTIFICATIONS': False,