Add phase 5: link import, Instagram fallback and share target
- SSRF-safe fetcher: public IPs only, connects to the checked address, redirects re-validated, size/time limits, honest user agent - schema.org/Recipe JSON-LD extraction (graphs, HowToSections, ISO durations, yields, keywords, images) without extra dependencies; AI fallback on plain page text when no structured recipe exists - Instagram: one login-free request to the public embed page; when Instagram withholds the caption, the import page explains how to paste the text or import a screenshot instead - Suggested source photo can be downloaded and attached on save - Text import works without AI (text becomes the description) - Installable PWA with manifest, icons and Android share target - Offline tests for SSRF rules, parsers, Instagram and import views Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1bf8ef8b65
commit
60785e2437
+21
-2
@@ -3,6 +3,7 @@ from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import transaction
|
||||
from django.db.models import Exists, OuterRef, Prefetch
|
||||
@@ -14,7 +15,7 @@ from django.views.decorators.http import require_GET, require_POST
|
||||
|
||||
from core import ratelimit
|
||||
|
||||
from . import ai, images
|
||||
from . import ai, fetch, images
|
||||
from .forms import CommentForm, PhotoUploadForm, RecipeForm, SearchForm
|
||||
from .models import Comment, Favorite, Recipe, RecipeImage, Tag
|
||||
|
||||
@@ -27,6 +28,19 @@ def _visible(request):
|
||||
return Recipe.objects.visible_to(request.user)
|
||||
|
||||
|
||||
def download_image(request, url):
|
||||
"""Fetch a suggested source photo (SSRF-safe). Returns an uploaded-file object or None."""
|
||||
try:
|
||||
result = fetch.fetch(url, accept="image/*", max_bytes=images.max_upload_bytes())
|
||||
except fetch.FetchError as error:
|
||||
messages.warning(request, f"Das Bild der Quelle konnte nicht geladen werden: {error.user_message}")
|
||||
return None
|
||||
if result.status != 200 or not result.content_type.lower().startswith("image/"):
|
||||
messages.warning(request, "Das Bild der Quelle konnte nicht geladen werden.")
|
||||
return None
|
||||
return SimpleUploadedFile("quelle.jpg", result.body, content_type=result.content_type)
|
||||
|
||||
|
||||
def attach_photos(request, recipe, files):
|
||||
"""Process uploaded files and attach them. Problems are reported as messages, not exceptions."""
|
||||
has_cover = recipe.images.filter(is_cover=True).exists()
|
||||
@@ -124,7 +138,12 @@ def recipe_create(request):
|
||||
form = RecipeForm(request.POST or None, request.FILES or None)
|
||||
if request.method == "POST" and form.is_valid():
|
||||
recipe = form.save(owner=request.user)
|
||||
attach_photos(request, recipe, form.cleaned_data.get("photos") or [])
|
||||
photos = list(form.cleaned_data.get("photos") or [])
|
||||
if form.cleaned_data.get("use_import_image") and form.cleaned_data.get("import_image_url"):
|
||||
downloaded = download_image(request, form.cleaned_data["import_image_url"])
|
||||
if downloaded:
|
||||
photos.insert(0, downloaded)
|
||||
attach_photos(request, recipe, photos[: settings.IMAGE_MAX_PER_RECIPE])
|
||||
messages.success(request, "Rezept gespeichert.")
|
||||
return redirect("recipes:detail", pk=recipe.pk)
|
||||
return _render_form(request, form)
|
||||
|
||||
Reference in New Issue
Block a user