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.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", }) } 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)) }