import os, json, re, uuid from datetime import datetime from pathlib import Path from flask import Blueprint, render_template, request, redirect, flash submit_bp = Blueprint("submit", __name__) PENDING_DIR = Path("/var/lib/pagan-web/pending") # Tylko kategorie obecne w drzewie recipes/ (core, gui, utils) – reszta # została skonsolidowana podczas migracji NuTyX -> PAGBUILD.yaml. ALLOWED_CATS = {"core", "gui", "utils"} NAME_RE = re.compile(r"^[a-z0-9][a-z0-9+_.-]*$") VER_RE = re.compile(r"^[0-9][a-zA-Z0-9._+-]*$") URL_RE = re.compile(r"^https?://\S+$") ITEM_RE = re.compile(r"^[a-zA-Z0-9+_.-]+$") def _validate(data): """Walidacja formularza – twarde reguły, brak możliwości wstrzyknięcia.""" errors = [] pkgname = (data.get("pkgname") or "").strip() if not NAME_RE.match(pkgname): errors.append("Nazwa pakietu: tylko [a-z0-9+_.-], musi zaczynać się literą/cyfrą") pkgver = (data.get("pkgver") or "").strip() if not VER_RE.match(pkgver): errors.append("Wersja: tylko [0-9a-zA-Z._+-], musi zaczynać się cyfrą") pkgrel = (data.get("pkgrel") or "1").strip() if not re.match(r"^\d+$", pkgrel): errors.append("pkgrel: tylko liczba całkowita") for f, label in (("pkgdesc", "Opis"), ("url", "URL projektu")): v = (data.get(f) or "").strip() if not v: errors.append(f"{label}: wymagany") elif len(v) > 300: errors.append(f"{label}: maksymalnie 300 znaków") if data.get("url") and not URL_RE.match((data.get("url") or "").strip()): errors.append("URL projektu: musi zaczynać się od http:// lub https://") srcs = [s.strip() for s in (data.get("source") or "").splitlines() if s.strip()] if not srcs: errors.append("Źródło (source): wymagany przynajmniej jeden URL") for s in srcs: if not URL_RE.match(s): errors.append(f"Źródło: nieprawidłowy URL: {s[:60]}") if len(s) > 500: errors.append(f"Źródło: URL za długi: {s[:60]}...") for field, label in (("build", "build"), ("package", "package")): v = data.get(field) or "" if not v.strip(): errors.append(f"Faza {label}: wymagana") elif len(v) > 8000: errors.append(f"Faza {label}: maksymalnie 8000 znaków") cat = (data.get("category") or "").strip() if cat not in ALLOWED_CATS: errors.append("Nieprawidłowa kategoria") for field, label in (("depends", "depends"), ("makedepends", "makedepends"), ("license", "license")): for it in (data.get(field) or "").split(","): it = it.strip() if it and not ITEM_RE.match(it): errors.append(f"{label}: nieprawidłowa wartość: {it[:40]}") return errors @submit_bp.route("/submit-recipe", methods=["GET", "POST"]) def submit_recipe(): if request.method == "POST": data = { "pkgname": (request.form.get("pkgname") or "").strip(), "pkgver": (request.form.get("pkgver") or "").strip(), "pkgrel": (request.form.get("pkgrel") or "1").strip(), "pkgdesc": (request.form.get("pkgdesc") or "").strip(), "url": (request.form.get("url") or "").strip(), "category": (request.form.get("category") or "libs").strip(), "license": (request.form.get("license") or "").strip(), "depends": (request.form.get("depends") or "").strip(), "makedepends": (request.form.get("makedepends") or "").strip(), "source": (request.form.get("source") or "").strip(), "sha256sums": (request.form.get("sha256sums") or "").strip(), "build": (request.form.get("build") or "").strip(), "package": (request.form.get("package") or "").strip(), } errors = _validate(data) if errors: for e in errors: flash(f"❌ {e}", "error") return render_template("submit_recipe.html", data=data, cats=sorted(ALLOWED_CATS)) if request.form.get("terms") != "on": flash("❌ Wymagana akceptacja regulaminu zgłaszania receptur.", "error") return render_template("submit_recipe.html", data=data, cats=sorted(ALLOWED_CATS)) rid = uuid.uuid4().hex[:12] PENDING_DIR.mkdir(parents=True, exist_ok=True) rec = { "id": rid, "status": "pending", "created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "terms_accepted": True, "data": data, } (PENDING_DIR / f"{rid}.json").write_text(json.dumps(rec, indent=2, ensure_ascii=False)) # Audit log try: import time as _t ip = request.headers.get("X-Real-IP", request.remote_addr) or "?" user = session.get("username", "?") with open("/var/lib/pagan-web/audit.log", "a") as af: af.write(f"{_t.strftime('%Y-%m-%d %H:%M:%S')} [{user}] {ip} SUBMIT_RECIPE {data['pkgname']}-{data['pkgver']}\n") except Exception: pass flash(f"✅ Zgłoszenie #{rid} przyjęte! Czeka na zatwierdzenie przez administratora.", "ok") return redirect("/submit-recipe") return render_template("submit_recipe.html", data={}, cats=sorted(ALLOWED_CATS))