package templates import ( "bytes" "crypto/rand" "fmt" "io" "log" weakrand "math/rand" "net" "net/http" "path" "strings" "sync" "text/template" "time" "os" "github.com/Masterminds/sprig" "github.com/caddyserver/caddy/modules/caddyhttp" "gopkg.in/russross/blackfriday.v2" ) // templateContext is the templateContext with which HTTP templates are executed. type templateContext struct { Root http.FileSystem Req *http.Request Args []interface{} // defined by arguments to .Include RespHeader tplWrappedHeader config *Templates } // Include returns the contents of filename relative to the site root. func (c templateContext) Include(filename string, args ...interface{}) (string, error) { if c.Root == nil { return "", fmt.Errorf("root file system not specified") } file, err := c.Root.Open(filename) if err != nil { return "", err } defer file.Close() bodyBuf := bufPool.Get().(*bytes.Buffer) bodyBuf.Reset() defer bufPool.Put(bodyBuf) _, err = io.Copy(bodyBuf, file) if err != nil { return "", err } c.Args = args err = c.executeTemplateInBuffer(filename, bodyBuf) if err != nil { return "", err } return bodyBuf.String(), nil } // HTTPInclude returns the body of a virtual (lightweight) request // to the given URI on the same server. func (c templateContext) HTTPInclude(uri string) (string, error) { if c.Req.Header.Get(recursionPreventionHeader) == "1" { return "", fmt.Errorf("virtual include cycle") } buf := bufPool.Get().(*bytes.Buffer) buf.Reset() defer bufPool.Put(buf) virtReq, err := http.NewRequest("GET", uri, nil) if err != nil { return "", err } virtReq.Header.Set(recursionPreventionHeader, "1") vrw := &virtualResponseWriter{body: buf, header: make(http.Header)} server := c.Req.Context().Value(caddyhttp.ServerCtxKey).(http.Handler) server.ServeHTTP(vrw, virtReq) if vrw.status >= 400 { return "", fmt.Errorf("http %d", vrw.status) } err = c.executeTemplateInBuffer(uri, buf) if err != nil { return "", err } return buf.String(), nil } func (c templateContext) executeTemplateInBuffer(tplName string, buf *bytes.Buffer) error { tpl := template.New(tplName).Funcs(sprig.TxtFuncMap()) if len(c.config.Delimiters) == 2 { tpl.Delims(c.config.Delimiters[0], c.config.Delimiters[1]) } parsedTpl, err := tpl.Parse(buf.String()) if err != nil { return err } buf.Reset() // reuse buffer for output return parsedTpl.Execute(buf, c) } // Now returns the current timestamp. func (c templateContext) Now() time.Time { return time.Now() } // Cookie gets the value of a cookie with name name. func (c templateContext) Cookie(name string) string { cookies := c.Req.Cookies() for _, cookie := range cookies { if cookie.Name == name { return cookie.Value } } return "" } // ReqHeader gets the value of a request header with field name. func (c templateContext) ReqHeader(name string) string { return c.Req.Header.Get(name) } // Hostname gets the (remote) hostname of the client making the request. func (c templateContext) Hostname() string { ip := c.IP() hostnameList, err := net.LookupAddr(ip) if err != nil || len(hostnameList) == 0 { return c.Req.RemoteAddr } return hostnameList[0] } // Env gets a map of the environment variables. func (c templateContext) Env() map[string]string { osEnv := os.Environ() envVars := make(map[string]string, len(osEnv)) for _, env := range osEnv { data := strings.SplitN(env, "=", 2) if len(data) == 2 && len(data[0]) > 0 { envVars[data[0]] = data[1] } } return envVars } // IP gets the (remote) IP address of the client making the request. func (c templateContext) IP() string { ip, _, err := net.SplitHostPort(c.Req.RemoteAddr) if err != nil { return c.Req.RemoteAddr } return ip } // Host returns the hostname portion of the Host header // from the HTTP request. func (c templateContext) Host() (string, error) { host, _, err := net.SplitHostPort(c.Req.Host) if err != nil { if !strings.Contains(c.Req.Host, ":") { // common with sites served on the default port 80 return c.Req.Host, nil } return "", err } return host, nil } // Truncate truncates the input string to the given length. // If length is negative, it returns that many characters // starting from the end of the string. If the absolute value // of length is greater than len(input), the whole input is // returned. func (c templateContext) Truncate(input string, length int) string { if length < 0 && len(input)+length > 0 { return input[len(input)+length:] } if length >= 0 && len(input) > length { return input[:length] } return input } // StripHTML returns s without HTML tags. It is fairly naive // but works with most valid HTML inputs. func (c templateContext) StripHTML(s string) string { var buf bytes.Buffer var inTag, inQuotes bool var tagStart int for i, ch := range s { if inTag { if ch == '>' && !inQuotes { inTag = false } else if ch == '<' && !inQuotes { // false start buf.WriteString(s[tagStart:i]) tagStart = i } else if ch == '"' { inQuotes = !inQuotes } continue } if ch == '<' { inTag = true tagStart = i continue } buf.WriteRune(ch) } if inTag { // false start buf.WriteString(s[tagStart:]) } return buf.String() } // Markdown renders the markdown body as HTML. func (c templateContext) Markdown(body string) string { return string(blackfriday.Run([]byte(body))) } // Ext returns the suffix beginning at the final dot in the final // slash-separated element of the pathStr (or in other words, the // file extension). func (c templateContext) Ext(pathStr string) string { return path.Ext(pathStr) } // StripExt returns the input string without the extension, // which is the suffix starting with the final '.' character // but not before the final path separator ('/') character. // If there is no extension, the whole input is returned. func (c templateContext) StripExt(path string) string { for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- { if path[i] == '.' { return path[:i] } } return path } // Replace replaces instances of find in input with replacement. func (c templateContext) Replace(input, find, replacement string) string { return strings.Replace(input, find, replacement, -1) } // HasPrefix returns true if s starts with prefix. func (c templateContext) HasPrefix(s, prefix string) bool { return strings.HasPrefix(s, prefix) } // ToLower will convert the given string to lower case. func (c templateContext) ToLower(s string) string { return strings.ToLower(s) } // ToUpper will convert the given string to upper case. func (c templateContext) ToUpper(s string) string { return strings.ToUpper(s) } // Split is a pass-through to strings.Split. It will split // the first argument at each instance of the separator and // return a slice of strings. func (c templateContext) Split(s string, sep string) []string { return strings.Split(s, sep) } // Join is a pass-through to strings.Join. It will join the // first argument slice with the separator in the second // argument and return the result. func (c templateContext) Join(a []string, sep string) string { return strings.Join(a, sep) } // Slice will convert the given arguments into a slice. func (c templateContext) Slice(elems ...interface{}) []interface{} { return elems } // Dict will convert the arguments into a dictionary (map). It expects // alternating keys and values of string types. This is useful since you // cannot express map literals directly in Go templates. func (c templateContext) Dict(values ...interface{}) (map[string]interface{}, error) { if len(values)%2 != 0 { return nil, fmt.Errorf("expected even number of arguments") } dict := make(map[string]interface{}, len(values)/2) for i := 0; i < len(values); i += 2 { key, ok := values[i].(string) if !ok { return nil, fmt.Errorf("argument %d: map keys must be strings", i) } dict[key] = values[i+1] } return dict, nil } // ListFiles reads and returns a slice of names from the given // directory relative to the root of c. func (c templateContext) ListFiles(name string) ([]string, error) { if c.Root == nil { return nil, fmt.Errorf("root file system not specified") } dir, err := c.Root.Open(path.Clean(name)) if err != nil { return nil, err } defer dir.Close() stat, err := dir.Stat() if err != nil { return nil, err } if !stat.IsDir() { return nil, fmt.Errorf("%v is not a directory", name) } dirInfo, err := dir.Readdir(0) if err != nil { return nil, err } names := make([]string, len(dirInfo)) for i, fileInfo := range dirInfo { names[i] = fileInfo.Name() } return names, nil } // RandomString generates a random string of random length given // length bounds. Thanks to http://stackoverflow.com/a/35615565/1048862 // for the clever technique that is fairly fast, secure, and maintains // proper distributions over the dictionary. func (c templateContext) RandomString(minLen, maxLen int) string { const ( letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" letterIdxBits = 6 // 6 bits to represent 64 possibilities (indexes) letterIdxMask = 1<