import hashlib
import re
import uuid
from datetime import datetime, timedelta, timezone
from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, UploadFile
from pydantic import BaseModel
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from api.depends import get_session, verify_identity_admin
from database.site_revision import bump_site_revision
from api.v2.schemas import WebBlockResponse, WebPageResponse, WebPageUpdate, WebTheme
from api.v2.schemas.web import (
WebPageSaveResponse,
WebPageThemeResponse,
WebPageThemeUpdate,
WebPageVariantCreate,
WebPageVariantSummary,
WebPageVariantUpdate,
WebPageVariantsResponse,
WebUploadResponse,
)
from database.models import (
WebBlock,
WebCustomElementBuild,
WebErrorReport,
WebFlow,
WebFlowEvent,
WebPage,
WebPageVariant,
WebPageVariantBlock,
WebTheme as WebThemeModel,
)
from logger import logger
UPLOAD_DIR = Path("static/web_uploads")
ALLOWED_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".mp4", ".webm"})
MAX_FILE_SIZE = 100 * 1024 * 1024
_IMAGE_RESIZE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".webp"})
_IMAGE_MAX_SIDE = 2048
_IMAGE_JPEG_QUALITY = 85
_IMAGE_WEBP_QUALITY = 85
_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9\-]*$")
EXTENSION_CONTENT_TYPES: dict[str, frozenset[str]] = {
".png": frozenset({"image/png"}),
".jpg": frozenset({"image/jpeg"}),
".jpeg": frozenset({"image/jpeg"}),
".gif": frozenset({"image/gif"}),
".webp": frozenset({"image/webp"}),
".svg": frozenset({"image/svg+xml", "text/xml", "application/xml", "text/plain"}),
".mp4": frozenset({"video/mp4"}),
".webm": frozenset({"video/webm"}),
}
def _optimize_image_bytes(data: bytes, ext: str) -> bytes:
"""Уменьшает большие картинки до _IMAGE_MAX_SIDE и пережимает с разумным качеством."""
try:
from io import BytesIO
from PIL import Image, ImageOps
with Image.open(BytesIO(data)) as img:
img = ImageOps.exif_transpose(img)
w, h = img.size
if max(w, h) <= _IMAGE_MAX_SIDE and len(data) < 500_000:
return data
if max(w, h) > _IMAGE_MAX_SIDE:
img.thumbnail((_IMAGE_MAX_SIDE, _IMAGE_MAX_SIDE), Image.Resampling.LANCZOS)
buffer = BytesIO()
save_kwargs: dict = {}
if ext in (".jpg", ".jpeg"):
if img.mode not in ("RGB", "L"):
img = img.convert("RGB")
save_kwargs = {"format": "JPEG", "quality": _IMAGE_JPEG_QUALITY, "optimize": True, "progressive": True}
elif ext == ".webp":
save_kwargs = {"format": "WEBP", "quality": _IMAGE_WEBP_QUALITY, "method": 6}
elif ext == ".png":
save_kwargs = {"format": "PNG", "optimize": True}
else:
return data
img.save(buffer, **save_kwargs)
optimized = buffer.getvalue()
return optimized if len(optimized) < len(data) else data
except Exception:
return data
def _sanitize_svg(data: bytes) -> bytes:
import re as _re
text = data.decode("utf-8", errors="replace")
text = _re.sub(r"", "", text, flags=_re.DOTALL | _re.IGNORECASE)
text = _re.sub(r"", "", text, flags=_re.DOTALL | _re.IGNORECASE)
text = _re.sub(r"\bon\w+\s*=\s*[\"'][^\"']*[\"']", "", text, flags=_re.IGNORECASE)
text = _re.sub(r"\bon\w+\s*=\s*\S+", "", text, flags=_re.IGNORECASE)
text = _re.sub(r"(?:href|xlink:href)\s*=\s*[\"']\s*javascript:[^\"']*[\"']", "", text, flags=_re.IGNORECASE)
text = _re.sub(r"(?:href|xlink:href)\s*=\s*[\"']\s*data:\s*text/html[^\"']*[\"']", "", text, flags=_re.IGNORECASE)
text = _re.sub(r"(?:href|xlink:href)\s*=\s*[\"']\s*vbscript:[^\"']*[\"']", "", text, flags=_re.IGNORECASE)
text = _re.sub(r"]*>.*?", "", text, flags=_re.DOTALL | _re.IGNORECASE)
text = _re.sub(r"", "", text, flags=_re.DOTALL | _re.IGNORECASE)
text = _re.sub(r"