mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
f4389e48ce
This ommmit contains some security hardening measures for the Hugo build runtime. There are some rarely used features in Hugo that would be good to have disabled by default. One example would be the "external helpers". For `asciidoctor` and some others we use Go's `os/exec` package to start a new process. These are a predefined set of binary names, all loaded from `PATH` and with a predefined set of arguments. Still, if you don't use `asciidoctor` in your project, you might as well have it turned off. You can configure your own in the new `security` configuration section, but the defaults are configured to create a minimal amount of site breakage. And if that do happen, you will get clear instructions in the loa about what to do. The default configuration is listed below. Note that almost all of these options are regular expression _whitelists_ (a string or a slice); the value `none` will block all. ```toml [security] enableInlineShortcodes = false [security.exec] allow = ['^dart-sass-embedded$', '^go$', '^npx$', '^postcss$'] osEnv = ['(?i)^(PATH|PATHEXT|APPDATA|TMP|TEMP|TERM)$'] [security.funcs] getenv = ['^HUGO_'] [security.http] methods = ['(?i)GET|POST'] urls = ['.*'] ```
1099 lines
27 KiB
Go
1099 lines
27 KiB
Go
package hugolib
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image/jpeg"
|
|
"io"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/gohugoio/hugo/config/security"
|
|
"github.com/gohugoio/hugo/htesting"
|
|
|
|
"github.com/gohugoio/hugo/output"
|
|
|
|
"github.com/gohugoio/hugo/parser/metadecoders"
|
|
"github.com/google/go-cmp/cmp"
|
|
|
|
"github.com/gohugoio/hugo/parser"
|
|
"github.com/pkg/errors"
|
|
|
|
"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"
|
|
"github.com/gohugoio/hugo/resources/page"
|
|
"github.com/sanity-io/litter"
|
|
"github.com/spf13/afero"
|
|
"github.com/spf13/cast"
|
|
|
|
"github.com/gohugoio/hugo/helpers"
|
|
"github.com/gohugoio/hugo/tpl"
|
|
|
|
"github.com/gohugoio/hugo/resources/resource"
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
"github.com/gohugoio/hugo/common/loggers"
|
|
"github.com/gohugoio/hugo/hugofs"
|
|
)
|
|
|
|
var (
|
|
deepEqualsPages = qt.CmpEquals(cmp.Comparer(func(p1, p2 *pageState) bool { return p1 == p2 }))
|
|
deepEqualsOutputFormats = qt.CmpEquals(cmp.Comparer(func(o1, o2 output.Format) bool {
|
|
return o1.Name == o2.Name && o1.MediaType.Type() == o2.MediaType.Type()
|
|
}))
|
|
)
|
|
|
|
type sitesBuilder struct {
|
|
Cfg config.Provider
|
|
environ []string
|
|
|
|
Fs *hugofs.Fs
|
|
T testing.TB
|
|
depsCfg deps.DepsCfg
|
|
|
|
*qt.C
|
|
|
|
logger loggers.Logger
|
|
rnd *rand.Rand
|
|
dumper litter.Options
|
|
|
|
// Used to test partial rebuilds.
|
|
changedFiles []string
|
|
removedFiles []string
|
|
|
|
// Aka the Hugo server mode.
|
|
running bool
|
|
|
|
H *HugoSites
|
|
|
|
theme string
|
|
|
|
// Default toml
|
|
configFormat string
|
|
configFileSet bool
|
|
configSet bool
|
|
|
|
// Default is empty.
|
|
// TODO(bep) revisit this and consider always setting it to something.
|
|
// Consider this in relation to using the BaseFs.PublishFs to all publishing.
|
|
workingDir string
|
|
|
|
addNothing bool
|
|
// Base data/content
|
|
contentFilePairs []filenameContent
|
|
templateFilePairs []filenameContent
|
|
i18nFilePairs []filenameContent
|
|
dataFilePairs []filenameContent
|
|
|
|
// Additional data/content.
|
|
// As in "use the base, but add these on top".
|
|
contentFilePairsAdded []filenameContent
|
|
templateFilePairsAdded []filenameContent
|
|
i18nFilePairsAdded []filenameContent
|
|
dataFilePairsAdded []filenameContent
|
|
}
|
|
|
|
type filenameContent struct {
|
|
filename string
|
|
content string
|
|
}
|
|
|
|
func newTestSitesBuilder(t testing.TB) *sitesBuilder {
|
|
v := config.New()
|
|
fs := hugofs.NewMem(v)
|
|
|
|
litterOptions := litter.Options{
|
|
HidePrivateFields: true,
|
|
StripPackageNames: true,
|
|
Separator: " ",
|
|
}
|
|
|
|
return &sitesBuilder{
|
|
T: t, C: qt.New(t), Fs: fs, configFormat: "toml",
|
|
dumper: litterOptions, rnd: rand.New(rand.NewSource(time.Now().Unix())),
|
|
}
|
|
}
|
|
|
|
func newTestSitesBuilderFromDepsCfg(t testing.TB, d deps.DepsCfg) *sitesBuilder {
|
|
c := qt.New(t)
|
|
|
|
litterOptions := litter.Options{
|
|
HidePrivateFields: true,
|
|
StripPackageNames: true,
|
|
Separator: " ",
|
|
}
|
|
|
|
b := &sitesBuilder{T: t, C: c, depsCfg: d, Fs: d.Fs, dumper: litterOptions, rnd: rand.New(rand.NewSource(time.Now().Unix()))}
|
|
workingDir := d.Cfg.GetString("workingDir")
|
|
|
|
b.WithWorkingDir(workingDir)
|
|
|
|
return b.WithViper(d.Cfg.(config.Provider))
|
|
}
|
|
|
|
func (s *sitesBuilder) Running() *sitesBuilder {
|
|
s.running = true
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithNothingAdded() *sitesBuilder {
|
|
s.addNothing = true
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithLogger(logger loggers.Logger) *sitesBuilder {
|
|
s.logger = logger
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithWorkingDir(dir string) *sitesBuilder {
|
|
s.workingDir = filepath.FromSlash(dir)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithEnviron(env ...string) *sitesBuilder {
|
|
for i := 0; i < len(env); i += 2 {
|
|
s.environ = append(s.environ, fmt.Sprintf("%s=%s", env[i], env[i+1]))
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithConfigTemplate(data interface{}, format, configTemplate string) *sitesBuilder {
|
|
s.T.Helper()
|
|
|
|
if format == "" {
|
|
format = "toml"
|
|
}
|
|
|
|
templ, err := template.New("test").Parse(configTemplate)
|
|
if err != nil {
|
|
s.Fatalf("Template parse failed: %s", err)
|
|
}
|
|
var b bytes.Buffer
|
|
templ.Execute(&b, data)
|
|
return s.WithConfigFile(format, b.String())
|
|
}
|
|
|
|
func (s *sitesBuilder) WithViper(v config.Provider) *sitesBuilder {
|
|
s.T.Helper()
|
|
if s.configFileSet {
|
|
s.T.Fatal("WithViper: use Viper or config.toml, not both")
|
|
}
|
|
defer func() {
|
|
s.configSet = true
|
|
}()
|
|
|
|
// Write to a config file to make sure the tests follow the same code path.
|
|
var buff bytes.Buffer
|
|
m := v.Get("").(maps.Params)
|
|
s.Assert(parser.InterfaceToConfig(m, metadecoders.TOML, &buff), qt.IsNil)
|
|
return s.WithConfigFile("toml", buff.String())
|
|
}
|
|
|
|
func (s *sitesBuilder) WithConfigFile(format, conf string) *sitesBuilder {
|
|
s.T.Helper()
|
|
if s.configSet {
|
|
s.T.Fatal("WithConfigFile: use config.Config or config.toml, not both")
|
|
}
|
|
s.configFileSet = true
|
|
filename := s.absFilename("config." + format)
|
|
writeSource(s.T, s.Fs, filename, conf)
|
|
s.configFormat = format
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithThemeConfigFile(format, conf string) *sitesBuilder {
|
|
s.T.Helper()
|
|
if s.theme == "" {
|
|
s.theme = "test-theme"
|
|
}
|
|
filename := filepath.Join("themes", s.theme, "config."+format)
|
|
writeSource(s.T, s.Fs, s.absFilename(filename), conf)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithSourceFile(filenameContent ...string) *sitesBuilder {
|
|
s.T.Helper()
|
|
for i := 0; i < len(filenameContent); i += 2 {
|
|
writeSource(s.T, s.Fs, s.absFilename(filenameContent[i]), filenameContent[i+1])
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) absFilename(filename string) string {
|
|
filename = filepath.FromSlash(filename)
|
|
if filepath.IsAbs(filename) {
|
|
return filename
|
|
}
|
|
if s.workingDir != "" && !strings.HasPrefix(filename, s.workingDir) {
|
|
filename = filepath.Join(s.workingDir, filename)
|
|
}
|
|
return filename
|
|
}
|
|
|
|
const commonConfigSections = `
|
|
|
|
[services]
|
|
[services.disqus]
|
|
shortname = "disqus_shortname"
|
|
[services.googleAnalytics]
|
|
id = "UA-ga_id"
|
|
|
|
[privacy]
|
|
[privacy.disqus]
|
|
disable = false
|
|
[privacy.googleAnalytics]
|
|
respectDoNotTrack = true
|
|
anonymizeIP = true
|
|
[privacy.instagram]
|
|
simple = true
|
|
[privacy.twitter]
|
|
enableDNT = true
|
|
[privacy.vimeo]
|
|
disable = false
|
|
[privacy.youtube]
|
|
disable = false
|
|
privacyEnhanced = true
|
|
|
|
`
|
|
|
|
func (s *sitesBuilder) WithSimpleConfigFile() *sitesBuilder {
|
|
s.T.Helper()
|
|
return s.WithSimpleConfigFileAndBaseURL("http://example.com/")
|
|
}
|
|
|
|
func (s *sitesBuilder) WithSimpleConfigFileAndBaseURL(baseURL string) *sitesBuilder {
|
|
s.T.Helper()
|
|
return s.WithSimpleConfigFileAndSettings(map[string]interface{}{"baseURL": baseURL})
|
|
}
|
|
|
|
func (s *sitesBuilder) WithSimpleConfigFileAndSettings(settings interface{}) *sitesBuilder {
|
|
s.T.Helper()
|
|
var buf bytes.Buffer
|
|
parser.InterfaceToConfig(settings, metadecoders.TOML, &buf)
|
|
config := buf.String() + commonConfigSections
|
|
return s.WithConfigFile("toml", config)
|
|
}
|
|
|
|
func (s *sitesBuilder) WithDefaultMultiSiteConfig() *sitesBuilder {
|
|
defaultMultiSiteConfig := `
|
|
baseURL = "http://example.com/blog"
|
|
|
|
paginate = 1
|
|
disablePathToLower = true
|
|
defaultContentLanguage = "en"
|
|
defaultContentLanguageInSubdir = true
|
|
|
|
[permalinks]
|
|
other = "/somewhere/else/:filename"
|
|
|
|
[blackfriday]
|
|
angledQuotes = true
|
|
|
|
[Taxonomies]
|
|
tag = "tags"
|
|
|
|
[Languages]
|
|
[Languages.en]
|
|
weight = 10
|
|
title = "In English"
|
|
languageName = "English"
|
|
[Languages.en.blackfriday]
|
|
angledQuotes = false
|
|
[[Languages.en.menu.main]]
|
|
url = "/"
|
|
name = "Home"
|
|
weight = 0
|
|
|
|
[Languages.fr]
|
|
weight = 20
|
|
title = "Le Français"
|
|
languageName = "Français"
|
|
[Languages.fr.Taxonomies]
|
|
plaque = "plaques"
|
|
|
|
[Languages.nn]
|
|
weight = 30
|
|
title = "På nynorsk"
|
|
languageName = "Nynorsk"
|
|
paginatePath = "side"
|
|
[Languages.nn.Taxonomies]
|
|
lag = "lag"
|
|
[[Languages.nn.menu.main]]
|
|
url = "/"
|
|
name = "Heim"
|
|
weight = 1
|
|
|
|
[Languages.nb]
|
|
weight = 40
|
|
title = "På bokmål"
|
|
languageName = "Bokmål"
|
|
paginatePath = "side"
|
|
[Languages.nb.Taxonomies]
|
|
lag = "lag"
|
|
` + commonConfigSections
|
|
|
|
return s.WithConfigFile("toml", defaultMultiSiteConfig)
|
|
}
|
|
|
|
func (s *sitesBuilder) WithSunset(in string) {
|
|
// Write a real image into one of the bundle above.
|
|
src, err := os.Open(filepath.FromSlash("testdata/sunset.jpg"))
|
|
s.Assert(err, qt.IsNil)
|
|
|
|
out, err := s.Fs.Source.Create(filepath.FromSlash(filepath.Join(s.workingDir, in)))
|
|
s.Assert(err, qt.IsNil)
|
|
|
|
_, err = io.Copy(out, src)
|
|
s.Assert(err, qt.IsNil)
|
|
|
|
out.Close()
|
|
src.Close()
|
|
}
|
|
|
|
func (s *sitesBuilder) createFilenameContent(pairs []string) []filenameContent {
|
|
var slice []filenameContent
|
|
s.appendFilenameContent(&slice, pairs...)
|
|
return slice
|
|
}
|
|
|
|
func (s *sitesBuilder) appendFilenameContent(slice *[]filenameContent, pairs ...string) {
|
|
if len(pairs)%2 != 0 {
|
|
panic("file content mismatch")
|
|
}
|
|
for i := 0; i < len(pairs); i += 2 {
|
|
c := filenameContent{
|
|
filename: pairs[i],
|
|
content: pairs[i+1],
|
|
}
|
|
*slice = append(*slice, c)
|
|
}
|
|
}
|
|
|
|
func (s *sitesBuilder) WithContent(filenameContent ...string) *sitesBuilder {
|
|
s.appendFilenameContent(&s.contentFilePairs, filenameContent...)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithContentAdded(filenameContent ...string) *sitesBuilder {
|
|
s.appendFilenameContent(&s.contentFilePairsAdded, filenameContent...)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithTemplates(filenameContent ...string) *sitesBuilder {
|
|
s.appendFilenameContent(&s.templateFilePairs, filenameContent...)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithTemplatesAdded(filenameContent ...string) *sitesBuilder {
|
|
s.appendFilenameContent(&s.templateFilePairsAdded, filenameContent...)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithData(filenameContent ...string) *sitesBuilder {
|
|
s.appendFilenameContent(&s.dataFilePairs, filenameContent...)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithDataAdded(filenameContent ...string) *sitesBuilder {
|
|
s.appendFilenameContent(&s.dataFilePairsAdded, filenameContent...)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithI18n(filenameContent ...string) *sitesBuilder {
|
|
s.appendFilenameContent(&s.i18nFilePairs, filenameContent...)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder {
|
|
s.appendFilenameContent(&s.i18nFilePairsAdded, filenameContent...)
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder {
|
|
for i := 0; i < len(filenameContent); i += 2 {
|
|
filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1]
|
|
absFilename := s.absFilename(filename)
|
|
s.changedFiles = append(s.changedFiles, absFilename)
|
|
writeSource(s.T, s.Fs, absFilename, content)
|
|
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) RemoveFiles(filenames ...string) *sitesBuilder {
|
|
for _, filename := range filenames {
|
|
absFilename := s.absFilename(filename)
|
|
s.removedFiles = append(s.removedFiles, absFilename)
|
|
s.Assert(s.Fs.Source.Remove(absFilename), qt.IsNil)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) writeFilePairs(folder string, files []filenameContent) *sitesBuilder {
|
|
// We have had some "filesystem ordering" bugs that we have not discovered in
|
|
// our tests running with the in memory filesystem.
|
|
// That file system is backed by a map so not sure how this helps, but some
|
|
// randomness in tests doesn't hurt.
|
|
// TODO(bep) this turns out to be more confusing than helpful.
|
|
// s.rnd.Shuffle(len(files), func(i, j int) { files[i], files[j] = files[j], files[i] })
|
|
|
|
for _, fc := range files {
|
|
target := folder
|
|
// TODO(bep) clean up this magic.
|
|
if strings.HasPrefix(fc.filename, folder) {
|
|
target = ""
|
|
}
|
|
|
|
if s.workingDir != "" {
|
|
target = filepath.Join(s.workingDir, target)
|
|
}
|
|
|
|
writeSource(s.T, s.Fs, filepath.Join(target, fc.filename), fc.content)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) CreateSites() *sitesBuilder {
|
|
if err := s.CreateSitesE(); err != nil {
|
|
herrors.PrintStackTraceFromErr(err)
|
|
s.Fatalf("Failed to create sites: %s", err)
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) LoadConfig() error {
|
|
if !s.configFileSet {
|
|
s.WithSimpleConfigFile()
|
|
}
|
|
|
|
cfg, _, err := LoadConfig(ConfigSourceDescriptor{
|
|
WorkingDir: s.workingDir,
|
|
Fs: s.Fs.Source,
|
|
Logger: s.logger,
|
|
Environ: s.environ,
|
|
Filename: "config." + s.configFormat,
|
|
}, func(cfg config.Provider) error {
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
s.Cfg = cfg
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *sitesBuilder) CreateSitesE() error {
|
|
if !s.addNothing {
|
|
if _, ok := s.Fs.Source.(*afero.OsFs); ok {
|
|
for _, dir := range []string{
|
|
"content/sect",
|
|
"layouts/_default",
|
|
"layouts/_default/_markup",
|
|
"layouts/partials",
|
|
"layouts/shortcodes",
|
|
"data",
|
|
"i18n",
|
|
} {
|
|
if err := os.MkdirAll(filepath.Join(s.workingDir, dir), 0777); err != nil {
|
|
return errors.Wrapf(err, "failed to create %q", dir)
|
|
}
|
|
}
|
|
}
|
|
|
|
s.addDefaults()
|
|
s.writeFilePairs("content", s.contentFilePairsAdded)
|
|
s.writeFilePairs("layouts", s.templateFilePairsAdded)
|
|
s.writeFilePairs("data", s.dataFilePairsAdded)
|
|
s.writeFilePairs("i18n", s.i18nFilePairsAdded)
|
|
|
|
s.writeFilePairs("i18n", s.i18nFilePairs)
|
|
s.writeFilePairs("data", s.dataFilePairs)
|
|
s.writeFilePairs("content", s.contentFilePairs)
|
|
s.writeFilePairs("layouts", s.templateFilePairs)
|
|
|
|
}
|
|
|
|
if err := s.LoadConfig(); err != nil {
|
|
return errors.Wrap(err, "failed to load config")
|
|
}
|
|
|
|
s.Fs.Destination = hugofs.NewCreateCountingFs(s.Fs.Destination)
|
|
|
|
depsCfg := s.depsCfg
|
|
depsCfg.Fs = s.Fs
|
|
depsCfg.Cfg = s.Cfg
|
|
depsCfg.Logger = s.logger
|
|
depsCfg.Running = s.running
|
|
|
|
sites, err := NewHugoSites(depsCfg)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to create sites")
|
|
}
|
|
s.H = sites
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *sitesBuilder) BuildE(cfg BuildCfg) error {
|
|
if s.H == nil {
|
|
s.CreateSites()
|
|
}
|
|
|
|
return s.H.Build(cfg)
|
|
}
|
|
|
|
func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder {
|
|
s.T.Helper()
|
|
return s.build(cfg, false)
|
|
}
|
|
|
|
func (s *sitesBuilder) BuildFail(cfg BuildCfg) *sitesBuilder {
|
|
s.T.Helper()
|
|
return s.build(cfg, true)
|
|
}
|
|
|
|
func (s *sitesBuilder) changeEvents() []fsnotify.Event {
|
|
var events []fsnotify.Event
|
|
|
|
for _, v := range s.changedFiles {
|
|
events = append(events, fsnotify.Event{
|
|
Name: v,
|
|
Op: fsnotify.Write,
|
|
})
|
|
}
|
|
for _, v := range s.removedFiles {
|
|
events = append(events, fsnotify.Event{
|
|
Name: v,
|
|
Op: fsnotify.Remove,
|
|
})
|
|
}
|
|
|
|
return events
|
|
}
|
|
|
|
func (s *sitesBuilder) build(cfg BuildCfg, shouldFail bool) *sitesBuilder {
|
|
s.Helper()
|
|
defer func() {
|
|
s.changedFiles = nil
|
|
}()
|
|
|
|
if s.H == nil {
|
|
s.CreateSites()
|
|
}
|
|
|
|
err := s.H.Build(cfg, s.changeEvents()...)
|
|
|
|
if err == nil {
|
|
logErrorCount := s.H.NumLogErrors()
|
|
if logErrorCount > 0 {
|
|
err = fmt.Errorf("logged %d errors", logErrorCount)
|
|
}
|
|
}
|
|
if err != nil && !shouldFail {
|
|
herrors.PrintStackTraceFromErr(err)
|
|
s.Fatalf("Build failed: %s", err)
|
|
} else if err == nil && shouldFail {
|
|
s.Fatalf("Expected error")
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (s *sitesBuilder) addDefaults() {
|
|
var (
|
|
contentTemplate = `---
|
|
title: doc1
|
|
weight: 1
|
|
tags:
|
|
- tag1
|
|
date: "2018-02-28"
|
|
---
|
|
# doc1
|
|
*some "content"*
|
|
{{< shortcode >}}
|
|
{{< lingo >}}
|
|
`
|
|
|
|
defaultContent = []string{
|
|
"content/sect/doc1.en.md", contentTemplate,
|
|
"content/sect/doc1.fr.md", contentTemplate,
|
|
"content/sect/doc1.nb.md", contentTemplate,
|
|
"content/sect/doc1.nn.md", contentTemplate,
|
|
}
|
|
|
|
listTemplateCommon = "{{ $p := .Paginator }}{{ $p.PageNumber }}|{{ .Title }}|{{ i18n \"hello\" }}|{{ .Permalink }}|Pager: {{ template \"_internal/pagination.html\" . }}|Kind: {{ .Kind }}|Content: {{ .Content }}|Len Pages: {{ len .Pages }}|Len RegularPages: {{ len .RegularPages }}| HasParent: {{ if .Parent }}YES{{ else }}NO{{ end }}"
|
|
|
|
defaultTemplates = []string{
|
|
"_default/single.html", "Single: {{ .Title }}|{{ i18n \"hello\" }}|{{.Language.Lang}}|RelPermalink: {{ .RelPermalink }}|Permalink: {{ .Permalink }}|{{ .Content }}|Resources: {{ range .Resources }}{{ .MediaType }}: {{ .RelPermalink}} -- {{ end }}|Summary: {{ .Summary }}|Truncated: {{ .Truncated }}|Parent: {{ .Parent.Title }}",
|
|
"_default/list.html", "List Page " + listTemplateCommon,
|
|
"index.html", "{{ $p := .Paginator }}Default Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}|String Resource: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").RelPermalink }}",
|
|
"index.fr.html", "{{ $p := .Paginator }}French Home Page {{ $p.PageNumber }}: {{ .Title }}|{{ .IsHome }}|{{ i18n \"hello\" }}|{{ .Permalink }}|{{ .Site.Data.hugo.slogan }}|String Resource: {{ ( \"Hugo Pipes\" | resources.FromString \"text/pipes.txt\").RelPermalink }}",
|
|
"_default/terms.html", "Taxonomy Term Page " + listTemplateCommon,
|
|
"_default/taxonomy.html", "Taxonomy List Page " + listTemplateCommon,
|
|
// Shortcodes
|
|
"shortcodes/shortcode.html", "Shortcode: {{ i18n \"hello\" }}",
|
|
// A shortcode in multiple languages
|
|
"shortcodes/lingo.html", "LingoDefault",
|
|
"shortcodes/lingo.fr.html", "LingoFrench",
|
|
// Special templates
|
|
"404.html", "404|{{ .Lang }}|{{ .Title }}",
|
|
"robots.txt", "robots|{{ .Lang }}|{{ .Title }}",
|
|
}
|
|
|
|
defaultI18n = []string{
|
|
"en.yaml", `
|
|
hello:
|
|
other: "Hello"
|
|
`,
|
|
"fr.yaml", `
|
|
hello:
|
|
other: "Bonjour"
|
|
`,
|
|
}
|
|
|
|
defaultData = []string{
|
|
"hugo.toml", "slogan = \"Hugo Rocks!\"",
|
|
}
|
|
)
|
|
|
|
if len(s.contentFilePairs) == 0 {
|
|
s.writeFilePairs("content", s.createFilenameContent(defaultContent))
|
|
}
|
|
|
|
if len(s.templateFilePairs) == 0 {
|
|
s.writeFilePairs("layouts", s.createFilenameContent(defaultTemplates))
|
|
}
|
|
if len(s.dataFilePairs) == 0 {
|
|
s.writeFilePairs("data", s.createFilenameContent(defaultData))
|
|
}
|
|
if len(s.i18nFilePairs) == 0 {
|
|
s.writeFilePairs("i18n", s.createFilenameContent(defaultI18n))
|
|
}
|
|
}
|
|
|
|
func (s *sitesBuilder) Fatalf(format string, args ...interface{}) {
|
|
s.T.Helper()
|
|
s.T.Fatalf(format, args...)
|
|
}
|
|
|
|
func (s *sitesBuilder) AssertFileContentFn(filename string, f func(s string) bool) {
|
|
s.T.Helper()
|
|
content := s.FileContent(filename)
|
|
if !f(content) {
|
|
s.Fatalf("Assert failed for %q in content\n%s", filename, content)
|
|
}
|
|
}
|
|
|
|
func (s *sitesBuilder) AssertHome(matches ...string) {
|
|
s.AssertFileContent("public/index.html", matches...)
|
|
}
|
|
|
|
func (s *sitesBuilder) AssertFileContent(filename string, matches ...string) {
|
|
s.T.Helper()
|
|
content := s.FileContent(filename)
|
|
for _, m := range matches {
|
|
lines := strings.Split(m, "\n")
|
|
for _, match := range lines {
|
|
match = strings.TrimSpace(match)
|
|
if match == "" {
|
|
continue
|
|
}
|
|
if !strings.Contains(content, match) {
|
|
s.Fatalf("No match for %q in content for %s\n%s\n%q", match, filename, content, content)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *sitesBuilder) AssertFileDoesNotExist(filename string) {
|
|
if s.CheckExists(filename) {
|
|
s.Fatalf("File %q exists but must not exist.", filename)
|
|
}
|
|
}
|
|
|
|
func (s *sitesBuilder) AssertImage(width, height int, filename string) {
|
|
filename = filepath.Join(s.workingDir, filename)
|
|
f, err := s.Fs.Destination.Open(filename)
|
|
s.Assert(err, qt.IsNil)
|
|
defer f.Close()
|
|
cfg, err := jpeg.DecodeConfig(f)
|
|
s.Assert(err, qt.IsNil)
|
|
s.Assert(cfg.Width, qt.Equals, width)
|
|
s.Assert(cfg.Height, qt.Equals, height)
|
|
}
|
|
|
|
func (s *sitesBuilder) AssertNoDuplicateWrites() {
|
|
s.Helper()
|
|
d := s.Fs.Destination.(hugofs.DuplicatesReporter)
|
|
s.Assert(d.ReportDuplicates(), qt.Equals, "")
|
|
}
|
|
|
|
func (s *sitesBuilder) FileContent(filename string) string {
|
|
s.T.Helper()
|
|
filename = filepath.FromSlash(filename)
|
|
if !strings.HasPrefix(filename, s.workingDir) {
|
|
filename = filepath.Join(s.workingDir, filename)
|
|
}
|
|
return readDestination(s.T, s.Fs, filename)
|
|
}
|
|
|
|
func (s *sitesBuilder) AssertObject(expected string, object interface{}) {
|
|
s.T.Helper()
|
|
got := s.dumper.Sdump(object)
|
|
expected = strings.TrimSpace(expected)
|
|
|
|
if expected != got {
|
|
fmt.Println(got)
|
|
diff := htesting.DiffStrings(expected, got)
|
|
s.Fatalf("diff:\n%s\nexpected\n%s\ngot\n%s", diff, expected, got)
|
|
}
|
|
}
|
|
|
|
func (s *sitesBuilder) AssertFileContentRe(filename string, matches ...string) {
|
|
content := readDestination(s.T, s.Fs, filename)
|
|
for _, match := range matches {
|
|
r := regexp.MustCompile("(?s)" + match)
|
|
if !r.MatchString(content) {
|
|
s.Fatalf("No match for %q in content for %s\n%q", match, filename, content)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *sitesBuilder) CheckExists(filename string) bool {
|
|
return destinationExists(s.Fs, filepath.Clean(filename))
|
|
}
|
|
|
|
func (s *sitesBuilder) GetPage(ref string) page.Page {
|
|
p, err := s.H.Sites[0].getPageNew(nil, ref)
|
|
s.Assert(err, qt.IsNil)
|
|
return p
|
|
}
|
|
|
|
func (s *sitesBuilder) GetPageRel(p page.Page, ref string) page.Page {
|
|
p, err := s.H.Sites[0].getPageNew(p, ref)
|
|
s.Assert(err, qt.IsNil)
|
|
return p
|
|
}
|
|
|
|
func (s *sitesBuilder) NpmInstall() hexec.Runner {
|
|
sc := security.DefaultConfig
|
|
sc.Exec.Allow = security.NewWhitelist("npm")
|
|
ex := hexec.New(sc)
|
|
command, err := ex.New("npm", "install")
|
|
s.Assert(err, qt.IsNil)
|
|
return command
|
|
|
|
}
|
|
|
|
func newTestHelper(cfg config.Provider, fs *hugofs.Fs, t testing.TB) testHelper {
|
|
return testHelper{
|
|
Cfg: cfg,
|
|
Fs: fs,
|
|
C: qt.New(t),
|
|
}
|
|
}
|
|
|
|
type testHelper struct {
|
|
Cfg config.Provider
|
|
Fs *hugofs.Fs
|
|
*qt.C
|
|
}
|
|
|
|
func (th testHelper) assertFileContent(filename string, matches ...string) {
|
|
th.Helper()
|
|
filename = th.replaceDefaultContentLanguageValue(filename)
|
|
content := readDestination(th, th.Fs, filename)
|
|
for _, match := range matches {
|
|
match = th.replaceDefaultContentLanguageValue(match)
|
|
th.Assert(strings.Contains(content, match), qt.Equals, true, qt.Commentf(match+" not in: \n"+content))
|
|
}
|
|
}
|
|
|
|
func (th testHelper) assertFileContentRegexp(filename string, matches ...string) {
|
|
filename = th.replaceDefaultContentLanguageValue(filename)
|
|
content := readDestination(th, th.Fs, filename)
|
|
for _, match := range matches {
|
|
match = th.replaceDefaultContentLanguageValue(match)
|
|
r := regexp.MustCompile(match)
|
|
matches := r.MatchString(content)
|
|
if !matches {
|
|
fmt.Println(match+":\n", content)
|
|
}
|
|
th.Assert(matches, qt.Equals, true)
|
|
}
|
|
}
|
|
|
|
func (th testHelper) assertFileNotExist(filename string) {
|
|
exists, err := helpers.Exists(filename, th.Fs.Destination)
|
|
th.Assert(err, qt.IsNil)
|
|
th.Assert(exists, qt.Equals, false)
|
|
}
|
|
|
|
func (th testHelper) replaceDefaultContentLanguageValue(value string) string {
|
|
defaultInSubDir := th.Cfg.GetBool("defaultContentLanguageInSubDir")
|
|
replace := th.Cfg.GetString("defaultContentLanguage") + "/"
|
|
|
|
if !defaultInSubDir {
|
|
value = strings.Replace(value, replace, "", 1)
|
|
}
|
|
return value
|
|
}
|
|
|
|
func loadTestConfig(fs afero.Fs, withConfig ...func(cfg config.Provider) error) (config.Provider, error) {
|
|
v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs}, withConfig...)
|
|
return v, err
|
|
}
|
|
|
|
func newTestCfgBasic() (config.Provider, *hugofs.Fs) {
|
|
mm := afero.NewMemMapFs()
|
|
v := config.New()
|
|
v.Set("defaultContentLanguageInSubdir", true)
|
|
|
|
fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v)
|
|
|
|
return v, fs
|
|
}
|
|
|
|
func newTestCfg(withConfig ...func(cfg config.Provider) error) (config.Provider, *hugofs.Fs) {
|
|
mm := afero.NewMemMapFs()
|
|
|
|
v, err := loadTestConfig(mm, func(cfg config.Provider) error {
|
|
// Default is false, but true is easier to use as default in tests
|
|
cfg.Set("defaultContentLanguageInSubdir", true)
|
|
|
|
for _, w := range withConfig {
|
|
w(cfg)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil && err != ErrNoConfigFile {
|
|
panic(err)
|
|
}
|
|
|
|
fs := hugofs.NewFrom(hugofs.NewBaseFileDecorator(mm), v)
|
|
|
|
return v, fs
|
|
}
|
|
|
|
func newTestSitesFromConfig(t testing.TB, afs afero.Fs, tomlConfig string, layoutPathContentPairs ...string) (testHelper, *HugoSites) {
|
|
if len(layoutPathContentPairs)%2 != 0 {
|
|
t.Fatalf("Layouts must be provided in pairs")
|
|
}
|
|
|
|
c := qt.New(t)
|
|
|
|
writeToFs(t, afs, filepath.Join("content", ".gitkeep"), "")
|
|
writeToFs(t, afs, "config.toml", tomlConfig)
|
|
|
|
cfg, err := LoadConfigDefault(afs)
|
|
c.Assert(err, qt.IsNil)
|
|
|
|
fs := hugofs.NewFrom(afs, cfg)
|
|
th := newTestHelper(cfg, fs, t)
|
|
|
|
for i := 0; i < len(layoutPathContentPairs); i += 2 {
|
|
writeSource(t, fs, layoutPathContentPairs[i], layoutPathContentPairs[i+1])
|
|
}
|
|
|
|
h, err := NewHugoSites(deps.DepsCfg{Fs: fs, Cfg: cfg})
|
|
|
|
c.Assert(err, qt.IsNil)
|
|
|
|
return th, h
|
|
}
|
|
|
|
func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ tpl.TemplateManager) error {
|
|
return func(templ tpl.TemplateManager) error {
|
|
for i := 0; i < len(additionalTemplates); i += 2 {
|
|
err := templ.AddTemplate(additionalTemplates[i], additionalTemplates[i+1])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// TODO(bep) replace these with the builder
|
|
func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site {
|
|
t.Helper()
|
|
return buildSingleSiteExpected(t, false, false, depsCfg, buildCfg)
|
|
}
|
|
|
|
func buildSingleSiteExpected(t testing.TB, expectSiteInitError, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site {
|
|
t.Helper()
|
|
b := newTestSitesBuilderFromDepsCfg(t, depsCfg).WithNothingAdded()
|
|
|
|
err := b.CreateSitesE()
|
|
|
|
if expectSiteInitError {
|
|
b.Assert(err, qt.Not(qt.IsNil))
|
|
return nil
|
|
} else {
|
|
b.Assert(err, qt.IsNil)
|
|
}
|
|
|
|
h := b.H
|
|
|
|
b.Assert(len(h.Sites), qt.Equals, 1)
|
|
|
|
if expectBuildError {
|
|
b.Assert(h.Build(buildCfg), qt.Not(qt.IsNil))
|
|
return nil
|
|
|
|
}
|
|
|
|
b.Assert(h.Build(buildCfg), qt.IsNil)
|
|
|
|
return h.Sites[0]
|
|
}
|
|
|
|
func writeSourcesToSource(t *testing.T, base string, fs *hugofs.Fs, sources ...[2]string) {
|
|
for _, src := range sources {
|
|
writeSource(t, fs, filepath.Join(base, src[0]), src[1])
|
|
}
|
|
}
|
|
|
|
func getPage(in page.Page, ref string) page.Page {
|
|
p, err := in.GetPage(ref)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
func content(c resource.ContentProvider) string {
|
|
cc, err := c.Content()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ccs, err := cast.ToStringE(cc)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return ccs
|
|
}
|
|
|
|
func pagesToString(pages ...page.Page) string {
|
|
var paths []string
|
|
for _, p := range pages {
|
|
paths = append(paths, p.Path())
|
|
}
|
|
sort.Strings(paths)
|
|
return strings.Join(paths, "|")
|
|
}
|
|
|
|
func dumpPagesLinks(pages ...page.Page) {
|
|
var links []string
|
|
for _, p := range pages {
|
|
links = append(links, p.RelPermalink())
|
|
}
|
|
sort.Strings(links)
|
|
|
|
for _, link := range links {
|
|
fmt.Println(link)
|
|
}
|
|
}
|
|
|
|
func dumpPages(pages ...page.Page) {
|
|
fmt.Println("---------")
|
|
for _, p := range pages {
|
|
fmt.Printf("Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s Lang: %s\n",
|
|
p.Kind(), p.Title(), p.RelPermalink(), p.Path(), p.SectionsPath(), p.Lang())
|
|
}
|
|
}
|
|
|
|
func dumpSPages(pages ...*pageState) {
|
|
for i, p := range pages {
|
|
fmt.Printf("%d: Kind: %s Title: %-10s RelPermalink: %-10s Path: %-10s sections: %s\n",
|
|
i+1,
|
|
p.Kind(), p.Title(), p.RelPermalink(), p.Path(), p.SectionsPath())
|
|
}
|
|
}
|
|
|
|
func printStringIndexes(s string) {
|
|
lines := strings.Split(s, "\n")
|
|
i := 0
|
|
|
|
for _, line := range lines {
|
|
|
|
for _, r := range line {
|
|
fmt.Printf("%-3s", strconv.Itoa(i))
|
|
i += utf8.RuneLen(r)
|
|
}
|
|
i++
|
|
fmt.Println()
|
|
for _, r := range line {
|
|
fmt.Printf("%-3s", string(r))
|
|
}
|
|
fmt.Println()
|
|
|
|
}
|
|
}
|
|
|
|
// See https://github.com/golang/go/issues/19280
|
|
// Not in use.
|
|
var parallelEnabled = true
|
|
|
|
func parallel(t *testing.T) {
|
|
if parallelEnabled {
|
|
t.Parallel()
|
|
}
|
|
}
|
|
|
|
func skipSymlink(t *testing.T) {
|
|
if runtime.GOOS == "windows" && os.Getenv("CI") == "" {
|
|
t.Skip("skip symlink test on local Windows (needs admin)")
|
|
}
|
|
}
|
|
|
|
func captureStderr(f func() error) (string, error) {
|
|
old := os.Stderr
|
|
r, w, _ := os.Pipe()
|
|
os.Stderr = w
|
|
|
|
err := f()
|
|
|
|
w.Close()
|
|
os.Stderr = old
|
|
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, r)
|
|
return buf.String(), err
|
|
}
|
|
|
|
func captureStdout(f func() error) (string, error) {
|
|
old := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
err := f()
|
|
|
|
w.Close()
|
|
os.Stdout = old
|
|
|
|
var buf bytes.Buffer
|
|
io.Copy(&buf, r)
|
|
return buf.String(), err
|
|
}
|