From 107c89155310f1278c621f06c54dd541f60b3e7f Mon Sep 17 00:00:00 2001 From: Darks Date: Thu, 6 Aug 2020 00:04:47 +0200 Subject: [PATCH] fix(validators): cleaned a bit the directory --- app/forms/account.py | 29 +++++----- app/forms/forum.py | 6 ++- app/utils/validators/__init__.py | 93 ++------------------------------ app/utils/validators/file.py | 23 +++++++- app/utils/validators/name.py | 49 +++++++++++++++++ app/utils/validators/password.py | 38 +++++++++++++ 6 files changed, 134 insertions(+), 104 deletions(-) create mode 100644 app/utils/validators/name.py create mode 100644 app/utils/validators/password.py diff --git a/app/forms/account.py b/app/forms/account.py index ac887f1..e9bee41 100644 --- a/app/forms/account.py +++ b/app/forms/account.py @@ -12,8 +12,8 @@ class RegistrationForm(FlaskForm): description='Ce nom est définitif !', validators=[ InputRequired(), - vd.name_valid, - vd.name_available, + vd.name.valid, + vd.name.available, ], ) email = EmailField( @@ -28,7 +28,7 @@ class RegistrationForm(FlaskForm): 'Mot de passe', validators=[ InputRequired(), - vd.password, + vd.password.is_strong, ], ) password2 = PasswordField( @@ -58,7 +58,8 @@ class UpdateAccountForm(FlaskForm): 'Avatar', validators=[ Optional(), - vd.avatar, + vd.file.is_image, + vd.file.avatar_size, ], ) email = EmailField( @@ -67,15 +68,15 @@ class UpdateAccountForm(FlaskForm): Optional(), Email(message="Addresse email invalide."), vd.email, - vd.old_password, + vd.password.old_password, ], ) password = PasswordField( 'Nouveau mot de passe', validators=[ Optional(), - vd.password, - vd.old_password, + vd.password.is_strong, + vd.password.old_password, ], ) password2 = PasswordField( @@ -136,7 +137,7 @@ class DeleteAccountForm(FlaskForm): 'Mot de passe', validators=[ InputRequired(), - vd.old_password, + vd.password.old_password, ], ) submit = SubmitField( @@ -160,7 +161,7 @@ class ResetPasswordForm(FlaskForm): 'Mot de passe', validators=[ Optional(), - vd.password, + vd.password.is_strong, ], ) password2 = PasswordField( @@ -178,14 +179,16 @@ class AdminUpdateAccountForm(FlaskForm): 'Pseudonyme', validators=[ Optional(), - vd.name_valid, + vd.name.valid, + vd.name.available, ], ) avatar = FileField( 'Avatar', validators=[ Optional(), - vd.avatar, + vd.file.is_image, + vd.file.avatar_size, ], ) email = EmailField( @@ -198,7 +201,7 @@ class AdminUpdateAccountForm(FlaskForm): ) email_confirmed = BooleanField( "Confirmer l'email", - description="Si décoché, l'utilisateur devra demander explicitement un email "\ + description="Si décoché, l'utilisateur devra demander explicitement un email " "de validation, ou faire valider son adresse email par un administrateur.", ) password = PasswordField( @@ -206,7 +209,7 @@ class AdminUpdateAccountForm(FlaskForm): description="L'ancien mot de passe ne pourra pas être récupéré !", validators=[ Optional(), - vd.password, + vd.password.is_strong, ], ) xp = DecimalField( diff --git a/app/forms/forum.py b/app/forms/forum.py index 328ff07..4cf4d2d 100644 --- a/app/forms/forum.py +++ b/app/forms/forum.py @@ -11,13 +11,16 @@ class CommentForm(FlaskForm): submit = SubmitField('Commenter') preview = SubmitField('Prévisualiser') + class AnonymousCommentForm(CommentForm): pseudo = StringField('Pseudo', - validators=[InputRequired(), vd.name_valid, vd.name_available]) + validators=[InputRequired(), vd.name.valid, vd.name.available]) + class CommentEditForm(CommentForm): submit = SubmitField('Modifier') + class AnonymousCommentEditForm(CommentEditForm, AnonymousCommentForm): pass @@ -27,5 +30,6 @@ class TopicCreationForm(CommentForm): validators=[InputRequired(), Length(min=3, max=32)]) submit = SubmitField('Créer le sujet') + class AnonymousTopicCreationForm(TopicCreationForm, AnonymousCommentForm): pass diff --git a/app/utils/validators/__init__.py b/app/utils/validators/__init__.py index c02d089..c7d2e70 100644 --- a/app/utils/validators/__init__.py +++ b/app/utils/validators/__init__.py @@ -1,104 +1,19 @@ from flask_login import current_user from wtforms.validators import ValidationError -from PIL import Image -from app.models.users import Member, User +from app.models.users import Member from app.models.trophies import Title -from app.utils.valid_name import valid_name -from app.utils.unicode_names import normalize -from math import log from werkzeug.exceptions import NotFound -import app.utils.ldap as ldap - -from config import V5Config from app.utils.validators.file import * +from app.utils.validators.name import * +from app.utils.validators.password import * -# TODO: clean this shit into split files - -def name_valid(form, name): - valid = valid_name(name.data) - default = "Nom d'utilisateur invalide (erreur interne)" - msg = { - "too-short": - "Le nom d'utilisateur doit faire au moins " - f"{User.NAME_MINLEN} caractères.", - "too-long": - "Le nom d'utilisateur doit faire au plus " - f"{User.NAME_MAXLEN} caractères.", - "cant-normalize": - "Ce nom d'utilisateur contient des caractères interdits. Les " - "caractères autorisés sont les lettres, lettres accentuées, " - 'chiffres ainsi que "-" (tiret), "." (point), "~" (tilde) et ' - '"_" (underscore).', - "no-letter": - "Le nom d'utilisateur doit contenir au moins une lettre.", - "forbidden": - "Ce nom d'utilisateur est interdit." - } - if valid is not True: - err = ' '.join(msg.get(code, default) for code in valid) - raise ValidationError(err) - -def name_available(form, name): - # If the name is invalid, name_valid() will return a meaningful message - try: - norm = normalize(name.data) - except ValueError: - return - - member = Member.query.filter_by(norm=norm).first() - if member is not None: - raise ValidationError("Ce nom d'utilisateur est indisponible.") - - # Double check with LDAP if needed - if V5Config.USE_LDAP: - member = ldap.get_member(norm) - if member is not None: - raise ValidationError("Ce nom d'utilisateur est indisponible.") def email(form, email): member = Member.query.filter_by(email=email.data).first() if member is not None: raise ValidationError('Adresse email déjà utilisée.') -def password(form, password): - # To avoid errors in forms where password is optionnal - if len(password.data) == 0: - return - - def entropy(password): - """Estimate entropy of a password, in bits""" - # If you edit this function, please edit accordingly the JS one - # in static/script/entropy.js - chars = [ - "abcdefghijklmnopqrstuvwxyz", - "ABCDFEGHIJKLMNOPQRSTUVWXYZ", - "0123456789", - " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars - "áàâéèêíìîóòôúùûç", - ] - used = set() - for c in password: - for i in chars: - if c in i: - used.add(i) - return log(len(''.join(used)) ** len(password), 2) - - if entropy(password.data) < 60: - raise ValidationError("Mot de passe pas assez complexe") - -def avatar(form, avatar): - try: - Image.open(avatar.data) - except IOError: - raise ValidationError("Avatar invalide") - -def old_password(form, field): - if field.data: - if not form.old_password.data: - raise ValidationError('Votre ancien mot de passe est requis pour cette modification.') - if not current_user.check_password(form.old_password.data): - raise ValidationError('Mot de passe actuel erroné.') def id_exists(object): """Check if an id exists in a table""" @@ -112,10 +27,12 @@ def id_exists(object): raise ValidationError('L\'id n\'existe pas dans la BDD') return _id_exists + def css(form, css): """Check if input is valid and sane CSS""" pass + def own_title(form, title): # Everyone can use "Member" if title.data == -1: diff --git a/app/utils/validators/file.py b/app/utils/validators/file.py index c120ef6..7c2da7b 100644 --- a/app/utils/validators/file.py +++ b/app/utils/validators/file.py @@ -2,12 +2,15 @@ from flask_login import current_user from wtforms.validators import ValidationError, StopValidation from werkzeug.utils import secure_filename from app.utils.filesize import filesize +from PIL import Image import re + def optional(form, files): if(len(files.data) == 0 or files.data[0].filename == ""): raise StopValidation() + def count(form, files): if current_user.is_authenticated: if current_user.priv("no-upload-limits"): @@ -18,6 +21,7 @@ def count(form, files): if len(files.data) > 3: raise ValidationError("3 fichiers maximum autorisés") + def extension(form, files): valid_extensions = [ "g[123][a-z]|cpa|c1a|fxi|cat|mcs|xcp|fls", # Casio files @@ -36,7 +40,9 @@ def extension(form, files): errors.append("." + ext) if len(errors) > 0: - raise ValidationError(f"Extension(s) invalide(s) ({', '.join(errors)})") + raise ValidationError("Extension(s) invalide(s)" + f"({', '.join(errors)})") + def size(form, files): """There is no global limit to file sizes""" @@ -50,6 +56,7 @@ def size(form, files): if size > 500e3: # 500 ko per comment for a guest raise ValidationError("Fichiers trop lourds (max 500 ko)") + def namelength(form, files): errors = [] for f in files.data: @@ -57,5 +64,17 @@ def namelength(form, files): if len(name) > 64: errors.append(f.filename) if len(errors) > 0: - raise ValidationError(f"Noms trop longs, 64 caractères max " \ + raise ValidationError("Noms trop longs, 64 caractères max " f"({', '.join(errors)})") + + +def is_image(form, avatar): + try: + Image.open(avatar.data) + except IOError: + raise ValidationError("Avatar invalide") + + +def avatar_size(form, file): + if filesize(file.data) > 200e3: + raise ValidationError("Fichier trop lourd (max 200 ko)") diff --git a/app/utils/validators/name.py b/app/utils/validators/name.py new file mode 100644 index 0000000..b86f23f --- /dev/null +++ b/app/utils/validators/name.py @@ -0,0 +1,49 @@ +from wtforms.validators import ValidationError +from app.utils.valid_name import valid_name +from app.models.users import User, Member +import app.utils.ldap as ldap +from app.utils.unicode_names import normalize +from config import V5Config + + +def valid(form, name): + valid = valid_name(name.data) + default = "Nom d'utilisateur invalide (erreur interne)" + msg = { + "too-short": + "Le nom d'utilisateur doit faire au moins " + f"{User.NAME_MINLEN} caractères.", + "too-long": + "Le nom d'utilisateur doit faire au plus " + f"{User.NAME_MAXLEN} caractères.", + "cant-normalize": + "Ce nom d'utilisateur contient des caractères interdits. Les " + "caractères autorisés sont les lettres, lettres accentuées, " + 'chiffres ainsi que "-" (tiret), "." (point), "~" (tilde) et ' + '"_" (underscore).', + "no-letter": + "Le nom d'utilisateur doit contenir au moins une lettre.", + "forbidden": + "Ce nom d'utilisateur est interdit." + } + if valid is not True: + err = ' '.join(msg.get(code, default) for code in valid) + raise ValidationError(err) + + +def available(form, name): + # If the name is invalid, name_valid() will return a meaningful message + try: + norm = normalize(name.data) + except ValueError: + return + + member = Member.query.filter_by(norm=norm).first() + if member is not None: + raise ValidationError("Ce nom d'utilisateur est indisponible.") + + # Double check with LDAP if needed + if V5Config.USE_LDAP: + member = ldap.get_member(norm) + if member is not None: + raise ValidationError("Ce nom d'utilisateur est indisponible.") diff --git a/app/utils/validators/password.py b/app/utils/validators/password.py new file mode 100644 index 0000000..7f1b9ab --- /dev/null +++ b/app/utils/validators/password.py @@ -0,0 +1,38 @@ +from wtforms.validators import ValidationError +from flask_login import current_user +from math import log + + +def is_strong(form, password): + # To avoid errors in forms where password is optionnal + if len(password.data) == 0: + return + + def entropy(password): + """Estimate entropy of a password, in bits""" + # If you edit this function, please edit accordingly the JS one + # in static/script/entropy.js + chars = [ + "abcdefghijklmnopqrstuvwxyz", + "ABCDFEGHIJKLMNOPQRSTUVWXYZ", + "0123456789", + " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~§", # OWASP special chars + "áàâéèêíìîóòôúùûç", + ] + used = set() + for c in password: + for i in chars: + if c in i: + used.add(i) + return log(len(''.join(used)) ** len(password), 2) + + if entropy(password.data) < 60: + raise ValidationError("Mot de passe pas assez complexe") + + +def old_password(form, field): + if field.data: + if not form.old_password.data: + raise ValidationError('Votre ancien mot de passe est requis pour cette modification.') + if not current_user.check_password(form.old_password.data): + raise ValidationError('Mot de passe actuel erroné.')