Add confirmation dialogue for deleting recordings. Normally I hate those "Are your really sure" dialogues, but this is a good place for one because you don't want to accidently delete a recording with no backup.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
# shellcheck shell=bash disable=SC2034,SC2154
|
# shellcheck shell=bash disable=SC2034,SC2154
|
||||||
|
|
||||||
pkgname=skald-git
|
pkgname=skald-git
|
||||||
pkgver=0.0.0.r1552.ge31bffd
|
pkgver=0.0.0.r1553.g0c852e8
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc='Audio-only hall-based conferencing server'
|
pkgdesc='Audio-only hall-based conferencing server'
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
|
|||||||
+60
-13
@@ -689,17 +689,16 @@ func handleHallAction(w http.ResponseWriter, r *http.Request, hall string) {
|
|||||||
q := r.Form.Get("q")
|
q := r.Form.Get("q")
|
||||||
|
|
||||||
switch q {
|
switch q {
|
||||||
case "delete":
|
case "confirm-delete":
|
||||||
filename := r.Form.Get("filename")
|
filename, ok := recordingFilenameFromForm(w, r, hall)
|
||||||
if hall == "" || filename == "" {
|
if !ok {
|
||||||
http.Error(w, "No filename provided",
|
|
||||||
http.StatusBadRequest)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.ContainsRune(filename, '/') ||
|
serveDeleteRecordingConfirmation(w, hall, filename)
|
||||||
strings.ContainsRune(filename, filepath.Separator) {
|
return
|
||||||
http.Error(w, "Bad character in filename",
|
case "delete":
|
||||||
http.StatusBadRequest)
|
filename, ok := recordingFilenameFromForm(w, r, hall)
|
||||||
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := os.Remove(
|
err := os.Remove(
|
||||||
@@ -713,7 +712,7 @@ func handleHallAction(w http.ResponseWriter, r *http.Request, hall string) {
|
|||||||
httpError(w, err)
|
httpError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/recordings/"+hall+"/",
|
http.Redirect(w, r, recordingsHallPath(hall),
|
||||||
http.StatusSeeOther)
|
http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
@@ -721,6 +720,22 @@ func handleHallAction(w http.ResponseWriter, r *http.Request, hall string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recordingFilenameFromForm(w http.ResponseWriter, r *http.Request, hall string) (string, bool) {
|
||||||
|
filename := r.Form.Get("filename")
|
||||||
|
if hall == "" || filename == "" {
|
||||||
|
http.Error(w, "No filename provided",
|
||||||
|
http.StatusBadRequest)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if strings.ContainsRune(filename, '/') ||
|
||||||
|
strings.ContainsRune(filename, filepath.Separator) {
|
||||||
|
http.Error(w, "Bad character in filename",
|
||||||
|
http.StatusBadRequest)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return filename, true
|
||||||
|
}
|
||||||
|
|
||||||
func checkHallPermissions(w http.ResponseWriter, r *http.Request, hallname string) bool {
|
func checkHallPermissions(w http.ResponseWriter, r *http.Request, hallname string) bool {
|
||||||
user, pass, ok := r.BasicAuth()
|
user, pass, ok := r.BasicAuth()
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -758,6 +773,37 @@ func checkHallPermissions(w http.ResponseWriter, r *http.Request, hallname strin
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func recordingsHallPath(hall string) string {
|
||||||
|
parts := strings.Split(hall, "/")
|
||||||
|
for i, part := range parts {
|
||||||
|
parts[i] = url.PathEscape(part)
|
||||||
|
}
|
||||||
|
return "/recordings/" + strings.Join(parts, "/") + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveDeleteRecordingConfirmation(w http.ResponseWriter, hall string, filename string) {
|
||||||
|
hallPath := recordingsHallPath(hall)
|
||||||
|
escapedFilename := html.EscapeString(filename)
|
||||||
|
|
||||||
|
w.Header().Set("content-type", "text/html; charset=utf-8")
|
||||||
|
w.Header().Set("cache-control", "no-cache")
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "<!DOCTYPE html>\n<html><head>\n")
|
||||||
|
fmt.Fprintf(w, "<title>Delete recording %v</title>\n", escapedFilename)
|
||||||
|
fmt.Fprintf(w, "<link rel=\"stylesheet\" type=\"text/css\" href=\"/common.css\"/>")
|
||||||
|
fmt.Fprintf(w, "</head><body>\n")
|
||||||
|
fmt.Fprintf(w, "<h1>Delete recording?</h1>\n")
|
||||||
|
fmt.Fprintf(w, "<p>Delete %v? This cannot be undone.</p>\n", escapedFilename)
|
||||||
|
fmt.Fprintf(w,
|
||||||
|
"<form action=\"%v\" method=\"post\">"+
|
||||||
|
"<input type=\"hidden\" name=\"filename\" value=\"%v\">"+
|
||||||
|
"<button type=\"submit\" name=\"q\" value=\"delete\">Delete recording</button>"+
|
||||||
|
"</form>\n",
|
||||||
|
hallPath, escapedFilename)
|
||||||
|
fmt.Fprintf(w, "<p><a href=\"%v\">Cancel</a></p>\n", hallPath)
|
||||||
|
fmt.Fprintf(w, "</body></html>\n")
|
||||||
|
}
|
||||||
|
|
||||||
func serveHallRecordings(w http.ResponseWriter, r *http.Request, f *os.File, hall string) {
|
func serveHallRecordings(w http.ResponseWriter, r *http.Request, f *os.File, hall string) {
|
||||||
// read early, so we return permission errors to HEAD
|
// read early, so we return permission errors to HEAD
|
||||||
fis, err := f.Readdir(-1)
|
fis, err := f.Readdir(-1)
|
||||||
@@ -793,11 +839,12 @@ func serveHallRecordings(w http.ResponseWriter, r *http.Request, f *os.File, hal
|
|||||||
fi.Size(),
|
fi.Size(),
|
||||||
)
|
)
|
||||||
fmt.Fprintf(w,
|
fmt.Fprintf(w,
|
||||||
"<td><form action=\"/recordings/%v/\" method=\"post\">"+
|
"<td><form action=\"%v\" method=\"post\">"+
|
||||||
"<input type=\"hidden\" name=\"filename\" value=\"%v\">"+
|
"<input type=\"hidden\" name=\"filename\" value=\"%v\">"+
|
||||||
"<button type=\"submit\" name=\"q\" value=\"delete\">Delete</button>"+
|
"<button type=\"submit\" name=\"q\" value=\"confirm-delete\">Delete</button>"+
|
||||||
"</form></td></tr>\n",
|
"</form></td></tr>\n",
|
||||||
url.PathEscape(hall), fi.Name())
|
recordingsHallPath(hall),
|
||||||
|
html.EscapeString(fi.Name()))
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, "</table>\n")
|
fmt.Fprintf(w, "</table>\n")
|
||||||
fmt.Fprintf(w, "</body></html>\n")
|
fmt.Fprintf(w, "</body></html>\n")
|
||||||
|
|||||||
@@ -59,6 +59,61 @@ func TestRecordingsAuthenticationFailureMessage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHallRecordingsDeleteButtonOpensConfirmation(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
recordingName := `skald-recording-20260707-"010203".ogg`
|
||||||
|
err := os.WriteFile(filepath.Join(dir, recordingName), nil, 0600)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("write recording: %v", err)
|
||||||
|
}
|
||||||
|
f, err := os.Open(dir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("open recordings dir: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("GET", "/recordings/class/audio/", nil)
|
||||||
|
serveHallRecordings(w, r, f, "class/audio")
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, `action="/recordings/class/audio/"`) {
|
||||||
|
t.Fatalf("recordings list missing encoded hall action: %q", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `name="q" value="confirm-delete">Delete</button>`) {
|
||||||
|
t.Fatalf("delete button does not open confirmation: %q", body)
|
||||||
|
}
|
||||||
|
if strings.Contains(body, `name="q" value="delete">Delete</button>`) {
|
||||||
|
t.Fatalf("recordings list still has direct delete button: %q", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `value="skald-recording-20260707-"010203".ogg"`) {
|
||||||
|
t.Fatalf("recordings list did not escape filename value: %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHallRecordingsDeleteConfirmationRequiresSecondPost(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r := httptest.NewRequest("POST", "/recordings/class/audio/",
|
||||||
|
strings.NewReader("q=confirm-delete&filename=skald-recording-20260707-010203.ogg"))
|
||||||
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
handleHallAction(w, r, "class/audio")
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
if !strings.Contains(body, "<h1>Delete recording?</h1>") {
|
||||||
|
t.Fatalf("confirmation page missing heading: %q", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `action="/recordings/class/audio/"`) {
|
||||||
|
t.Fatalf("confirmation page missing encoded hall action: %q", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `name="q" value="delete">Delete recording</button>`) {
|
||||||
|
t.Fatalf("confirmation page missing final delete button: %q", body)
|
||||||
|
}
|
||||||
|
if !strings.Contains(body, `<a href="/recordings/class/audio/">Cancel</a>`) {
|
||||||
|
t.Fatalf("confirmation page missing cancel link: %q", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBase(t *testing.T) {
|
func TestBase(t *testing.T) {
|
||||||
a := []struct {
|
a := []struct {
|
||||||
p string
|
p string
|
||||||
|
|||||||
Reference in New Issue
Block a user