package web import ( "embed" "errors" "github.com/1f349/lavender/logger" "github.com/1f349/lavender/utils" "github.com/1f349/overlapfs" "html" "html/template" "io" "io/fs" "net/http" "os" "path/filepath" "strings" ) var ( //go:embed dist webDist embed.FS webCombinedDir fs.FS pageTemplates *template.Template loadOnce utils.Once[error] ) func LoadPages(wd string) error { return loadOnce.Do(func() (err error) { webBuild, err := fs.Sub(webDist, "dist") if err != nil { return err } webCombinedDir = webBuild if wd != "" { webDir := filepath.Join(wd, "web") err = os.Mkdir(webDir, os.ModePerm) if err != nil && !errors.Is(err, fs.ErrExist) { return err } wdFs := os.DirFS(webDir) webCombinedDir = overlapfs.OverlapFS{A: webBuild, B: wdFs} } // TODO(melon): figure this out layer webCombinedDir = webBuild pageTemplates, err = findAndParseTemplates(webCombinedDir, template.FuncMap{ "emailHide": utils.EmailHide, "renderOptionTag": renderOptionTag, "renderCheckboxTag": renderCheckboxTag, }) return err }) } func findAndParseTemplates(rootDir fs.FS, funcMap template.FuncMap) (*template.Template, error) { root := template.New("") err := fs.WalkDir(rootDir, ".", func(p string, d fs.DirEntry, e1 error) error { if d.IsDir() || !strings.HasSuffix(p, ".html") { return nil } if e1 != nil { return e1 } fileContents, err := fs.ReadFile(webCombinedDir, p) if err != nil { return err } t := root.New(p).Delims("[[", "]]").Funcs(funcMap) _, err = t.Parse(string(fileContents)) return err }) return root, err } func renderOptionTag(value, display string, selectedValue string) template.HTML { var selectedParam string if value == selectedValue { selectedParam = " selected" } return template.HTML("") } func renderCheckboxTag(name, id string, checked bool) template.HTML { var checkedParam string if checked { checkedParam = " checked" } return template.HTML("") } func RenderPageTemplate(wr io.Writer, name string, data any) bool { logger.Logger.Helper() p := name + ".html" err := pageTemplates.ExecuteTemplate(wr, p, data) if err != nil { logger.Logger.Warn("Failed to render page", "name", name, "err", err) } return err == nil } func RenderWebAsset(rw http.ResponseWriter, req *http.Request, name string) { // Disallow paths containing ".." - directory traversal is a security issue. if containsDotDot(name) { http.Error(rw, "400 Bad Request", http.StatusBadRequest) } // Disallow paths ending in ".html" - these should only be processed by HTML // template. if strings.HasSuffix(name, ".html") { http.Error(rw, "404 Not Found", http.StatusNotFound) return } // Enjoy the power of Go stdlib http.ServeFileFS(rw, req, webCombinedDir, name) } // Go stdlib net/http/fs.go (containsDotDot) func containsDotDot(v string) bool { if !strings.Contains(v, "..") { return false } for _, ent := range strings.FieldsFunc(v, isSlashRune) { if ent == ".." { return true } } return false } // Go stdlib net/http/fs.go (isSlashRune) func isSlashRune(r rune) bool { return r == '/' || r == '\\' }