Files
2026-05-13 22:34:11 +02:00

393 lines
11 KiB
Go

package middleware
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/labstack/echo/v4"
psconfig "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/config"
pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie"
)
type AnonymousSessionInitializer interface {
NewAnonymous(ctx context.Context, req *http.Request, cookieName string) (*pscookie.SessionContext, error)
}
type SessionExpiryRefresher interface {
RefreshExpiry(ctx context.Context, session *pscookie.SessionContext) error
}
type SessionCookieNameResolver interface {
ResolveCookieName(ctx context.Context, req *http.Request) (string, error)
}
type SessionCookiePathResolver interface {
ResolveCookiePath(ctx context.Context, req *http.Request) (string, error)
}
type LanguageResolver interface {
ResolveLanguageID(ctx context.Context, req *http.Request, fallback int64) int64
}
type ProductRouteMatcher interface {
Owns(path string) bool
}
func Session(cfg psconfig.Config, codec pscookie.Codec, initializer AnonymousSessionInitializer, languageResolver LanguageResolver, matcher ProductRouteMatcher) echo.MiddlewareFunc {
ownership := cfg.ParseRouteOwnership()
expiryRefresher, _ := initializer.(SessionExpiryRefresher)
cookieNameResolver, _ := initializer.(SessionCookieNameResolver)
cookiePathResolver, _ := initializer.(SessionCookiePathResolver)
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ownedRoute := ownsProductRoute(ownership.ProductPrefixes, c.Request().URL.Path, matcher)
configuredCookieName := cfg.DeriveCookieName(requestCookieHost(c.Request()))
if cookieNameResolver != nil {
resolvedCookieName, err := cookieNameResolver.ResolveCookieName(c.Request().Context(), c.Request())
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("prestashop cookie name resolution failed: %v", err))
}
if strings.TrimSpace(resolvedCookieName) != "" {
configuredCookieName = resolvedCookieName
}
}
cookieName, rawCookie := findPrestaShopCookie(c.Request(), configuredCookieName)
if cookieName == "" {
cookieName = configuredCookieName
}
session, err := codec.Decode(rawCookie)
if err != nil {
if ownedRoute {
return echo.NewHTTPError(http.StatusInternalServerError, "prestashop cookie decode failed")
}
SetSession(c, &pscookie.SessionContext{
CookieName: cookieName,
RawCookie: rawCookie,
Values: map[string]string{},
ParseStatus: pscookie.ParseStatusInvalid,
})
return next(c)
}
session.CookieName = cookieName
if ownedRoute && initializer != nil && shouldBootstrapAnonymousSession(rawCookie, session) {
session, err = initializer.NewAnonymous(c.Request().Context(), c.Request(), cookieName)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("prestashop session bootstrap failed: %v", err))
}
}
if ownedRoute {
applyRequestLanguage(session, resolveRequestLanguageID(c.Request().Context(), c.Request(), session, languageResolver))
applyRequestMarket(session, requestMarketSelection(c.Request()))
}
if ownedRoute && shouldSetSessionCookie(rawCookie, session) {
cookiePath := "/"
if cookiePathResolver != nil {
resolvedCookiePath, err := cookiePathResolver.ResolveCookiePath(c.Request().Context(), c.Request())
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("prestashop cookie path resolution failed: %v", err))
}
if strings.TrimSpace(resolvedCookiePath) != "" {
cookiePath = resolvedCookiePath
}
}
if expiryRefresher != nil {
if err := expiryRefresher.RefreshExpiry(c.Request().Context(), session); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("prestashop session expiry refresh failed: %v", err))
}
}
encoded, err := codec.Encode(session)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "prestashop cookie encode failed")
}
session.RawCookie = encoded
setPrestaShopCookie(c.Request(), c.Response(), session, cookieName, encoded, cookiePath)
if redirectURL, ok := clearMarketSelectionURL(c.Request()); ok {
return c.Redirect(http.StatusSeeOther, redirectURL)
}
}
SetSession(c, session)
return next(c)
}
}
}
type marketSelection struct {
CountryID int64
CountryISO string
CurrencyID int64
}
func resolveRequestLanguageID(ctx context.Context, req *http.Request, session *pscookie.SessionContext, resolver LanguageResolver) int64 {
if resolver == nil {
return 0
}
return resolver.ResolveLanguageID(ctx, req, sessionLanguageID(session))
}
func findPrestaShopCookie(req *http.Request, configuredName string) (name, value string) {
cookies := req.Cookies()
for _, cookie := range cookies {
if cookie.Name == configuredName {
return cookie.Name, cookie.Value
}
}
prefix := cookiePrefix(configuredName)
if prefix == "" {
return "", ""
}
for _, cookie := range cookies {
if strings.HasPrefix(cookie.Name, prefix) {
return cookie.Name, cookie.Value
}
}
return "", ""
}
func cookiePrefix(configuredName string) string {
if configuredName == "" {
return ""
}
if strings.HasPrefix(configuredName, "PrestaShop-") {
return "PrestaShop-"
}
if idx := strings.Index(configuredName, "-"); idx >= 0 {
return configuredName[:idx+1]
}
return ""
}
func shouldBootstrapAnonymousSession(rawCookie string, session *pscookie.SessionContext) bool {
return session == nil || rawCookie == ""
}
func shouldSetSessionCookie(rawCookie string, session *pscookie.SessionContext) bool {
if session == nil {
return false
}
if rawCookie == "" {
return true
}
return rawCookie != session.RawCookie
}
func applyRequestLanguage(session *pscookie.SessionContext, languageID int64) {
if session == nil || languageID == 0 {
return
}
if current := sessionLanguageID(session); current == languageID {
return
}
if session.Values == nil {
session.Values = map[string]string{}
}
value := strconv.FormatInt(languageID, 10)
session.LanguageID = int64Ptr(languageID)
session.Values["id_lang"] = value
session.Values["id_language"] = value
session.OrderedKeys = appendOrderedKeyIfMissing(session.OrderedKeys, "id_lang")
session.OrderedKeys = appendOrderedKeyIfMissing(session.OrderedKeys, "id_language")
session.OrderedKeys = moveOrderedKeyToEnd(session.OrderedKeys, "checksum")
session.Plaintext = ""
session.RawCookie = ""
}
func applyRequestMarket(session *pscookie.SessionContext, selection marketSelection) {
if session == nil || selection.CountryISO == "" || selection.CurrencyID == 0 {
return
}
currentCountry := ""
currentCurrency := int64(0)
currentCountryID := int64(0)
if session.Values != nil {
currentCountry = strings.ToUpper(strings.TrimSpace(session.Values["iso_code_country"]))
if session.CurrencyID != nil {
currentCurrency = *session.CurrencyID
}
currentCountryID, _ = strconv.ParseInt(session.Values["id_country"], 10, 64)
}
if currentCountry == selection.CountryISO && currentCurrency == selection.CurrencyID && currentCountryID == selection.CountryID {
return
}
if session.Values == nil {
session.Values = map[string]string{}
}
session.CurrencyID = int64Ptr(selection.CurrencyID)
session.Values["iso_code_country"] = selection.CountryISO
session.Values["id_currency"] = strconv.FormatInt(selection.CurrencyID, 10)
session.OrderedKeys = appendOrderedKeyIfMissing(session.OrderedKeys, "iso_code_country")
session.OrderedKeys = appendOrderedKeyIfMissing(session.OrderedKeys, "id_currency")
session.OrderedKeys = moveOrderedKeyToEnd(session.OrderedKeys, "checksum")
session.Plaintext = ""
session.RawCookie = ""
}
func sessionLanguageID(session *pscookie.SessionContext) int64 {
if session == nil || session.LanguageID == nil {
return 0
}
return *session.LanguageID
}
func appendOrderedKeyIfMissing(keys []string, key string) []string {
for i, existing := range keys {
if existing != key {
continue
}
if i >= 0 {
return keys
}
}
return append(keys, key)
}
func moveOrderedKeyToEnd(keys []string, key string) []string {
for i, existing := range keys {
if existing == key {
keys = append(keys[:i], keys[i+1:]...)
return append(keys, key)
}
}
return keys
}
func int64Ptr(value int64) *int64 {
if value == 0 {
return nil
}
v := value
return &v
}
func requestMarketSelection(req *http.Request) marketSelection {
if req == nil || req.URL == nil {
return marketSelection{}
}
raw := strings.TrimSpace(req.URL.Query().Get("market"))
if raw == "" {
return marketSelection{}
}
parts := strings.Split(raw, ":")
if len(parts) != 2 && len(parts) != 3 {
return marketSelection{}
}
selection := marketSelection{}
var countryISO string
var currencyValue string
if len(parts) == 3 {
countryID, err := strconv.ParseInt(strings.TrimSpace(parts[0]), 10, 64)
if err != nil || countryID == 0 {
return marketSelection{}
}
selection.CountryID = countryID
countryISO = strings.ToUpper(strings.TrimSpace(parts[1]))
currencyValue = parts[2]
} else {
countryISO = strings.ToUpper(strings.TrimSpace(parts[0]))
currencyValue = parts[1]
}
currencyID, err := strconv.ParseInt(strings.TrimSpace(currencyValue), 10, 64)
if err != nil || currencyID == 0 {
return marketSelection{}
}
if len(countryISO) < 2 || len(countryISO) > 5 {
return marketSelection{}
}
selection.CountryISO = countryISO
selection.CurrencyID = currencyID
return selection
}
func clearMarketSelectionURL(req *http.Request) (string, bool) {
if req == nil || req.URL == nil {
return "", false
}
query := req.URL.Query()
if query.Get("market") == "" {
return "", false
}
query.Del("market")
cleanPath := req.URL.Path
if cleanPath == "" {
cleanPath = "/"
}
if encoded := query.Encode(); encoded != "" {
return cleanPath + "?" + encoded, true
}
return cleanPath, true
}
func setPrestaShopCookie(req *http.Request, res *echo.Response, session *pscookie.SessionContext, name, value, path string) {
maxAge := 1
if session != nil && session.ExpiresAt != nil {
maxAge = int(session.ExpiresAt.UTC().Unix())
}
if strings.TrimSpace(path) == "" {
path = "/"
}
header := fmt.Sprintf("%s=%s; path=%s; max-age=%d; HttpOnly; SameSite=Lax", name, value, path, maxAge)
if requestCookieSecure(req) {
header += "; Secure"
}
res.Header().Add(echo.HeaderSetCookie, header)
}
func requestCookieHost(req *http.Request) string {
if req == nil {
return ""
}
host := req.Header.Get("X-Forwarded-Host")
if host == "" {
host = req.Host
}
if strings.Contains(host, ",") {
host = strings.TrimSpace(strings.Split(host, ",")[0])
}
return host
}
func requestCookieSecure(req *http.Request) bool {
if req.TLS != nil {
return true
}
if forwarded := req.Header.Get("X-Forwarded-Proto"); forwarded != "" {
if strings.Contains(forwarded, ",") {
forwarded = strings.TrimSpace(strings.Split(forwarded, ",")[0])
}
return strings.EqualFold(forwarded, "https")
}
return false
}
func ownsProductRoute(prefixes []string, path string, matcher ProductRouteMatcher) bool {
if matcher != nil {
return matcher.Owns(path)
}
for _, prefix := range prefixes {
if prefix != "" && len(path) >= len(prefix) && path[:len(prefix)] == prefix {
return true
}
}
return false
}