Les meta `og:image` / `twitter:image` ajoutés en v7.69.0 pointaient vers une URL relative (`/static/img/logo-512.jpg`). Les crawlers — LinkedIn, Slack, Discord, iMessage — ignorent une URL relative : la carte de partage de FlowDeck était donc vide partout où un lien est collé. Correction via le motif déjà en place dans `app/templating.py` : un global Jinja `app_base_url()` construit à partir de `settings.app_base_url`, qui est LA MÊME source que les liens de partage de pages (`collaboration.py:32`). Si cette valeur est mal renseignée, ces liens l'étaient déjà — comportement cohérent plutôt qu'une deuxième source de vérité à maintenir. Lecture paresseuse de `settings` dans la fonction, à l'image de `_plugin_enabled` : un import au chargement du module créerait un import circulaire. Rendu vérifié sur l'instance déployée : og:image" content="http://localhost:8080/static/img/logo-512.jpg Tests : `test_og_image_url_is_absolute_via_base_url` vérifie le global ET l'absence de la forme relative dans les deux templates. Piège évité au passage — la forme absolue CONTIENT l'ancienne en sous-chaîne, donc l'assertion négative porte sur `content="/static/img/` (le guillemet avant la barre) et non sur l'URL complète. pytest 1413 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.1 e2e 14/14 verts sur l'instance redéployée
100 lines
3.5 KiB
Python
100 lines
3.5 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()
|
|
|
|
|
|
def _app_base_url() -> str:
|
|
from app.config import settings
|
|
|
|
return (settings.app_base_url or "").rstrip("/")
|
|
|
|
|
|
def _plugin_enabled(slug: str) -> bool:
|
|
from app.services.plugins import is_enabled
|
|
|
|
return is_enabled(slug)
|