package handler
import (
"html/template"
"net/http"
"os"
"path/filepath"
"codeberg.org/VARASYS/ZDDC/zddc/internal/config"
"codeberg.org/VARASYS/ZDDC/zddc/internal/zddc"
)
// editorView is the data passed to the editor template. Field naming is
// kept short for template ergonomics.
type editorView struct {
Path string
IsRoot bool
CanEdit bool
Exists bool
Email string
HasCustomCSS bool
File zddc.ZddcFile
EffectiveChain []chainEntry
ProfilePathPrefix string // /.profile
AssetsPathPrefix string // /.profile/zddc/assets
}
// serveZddcEditor renders the form-based .zddc editor at
// GET /.profile/zddc/edit?path=
. The form posts JSON back to
// /.profile/zddc?path=; the inline JS shim handles dynamic-row
// add/remove and surfaces field errors from the JSON response.
func serveZddcEditor(cfg config.Config, w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
email := EmailFromContext(r)
abs, err := resolvePath(cfg.Root, r.URL.Query().Get("path"))
if err != nil {
http.NotFound(w, r)
return
}
zf, err := zddc.ParseFile(filepath.Join(abs, ".zddc"))
if err != nil {
http.Error(w, "Cannot parse existing .zddc: "+err.Error(), http.StatusBadRequest)
return
}
exists := false
if _, err := os.Stat(filepath.Join(abs, ".zddc")); err == nil {
exists = true
}
chain, _ := zddc.EffectivePolicy(cfg.Root, abs)
dirs := chainDirs(cfg.Root, abs)
entries := make([]chainEntry, 0, len(chain.Levels))
for i, level := range chain.Levels {
levelDir := dirs[i]
levelExists := false
if _, err := os.Stat(filepath.Join(levelDir, ".zddc")); err == nil {
levelExists = true
}
entries = append(entries, chainEntry{
Dir: urlPathOf(cfg.Root, levelDir),
Exists: levelExists,
Title: level.Title,
ACL: level.ACL,
Admins: level.Admins,
})
}
view := editorView{
Path: urlPathOf(cfg.Root, abs),
IsRoot: abs == cfg.Root,
CanEdit: zddc.CanEditZddc(cfg.Root, abs, email),
Exists: exists,
Email: email,
HasCustomCSS: hasCustomProfileCSS(cfg.Root),
File: zf,
EffectiveChain: entries,
ProfilePathPrefix: ProfilePathPrefix,
AssetsPathPrefix: zddcAssetsPathPrefix,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
if err := editorTemplate.Execute(w, view); err != nil {
// Headers may already be flushed; best effort.
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// editorTemplate is the html/template body for the editor page.
//
// Style choices:
// - inline CSS uses the same custom-property naming as shared/base.css
// so a future server-side merge with shared/base.css remains trivial.
// - inline JS is one IIFE, ~80 lines, handling: add/remove row,
// collect-into-JSON-on-submit, render server-side field errors.
// - the form falls back to a plain HTTP POST (urlencoded) without JS;
// a tiny same-handler endpoint accepts urlencoded too. (V1: JS only;
// no-JS fallback is documented as a TODO in the file header.)
var editorTemplate = template.Must(template.New("editor").Parse(`
.zddc editor — {{ .Path }}
{{ if .HasCustomCSS }}{{ end }}
← profile /
root{{ if not .IsRoot }} / {{ .Path }}{{ end }}
.zddc editor
{{ if .IsRoot }}Editing the root /.zddc.{{ else }}Editing {{ .Path }}/.zddc.{{ end }} You are signed in as {{ .Email }}.
{{ if not .CanEdit }}
Read-only. You can view this file's contents and the inherited rules below, but you do not have permission to edit it. Subtree admins cannot edit the .zddc file that grants their own authority — only an admin from a higher level can.