summaryrefslogtreecommitdiffstats
path: root/hugolib
diff options
context:
space:
mode:
Diffstat (limited to 'hugolib')
-rw-r--r--hugolib/config.go9
-rw-r--r--hugolib/js_test.go10
-rw-r--r--hugolib/page_test.go73
-rw-r--r--hugolib/resource_chain_babel_test.go14
-rw-r--r--hugolib/resource_chain_test.go8
-rw-r--r--hugolib/securitypolicies_test.go202
-rw-r--r--hugolib/shortcode.go4
-rw-r--r--hugolib/shortcode_test.go6
-rw-r--r--hugolib/site.go42
-rw-r--r--hugolib/testdata/cities.csv130
-rw-r--r--hugolib/testdata/fruits.json5
-rw-r--r--hugolib/testhelpers_test.go12
12 files changed, 441 insertions, 74 deletions
diff --git a/hugolib/config.go b/hugolib/config.go
index 3b5ade598..e79899b94 100644
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -18,6 +18,7 @@ import (
"path/filepath"
"strings"
+ "github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/maps"
@@ -41,6 +42,7 @@ import (
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
+ "github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero"
@@ -377,6 +379,12 @@ func (l configLoader) collectModules(modConfig modules.Config, v1 config.Provide
return nil, nil, err
}
+ secConfig, err := security.DecodeConfig(v1)
+ if err != nil {
+ return nil, nil, err
+ }
+ ex := hexec.New(secConfig)
+
v1.Set("filecacheConfigs", filecacheConfigs)
var configFilenames []string
@@ -405,6 +413,7 @@ func (l configLoader) collectModules(modConfig modules.Config, v1 config.Provide
modulesClient := modules.NewClient(modules.ClientConfig{
Fs: l.Fs,
Logger: l.Logger,
+ Exec: ex,
HookBeforeFinalize: hook,
WorkingDir: workingDir,
ThemesDir: themesDir,
diff --git a/hugolib/js_test.go b/hugolib/js_test.go
index 66c284d8b..69f528758 100644
--- a/hugolib/js_test.go
+++ b/hugolib/js_test.go
@@ -20,7 +20,6 @@ import (
"runtime"
"testing"
- "github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/htesting"
@@ -123,10 +122,9 @@ TS2: {{ template "print" $ts2 }}
b.WithSourceFile("assets/js/included.js", includedJS)
- cmd, err := hexec.SafeCommand("npm", "install")
+ cmd := b.NpmInstall()
+ err = cmd.Run()
b.Assert(err, qt.IsNil)
- out, err := cmd.CombinedOutput()
- b.Assert(err, qt.IsNil, qt.Commentf(string(out)))
b.Build(BuildCfg{})
@@ -195,8 +193,8 @@ require github.com/gohugoio/hugoTestProjectJSModImports v0.9.0 // indirect
}`)
b.Assert(os.Chdir(workDir), qt.IsNil)
- cmd, _ := hexec.SafeCommand("npm", "install")
- _, err = cmd.CombinedOutput()
+ cmd := b.NpmInstall()
+ err = cmd.Run()
b.Assert(err, qt.IsNil)
b.Build(BuildCfg{})
diff --git a/hugolib/page_test.go b/hugolib/page_test.go
index 7a1ff6c4e..50263d483 100644
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -24,8 +24,6 @@ import (
"github.com/gohugoio/hugo/htesting"
- "github.com/gohugoio/hugo/markup/rst"
-
"github.com/gohugoio/hugo/markup/asciidocext"
"github.com/gohugoio/hugo/config"
@@ -370,6 +368,7 @@ func normalizeExpected(ext, str string) string {
func testAllMarkdownEnginesForPages(t *testing.T,
assertFunc func(t *testing.T, ext string, pages page.Pages), settings map[string]interface{}, pageSources ...string) {
+
engines := []struct {
ext string
shouldExecute func() bool
@@ -377,7 +376,7 @@ func testAllMarkdownEnginesForPages(t *testing.T,
{"md", func() bool { return true }},
{"mmark", func() bool { return true }},
{"ad", func() bool { return asciidocext.Supports() }},
- {"rst", func() bool { return rst.Supports() }},
+ {"rst", func() bool { return true }},
}
for _, e := range engines {
@@ -385,47 +384,57 @@ func testAllMarkdownEnginesForPages(t *testing.T,
continue
}
- cfg, fs := newTestCfg(func(cfg config.Provider) error {
- for k, v := range settings {
- cfg.Set(k, v)
+ t.Run(e.ext, func(t *testing.T) {
+
+ cfg, fs := newTestCfg(func(cfg config.Provider) error {
+ for k, v := range settings {
+ cfg.Set(k, v)
+ }
+ return nil
+ })
+
+ contentDir := "content"
+
+ if s := cfg.GetString("contentDir"); s != "" {
+ contentDir = s
}
- return nil
- })
- contentDir := "content"
+ cfg.Set("security", map[string]interface{}{
+ "exec": map[string]interface{}{
+ "allow": []string{"^python$", "^rst2html.*", "^asciidoctor$"},
+ },
+ })
- if s := cfg.GetString("contentDir"); s != "" {
- contentDir = s
- }
+ var fileSourcePairs []string
- var fileSourcePairs []string
+ for i, source := range pageSources {
+ fileSourcePairs = append(fileSourcePairs, fmt.Sprintf("p%d.%s", i, e.ext), source)
+ }
- for i, source := range pageSources {
- fileSourcePairs = append(fileSourcePairs, fmt.Sprintf("p%d.%s", i, e.ext), source)
- }
+ for i := 0; i < len(fileSourcePairs); i += 2 {
+ writeSource(t, fs, filepath.Join(contentDir, fileSourcePairs[i]), fileSourcePairs[i+1])
+ }
- for i := 0; i < len(fileSourcePairs); i += 2 {
- writeSource(t, fs, filepath.Join(contentDir, fileSourcePairs[i]), fileSourcePairs[i+1])
- }
+ // Add a content page for the home page
+ homePath := fmt.Sprintf("_index.%s", e.ext)
+ writeSource(t, fs, filepath.Join(contentDir, homePath), homePage)
- // Add a content page for the home page
- homePath := fmt.Sprintf("_index.%s", e.ext)
- writeSource(t, fs, filepath.Join(contentDir, homePath), homePage)
+ b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded()
+ b.Build(BuildCfg{})
- b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded()
- b.Build(BuildCfg{SkipRender: true})
+ s := b.H.Sites[0]
- s := b.H.Sites[0]
+ b.Assert(len(s.RegularPages()), qt.Equals, len(pageSources))
- b.Assert(len(s.RegularPages()), qt.Equals, len(pageSources))
+ assertFunc(t, e.ext, s.RegularPages())
- assertFunc(t, e.ext, s.RegularPages())
+ home, err := s.Info.Home()
+ b.Assert(err, qt.IsNil)
+ b.Assert(home, qt.Not(qt.IsNil))
+ b.Assert(home.File().Path(), qt.Equals, homePath)
+ b.Assert(content(home), qt.Contains, "Home Page Content")
- home, err := s.Info.Home()
- b.Assert(err, qt.IsNil)
- b.Assert(home, qt.Not(qt.IsNil))
- b.Assert(home.File().Path(), qt.Equals, homePath)
- b.Assert(content(home), qt.Contains, "Home Page Content")
+ })
}
}
diff --git a/hugolib/resource_chain_babel_test.go b/hugolib/resource_chain_babel_test.go
index 5cca22ba1..7a97e820a 100644
--- a/hugolib/resource_chain_babel_test.go
+++ b/hugolib/resource_chain_babel_test.go
@@ -21,8 +21,6 @@ import (
"github.com/gohugoio/hugo/config"
- "github.com/gohugoio/hugo/common/hexec"
-
jww "github.com/spf13/jwalterweatherman"
"github.com/gohugoio/hugo/htesting"
@@ -51,7 +49,7 @@ func TestResourceChainBabel(t *testing.T) {
"devDependencies": {
"@babel/cli": "7.8.4",
- "@babel/core": "7.9.0",
+ "@babel/core": "7.9.0",
"@babel/preset-env": "7.9.5"
}
}
@@ -94,6 +92,12 @@ class Car2 {
v := config.New()
v.Set("workingDir", workDir)
v.Set("disableKinds", []string{"taxonomy", "term", "page"})
+ v.Set("security", map[string]interface{}{
+ "exec": map[string]interface{}{
+ "allow": []string{"^npx$", "^babel$"},
+ },
+ })
+
b := newTestSitesBuilder(t).WithLogger(logger)
// Need to use OS fs for this.
@@ -123,8 +127,8 @@ Transpiled3: {{ $transpiled.Permalink }}
b.WithSourceFile("babel.config.js", babelConfig)
b.Assert(os.Chdir(workDir), qt.IsNil)
- cmd, _ := hexec.SafeCommand("npm", "install")
- _, err = cmd.CombinedOutput()
+ cmd := b.NpmInstall()
+ err = cmd.Run()
b.Assert(err, qt.IsNil)
b.Build(BuildCfg{})
diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go
index 214dda216..0a5b9177c 100644
--- a/hugolib/resource_chain_test.go
+++ b/hugolib/resource_chain_test.go
@@ -32,8 +32,6 @@ import (
"testing"
"time"
- "github.com/gohugoio/hugo/common/hexec"
-
jww "github.com/spf13/jwalterweatherman"
"github.com/gohugoio/hugo/common/herrors"
@@ -387,8 +385,6 @@ T1: {{ $r.Content }}
}
func TestResourceChainBasic(t *testing.T) {
- t.Parallel()
-
ts := httptest.NewServer(http.FileServer(http.Dir("testdata/")))
t.Cleanup(func() {
ts.Close()
@@ -1184,8 +1180,8 @@ class-in-b {
b.WithSourceFile("postcss.config.js", postcssConfig)
b.Assert(os.Chdir(workDir), qt.IsNil)
- cmd, err := hexec.SafeCommand("npm", "install")
- _, err = cmd.CombinedOutput()
+ cmd := b.NpmInstall()
+ err = cmd.Run()
b.Assert(err, qt.IsNil)
b.Build(BuildCfg{})
diff --git a/hugolib/securitypolicies_test.go b/hugolib/securitypolicies_test.go
new file mode 100644
index 000000000..297f49479
--- /dev/null
+++ b/hugolib/securitypolicies_test.go
@@ -0,0 +1,202 @@
+// Copyright 2019 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hugolib
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "runtime"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/markup/asciidocext"
+ "github.com/gohugoio/hugo/markup/pandoc"
+ "github.com/gohugoio/hugo/markup/rst"
+ "github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
+)
+
+func TestSecurityPolicies(t *testing.T) {
+ c := qt.New(t)
+
+ testVariant := func(c *qt.C, withBuilder func(b *sitesBuilder), expectErr string) {
+ c.Helper()
+ b := newTestSitesBuilder(c)
+ withBuilder(b)
+
+ if expectErr != "" {
+ err := b.BuildE(BuildCfg{})
+ b.Assert(err, qt.IsNotNil)
+ b.Assert(err, qt.ErrorMatches, expectErr)
+ } else {
+ b.Build(BuildCfg{})
+ }
+
+ }
+
+ httpTestVariant := func(c *qt.C, templ, expectErr string, withBuilder func(b *sitesBuilder)) {
+ ts := httptest.NewServer(http.FileServer(http.Dir("testdata/")))
+ c.Cleanup(func() {
+ ts.Close()
+ })
+ cb := func(b *sitesBuilder) {
+ b.WithTemplatesAdded("index.html", fmt.Sprintf(templ, ts.URL))
+ if withBuilder != nil {
+ withBuilder(b)
+ }
+ }
+ testVariant(c, cb, expectErr)
+ }
+
+ c.Run("os.GetEnv, denied", func(c *qt.C) {
+ c.Parallel()
+ cb := func(b *sitesBuilder) {
+ b.WithTemplatesAdded("index.html", `{{ os.Getenv "FOOBAR" }}`)
+ }
+ testVariant(c, cb, `(?s).*"FOOBAR" is not whitelisted in policy "security\.funcs\.getenv".*`)
+ })
+
+ c.Run("os.GetEnv, OK", func(c *qt.C) {
+ c.Parallel()
+ cb := func(b *sitesBuilder) {
+ b.WithTemplatesAdded("index.html", `{{ os.Getenv "HUGO_FOO" }}`)
+ }
+ testVariant(c, cb, "")
+ })
+
+ c.Run("Asciidoc, denied", func(c *qt.C) {
+ c.Parallel()
+ if !asciidocext.Supports() {
+ c.Skip()
+ }
+
+ cb := func(b *sitesBuilder) {
+ b.WithContent("page.ad", "foo")
+ }
+
+ testVariant(c, cb, `(?s).*"asciidoctor" is not whitelisted in policy "security\.exec\.allow".*`)
+ })
+
+ c.Run("RST, denied", func(c *qt.C) {
+ c.Parallel()
+ if !rst.Supports() {
+ c.Skip()
+ }
+
+ cb := func(b *sitesBuilder) {
+ b.WithContent("page.rst", "foo")
+ }
+
+ if runtime.GOOS == "windows" {
+ testVariant(c, cb, `(?s).*python(\.exe)?" is not whitelisted in policy "security\.exec\.allow".*`)
+ } else {
+ testVariant(c, cb, `(?s).*"rst2html(\.py)?" is not whitelisted in policy "security\.exec\.allow".*`)
+
+ }
+
+ })
+
+ c.Run("Pandoc, denied", func(c *qt.C) {
+ c.Parallel()
+ if !pandoc.Supports() {
+ c.Skip()
+ }
+
+ cb := func(b *sitesBuilder) {
+ b.WithContent("page.pdc", "foo")
+ }
+
+ testVariant(c, cb, `"(?s).*pandoc" is not whitelisted in policy "security\.exec\.allow".*`)
+ })
+
+ c.Run("Dart SASS, OK", func(c *qt.C) {
+ c.Parallel()
+ if !dartsass.Supports() {
+ c.Skip()
+ }
+ cb := func(b *sitesBuilder) {
+ b.WithTemplatesAdded("index.html", `{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | resources.ToCSS (dict "transpiler" "dartsass") }}`)
+ }
+ testVariant(c, cb, "")
+ })
+
+ c.Run("Dart SASS, denied", func(c *qt.C) {
+ c.Parallel()
+ if !dartsass.Supports() {
+ c.Skip()
+ }
+ cb := func(b *sitesBuilder) {
+ b.WithConfigFile("toml", `
+ [security]
+ [security.exec]
+ allow="none"
+
+ `)
+ b.WithTemplatesAdded("index.html", `{{ $scss := "body { color: #333; }" | resources.FromString "foo.scss" | resources.ToCSS (dict "transpiler" "dartsass") }}`)
+ }
+ testVariant(c, cb, `(?s).*"dart-sass-embedded" is not whitelisted in policy "security\.exec\.allow".*`)
+ })
+
+ c.Run("resources.Get, OK", func(c *qt.C) {
+ c.Parallel()
+ httpTestVariant(c, `{{ $json := resources.Get "%[1]s/fruits.json" }}{{ $json.Content }}`, "", nil)
+ })
+
+ c.Run("resources.Get, denied method", func(c *qt.C) {
+ c.Parallel()
+ httpTestVariant(c, `{{ $json := resources.Get "%[1]s/fruits.json" (dict "method" "DELETE" ) }}{{ $json.Content }}`, `(?s).*"DELETE" is not whitelisted in policy "security\.http\.method".*`, nil)
+ })
+
+ c.Run("resources.Get, denied URL", func(c *qt.C) {
+ c.Parallel()
+ httpTestVariant(c, `{{ $json := resources.Get "%[1]s/fruits.json" }}{{ $json.Content }}`, `(?s).*is not whitelisted in policy "security\.http\.urls".*`,
+ func(b *sitesBuilder) {
+ b.WithConfigFile("toml", `
+[security]
+[security.http]
+urls="none"
+`)
+ })
+ })
+
+ c.Run("getJSON, OK", func(c *qt.C) {
+ c.Parallel()
+ httpTestVariant(c, `{{ $json := getJSON "%[1]s/fruits.json" }}{{ $json.Content }}`, "", nil)
+ })
+
+ c.Run("getJSON, denied URL", func(c *qt.C) {
+ c.Parallel()
+ httpTestVariant(c, `{{ $json := getJSON "%[1]s/fruits.json" }}{{ $json.Content }}`, `(?s).*is not whitelisted in policy "security\.http\.urls".*`,
+ func(b *sitesBuilder) {
+ b.WithConfigFile("toml", `
+[security]
+[security.http]
+urls="none"
+`)
+ })
+ })
+
+ c.Run("getCSV, denied URL", func(c *qt.C) {
+ c.Parallel()
+ httpTestVariant(c, `{{ $d := getCSV ";" "%[1]s/cities.csv" }}{{ $d.Content }}`, `(?s).*is not whitelisted in policy "security\.http\.urls".*`,
+ func(b *sitesBuilder) {
+ b.WithConfigFile("toml", `
+[security]
+[security.http]
+urls="none"
+`)
+ })
+ })
+
+}
diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go
index 21d65de32..ec3a4a01b 100644
--- a/hugolib/shortcode.go
+++ b/hugolib/shortcode.go
@@ -257,7 +257,7 @@ func newShortcodeHandler(p *pageState, s *Site, placeholderFunc func() string) *
sh := &shortcodeHandler{
p: p,
s: s,
- enableInlineShortcodes: s.enableInlineShortcodes,
+ enableInlineShortcodes: s.ExecHelper.Sec().EnableInlineShortcodes,
shortcodes: make([]*shortcode, 0, 4),
nameSet: make(map[string]bool),
}
@@ -287,7 +287,7 @@ func renderShortcode(
var hasVariants bool
if sc.isInline {
- if !p.s.enableInlineShortcodes {
+ if !p.s.ExecHelper.Sec().EnableInlineShortcodes {
return "", false, nil
}
templName := path.Join("_inline_shortcode", p.File().Path(), sc.name)
diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go
index 6ef110c9b..6316afc98 100644
--- a/hugolib/shortcode_test.go
+++ b/hugolib/shortcode_test.go
@@ -619,6 +619,12 @@ title: "Foo"
cfg.Set("uglyURLs", false)
cfg.Set("verbose", true)
+ cfg.Set("security", map[string]interface{}{
+ "exec": map[string]interface{}{
+ "allow": []string{"^python$", "^rst2html.*", "^asciidoctor$"},
+ },
+ })
+
cfg.Set("markup.highlight.noClasses", false)
cfg.Set("markup.highlight.codeFences", true)
cfg.Set("markup", map[string]interface{}{
diff --git a/hugolib/site.go b/hugolib/site.go
index 96cf0b93c..dce4b8d25 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -120,8 +120,6 @@ type Site struct {
disabledKinds map[string]bool
- enableInlineShortcodes bool
-
// Output formats defined in site config per Page Kind, or some defaults
// if not set.
// Output formats defined in Page front matter will override these.
@@ -378,25 +376,24 @@ func (s *Site) isEnabled(kind string) bool {
// reset returns a new Site prepared for rebuild.
func (s *Site) reset() *Site {
return &Site{
- Deps: s.Deps,
- disabledKinds: s.disabledKinds,
- titleFunc: s.titleFunc,
- relatedDocsHandler: s.relatedDocsHandler.Clone(),
- siteRefLinker: s.siteRefLinker,
- outputFormats: s.outputFormats,
- rc: s.rc,
- outputFormatsConfig: s.outputFormatsConfig,
- frontmatterHandler: s.frontmatterHandler,
- mediaTypesConfig: s.mediaTypesConfig,
- language: s.language,
- siteBucket: s.siteBucket,
- h: s.h,
- publisher: s.publisher,
- siteConfigConfig: s.siteConfigConfig,
- enableInlineShortcodes: s.enableInlineShortcodes,
- init: s.init,
- PageCollections: s.PageCollections,
- siteCfg: s.siteCfg,
+ Deps: s.Deps,
+ disabledKinds: s.disabledKinds,
+ titleFunc: s.titleFunc,
+ relatedDocsHandler: s.relatedDocsHandler.Clone(),
+ siteRefLinker: s.siteRefLinker,
+ outputFormats: s.outputFormats,
+ rc: s.rc,
+ outputFormatsConfig: s.outputFormatsConfig,
+ frontmatterHandler: s.frontmatterHandler,
+ mediaTypesConfig: s.mediaTypesConfig,
+ language: s.language,
+ siteBucket: s.siteBucket,
+ h: s.h,
+ publisher: s.publisher,
+ siteConfigConfig: s.siteConfigConfig,
+ init: s.init,
+ PageCollections: s.PageCollections,
+ siteCfg: s.siteCfg,
}
}
@@ -564,8 +561,7 @@ But this also means that your site configuration may not do what you expect. If
outputFormatsConfig: siteOutputFormatsConfig,
mediaTypesConfig: siteMediaTypesConfig,
- enableInlineShortcodes: cfg.Language.GetBool("enableInlineShortcodes"),
- siteCfg: siteConfig,
+ siteCfg: siteConfig,
titleFunc: titleFunc,
diff --git a/hugolib/testdata/cities.csv b/hugolib/testdata/cities.csv
new file mode 100644
index 000000000..ee6b058b6
--- /dev/null
+++ b/hugolib/testdata/cities.csv
@@ -0,0 +1,130 @@
+"LatD", "LatM", "LatS", "NS", "LonD", "LonM", "LonS", "EW", "City", "State"
+ 41, 5, 59, "N", 80, 39, 0, "W", "Youngstown", OH
+ 42, 52, 48, "N", 97, 23, 23, "W", "Yankton", SD
+ 46, 35, 59, "N", 120, 30, 36, "W", "Yakima", WA
+ 42, 16, 12, "N", 71, 48, 0, "W", "Worcester", MA
+ 43, 37, 48, "N", 89, 46, 11, "W", "Wisconsin Dells", WI
+ 36, 5, 59, "N", 80, 15, 0, "W", "Winston-Salem", NC
+ 49, 52, 48, "N", 97, 9, 0, "W", "Winnipeg", MB
+ 39, 11, 23, "N", 78, 9, 36, "W", "Winchester", VA
+ 34, 14, 24, "N", 77, 55, 11, "W", "Wilmington", NC
+ 39, 45, 0, "N", 75, 33, 0, "W", "Wilmington", DE
+ 48, 9, 0, "N", 103, 37, 12, "W", "Williston", ND
+ 41, 15, 0, "N", 77, 0, 0, "W", "Williamsport", PA
+ 37, 40, 48, "N", 82, 16, 47, "W", "Williamson", WV
+ 33, 54, 0, "N", 98, 29, 23, "W", "Wichita Falls", TX
+ 37, 41, 23, "N", 97, 20, 23, "W", "Wichita", KS
+ 40, 4, 11, "N", 80, 43, 12, "W", "Wheeling", WV
+ 26, 43, 11, "N", 80, 3, 0, "W", "West Palm Beach", FL
+ 47, 25, 11, "N", 120, 19, 11, "W", "Wenatchee", WA
+ 41, 25, 11, "N", 122, 23, 23, "W", "Weed", CA
+ 31, 13, 11, "N", 82, 20, 59, "W", "Waycross", GA
+ 44, 57, 35, "N", 89, 38, 23, "W", "Wausau", WI
+ 42, 21, 36, "N", 87, 49, 48, "W", "Waukegan", IL
+ 44, 54, 0, "N", 97, 6, 36, "W", "Watertown", SD
+ 43, 58, 47, "N", 75, 55, 11, "W", "Watertown", NY
+ 42, 30, 0, "N", 92, 20, 23, "W", "Waterloo", IA
+ 41, 32, 59, "N", 73, 3, 0, "W", "Waterbury", CT
+ 38, 53, 23, "N", 77, 1, 47, "W", "Washington", DC
+ 41, 50, 59, "N", 79, 8, 23, "W", "Warren", PA
+ 46, 4, 11, "N", 118, 19, 48, "W", "Walla Walla", WA
+ 31, 32, 59, "N", 97, 8, 23, "W", "Waco", TX
+ 38, 40, 48, "N", 87, 31, 47, "W", "Vincennes", IN
+ 28, 48, 35, "N", 97, 0, 36, "W", "Victoria", TX
+ 32, 20, 59, "N", 90, 52, 47, "W", "Vicksburg", MS
+ 49, 16, 12, "N", 123, 7, 12, "W", "Vancouver", BC
+ 46, 55, 11, "N", 98, 0, 36, "W", "Valley City", ND
+ 30, 49, 47, "N", 83, 16, 47, "W", "Valdosta", GA
+ 43, 6, 36, "N", 75, 13, 48, "W", "Utica", NY
+ 39, 54, 0, "N", 79, 43, 48, "W", "Uniontown", PA
+ 32, 20, 59, "N", 95, 18, 0, "W", "Tyler", TX
+ 42, 33, 36, "N", 114, 28, 12, "W", "Twin Falls", ID
+ 33, 12, 35, "N", 87, 34, 11, "W", "Tuscaloosa", AL
+ 34, 15, 35, "N", 88, 42, 35, "W", "Tupelo", MS
+ 36, 9, 35, "N", 95, 54, 36, "W", "Tulsa", OK
+ 32, 13, 12, "N", 110, 58, 12, "W", "Tucson", AZ
+ 37, 10, 11, "N", 104, 30, 36, "W", "Trinidad", CO
+ 40, 13, 47, "N", 74, 46, 11, "W", "Trenton", NJ
+ 44, 45, 35, "N", 85, 37, 47, "W", "Traverse City", MI
+ 43, 39, 0, "N", 79, 22, 47, "W", "Toronto", ON
+ 39, 2, 59, "N", 95, 40, 11, "W", "Topeka", KS
+ 41, 39, 0, "N", 83, 32, 24, "W", "Toledo", OH
+ 33, 25, 48, "N", 94, 3, 0, "W", "Texarkana", TX
+ 39, 28, 12, "N", 87, 24, 36, "W", "Terre Haute", IN
+ 27, 57, 0, "N", 82, 26, 59, "W", "Tampa", FL
+ 30, 27, 0, "N", 84, 16, 47, "W", "Tallahassee", FL
+ 47, 14, 24, "N", 122, 25, 48, "W", "Tacoma", WA
+ 43, 2, 59, "N", 76, 9, 0, "W", "Syracuse", NY
+ 32, 35, 59, "N", 82, 20, 23, "W", "Swainsboro", GA
+ 33, 55, 11, "N", 80, 20, 59, "W", "Sumter", SC
+ 40, 59, 24, "N", 75, 11, 24, "W", "Stroudsburg", PA
+ 37, 57, 35, "N", 121, 17, 24, "W", "Stockton", CA
+ 44, 31, 12, "N", 89, 34, 11, "W", "Stevens Point", WI
+ 40, 21, 36, "N", 80, 37, 12, "W", "Steubenville", OH
+ 40, 37, 11, "N", 103, 13, 12, "W", "Sterling", CO
+ 38, 9, 0, "N", 79, 4, 11, "W", "Staunton", VA
+ 39, 55, 11, "N", 83, 48, 35, "W", "Springfield", OH
+ 37, 13, 12, "N", 93, 17, 24, "W", "Springfield", MO
+ 42, 5, 59, "N", 72, 35, 23, "W", "Springfield", MA
+ 39, 47, 59, "N", 89, 39, 0, "W", "Springfield", IL
+ 47, 40, 11, "N", 117, 24, 36, "W", "Spokane", WA
+ 41, 40, 48, "N", 86, 15, 0, "W", "South Bend", IN
+ 43, 32, 24, "N", 96, 43, 48, "W", "Sioux Falls", SD
+ 42, 29, 24, "N", 96, 23, 23, "W", "Sioux City", IA
+ 32, 30, 35, "N", 93, 45, 0, "W", "Shreveport", LA
+ 33, 38, 23, "N", 96, 36, 36, "W", "Sherman", TX
+ 44, 47, 59, "N", 106, 57, 35, "W", "Sheridan", WY
+ 35, 13, 47, "N", 96, 40, 48, "W", "Seminole", OK
+ 32, 25, 11, "N", 87, 1, 11, "W", "Selma", AL
+ 38, 42, 35, "N", 93, 13, 48, "W", "Sedalia", MO
+ 47, 35, 59, "N", 122, 19, 48, "W", "Seattle", WA
+ 41, 24, 35, "N", 75, 40, 11, "W", "Scranton", PA
+ 41, 52, 11, "N", 103, 39, 36, "W", "Scottsbluff", NB
+ 42, 49, 11, "N", 73, 56, 59, "W", "Schenectady", NY
+ 32, 4, 48, "N", 81, 5, 23, "W", "Savannah", GA
+ 46, 29, 24, "N", 84, 20, 59, "W", "Sault Sainte Marie", MI
+ 27, 20, 24, "N", 82, 31, 47, "W", "Sarasota", FL
+ 38, 26, 23, "N", 122, 43, 12, "W", "Santa Rosa", CA
+ 35, 40, 48, "N", 105, 56, 59, "W", "Santa Fe", NM
+ 34, 25, 11, "N", 119, 41, 59, "W", "Santa Barbara", CA
+ 33, 45, 35, "N", 117, 52, 12, "W", "Santa Ana", CA
+ 37, 20, 24, "N", 121, 52, 47, "W", "San Jose", CA
+ 37, 46, 47, "N", 122, 25, 11, "W", "San Francisco", CA
+ 41, 27, 0, "N", 82, 42, 35, "W", "Sandusky", OH
+ 32, 42, 35, "N", 117, 9, 0, "W", "San Diego", CA
+ 34, 6, 36, "N", 117, 18, 35, "W", "San Bernardino", CA
+ 29, 25, 12, "N", 98, 30, 0, "W", "San Antonio", TX
+ 31, 27, 35, "N", 100, 26, 24, "W", "San Angelo", TX
+ 40, 45, 35, "N", 111, 52, 47, "W", "Salt Lake City", UT
+ 38, 22, 11, "N", 75, 35, 59, "W", "Salisbury", MD
+ 36, 40, 11, "N", 121, 39, 0, "W", "Salinas", CA
+ 38, 50, 24, "N", 97, 36, 36, "W", "Salina", KS
+ 38, 31, 47, "N", 106, 0, 0, "W", "Salida", CO
+ 44, 56, 23, "N", 123, 1, 47, "W", "Salem", OR
+ 44, 57, 0, "N", 93, 5, 59, "W", "Saint Paul", MN
+ 38, 37, 11, "N", 90, 11, 24, "W", "Saint Louis", MO
+ 39, 46, 12, "N", 94, 50, 23, "W", "Saint Joseph", MO
+ 42, 5, 59, "N", 86, 28, 48, "W", "Saint Joseph", MI
+ 44, 25, 11, "N", 72, 1, 11, "W", "Saint Johnsbury", VT
+ 45, 34, 11, "N", 94, 10, 11, "W", "Saint Cloud", MN
+ 29, 53, 23, "N", 81, 19, 11, "W", "Saint Augustine", FL
+ 43, 25, 48, "N", 83, 56, 24, "W", "Saginaw", MI
+ 38, 35, 24, "N", 121, 29, 23, "W", "Sacramento", CA
+ 43, 36, 36, "N", 72, 58, 12, "W", "Rutland", VT
+ 33, 24, 0, "N", 104, 31, 47, "W", "Roswell", NM
+ 35, 56, 23, "N", 77, 48, 0, "W", "Rocky Mount", NC
+ 41, 35, 24, "N", 109, 13, 48, "W", "Rock Springs", WY
+ 42, 16, 12, "N", 89, 5, 59, "W", "Rockford", IL
+ 43, 9, 35, "N", 77, 36, 36, "W", "Rochester", NY
+ 44, 1, 12, "N", 92, 27, 35, "W", "Rochester", MN
+ 37, 16, 12, "N", 79, 56, 24, "W", "Roanoke", VA
+ 37, 32, 24, "N", 77, 26, 59, "W", "Richmond", VA
+ 39, 49, 48, "N", 84, 53, 23, "W", "Richmond", IN
+ 38, 46, 12, "N", 112, 5, 23, "W", "Richfield", UT
+ 45, 38, 23, "N", 89, 25, 11, "W", "Rhinelander", WI
+ 39, 31, 12, "N", 119, 48, 35, "W", "Reno", NV
+ 50, 25, 11, "N", 104, 39, 0, "W", "Regina", SA
+ 40, 10, 48, "N", 122, 14, 23, "W", "Red Bluff", CA
+ 40, 19, 48, "N", 75, 55, 48, "W", "Reading", PA
+ 41, 9, 35, "N", 81, 14, 23, "W", "Ravenna", OH
+
diff --git a/hugolib/testdata/fruits.json b/hugolib/testdata/fruits.json
new file mode 100644
index 000000000..3bb802a16
--- /dev/null
+++ b/hugolib/testdata/fruits.json
@@ -0,0 +1,5 @@
+{
+ "fruit": "Apple",
+ "size": "Large",
+ "color": "Red"
+}
diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go
index ba3965675..72e22ed1d 100644
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -18,6 +18,7 @@ import (
"time"
"unicode/utf8"
+ "github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/output"
@@ -30,6 +31,7 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps"
@@ -791,6 +793,16 @@ func (s *sitesBuilder) GetPageRel(p page.Page, ref string) page.Page {
return p
}
+func (s *sitesBuilder) NpmInstall() hexec.Runner {
+ sc := security.DefaultConfig