Umsetzung der Ausbaustufen 2.1, 3.6, 3.8, 3.9, 4.1, 4.2, 4.4, 4.6, 5.1–5.4 (Details in AUSBAUSTUFEN.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
183 lines
5.9 KiB
Go
183 lines
5.9 KiB
Go
package web
|
||
|
||
// Papierkorb je Projekt (AUSBAUSTUFEN.md 3.9).
|
||
//
|
||
// Alle drei Wege verlangen Schreibrecht, auch das bloße Ansehen. Grund: Ein
|
||
// Eintrag landet oft genau deshalb im Papierkorb, weil er etwas enthielt, das
|
||
// dort nicht hingehört — ein versehentlich abgelegtes Passwort etwa. Wer nur
|
||
// lesen darf, soll das nicht über den Papierkorb doch noch zu sehen bekommen.
|
||
//
|
||
// Wiederherstellen und endgültig Löschen gibt es bewusst NUR hier, nicht über
|
||
// MCP: Ein fehlgeleiteter Agent soll seinen Schaden nicht selbst "vollenden"
|
||
// können, indem er nach dem Löschen auch noch den Papierkorb leert.
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
|
||
"munin/internal/store"
|
||
)
|
||
|
||
// trashData sind die Daten der Papierkorb-Seite.
|
||
type trashData struct {
|
||
Entries []store.TrashedEntry
|
||
|
||
// Von ist der Filter "nur, was dieser Zugang gelöscht hat" — leer, wenn
|
||
// alles angezeigt wird.
|
||
Von string
|
||
|
||
// Frist in Tagen, für den Hinweistext.
|
||
RetentionDays int
|
||
}
|
||
|
||
// handleTrashList zeigt den Papierkorb eines Projekts.
|
||
func (s *Server) handleTrashList(w http.ResponseWriter, r *http.Request) {
|
||
ws, _ := WorkspaceFrom(r)
|
||
project, _ := ProjectFrom(r)
|
||
von := r.URL.Query().Get("von")
|
||
|
||
eintraege, err := ws.Store.ListTrash(r.Context(), project.ID, von)
|
||
if err != nil {
|
||
s.serverError(w, "Papierkorb konnte nicht geladen werden", err)
|
||
return
|
||
}
|
||
|
||
s.render(w, r, "papierkorb.html", http.StatusOK, pageData{
|
||
Title: "Papierkorb – " + project.Name,
|
||
Bereich: "papierkorb",
|
||
Flash: r.URL.Query().Get("hinweis"),
|
||
Data: trashData{Entries: eintraege, Von: von, RetentionDays: store.TrashRetentionDays},
|
||
})
|
||
}
|
||
|
||
// papierkorbAuswahl liest die ausgewählten Bezeichner aus dem Formular.
|
||
//
|
||
// Das Formular schickt für jeden angehakten Eintrag ein eigenes Feld "slugs"
|
||
// — mehrere Felder mit demselben Namen. r.Form["slugs"] liefert sie alle als
|
||
// Liste; r.FormValue("slugs") gäbe nur das erste zurück.
|
||
func papierkorbAuswahl(r *http.Request) ([]string, error) {
|
||
if err := r.ParseForm(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Doppelte Angaben (etwa ein Kästchen plus derselbe Eintrag über "alle
|
||
// wiederherstellen") nur einmal behandeln — sonst stünde der Vorgang
|
||
// zweimal im Protokoll. Die map merkt sich, was schon übernommen wurde.
|
||
gesehen := map[string]bool{}
|
||
var slugs []string
|
||
for _, slug := range r.Form["slugs"] {
|
||
if slug == "" || gesehen[slug] {
|
||
continue
|
||
}
|
||
gesehen[slug] = true
|
||
slugs = append(slugs, slug)
|
||
}
|
||
return slugs, nil
|
||
}
|
||
|
||
// zurueckZumPapierkorb leitet mit einer Meldung auf die Papierkorb-Seite
|
||
// zurück — und behält dabei einen gesetzten Filter bei.
|
||
func zurueckZumPapierkorb(w http.ResponseWriter, r *http.Request, projektSlug, meldung string) {
|
||
// url.Values baut eine Query ("a=1&b=2") und maskiert dabei jeden Wert
|
||
// korrekt — sicherer als Zeichenketten von Hand zusammenzusetzen.
|
||
werte := url.Values{}
|
||
if von := r.FormValue("von"); von != "" {
|
||
werte.Set("von", von)
|
||
}
|
||
if meldung != "" {
|
||
werte.Set("hinweis", meldung)
|
||
}
|
||
|
||
ziel := "/p/" + projektSlug + "/trash"
|
||
if len(werte) > 0 {
|
||
ziel += "?" + werte.Encode()
|
||
}
|
||
http.Redirect(w, r, ziel, http.StatusSeeOther)
|
||
}
|
||
|
||
// handleTrashRestore holt die ausgewählten Einträge zurück.
|
||
func (s *Server) handleTrashRestore(w http.ResponseWriter, r *http.Request) {
|
||
ws, _ := WorkspaceFrom(r)
|
||
project, _ := ProjectFrom(r)
|
||
sess, _ := SessionFrom(r)
|
||
|
||
slugs, err := papierkorbAuswahl(r)
|
||
if err != nil {
|
||
s.serverError(w, "Formular konnte nicht gelesen werden", err)
|
||
return
|
||
}
|
||
if len(slugs) == 0 {
|
||
zurueckZumPapierkorb(w, r, project.Slug, "Nichts ausgewählt.")
|
||
return
|
||
}
|
||
|
||
zurueck, err := ws.Store.RestoreFromTrash(r.Context(), project.ID, slugs)
|
||
if err != nil {
|
||
s.serverError(w, "Einträge konnten nicht wiederhergestellt werden", err)
|
||
return
|
||
}
|
||
|
||
// Jeder zurückgeholte Eintrag kommt wieder in den Suchindex und bekommt
|
||
// eine eigene Protokollzeile — so lässt sich im Protokoll nach einem
|
||
// einzelnen Bezeichner filtern und sein ganzer Weg nachvollziehen.
|
||
for _, e := range zurueck {
|
||
ws.Search.Update(project.ID, e)
|
||
ws.Store.LogAudit(r.Context(), store.AuditEvent{
|
||
ProjectID: project.ID, APIKeyID: sess.Key.ID, Actor: sess.ActorName(),
|
||
Action: store.ActionEntryRestore, EntrySlug: e.Slug, Source: store.SourceWeb,
|
||
Detail: "aus dem Papierkorb",
|
||
})
|
||
}
|
||
|
||
zurueckZumPapierkorb(w, r, project.Slug, fmt.Sprintf("%d Eintrag/Einträge wiederhergestellt.", len(zurueck)))
|
||
}
|
||
|
||
// handleTrashPurge löscht die ausgewählten Einträge endgültig.
|
||
func (s *Server) handleTrashPurge(w http.ResponseWriter, r *http.Request) {
|
||
ws, _ := WorkspaceFrom(r)
|
||
project, _ := ProjectFrom(r)
|
||
sess, _ := SessionFrom(r)
|
||
|
||
slugs, err := papierkorbAuswahl(r)
|
||
if err != nil {
|
||
s.serverError(w, "Formular konnte nicht gelesen werden", err)
|
||
return
|
||
}
|
||
if len(slugs) == 0 {
|
||
zurueckZumPapierkorb(w, r, project.Slug, "Nichts ausgewählt.")
|
||
return
|
||
}
|
||
|
||
// Vorher festhalten, welche Bezeichner wirklich im Papierkorb liegen —
|
||
// nur die werden gelöscht und nur die gehören ins Protokoll. Eine
|
||
// untergeschobene Angabe taucht so auch dort nicht auf.
|
||
imPapierkorb, err := ws.Store.ListTrash(r.Context(), project.ID, "")
|
||
if err != nil {
|
||
s.serverError(w, "Papierkorb konnte nicht geladen werden", err)
|
||
return
|
||
}
|
||
vorhanden := make(map[string]bool, len(imPapierkorb))
|
||
for _, t := range imPapierkorb {
|
||
vorhanden[t.Slug] = true
|
||
}
|
||
|
||
anzahl, err := ws.Store.PurgeFromTrash(r.Context(), project.ID, slugs)
|
||
if err != nil {
|
||
s.serverError(w, "Einträge konnten nicht endgültig gelöscht werden", err)
|
||
return
|
||
}
|
||
|
||
for _, slug := range slugs {
|
||
if !vorhanden[slug] {
|
||
continue
|
||
}
|
||
ws.Store.LogAudit(r.Context(), store.AuditEvent{
|
||
ProjectID: project.ID, APIKeyID: sess.Key.ID, Actor: sess.ActorName(),
|
||
Action: store.ActionEntryPurge, EntrySlug: slug, Source: store.SourceWeb,
|
||
Detail: "von Hand aus dem Papierkorb",
|
||
})
|
||
}
|
||
|
||
zurueckZumPapierkorb(w, r, project.Slug, fmt.Sprintf("%d Eintrag/Einträge endgültig gelöscht.", anzahl))
|
||
}
|