| OLD | NEW |
| (Empty) |
| 1 // Copyright 2015 The LUCI Authors. All rights reserved. | |
| 2 // Use of this source code is governed under the Apache License, Version 2.0 | |
| 3 // that can be found in the LICENSE file. | |
| 4 | |
| 5 package settings | |
| 6 | |
| 7 import ( | |
| 8 "fmt" | |
| 9 "io" | |
| 10 "net/http" | |
| 11 "path" | |
| 12 "sort" | |
| 13 "strings" | |
| 14 | |
| 15 "github.com/julienschmidt/httprouter" | |
| 16 "github.com/luci/gae/service/info" | |
| 17 "github.com/luci/luci-go/appengine/gaeauth/server" | |
| 18 "github.com/luci/luci-go/appengine/gaemiddleware" | |
| 19 "github.com/luci/luci-go/common/clock" | |
| 20 "github.com/luci/luci-go/milo/common/miloerror" | |
| 21 "github.com/luci/luci-go/server/analytics" | |
| 22 "github.com/luci/luci-go/server/auth" | |
| 23 "github.com/luci/luci-go/server/router" | |
| 24 "github.com/luci/luci-go/server/templates" | |
| 25 "golang.org/x/net/context" | |
| 26 ) | |
| 27 | |
| 28 type themeContextKey string | |
| 29 | |
| 30 // NamedBundle is a tuple of a name (That matches it's corresponding theme) | |
| 31 // and a template bundle. | |
| 32 type NamedBundle struct { | |
| 33 Name string | |
| 34 Bundle *templates.Bundle | |
| 35 Theme *Theme | |
| 36 } | |
| 37 | |
| 38 // Theme is the base type for specifying where to find a Theme. | |
| 39 type Theme struct { | |
| 40 // IsTemplate is true if this theme is a Go template type template, and
false | |
| 41 // if it is a client side (eg. Polymer) type template. | |
| 42 IsTemplate bool | |
| 43 // Name is the name of the Theme. | |
| 44 Name string | |
| 45 } | |
| 46 | |
| 47 // ThemedHandler is the base type for any milo html handlers. | |
| 48 type ThemedHandler interface { | |
| 49 // Return the template name given a theme name. | |
| 50 GetTemplateName(Theme) string | |
| 51 // Render the page for server side renders. | |
| 52 Render(context.Context, *http.Request, httprouter.Params) (*templates.Ar
gs, error) | |
| 53 } | |
| 54 | |
| 55 var ( | |
| 56 // Default is the global default theme for anonomyous users. | |
| 57 Default = Theme{IsTemplate: true, Name: "buildbot"} | |
| 58 // Themes is a list of all known themes. | |
| 59 Themes = map[string]Theme{ | |
| 60 "buildbot": Default, | |
| 61 "bootstrap": {IsTemplate: true, Name: "bootstrap"}, | |
| 62 } | |
| 63 ) | |
| 64 | |
| 65 // GetAllThemes gets all known themes as a list of strings. | |
| 66 func GetAllThemes() []string { | |
| 67 results := make([]string, 0, len(Themes)) | |
| 68 for k := range Themes { | |
| 69 results = append(results, k) | |
| 70 } | |
| 71 sort.Strings(results) | |
| 72 return results | |
| 73 } | |
| 74 | |
| 75 // GetTemplateBundles is used to render HTML templates. It provides a base args | |
| 76 // passed to all templates. | |
| 77 func GetTemplateBundles() []NamedBundle { | |
| 78 result := []NamedBundle{} | |
| 79 for name, t := range Themes { | |
| 80 if t.IsTemplate { | |
| 81 templateBundle := &templates.Bundle{ | |
| 82 Loader: templates.FileSystemLoader(path
.Join("templates", name)), | |
| 83 DefaultTemplate: name, | |
| 84 DebugMode: info.IsDevAppServer, | |
| 85 DefaultArgs: func(c context.Context) (templates.
Args, error) { | |
| 86 r := getRequest(c) | |
| 87 path := r.URL.Path | |
| 88 loginURL, err := auth.LoginURL(c, path) | |
| 89 if err != nil { | |
| 90 return nil, err | |
| 91 } | |
| 92 logoutURL, err := auth.LogoutURL(c, path
) | |
| 93 if err != nil { | |
| 94 return nil, err | |
| 95 } | |
| 96 if err != nil { | |
| 97 return nil, err | |
| 98 } | |
| 99 return templates.Args{ | |
| 100 "AppVersion": strings.Split(inf
o.VersionID(c), ".")[0], | |
| 101 "IsAnonymous": auth.CurrentIdent
ity(c) == "anonymous:anonymous", | |
| 102 "User": auth.CurrentUser(
c), | |
| 103 "LoginURL": loginURL, | |
| 104 "LogoutURL": logoutURL, | |
| 105 "CurrentTime": clock.Now(c), | |
| 106 "Analytics": analytics.Snippet
(c), | |
| 107 "RequestID": info.RequestID(c)
, | |
| 108 }, nil | |
| 109 }, | |
| 110 FuncMap: funcMap, | |
| 111 } | |
| 112 result = append(result, NamedBundle{name, templateBundle
, &t}) | |
| 113 } | |
| 114 } | |
| 115 return result | |
| 116 } | |
| 117 | |
| 118 // UseNamedBundle is like templates.Use, but with the choice of one of many bund
les (themes) | |
| 119 func UseNamedBundle(c context.Context, nb NamedBundle) (context.Context, error)
{ | |
| 120 err := nb.Bundle.EnsureLoaded(c) | |
| 121 return context.WithValue(c, themeContextKey(nb.Name), nb.Bundle), err | |
| 122 } | |
| 123 | |
| 124 // withNamedBundle is like templates.WithTemplates, but with the choice of one o
f many bundles (themes) | |
| 125 func withNamedBundle(nb NamedBundle) router.Middleware { | |
| 126 return func(c *router.Context, next router.Handler) { | |
| 127 var err error | |
| 128 c.Context, err = UseNamedBundle(c.Context, nb) // calls EnsureLo
aded and initializes b.err inside | |
| 129 if err != nil { | |
| 130 http.Error(c.Writer, fmt.Sprintf("Can't load HTML templa
tes.\n%s", err), http.StatusInternalServerError) | |
| 131 return | |
| 132 } | |
| 133 next(c) | |
| 134 } | |
| 135 } | |
| 136 | |
| 137 // themedMustRender renders theme and panics if it can't be rendered. This shou
ld never fail in | |
| 138 // production. | |
| 139 func themedMustRender(c context.Context, out io.Writer, theme, name string, args
templates.Args) { | |
| 140 if b, _ := c.Value(themeContextKey(theme)).(*templates.Bundle); b != nil
{ | |
| 141 blob, err := b.Render(c, name, args) | |
| 142 if err != nil { | |
| 143 panic(fmt.Errorf("Could not render template %s from them
e %s:\n%s", name, theme, err)) | |
| 144 } | |
| 145 _, err = out.Write(blob) | |
| 146 if err != nil { | |
| 147 panic(fmt.Errorf("Could not write out template %s from t
heme %s:\n%s", name, theme, err)) | |
| 148 } | |
| 149 return | |
| 150 } | |
| 151 panic(fmt.Errorf("Error: Could not load template %s from theme %s", name
, theme)) | |
| 152 } | |
| 153 | |
| 154 // Base returns the basic luci appengine middlewares. | |
| 155 func Base() router.MiddlewareChain { | |
| 156 methods := auth.Authenticator{ | |
| 157 &server.OAuth2Method{Scopes: []string{server.EmailScope}}, | |
| 158 server.CookieAuth, | |
| 159 &server.InboundAppIDAuthMethod{}, | |
| 160 } | |
| 161 m := gaemiddleware.BaseProd().Extend(auth.Use(methods), auth.Authenticat
e) | |
| 162 for _, nb := range GetTemplateBundles() { | |
| 163 m = m.Extend(withNamedBundle(nb)) | |
| 164 } | |
| 165 return m | |
| 166 } | |
| 167 | |
| 168 // Wrap adapts a ThemedHandler into a router.Handler. Of note, the | |
| 169 // Render functions' interface into rendering is purely through a single | |
| 170 // templates.Args value which gets rendered here, while the http.ResponseWriter | |
| 171 // is stripped out. | |
| 172 func Wrap(h ThemedHandler) router.Handler { | |
| 173 return func(c *router.Context) { | |
| 174 // Figure out if we need to do the things. | |
| 175 theme := GetTheme(c.Context, c.Request) | |
| 176 template := h.GetTemplateName(theme) | |
| 177 | |
| 178 // Inject the request into the context. | |
| 179 c.Context = WithRequest(c.Context, c.Request) | |
| 180 | |
| 181 // Do the things. | |
| 182 args, err := h.Render(c.Context, c.Request, c.Params) | |
| 183 | |
| 184 // Throw errors. | |
| 185 // TODO(hinoka): Add themes and templates for errors so they loo
k better. | |
| 186 if err != nil { | |
| 187 msg := err.Error() | |
| 188 code := http.StatusInternalServerError | |
| 189 if merr, ok := err.(miloerror.Error); ok { | |
| 190 msg = merr.Message | |
| 191 code = merr.Code | |
| 192 } | |
| 193 c.Writer.WriteHeader(code) | |
| 194 name := "pages/error.html" | |
| 195 themedMustRender(c.Context, c.Writer, theme.Name, name,
templates.Args{ | |
| 196 "Code": code, | |
| 197 "Message": msg, | |
| 198 }) | |
| 199 return | |
| 200 } | |
| 201 | |
| 202 // Render the stuff. | |
| 203 name := fmt.Sprintf("pages/%s", template) | |
| 204 themedMustRender(c.Context, c.Writer, theme.Name, name, *args) | |
| 205 } | |
| 206 } | |
| 207 | |
| 208 // The context key | |
| 209 var requestKey = "http.request" | |
| 210 | |
| 211 func WithRequest(c context.Context, r *http.Request) context.Context { | |
| 212 return context.WithValue(c, &requestKey, r) | |
| 213 } | |
| 214 | |
| 215 func getRequest(c context.Context) *http.Request { | |
| 216 if req, ok := c.Value(&requestKey).(*http.Request); ok { | |
| 217 return req | |
| 218 } | |
| 219 panic("No http.request found in context") | |
| 220 } | |
| OLD | NEW |