diff --git a/_docs/data/data.yaml b/_docs/data/data.yaml index 0aae158..4c0671c 100644 --- a/_docs/data/data.yaml +++ b/_docs/data/data.yaml @@ -36,8 +36,16 @@ properties: Golang template for the message. The [Metadata struct](https://pkg.go.dev/github.com/thegeeklab/wp-plugin-go/plugin#Metadata) is exposed to the template and all fields can be referenced. To extend the functionality, [sprig functions](https://masterminds.github.io/sprig/) can also be used. defaultValue: | - Status: **{{ .Pipeline.Status }}**
- Build: [{{ .Repository.Slug }}]({{ .Pipeline.URL }}){{ if .Curr.Branch }} ({{ .Curr.Branch }}){{ end }} by {{ .Curr.Author.Name }}
+ Status: **{{ .Pipeline.Status }}** + Build: [{{ .Repository.Slug }}]({{ .Pipeline.URL }}){{ if .Curr.Branch }} ({{ .Curr.Branch }}){{ end }} by {{ .Curr.Author.Name }} Message: {{ .Curr.Message }}{{ if .Curr.URL }} ([source]({{ .Curr.URL }})){{ end }} type: string required: false + + - name: template_unsafe + description: | + By default, raw HTML and potentially dangerous links in the template are not rendered. If you want to use inline HTML, you may need to turn this on. + In such cases, please ensure that the CI configuration files in the Git repository are protected against malicious changes. + defaultValue: false + type: bool + required: false diff --git a/cmd/wp-matrix/config.go b/cmd/wp-matrix/config.go index d2c9c72..da9574a 100644 --- a/cmd/wp-matrix/config.go +++ b/cmd/wp-matrix/config.go @@ -11,13 +11,6 @@ import ( "github.com/urfave/cli/v2" ) -//nolint:lll -const defaultTemplate = ` -Status: **{{ .Pipeline.Status }}**
-Build: [{{ .Repository.Slug }}]({{ .Pipeline.URL }}){{ if .Curr.Branch }} ({{ .Curr.Branch }}){{ end }} by {{ .Curr.Author.Name }}
-Message: {{ .Curr.Title }}{{ if .Curr.URL }} ([source]({{ .Curr.URL }})){{ end }} -` - // settingsFlags has the cli.Flags for the plugin.Settings. func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { return []cli.Flag{ @@ -68,9 +61,16 @@ func settingsFlags(settings *plugin.Settings, category string) []cli.Flag { Name: "template", EnvVars: []string{"PLUGIN_TEMPLATE", "MATRIX_TEMPLATE"}, Usage: "message template", - Value: defaultTemplate, + Value: plugin.DefaultMessageTemplate, Destination: &settings.Template, Category: category, }, + &cli.BoolFlag{ + Name: "template-unsafe", + EnvVars: []string{"PLUGIN_TEMPLATE_UNSAFE", "MATRIX_TEMPLATE_UNSAFE"}, + Usage: "render raw HTML and potentially dangerous links in template", + Destination: &settings.TemplateUnsafe, + Category: category, + }, } } diff --git a/plugin/impl.go b/plugin/impl.go index 31a0454..1d82999 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -10,6 +10,7 @@ import ( "context" "errors" "fmt" + "net/http" "strings" "github.com/microcosm-cc/bluemonday" @@ -75,14 +76,11 @@ func (p *Plugin) Execute() error { return fmt.Errorf("failed to join room: %w", err) } - message, err := template.RenderTrim(p.Network.Context, *p.Network.Client, p.Settings.Template, p.Metadata) + content, err := p.messageContent(p.Network.Context, *p.Network.Client) if err != nil { return fmt.Errorf("failed to render template: %w", err) } - formatted := bluemonday.UGCPolicy().SanitizeBytes([]byte(message)) - content := format.RenderMarkdown(string(formatted), true, false) - if _, err := client.SendMessageEvent(joined.RoomID, event.EventMessage, content); err != nil { return fmt.Errorf("failed to submit message: %w", err) } @@ -92,6 +90,26 @@ func (p *Plugin) Execute() error { return nil } +func (p *Plugin) messageContent(ctx context.Context, client http.Client) (event.MessageEventContent, error) { + message, err := template.RenderTrim(ctx, client, p.Settings.Template, p.Metadata) + if err != nil { + return event.MessageEventContent{}, err + } + + content := format.RenderMarkdown(message, true, p.Settings.TemplateUnsafe) + + safeBody := format.HTMLToMarkdown(bluemonday.UGCPolicy().Sanitize(content.FormattedBody)) + if content.Body != safeBody { + content.Body = safeBody + } + + if content.FormattedBody != "" { + content.FormattedBody = bluemonday.UGCPolicy().Sanitize(content.FormattedBody) + } + + return content, nil +} + func prepend(prefix, input string) string { if strings.TrimSpace(input) == "" { return input diff --git a/plugin/impl_test.go b/plugin/impl_test.go new file mode 100644 index 0000000..9f72737 --- /dev/null +++ b/plugin/impl_test.go @@ -0,0 +1,78 @@ +package plugin + +import ( + "context" + "net/http" + "testing" + + wp "github.com/thegeeklab/wp-plugin-go/plugin" +) + +func Test_messageContent(t *testing.T) { + //nolint:lll + tests := []struct { + name string + want string + unsafe bool + template string + meta wp.Metadata + }{ + { + name: "render default template", + want: "Status: **success**\nBuild: [octocat/demo](https://ci.example.com) (main) by octobot\nMessage: feat: demo commit title ([source](https://git.example.com))", + template: DefaultMessageTemplate, + }, + { + name: "render unsafe html template", + want: "Status: **success**\nBuild: octocat/demo", + unsafe: true, + template: "Status: **{{ .Pipeline.Status }}**
Build: {{ .Repository.Slug }}", + }, + { + name: "render html xss template", + want: "Status: **success**\nBuild: octocat/demo", + unsafe: true, + template: "Status: **{{ .Pipeline.Status }}**
Build: {{ .Repository.Slug }}", + }, + { + name: "render markdown xss template", + want: "Status: **success**\nBuild: octocat/demo", + unsafe: true, + template: "Status: **{{ .Pipeline.Status }}**
Build: [{{ .Repository.Slug }}](javascript:alert(XSS1'))", + }, + } + + options := wp.Options{ + Name: "wp-matrix", + Execute: func(ctx context.Context) error { return nil }, + } + + p := New(options, &Settings{}) + p.Metadata = wp.Metadata{ + Curr: wp.Commit{ + Branch: "main", + Title: "feat: demo commit title", + URL: "https://git.example.com", + Author: wp.Author{ + Name: "octobot", + }, + }, + Pipeline: wp.Pipeline{ + Status: "success", + URL: "https://ci.example.com", + }, + Repository: wp.Repository{ + Slug: "octocat/demo", + }, + } + + for _, tt := range tests { + p.Settings.Template = tt.template + p.Settings.TemplateUnsafe = tt.unsafe + content, _ := p.messageContent(context.Background(), http.Client{}) + + if content.Body != tt.want { + t.Errorf("messageContent: %q got: %q, want: %q", tt.name, content.Body, tt.want) + } + } +} diff --git a/plugin/plugin.go b/plugin/plugin.go index 879cf0c..e5faaa2 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -10,6 +10,13 @@ import ( wp "github.com/thegeeklab/wp-plugin-go/plugin" ) +//nolint:lll +const DefaultMessageTemplate = ` +Status: **{{ .Pipeline.Status }}** +Build: [{{ .Repository.Slug }}]({{ .Pipeline.URL }}){{ if .Curr.Branch }} ({{ .Curr.Branch }}){{ end }} by {{ .Curr.Author.Name }} +Message: {{ .Curr.Title }}{{ if .Curr.URL }} ([source]({{ .Curr.URL }})){{ end }} +` + // Plugin implements provide the plugin. type Plugin struct { *wp.Plugin @@ -18,13 +25,14 @@ type Plugin struct { // Settings for the plugin. type Settings struct { - Username string - Password string - UserID string - AccessToken string - Homeserver string - RoomID string - Template string + Username string + Password string + UserID string + AccessToken string + Homeserver string + RoomID string + Template string + TemplateUnsafe bool } func New(options wp.Options, settings *Settings) *Plugin {