check-safety/toolbar.go
Alex Dunmow c048766075 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>
2026-06-17 22:37:59 +08:00

97 lines
2.3 KiB
Go

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
}