La façade Cookies¶
Objectif : regrouper la lecture et l'écriture des cookies applicatifs
(thème, préférence…) sous une façade Cookies, distincte du cookie de session.
Ce que vous allez apprendre : lire un cookie depuis la requête, en poser un
sur la réponse avec des défauts sûrs, et une limite importante du noyau à
connaître avant de vous en servir.
Là où nous en sommes¶
Vous avez _facade.py (palier 1) et session.py (palier 2). Nous ajoutons une
façade pour les cookies génériques, qui ne sont pas du ressort de Session.
L'ajout¶
Créez mvc/helpers/cookies.py :
# mvc/helpers/cookies.py
"""Façade de confort pour les cookies applicatifs (helper applicatif).
Limite : Response.headers est un dict, donc UN SEUL Set-Cookie par réponse.
Pour le cookie de SESSION (durci __Host-), utiliser Session.set_cookie.
"""
from core.http.request import Request
from core.http.response import Response
from mvc.helpers._facade import Facade
class Cookies(Facade):
@staticmethod
def get(request: Request, name: str, default: str | None = None) -> str | None:
for part in request.headers.get("Cookie", "").split(";"):
part = part.strip()
if part.startswith(name + "="):
return part[len(name) + 1:]
return default
@staticmethod
def all(request: Request) -> dict[str, str]:
jar: dict[str, str] = {}
for part in request.headers.get("Cookie", "").split(";"):
part = part.strip()
if "=" in part:
key, value = part.split("=", 1)
jar[key.strip()] = value.strip()
return jar
@staticmethod
def set(response: Response, name: str, value: str, *, max_age: int | None = None,
path: str = "/", secure: bool = True, http_only: bool = True,
same_site: str = "Strict") -> None:
parts = [f"{name}={value}", f"Path={path}"]
if max_age is not None:
parts.append(f"Max-Age={int(max_age)}")
if http_only:
parts.append("HttpOnly")
if secure:
parts.append("Secure")
if same_site:
parts.append(f"SameSite={same_site}")
response.headers["Set-Cookie"] = "; ".join(parts)
@staticmethod
def clear(response: Response, name: str, *, path: str = "/") -> None:
Cookies.set(response, name, "", max_age=0, path=path)
Comprendre ce code¶
- Lecture :
getparcourt l'en-têteCookiede la requête et renvoie la
valeur (oudefault).allrenvoie tous les cookies sous forme de
dictionnaire. Aucune écriture, aucun effet de bord. - Écriture :
setconstruit la chaîneSet-Cookieavec des défauts sûrs
(Secure,HttpOnly,SameSite=Strict). Ces défauts sont surchargeables :
pour un cookie lisible côté JavaScript (un thème, par exemple), passez
http_only=False. clearefface un cookie en le réémettant avecMax-Age=0.
Limite : un seul Set-Cookie par réponse
Response.headers est un dictionnaire : il ne peut porter qu'un seul
Set-Cookie. Donc Cookies.set(...) écrase tout cookie déjà posé sur la
même réponse, y compris le cookie de session (Session.set_cookie écrit
la même clé). Concrètement : ne posez pas un cookie applicatif sur une
réponse qui pose aussi la session, l'un des deux serait perdu. La lecture
(get / all) n'est pas concernée ; c'est l'écriture multiple qui est
bloquée, côté noyau, tant que Response ne gère pas une liste de cookies.
Tester¶
Dans un shell Python du projet :
>>> import types
>>> from core.http.response import Response
>>> from mvc.helpers.cookies import Cookies
>>> # écriture
>>> r = Response.text("ok")
>>> Cookies.set(r, "theme", "dark", http_only=False, max_age=31536000)
>>> r.headers["Set-Cookie"]
'theme=dark; Path=/; Max-Age=31536000; Secure; SameSite=Strict'
>>> # lecture
>>> req = types.SimpleNamespace(headers={"Cookie": "theme=dark; lang=fr"})
>>> Cookies.get(req, "theme")
'dark'
>>> Cookies.all(req)
{'theme': 'dark', 'lang': 'fr'}
À retenir¶
Cookiesest pour les cookies applicatifs génériques ; le cookie de
session reste géré parSession.set_cookie(politique__Host-du noyau).- Lire un cookie = parser l'en-tête
Cookiede la requête ; écrire = poser un
Set-Cookiesur la réponse. - Un seul
Set-Cookiepar réponse : ne combinez pas un cookie applicatif et
le cookie de session sur la même réponse.
Au palier suivant, nous ajoutons une façade pour les messages flash.