summaryrefslogtreecommitdiff
path: root/internal/router/router.go
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
Diffstat (limited to 'internal/router/router.go')
-rw-r--r--internal/router/router.go699
1 files changed, 699 insertions, 0 deletions
diff --git a/internal/router/router.go b/internal/router/router.go
new file mode 100644
index 0000000..c8b5686
--- /dev/null
+++ b/internal/router/router.go
@@ -0,0 +1,699 @@
+package router
+
+import (
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "html/template"
+ "log/slog"
+ "net"
+ "net/url"
+ "os"
+ "slices"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/SayaAndy/saya-today-web/config"
+ "github.com/SayaAndy/saya-today-web/internal/blog"
+ "github.com/SayaAndy/saya-today-web/internal/blogtrigger"
+ "github.com/SayaAndy/saya-today-web/internal/factgiver"
+ "github.com/SayaAndy/saya-today-web/internal/glightbox"
+ "github.com/SayaAndy/saya-today-web/internal/mailer"
+ "github.com/SayaAndy/saya-today-web/internal/tailwind"
+ "github.com/SayaAndy/saya-today-web/internal/templatemanager"
+ "github.com/dgraph-io/ristretto/v2"
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/fiber/v2/middleware/compress"
+ "github.com/gofiber/fiber/v2/middleware/cors"
+ "github.com/gofiber/fiber/v2/middleware/etag"
+ "github.com/golang-migrate/migrate/v4"
+ "github.com/golang-migrate/migrate/v4/database/sqlite3"
+ "github.com/yuin/goldmark"
+ "github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
+ "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/util"
+)
+
+type CacheSetting int
+
+const (
+ Disabled CacheSetting = iota
+ ByUrlOnly
+ ByUrlAndQuery
+)
+
+type LangSetting int
+
+const (
+ NotRequired LangSetting = iota
+ InPath
+ InForm
+ InReferer
+)
+
+var (
+ Routes = make([]Route, 0)
+)
+
+type SitemapInfo struct {
+ Loc string
+ LastModified time.Time
+ ChangeFreq string
+ Priority float32
+}
+
+type MetaField struct {
+ Name string
+ Property string
+ Content string
+}
+
+type Route interface {
+ Filter() (method string, path string)
+ IsTemplated() bool
+ ToCache() CacheSetting
+ CacheDuration() time.Duration
+ ToValidateLang() LangSetting
+ TemplatesToInject() []string
+ SitemapInfo(supplements *Supplements) []SitemapInfo
+ ContentType() string
+ RateLimiter() *fiber.Handler
+ Render(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error)
+ AddMeta(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (meta []MetaField, err error)
+ AddLinkedData(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (ld map[string]any, err error)
+ RenderHeader(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error)
+ RenderBody(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error)
+ RenderFooter(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error)
+ RenderTopEmbeds(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error)
+ RenderBottomEmbeds(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (statusCode int, err error)
+}
+
+type Supplements struct {
+ DB *sql.DB
+ BlogClient blog.Client
+ AvailableLanguages []config.AvailableLanguageConfig
+ ClientCache *ClientCache
+ PageCache *ristretto.Cache[string, []byte]
+ FactGiver *factgiver.FactGiver
+ Mailer *mailer.Mailer
+ BlogTrigger *blogtrigger.BlogTriggerScheduler
+ TemplateManager *templatemanager.TemplateManager
+ MarkdownRenderer goldmark.Markdown
+ Meta config.MetaConfig
+ PhotoStorage config.PhotoStorageConfig
+ StaticStorage config.StaticStorageConfig
+}
+
+type Router struct {
+ supplements *Supplements
+ app *fiber.App
+ templatedRoutes map[string]map[string]Route
+ templatedPathMatcher *PathMatcher
+ canonicalEndpoint string
+ endpoint config.EndpointConfig
+}
+
+func NewRouter(cfg *config.Config) (*Router, error) {
+ supplements := &Supplements{
+ AvailableLanguages: cfg.AvailableLanguages,
+ }
+
+ var err error
+
+ supplements.DB, err = sql.Open(cfg.Auth.Db.Type, cfg.Auth.Db.Cfg.DSN)
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize db: %w", err)
+ }
+
+ driver, err := sqlite3.WithInstance(supplements.DB, &sqlite3.Config{})
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize driver for migrating db: %w", err)
+ }
+
+ m, err := migrate.NewWithDatabaseInstance(
+ "file://migrations",
+ cfg.Auth.Db.Type, driver)
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize migration client: %w", err)
+ }
+
+ if err = m.Up(); err != nil && err == errors.New("no change") {
+ return nil, fmt.Errorf("fail to apply migrations: %w", err)
+ }
+ slog.Debug("successfully applied migrations")
+
+ supplements.BlogClient, err = blog.NewClientMap[cfg.BlogPages.Storage.Type](&cfg.BlogPages.Storage)
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize blog client: type %s: %w", cfg.BlogPages.Storage.Type, err)
+ }
+
+ supplements.MarkdownRenderer = goldmark.New(
+ goldmark.WithExtensions(
+ glightbox.NewGLightboxExtension(cfg.PhotoStorage),
+ tailwind.NewTailwindExtension(),
+ ),
+ goldmark.WithParserOptions(
+ parser.WithAutoHeadingID(),
+ parser.WithAttribute(),
+ ),
+ goldmark.WithRenderer(
+ renderer.NewRenderer(
+ renderer.WithNodeRenderers(
+ util.Prioritized(tailwind.NewCustomLinkRenderer(html.WithUnsafe(), html.WithXHTML()), 50),
+ util.Prioritized(html.NewRenderer(html.WithXHTML()), 100),
+ ),
+ ),
+ ),
+ )
+
+ supplements.ClientCache, err = NewClientCache(supplements.DB, []byte(cfg.Auth.Salt))
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize client cache: %w", err)
+ }
+
+ supplements.PageCache, err = ristretto.NewCache(&ristretto.Config[string, []byte]{
+ NumCounters: 1e6, // 1,000,000
+ MaxCost: 1 << 29, // 512 MB
+ BufferItems: 64, // number of keys per Get buffer.
+ })
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize page cache: %w", err)
+ }
+
+ supplements.FactGiver, err = factgiver.NewFactGiver(&cfg.FactGiver, supplements.AvailableLanguages)
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize fact giver: %w", err)
+ }
+
+ supplements.Mailer, err = mailer.NewMailer(supplements.DB, cfg.Mail.ClientHost, cfg.Mail.MailHost,
+ cfg.Mail.PublicName, cfg.Mail.MailAddress, cfg.Mail.Username, cfg.Mail.Password, []byte(cfg.Mail.Salt))
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize mailer: %w", err)
+ }
+
+ supplements.BlogTrigger, err = blogtrigger.NewBlogTriggerScheduler(supplements.BlogClient, cfg.AvailableLanguages, cfg.Mail.Trigger.OnNewPost,
+ func(bp []*blog.Page) error {
+ for _, post := range bp {
+ if err := supplements.Mailer.NewPost(post); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize blog trigger: %w", err)
+ }
+
+ supplements.TemplateManager, err = templatemanager.NewTemplateManager()
+ if err != nil {
+ return nil, fmt.Errorf("fail to initialize template manager: %w", err)
+ }
+
+ supplements.Meta = cfg.Meta
+ supplements.PhotoStorage = cfg.PhotoStorage
+ supplements.StaticStorage = cfg.StaticStorage
+
+ enablePrintRoutes := false
+ if cfg.LogLevel <= slog.LevelDebug {
+ enablePrintRoutes = true
+ }
+
+ app := fiber.New(fiber.Config{
+ EnablePrintRoutes: enablePrintRoutes,
+ ProxyHeader: "X-Forwarded-For",
+ })
+
+ app.Use(cors.New(cors.Config{
+ AllowOrigins: strings.Join(cfg.AllowOrigins, ","),
+ AllowMethods: "GET,POST,OPTIONS",
+ AllowHeaders: "Origin, Content-Type, Accept",
+ }))
+
+ app.Use(compress.New(compress.Config{
+ Level: compress.LevelBestSpeed,
+ }))
+
+ app.Use(etag.New())
+
+ app.Use(func(c *fiber.Ctx) error {
+ path := c.Path()
+ if len(path) > 1 && path[len(path)-1] == '/' {
+ return c.Redirect(strings.TrimRight(path, "/"), fiber.StatusMovedPermanently)
+ }
+ return c.Next()
+ })
+
+ templatedRoutes := make(map[string]map[string]Route)
+ templatedPathMatcher := NewPathMatcher()
+
+ return &Router{supplements, app, templatedRoutes, templatedPathMatcher, cfg.CanonicalEndpoint, cfg.Endpoint}, nil
+}
+
+func (r *Router) InitRoutes() (err error) {
+ r.supplements.TemplateManager.Add("general-page", "views/layouts/general-page.html")
+ for _, route := range Routes {
+ method, match := route.Filter()
+
+ if err = r.supplements.TemplateManager.Add(method+" "+match, route.TemplatesToInject()...); err != nil {
+ return fmt.Errorf("failed to add '%s %s' route into template manager: %w", method, match, err)
+ }
+
+ if rateLimiter := route.RateLimiter(); rateLimiter != nil {
+ r.app.Use(match, *rateLimiter)
+ }
+
+ if route.IsTemplated() {
+ if _, ok := r.templatedRoutes[method]; !ok {
+ r.templatedRoutes[method] = make(map[string]Route)
+ }
+ r.templatedRoutes[method][match] = route
+ r.templatedPathMatcher.AddRoute(method, match)
+
+ currentRoute := route
+ r.app.Add(method, match, func(c *fiber.Ctx) error {
+ lang, err := r.getAndValidateLang(c, currentRoute.ToValidateLang())
+ if err != nil {
+ return err
+ }
+ err = r.generalPage(c, currentRoute, lang)
+ return err
+ })
+ } else {
+ currentRoute := route
+ r.app.Add(method, match, func(c *fiber.Ctx) error {
+ lang, err := r.getAndValidateLang(c, currentRoute.ToValidateLang())
+ if err != nil {
+ return err
+ }
+
+ var cacheKey string
+ trimmedPath := strings.Trim(c.Path(), "/")
+ queryString := c.Request().URI().QueryString()
+
+ switch currentRoute.ToCache() {
+ case ByUrlOnly:
+ cacheKey = fmt.Sprintf("%s.full-page.%s", method, trimmedPath)
+ case ByUrlAndQuery:
+ cacheKey = fmt.Sprintf("%s.full-page.%s.%s", method, trimmedPath, queryString)
+ case Disabled:
+ c.Set("Cache-Control", "no-store, no-cache, must-revalidate")
+ }
+
+ defaultMap := fiber.Map{
+ "Lang": lang,
+ "Path": trimmedPath,
+ "QueryString": queryString,
+ "CanonicalEndpoint": r.canonicalEndpoint,
+ "StaticStorage": r.supplements.StaticStorage,
+ "PhotoStorage": r.supplements.PhotoStorage,
+ }
+
+ statusCode, err := currentRoute.Render(c, r.supplements, lang, defaultMap)
+ method := c.Method()
+ _, match := currentRoute.Filter()
+ if err != nil {
+ slog.Error("failed to finish rendering a page",
+ slog.Int("status_code", statusCode),
+ slog.String("method", method),
+ slog.String("path", c.Path()),
+ slog.String("match", match),
+ slog.String("query", string(queryString)),
+ slog.String("error", err.Error()),
+ )
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(statusCode).SendString(err.Error())
+ }
+
+ content, err := r.supplements.TemplateManager.Render(method+" "+match, defaultMap)
+ if err != nil {
+ slog.Error("failed to generate div",
+ slog.String("method", method),
+ slog.String("path", c.Path()),
+ slog.String("match", match),
+ slog.String("query", string(queryString)),
+ slog.String("error", err.Error()),
+ )
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div")
+ }
+ if val, ok := defaultMap["Output"]; ok && len(content) == 0 {
+ content = val.([]byte)
+ }
+
+ if statusCode >= 200 && statusCode < 300 && route.ToCache() != Disabled {
+ go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration())
+ }
+
+ c.Set(fiber.HeaderContentType, route.ContentType())
+ return c.Status(statusCode).Send(content)
+ })
+ }
+ }
+
+ segments := []string{"header", "body", "footer", "top-embeds", "bottom-embeds"}
+
+ r.app.Get("/api/v1/general-page/:part", func(c *fiber.Ctx) error {
+ part := c.Params("part")
+ if !slices.Contains(segments, part) {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("unknown segment '%s'", part))
+ }
+ c.Set("Cache-Control", "no-store, no-cache, must-revalidate")
+ err := r.generalPageSegment(c, part)
+ return err
+ })
+
+ for _, segment := range segments {
+ r.supplements.TemplateManager.Add("general-page-"+segment, "views/partials/general-page-"+segment+".html")
+ }
+
+ r.app.Static("/", "./static")
+
+ // Routes = make([]Route, 0)
+
+ return nil
+}
+
+func (r *Router) Listen() error {
+ switch r.endpoint.Type {
+ case "unix":
+ unixConfig := r.endpoint.Config.(*config.UnixConfig)
+ endpoint, _ := strings.CutPrefix(unixConfig.Path, "unix://")
+
+ if err := os.Remove(endpoint); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("error while cleaning up existing unix socket: %w", err)
+ }
+
+ ln, err := net.Listen("unix", endpoint)
+ if err != nil {
+ return fmt.Errorf("error while initializing unix listener: %w", err)
+ }
+ chmod, _ := strconv.ParseUint(unixConfig.Chmod[1:], 8, 32)
+ os.Chmod(unixConfig.Path, os.FileMode(chmod))
+ if err := r.app.Listener(ln); err != nil {
+ return fmt.Errorf("error while running fiber server: %w", err)
+ }
+ case "http":
+ httpConfig := r.endpoint.Config.(*config.HttpConfig)
+ fmt.Print(httpConfig)
+ if err := r.app.Listen(httpConfig.ListenOn); err != nil {
+ return fmt.Errorf("error while running fiber server: %w", err)
+ }
+ default:
+ return fmt.Errorf("error with initializing fiber server: invalid endpoint type (supported are unix and http)")
+ }
+
+ return nil
+}
+
+func (r *Router) Close() (err error) {
+ allErrors := make([]error, 0)
+ slog.Debug("shutting down blog trigger scheduler")
+ if err = r.supplements.BlogTrigger.Close(); err != nil {
+ allErrors = append(allErrors, fmt.Errorf("fail to shutdown blog trigger scheduler: %w", err))
+ }
+ slog.Debug("shutting down fiber server")
+ if err = r.app.Shutdown(); err != nil {
+ allErrors = append(allErrors, fmt.Errorf("fail to shutdown fiber server: %w", err))
+ }
+ slog.Debug("dumping cache")
+ if err = r.supplements.ClientCache.Close(); err != nil {
+ allErrors = append(allErrors, fmt.Errorf("fail to dump cache: %w", err))
+ }
+ slog.Debug("closing db connection")
+ if err = r.supplements.DB.Close(); err != nil {
+ allErrors = append(allErrors, fmt.Errorf("fail to close db connection: %w", err))
+ }
+ slog.Debug("closing page cache")
+ r.supplements.PageCache.Close()
+
+ return errors.Join(allErrors...)
+}
+
+func (r *Router) generalPage(c *fiber.Ctx, route Route, lang string) error {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+
+ path := c.Path()
+ method := c.Method()
+ trimmedPath := strings.Trim(path, "/")
+ queryString := c.Request().URI().QueryString()
+ cacheKey := ""
+
+ switch route.ToCache() {
+ case ByUrlOnly:
+ cacheKey = fmt.Sprintf("%s.general-page.%s", method, trimmedPath)
+ case ByUrlAndQuery:
+ cacheKey = fmt.Sprintf("%s.general-page.%s.%s", method, trimmedPath, queryString)
+ }
+
+ if val, ok := r.supplements.PageCache.Get(cacheKey); val != nil && ok {
+ c.Set(fiber.HeaderContentType, route.ContentType())
+ return c.Status(fiber.StatusOK).Send(val)
+ }
+
+ valueMap := fiber.Map{
+ "Lang": lang,
+ "Path": trimmedPath,
+ "QueryString": queryString,
+ "CanonicalEndpoint": r.canonicalEndpoint,
+ "StaticStorage": r.supplements.StaticStorage,
+ "PhotoStorage": r.supplements.PhotoStorage,
+ }
+
+ var err error
+ if valueMap["Meta"], err = route.AddMeta(c, r.supplements, lang, valueMap); err != nil {
+ slog.Warn("failed to receive meta", slog.String("path", path), slog.String("error", err.Error()))
+ }
+
+ var ld map[string]any
+ if ld, err = route.AddLinkedData(c, r.supplements, lang, valueMap); err != nil {
+ slog.Warn("failed to receive linked data", slog.String("path", path), slog.String("error", err.Error()))
+ }
+ if ld != nil {
+ ldBytes, _ := json.Marshal(ld)
+ valueMap["LinkedData"] = template.JS(ldBytes)
+ }
+
+ syntheticReferer := path
+ if len(queryString) > 0 {
+ syntheticReferer += "?" + string(queryString)
+ }
+ c.Request().Header.Set("Referer", syntheticReferer)
+
+ parts := []struct {
+ name string
+ key string
+ render func(c *fiber.Ctx, supplements *Supplements, lang string, templateMap fiber.Map) (int, error)
+ }{
+ {"top-embeds", "RenderedTopEmbeds", route.RenderTopEmbeds},
+ {"header", "RenderedHeader", route.RenderHeader},
+ {"body", "RenderedBody", route.RenderBody},
+ {"footer", "RenderedFooter", route.RenderFooter},
+ {"bottom-embeds", "RenderedBottomEmbeds", route.RenderBottomEmbeds},
+ }
+
+ for _, p := range parts {
+ statusCode, err := p.render(c, r.supplements, lang, valueMap)
+ if err != nil {
+ slog.Error("failed to render segment for full page",
+ slog.String("path", path),
+ slog.String("segment", p.name),
+ slog.String("error", err.Error()),
+ )
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(statusCode).SendString(err.Error())
+ }
+ segContent, err := r.supplements.TemplateManager.Render("general-page-"+p.name, valueMap, route.TemplatesToInject()...)
+ if err != nil {
+ slog.Error("failed to render segment template for full page",
+ slog.String("path", path),
+ slog.String("segment", p.name),
+ slog.String("error", err.Error()),
+ )
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate segment")
+ }
+ valueMap[p.key] = template.HTML(segContent)
+ }
+
+ content, err := r.supplements.TemplateManager.Render("general-page", valueMap)
+ if err != nil {
+ slog.Warn("failed to generate full page", slog.String("path", path), slog.String("error", err.Error()))
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate full page")
+ }
+
+ go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration())
+ c.Set(fiber.HeaderContentType, route.ContentType())
+ return c.Status(fiber.StatusOK).Send(content)
+}
+
+func (r *Router) generalPageSegment(c *fiber.Ctx, part string) error {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+
+ path, pathParts, queryString, err := GetPathFromReferer(c)
+ if err != nil {
+ return err
+ }
+
+ method := c.Method()
+ pattern, _, matched := r.templatedPathMatcher.MatchPath(method, path)
+ if !matched {
+ return c.Status(fiber.ErrNotFound.Code).SendString(fmt.Sprintf("fail to get templated page for '%s %s': not found", method, path))
+ }
+ route := r.templatedRoutes[method][pattern]
+
+ lang := ""
+
+ switch route.ToValidateLang() {
+ case InPath, InReferer:
+ if len(pathParts) > 0 {
+ lang = pathParts[0]
+ }
+ if _, err := r.getAndValidateLang(c, NotRequired, lang); err != nil {
+ return err
+ }
+ case InForm:
+ if lang, err = r.getAndValidateLang(c, InForm); err != nil {
+ return err
+ }
+ }
+
+ cacheKey := ""
+ trimmedPath := strings.Trim(path, "/")
+ requestQuery := string(c.Request().URI().QueryString())
+ switch route.ToCache() {
+ case ByUrlOnly:
+ cacheKey = fmt.Sprintf("%s.%s.%s", method, part, trimmedPath)
+ case ByUrlAndQuery:
+ cacheKey = fmt.Sprintf("%s.%s.%s.%s.%s", method, part, trimmedPath, queryString, requestQuery)
+ }
+
+ if route.ToCache() != Disabled {
+ if val, ok := r.supplements.PageCache.Get(cacheKey); val != nil && ok {
+ c.Set(fiber.HeaderContentType, route.ContentType())
+ return c.Status(fiber.StatusOK).Send(val)
+ }
+ }
+
+ var statusCode int
+ defaultMap := fiber.Map{
+ "Lang": lang,
+ "Path": strings.Trim(path, "/"),
+ "QueryString": queryString,
+ "CanonicalEndpoint": r.canonicalEndpoint,
+ "StaticStorage": r.supplements.StaticStorage,
+ "PhotoStorage": r.supplements.PhotoStorage,
+ }
+
+ switch part {
+ case "body":
+ statusCode, err = route.RenderBody(c, r.supplements, lang, defaultMap)
+ case "header":
+ statusCode, err = route.RenderHeader(c, r.supplements, lang, defaultMap)
+ case "footer":
+ statusCode, err = route.RenderFooter(c, r.supplements, lang, defaultMap)
+ case "top-embeds":
+ statusCode, err = route.RenderTopEmbeds(c, r.supplements, lang, defaultMap)
+ case "bottom-embeds":
+ statusCode, err = route.RenderBottomEmbeds(c, r.supplements, lang, defaultMap)
+ }
+ if err != nil {
+ slog.Error(err.Error(),
+ slog.String("method", method),
+ slog.String("path", path),
+ slog.String("query", string(queryString)),
+ slog.String("segment", part),
+ slog.String("error", err.Error()),
+ )
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(statusCode).SendString(err.Error())
+ }
+
+ content, err := r.supplements.TemplateManager.Render("general-page-"+part, defaultMap, route.TemplatesToInject()...)
+ if err != nil {
+ slog.Error("failed to generate div",
+ slog.String("method", method),
+ slog.String("path", path),
+ slog.String("query", string(queryString)),
+ slog.String("segment", part),
+ slog.String("error", err.Error()),
+ )
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return c.Status(fiber.ErrInternalServerError.Code).SendString("failed to generate div")
+ }
+
+ if statusCode == fiber.StatusNoContent && len(content) != 0 {
+ statusCode = fiber.StatusOK
+ }
+
+ if statusCode >= 200 && statusCode < 300 && route.ToCache() != Disabled {
+ go r.supplements.PageCache.SetWithTTL(cacheKey, content, int64(len(content)), route.CacheDuration())
+ }
+
+ c.Set(fiber.HeaderContentType, route.ContentType())
+ return c.Status(statusCode).Send(content)
+}
+
+func (r *Router) getAndValidateLang(c *fiber.Ctx, langSetting LangSetting, defaultLang ...string) (string, error) {
+ var lang string
+ if len(defaultLang) > 0 {
+ lang = defaultLang[0]
+ }
+
+ switch langSetting {
+ case NotRequired:
+ return lang, nil
+
+ case InPath:
+ path := c.Path()
+
+ pathParts := strings.Split(strings.Trim(path, "/"), "/")
+ if len(pathParts) == 1 && pathParts[0] == "" {
+ pathParts = []string{}
+ }
+ if len(pathParts) > 0 && len(pathParts[0]) == 2 {
+ lang = pathParts[0]
+ }
+
+ case InForm:
+ lang = c.FormValue("lang")
+
+ case InReferer:
+ _, pathParts, _, err := GetPathFromReferer(c)
+ if err != nil {
+ return "", err
+ }
+
+ if len(pathParts) >= 1 {
+ lang = pathParts[0]
+ }
+ }
+
+ for _, availableLang := range r.supplements.AvailableLanguages {
+ if availableLang.Name == lang {
+ return lang, nil
+ }
+ }
+ return "", fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("lang value is invalid: '%s' is not considered an available language", lang))
+}
+
+func GetPathFromReferer(c *fiber.Ctx) (path string, pathParts []string, queryString string, err error) {
+ referer := c.Get("Referer")
+ if referer == "" {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return "", nil, "", c.Status(fiber.ErrBadRequest.Code).SendString("'Referer' header is empty")
+ }
+ urlStruct, err := url.ParseRequestURI(referer)
+ if err != nil {
+ c.Set(fiber.HeaderContentType, fiber.MIMETextPlainCharsetUTF8)
+ return "", nil, "", c.Status(fiber.ErrBadRequest.Code).SendString(fmt.Sprintf("'Referer' header is invalid: %s", err.Error()))
+ }
+
+ path = urlStruct.EscapedPath()
+ pathParts = strings.Split(strings.Trim(path, "/"), "/")
+ queryString = urlStruct.RawQuery
+ return
+}