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 {