Files
flowdeck/app/templating.py
T
Spark 463dd7b752
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 1s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
FlowDeck CI / lint (push) Failing after 11s
Flowdeck 7.85.3 — HUB-21 : consommation des réglages généraux du Hub (bannière + drapeau recherche_partout)
2026-11-06 16:57:05 +00:00

124 lines
4.4 KiB
Python

"""Environment Jinja2 partagé (A10 : autoescape activé partout).
Une seule instance au lieu de 29 `Environment(loader=FileSystemLoader(...))`
sans autoescape — 326 interpolations `{{ … }}` étaient servies crues et tous
les `|safe` du codebase étaient des no-op.
"""
from __future__ import annotations
import logging
import pathlib
from contextvars import ContextVar
from jinja2 import Environment, FileSystemLoader, Undefined, select_autoescape
logger = logging.getLogger(__name__)
class LoggingUndefined(Undefined):
"""Undefined qui se rend comme une chaîne vide mais **journalise**.
v7.46.0 : avec l'`Undefined` par défaut, une variable mal orthographiée
(ou absente du contexte de la route) rendait `""` sans aucune trace — c'est
exactement ce qui a rendu le bug du bouton Home invisible (``local_workspaces``
absent ⇒ `/workspaces`` en silence). ``StrictUndefined`` est inutilisable ici
(40+ templates optionnelles, ex. ``embed_mode``), donc on garde le rendu et
on rend le silence visible dans les logs.
"""
__slots__ = ()
def _warn(self) -> None:
logger.warning("Jinja: variable non définie utilisée dans un template: %r", self._undefined_name)
def __str__(self) -> str:
self._warn()
return ""
def __bool__(self) -> bool:
self._warn()
return False
def __iter__(self):
self._warn()
return iter(())
def __len__(self) -> int:
self._warn()
return 0
# A20 : nonce de script par requête, posé par le middleware CSP, lu par les
# templates via `{{ csp_nonce() }}` (vide hors requête — pas de header CSP
# dans ce cas, donc rien n'est bloqué).
CSP_NONCE: ContextVar[str] = ContextVar("csp_nonce", default="")
# A43 : jeton CSRF rendu côté serveur dans `hx-headers` (base.html) — posé
# par le middleware CSRF AVANT call_next, lu via `{{ csrf_token() }}`
# (vide = cookie absent sur cette requête, htmx:configRequest re-lit le
# cookie au moment de l'appel → jamais de « __CSRF_PLACEHOLDER__ » servi).
CSRF_TOKEN: ContextVar[str] = ContextVar("csrf_token", default="")
ENV = Environment(
loader=FileSystemLoader("app/templates"),
autoescape=select_autoescape(["html"]),
undefined=LoggingUndefined,
)
# A40 : version des assets statiques première main, lue une seule fois (le
# fichier VERSION fait foi ; repli "dev" si l'image ne le contient pas).
try:
ASSET_VERSION = (pathlib.Path(__file__).resolve().parent.parent / "VERSION").read_text(
encoding="utf-8"
).strip()
except OSError: # pragma: no cover
ASSET_VERSION = "dev"
ENV.globals["asset_version"] = ASSET_VERSION
ENV.globals["csp_nonce"] = lambda: CSP_NONCE.get()
ENV.globals["csrf_token"] = lambda: CSRF_TOKEN.get()
# Plugins activés : conditionne les blocs de UI rendus côté serveur.
ENV.globals["plugin_enabled"] = lambda slug: _plugin_enabled(slug)
# v7.69 : URL de base absolue pour og:image / twitter:image — les crawlers
# ignorent les URL relatives. Lecture paresseuse comme _plugin_enabled
# (évite l'import circulaire au chargement du module). Même source que les
# liens de partage de pages (collaboration.py) : si elle est mal renseignée,
# ces liens le sont déjà.
ENV.globals["app_base_url"] = lambda: _app_base_url()
# URLs publiques dracodev résolues par la config (dev/prod, v7.82.0) :
# l'hôte fournit les URL aux composants vendangés (barre commune,
# panneau Spark) — aucun nom public figé dans les templates/JS.
ENV.globals["draco_urls"] = lambda: _draco_urls()
ENV.globals["draco_agent_url"] = lambda: _draco_urls()["agent"]
ENV.globals["gitea_url"] = lambda: _draco_urls()["gitea"]
# Bannière d'annonce des réglages généraux du Hub (HUB-21) : la
# FONCTION est exposée (pas un instantané) — chaque rendu du
# gabarit principal lit le cache serveur de 60 s de
# app.services.hub_reglages. Hub muet → chaîne vide → aucune
# bannière, comme aujourd'hui.
ENV.globals["banniere_hub"] = lambda: _banniere_hub()
def _app_base_url() -> str:
from app.config import settings
return (settings.app_base_url or "").rstrip("/")
def _draco_urls() -> dict:
from app.services import draco_urls
return draco_urls.toutes()
def _banniere_hub() -> str:
from app.services import hub_reglages
return hub_reglages.banniere()
def _plugin_enabled(slug: str) -> bool:
from app.services.plugins import is_enabled
return is_enabled(slug)