dnscontrol/pkg/js/js.go
2020-09-21 09:39:45 -04:00

225 lines
7.6 KiB
Go

package js
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/robertkrimen/otto" // load underscore js into vm by default
_ "github.com/robertkrimen/otto/underscore" // required by otto
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
"github.com/StackExchange/dnscontrol/v3/pkg/transform"
)
// currentDirectory is the current directory as used by require().
// This is used to emulate nodejs-style require() directory handling.
// If require("a/b/c.js") is called, any require() statement in c.js
// needs to be accessed relative to "a/b". Therefore we
// track the currentDirectory (which is the current directory as
// far as require() is concerned, not the actual os.Getwd().
var currentDirectory string
// ExecuteJavascript accepts a javascript string and runs it, returning the resulting dnsConfig.
func ExecuteJavascript(file string, devMode bool) (*models.DNSConfig, error) {
script, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
// Record the directory path leading up to this file.
currentDirectory = filepath.Dir(file)
vm := otto.New()
vm.Set("require", require)
vm.Set("REV", reverse)
vm.Set("glob", listFiles) // used for require_glob()
helperJs := GetHelpers(devMode)
// run helper script to prime vm and initialize variables
if _, err := vm.Run(helperJs); err != nil {
return nil, err
}
// run user script
if _, err := vm.Run(script); err != nil {
return nil, err
}
// export conf as string and unmarshal
value, err := vm.Run(`JSON.stringify(conf)`)
if err != nil {
return nil, err
}
str, err := value.ToString()
if err != nil {
return nil, err
}
conf := &models.DNSConfig{}
if err = json.Unmarshal([]byte(str), conf); err != nil {
return nil, err
}
return conf, nil
}
// GetHelpers returns the filename of helpers.js, or the esc'ed version.
func GetHelpers(devMode bool) string {
return _escFSMustString(devMode, "/helpers.js")
}
func require(call otto.FunctionCall) otto.Value {
if len(call.ArgumentList) != 1 {
throw(call.Otto, "require takes exactly one argument")
}
file := call.Argument(0).String() // The filename as given by the user
// relFile is the file we're actually going to pass to ReadFile().
// It defaults to the user-provided name unless it is relative.
relFile := file
cleanFile := filepath.Clean(filepath.Join(currentDirectory, file))
if strings.HasPrefix(file, ".") {
relFile = cleanFile
}
// Record the old currentDirectory so that we can return there.
currentDirectoryOld := currentDirectory
// Record the directory path leading up to the file we're about to require.
currentDirectory = filepath.Dir(cleanFile)
printer.Debugf("requiring: %s (%s)\n", file, relFile)
// quick fix, by replacing to linux slashes, to make it work with windows paths too.
data, err := ioutil.ReadFile(filepath.ToSlash(relFile))
if err != nil {
throw(call.Otto, err.Error())
}
var value otto.Value = otto.TrueValue()
// If its a json file return the json value, else default to true
if strings.HasSuffix(filepath.Ext(relFile), "json") {
cmd := fmt.Sprintf(`JSON.parse(JSON.stringify(%s))`, string(data))
value, err = call.Otto.Run(cmd)
} else {
_, err = call.Otto.Run(string(data))
}
if err != nil {
throw(call.Otto, fmt.Sprintf("File %s: %s", filepath.Base(relFile), err.Error()))
}
// Pop back to the old directory.
currentDirectory = currentDirectoryOld
return value
}
func listFiles(call otto.FunctionCall) otto.Value {
// Check amount of arguments provided
if !(len(call.ArgumentList) >= 1 && len(call.ArgumentList) <= 3) {
throw(call.Otto, "glob requires at least one argument: folder (string). "+
"Optional: recursive (bool) [true], fileExtension (string) [.js]")
}
// Check if provided parameters are valid
// First: Let's check dir.
if !(call.Argument(0).IsDefined() && call.Argument(0).IsString() &&
len(call.Argument(0).String()) > 0) {
throw(call.Otto, "glob: first argument needs to be a path, provided as string.")
}
dir := call.Argument(0).String() // Path where to start listing
printer.Debugf("listFiles: cd: %s, user: %s \n", currentDirectory, dir)
// now we always prepend the current directory we're working in, which is being set within
// the func ExecuteJavascript() above. So when require("domains/load_all.js") is being used,
// where glob("customer1/") is being used, we basically search for files in domains/customer1/.
dir = filepath.ToSlash(filepath.Join(currentDirectory, dir))
if _, err := os.Stat(dir); os.IsNotExist(err) {
throw(call.Otto, "glob: provided path does not exist.")
}
// Second: Recursive?
var recursive bool = true
if call.Argument(1).IsDefined() && !call.Argument(1).IsNull() {
if call.Argument(1).IsBoolean() {
recursive, _ = call.Argument(1).ToBoolean() // If it should be recursive
} else {
throw(call.Otto, "glob: second argument, if recursive, needs to be bool.")
}
}
// Third: File extension filter.
var fileExtension string = ".js"
if call.Argument(2).IsDefined() && !call.Argument(2).IsNull() {
if call.Argument(2).IsString() {
fileExtension = call.Argument(2).String() // Which file extension to filter for.
if !strings.HasPrefix(fileExtension, ".") {
// If it doesn't start with a dot, probably user forgot it and we do it instead.
fileExtension = "." + fileExtension
}
} else {
throw(call.Otto, "glob: third argument, file extension, needs to be a string. * for no filter.")
}
}
// Now we're doing the actual work: Listing files.
// Folders are ending with a slash. Can be identified later on from the user with JavaScript.
// Additionally, when more smart logic required, user can use regex in JS.
files := make([]string, 0) // init files list
dirClean := filepath.Clean(dir) // let's clean it here once, instead of over-and-over again within loop
err := filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
// quick fix to get it working on windows, as it returns paths with double-backslash, what usually
// require() doesn't seem to handle well. For the sake of compatibility (and because slash looks nicer),
// we simply replace "\\" to "/" using filepath.ToSlash()..
path = filepath.ToSlash(filepath.Clean(path)) // convert to slashes for directories
if !recursive && fi.IsDir() {
// If recursive is disabled, it is a dir what we're processing, and the path is different
// than specified, we're apparently in a different folder. Therefore: Skip it.
// So: Why this way? Because Walk() is always recursive and otherwise would require a complete
// different function to handle this scenario. This way it's easier to maintain.
if path != dirClean {
return filepath.SkipDir
}
}
if fileExtension != "*" && fileExtension != filepath.Ext(path) {
// ONLY skip, when the file extension is NOT matching, or when filter is NOT disabled.
return nil
}
//dirPath := filepath.ToSlash(filepath.Dir(path)) + "/"
files = append(files, path)
return err
})
if err != nil {
throw(call.Otto, fmt.Sprintf("dirwalk failed: %v", err.Error()))
}
// let's pass the data back to the JS engine.
value, err := call.Otto.ToValue(files)
if err != nil {
throw(call.Otto, fmt.Sprintf("converting value failed: %v", err.Error()))
}
return value
}
func throw(vm *otto.Otto, str string) {
panic(vm.MakeCustomError("Error", str))
}
func reverse(call otto.FunctionCall) otto.Value {
if len(call.ArgumentList) != 1 {
throw(call.Otto, "REV takes exactly one argument")
}
dom := call.Argument(0).String()
rev, err := transform.ReverseDomainName(dom)
if err != nil {
throw(call.Otto, err.Error())
}
v, _ := otto.ToValue(rev)
return v
}