package gonexapp import ( "html/template" "net/http" ) // shellCSP lets the ExApp iframe render this document. AppAPI blanks a frame // whose response omits frame-ancestors. const shellCSP = "default-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; font-src 'self' data:" // Item is one App navigation entry. The ExApp chooses the ids and labels. // Children may nest. The shell does not interpret the ids. // Icon is an optional same-origin image URL. NextcloudIcons names the core SVGs. type Item struct { ID string Label string Icon string Children []Item } // AppNavigation is the Files-style shell an ExApp mounts. // Items, Page, and DefaultID are required. Header and MissingMessage are // optional. SelectKey defaults to "item". Every other query parameter, // including the Visit folder, is copied onto the corrected address. // An ExApp that never calls Handler keeps its own page. type AppNavigation struct { Items func(*http.Request) []Item Page func(*http.Request, string) (string, bool) Header func(*http.Request) string DefaultID string MissingMessage string SelectKey string // Title is the document title. Empty means "App navigation". Title string // DisableTheme skips the Nextcloud theme stylesheets. The zero value // loads the active theme. DisableTheme bool } // Handler serves the shell. The selected item is the SelectKey query parameter. // A missing item renders DefaultID, includes MissingMessage, and publishes the // corrected query on data-address so the page can replace the address. // Every parent starts expanded. Folding is client state for this document only. func (n AppNavigation) Handler() http.Handler { return http.HandlerFunc(n.serve) } func (n AppNavigation) selectKey() string { if n.SelectKey != "" { return n.SelectKey } return "item" } func (n AppNavigation) serve(w http.ResponseWriter, r *http.Request) { var items []Item if n.Items != nil { items = n.Items(r) } asked := r.URL.Query().Get(n.selectKey()) selected := n.DefaultID missing := false if asked != "" { if _, ok := findItem(items, asked); ok { selected = asked } else { missing = true } } page := "" if n.Page != nil { body, ok := n.Page(r, selected) if !ok && selected != n.DefaultID { missing = true selected = n.DefaultID body, ok = n.Page(r, selected) } if ok { page = body } } header := "" if n.Header != nil { header = n.Header(r) } msg := "" if missing { msg = n.MissingMessage } title := n.Title if title == "" { title = "App navigation" } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Content-Security-Policy", shellCSP) w.WriteHeader(http.StatusOK) // Status is already committed; a template error cannot change the response. _ = shellTmpl.Execute(w, shellView{ Selected: selected, Address: n.address(r, selected), Header: template.HTML(header), Items: viewItems(r, items, selected, n.address), Page: template.HTML(page), Missing: msg, Title: title, Theme: !n.DisableTheme, }) } func (n AppNavigation) address(r *http.Request, id string) string { q := r.URL.Query() q.Set(n.selectKey(), id) return "?" + q.Encode() } func findItem(items []Item, id string) (Item, bool) { for _, it := range items { if it.ID == id { return it, true } if child, ok := findItem(it.Children, id); ok { return child, true } } return Item{}, false } type shellView struct { Selected string Address string Header template.HTML Items []itemView Page template.HTML Missing string Title string Theme bool } type itemView struct { ID string Label string Icon string Href string Current bool HasChildren bool Children []itemView } func viewItems(r *http.Request, items []Item, selected string, href func(*http.Request, string) string) []itemView { out := make([]itemView, 0, len(items)) for _, it := range items { children := viewItems(r, it.Children, selected, href) out = append(out, itemView{ ID: it.ID, Label: it.Label, Icon: it.Icon, Href: href(r, it.ID), Current: it.ID == selected, HasChildren: len(children) > 0, Children: children, }) } return out } var shellTmpl = template.Must(template.New("shell").Parse(`