🔒 Repository is read-only – file editing is disabled.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
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))