feat(checks): add check 28 — public page handlers inject admin toolbar
New AST check scans handler render functions that build site settings (getSiteSettings + doc["site_settings"]) and fails if they don't also set siteSettings["toolbar"], so admin-toolbar injection can't silently regress when a new public page type is added. - toolbar.go: AST scanner; exempts renderListingPage (auto SEO pages) and renderVersionPreview (carries its own preview banner). - check_toolbar.go: registration at Seq 280 (ID "28"). - registry_test.go: +1 expected check; golden fixtures regenerated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7599fff971
commit
c048766075
37
check_toolbar.go
Normal file
37
check_toolbar.go
Normal file
@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func init() {
|
||||
register(Check{
|
||||
Seq: 280,
|
||||
ID: "28",
|
||||
Title: "Public page handlers inject admin toolbar data",
|
||||
Run: func(ctx *ScanContext, rep *Reporter) {
|
||||
fmt.Println("=== Check 28: Public page handlers inject admin toolbar data ===")
|
||||
var violations []toolbarViolation
|
||||
for _, target := range ctx.backendTargets {
|
||||
for _, v := range checkToolbarInjection(target.root) {
|
||||
v.file = prefixDisplayPath(target.display, v.file)
|
||||
violations = append(violations, v)
|
||||
}
|
||||
}
|
||||
if len(violations) > 0 {
|
||||
fmt.Printf(" FAIL: %d public page handler(s) missing toolbar injection:\n", len(violations))
|
||||
for _, v := range violations {
|
||||
fmt.Printf(" %s:%d func %s — missing siteSettings[\"toolbar\"] for admin toolbar\n", v.file, v.line, v.funcName)
|
||||
}
|
||||
fmt.Println("\n Fix: Add auth check + toolbar injection between getSiteSettings() and doc[\"site_settings\"]:")
|
||||
fmt.Println(" if claims, ok := auth.GetUserFromContext(ctx); ok && claims != nil {")
|
||||
fmt.Println(" toolbarData := h.buildToolbarData(ctx, r, page, systemTemplate, pageTemplateKey)")
|
||||
fmt.Println(" siteSettings[\"toolbar\"] = toolbarData")
|
||||
fmt.Println(" }")
|
||||
rep.Fail()
|
||||
} else {
|
||||
fmt.Println(" OK: All public page handlers inject admin toolbar data")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
},
|
||||
})
|
||||
}
|
||||
1
main.go
1
main.go
@ -33,6 +33,7 @@
|
||||
// 25. sqlc compile and buf generate must succeed for scanned roots that define them
|
||||
// 26. Orchestrator backend tests must pass when checking the core BlockNinja repo
|
||||
// 27. No hand-rolled HTML sanitization — use bluemonday
|
||||
// 28. Public page handlers inject admin toolbar data
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@ -18,6 +18,7 @@ func TestRegistryOrder(t *testing.T) {
|
||||
"1", "2", "2b", "2c", "2d", "2e", "2f", "3", "3b", "4",
|
||||
"5", "6", "7", "8", "9", "10", "10b", "10disc", "11", "12",
|
||||
"13", "14", "15", "16", "17", "18", "19", "20", "21", "22",
|
||||
"28",
|
||||
}
|
||||
|
||||
ordered := make([]Check, len(registry))
|
||||
|
||||
3
testdata/golden/clean/expected.stdout
vendored
3
testdata/golden/clean/expected.stdout
vendored
@ -87,3 +87,6 @@
|
||||
|
||||
=== Check 22: No hand-rolled HTML sanitization (use bluemonday) ===
|
||||
OK: No hand-rolled HTML sanitization detected
|
||||
=== Check 28: Public page handlers inject admin toolbar data ===
|
||||
OK: All public page handlers inject admin toolbar data
|
||||
|
||||
|
||||
3
testdata/golden/nomod/expected.stdout
vendored
3
testdata/golden/nomod/expected.stdout
vendored
@ -108,3 +108,6 @@
|
||||
|
||||
=== Check 22: No hand-rolled HTML sanitization (use bluemonday) ===
|
||||
OK: No hand-rolled HTML sanitization detected
|
||||
=== Check 28: Public page handlers inject admin toolbar data ===
|
||||
OK: All public page handlers inject admin toolbar data
|
||||
|
||||
|
||||
96
toolbar.go
Normal file
96
toolbar.go
Normal file
@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type toolbarViolation struct {
|
||||
file string
|
||||
line int
|
||||
funcName string
|
||||
}
|
||||
|
||||
// Functions that render full public pages but legitimately skip toolbar injection.
|
||||
var toolbarExemptFuncs = map[string]bool{
|
||||
// Auto-generated SEO listing pages — not block-editable.
|
||||
"renderListingPage": true,
|
||||
// Historical version preview — has its own preview banner; toolbar edit/publish
|
||||
// buttons would be misleading in that context.
|
||||
"renderVersionPreview": true,
|
||||
}
|
||||
|
||||
// checkToolbarInjection scans handler Go files for public page render functions
|
||||
// (identified by containing both getSiteSettings and doc["site_settings"]) and
|
||||
// verifies each one also injects siteSettings["toolbar"] for the admin toolbar.
|
||||
func checkToolbarInjection(root string) []toolbarViolation {
|
||||
var violations []toolbarViolation
|
||||
|
||||
handlersDir := filepath.Join(root, "internal", "handlers")
|
||||
if _, err := os.Stat(handlersDir); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_ = filepath.Walk(handlersDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(path, "_test.go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
src, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
srcText := string(src)
|
||||
|
||||
fset := token.NewFileSet()
|
||||
f, err := parser.ParseFile(fset, path, src, 0)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, _ := filepath.Rel(root, path)
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
fn, ok := decl.(*ast.FuncDecl)
|
||||
if !ok || fn.Body == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if toolbarExemptFuncs[fn.Name.Name] {
|
||||
continue
|
||||
}
|
||||
|
||||
bodyStart := fset.Position(fn.Body.Pos()).Offset
|
||||
bodyEnd := fset.Position(fn.Body.End()).Offset
|
||||
if bodyEnd > len(srcText) {
|
||||
bodyEnd = len(srcText)
|
||||
}
|
||||
body := srcText[bodyStart:bodyEnd]
|
||||
|
||||
hasSiteSettings := strings.Contains(body, "getSiteSettings")
|
||||
hasDocAssign := strings.Contains(body, `doc["site_settings"]`)
|
||||
|
||||
if hasSiteSettings && hasDocAssign {
|
||||
hasToolbar := strings.Contains(body, `siteSettings["toolbar"]`)
|
||||
if !hasToolbar {
|
||||
violations = append(violations, toolbarViolation{
|
||||
file: relPath,
|
||||
line: fset.Position(fn.Pos()).Line,
|
||||
funcName: fn.Name.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return violations
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user