From 32d1366ec6216afd6c10b00e78d76a82ef1a87a1 Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Sun, 19 May 2024 12:27:48 +0100 Subject: [PATCH 1/7] feat: add JSONString and JSONScript functions, update docs, mark templ script as legacy --- .version | 2 +- README.md | 6 + .../12-script-templates.md | 142 +++++++++++++++++- examples/typescript/components/index.templ | 38 +---- examples/typescript/components/index_templ.go | 40 +---- flake.nix | 7 +- jsonscript.go | 74 +++++++++ jsonscript_test.go | 53 +++++++ jsonstring.go | 14 ++ jsonstring_test.go | 28 ++++ 10 files changed, 324 insertions(+), 80 deletions(-) create mode 100644 jsonscript.go create mode 100644 jsonscript_test.go create mode 100644 jsonstring.go create mode 100644 jsonstring_test.go diff --git a/.version b/.version index 99d7f0c2b..283613393 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.2.696 \ No newline at end of file +0.2.697 \ No newline at end of file diff --git a/README.md b/README.md index b888fd54d..62839a00b 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,12 @@ go tool covdata textfmt -i=./coverage/fmt,./coverage/generate,./coverage/version go tool cover -func coverage.out | grep total ``` +### test-cover-watch + +```sh +gotestsum --watch -- -coverprofile=coverage.out +``` + ### benchmark Run benchmarks. diff --git a/docs/docs/03-syntax-and-usage/12-script-templates.md b/docs/docs/03-syntax-and-usage/12-script-templates.md index a3495f4ac..d197a66f6 100644 --- a/docs/docs/03-syntax-and-usage/12-script-templates.md +++ b/docs/docs/03-syntax-and-usage/12-script-templates.md @@ -2,7 +2,7 @@ ## Scripts -Use standard ` +``` + +The data in the script tag can then be accessed from client-side JavaScript. + +```javascript +const data = JSON.parse(document.getElementById('id').textContent); +``` + +## Working with NPM projects + +https://github.com/a-h/templ/tree/main/examples/typescript contains a TypeScript example that uses `esbuild` to transpile TypeScript into plain JavaScript, along with any required `npm` modules. + +After transpilation and bundling, the output JavaScript code can be used in a web page by including a ` + +} +``` + +You will need to configure your Go web server to serve the static content. + +```go title="main.go" +func main() { + mux := http.NewServeMux() + // Serve the JS bundle. + mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("assets")))) + + // Serve components. + data := map[string]any{"msg": "Hello, World!"} + h := templ.Handler(components.Page(data)) + mux.Handle("/", h) + + fmt.Println("Listening on http://localhost:8080") + http.ListenAndServe("localhost:8080", mux) +} +``` + ## Script templates +:::warning +Script templates are a legacy feature and are not recommended for new projects. Use standard ``, string(dataJSON)); err != nil { - return err - } - return nil - }) -} - templ Page(attributeData Data, scriptData Data) { @@ -46,8 +12,8 @@ templ Page(attributeData Data, scriptData Data) { - - @JSONScript("scriptData", scriptData) + + @templ.JSONScript("scriptData", scriptData) diff --git a/examples/typescript/components/index_templ.go b/examples/typescript/components/index_templ.go index 4f7b0bba5..70d484833 100644 --- a/examples/typescript/components/index_templ.go +++ b/examples/typescript/components/index_templ.go @@ -9,44 +9,10 @@ import "context" import "io" import "bytes" -import ( - "encoding/json" - "fmt" -) - type Data struct { Message string `json:"msg"` } -func JSON(v any) (string, error) { - s, err := json.Marshal(v) - if err != nil { - return "", err - } - return string(s), nil -} - -func JSONScript(id string, data any) templ.Component { - return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { - dataJSON, err := json.Marshal(data) - if err != nil { - return err - } - if _, err = io.WriteString(w, `%s`, string(dataJSON)); err != nil { - return err - } - return nil - }) -} - func Page(attributeData Data, scriptData Data) templ.Component { return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) @@ -65,9 +31,9 @@ func Page(attributeData Data, scriptData Data) templ.Component { return templ_7745c5c3_Err } var templ_7745c5c3_Var2 string - templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(JSON(attributeData)) + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(templ.JSONString(attributeData)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/typescript/components/index.templ`, Line: 49, Col: 65} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `examples/typescript/components/index.templ`, Line: 15, Col: 77} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -77,7 +43,7 @@ func Page(attributeData Data, scriptData Data) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = JSONScript("scriptData", scriptData).Render(ctx, templ_7745c5c3_Buffer) + templ_7745c5c3_Err = templ.JSONScript("scriptData", scriptData).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/flake.nix b/flake.nix index 87dd59d16..1e9009feb 100644 --- a/flake.nix +++ b/flake.nix @@ -62,14 +62,15 @@ pkgs.mkShell { buildInputs = with pkgs; [ (golangci-lint.override { buildGoModule = buildGo121Module; }) + cosign # Used to sign container images. esbuild # Used to package JS examples. go_1_21 + gomod2nix.legacyPackages.${system}.gomod2nix gopls goreleaser - nodejs # Used to build templ-docs. + gotestsum ko # Used to build Docker images. - cosign # Used to sign container images. - gomod2nix.legacyPackages.${system}.gomod2nix + nodejs # Used to build templ-docs. xc.packages.${system}.xc ]; }); diff --git a/jsonscript.go b/jsonscript.go new file mode 100644 index 000000000..e0eb0c7da --- /dev/null +++ b/jsonscript.go @@ -0,0 +1,74 @@ +package templ + +import ( + "context" + "encoding/json" + "fmt" + "io" +) + +type CSPContextKeyType int + +const CSPContextKey CSPContextKeyType = iota + +func CSPNonceFromContext(ctx context.Context) string { + if ctx == nil { + return "" + } + if nonce, ok := ctx.Value(CSPContextKey).(string); ok { + return nonce + } + return "" +} + +var _ Component = JSONScriptElement{} + +// JSONScript renders a JSON object inside a script element. +// e.g. +func JSONScript(id string, data any) JSONScriptElement { + return JSONScriptElement{ + ID: id, + Data: data, + Nonce: CSPNonceFromContext, + } +} + +func (j JSONScriptElement) WithNonceFromString(nonce string) JSONScriptElement { + j.Nonce = func(context.Context) string { + return nonce + } + return j +} + +func (j JSONScriptElement) WithNonceFrom(f func(context.Context) string) JSONScriptElement { + j.Nonce = f + return j +} + +type JSONScriptElement struct { + // ID of the element in the DOM. + ID string + // Data that will be encoded as JSON. + Data any + // Nonce is a function that returns a CSP nonce. + // Defaults to CSPNonceFromContext. + // See https://content-security-policy.com/nonce for more information. + Nonce func(ctx context.Context) string +} + +func (j JSONScriptElement) Render(ctx context.Context, w io.Writer) (err error) { + var nonceAttr string + if nonce := j.Nonce(ctx); nonce != "" { + nonceAttr = fmt.Sprintf(" nonce=\"%s\"", EscapeString(nonce)) + } + if _, err = fmt.Fprintf(w, ""); err != nil { + return err + } + return nil +} diff --git a/jsonscript_test.go b/jsonscript_test.go new file mode 100644 index 000000000..61bea8c75 --- /dev/null +++ b/jsonscript_test.go @@ -0,0 +1,53 @@ +package templ_test + +import ( + "bytes" + "context" + "testing" + + "github.com/a-h/templ" + "github.com/google/go-cmp/cmp" +) + +func TestJSONScriptElement(t *testing.T) { + data := map[string]interface{}{"foo": "bar"} + tests := []struct { + name string + ctx context.Context + e templ.JSONScriptElement + expected string + }{ + { + name: "renders data as JSON inside a script element", + e: templ.JSONScript("id", data), + expected: "", + }, + { + name: "if a nonce is available in the context, it is used", + ctx: context.WithValue(context.Background(), templ.CSPContextKey, "nonce-from-context"), + e: templ.JSONScript("idc", data), + expected: "", + }, + { + name: "if a nonce is provided, it is used", + e: templ.JSONScript("ids", data).WithNonceFromString("nonce-from-string"), + expected: "", + }, + { + name: "if a nonce function is provided, it is used", + e: templ.JSONScript("idf", data).WithNonceFrom(func(context.Context) string { return "nonce-from-function" }), + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := new(bytes.Buffer) + if err := tt.e.Render(tt.ctx, w); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(tt.expected, w.String()); diff != "" { + t.Fatalf("unexpected output (-want +got):\n%s", diff) + } + }) + } +} diff --git a/jsonstring.go b/jsonstring.go new file mode 100644 index 000000000..425e4e8c1 --- /dev/null +++ b/jsonstring.go @@ -0,0 +1,14 @@ +package templ + +import ( + "encoding/json" +) + +// JSONString returns a JSON encoded string of v. +func JSONString(v any) (string, error) { + b, err := json.Marshal(v) + if err != nil { + return "", err + } + return string(b), nil +} diff --git a/jsonstring_test.go b/jsonstring_test.go new file mode 100644 index 000000000..b40c31e4c --- /dev/null +++ b/jsonstring_test.go @@ -0,0 +1,28 @@ +package templ_test + +import ( + "testing" + + "github.com/a-h/templ" +) + +func TestJSONString(t *testing.T) { + t.Run("renders input data as a JSON string", func(t *testing.T) { + data := map[string]any{"foo": "bar"} + actual, err := templ.JSONString(data) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := "{\"foo\":\"bar\"}" + if actual != expected { + t.Fatalf("unexpected output: want %q, got %q", expected, actual) + } + }) + t.Run("returns an error if the data cannot be marshalled", func(t *testing.T) { + data := make(chan int) + _, err := templ.JSONString(data) + if err == nil { + t.Fatalf("expected an error, got nil") + } + }) +} From a93a8c66b7068b4339835b03ec521ddcc98e8c64 Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Mon, 20 May 2024 09:51:35 +0100 Subject: [PATCH 2/7] fix: update push-tag to check local git is clean, and up to date --- push-tag.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/push-tag.sh b/push-tag.sh index 3d5232315..9eedeed87 100755 --- a/push-tag.sh +++ b/push-tag.sh @@ -1,4 +1,13 @@ #!/bin/sh +if [ `git rev-parse --abbrev-ref HEAD` != "main" ]; then + echo "Error: Not on main branch. Please switch to main branch."; + exit 1; +fi +git pull +if ! git diff --quiet; then + echo "Error: Working directory is not clean. Please commit the changes first."; + exit 1; +fi export VERSION=`cat .version` echo Adding git tag with version v${VERSION}; git tag v${VERSION}; From 710ba9203de31910d60961b53d722bcf324961c2 Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Mon, 20 May 2024 09:55:13 +0100 Subject: [PATCH 3/7] docs: fix broken link [no ci] --- docs/docs/12-integrations/02-internationalization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/12-integrations/02-internationalization.md b/docs/docs/12-integrations/02-internationalization.md index 4b3987154..1443d6f6e 100644 --- a/docs/docs/12-integrations/02-internationalization.md +++ b/docs/docs/12-integrations/02-internationalization.md @@ -4,7 +4,7 @@ templ can be used with 3rd party internationalization libraries. ## ctxi18n -[ctxi18n](github.com/invopop/ctxi18n) uses the context package to load strings based on the selected locale. +https://github.com/invopop/ctxi18n uses the context package to load strings based on the selected locale. An example is available at https://github.com/a-h/templ/tree/main/examples/internationalization From a75e6ab0edd6b32033e1cfbc2e1b73ced02f83e9 Mon Sep 17 00:00:00 2001 From: Joe Davidson Date: Tue, 21 May 2024 13:49:44 +0100 Subject: [PATCH 4/7] feat: add WithNonce for CSP compatibility (#752) Co-authored-by: Adrian Hesketh --- .version | 2 +- .../{index.md => 01-injection-attacks.md} | 9 +- .../10-security/02-content-security-policy.md | 86 +++++++ docs/docs/10-security/03-code-signing.md | 7 + examples/content-security-policy/main.go | 67 ++++++ .../content-security-policy/templates.templ | 9 + .../templates_templ.go | 44 ++++ .../test-script-usage-nonce/expected.html | 19 ++ .../test-script-usage-nonce/render_test.go | 26 +++ .../test-script-usage-nonce/template.templ | 44 ++++ .../test-script-usage-nonce/template_templ.go | 219 ++++++++++++++++++ runtime.go | 28 ++- runtime_test.go | 18 ++ 13 files changed, 567 insertions(+), 11 deletions(-) rename docs/docs/10-security/{index.md => 01-injection-attacks.md} (94%) create mode 100644 docs/docs/10-security/02-content-security-policy.md create mode 100644 docs/docs/10-security/03-code-signing.md create mode 100644 examples/content-security-policy/main.go create mode 100644 examples/content-security-policy/templates.templ create mode 100644 examples/content-security-policy/templates_templ.go create mode 100644 generator/test-script-usage-nonce/expected.html create mode 100644 generator/test-script-usage-nonce/render_test.go create mode 100644 generator/test-script-usage-nonce/template.templ create mode 100644 generator/test-script-usage-nonce/template_templ.go diff --git a/.version b/.version index 283613393..5629baa03 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.2.697 \ No newline at end of file +0.2.700 \ No newline at end of file diff --git a/docs/docs/10-security/index.md b/docs/docs/10-security/01-injection-attacks.md similarity index 94% rename from docs/docs/10-security/index.md rename to docs/docs/10-security/01-injection-attacks.md index c7775e876..9a1f832cf 100644 --- a/docs/docs/10-security/index.md +++ b/docs/docs/10-security/01-injection-attacks.md @@ -1,6 +1,4 @@ -# Security - -## Injection attacks +# Injection attacks templ is designed to prevent user-provided data from being used to inject vulnerabilities. @@ -87,8 +85,3 @@ css className() { color: { red }; } ``` - -## Code signing - -Binaries are created by https://github.com/a-h and signed with https://adrianhesketh.com/a-h.gpg - diff --git a/docs/docs/10-security/02-content-security-policy.md b/docs/docs/10-security/02-content-security-policy.md new file mode 100644 index 000000000..6ba5e0410 --- /dev/null +++ b/docs/docs/10-security/02-content-security-policy.md @@ -0,0 +1,86 @@ +# Content security policy + +## Nonces + +In templ [script templates](/syntax-and-usage/script-templates#script-templates) are rendered as inline ` + +``` diff --git a/docs/docs/10-security/03-code-signing.md b/docs/docs/10-security/03-code-signing.md new file mode 100644 index 000000000..473850196 --- /dev/null +++ b/docs/docs/10-security/03-code-signing.md @@ -0,0 +1,7 @@ +# Code signing + +Binaries are created by the Github Actions workflow at https://github.com/a-h/templ/blob/main/.github/workflows/release.yml + +Binaries are signed by cosign. The public key is stored in the repository at https://github.com/a-h/templ/blob/main/cosign.pub + +Instructions for key verification at https://docs.sigstore.dev/verifying/verify/ diff --git a/examples/content-security-policy/main.go b/examples/content-security-policy/main.go new file mode 100644 index 000000000..72c33a410 --- /dev/null +++ b/examples/content-security-policy/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "crypto/rand" + "fmt" + "math/big" + "net/http" + "os" + + "log/slog" + + "github.com/a-h/templ" +) + +func main() { + log := slog.New(slog.NewJSONHandler(os.Stdout, nil)) + + // Create HTTP routes. + mux := http.NewServeMux() + mux.Handle("/", templ.Handler(template())) + + // Wrap the router with CSP middleware to apply the CSP nonce to templ scripts. + withCSPMiddleware := NewCSPMiddleware(log, mux) + + log.Info("Listening...", slog.String("addr", "127.0.0.1:7001")) + if err := http.ListenAndServe("127.0.0.1:7001", withCSPMiddleware); err != nil { + log.Error("failed to start server", slog.Any("error", err)) + } +} + +func NewCSPMiddleware(log *slog.Logger, next http.Handler) *CSPMiddleware { + return &CSPMiddleware{ + Log: log, + Next: next, + Size: 28, + } +} + +type CSPMiddleware struct { + Log *slog.Logger + Next http.Handler + Size int +} + +func (m *CSPMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + nonce, err := m.generateNonce() + if err != nil { + m.Log.Error("failed to generate nonce", slog.Any("error", err)) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + ctx := templ.WithNonce(r.Context(), nonce) + w.Header().Add("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce)) + m.Next.ServeHTTP(w, r.WithContext(ctx)) +} + +func (m *CSPMiddleware) generateNonce() (string, error) { + const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" + ret := make([]byte, m.Size) + for i := 0; i < m.Size; i++ { + num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + if err != nil { + return "", err + } + ret[i] = letters[num.Int64()] + } + return string(ret), nil +} diff --git a/examples/content-security-policy/templates.templ b/examples/content-security-policy/templates.templ new file mode 100644 index 000000000..c88808adc --- /dev/null +++ b/examples/content-security-policy/templates.templ @@ -0,0 +1,9 @@ +package main + +script sayHello() { + alert("Hello") +} + +templ template() { + @sayHello() +} diff --git a/examples/content-security-policy/templates_templ.go b/examples/content-security-policy/templates_templ.go new file mode 100644 index 000000000..4d463f6cb --- /dev/null +++ b/examples/content-security-policy/templates_templ.go @@ -0,0 +1,44 @@ +// Code generated by templ - DO NOT EDIT. + +package main + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func sayHello() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_sayHello_6bd3`, + Function: `function __templ_sayHello_6bd3(){alert("Hello") +}`, + Call: templ.SafeScript(`__templ_sayHello_6bd3`), + CallInline: templ.SafeScriptInline(`__templ_sayHello_6bd3`), + } +} + +func template() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = sayHello().Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/generator/test-script-usage-nonce/expected.html b/generator/test-script-usage-nonce/expected.html new file mode 100644 index 000000000..9f15abc46 --- /dev/null +++ b/generator/test-script-usage-nonce/expected.html @@ -0,0 +1,19 @@ + + + + + + + + + diff --git a/generator/test-script-usage-nonce/render_test.go b/generator/test-script-usage-nonce/render_test.go new file mode 100644 index 000000000..48b2da861 --- /dev/null +++ b/generator/test-script-usage-nonce/render_test.go @@ -0,0 +1,26 @@ +package testscriptusage + +import ( + "context" + _ "embed" + "testing" + + "github.com/a-h/templ" + "github.com/a-h/templ/generator/htmldiff" +) + +//go:embed expected.html +var expected string + +func Test(t *testing.T) { + component := ThreeButtons() + + ctx := templ.WithNonce(context.Background(), "nonce1") + diff, err := htmldiff.DiffCtx(ctx, component, expected) + if err != nil { + t.Fatal(err) + } + if diff != "" { + t.Error(diff) + } +} diff --git a/generator/test-script-usage-nonce/template.templ b/generator/test-script-usage-nonce/template.templ new file mode 100644 index 000000000..9364a7525 --- /dev/null +++ b/generator/test-script-usage-nonce/template.templ @@ -0,0 +1,44 @@ +package testscriptusage + +script withParameters(a string, b string, c int) { + console.log(a, b, c); +} + +script withoutParameters() { + alert("hello"); +} + +script onClick() { + alert("clicked"); +} + +templ Button(text string) { + +} + +script withComment() { + //' +} + +templ ThreeButtons() { + @Button("A") + @Button("B") + + + + @Conditional(true) +} + +script conditionalScript() { + alert("conditional"); +} + +templ Conditional(show bool) { + +} diff --git a/generator/test-script-usage-nonce/template_templ.go b/generator/test-script-usage-nonce/template_templ.go new file mode 100644 index 000000000..e30405e4b --- /dev/null +++ b/generator/test-script-usage-nonce/template_templ.go @@ -0,0 +1,219 @@ +// Code generated by templ - DO NOT EDIT. + +package testscriptusage + +//lint:file-ignore SA4006 This context is only used if a nested component is present. + +import "github.com/a-h/templ" +import "context" +import "io" +import "bytes" + +func withParameters(a string, b string, c int) templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_withParameters_1056`, + Function: `function __templ_withParameters_1056(a, b, c){console.log(a, b, c); +}`, + Call: templ.SafeScript(`__templ_withParameters_1056`, a, b, c), + CallInline: templ.SafeScriptInline(`__templ_withParameters_1056`, a, b, c), + } +} + +func withoutParameters() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_withoutParameters_6bbf`, + Function: `function __templ_withoutParameters_6bbf(){alert("hello"); +}`, + Call: templ.SafeScript(`__templ_withoutParameters_6bbf`), + CallInline: templ.SafeScriptInline(`__templ_withoutParameters_6bbf`), + } +} + +func onClick() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_onClick_657d`, + Function: `function __templ_onClick_657d(){alert("clicked"); +}`, + Call: templ.SafeScript(`__templ_onClick_657d`), + CallInline: templ.SafeScriptInline(`__templ_onClick_657d`), + } +} + +func Button(text string) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var1 := templ.GetChildren(ctx) + if templ_7745c5c3_Var1 == nil { + templ_7745c5c3_Var1 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, withParameters("test", text, 123), withoutParameters()) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func withComment() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_withComment_9cf8`, + Function: `function __templ_withComment_9cf8(){//' +}`, + Call: templ.SafeScript(`__templ_withComment_9cf8`), + CallInline: templ.SafeScriptInline(`__templ_withComment_9cf8`), + } +} + +func ThreeButtons() templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var5 := templ.GetChildren(ctx) + if templ_7745c5c3_Var5 == nil { + templ_7745c5c3_Var5 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = Button("A").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Button("B").Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, onClick()) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = Conditional(true).Render(ctx, templ_7745c5c3_Buffer) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} + +func conditionalScript() templ.ComponentScript { + return templ.ComponentScript{ + Name: `__templ_conditionalScript_de41`, + Function: `function __templ_conditionalScript_de41(){alert("conditional"); +}`, + Call: templ.SafeScript(`__templ_conditionalScript_de41`), + CallInline: templ.SafeScriptInline(`__templ_conditionalScript_de41`), + } +} + +func Conditional(show bool) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) { + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer) + if !templ_7745c5c3_IsBuffer { + templ_7745c5c3_Buffer = templ.GetBuffer() + defer templ.ReleaseBuffer(templ_7745c5c3_Buffer) + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var7 := templ.GetChildren(ctx) + if templ_7745c5c3_Var7 == nil { + templ_7745c5c3_Var7 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, conditionalScript()) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if !templ_7745c5c3_IsBuffer { + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W) + } + return templ_7745c5c3_Err + }) +} diff --git a/runtime.go b/runtime.go index 3a86939a5..b918a3f7f 100644 --- a/runtime.go +++ b/runtime.go @@ -41,6 +41,20 @@ func (cf ComponentFunc) Render(ctx context.Context, w io.Writer) error { return cf(ctx, w) } +// WithNonce sets a CSP nonce on the context and returns it. +func WithNonce(ctx context.Context, nonce string) context.Context { + ctx, v := getContext(ctx) + v.nonce = nonce + return ctx +} + +// GetNonce returns the CSP nonce value set with WithNonce, or an +// empty string if none has been set. +func GetNonce(ctx context.Context) (nonce string) { + _, v := getContext(ctx) + return v.nonce +} + func WithChildren(ctx context.Context, children Component) context.Context { ctx, v := getContext(ctx) v.children = &children @@ -578,6 +592,7 @@ const contextKey = contextKeyType(0) type contextValue struct { ss map[string]struct{} children *Component + nonce string } func (v *contextValue) addScript(s string) { @@ -663,13 +678,22 @@ type ComponentScript struct { var _ Component = ComponentScript{} +func writeScriptHeader(ctx context.Context, w io.Writer) (err error) { + var nonceAttr string + if nonce := GetNonce(ctx); nonce != "" { + nonceAttr = " nonce=\"" + EscapeString(nonce) + "\"" + } + _, err = fmt.Fprintf(w, `", }, diff --git a/runtime.go b/runtime.go index b918a3f7f..ef9df246a 100644 --- a/runtime.go +++ b/runtime.go @@ -51,6 +51,9 @@ func WithNonce(ctx context.Context, nonce string) context.Context { // GetNonce returns the CSP nonce value set with WithNonce, or an // empty string if none has been set. func GetNonce(ctx context.Context) (nonce string) { + if ctx == nil { + return "" + } _, v := getContext(ctx) return v.nonce } From 4e5d5e4ab842462a4921bcc04371d465a7788679 Mon Sep 17 00:00:00 2001 From: Joe Davidson Date: Tue, 21 May 2024 13:49:44 +0100 Subject: [PATCH 6/7] feat: add WithNonce for CSP compatibility (#752) Co-authored-by: Adrian Hesketh --- .version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.version b/.version index 5629baa03..f20737506 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -0.2.700 \ No newline at end of file +0.2.700 From 1aa5560caaaf0c1a334acc2e0db6cb3244e35717 Mon Sep 17 00:00:00 2001 From: Adrian Hesketh Date: Tue, 21 May 2024 14:26:23 +0100 Subject: [PATCH 7/7] fix: missing return --- examples/content-security-policy/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/content-security-policy/main.go b/examples/content-security-policy/main.go index 72c33a410..47a75e2cc 100644 --- a/examples/content-security-policy/main.go +++ b/examples/content-security-policy/main.go @@ -47,6 +47,7 @@ func (m *CSPMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err != nil { m.Log.Error("failed to generate nonce", slog.Any("error", err)) http.Error(w, "Internal server error", http.StatusInternalServerError) + return } ctx := templ.WithNonce(r.Context(), nonce) w.Header().Add("Content-Security-Policy", fmt.Sprintf("script-src 'nonce-%s'", nonce))