diff --git a/pkg/api/avatar/avatar.go b/pkg/api/avatar/avatar.go index 2c3952684c0..7ef583e0fe1 100644 --- a/pkg/api/avatar/avatar.go +++ b/pkg/api/avatar/avatar.go @@ -118,7 +118,7 @@ func (a *CacheServer) Handler(ctx *models.ReqContext) { if err := avatar.Encode(ctx.Resp); err != nil { log.Warnf("avatar encode error: %v", err) - ctx.WriteHeader(500) + ctx.Resp.WriteHeader(500) } } diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index db083c30e26..e5a95a0525e 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -217,10 +217,7 @@ func setupScenarioContext(t *testing.T, url string) *scenarioContext { require.Truef(t, exists, "Views should be in %q", viewsPath) sc.m = macaron.New() - sc.m.Use(macaron.Renderer(macaron.RenderOptions{ - Directory: viewsPath, - Delims: macaron.Delims{Left: "[[", Right: "]]"}, - })) + sc.m.UseMiddleware(macaron.Renderer(viewsPath, "[[", "]]")) sc.m.Use(getContextHandler(t, cfg).Middleware) return sc diff --git a/pkg/api/frontendsettings_test.go b/pkg/api/frontendsettings_test.go index bd9b4dfcbea..11c858be3a8 100644 --- a/pkg/api/frontendsettings_test.go +++ b/pkg/api/frontendsettings_test.go @@ -58,11 +58,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg) (*macaron.Macaron, *HT m := macaron.New() m.Use(getContextHandler(t, cfg).Middleware) - m.Use(macaron.Renderer(macaron.RenderOptions{ - Directory: filepath.Join(setting.StaticRootPath, "views"), - IndentJSON: true, - Delims: macaron.Delims{Left: "[[", Right: "]]"}, - })) + m.UseMiddleware(macaron.Renderer(filepath.Join(setting.StaticRootPath, "views"), "[[", "]]")) m.Get("/api/frontend/settings/", hs.GetFrontendSettings) return m, hs diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 6dc9d5ef994..4e8be016ffa 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -335,7 +335,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.Use(middleware.Logger(hs.Cfg)) if hs.Cfg.EnableGzip { - m.Use(middleware.Gziper()) + m.UseMiddleware(middleware.Gziper()) } m.Use(middleware.Recovery(hs.Cfg)) @@ -354,11 +354,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.SetURLPrefix(hs.Cfg.AppSubURL) } - m.Use(macaron.Renderer(macaron.RenderOptions{ - Directory: filepath.Join(hs.Cfg.StaticRootPath, "views"), - IndentJSON: macaron.Env != macaron.PROD, - Delims: macaron.Delims{Left: "[[", Right: "]]"}, - })) + m.UseMiddleware(macaron.Renderer(filepath.Join(hs.Cfg.StaticRootPath, "views"), "[[", "]]")) // These endpoints are used for monitoring the Grafana instance // and should not be redirected or rejected. @@ -408,7 +404,7 @@ func (hs *HTTPServer) healthzHandler(ctx *macaron.Context) { return } - ctx.WriteHeader(200) + ctx.Resp.WriteHeader(200) _, err := ctx.Resp.Write([]byte("Ok")) if err != nil { hs.log.Error("could not write to response", "err", err) diff --git a/pkg/macaron/context.go b/pkg/macaron/context.go index 2920f834af8..a26b37aafdb 100644 --- a/pkg/macaron/context.go +++ b/pkg/macaron/context.go @@ -15,6 +15,8 @@ package macaron import ( + "encoding/json" + "html/template" "net/http" "net/url" "reflect" @@ -44,11 +46,11 @@ type Context struct { index int *Router - Req Request - Resp ResponseWriter - params Params - Render - Data map[string]interface{} + Req Request + Resp ResponseWriter + params Params + template *template.Template + Data map[string]interface{} } func (ctx *Context) handler() Handler { @@ -108,19 +110,31 @@ func (ctx *Context) RemoteAddr() string { return addr } -func (ctx *Context) renderHTML(status int, setName, tplName string, data ...interface{}) { - if len(data) <= 0 { - ctx.Render.HTMLSet(status, setName, tplName, ctx.Data) - } else if len(data) == 1 { - ctx.Render.HTMLSet(status, setName, tplName, data[0]) - } else { - ctx.Render.HTMLSet(status, setName, tplName, data[0], data[1].(HTMLOptions)) +const ( + headerContentType = "Content-Type" + contentTypeJSON = "application/json; charset=UTF-8" + contentTypeHTML = "text/html; charset=UTF-8" +) + +// HTML renders the HTML with default template set. +func (ctx *Context) HTML(status int, name string, data interface{}) { + ctx.Resp.Header().Set(headerContentType, contentTypeHTML) + ctx.Resp.WriteHeader(status) + if err := ctx.template.ExecuteTemplate(ctx.Resp, name, data); err != nil { + panic("Context.HTML:" + err.Error()) } } -// HTML renders the HTML with default template set. -func (ctx *Context) HTML(status int, name string, data ...interface{}) { - ctx.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data...) +func (ctx *Context) JSON(status int, data interface{}) { + ctx.Resp.Header().Set(headerContentType, contentTypeJSON) + ctx.Resp.WriteHeader(status) + enc := json.NewEncoder(ctx.Resp) + if Env != PROD { + enc.SetIndent("", " ") + } + if err := enc.Encode(data); err != nil { + panic("Context.JSON: " + err.Error()) + } } // Redirect sends a redirect response @@ -142,7 +156,7 @@ func (ctx *Context) parseForm() { return } - contentType := ctx.Req.Header.Get(_CONTENT_TYPE) + contentType := ctx.Req.Header.Get(headerContentType) if (ctx.Req.Method == "POST" || ctx.Req.Method == "PUT") && len(contentType) > 0 && strings.Contains(contentType, "multipart/form-data") { _ = ctx.Req.ParseMultipartForm(MaxMemory) diff --git a/pkg/macaron/macaron.go b/pkg/macaron/macaron.go index 40701d7bfcd..ce44a6ebf26 100644 --- a/pkg/macaron/macaron.go +++ b/pkg/macaron/macaron.go @@ -182,7 +182,6 @@ func (m *Macaron) createContext(rw http.ResponseWriter, req *http.Request) *Cont index: 0, Router: m.Router, Resp: NewResponseWriter(req.Method, rw), - Render: &DummyRender{rw}, Data: make(map[string]interface{}), } req = req.WithContext(context.WithValue(req.Context(), macaronContextKey{}, c)) diff --git a/pkg/macaron/render.go b/pkg/macaron/render.go index 8aec82bee59..c6acbee5bb0 100644 --- a/pkg/macaron/render.go +++ b/pkg/macaron/render.go @@ -16,708 +16,57 @@ package macaron import ( - "bytes" - "encoding/json" - "encoding/xml" - "fmt" "html/template" - "io" - "io/ioutil" + "io/fs" "net/http" "os" - "path" "path/filepath" - "strings" - "sync" - "time" ) -const ( - _CONTENT_TYPE = "Content-Type" - _CONTENT_BINARY = "application/octet-stream" - _CONTENT_JSON = "application/json" - _CONTENT_HTML = "text/html" - _CONTENT_PLAIN = "text/plain" - _CONTENT_XHTML = "application/xhtml+xml" - _CONTENT_XML = "text/xml" - _DEFAULT_CHARSET = "UTF-8" -) - -var ( - // Provides a temporary buffer to execute templates into and catch errors. - bufpool = sync.Pool{ - New: func() interface{} { return new(bytes.Buffer) }, +// Renderer is a Middleware that injects a template renderer into the macaron context, enabling ctx.HTML calls in the handlers. +// If MACARON_ENV is set to "development" then templates will be recompiled on every request. For more performance, set the +// MACARON_ENV environment variable to "production". +func Renderer(dir, leftDelim, rightDelim string) func(http.Handler) http.Handler { + fs := os.DirFS(dir) + t, err := compileTemplates(fs, leftDelim, rightDelim) + if err != nil { + panic("Renderer: " + err.Error()) } - - // Included helper functions for use when rendering html - helperFuncs = template.FuncMap{ - "yield": func() (string, error) { - return "", fmt.Errorf("yield called with no layout defined") - }, - "current": func() (string, error) { - return "", nil - }, + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + ctx := FromContext(req.Context()) + if Env == DEV { + if t, err = compileTemplates(fs, leftDelim, rightDelim); err != nil { + panic("Context.HTML:" + err.Error()) + } + } + ctx.template = t + next.ServeHTTP(rw, req) + }) } -) - -type ( - // TemplateFile represents a interface of template file that has name and can be read. - TemplateFile interface { - Name() string - Data() []byte - Ext() string - } - // TemplateFileSystem represents a interface of template file system that able to list all files. - TemplateFileSystem interface { - ListFiles() []TemplateFile - Get(string) (io.Reader, error) - } - - // Delims represents a set of Left and Right delimiters for HTML template rendering - Delims struct { - // Left delimiter, defaults to {{ - Left string - // Right delimiter, defaults to }} - Right string - } - - // RenderOptions represents a struct for specifying configuration options for the Render middleware. - RenderOptions struct { - // Directory to load templates. Default is "templates". - Directory string - // Addtional directories to overwite templates. - AppendDirectories []string - // Layout template name. Will not render a layout if "". Default is to "". - Layout string - // Extensions to parse template files from. Defaults are [".tmpl", ".html"]. - Extensions []string - // Funcs is a slice of FuncMaps to apply to the template upon compilation. This is useful for helper functions. Default is []. - Funcs []template.FuncMap - // Delims sets the action delimiters to the specified strings in the Delims struct. - Delims Delims - // Appends the given charset to the Content-Type header. Default is "UTF-8". - Charset string - // Outputs human readable JSON. - IndentJSON bool - // Outputs human readable XML. - IndentXML bool - // Prefixes the JSON output with the given bytes. - PrefixJSON []byte - // Prefixes the XML output with the given bytes. - PrefixXML []byte - // Allows changing of output to XHTML instead of HTML. Default is "text/html" - HTMLContentType string - // TemplateFileSystem is the interface for supporting any implmentation of template file system. - TemplateFileSystem - } - - // HTMLOptions is a struct for overriding some rendering Options for specific HTML call - HTMLOptions struct { - // Layout template name. Overrides Options.Layout. - Layout string - } - - Render interface { - http.ResponseWriter - SetResponseWriter(http.ResponseWriter) - - JSON(int, interface{}) - JSONString(interface{}) (string, error) - RawData(int, []byte) // Serve content as binary - PlainText(int, []byte) // Serve content as plain text - HTML(int, string, interface{}, ...HTMLOptions) - HTMLSet(int, string, string, interface{}, ...HTMLOptions) - HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error) - HTMLString(string, interface{}, ...HTMLOptions) (string, error) - HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error) - HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error) - XML(int, interface{}) - Error(int, ...string) - Status(int) - SetTemplatePath(string, string) - HasTemplateSet(string) bool - } -) - -// TplFile implements TemplateFile interface. -type TplFile struct { - name string - data []byte - ext string } -// NewTplFile cerates new template file with given name and data. -func NewTplFile(name string, data []byte, ext string) *TplFile { - return &TplFile{name, data, ext} -} - -func (f *TplFile) Name() string { - return f.name -} - -func (f *TplFile) Data() []byte { - return f.data -} - -func (f *TplFile) Ext() string { - return f.ext -} - -// TplFileSystem implements TemplateFileSystem interface. -type TplFileSystem struct { - files []TemplateFile -} - -// NewTemplateFileSystem creates new template file system with given options. -func NewTemplateFileSystem(opt RenderOptions, omitData bool) TplFileSystem { - fs := TplFileSystem{} - fs.files = make([]TemplateFile, 0, 10) - - // Directories are composed in reverse order because later one overwrites previous ones, - // so once found, we can directly jump out of the loop. - dirs := make([]string, 0, len(opt.AppendDirectories)+1) - for i := len(opt.AppendDirectories) - 1; i >= 0; i-- { - dirs = append(dirs, opt.AppendDirectories[i]) - } - dirs = append(dirs, opt.Directory) - - var err error - for i := range dirs { - // Skip ones that does not exists for symlink test, - // but allow non-symlink ones added after start. - if _, err := os.Stat(dirs[i]); err != nil && os.IsNotExist(err) { - continue +func compileTemplates(filesystem fs.FS, leftDelim, rightDelim string) (*template.Template, error) { + t := template.New("") + t.Delims(leftDelim, rightDelim) + err := fs.WalkDir(filesystem, ".", func(path string, d fs.DirEntry, e error) error { + if e != nil { + return nil // skip unreadable or erroneous filesystem items } - - dirs[i], err = filepath.EvalSymlinks(dirs[i]) - if err != nil { - panic("EvalSymlinks(" + dirs[i] + "): " + err.Error()) + if d.IsDir() { + return nil } - } - lastDir := dirs[len(dirs)-1] - - // We still walk the last (original) directory because it's non-sense we load templates not exist in original directory. - if err = filepath.Walk(lastDir, func(path string, info os.FileInfo, _ error) error { - r, err := filepath.Rel(lastDir, path) + ext := filepath.Ext(path) + if ext != ".html" && ext != ".tmpl" { + return nil + } + data, err := fs.ReadFile(filesystem, path) if err != nil { return err } - - ext := GetExt(r) - - for _, extension := range opt.Extensions { - if ext != extension { - continue - } - - var data []byte - if !omitData { - // Loop over candidates of directory, break out once found. - // The file always exists because it's inside the walk function, - // and read original file is the worst case. - for i := range dirs { - path = filepath.Join(dirs[i], r) - if f, err := os.Stat(path); err != nil || f.IsDir() { - continue - } - - data, err = ioutil.ReadFile(path) - if err != nil { - return err - } - break - } - } - - name := filepath.ToSlash((r[0 : len(r)-len(ext)])) - fs.files = append(fs.files, NewTplFile(name, data, ext)) - } - - return nil - }); err != nil { - panic("NewTemplateFileSystem: " + err.Error()) - } - - return fs -} - -func (fs TplFileSystem) ListFiles() []TemplateFile { - return fs.files -} - -func (fs TplFileSystem) Get(name string) (io.Reader, error) { - for i := range fs.files { - if fs.files[i].Name()+fs.files[i].Ext() == name { - return bytes.NewReader(fs.files[i].Data()), nil - } - } - return nil, fmt.Errorf("file '%s' not found", name) -} - -func PrepareCharset(charset string) string { - if len(charset) != 0 { - return "; charset=" + charset - } - - return "; charset=" + _DEFAULT_CHARSET -} - -func GetExt(s string) string { - index := strings.Index(s, ".") - if index == -1 { - return "" - } - return s[index:] -} - -func compile(opt RenderOptions) *template.Template { - t := template.New(opt.Directory) - t.Delims(opt.Delims.Left, opt.Delims.Right) - // Parse an initial template in case we don't have any. - template.Must(t.Parse("Macaron")) - - if opt.TemplateFileSystem == nil { - opt.TemplateFileSystem = NewTemplateFileSystem(opt, false) - } - - for _, f := range opt.TemplateFileSystem.ListFiles() { - tmpl := t.New(f.Name()) - for _, funcs := range opt.Funcs { - tmpl.Funcs(funcs) - } - // Bomb out if parse fails. We don't want any silent server starts. - template.Must(tmpl.Funcs(helperFuncs).Parse(string(f.Data()))) - } - - return t -} - -const ( - DEFAULT_TPL_SET_NAME = "DEFAULT" -) - -// TemplateSet represents a template set of type *template.Template. -type TemplateSet struct { - lock sync.RWMutex - sets map[string]*template.Template - dirs map[string]string -} - -// NewTemplateSet initializes a new empty template set. -func NewTemplateSet() *TemplateSet { - return &TemplateSet{ - sets: make(map[string]*template.Template), - dirs: make(map[string]string), - } -} - -func (ts *TemplateSet) Set(name string, opt *RenderOptions) *template.Template { - t := compile(*opt) - - ts.lock.Lock() - defer ts.lock.Unlock() - - ts.sets[name] = t - ts.dirs[name] = opt.Directory - return t -} - -func (ts *TemplateSet) Get(name string) *template.Template { - ts.lock.RLock() - defer ts.lock.RUnlock() - - return ts.sets[name] -} - -func (ts *TemplateSet) GetDir(name string) string { - ts.lock.RLock() - defer ts.lock.RUnlock() - - return ts.dirs[name] -} - -func prepareRenderOptions(options []RenderOptions) RenderOptions { - var opt RenderOptions - if len(options) > 0 { - opt = options[0] - } - - // Defaults. - if len(opt.Directory) == 0 { - opt.Directory = "templates" - } - if len(opt.Extensions) == 0 { - opt.Extensions = []string{".tmpl", ".html"} - } - if len(opt.HTMLContentType) == 0 { - opt.HTMLContentType = _CONTENT_HTML - } - - return opt -} - -func ParseTplSet(tplSet string) (tplName string, tplDir string) { - tplSet = strings.TrimSpace(tplSet) - if len(tplSet) == 0 { - panic("empty template set argument") - } - infos := strings.Split(tplSet, ":") - if len(infos) == 1 { - tplDir = infos[0] - tplName = path.Base(tplDir) - } else { - tplName = infos[0] - tplDir = infos[1] - } - - dir, err := os.Stat(tplDir) - if err != nil || !dir.IsDir() { - panic("template set path does not exist or is not a directory") - } - return tplName, tplDir -} - -func renderHandler(opt RenderOptions, tplSets []string) Handler { - cs := PrepareCharset(opt.Charset) - ts := NewTemplateSet() - ts.Set(DEFAULT_TPL_SET_NAME, &opt) - - var tmpOpt RenderOptions - for _, tplSet := range tplSets { - tplName, tplDir := ParseTplSet(tplSet) - tmpOpt = opt - tmpOpt.Directory = tplDir - ts.Set(tplName, &tmpOpt) - } - - return func(ctx *Context) { - r := &TplRender{ - ResponseWriter: ctx.Resp, - TemplateSet: ts, - Opt: &opt, - CompiledCharset: cs, - } - ctx.Data["TmplLoadTimes"] = func() string { - if r.startTime.IsZero() { - return "" - } - return fmt.Sprint(time.Since(r.startTime).Nanoseconds()/1e6) + "ms" - } - - ctx.Render = r - ctx.MapTo(r, (*Render)(nil)) - } -} - -// Renderer is a Middleware that maps a macaron.Render service into the Macaron handler chain. -// An single variadic macaron.RenderOptions struct can be optionally provided to configure -// HTML rendering. The default directory for templates is "templates" and the default -// file extension is ".tmpl" and ".html". -// -// If MACARON_ENV is set to "" or "development" then templates will be recompiled on every request. For more performance, set the -// MACARON_ENV environment variable to "production". -func Renderer(options ...RenderOptions) Handler { - return renderHandler(prepareRenderOptions(options), []string{}) -} - -func Renderers(options RenderOptions, tplSets ...string) Handler { - return renderHandler(prepareRenderOptions([]RenderOptions{options}), tplSets) -} - -type TplRender struct { - http.ResponseWriter - *TemplateSet - Opt *RenderOptions - CompiledCharset string - - startTime time.Time -} - -func (r *TplRender) SetResponseWriter(rw http.ResponseWriter) { - r.ResponseWriter = rw -} - -func (r *TplRender) JSON(status int, v interface{}) { - var ( - result []byte - err error - ) - if r.Opt.IndentJSON { - result, err = json.MarshalIndent(v, "", " ") - } else { - result, err = json.Marshal(v) - } - if err != nil { - http.Error(r, err.Error(), 500) - return - } - - // json rendered fine, write out the result - r.Header().Set(_CONTENT_TYPE, _CONTENT_JSON+r.CompiledCharset) - r.WriteHeader(status) - if len(r.Opt.PrefixJSON) > 0 { - _, _ = r.Write(r.Opt.PrefixJSON) - } - _, _ = r.Write(result) -} - -func (r *TplRender) JSONString(v interface{}) (string, error) { - var result []byte - var err error - if r.Opt.IndentJSON { - result, err = json.MarshalIndent(v, "", " ") - } else { - result, err = json.Marshal(v) - } - if err != nil { - return "", err - } - return string(result), nil -} - -func (r *TplRender) XML(status int, v interface{}) { - var result []byte - var err error - if r.Opt.IndentXML { - result, err = xml.MarshalIndent(v, "", " ") - } else { - result, err = xml.Marshal(v) - } - if err != nil { - http.Error(r, err.Error(), 500) - return - } - - // XML rendered fine, write out the result - r.Header().Set(_CONTENT_TYPE, _CONTENT_XML+r.CompiledCharset) - r.WriteHeader(status) - if len(r.Opt.PrefixXML) > 0 { - _, _ = r.Write(r.Opt.PrefixXML) - } - _, _ = r.Write(result) -} - -func (r *TplRender) data(status int, contentType string, v []byte) { - if r.Header().Get(_CONTENT_TYPE) == "" { - r.Header().Set(_CONTENT_TYPE, contentType) - } - r.WriteHeader(status) - _, _ = r.Write(v) -} - -func (r *TplRender) RawData(status int, v []byte) { - r.data(status, _CONTENT_BINARY, v) -} - -func (r *TplRender) PlainText(status int, v []byte) { - r.data(status, _CONTENT_PLAIN, v) -} - -func (r *TplRender) execute(t *template.Template, name string, data interface{}) (*bytes.Buffer, error) { - buf := bufpool.Get().(*bytes.Buffer) - return buf, t.ExecuteTemplate(buf, name, data) -} - -func (r *TplRender) addYield(t *template.Template, tplName string, data interface{}) { - funcs := template.FuncMap{ - "yield": func() (template.HTML, error) { - buf, err := r.execute(t, tplName, data) - // return safe html here since we are rendering our own template - return template.HTML(buf.String()), err - }, - "current": func() (string, error) { - return tplName, nil - }, - } - t.Funcs(funcs) -} - -func (r *TplRender) renderBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (*bytes.Buffer, error) { - t := r.TemplateSet.Get(setName) - if Env == DEV { - opt := *r.Opt - opt.Directory = r.TemplateSet.GetDir(setName) - t = r.TemplateSet.Set(setName, &opt) - } - if t == nil { - return nil, fmt.Errorf("html/template: template \"%s\" is undefined", tplName) - } - - opt := r.prepareHTMLOptions(htmlOpt) - - if len(opt.Layout) > 0 { - r.addYield(t, tplName, data) - tplName = opt.Layout - } - - out, err := r.execute(t, tplName, data) - if err != nil { - return nil, err - } - - return out, nil -} - -func (r *TplRender) renderHTML(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) { - r.startTime = time.Now() - - out, err := r.renderBytes(setName, tplName, data, htmlOpt...) - if err != nil { - http.Error(r, err.Error(), http.StatusInternalServerError) - return - } - - r.Header().Set(_CONTENT_TYPE, r.Opt.HTMLContentType+r.CompiledCharset) - r.WriteHeader(status) - - if _, err := out.WriteTo(r); err != nil { - out.Reset() - } - bufpool.Put(out) -} - -func (r *TplRender) HTML(status int, name string, data interface{}, htmlOpt ...HTMLOptions) { - r.renderHTML(status, DEFAULT_TPL_SET_NAME, name, data, htmlOpt...) -} - -func (r *TplRender) HTMLSet(status int, setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) { - r.renderHTML(status, setName, tplName, data, htmlOpt...) -} - -func (r *TplRender) HTMLSetBytes(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) { - out, err := r.renderBytes(setName, tplName, data, htmlOpt...) - if err != nil { - return []byte(""), err - } - return out.Bytes(), nil -} - -func (r *TplRender) HTMLBytes(name string, data interface{}, htmlOpt ...HTMLOptions) ([]byte, error) { - return r.HTMLSetBytes(DEFAULT_TPL_SET_NAME, name, data, htmlOpt...) -} - -func (r *TplRender) HTMLSetString(setName, tplName string, data interface{}, htmlOpt ...HTMLOptions) (string, error) { - p, err := r.HTMLSetBytes(setName, tplName, data, htmlOpt...) - return string(p), err -} - -func (r *TplRender) HTMLString(name string, data interface{}, htmlOpt ...HTMLOptions) (string, error) { - p, err := r.HTMLBytes(name, data, htmlOpt...) - return string(p), err -} - -// Error writes the given HTTP status to the current ResponseWriter -func (r *TplRender) Error(status int, message ...string) { - r.WriteHeader(status) - if len(message) > 0 { - _, _ = r.Write([]byte(message[0])) - } -} - -func (r *TplRender) Status(status int) { - r.WriteHeader(status) -} - -func (r *TplRender) prepareHTMLOptions(htmlOpt []HTMLOptions) HTMLOptions { - if len(htmlOpt) > 0 { - return htmlOpt[0] - } - - return HTMLOptions{ - Layout: r.Opt.Layout, - } -} - -func (r *TplRender) SetTemplatePath(setName, dir string) { - if len(setName) == 0 { - setName = DEFAULT_TPL_SET_NAME - } - opt := *r.Opt - opt.Directory = dir - r.TemplateSet.Set(setName, &opt) -} - -func (r *TplRender) HasTemplateSet(name string) bool { - return r.TemplateSet.Get(name) != nil -} - -// DummyRender is used when user does not choose any real render to use. -// This way, we can print out friendly message which asks them to register one, -// instead of ugly and confusing 'nil pointer' panic. -type DummyRender struct { - http.ResponseWriter -} - -func renderNotRegistered() { - panic("middleware render hasn't been registered") -} - -func (r *DummyRender) SetResponseWriter(http.ResponseWriter) { - renderNotRegistered() -} - -func (r *DummyRender) JSON(int, interface{}) { - renderNotRegistered() -} - -func (r *DummyRender) JSONString(interface{}) (string, error) { - renderNotRegistered() - return "", nil -} - -func (r *DummyRender) RawData(int, []byte) { - renderNotRegistered() -} - -func (r *DummyRender) PlainText(int, []byte) { - renderNotRegistered() -} - -func (r *DummyRender) HTML(int, string, interface{}, ...HTMLOptions) { - renderNotRegistered() -} - -func (r *DummyRender) HTMLSet(int, string, string, interface{}, ...HTMLOptions) { - renderNotRegistered() -} - -func (r *DummyRender) HTMLSetString(string, string, interface{}, ...HTMLOptions) (string, error) { - renderNotRegistered() - return "", nil -} - -func (r *DummyRender) HTMLString(string, interface{}, ...HTMLOptions) (string, error) { - renderNotRegistered() - return "", nil -} - -func (r *DummyRender) HTMLSetBytes(string, string, interface{}, ...HTMLOptions) ([]byte, error) { - renderNotRegistered() - return nil, nil -} - -func (r *DummyRender) HTMLBytes(string, interface{}, ...HTMLOptions) ([]byte, error) { - renderNotRegistered() - return nil, nil -} - -func (r *DummyRender) XML(int, interface{}) { - renderNotRegistered() -} - -func (r *DummyRender) Error(int, ...string) { - renderNotRegistered() -} - -func (r *DummyRender) Status(int) { - renderNotRegistered() -} - -func (r *DummyRender) SetTemplatePath(string, string) { - renderNotRegistered() -} - -func (r *DummyRender) HasTemplateSet(string) bool { - renderNotRegistered() - return false + basename := path[:len(path)-len(ext)] + _, err = t.New(basename).Parse(string(data)) + return err + }) + return t, err } diff --git a/pkg/middleware/gziper.go b/pkg/middleware/gziper.go index 5427edd1aef..d0248b345b8 100644 --- a/pkg/middleware/gziper.go +++ b/pkg/middleware/gziper.go @@ -1,43 +1,81 @@ package middleware import ( + "bufio" + "compress/gzip" + "fmt" + "net" + "net/http" "strings" - "github.com/go-macaron/gzip" - "github.com/grafana/grafana/pkg/infra/log" - "gopkg.in/macaron.v1" + macaron "gopkg.in/macaron.v1" ) -const resourcesPath = "/resources" - -var gzipIgnoredPathPrefixes = []string{ - "/api/datasources/proxy", // Ignore datasource proxy requests. - "/api/plugin-proxy/", - "/metrics", - "/api/live/ws", // WebSocket does not support gzip compression. - "/api/live/push", // WebSocket does not support gzip compression. +type gzipResponseWriter struct { + w *gzip.Writer + macaron.ResponseWriter } -func Gziper() macaron.Handler { - gziperLogger := log.New("gziper") - gziper := gzip.Gziper() +func (grw *gzipResponseWriter) WriteHeader(c int) { + grw.Header().Del("Content-Length") + grw.ResponseWriter.WriteHeader(c) +} - return func(ctx *macaron.Context) { - requestPath := ctx.Req.URL.RequestURI() +func (grw gzipResponseWriter) Write(p []byte) (int, error) { + if grw.Header().Get("Content-Type") == "" { + grw.Header().Set("Content-Type", http.DetectContentType(p)) + } + grw.Header().Del("Content-Length") + return grw.w.Write(p) +} - for _, pathPrefix := range gzipIgnoredPathPrefixes { - if strings.HasPrefix(requestPath, pathPrefix) { +func (grw gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if hijacker, ok := grw.ResponseWriter.(http.Hijacker); ok { + return hijacker.Hijack() + } + return nil, nil, fmt.Errorf("GZIP ResponseWriter doesn't implement the Hijacker interface") +} + +type matcher func(s string) bool + +func prefix(p string) matcher { return func(s string) bool { return strings.HasPrefix(s, p) } } +func substr(p string) matcher { return func(s string) bool { return strings.Contains(s, p) } } + +var gzipIgnoredPaths = []matcher{ + prefix("/api/datasources"), + prefix("/api/plugins"), + prefix("/api/plugin-proxy/"), + prefix("/metrics"), + prefix("/api/live/ws"), // WebSocket does not support gzip compression. + prefix("/api/live/push"), // WebSocket does not support gzip compression. + substr("/resources"), +} + +func Gziper() func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + requestPath := req.URL.RequestURI() + + for _, pathMatcher := range gzipIgnoredPaths { + if pathMatcher(requestPath) { + fmt.Println("skip path", requestPath) + next.ServeHTTP(rw, req) + return + } + } + + if !strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") { + next.ServeHTTP(rw, req) return } - } - // ignore resources - if (strings.HasPrefix(requestPath, "/api/datasources/") || strings.HasPrefix(requestPath, "/api/plugins/")) && strings.Contains(requestPath, resourcesPath) { - return - } + grw := &gzipResponseWriter{gzip.NewWriter(rw), rw.(macaron.ResponseWriter)} + grw.Header().Set("Content-Encoding", "gzip") + grw.Header().Set("Vary", "Accept-Encoding") - if _, err := ctx.Invoke(gziper); err != nil { - gziperLogger.Error("Invoking gzip handler failed", "err", err) - } + next.ServeHTTP(grw, req) + // We can't really handle close errors at this point and we can't report them to the caller + _ = grw.w.Close() + }) } } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 95defe3376c..f7cbdf5102e 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -111,7 +111,7 @@ func TestMiddlewareContext(t *testing.T) { Settings: map[string]interface{}{}, NavTree: []*dtos.NavLink{}, } - t.Log("Calling HTML", "data", data, "render", c.Render) + t.Log("Calling HTML", "data", data) c.HTML(200, "index-template", data) t.Log("Returned HTML with code 200") } @@ -633,10 +633,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func( sc.m = macaron.New() sc.m.Use(AddDefaultResponseHeaders(cfg)) sc.m.UseMiddleware(AddCSPHeader(cfg, logger)) - sc.m.Use(macaron.Renderer(macaron.RenderOptions{ - Directory: viewsPath, - Delims: macaron.Delims{Left: "[[", Right: "]]"}, - })) + sc.m.UseMiddleware(macaron.Renderer(viewsPath, "[[", "]]")) ctxHdlr := getContextHandler(t, cfg) sc.sqlStore = ctxHdlr.SQLStore diff --git a/pkg/middleware/org_redirect.go b/pkg/middleware/org_redirect.go index cb1813ade7f..59cbddf58e9 100644 --- a/pkg/middleware/org_redirect.go +++ b/pkg/middleware/org_redirect.go @@ -37,7 +37,7 @@ func OrgRedirect(cfg *setting.Cfg) macaron.Handler { if ctx.IsApiRequest() { ctx.JsonApiErr(404, "Not found", nil) } else { - ctx.Error(404, "Not found") + http.Error(ctx.Resp, "Not found", http.StatusNotFound) } return diff --git a/pkg/middleware/rate_limit_test.go b/pkg/middleware/rate_limit_test.go index c78bbe9136a..109a2e1220c 100644 --- a/pkg/middleware/rate_limit_test.go +++ b/pkg/middleware/rate_limit_test.go @@ -32,10 +32,7 @@ func rateLimiterScenario(t *testing.T, desc string, rps int, burst int, fn rateL cfg := setting.NewCfg() m := macaron.New() - m.Use(macaron.Renderer(macaron.RenderOptions{ - Directory: "", - Delims: macaron.Delims{Left: "[[", Right: "]]"}, - })) + m.UseMiddleware(macaron.Renderer("../../public/views", "[[", "]]")) m.Use(getContextHandler(t, cfg).Middleware) m.Get("/foo", RateLimit(rps, burst, func() time.Time { return currentTime }), defaultHandler) diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index 336a3425da9..88a4f8b32e0 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -158,7 +158,7 @@ func Recovery(cfg *setting.Cfg) macaron.Handler { c.JSON(500, resp) } else { - c.HTML(500, cfg.ErrTemplateName) + c.HTML(500, cfg.ErrTemplateName, c.Data) } } }() diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 0bc002b8b5d..c62079372b2 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -65,10 +65,7 @@ func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) { sc.m.Use(Recovery(cfg)) sc.m.Use(AddDefaultResponseHeaders(cfg)) - sc.m.Use(macaron.Renderer(macaron.RenderOptions{ - Directory: viewsPath, - Delims: macaron.Delims{Left: "[[", Right: "]]"}, - })) + sc.m.UseMiddleware(macaron.Renderer(viewsPath, "[[", "]]")) sc.userAuthTokenService = auth.NewFakeUserAuthTokenService() sc.remoteCacheService = remotecache.NewFakeStore(t) diff --git a/pkg/models/context.go b/pkg/models/context.go index 42f7036551e..a3bb6838985 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -36,7 +36,7 @@ func (ctx *ReqContext) Handle(cfg *setting.Cfg, status int, title string, err er ctx.Data["AppSubUrl"] = cfg.AppSubURL ctx.Data["Theme"] = "dark" - ctx.HTML(status, cfg.ErrTemplateName) + ctx.HTML(status, cfg.ErrTemplateName, ctx.Data) } func (ctx *ReqContext) IsApiRequest() bool {