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
|
// 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
|
// 26. Orchestrator backend tests must pass when checking the core BlockNinja repo
|
||||||
// 27. No hand-rolled HTML sanitization — use bluemonday
|
// 27. No hand-rolled HTML sanitization — use bluemonday
|
||||||
|
// 28. Public page handlers inject admin toolbar data
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@ -18,6 +18,7 @@ func TestRegistryOrder(t *testing.T) {
|
|||||||
"1", "2", "2b", "2c", "2d", "2e", "2f", "3", "3b", "4",
|
"1", "2", "2b", "2c", "2d", "2e", "2f", "3", "3b", "4",
|
||||||
"5", "6", "7", "8", "9", "10", "10b", "10disc", "11", "12",
|
"5", "6", "7", "8", "9", "10", "10b", "10disc", "11", "12",
|
||||||
"13", "14", "15", "16", "17", "18", "19", "20", "21", "22",
|
"13", "14", "15", "16", "17", "18", "19", "20", "21", "22",
|
||||||
|
"28",
|
||||||
}
|
}
|
||||||
|
|
||||||
ordered := make([]Check, len(registry))
|
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) ===
|
=== Check 22: No hand-rolled HTML sanitization (use bluemonday) ===
|
||||||
OK: No hand-rolled HTML sanitization detected
|
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) ===
|
=== Check 22: No hand-rolled HTML sanitization (use bluemonday) ===
|
||||||
OK: No hand-rolled HTML sanitization detected
|
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