Datenschutz: Bild-Metadaten, private Vorschläge, fremde Bilder, API-Doku
- Uploads: Server entfernt EXIF/XMP/Text (GPS-Standort, Kamera, Uhrzeit) per Pillow; Drehung von Hochkant-Fotos wird vorher angewendet, Farbprofil bleibt; GIFs unverändert (neue Abhängigkeit pillow) - Produktvorschläge pro Liste (Tabelle list_products): Einträge privater Listen tauchen bei anderen weder in Vorschlägen noch beim Rezeptabgleich auf; "Vorschläge verwalten" löscht nur aus eigenen sichtbaren Listen - Artikel: Bilder nur noch aus den eigenen Uploads (kein Tracking-Pixel fremder Server); Bereinigung in htmlclean.py - /api/docs und /api/openapi.json nur noch im Entwicklungsmodus (RELOAD=1 oder P42_API_DOCS=1) - Migration Schritt 2: bestehende Vorschläge den Listen zuordnen, Artikel neu bereinigen, gespeicherte Bilder mit Metadaten säubern - Tests in test_datenschutz.py (u. a. Rezept-Zuordnung, Umstellung) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
419d200bf9
commit
bb20135876
11 files changed
+367
-61
No files matched your search
@@ -0,0 +1,54 @@
|
||||
"""Hochgeladene Bilder ohne Metadaten speichern.
|
||||
|
||||
Fotos vom Handy enthalten in den EXIF-Daten oft den Aufnahmeort (GPS), das Gerät und die Uhrzeit.
|
||||
Die Bilder sind für alle Konten sichtbar, deshalb wird alles außer den Bildpunkten entfernt.
|
||||
Die Drehung aus den EXIF-Daten wird vorher angewendet, damit Hochkant-Fotos richtig stehen bleiben.
|
||||
Nur das Farbprofil (ICC) bleibt erhalten, damit sich die Farben nicht verändern.
|
||||
|
||||
GIFs bleiben unverändert: Sie haben keine EXIF-/GPS-Daten, und neu speichern würde Animationen verschlechtern.
|
||||
"""
|
||||
import io
|
||||
|
||||
from PIL import Image, ImageOps
|
||||
|
||||
from .common import ValidationError
|
||||
|
||||
# Schutz vor "Dekompressionsbomben" (winzige Datei, riesiges Bild)
|
||||
Image.MAX_IMAGE_PIXELS = 60_000_000
|
||||
|
||||
|
||||
def hat_metadaten(data: bytes) -> bool:
|
||||
"""Enthält das Bild EXIF-, XMP- oder Text-Angaben (z. B. GPS, Kamera, Kommentar)?"""
|
||||
try:
|
||||
with Image.open(io.BytesIO(data)) as img:
|
||||
return bool(img.getexif()) or any(k in img.info for k in ("xmp", "XML:com.adobe.xmp", "comment", "exif")) \
|
||||
or bool(getattr(img, "text", None))
|
||||
except (OSError, ValueError, Image.DecompressionBombError):
|
||||
return False
|
||||
|
||||
|
||||
def ohne_metadaten(data: bytes, ext: str) -> bytes:
|
||||
"""Bild neu schreiben, ohne EXIF/XMP/Text-Angaben. ext: jpg, png, webp oder gif."""
|
||||
if ext == "gif":
|
||||
return data
|
||||
try:
|
||||
with Image.open(io.BytesIO(data)) as img:
|
||||
animated = getattr(img, "is_animated", False)
|
||||
icc = img.info.get("icc_profile")
|
||||
out = io.BytesIO()
|
||||
if ext == "webp" and animated:
|
||||
# Animierte WebP: alle Bilder übernehmen, Metadaten fallen weg
|
||||
img.save(out, "WEBP", save_all=True, quality=90, icc_profile=icc, duration=img.info.get("duration"), loop=img.info.get("loop", 0))
|
||||
return out.getvalue()
|
||||
img = ImageOps.exif_transpose(img)
|
||||
if ext == "jpg":
|
||||
if img.mode not in ("RGB", "L", "CMYK"):
|
||||
img = img.convert("RGB")
|
||||
img.save(out, "JPEG", quality=92, optimize=True, icc_profile=icc)
|
||||
elif ext == "png":
|
||||
img.save(out, "PNG", optimize=True, icc_profile=icc)
|
||||
else:
|
||||
img.save(out, "WEBP", quality=90, icc_profile=icc)
|
||||
return out.getvalue()
|
||||
except (OSError, ValueError, Image.DecompressionBombError):
|
||||
raise ValidationError("Das Bild konnte nicht gelesen werden oder ist zu groß.")
|
||||
Reference in new issue
Block a user