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", "sv",
], converter=autosplit) ], converter=autosplit)
apple_store_url = environ.var("") 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))) _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["SITE_NAME"] = config.site_name or config.domain
app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl
app.config["APPLE_STORE_URL"] = config.apple_store_url app.config["APPLE_STORE_URL"] = config.apple_store_url
app.config["MAX_AVATAR_SIZE"] = config.max_avatar_size
app.context_processor(proc) app.context_processor(proc)
app.register_error_handler( app.register_error_handler(

View File

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

View File

@@ -1,10 +1,11 @@
{% extends "app.html" %} {% 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 %} {% block content %}
<h1>{% trans %}Update your profile{% endtrans %}</h1> <h1>{% trans %}Update your profile{% endtrans %}</h1>
<div class="form layout-expanded"><form method="POST" enctype="multipart/form-data"> <div class="form layout-expanded"><form method="POST" enctype="multipart/form-data">
<h2 class="form-title">{% trans %}Profile{% endtrans %}</h2> <h2 class="form-title">{% trans %}Profile{% endtrans %}</h2>
{{ form.csrf_token }} {{ form.csrf_token }}
{% call render_errors(form) %}{% endcall %}
<div class="f-ebox"> <div class="f-ebox">
{{ form.nickname.label }} {{ form.nickname.label }}
{{ form.nickname(placeholder=user_info.username) }} {{ form.nickname(placeholder=user_info.username) }}
@@ -13,7 +14,10 @@
{{ form.avatar.label }} {{ form.avatar.label }}
<div class="avatar-wrap"> <div class="avatar-wrap">
{%- call avatar(user_info.address, user_info.avatar_hash ) %}{% endcall -%} {%- 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>
</div> </div>
<h3 class="form-title">{% trans %}Visibility{% endtrans %}</h3> <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 standard_button("back", url_for('.index'), class="secondary") %}{% trans %}Back{% endtrans %}{% endcall -%}
{%- call form_button("done", form.action_save, class="primary") %}{% endcall -%} {%- call form_button("done", form.action_save, class="primary") %}{% endcall -%}
</div> </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> </form></div>
{% endblock %} {% endblock %}

View File

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

View File

@@ -9,6 +9,7 @@ from quart import (
redirect, redirect,
url_for, url_for,
flash, flash,
current_app,
) )
import quart.exceptions 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) 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"]) @bp.route("/profile", methods=["GET", "POST"])
@client.require_session() @client.require_session()
async def profile() -> typing.Union[str, quart.Response]: async def profile() -> typing.Union[str, quart.Response]:
max_avatar_size = current_app.config["MAX_AVATAR_SIZE"]
form = ProfileForm() form = ProfileForm()
if request.method != "POST": if request.method != "POST":
user_info = await client.get_user_info() user_info = await client.get_user_info()
@@ -125,30 +134,40 @@ async def profile() -> typing.Union[str, quart.Response]:
if form.validate_on_submit(): if form.validate_on_submit():
user_info = await client.get_user_info() user_info = await client.get_user_info()
ok = True
file_info = (await request.files).get(form.avatar.name) file_info = (await request.files).get(form.avatar.name)
if file_info is not None: if file_info is not None:
mimetype = file_info.mimetype mimetype = file_info.mimetype
data = file_info.stream.read() 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) await client.set_user_avatar(data, mimetype)
if user_info.get("nickname") != form.nickname.data: if ok:
await client.set_user_nickname(form.nickname.data) if user_info.get("nickname") != form.nickname.data:
await client.set_user_nickname(form.nickname.data)
access_model = form.profile_access_model.data access_model = form.profile_access_model.data
await asyncio.gather( await asyncio.gather(
client.set_avatar_access_model(access_model), client.set_avatar_access_model(access_model),
client.set_vcard_access_model(access_model), client.set_vcard_access_model(access_model),
client.set_nickname_access_model(access_model), client.set_nickname_access_model(access_model),
) )
await flash( await flash(
_("Profile updated"), _("Profile updated"),
"success", "success",
) )
return redirect(url_for(".profile")) 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"]) @bp.route("/logout", methods=["GET", "POST"])