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:
@@ -70,6 +70,9 @@ func compileExtension(pluginID string, cc *CompiledController, fsys fs.FS) error
|
||||
if err := compileClientAssets(pluginID, cc, fsys); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compilePartials(pluginID, cc, fsys); err != nil {
|
||||
return err
|
||||
}
|
||||
if cc.Form == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -115,6 +118,55 @@ func compileExtension(pluginID string, cc *CompiledController, fsys fs.FS) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// compilePartials reads and parses every partial the controller declares:
|
||||
// config_list.yaml headerPartial and each `type: partial` field's path, both
|
||||
// resolving to {ConfigDir}/_{name}.htm. A missing or unparsable template, or a
|
||||
// controller without pact.AdminPartialData, fails boot (D-11).
|
||||
func compilePartials(pluginID string, cc *CompiledController, fsys fs.FS) error {
|
||||
id := cc.Controller.ID()
|
||||
var names []string
|
||||
formNames := map[string]bool{}
|
||||
if cc.List != nil && cc.List.HeaderPartial != "" {
|
||||
names = append(names, cc.List.HeaderPartial)
|
||||
}
|
||||
if cc.Form != nil {
|
||||
for _, field := range cc.Form.Fields {
|
||||
if field.Type == "partial" {
|
||||
names = append(names, field.Path)
|
||||
formNames[field.Path] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
dir := strings.Trim(path.Clean(cc.Controller.ConfigDir()), "/")
|
||||
if dir == "." || strings.HasPrefix(dir, "..") {
|
||||
return bootErr(pluginID, id, cc.Controller.ConfigDir(), fmt.Errorf("config directory escapes the plugin"))
|
||||
}
|
||||
partials := map[string]*compiledPartial{}
|
||||
for _, name := range names {
|
||||
if _, done := partials[name]; done {
|
||||
continue
|
||||
}
|
||||
file := path.Join(dir, "_"+name+".htm")
|
||||
if provider, ok := cc.Controller.(pact.AdminPartialData); !ok || provider == nil {
|
||||
return bootErr(pluginID, id, file, fmt.Errorf("partial %s needs the controller to implement pact.AdminPartialData", name))
|
||||
}
|
||||
src, err := readAsset(fsys, file)
|
||||
if err != nil {
|
||||
return bootErr(pluginID, id, file, fmt.Errorf("partial %s: template is not in the plugin's embedded files: %w", name, err))
|
||||
}
|
||||
compiled, err := parsePartial(name, src)
|
||||
if err != nil {
|
||||
return bootErr(pluginID, id, file, fmt.Errorf("partial %s: %w", name, err))
|
||||
}
|
||||
partials[name] = compiled
|
||||
}
|
||||
cc.partials, cc.formPartials = partials, formNames
|
||||
return nil
|
||||
}
|
||||
|
||||
// compileClientAssets reads and hashes the files a controller declares
|
||||
// through pact.AdminClientAssets. Each path must be clean, live under
|
||||
// assets/, carry a JS (.js, .mjs) or CSS (.css) extension and exist in the
|
||||
|
||||
Reference in New Issue
Block a user