CI - Security, Lint & Tests / validate (push) Failing after 1m15s
Tentative d'obtenir un token de connection échoue
110 lines
3.9 KiB
Python
110 lines
3.9 KiB
Python
"""Helpers used by more than one route module in this package.
|
|
|
|
Nothing here touches the blueprint: these are plain functions, so a test
|
|
can call them with a request context and nothing else.
|
|
"""
|
|
|
|
from flask_babel import gettext as _
|
|
|
|
from app.extensions import db
|
|
|
|
# Re-exported: these two moved to app/forms.py once the match and tryout
|
|
# routes needed them as well (ARCH-005). Importing them from here still
|
|
# works, so the thirty call sites in this package did not have to move.
|
|
from app.forms import flash_validation_errors, form_payload # noqa: F401
|
|
from app.models import Admin, Coach, Manager, Player, Scout, UserGamertag
|
|
|
|
ALLOWED_CONTRACT_EXTENSIONS = {'pdf'}
|
|
ALLOWED_SIGNED_EXTENSIONS = {'pdf'}
|
|
|
|
#: Every PDF starts with this. Checking the name alone accepted a file
|
|
#: called anything.pdf holding anything at all.
|
|
PDF_SIGNATURE = b'%PDF-'
|
|
|
|
#: USER_TYPE → model class, for create_user.
|
|
USER_CLASS_MAP = {
|
|
'admin': Admin,
|
|
'manager': Manager,
|
|
'coach': Coach,
|
|
'player': Player,
|
|
'scout': Scout,
|
|
}
|
|
|
|
|
|
def pdf_upload_error(file, allowed_extensions):
|
|
"""Why this upload is not an acceptable PDF, or None if it is.
|
|
|
|
upload_signed_contract checked nothing beyond a non-empty filename —
|
|
ALLOWED_SIGNED_EXTENSIONS was declared and never read — so a player
|
|
could put an arbitrary file on the server under a name the application
|
|
later hands back for download (SEC-021).
|
|
|
|
The check now goes beyond the header: it parses the upload with pypdf
|
|
and requires at least one parseable page, so a file with ``%PDF-``
|
|
prepended or a renamed non-PDF is rejected.
|
|
|
|
Args:
|
|
file: The uploaded FileStorage, or None.
|
|
allowed_extensions: Extensions to accept, lowercase and without dot.
|
|
|
|
Returns:
|
|
str | None: A message to flash, or None when the file is acceptable.
|
|
"""
|
|
if file is None or not file.filename:
|
|
return _('No file selected.')
|
|
|
|
stem, dot, extension = file.filename.rpartition('.')
|
|
if not (stem and dot) or extension.lower() not in allowed_extensions:
|
|
return _('Only PDF files are allowed for contracts.')
|
|
|
|
# Fast-fail: the PDF signature must appear at the start of the stream.
|
|
head = file.stream.read(len(PDF_SIGNATURE))
|
|
file.stream.seek(0)
|
|
if head != PDF_SIGNATURE:
|
|
return _('That file is not a PDF, whatever its name says.')
|
|
|
|
# Full-content validation: parse with pypdf and require at least one
|
|
# page. A file with a forged header but no real PDF structure (e.g. a
|
|
# renamed executable with ``%PDF-`` prepended) will fail here.
|
|
try:
|
|
from pypdf import PdfReader
|
|
from pypdf.errors import PyPdfError
|
|
|
|
reader = PdfReader(file.stream, strict=False)
|
|
if len(reader.pages) == 0:
|
|
return _('That file is not a PDF, whatever its name says.')
|
|
except (PyPdfError, ValueError, TypeError):
|
|
return _('That file is not a PDF, whatever its name says.')
|
|
finally:
|
|
file.stream.seek(0)
|
|
|
|
return None
|
|
|
|
|
|
def update_user_gamertags(user, selected_games):
|
|
"""Update gamertags for a user from validated dynamic form fields."""
|
|
from app.forms import form_gamertags
|
|
|
|
submitted = form_gamertags(selected_games)
|
|
existing_gamertags = {gt.game: gt for gt in user.gamertags}
|
|
for game in selected_games:
|
|
payload = submitted.get(game)
|
|
existing = existing_gamertags.get(game)
|
|
if payload:
|
|
if existing:
|
|
existing.gamertag = payload['gamertag']
|
|
existing.platform = payload['platform']
|
|
else:
|
|
gt = UserGamertag(
|
|
user_id=user.id,
|
|
game=game,
|
|
gamertag=payload['gamertag'],
|
|
platform=payload['platform'],
|
|
)
|
|
db.session.add(gt)
|
|
elif existing:
|
|
db.session.delete(existing)
|
|
for game in existing_gamertags:
|
|
if game not in selected_games:
|
|
db.session.delete(existing_gamertags[game])
|