A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

207 lines
4.4 KiB

  1. package alps
  2. import (
  3. "fmt"
  4. "html/template"
  5. "io"
  6. "io/ioutil"
  7. "net/url"
  8. "os"
  9. "strings"
  10. "github.com/labstack/echo/v4"
  11. )
  12. const themesDir = "themes"
  13. // GlobalRenderData contains data available in all templates.
  14. type GlobalRenderData struct {
  15. Path []string
  16. URL *url.URL
  17. LoggedIn bool
  18. // if logged in
  19. Username string
  20. Title string
  21. HavePlugin func(name string) bool
  22. // additional plugin-specific data
  23. Extra map[string]interface{}
  24. }
  25. // BaseRenderData is the base type for templates. It should be extended with
  26. // additional template-specific fields:
  27. //
  28. // type MyRenderData struct {
  29. // BaseRenderData
  30. // // add additional fields here
  31. // }
  32. type BaseRenderData struct {
  33. GlobalData GlobalRenderData
  34. // additional plugin-specific data
  35. Extra map[string]interface{}
  36. }
  37. // Global implements RenderData.
  38. func (brd *BaseRenderData) Global() *GlobalRenderData {
  39. return &brd.GlobalData
  40. }
  41. // RenderData is implemented by template data structs. It can be used to inject
  42. // additional data to all templates.
  43. type RenderData interface {
  44. // GlobalData returns a pointer to the global render data.
  45. Global() *GlobalRenderData
  46. }
  47. // NewBaseRenderData initializes a new BaseRenderData.
  48. //
  49. // It can be used by routes to pre-fill the base data:
  50. //
  51. // type MyRenderData struct {
  52. // BaseRenderData
  53. // // add additional fields here
  54. // }
  55. //
  56. // data := &MyRenderData{
  57. // BaseRenderData: *alps.NewBaseRenderData(ctx),
  58. // // other fields...
  59. // }
  60. func NewBaseRenderData(ectx echo.Context) *BaseRenderData {
  61. ctx, isactx := ectx.(*Context)
  62. global := GlobalRenderData{
  63. Extra: make(map[string]interface{}),
  64. Path: strings.Split(ectx.Request().URL.Path, "/")[1:],
  65. Title: "Webmail",
  66. URL: ectx.Request().URL,
  67. HavePlugin: func(name string) bool {
  68. if !isactx {
  69. return false
  70. }
  71. for _, plugin := range ctx.Server.plugins {
  72. if plugin.Name() == name {
  73. return true
  74. }
  75. }
  76. return false
  77. },
  78. }
  79. if isactx && ctx.Session != nil {
  80. global.LoggedIn = true
  81. global.Username = ctx.Session.username
  82. }
  83. return &BaseRenderData{
  84. GlobalData: global,
  85. Extra: make(map[string]interface{}),
  86. }
  87. }
  88. func (brd *BaseRenderData) WithTitle(title string) *BaseRenderData {
  89. brd.GlobalData.Title = title
  90. return brd
  91. }
  92. type renderer struct {
  93. logger echo.Logger
  94. defaultTheme string
  95. base *template.Template
  96. themes map[string]*template.Template
  97. }
  98. func (r *renderer) Render(w io.Writer, name string, data interface{}, ectx echo.Context) error {
  99. // ectx is the raw *echo.context, not our own *Context
  100. ctx := ectx.Get("context").(*Context)
  101. var renderData RenderData
  102. if data == nil {
  103. renderData = &struct{ BaseRenderData }{*NewBaseRenderData(ctx)}
  104. } else {
  105. var ok bool
  106. renderData, ok = data.(RenderData)
  107. if !ok {
  108. return fmt.Errorf("data passed to template %q doesn't implement RenderData", name)
  109. }
  110. }
  111. for _, plugin := range ctx.Server.plugins {
  112. if err := plugin.Inject(ctx, name, renderData); err != nil {
  113. return fmt.Errorf("failed to run plugin %q: %v", plugin.Name(), err)
  114. }
  115. }
  116. // TODO: per-user theme selection
  117. t := r.base
  118. if r.defaultTheme != "" {
  119. t = r.themes[r.defaultTheme]
  120. }
  121. return t.ExecuteTemplate(w, name, data)
  122. }
  123. func loadTheme(name string, base *template.Template) (*template.Template, error) {
  124. theme, err := base.Clone()
  125. if err != nil {
  126. return nil, err
  127. }
  128. theme, err = theme.ParseGlob(themesDir + "/" + name + "/*.html")
  129. if err != nil {
  130. return nil, err
  131. }
  132. return theme, nil
  133. }
  134. func (r *renderer) Load(plugins []Plugin) error {
  135. base := template.New("")
  136. for _, p := range plugins {
  137. if err := p.LoadTemplate(base); err != nil {
  138. return fmt.Errorf("failed to load template for plugin %q: %v", p.Name(), err)
  139. }
  140. }
  141. themes := make(map[string]*template.Template)
  142. files, err := ioutil.ReadDir(themesDir)
  143. if err != nil && !os.IsNotExist(err) {
  144. return err
  145. }
  146. for _, fi := range files {
  147. if !fi.IsDir() {
  148. continue
  149. }
  150. r.logger.Printf("Loading theme %q", fi.Name())
  151. var err error
  152. if themes[fi.Name()], err = loadTheme(fi.Name(), base); err != nil {
  153. return fmt.Errorf("failed to load theme %q: %v", fi.Name(), err)
  154. }
  155. }
  156. if r.defaultTheme != "" {
  157. if _, ok := themes[r.defaultTheme]; !ok {
  158. return fmt.Errorf("failed to find default theme %q", r.defaultTheme)
  159. }
  160. }
  161. r.base = base
  162. r.themes = themes
  163. return nil
  164. }
  165. func newRenderer(logger echo.Logger, defaultTheme string) *renderer {
  166. return &renderer{
  167. logger: logger,
  168. defaultTheme: defaultTheme,
  169. }
  170. }