diff --git a/commands/hugo.go b/commands/hugo.go index dd3ca289e48..21242a32cef 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -301,6 +301,7 @@ func LoadDefaultSettings() { viper.SetDefault("SectionPagesMenu", "") viper.SetDefault("DisablePathToLower", false) viper.SetDefault("HasCJKLanguage", false) + viper.SetDefault("ExecWhitelist", []string{}) } // InitializeConfig initializes a config file with sensible default configuration flags. diff --git a/docs/content/overview/configuration.md b/docs/content/overview/configuration.md index 18f4b14bd0e..2e58574bca7 100644 --- a/docs/content/overview/configuration.md +++ b/docs/content/overview/configuration.md @@ -150,6 +150,8 @@ Following is a list of Hugo-defined variables that you can configure and their c verboseLog: false # watch filesystem for changes and recreate as needed watch: true + # commands that can be executed via the exec command + execWhitelist: [] --- ## Ignore files on build diff --git a/docs/content/templates/functions.md b/docs/content/templates/functions.md index 599e61bda6a..55ad864ffe2 100644 --- a/docs/content/templates/functions.md +++ b/docs/content/templates/functions.md @@ -822,3 +822,15 @@ responses of APIs. {{ $resp.content | base64Decode | markdownify }} The response of the GitHub API contains the base64-encoded version of the [README.md](https://github.com/spf13/hugo/blob/master/README.md) in the Hugo repository. Now we can decode it and parse the Markdown. The final output will look similar to the rendered version on GitHub. + +### exec + +Executes an external command and include its standard output as content. The +actual commands that can be executed need to be given using the `execWhitelist` +configuration settings. + + {{ echo 42 }} + + +This assumes that `echo` is listed within the `execWhitelist`, which is not the +case by default. diff --git a/tpl/template_funcs.go b/tpl/template_funcs.go index 3e21e81d621..ff3a2099ff7 100644 --- a/tpl/template_funcs.go +++ b/tpl/template_funcs.go @@ -28,6 +28,7 @@ import ( "html/template" "math/rand" "os" + _exec "os/exec" "reflect" "sort" "strconv" @@ -40,6 +41,7 @@ import ( "github.com/spf13/cast" "github.com/spf13/hugo/helpers" jww "github.com/spf13/jwalterweatherman" + "github.com/spf13/viper" ) var funcMap template.FuncMap @@ -1137,6 +1139,35 @@ func highlight(in interface{}, lang, opts string) (template.HTML, error) { return template.HTML(helpers.Highlight(html.UnescapeString(str), lang, opts)), nil } +// exec executes a command and returns its output as string. +func exec(in interface{}, args ...string) (string, error) { + str, err := cast.ToStringE(in) + + if err != nil { + return "", err + } + + // Check if command is accepted (in white list) + commandAccepted := false + for _, validCommand := range viper.GetStringSlice("ExecWhiteList") { + if str == validCommand { + commandAccepted = true + break + } + } + + if !commandAccepted { + return "", fmt.Errorf("Executing %s is not allowed. Check execWhiteList settings.", str) + } + + out, err := _exec.Command(strings.TrimSpace(str), args...).Output() + if err != nil { + return "", err + } + + return string(out[:]), nil +} + var markdownTrimPrefix = []byte("

") var markdownTrimSuffix = []byte("

\n") @@ -1684,6 +1715,7 @@ func init() { "div": func(a, b interface{}) (interface{}, error) { return doArithmetic(a, b, '/') }, "echoParam": returnWhenSet, "eq": eq, + "exec": exec, "first": first, "ge": ge, "getCSV": getCSV, diff --git a/tpl/template_funcs_test.go b/tpl/template_funcs_test.go index 96d0c013b70..1d76e6bd5ad 100644 --- a/tpl/template_funcs_test.go +++ b/tpl/template_funcs_test.go @@ -58,6 +58,56 @@ func tstIsLt(tp tstCompareType) bool { return tp == tstLt || tp == tstLe } +func TestExecFuncInTemplate(t *testing.T) { + + viper.Reset() + defer viper.Reset() + + viper.Set("ExecWhitelist", []string{"echo"}) + + in := "{{exec \"echo\" \"test\"}}" + expected := "test\n" + + templ, err := New().New("test").Parse(in) + if err != nil { + t.Fatal("Got error on parse", err) + } + + var b bytes.Buffer + err = templ.Execute(&b, nil) + + if err != nil { + t.Fatal("Got error on execute", err) + } + + if b.String() != expected { + t.Errorf("Got\n%q\nExpected\n>%q<", b.String(), expected) + } +} + +func TestExecFuncInTemplateCmdNotInWhitelist(t *testing.T) { + + viper.Reset() + defer viper.Reset() + + viper.Set("ExecWhitelist", []string{}) + + in := "{{exec \"echo\" \"test\"}}" + expectedErrSuffix := "Executing echo is not allowed. Check execWhiteList settings." + + templ, err := New().New("test").Parse(in) + if err != nil { + t.Fatal("Got error on parse", err) + } + + var b bytes.Buffer + err = templ.Execute(&b, nil) + + if !strings.HasSuffix(err.Error(), expectedErrSuffix) { + t.Errorf("Expected suffix %q in %q", expectedErrSuffix, err.Error()) + } +} + func TestFuncsInTemplate(t *testing.T) { viper.Reset()