feat(10.1-01): render header and form partials into an allowlisted node tree
- fields.yaml type: partial with a bare path name and config_list.yaml
headerPartial resolve to {ConfigDir}/_{name}.htm, parsed at boot; the
controller must implement pact.AdminPartialData
- html/template render against a curated view model, then x/net/html
ParseFragment and a tag, attribute and URL allowlist with 64 KiB, 2000-node
and depth-32 caps; the model type and trusted template types are refused
- GET .../partials/{name} with optional ?id= loaded through the form scope
- golang.org/x/net becomes a direct requirement (D-18), no new module
This commit is contained in:
@@ -130,6 +130,17 @@ func TestPhase10OpenAPIConformance(t *testing.T) {
|
||||
}
|
||||
return rec
|
||||
}, into[cabana.Envelope[cabana.AdminActionResult]]()},
|
||||
{"GET /{vendor}/{plugin}/{controller}/partials/{name}", 200, "cabana.Envelope-cabana_PartialView", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
|
||||
rec := e.send(t, http.MethodGet, fmt.Sprintf("/acme/conform/gadgets/partials/summary?id=%d", e.gadgetID), nil, true)
|
||||
if !strings.Contains(rec.Body.String(), `"text":"gadget-`+e.stamp+`"`) {
|
||||
t.Fatalf("form partial did not render the scoped record: %s", rec.Body.String())
|
||||
}
|
||||
header := e.send(t, http.MethodGet, "/acme/conform/gadgets/partials/stats", nil, true)
|
||||
if header.Code != http.StatusOK || !strings.Contains(header.Body.String(), `"class":"summer-stats"`) {
|
||||
t.Fatalf("header partial status=%d body=%s", header.Code, header.Body.String())
|
||||
}
|
||||
return rec
|
||||
}, into[cabana.Envelope[cabana.PartialView]]()},
|
||||
{"GET /{vendor}/{plugin}/{controller}", 200, "cabana.ListEnvelope-array_cabana_AdminRecord", func(t *testing.T, e *conformEnv) *httptest.ResponseRecorder {
|
||||
return e.send(t, http.MethodGet, "/acme/conform/gadgets?search="+e.stamp, nil, true)
|
||||
}, into[cabana.ListEnvelope[[]cabana.AdminRecord]]()},
|
||||
@@ -475,7 +486,23 @@ func (c conformController) AdminActions() []pact.AdminAction {
|
||||
},
|
||||
}}
|
||||
}
|
||||
func (conformController) AdminJS() []string { return []string{"assets/js/lookup.js"} }
|
||||
func (conformController) AdminJS() []string { return []string{"assets/js/lookup.js"} }
|
||||
|
||||
// PartialData supplies curated view models, never the gadget model itself.
|
||||
func (conformController) PartialData(_ context.Context, name string, record any) (any, error) {
|
||||
switch name {
|
||||
case "stats":
|
||||
return struct{ Total int }{Total: 1}, nil
|
||||
case "summary":
|
||||
view := struct{ Name string }{}
|
||||
if gadget, ok := record.(*conformGadget); ok && gadget != nil {
|
||||
view.Name = gadget.Name
|
||||
}
|
||||
return view, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown partial %s", name)
|
||||
}
|
||||
}
|
||||
func (conformController) AdminCSS() []string { return []string{"assets/css/gadgets.css"} }
|
||||
|
||||
func conformFS() fs.FS {
|
||||
@@ -488,6 +515,7 @@ recordUrl: acme/conform/gadgets/update/:id
|
||||
recordsPerPage: 20
|
||||
showCheckboxes: true
|
||||
filter: config_filter.yaml
|
||||
headerPartial: stats
|
||||
toolbar:
|
||||
buttons: [create, delete, recount]
|
||||
search:
|
||||
@@ -513,6 +541,12 @@ if (!customElements.get('acme-conform-lookup')) {
|
||||
}
|
||||
`),
|
||||
"assets/css/gadgets.css": file(`acme-conform-lookup button { font: inherit; }
|
||||
`),
|
||||
"controllers/gadgets/_stats.htm": file(`<dl class="summer-stats">
|
||||
<div class="summer-stat"><dt class="summer-stat__label">{{ trans "backend::lang.list.search" }}</dt><dd class="summer-stat__value">{{ .Data.Total }}</dd></div>
|
||||
</dl>
|
||||
`),
|
||||
"controllers/gadgets/_summary.htm": file(`<p>{{ .Data.Name }}</p>
|
||||
`),
|
||||
"controllers/gadgets/config_filter.yaml": file(`scopes:
|
||||
grouped:
|
||||
@@ -585,6 +619,10 @@ update:
|
||||
widget: acme-conform-lookup
|
||||
action: lookup
|
||||
fill: [name]
|
||||
summary:
|
||||
label: Summary
|
||||
type: partial
|
||||
path: summary
|
||||
`),
|
||||
"models/settings/fields.yaml": file(`fields:
|
||||
enabled:
|
||||
|
||||
Reference in New Issue
Block a user