184 lines
5.8 KiB
Go
184 lines
5.8 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
|
||
|
||
// From ist der Filter "nur, was dieser Zugang gelöscht hat" — leer, wenn
|
||
// alles angezeigt wird.
|
||
From 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)
|
||
from := r.URL.Query().Get("von")
|
||
|
||
entries, err := ws.Store.ListTrash(r.Context(), project.ID, from)
|
||
if err != nil {
|
||
s.serverError(w, "Papierkorb konnte nicht geladen werden", err)
|
||
return
|
||
}
|
||
|
||
s.render(w, r, "trash.html", http.StatusOK, pageData{
|
||
Title: "Papierkorb – " + project.Name,
|
||
Section: "papierkorb",
|
||
Flash: s.hintFrom(r),
|
||
Data: trashData{Entries: entries, From: from, RetentionDays: store.TrashRetentionDays},
|
||
})
|
||
}
|
||
|
||
// trashSelection 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 trashSelection(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.
|
||
seen := map[string]bool{}
|
||
var slugs []string
|
||
for _, slug := range r.Form["slugs"] {
|
||
if slug == "" || seen[slug] {
|
||
continue
|
||
}
|
||
seen[slug] = true
|
||
slugs = append(slugs, slug)
|
||
}
|
||
return slugs, nil
|
||
}
|
||
|
||
// backToTrash leitet mit einer Meldung auf die Papierkorb-Seite
|
||
// zurück — und behält dabei einen gesetzten Filter bei.
|
||
func (s *Server) backToTrash(w http.ResponseWriter, r *http.Request, projectSlug, message string) {
|
||
// url.Values baut eine Query ("a=1&b=2") und maskiert dabei jeden Wert
|
||
// korrekt — sicherer als Zeichenketten von Hand zusammenzusetzen.
|
||
values := url.Values{}
|
||
if from := r.FormValue("von"); from != "" {
|
||
values.Set("von", from)
|
||
}
|
||
if message != "" {
|
||
values.Set(hintParam, message)
|
||
values.Set(hintSigParam, s.signer.SignHint(message))
|
||
}
|
||
|
||
target := "/p/" + projectSlug + "/trash"
|
||
if len(values) > 0 {
|
||
target += "?" + values.Encode()
|
||
}
|
||
http.Redirect(w, r, target, 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 := trashSelection(r)
|
||
if err != nil {
|
||
s.serverError(w, "Formular konnte nicht gelesen werden", err)
|
||
return
|
||
}
|
||
if len(slugs) == 0 {
|
||
s.backToTrash(w, r, project.Slug, "Nichts ausgewählt.")
|
||
return
|
||
}
|
||
|
||
back, 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 back {
|
||
ws.Search.Refresh(r.Context(), project.ID, e.Slug)
|
||
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",
|
||
})
|
||
}
|
||
|
||
s.backToTrash(w, r, project.Slug, fmt.Sprintf("%d Eintrag/Einträge wiederhergestellt.", len(back)))
|
||
}
|
||
|
||
// 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 := trashSelection(r)
|
||
if err != nil {
|
||
s.serverError(w, "Formular konnte nicht gelesen werden", err)
|
||
return
|
||
}
|
||
if len(slugs) == 0 {
|
||
s.backToTrash(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.
|
||
inTrash, err := ws.Store.ListTrash(r.Context(), project.ID, "")
|
||
if err != nil {
|
||
s.serverError(w, "Papierkorb konnte nicht geladen werden", err)
|
||
return
|
||
}
|
||
existing := make(map[string]bool, len(inTrash))
|
||
for _, t := range inTrash {
|
||
existing[t.Slug] = true
|
||
}
|
||
|
||
count, 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 !existing[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",
|
||
})
|
||
}
|
||
|
||
s.backToTrash(w, r, project.Slug, fmt.Sprintf("%d Eintrag/Einträge endgültig gelöscht.", count))
|
||
}
|