Implement size checking for the avatar

This checks the avatar size on the client side (if available) and
on the server side against a configuration-defined limit. The
default limit is set to use the same value as in the original
report, as no sensible limit value is known.

Fixes #67.
This commit is contained in:
Jonas Schäfer
2021-03-20 12:53:58 +01:00
parent 02ed390cd2
commit 3eb8036ebd
5 changed files with 118 additions and 50 deletions

View File

@@ -155,6 +155,11 @@ class AppConfig:
"sv",
], converter=autosplit)
apple_store_url = environ.var("")
# Default limit of 1 MiB is what was discovered to be the effective limit
# in #67, hence we set that here for now.
# Future versions may change this default, and the standard deployment
# tools may also very well override it.
max_avatar_size = environ.var(1024*1024, converter=int)
_UPPER_CASE = "".join(map(chr, range(ord("A"), ord("Z")+1)))
@@ -185,6 +190,7 @@ def create_app() -> quart.Quart:
app.config["SITE_NAME"] = config.site_name or config.domain
app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl
app.config["APPLE_STORE_URL"] = config.apple_store_url
app.config["MAX_AVATAR_SIZE"] = config.max_avatar_size
app.context_processor(proc)
app.register_error_handler(

View File

@@ -9,7 +9,7 @@
</div>
<div id="mwrap">
{#- -#}
<div class="flashbox">
<div class="flashbox" id="flashbox">
{%- for category, message in get_flashed_messages(True) -%}
<div class="box {{ category }} el-5" role="alert">
{% if category == "success" %}

View File

@@ -1,10 +1,11 @@
{% extends "app.html" %}
{% from "library.j2" import standard_button, form_button, avatar with context %}
{% from "library.j2" import standard_button, form_button, render_errors, avatar with context %}
{% block content %}
<h1>{% trans %}Update your profile{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST" enctype="multipart/form-data">
<h2 class="form-title">{% trans %}Profile{% endtrans %}</h2>
{{ form.csrf_token }}
{% call render_errors(form) %}{% endcall %}
<div class="f-ebox">
{{ form.nickname.label }}
{{ form.nickname(placeholder=user_info.username) }}
@@ -13,7 +14,10 @@
{{ form.avatar.label }}
<div class="avatar-wrap">
{%- call avatar(user_info.address, user_info.avatar_hash ) %}{% endcall -%}
{{ form.avatar(accept="image/png") }}
{{ form.avatar(accept="image/png",
data_maxsize=max_avatar_size,
data_warning_header=avatar_too_big_warning_header,
data_maxsize_warning=avatar_too_big_warning) }}
</div>
</div>
<h3 class="form-title">{% trans %}Visibility{% endtrans %}</h3>
@@ -28,5 +32,38 @@
{%- call standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
</div>
<script type="text/javascript">
document.getElementById("{{ form.avatar.id }}").onchange = function() {
var maxsize_s = this.dataset.maxsize;
var maxsize = parseInt(maxsize_s);
var existing_alert = document.getElementById("avatar-alert");
if (existing_alert) {
existing_alert.parentNode.removeChild(existing_alert);
}
if (this.files[0].size > maxsize) {
var warning_header = this.dataset.warningHeader;
var warning_text = this.dataset.maxsizeWarning;
var flash = document.createElement("div");
flash.id = "avatar-alert";
flash.classList.add("box");
flash.classList.add("alert");
flash.classList.add("el-5");
flash.setAttribute("role", "alert");
var header = document.createElement("header");
header.innerText = warning_header;
flash.appendChild(header);
var p = document.createElement("p")
p.innerText = warning_text + ".";
flash.appendChild(p);
var flashbox = document.getElementById("flashbox");
flashbox.appendChild(flash);
this.value = null;
}
};
</script>
</form></div>
{% endblock %}

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-02-23 07:55+0100\n"
"POT-Creation-Date: 2021-03-20 12:56+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -149,39 +149,39 @@ msgstr ""
msgid "Main"
msgstr ""
#: snikket_web/invite.py:104
#: snikket_web/invite.py:106
msgid "Username"
msgstr ""
#: snikket_web/invite.py:108 snikket_web/invite.py:175 snikket_web/main.py:42
#: snikket_web/invite.py:110 snikket_web/invite.py:177 snikket_web/main.py:42
msgid "Password"
msgstr ""
#: snikket_web/invite.py:112 snikket_web/invite.py:179
#: snikket_web/invite.py:114 snikket_web/invite.py:181
msgid "Confirm password"
msgstr ""
#: snikket_web/invite.py:116 snikket_web/invite.py:183
#: snikket_web/invite.py:118 snikket_web/invite.py:185
msgid "The passwords must match"
msgstr ""
#: snikket_web/invite.py:121
#: snikket_web/invite.py:123
msgid "Create account"
msgstr ""
#: snikket_web/invite.py:148
#: snikket_web/invite.py:150
msgid "That username is already taken"
msgstr ""
#: snikket_web/invite.py:152 snikket_web/invite.py:216
#: snikket_web/invite.py:154 snikket_web/invite.py:218
msgid "Registration was declined for unknown reasons"
msgstr ""
#: snikket_web/invite.py:156
#: snikket_web/invite.py:158
msgid "The username is not valid"
msgstr ""
#: snikket_web/invite.py:188 snikket_web/templates/user_home.html:32
#: snikket_web/invite.py:190 snikket_web/templates/user_home.html:32
#: snikket_web/templates/user_passwd.html:29
msgid "Change password"
msgstr ""
@@ -202,67 +202,77 @@ msgstr ""
msgid "Login successful!"
msgstr ""
#: snikket_web/user.py:28
#: snikket_web/user.py:29
msgid "Current password"
msgstr ""
#: snikket_web/user.py:33
#: snikket_web/user.py:34
msgid "New password"
msgstr ""
#: snikket_web/user.py:38
#: snikket_web/user.py:39
msgid "Confirm new password"
msgstr ""
#: snikket_web/user.py:42
#: snikket_web/user.py:43
msgid "The new passwords must match"
msgstr ""
#: snikket_web/user.py:49
#: snikket_web/user.py:50
msgid "Sign out"
msgstr ""
#: snikket_web/user.py:54
#: snikket_web/user.py:55
msgid "Nobody"
msgstr ""
#: snikket_web/user.py:55
#: snikket_web/user.py:56
msgid "Friends only"
msgstr ""
#: snikket_web/user.py:56
#: snikket_web/user.py:57
msgid "Everyone"
msgstr ""
#: snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:62
#: snikket_web/templates/admin_users.html:11 snikket_web/user.py:63
msgid "Display name"
msgstr ""
#: snikket_web/user.py:66
#: snikket_web/user.py:67
msgid "Avatar"
msgstr ""
#: snikket_web/user.py:70
#: snikket_web/user.py:71
msgid "Profile visibility"
msgstr ""
#: snikket_web/user.py:75
#: snikket_web/user.py:76
msgid "Update profile"
msgstr ""
#: snikket_web/user.py:100
#: snikket_web/user.py:101
msgid "Incorrect password"
msgstr ""
#: snikket_web/user.py:104
#: snikket_web/user.py:105
msgid "Password changed"
msgstr ""
#: snikket_web/user.py:146
#: snikket_web/user.py:113
msgid ""
"The chosen avatar is too big. To be able to upload larger avatars, please"
" use the app"
msgstr ""
#: snikket_web/user.py:161
msgid "Profile updated"
msgstr ""
#: snikket_web/templates/unauth.html:18 snikket_web/user.py:169
msgid "Error"
msgstr ""
#: snikket_web/templates/_footer.html:4
#, python-format
msgid "A <a href=\"%(about_url)s\">Snikket</a> service"
@@ -480,7 +490,7 @@ msgstr ""
#: snikket_web/templates/admin_reset_user_password.html:25
#: snikket_web/templates/user_logout.html:10
#: snikket_web/templates/user_passwd.html:27
#: snikket_web/templates/user_profile.html:28
#: snikket_web/templates/user_profile.html:32
msgid "Back"
msgstr ""
@@ -1068,10 +1078,6 @@ msgstr ""
msgid "Operation successful"
msgstr ""
#: snikket_web/templates/unauth.html:18
msgid "Error"
msgstr ""
#: snikket_web/templates/user_home.html:9
msgid "Welcome!"
msgstr ""
@@ -1140,11 +1146,11 @@ msgstr ""
msgid "Profile"
msgstr ""
#: snikket_web/templates/user_profile.html:19
#: snikket_web/templates/user_profile.html:23
msgid "Visibility"
msgstr ""
#: snikket_web/templates/user_profile.html:20
#: snikket_web/templates/user_profile.html:24
msgid ""
"This section allows you to control who can see your profile information, "
"like avatar and nickname."

View File

@@ -9,6 +9,7 @@ from quart import (
redirect,
url_for,
flash,
current_app,
)
import quart.exceptions
@@ -109,9 +110,17 @@ async def change_pw() -> typing.Union[str, quart.Response]:
return await render_template("user_passwd.html", form=form)
EAVATARTOOBIG = _l(
"The chosen avatar is too big. To be able to upload larger "
"avatars, please use the app"
)
@bp.route("/profile", methods=["GET", "POST"])
@client.require_session()
async def profile() -> typing.Union[str, quart.Response]:
max_avatar_size = current_app.config["MAX_AVATAR_SIZE"]
form = ProfileForm()
if request.method != "POST":
user_info = await client.get_user_info()
@@ -125,30 +134,40 @@ async def profile() -> typing.Union[str, quart.Response]:
if form.validate_on_submit():
user_info = await client.get_user_info()
ok = True
file_info = (await request.files).get(form.avatar.name)
if file_info is not None:
mimetype = file_info.mimetype
data = file_info.stream.read()
if len(data) > 0:
if len(data) > max_avatar_size:
print(len(data), max_avatar_size)
form.avatar.errors.append(EAVATARTOOBIG)
ok = False
elif len(data) > 0:
await client.set_user_avatar(data, mimetype)
if user_info.get("nickname") != form.nickname.data:
await client.set_user_nickname(form.nickname.data)
if ok:
if user_info.get("nickname") != form.nickname.data:
await client.set_user_nickname(form.nickname.data)
access_model = form.profile_access_model.data
await asyncio.gather(
client.set_avatar_access_model(access_model),
client.set_vcard_access_model(access_model),
client.set_nickname_access_model(access_model),
)
access_model = form.profile_access_model.data
await asyncio.gather(
client.set_avatar_access_model(access_model),
client.set_vcard_access_model(access_model),
client.set_nickname_access_model(access_model),
)
await flash(
_("Profile updated"),
"success",
)
return redirect(url_for(".profile"))
await flash(
_("Profile updated"),
"success",
)
return redirect(url_for(".profile"))
return await render_template("user_profile.html", form=form)
return await render_template("user_profile.html",
form=form,
max_avatar_size=max_avatar_size,
avatar_too_big_warning_header=_l("Error"),
avatar_too_big_warning=EAVATARTOOBIG)
@bp.route("/logout", methods=["GET", "POST"])