2019-05-18 18:06:10 -04:00
|
|
|
package template_renderer
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"html/template"
|
|
|
|
"math"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2019-07-31 13:47:30 -08:00
|
|
|
"reflect"
|
2019-05-18 18:06:10 -04:00
|
|
|
"strings"
|
|
|
|
|
2019-08-03 16:35:57 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
|
2019-07-13 12:16:28 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
2019-08-03 16:35:57 -08:00
|
|
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
|
|
|
|
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
|
2019-05-18 18:06:10 -04:00
|
|
|
"github.com/pkg/errors"
|
|
|
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
|
|
|
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext"
|
|
|
|
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
errInvalidTemplate = errors.New("Invalid template")
|
|
|
|
)
|
|
|
|
|
|
|
|
type Template struct {
|
2019-05-20 22:16:58 -05:00
|
|
|
Funcs template.FuncMap
|
2019-05-18 18:06:10 -04:00
|
|
|
mainTemplate *template.Template
|
|
|
|
}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// NewTemplate defines a base set of functions that will be applied to all templates
|
|
|
|
// being rendered.
|
2019-05-18 18:06:10 -04:00
|
|
|
func NewTemplate(templateFuncs template.FuncMap) *Template {
|
|
|
|
t := &Template{}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Default functions are defined and available for all templates being rendered.
|
|
|
|
// These base function help with provided basic formatting so don't have to use javascript/jquery,
|
|
|
|
// transformation happens server-side instead of client-side to provide base-level consistency.
|
|
|
|
// Any defined function below will be overwritten if a matching function key is included.
|
2019-05-18 18:06:10 -04:00
|
|
|
t.Funcs = template.FuncMap{
|
|
|
|
// probably could provide examples of each of these
|
|
|
|
"Minus": func(a, b int) int {
|
|
|
|
return a - b
|
|
|
|
},
|
|
|
|
"Add": func(a, b int) int {
|
|
|
|
return a + b
|
|
|
|
},
|
|
|
|
"Mod": func(a, b int) int {
|
|
|
|
return int(math.Mod(float64(a), float64(b)))
|
|
|
|
},
|
|
|
|
"AssetUrl": func(p string) string {
|
|
|
|
if !strings.HasPrefix(p, "/") {
|
|
|
|
p = "/" + p
|
|
|
|
}
|
|
|
|
return p
|
|
|
|
},
|
|
|
|
"AppAssetUrl": func(p string) string {
|
|
|
|
if !strings.HasPrefix(p, "/") {
|
|
|
|
p = "/" + p
|
|
|
|
}
|
|
|
|
return p
|
|
|
|
},
|
|
|
|
"SiteS3Url": func(p string) string {
|
|
|
|
return p
|
|
|
|
},
|
|
|
|
"S3Url": func(p string) string {
|
|
|
|
return p
|
|
|
|
},
|
|
|
|
"AppBaseUrl": func(p string) string {
|
|
|
|
return p
|
|
|
|
},
|
|
|
|
"Http2Https": func(u string) string {
|
|
|
|
return strings.Replace(u, "http:", "https:", 1)
|
|
|
|
},
|
|
|
|
"StringHasPrefix": func(str, match string) bool {
|
|
|
|
if strings.HasPrefix(str, match) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
"StringHasSuffix": func(str, match string) bool {
|
|
|
|
if strings.HasSuffix(str, match) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
"StringContains": func(str, match string) bool {
|
|
|
|
if strings.Contains(str, match) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
"NavPageClass": func(uri, uriMatch, uriClass string) string {
|
|
|
|
u, err := url.Parse(uri)
|
|
|
|
if err != nil {
|
|
|
|
return "?"
|
|
|
|
}
|
|
|
|
if strings.HasPrefix(u.Path, uriMatch) {
|
|
|
|
return uriClass
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
},
|
|
|
|
"UrlEncode": func(k string) string {
|
|
|
|
return url.QueryEscape(k)
|
|
|
|
},
|
|
|
|
"html": func(value interface{}) template.HTML {
|
|
|
|
return template.HTML(fmt.Sprint(value))
|
|
|
|
},
|
2019-07-31 13:47:30 -08:00
|
|
|
"HasAuth": func(ctx context.Context) bool {
|
|
|
|
claims, err := auth.ClaimsFromContext(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return claims.HasAuth()
|
|
|
|
},
|
|
|
|
"HasRole": func(ctx context.Context, roles ...string) bool {
|
|
|
|
claims, err := auth.ClaimsFromContext(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return claims.HasRole(roles...)
|
|
|
|
},
|
|
|
|
"dict": func(values ...interface{}) (map[string]interface{}, error) {
|
|
|
|
if len(values) == 0 {
|
|
|
|
return nil, errors.New("invalid dict call")
|
|
|
|
}
|
|
|
|
|
|
|
|
dict := make(map[string]interface{})
|
|
|
|
|
|
|
|
for i := 0; i < len(values); i++ {
|
|
|
|
key, isset := values[i].(string)
|
|
|
|
if !isset {
|
|
|
|
if reflect.TypeOf(values[i]).Kind() == reflect.Map {
|
|
|
|
m := values[i].(map[string]interface{})
|
|
|
|
for i, v := range m {
|
|
|
|
dict[i] = v
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return nil, errors.New("dict values must be maps")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
i++
|
|
|
|
if i == len(values) {
|
|
|
|
return nil, errors.New("specify the key for non array values")
|
|
|
|
}
|
|
|
|
dict[key] = values[i]
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
return dict, nil
|
|
|
|
},
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
for fn, f := range templateFuncs {
|
|
|
|
t.Funcs[fn] = f
|
|
|
|
}
|
|
|
|
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
|
|
|
// TemplateRenderer is a custom html/template renderer for Echo framework
|
|
|
|
type TemplateRenderer struct {
|
|
|
|
templateDir string
|
|
|
|
// has to be map so can know the name and map the name to the location / file path
|
2019-05-20 22:16:58 -05:00
|
|
|
layoutFiles map[string]string
|
|
|
|
contentFiles map[string]string
|
|
|
|
partialFiles map[string]string
|
2019-05-18 18:06:10 -04:00
|
|
|
enableHotReload bool
|
2019-05-20 22:16:58 -05:00
|
|
|
templates map[string]*template.Template
|
|
|
|
globalViewData map[string]interface{}
|
|
|
|
mainTemplate *template.Template
|
|
|
|
errorHandler func(ctx context.Context, w http.ResponseWriter, req *http.Request, renderer web.Renderer, statusCode int, er error) error
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// NewTemplateRenderer implements the interface web.Renderer allowing for execution of
|
|
|
|
// nested html templates. The templateDir should include three directories:
|
|
|
|
// 1. layouts: base layouts defined for the entire application
|
|
|
|
// 2. content: page specific templates that will be nested instead of a layout template
|
|
|
|
// 3. partials: templates used by multiple layout or content templates
|
2019-05-18 18:06:10 -04:00
|
|
|
func NewTemplateRenderer(templateDir string, enableHotReload bool, globalViewData map[string]interface{}, tmpl *Template, errorHandler func(ctx context.Context, w http.ResponseWriter, req *http.Request, renderer web.Renderer, statusCode int, er error) error) (*TemplateRenderer, error) {
|
|
|
|
r := &TemplateRenderer{
|
2019-05-20 22:16:58 -05:00
|
|
|
templateDir: templateDir,
|
|
|
|
layoutFiles: make(map[string]string),
|
|
|
|
contentFiles: make(map[string]string),
|
|
|
|
partialFiles: make(map[string]string),
|
2019-05-18 18:06:10 -04:00
|
|
|
enableHotReload: enableHotReload,
|
2019-05-20 22:16:58 -05:00
|
|
|
templates: make(map[string]*template.Template),
|
|
|
|
globalViewData: globalViewData,
|
|
|
|
errorHandler: errorHandler,
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Recursively loop through all folders/files in the template directory and group them by their
|
|
|
|
// template type. They are filename / filepath for lookup on render.
|
2019-05-18 18:06:10 -04:00
|
|
|
err := filepath.Walk(templateDir, func(path string, info os.FileInfo, err error) error {
|
|
|
|
dir := filepath.Base(filepath.Dir(path))
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Skip directories.
|
2019-05-18 18:06:10 -04:00
|
|
|
if info.IsDir() {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
baseName := filepath.Base(path)
|
|
|
|
|
|
|
|
if dir == "content" {
|
|
|
|
r.contentFiles[baseName] = path
|
|
|
|
} else if dir == "layouts" {
|
|
|
|
r.layoutFiles[baseName] = path
|
|
|
|
} else if dir == "partials" {
|
|
|
|
r.partialFiles[baseName] = path
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return r, err
|
|
|
|
}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Main template used to render execute all templates against.
|
|
|
|
r.mainTemplate = template.New("main")
|
2019-07-31 18:34:27 -08:00
|
|
|
r.mainTemplate, _ = r.mainTemplate.Parse(`{{define "main" }}{{ template "base" . }}{{ end }}`)
|
2019-05-18 18:34:39 -04:00
|
|
|
r.mainTemplate.Funcs(tmpl.Funcs)
|
|
|
|
|
2019-05-18 18:06:10 -04:00
|
|
|
// Ensure all layout files render successfully with no errors.
|
|
|
|
for _, f := range r.layoutFiles {
|
2019-05-18 18:34:39 -04:00
|
|
|
t, err := r.mainTemplate.Clone()
|
|
|
|
if err != nil {
|
|
|
|
return r, err
|
|
|
|
}
|
2019-05-18 18:06:10 -04:00
|
|
|
template.Must(t.ParseFiles(f))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure all partial files render successfully with no errors.
|
|
|
|
for _, f := range r.partialFiles {
|
2019-05-18 18:34:39 -04:00
|
|
|
t, err := r.mainTemplate.Clone()
|
|
|
|
if err != nil {
|
|
|
|
return r, err
|
|
|
|
}
|
2019-05-18 18:06:10 -04:00
|
|
|
template.Must(t.ParseFiles(f))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure all content files render successfully with no errors.
|
|
|
|
for _, f := range r.contentFiles {
|
2019-05-18 18:34:39 -04:00
|
|
|
t, err := r.mainTemplate.Clone()
|
|
|
|
if err != nil {
|
|
|
|
return r, err
|
|
|
|
}
|
2019-05-18 18:06:10 -04:00
|
|
|
template.Must(t.ParseFiles(f))
|
|
|
|
}
|
|
|
|
|
|
|
|
return r, nil
|
|
|
|
}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Render executes the nested templates and returns the result to the client.
|
|
|
|
// contentType: supports any content type to allow for rendering text, emails and other formats
|
|
|
|
// statusCode: the error method calls this function so allow the HTTP Status Code to be set
|
|
|
|
// data: map[string]interface{} to allow including additional request and globally defined values.
|
2019-05-18 18:06:10 -04:00
|
|
|
func (r *TemplateRenderer) Render(ctx context.Context, w http.ResponseWriter, req *http.Request, templateLayoutName, templateContentName, contentType string, statusCode int, data map[string]interface{}) error {
|
2019-05-18 18:34:39 -04:00
|
|
|
// If the template has not been rendered yet or hot reload is enabled,
|
|
|
|
// then parse the template files.
|
2019-05-18 18:06:10 -04:00
|
|
|
t, ok := r.templates[templateContentName]
|
|
|
|
if !ok || r.enableHotReload {
|
2019-05-20 22:16:58 -05:00
|
|
|
var err error
|
|
|
|
t, err = r.mainTemplate.Clone()
|
2019-05-18 18:34:39 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the base template file path.
|
2019-05-18 18:06:10 -04:00
|
|
|
layoutFile, ok := r.layoutFiles[templateLayoutName]
|
|
|
|
if !ok {
|
|
|
|
return errors.Wrapf(errInvalidTemplate, "template layout file for %s does not exist", templateLayoutName)
|
|
|
|
}
|
2019-05-18 18:34:39 -04:00
|
|
|
// The base layout will be the first template.
|
2019-05-18 18:06:10 -04:00
|
|
|
files := []string{layoutFile}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Append all of the partials that are defined. Not an easy way to determine if the
|
|
|
|
// layout or content template contain any references to a partial so load all of them.
|
|
|
|
// This assumes that all partial templates should be uniquely named and not conflict with
|
|
|
|
// and base layout or content definitions.
|
2019-05-18 18:06:10 -04:00
|
|
|
for _, f := range r.partialFiles {
|
|
|
|
files = append(files, f)
|
|
|
|
}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Load the content template file path.
|
2019-05-18 18:06:10 -04:00
|
|
|
contentFile, ok := r.contentFiles[templateContentName]
|
|
|
|
if !ok {
|
|
|
|
return errors.Wrapf(errInvalidTemplate, "template content file for %s does not exist", templateContentName)
|
|
|
|
}
|
|
|
|
files = append(files, contentFile)
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Render all of template files
|
2019-05-18 18:06:10 -04:00
|
|
|
t = template.Must(t.ParseFiles(files...))
|
|
|
|
r.templates[templateContentName] = t
|
|
|
|
}
|
|
|
|
|
|
|
|
opts := []ddtrace.StartSpanOption{
|
|
|
|
tracer.SpanType(ext.SpanTypeWeb),
|
|
|
|
tracer.ResourceName(templateContentName),
|
|
|
|
}
|
|
|
|
|
|
|
|
var span tracer.Span
|
|
|
|
span, ctx = tracer.StartSpanFromContext(ctx, "web.Render", opts...)
|
|
|
|
defer span.Finish()
|
|
|
|
|
|
|
|
// Specific new data map for render to allow values to be overwritten on a request
|
|
|
|
// basis.
|
|
|
|
// append the global key/pairs
|
2019-08-03 16:35:57 -08:00
|
|
|
renderData := make(map[string]interface{}, len(r.globalViewData))
|
|
|
|
for k, v := range r.globalViewData {
|
|
|
|
renderData[k] = v
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add Request URL to render data
|
|
|
|
reqData := map[string]interface{}{
|
|
|
|
"Url": "",
|
2019-05-20 22:16:58 -05:00
|
|
|
"Uri": "",
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
if req != nil {
|
|
|
|
reqData["Url"] = req.URL.String()
|
|
|
|
reqData["Uri"] = req.URL.RequestURI()
|
|
|
|
}
|
|
|
|
renderData["_Request"] = reqData
|
|
|
|
|
|
|
|
// Add context to render data, this supports template functions having the ability
|
|
|
|
// to define context.Context as an argument
|
|
|
|
renderData["_Ctx"] = ctx
|
|
|
|
|
2019-07-31 13:47:30 -08:00
|
|
|
if qv := req.URL.Query().Get("test-validation-error"); qv != "" {
|
|
|
|
data["validationErrors"] = data["validationDefaults"]
|
|
|
|
}
|
|
|
|
|
|
|
|
if qv := req.URL.Query().Get("test-web-error"); qv != "" {
|
|
|
|
terr := errors.New("Some random error")
|
|
|
|
terr = errors.WithMessage(terr, "Actual error message")
|
|
|
|
rerr := weberror.NewError(ctx, terr, http.StatusBadRequest).(*weberror.Error)
|
|
|
|
rerr.Message = "Test Web Error Message"
|
|
|
|
data["error"] = rerr
|
|
|
|
}
|
|
|
|
|
|
|
|
if qv := req.URL.Query().Get("test-error"); qv != "" {
|
|
|
|
terr := errors.New("Test error")
|
|
|
|
terr = errors.WithMessage(terr, "Error message")
|
|
|
|
data["error"] = terr
|
|
|
|
}
|
|
|
|
|
2019-05-18 18:06:10 -04:00
|
|
|
// Append request data map to render data last so any previous value can be overwritten.
|
|
|
|
if data != nil {
|
|
|
|
for k, v := range data {
|
|
|
|
renderData[k] = v
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-03 15:01:17 -08:00
|
|
|
// If there is a session, check for flashes and ensure the session is saved.
|
|
|
|
sess := webcontext.ContextSession(ctx)
|
|
|
|
if sess != nil {
|
|
|
|
// Load any flash messages and append to response data to be included in the rendered template.
|
|
|
|
if flashes := sess.Flashes(); len(flashes) > 0 {
|
|
|
|
renderData["flashes"] = flashes
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save the session before writing to the response for the session cookie to be sent to the client.
|
|
|
|
if err := sess.Save(req, w); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-18 18:06:10 -04:00
|
|
|
// Render template with data.
|
2019-08-03 15:01:17 -08:00
|
|
|
if err := t.Execute(w, renderData); err != nil {
|
|
|
|
return errors.WithStack(err)
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Error formats an error and returns the result to the client.
|
2019-05-18 18:06:10 -04:00
|
|
|
func (r *TemplateRenderer) Error(ctx context.Context, w http.ResponseWriter, req *http.Request, statusCode int, er error) error {
|
2019-05-18 18:34:39 -04:00
|
|
|
// If error handler was defined to support formatted response for web, used it.
|
2019-05-18 18:06:10 -04:00
|
|
|
if r.errorHandler != nil {
|
2019-05-20 22:16:58 -05:00
|
|
|
return r.errorHandler(ctx, w, req, r, statusCode, er)
|
2019-05-18 18:06:10 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Default response text response of error.
|
|
|
|
return web.RespondError(ctx, w, er)
|
|
|
|
}
|
|
|
|
|
2019-05-18 18:34:39 -04:00
|
|
|
// Static serves files from the local file exist.
|
|
|
|
// If an error is encountered, it will handled by TemplateRenderer.Error
|
2019-05-18 18:06:10 -04:00
|
|
|
func (tr *TemplateRenderer) Static(rootDir, prefix string) web.Handler {
|
|
|
|
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
|
|
|
err := web.StaticHandler(ctx, w, r, params, rootDir, prefix)
|
|
|
|
if err != nil {
|
|
|
|
return tr.Error(ctx, w, r, http.StatusNotFound, err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return h
|
|
|
|
}
|