Jul 24 - Update Rich-text editor to support uploading images and inserting table
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import hmac
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
from functools import wraps
|
||||
|
||||
from flask import (
|
||||
@@ -17,6 +19,26 @@ auth_log = logging.getLogger("jqc.auth")
|
||||
|
||||
MEDIA_TYPES = ("none", "image", "video", "embed")
|
||||
|
||||
# In-body image uploads (rich-text editor). Extension allowlist plus a
|
||||
# magic-byte sniff so a renamed file can't slip a non-image through. SVG is
|
||||
# deliberately excluded (it can carry script). 8 MB cap.
|
||||
ALLOWED_IMAGE_EXT = {"png", "jpg", "jpeg", "gif", "webp"}
|
||||
MAX_UPLOAD_BYTES = 8 * 1024 * 1024
|
||||
|
||||
|
||||
def _sniff_image(head):
|
||||
"""Return a canonical extension if `head` (first bytes of a file) looks like
|
||||
a supported image, else None."""
|
||||
if head[:8] == b"\x89PNG\r\n\x1a\n":
|
||||
return "png"
|
||||
if head[:3] == b"\xff\xd8\xff":
|
||||
return "jpg"
|
||||
if head[:6] in (b"GIF87a", b"GIF89a"):
|
||||
return "gif"
|
||||
if head[:4] == b"RIFF" and head[8:12] == b"WEBP":
|
||||
return "webp"
|
||||
return None
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ auth
|
||||
def login_required(view):
|
||||
@@ -268,6 +290,40 @@ def reorder():
|
||||
return jsonify(ok=True)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ uploads
|
||||
@admin_bp.route("/upload", methods=["POST"])
|
||||
@login_required
|
||||
def upload():
|
||||
"""Store an image dropped/picked in the rich-text editor and return its
|
||||
public URL as JSON: {"url": "/static/uploads/<name>"}. CSRF is enforced by
|
||||
the global CSRFProtect via the X-CSRFToken header the editor sends."""
|
||||
file = request.files.get("file")
|
||||
if file is None or not file.filename:
|
||||
return jsonify(error="No file provided."), 400
|
||||
|
||||
if request.content_length and request.content_length > MAX_UPLOAD_BYTES:
|
||||
return jsonify(error="File too large (max 8 MB)."), 413
|
||||
|
||||
ext = file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
|
||||
if ext not in ALLOWED_IMAGE_EXT:
|
||||
return jsonify(error="Unsupported file type."), 400
|
||||
|
||||
# Verify the bytes actually look like an image, not just the name.
|
||||
head = file.stream.read(12)
|
||||
file.stream.seek(0)
|
||||
sniffed = _sniff_image(head)
|
||||
if sniffed is None:
|
||||
return jsonify(error="File is not a valid image."), 400
|
||||
|
||||
upload_dir = os.path.join(current_app.static_folder, "uploads")
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
name = f"{uuid.uuid4().hex}.{sniffed}"
|
||||
file.save(os.path.join(upload_dir, name))
|
||||
|
||||
log_action(session.get("admin"), "create", "upload", None, name)
|
||||
return jsonify(url=url_for("static", filename=f"uploads/{name}"))
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ sections
|
||||
@admin_bp.route("/section/new", methods=["GET", "POST"])
|
||||
@admin_bp.route("/section/<int:section_id>", methods=["GET", "POST"])
|
||||
|
||||
Reference in New Issue
Block a user