package handler import ( "blackforestbytes.com/simplecloudnotifier/api/ginresp" "blackforestbytes.com/simplecloudnotifier/logic" "blackforestbytes.com/simplecloudnotifier/models" "blackforestbytes.com/simplecloudnotifier/website" "errors" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" "gogs.mikescher.com/BlackForestBytes/goext/ginext" "gogs.mikescher.com/BlackForestBytes/goext/rext" "net/http" "regexp" "strings" ) type WebsiteHandler struct { app *logic.Application rexTemplate rext.Regex rexConfig rext.Regex } func NewWebsiteHandler(app *logic.Application) WebsiteHandler { return WebsiteHandler{ app: app, rexTemplate: rext.W(regexp.MustCompile("{{template\\|[A-Za-z0-9_\\-\\[\\].]+}}")), rexConfig: rext.W(regexp.MustCompile("{{config\\|[A-Za-z0-9_\\-.]+}}")), } } func (h WebsiteHandler) Index(pctx ginext.PreContext) ginext.HTTPResponse { ctx, g, errResp := pctx.Start() if errResp != nil { return *errResp } defer ctx.Cancel() return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { return h.serveAsset(g, "index.html", true) }) } func (h WebsiteHandler) APIDocs(pctx ginext.PreContext) ginext.HTTPResponse { ctx, g, errResp := pctx.Start() if errResp != nil { return *errResp } defer ctx.Cancel() return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { return h.serveAsset(g, "api.html", true) }) } func (h WebsiteHandler) APIDocsMore(pctx ginext.PreContext) ginext.HTTPResponse { ctx, g, errResp := pctx.Start() if errResp != nil { return *errResp } defer ctx.Cancel() return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { return h.serveAsset(g, "api_more.html", true) }) } func (h WebsiteHandler) MessageSent(pctx ginext.PreContext) ginext.HTTPResponse { ctx, g, errResp := pctx.Start() if errResp != nil { return *errResp } defer ctx.Cancel() return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { return h.serveAsset(g, "message_sent.html", true) }) } func (h WebsiteHandler) FaviconIco(pctx ginext.PreContext) ginext.HTTPResponse { ctx, g, errResp := pctx.Start() if errResp != nil { return *errResp } defer ctx.Cancel() return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { return h.serveAsset(g, "favicon.ico", false) }) } func (h WebsiteHandler) FaviconPNG(pctx ginext.PreContext) ginext.HTTPResponse { ctx, g, errResp := pctx.Start() if errResp != nil { return *errResp } defer ctx.Cancel() return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { return h.serveAsset(g, "favicon.png", false) }) } func (h WebsiteHandler) Javascript(pctx ginext.PreContext) ginext.HTTPResponse { ctx, g, errResp := pctx.Start() if errResp != nil { return *errResp } defer ctx.Cancel() return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { type uri struct { Filename string `uri:"fn"` } var u uri if err := g.ShouldBindUri(&u); err != nil { return ginext.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) } return h.serveAsset(g, "js/"+u.Filename, false) }) } func (h WebsiteHandler) CSS(pctx ginext.PreContext) ginext.HTTPResponse { type uri struct { Filename string `uri:"fn"` } var u uri ctx, g, errResp := pctx.URI(&u).Start() if errResp != nil { return *errResp } defer ctx.Cancel() return h.app.DoRequest(ctx, g, models.TLockNone, func(ctx *logic.AppContext, finishSuccess func(r ginext.HTTPResponse) ginext.HTTPResponse) ginext.HTTPResponse { return h.serveAsset(g, "css/"+u.Filename, false) }) } func (h WebsiteHandler) serveAsset(g *gin.Context, fn string, repl bool) ginext.HTTPResponse { _data, err := website.Assets.ReadFile(fn) if err != nil { return ginext.Status(http.StatusNotFound) } data := string(_data) if repl { failed := false data = h.rexTemplate.ReplaceAllFunc(data, func(match string) string { prefix := len("{{template|") suffix := len("}}") fnSub := match[prefix : len(match)-suffix] fnSub = strings.ReplaceAll(fnSub, "[theme]", h.getTheme(g)) subdata, err := website.Assets.ReadFile(fnSub) if err != nil { log.Error().Str("templ", string(match)).Str("fnSub", fnSub).Str("source", fn).Msg("Failed to replace template") failed = true } return string(subdata) }) if failed { return ginresp.InternalError(errors.New("template replacement failed")) } data = h.rexConfig.ReplaceAllFunc(data, func(match string) string { prefix := len("{{config|") suffix := len("}}") cfgKey := match[prefix : len(match)-suffix] cval, ok := h.getReplConfig(cfgKey) if !ok { log.Error().Str("templ", match).Str("source", fn).Msg("Failed to replace config") failed = true } return cval }) if failed { return ginresp.InternalError(errors.New("config replacement failed")) } } mime := "text/plain" lowerFN := strings.ToLower(fn) if strings.HasSuffix(lowerFN, ".html") || strings.HasSuffix(lowerFN, ".htm") { mime = "text/html" } else if strings.HasSuffix(lowerFN, ".css") { mime = "text/css" } else if strings.HasSuffix(lowerFN, ".js") { mime = "text/javascript" } else if strings.HasSuffix(lowerFN, ".json") { mime = "application/json" } else if strings.HasSuffix(lowerFN, ".jpeg") || strings.HasSuffix(lowerFN, ".jpg") { mime = "image/jpeg" } else if strings.HasSuffix(lowerFN, ".png") { mime = "image/png" } else if strings.HasSuffix(lowerFN, ".svg") { mime = "image/svg+xml" } return ginext.Data(http.StatusOK, mime, []byte(data)) } func (h WebsiteHandler) getReplConfig(key string) (string, bool) { key = strings.TrimSpace(strings.ToLower(key)) if key == "baseurl" { return h.app.Config.BaseURL, true } if key == "ip" { return h.app.Config.ServerIP, true } if key == "port" { return h.app.Config.ServerPort, true } if key == "namespace" { return h.app.Config.Namespace, true } return "", false } func (h WebsiteHandler) getTheme(g *gin.Context) string { if c, err := g.Cookie("theme"); err != nil { return "light" } else if c == "light" { return "light" } else if c == "dark" { return "dark" } else { return "light" } }