Ga naar hoofdinhoud

HTML van een bezoeker: templates escapen vanzelf

Met escape uit les 2 moet je aan elke plek denken, en in de "Er gaat iets mis" van die les zag je hoe makkelijk je er een vergeet. Een template van Jinja doet het voor je: alles tussen {{ }} escapet hij, zonder dat je erom vraagt.

De lijst met een template​

Maak templates/berichten_lijst.html, zoals in het recept Verversen van het htmx-overzicht:

<ul>
{% for bericht in berichten %}
<li>{{ bericht.naam }}: {{ bericht.bericht }}</li>
{% endfor %}
</ul>

Het endpoint geeft nu het template terug, in plaats van zelf HTML te bouwen:

@app.get("/berichten/lijst")
async def berichten_lijst(request: Request):
with SqliteDict("gastenboek.db") as db:
alle_berichten = list(db.values())
return templates.TemplateResponse(request, "berichten_lijst.html", {"berichten": alle_berichten})
  1. Regel 2:

    Een TemplateResponse heeft de request nodig, net als in de andere lessen met templates.

  2. Regel 5:

    Geen f-string en geen escape meer: de berichten gaan naar het template, en Jinja zet ze erin.

Zet bovenaan main.py wat daarvoor nodig is:

from fastapi import Request
from fastapi.templating import Jinja2Templates

templates = Jinja2Templates(directory="templates")

Het antwoord na het versturen blijft één zin in een f-string, met escape(naam). Voor één zin is een template wat veel.

Doe de test opnieuw​

Gooi gastenboek.db weg, start de server opnieuw, en vul als Sara weer <b>vet</b> in. lijst.py uit les 1 geeft nu:

<ul>

<li>Sara: &lt;b&gt;vet&lt;/b&gt;</li>

</ul>

De witruimte komt uit het template: de regels met {% for %} en {% endfor %} worden elk een regel met alleen vier spaties. Wat Sara typte staat er, maar als tekst, net als met escape.

Er gaat iets mis​

Je noemt het template berichten_lijst.jinja, en <b>vet</b> is weer vet.

Oorzaak: Jinja escapet in FastAPI alleen templates die op .html, .htm of .xml eindigen. Een andere extensie zet het escapen stil uit: geen foutmelding, geen waarschuwing.

Oplossing: houd je templates op .html, en pas de naam in TemplateResponse mee aan.

Opdrachten​

Opdracht 1: Modify - Namen vet, veilig​

Maak in de lijst de naam van elke schrijver vet, zonder dat een bezoeker zelf HTML kan invullen.

Tip

De <b> mag in het template staan. Het is jouw HTML, niet die van de bezoeker.

Antwoord
<ul>
{% for bericht in berichten %}
<li><b>{{ bericht.naam }}</b>: {{ bericht.bericht }}</li>
{% endfor %}
</ul>

De tags komen van jou, de inhoud van de bezoeker. Jinja escapet alleen wat tussen {{ }} staat, dus de naam blijft tekst en jouw <b> blijft HTML.

Opdracht 2: Investigate - Waarom nog escape?​

De lijst gebruikt nu een template. Waarom staat er in gastenboek_opslaan nog steeds escape(naam)?

Antwoord

Omdat dat antwoord geen template is maar een f-string in een HTMLResponse. Een template escapet alleen wat door het template gaat. Haal je daar escape weg, dan is de naam na het versturen weer HTML.

Zo ziet je main.py er nu uit
import time
from html import escape

from fastapi import FastAPI, Form, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlitedict import SqliteDict

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")

@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...), bericht: str = Form(...)):
with SqliteDict("gastenboek.db") as db:
db[f"bericht_{time.time_ns()}"] = {"naam": naam, "bericht": bericht}
db.commit()
return HTMLResponse(f"Bedankt, {escape(naam)}. Je bericht staat in het gastenboek.")

@app.get("/berichten/lijst")
async def berichten_lijst(request: Request):
with SqliteDict("gastenboek.db") as db:
alle_berichten = list(db.values())
return templates.TemplateResponse(request, "berichten_lijst.html", {"berichten": alle_berichten})

Naast main.py staan static/gastenboek.html, static/js/htmx.min.js en templates/berichten_lijst.html.

Door naar les 4: |safe, de bescherming uitzetten.