Merge branch 'dev' into preprod

This commit is contained in:
Darks 2020-08-25 23:46:53 +02:00
commit b6a8302074
Signed by: Darks
GPG Key ID: F61F10FA138E797C
40 changed files with 244 additions and 217 deletions

9
.gitignore vendored
View File

@ -4,8 +4,12 @@ app/__pycache__/
app/static/avatars/
app/static/images/trophies/
## Devlopement files
# Development files
# Flask env
.env
.flaskenv
# virtualenv
requirements.txt
venv/
@ -14,6 +18,7 @@ venv/
Pipfile
Pipfile.lock
## Deployment files
# uWSGI configuration file
@ -25,10 +30,12 @@ update.sh
# Config to set up some server specific config
local_config.py
## Wiki
wiki/
## Personal folder
exclude/

4
V5.py
View File

@ -1,6 +1,4 @@
from app import app, db
from app.models.users import User, Guest, Member, Group, GroupPrivilege
from app.models.topic import Topic
from app import app
@app.shell_context_processor

View File

@ -1,9 +1,8 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, TextAreaField, SubmitField, DecimalField, SelectField
from wtforms.fields.html5 import DateField, EmailField
from wtforms.validators import DataRequired, InputRequired, Optional, Email, EqualTo
from wtforms.validators import InputRequired, Optional, Email, EqualTo
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
from app.models.trophies import Trophy
import app.utils.validators as vd
@ -12,15 +11,15 @@ class RegistrationForm(FlaskForm):
'Pseudonyme',
description='Ce nom est définitif !',
validators=[
DataRequired(),
vd.name_valid,
vd.name_available,
InputRequired(),
vd.name.valid,
vd.name.available,
],
)
email = EmailField(
'Adresse Email',
validators=[
DataRequired(),
InputRequired(),
Email(message="Adresse email invalide."),
vd.email,
],
@ -28,21 +27,21 @@ class RegistrationForm(FlaskForm):
password = PasswordField(
'Mot de passe',
validators=[
DataRequired(),
vd.password,
InputRequired(),
vd.password.is_strong,
],
)
password2 = PasswordField(
'Répéter le mot de passe',
validators=[
DataRequired(),
InputRequired(),
EqualTo('password', message="Les mots de passe doivent être identiques."),
],
)
guidelines = BooleanField(
"""J'accepte les <a href="#">CGU</a>""",
validators=[
DataRequired(),
InputRequired(),
],
)
newsletter = BooleanField(
@ -59,7 +58,8 @@ class UpdateAccountForm(FlaskForm):
'Avatar',
validators=[
Optional(),
vd.avatar,
vd.file.is_image,
vd.file.avatar_size,
],
)
email = EmailField(
@ -68,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(
@ -129,15 +129,15 @@ class DeleteAccountForm(FlaskForm):
delete = BooleanField(
'Confirmer la suppression',
validators=[
DataRequired(),
InputRequired(),
],
description='Attention, cette opération est irréversible!'
)
old_password = PasswordField(
'Mot de passe',
validators=[
DataRequired(),
vd.old_password,
InputRequired(),
vd.password.old_password,
],
)
submit = SubmitField(
@ -161,7 +161,7 @@ class ResetPasswordForm(FlaskForm):
'Mot de passe',
validators=[
Optional(),
vd.password,
vd.password.is_strong,
],
)
password2 = PasswordField(
@ -179,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(
@ -199,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(
@ -207,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(
@ -269,7 +271,7 @@ class AdminDeleteAccountForm(FlaskForm):
delete = BooleanField(
'Confirmer la suppression',
validators=[
DataRequired(),
InputRequired(),
],
description='Attention, cette opération est irréversible!',
)

View File

@ -1,32 +1,35 @@
from flask_wtf import FlaskForm
from wtforms import StringField, FormField, SubmitField, TextAreaField, \
MultipleFileField
from wtforms.validators import DataRequired, Length, Optional
from wtforms import StringField, SubmitField, TextAreaField, MultipleFileField
from wtforms.validators import InputRequired, Length
import app.utils.validators as vd
class CommentForm(FlaskForm):
message = TextAreaField('Message', validators=[DataRequired()])
message = TextAreaField('Message', validators=[InputRequired()])
attachments = MultipleFileField('Pièces-jointes',
validators=[vd.file.optional, vd.file.count, vd.file.extension,
vd.file.size, vd.file.namelength])
submit = SubmitField('Commenter')
preview = SubmitField('Prévisualiser')
class AnonymousCommentForm(CommentForm):
pseudo = StringField('Pseudo',
validators=[DataRequired(), 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
class TopicCreationForm(CommentForm):
title = StringField('Nom du sujet',
validators=[DataRequired(), Length(min=3, max=32)])
validators=[InputRequired(), Length(min=3, max=32)])
submit = SubmitField('Créer le sujet')
class AnonymousTopicCreationForm(TopicCreationForm, AnonymousCommentForm):
pass

View File

@ -1,19 +1,19 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
from wtforms.validators import InputRequired
class LoginForm(FlaskForm):
username = StringField(
'Identifiant',
validators=[
DataRequired(),
InputRequired(),
],
)
password = PasswordField(
'Mot de passe',
validators=[
DataRequired(),
InputRequired(),
],
)
remember_me = BooleanField(

View File

@ -1,7 +1,7 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.fields.html5 import DateField
from wtforms.validators import DataRequired, Optional
from wtforms.validators import InputRequired, Optional
# TODO: compléter le formulaire de recherche avancée
@ -9,7 +9,7 @@ class AdvancedSearchForm(FlaskForm):
q = StringField(
'Rechercher :',
validators=[
DataRequired(),
InputRequired(),
],
)
date = DateField(
@ -27,6 +27,6 @@ class SearchForm(FlaskForm):
q = StringField(
'Rechercher',
validators=[
DataRequired(),
InputRequired(),
],
)

View File

@ -1,6 +1,6 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, BooleanField
from wtforms.validators import DataRequired, Optional
from wtforms.validators import InputRequired, Optional
from flask_wtf.file import FileField # Cuz' wtforms' FileField is shitty
@ -8,7 +8,7 @@ class TrophyForm(FlaskForm):
name = StringField(
'Nom',
validators=[
DataRequired(),
InputRequired(),
],
)
icon = FileField(
@ -43,7 +43,7 @@ class DeleteTrophyForm(FlaskForm):
delete = BooleanField(
'Confirmer la suppression',
validators=[
DataRequired(),
InputRequired(),
],
description='Attention, cette opération est irréversible!',
)

View File

@ -16,8 +16,8 @@ class Forum(db.Model):
# Relationships
parent_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=True)
parent = db.relationship('Forum', backref='sub_forums', remote_side=id,
lazy=True, foreign_keys=parent_id)
parent = db.relationship('Forum', backref='sub_forums', remote_side=id,
lazy=True, foreign_keys=parent_id)
# Other fields populated automatically through relations:
# <topics> List of topics in this exact forum (of type Topic)

View File

@ -1,5 +1,4 @@
from app import db
from app.models.users import User
from datetime import datetime

View File

@ -1,5 +1,5 @@
# Planète Casio v5
# models.privs: Database models for groups and privilege management
# models.priv: Database models for groups and privilege management
from app import db

View File

@ -16,8 +16,8 @@ class Program(Post):
# TODO: Compatible calculator models
thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False)
thread = db.relationship('Thread', foreign_keys=thread_id,
back_populates='owner_program')
thread = db.relationship('Thread', foreign_keys=thread_id,
back_populates='owner_program')
# TODO: Number of views, statistics, attached files, etc

View File

@ -10,11 +10,11 @@ class Thread(db.Model):
# Top comment
top_comment_id = db.Column(db.Integer, db.ForeignKey('comment.id'))
top_comment = db.relationship('Comment', foreign_keys=top_comment_id)
top_comment = db.relationship('Comment', foreign_keys=top_comment_id)
# Post owning the thread, set only by Topic, Program, etc. In general, you
# should use [owner_post] which groups them together.
owner_topic = db.relationship('Topic')
owner_topic = db.relationship('Topic')
owner_program = db.relationship('Program')
# Other fields populated automatically through relations:

View File

@ -16,19 +16,19 @@ class Topic(Post):
# Post that the topic was promoted into. If this is not None, then the
# topic was published into a project and a redirection should be emitted
promotion_id = db.Column(db.Integer,db.ForeignKey('post.id'),nullable=True)
promotion = db.relationship('Post', foreign_keys=promotion_id)
promotion = db.relationship('Post', foreign_keys=promotion_id)
# Topic title
title = db.Column(db.Unicode(128))
# Parent forum
forum_id = db.Column(db.Integer, db.ForeignKey('forum.id'), nullable=False)
forum = db.relationship('Forum',
backref=backref('topics', lazy='dynamic'),foreign_keys=forum_id)
forum = db.relationship('Forum',
backref=backref('topics', lazy='dynamic'), foreign_keys=forum_id)
# Associated thread
thread_id = db.Column(db.Integer,db.ForeignKey('thread.id'),nullable=False)
thread = db.relationship('Thread', foreign_keys=thread_id,
thread = db.relationship('Thread', foreign_keys=thread_id,
back_populates='owner_topic')
# Number of views in the forum

View File

@ -1,21 +1,18 @@
from datetime import date
from flask import flash
from flask_login import UserMixin
from sqlalchemy import func as SQLfunc
from os.path import isfile
from PIL import Image
from app import app, db
from app.models.privs import SpecialPrivilege, Group, GroupMember, \
from app.models.priv import SpecialPrivilege, Group, GroupMember, \
GroupPrivilege
from app.models.trophies import Trophy, TrophyMember, Title
from app.models.trophy import Trophy, TrophyMember, Title
from app.models.notification import Notification
import app.utils.unicode_names as unicode_names
from app.utils.notify import notify
import app.utils.ldap as ldap
from config import V5Config
import werkzeug.security
import re
import math
import app
import os
@ -78,8 +75,7 @@ class Member(User):
# Primary attributes (needed for the system to work)
name = db.Column(db.Unicode(User.NAME_MAXLEN), index=True)
norm = db.Column(db.Unicode(User.NAME_MAXLEN), index=True,
unique=True)
norm = db.Column(db.Unicode(User.NAME_MAXLEN), index=True, unique=True)
email = db.Column(db.Unicode(120), index=True, unique=True)
email_confirmed = db.Column(db.Boolean)
password_hash = db.Column(db.String(255))
@ -107,7 +103,7 @@ class Member(User):
# Displayed title, if set
title_id = db.Column(db.Integer, db.ForeignKey('title.id'), nullable=True)
title = db.relationship('Title', foreign_keys=title_id)
title = db.relationship('Title', foreign_keys=title_id)
# Settings
newsletter = db.Column(db.Boolean, default=False)
@ -116,6 +112,7 @@ class Member(User):
trophies = db.relationship('Trophy', secondary=TrophyMember,
back_populates='owners')
topics = db.relationship('Topic')
programs = db.relationship('Program')
comments = db.relationship('Comment')
# Displayed title
@ -158,7 +155,7 @@ class Member(User):
if SpecialPrivilege.query.filter_by(mid=self.id, priv=priv).first():
return True
return db.session.query(Group, GroupPrivilege).filter(
Group.id.in_([ g.id for g in self.groups ]),
Group.id.in_([g.id for g in self.groups]),
GroupPrivilege.gid==Group.id,
GroupPrivilege.priv==priv).first() is not None
@ -377,8 +374,7 @@ class Member(User):
progress(levels, post_count)
if context in ["new-program", None]:
# TODO: Amount of programs by the user
program_count = 0
program_count = self.programs.count()
levels = {
5: "Programmeur du dimanche",

View File

@ -3,9 +3,7 @@ from app.forms.login import LoginForm
from app.forms.search import SearchForm
from app.models.forum import Forum
from app.models.topic import Topic
from app.models.thread import Thread
from app.models.comment import Comment
from app.models.users import Member
@app.context_processor
def menu_processor():

View File

@ -9,7 +9,7 @@ def utilities_processor():
return dict(
len=len,
# enumerate=enumerate,
_url_for = lambda route, args, **other: url_for(route, **args, **other),
V5Config = V5Config,
_url_for=lambda route, args, **other: url_for(route, **args, **other),
V5Config=V5Config,
slugify=slugify,
)

View File

@ -3,8 +3,8 @@ from flask_login import login_required, current_user, logout_user
from app import app, db
from app.forms.account import UpdateAccountForm, RegistrationForm, \
DeleteAccountForm, AskResetPasswordForm, ResetPasswordForm
from app.models.users import Member
from app.models.trophies import Title
from app.models.user import Member
from app.models.trophy import Title
from app.utils.render import render
from app.utils.send_mail import send_validation_mail, send_reset_password_mail
from app.utils.priv_required import guest_only

View File

@ -3,8 +3,8 @@ from flask_login import login_user, logout_user, login_required, current_user
from urllib.parse import urlparse, urljoin
from app import app
from app.forms.login import LoginForm
from app.models.users import Member
from app.models.privs import Group
from app.models.user import Member
from app.models.priv import Group
from app.utils.render import render
from app.utils.send_mail import send_validation_mail
import datetime

View File

@ -2,9 +2,9 @@ from flask import flash, redirect, url_for, request
from flask_login import current_user
from wtforms import BooleanField
from app.utils.priv_required import priv_required
from app.models.users import Member
from app.models.trophies import Trophy, Title
from app.models.privs import Group
from app.models.user import Member
from app.models.trophy import Trophy, Title
from app.models.priv import Group
from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \
AdminAccountEditTrophyForm, AdminAccountEditGroupForm
from app.utils.render import render

View File

@ -1,8 +1,8 @@
from app.utils.priv_required import priv_required
from flask_wtf import FlaskForm
from wtforms import SubmitField
from app.models.users import Member, Group, GroupPrivilege
from app.models.privs import SpecialPrivilege
from app.models.user import Member, Group, GroupPrivilege
from app.models.priv import SpecialPrivilege
from app.utils.render import render
from app import app, db
import yaml

View File

@ -1,7 +1,7 @@
from flask import request, flash, redirect, url_for
from app.utils.priv_required import priv_required
from app.models.trophies import Trophy, Title
from app.forms.trophies import TrophyForm, DeleteTrophyForm
from app.models.trophy import Trophy, Title
from app.forms.trophy import TrophyForm, DeleteTrophyForm
from app.utils.render import render
from app import app, db

View File

@ -9,7 +9,7 @@ import os
@app.route('/avatar/<filename>')
def avatar(filename):
filename = secure_filename(filename) # No h4ckers allowed
filename = secure_filename(filename) # No h4ckers allowed
filepath = os.path.join(V5Config.DATA_FOLDER, "avatars", filename)
if os.path.isfile(filepath):
return send_file(filepath)

View File

@ -9,7 +9,7 @@ from app.models.forum import Forum
from app.models.topic import Topic
from app.models.thread import Thread
from app.models.comment import Comment
from app.models.users import Guest
from app.models.user import Guest
from app.models.attachment import Attachment
@ -72,7 +72,7 @@ def forum_page(f, page=1):
# Update member's xp and trophies
if current_user.is_authenticated:
current_user.add_xp(2) # 2 points for a topic
current_user.add_xp(2) # 2 points for a topic
current_user.update_trophies('new-post')
flash('Le sujet a bien été créé', 'ok')

View File

@ -9,7 +9,7 @@ from app.models.forum import Forum
from app.models.topic import Topic
from app.models.thread import Thread
from app.models.comment import Comment
from app.models.users import Guest
from app.models.user import Guest
from app.models.attachment import Attachment
@ -53,7 +53,7 @@ def forum_topic(f, page):
# Update member's xp and trophies
if current_user.is_authenticated:
current_user.add_xp(1) # 1 point for a comment
current_user.add_xp(1) # 1 point for a comment
current_user.update_trophies('new-post')
flash('Message envoyé', 'ok')

View File

@ -2,8 +2,8 @@ from flask import redirect, url_for, send_file
from werkzeug.utils import secure_filename
import os.path
from app import app
from app.models.users import Member
from app.models.trophies import Trophy
from app.models.user import Member
from app.models.trophy import Trophy
from app.utils import unicode_names
from app.utils.render import render
from config import V5Config

View File

@ -20,8 +20,7 @@ from werkzeug.routing import BaseConverter, ValidationError
from app.models.forum import Forum
from app.models.topic import Topic
from slugify import slugify
import re
import sys
class ForumConverter(BaseConverter):

View File

@ -1,5 +1,5 @@
import os
from random import getrandbits
def filesize(file):
"""Return the filesize. Save in /tmp and delete it when done"""

View File

@ -1,5 +1,5 @@
from app import app
from app.models.trophies import Title
from app.models.trophy import Title
@app.template_filter('is_title')

View File

@ -1,13 +1,15 @@
import ldap
from ldap.modlist import addModlist, modifyModlist
from app.utils.unicode_names import normalize
from config import V5Config
def get_member(username):
""" Get informations about member. Username must be normalized! """
""" Get informations about member"""
username = normalize(username) # Never safe enough
conn = ldap.initialize("ldap://localhost")
# Search for user
r = conn.search_s(V5Config.LDAP_ORGANIZATION, ldap.SCOPE_SUBTREE,
f'(cn={username})')
r = conn.search_s(f"{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}",
ldap.SCOPE_SUBTREE, f'(cn={username})')
if len(r) > 0:
return r[0]
else:
@ -17,13 +19,15 @@ def get_member(username):
def edit(user, fields):
""" Edit a user. Fields is {'name': ['value'], …} """
conn = ldap.initialize("ldap://localhost")
# TODO: do this
# Connect as root
# conn.simple_bind_s(f'cn=ldap-root,{LDAP_ORGANIZATION}', LDAP_PASSWORD)
# conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ENV}',
# V5Config.LDAP_PASSWORD)
# old_value = {"userPassword": ["my_old_password"]}
# new_value = {"userPassword": ["my_new_password"]}
modlist = ldap.modlist.modifyModlist(old_value, new_value)
con.modify_s(dn, modlist)
# modlist = modifyModlist(old_value, new_value)
# conn.modify_s(dn, modlist)
def set_email(user, email):
@ -34,9 +38,9 @@ def set_password(user, password):
""" Set password for a user. """
conn = ldap.initialize("ldap://localhost")
# Connect as root
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}',
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ROOT}',
V5Config.LDAP_PASSWORD)
conn.passwd_s(f"cn={user.norm},{V5Config.LDAP_ORGANIZATION}",
conn.passwd_s(f"cn={user.norm},{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}",
None, password)
@ -44,8 +48,8 @@ def check_password(user, password):
""" Try to login a user through LDAP register. """
conn = ldap.initialize("ldap://localhost")
try:
conn.simple_bind_s(f"cn={user.norm},{V5Config.LDAP_ORGANIZATION}",
password)
conn.simple_bind_s(f"cn={user.norm},{V5Config.LDAP_ENV}," \
f"{V5Config.LDAP_ROOT}", password)
except ldap.INVALID_CREDENTIALS:
return False
return True
@ -58,10 +62,10 @@ def add_member(member):
return
conn = ldap.initialize("ldap://localhost")
# Connect as root
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}',
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ROOT}',
V5Config.LDAP_PASSWORD)
# Create fields
dn = f'cn={member.norm},{V5Config.LDAP_ORGANIZATION}'
dn = f'cn={member.norm},{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}'
modlist = addModlist({
'objectClass': [bytes('inetOrgPerson', 'UTF8')],
'cn': [bytes(member.norm, 'UTF8')],
@ -79,9 +83,9 @@ def delete_member(member):
""" Remove a member from LDAP register """
conn = ldap.initialize("ldap://localhost")
# Connect as root
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ORGANIZATION}',
conn.simple_bind_s(f'cn=ldap-root,{V5Config.LDAP_ROOT}',
V5Config.LDAP_PASSWORD)
# Create fields
dn = f'cn={member.norm},{V5Config.LDAP_ORGANIZATION}'
dn = f'cn={member.norm},{V5Config.LDAP_ENV},{V5Config.LDAP_ROOT}'
# Delete the user
conn.delete_s(dn)

View File

@ -1,6 +1,6 @@
from app import db
from app.models.notification import Notification
# from app.models.users import Member
from app.models.user import Member
def notify(user, message, href=None):
""" Notify a user (by id, name or object reference) with a message.

View File

@ -1,7 +1,4 @@
from flask import render_template
from app.forms.login import LoginForm
from app.forms.search import SearchForm
from app.models.forum import Forum
def render(*args, styles=[], scripts=[], **kwargs):
# TODO: debugguer cette merde : au logout, ça foire

View File

@ -1,5 +1,5 @@
from app.utils.unicode_names import normalize
from app.models.users import User
from app.models.user import User
import re
def valid_name(name, msg=False):

View File

@ -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.trophies import Title
from app.utils.valid_name import valid_name
from app.utils.unicode_names import normalize
from math import log
from app.models.user import Member
from app.models.trophy import Title
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:

View File

@ -2,29 +2,33 @@ 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"):
return
if len(files.data) > 100: # 100 files for a authenticated user
if len(files.data) > 100: # 100 files for a authenticated user
raise ValidationError("100 fichiers maximum autorisés")
else:
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
"png|jpg|jpeg|bmp|tiff|gif|xcf", # Images
"[ch](pp|\+\+|xx)?|s|py|bide|lua|lc", # Source code
"txt|md|tex|pdf|odt|ods|docx|xlsx", # Office files
"zip|7z|tar|bz2?|t?gz|xz|zst", # Archives
"g[123][a-z]|cpa|c1a|fxi|cat|mcs|xcp|fls", # Casio files
"png|jpg|jpeg|bmp|tiff|gif|xcf", # Images
"[ch](pp|\+\+|xx)?|s|py|bide|lua|lc", # Source code
"txt|md|tex|pdf|odt|ods|docx|xlsx", # Office files
"zip|7z|tar|bz2?|t?gz|xz|zst", # Archives
]
r = re.compile("|".join(valid_extensions), re.IGNORECASE)
errors = []
@ -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"""
@ -44,12 +50,13 @@ def size(form, files):
if current_user.is_authenticated:
if current_user.priv("no-upload-limits"):
return
if size > 5e6: # 5 Mo per comment for an authenticated user
if size > 5e6: # 5 Mo per comment for an authenticated user
raise ValidationError("Fichiers trop lourds (max 5Mo)")
else:
if size > 500e3: # 500 ko per comment for a guest
if size > 500e3: # 500 ko per comment for a guest
raise ValidationError("Fichiers trop lourds (max 500ko)")
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 200ko)")

View File

@ -0,0 +1,49 @@
from wtforms.validators import ValidationError
from app.utils.valid_name import valid_name
from app.models.user 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.")

View File

@ -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é.')

View File

@ -29,7 +29,8 @@ class DefaultConfig(object):
USE_LDAP = False
# LDAP configuration
LDAP_PASSWORD = "openldap"
LDAP_ORGANIZATION = "o=planet-casio"
LDAP_ROOT = "o=planet-casio"
LDAP_ENV = "o=prod"
# Secret key used to authenticate tokens. **USE YOURS!**
SECRET_KEY = "a-random-secret-key"
# Uploaded data folder

View File

@ -5,7 +5,7 @@ class LocalConfig(object):
DB_NAME = "pcv5"
USE_LDAP = True
LDAP_PASSWORD = "openldap"
LDAP_ORGANIZATION = "o=planet-casio"
LDAP_ENV = "o=prod"
SECRET_KEY = "a-random-secret-key" # CHANGE THIS VALUE *NOW*
AVATARS_FOLDER = '/home/pc/data/avatars/'
ENABLE_GUEST_POST = True

View File

@ -1,18 +1,18 @@
#! /usr/bin/python3
from app import app, db
from app.models.users import Member, Group, GroupPrivilege
from app.models.privs import SpecialPrivilege
from app.models.trophies import Trophy, Title, TrophyMember
from app.models.user import Member, Group, GroupPrivilege
from app.models.priv import SpecialPrivilege
from app.models.trophy import Trophy, Title
from app.models.forum import Forum
from app.utils import unicode_names
import os
import sys
import yaml
import readline
import slugify
from PIL import Image
help_msg = """
This is the Planète Casio master shell. Type 'exit' or C-D to leave.
@ -186,7 +186,7 @@ def create_trophies():
print(f"Created {len(tr)} trophies.")
# Create their icons in /app/static/images/trophies
names = [ slugify.slugify(t["name"]) for t in tr ]
names = [slugify.slugify(t["name"]) for t in tr]
src = os.path.join(app.root_path, "data", "trophies.png")
dst = os.path.join(app.root_path, "static", "images", "trophies")