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

301 lines
8.1 KiB
Go

package config
import (
"bufio"
"crypto/md5"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie"
)
type Config struct {
AppAddr string
AppEnv string
AppDBDSN string
AssetManifestPath string
PrestaShopBaseURL string
PrestaShopProxyTarget string
PrestaShopVersion string
DomainCookie string
PrestaShopCookieKey string
PrestaShopCookieIV string
PrestaShopCookieName string
PrestaShopDBDSN string
PrestaShopDBDialect string
PrestaShopTablePrefix string
PrestaShopProjectRoot string
PrestaShopBootstrap string
RouteOwnershipConfig string
}
func Load() (Config, error) {
if err := loadDotEnv(".env"); err != nil {
return Config{}, err
}
cfg := Config{
AppAddr: envOr("APP_ADDR", ":8080"),
AppEnv: envOr("APP_ENV", "development"),
AppDBDSN: firstNonEmpty(os.Getenv("APP_DB_DSN"), os.Getenv("PRESTASHOP_DB_DSN"), dbDSNFromEnv()),
AssetManifestPath: envOr("ASSET_MANIFEST_PATH", "web/dist/manifest.json"),
PrestaShopBaseURL: os.Getenv("PRESTASHOP_BASE_URL"),
PrestaShopProxyTarget: os.Getenv("PRESTASHOP_PROXY_TARGET"),
PrestaShopVersion: envOr("PRESTASHOP_VERSION", "1.7.3"),
DomainCookie: os.Getenv("DOMAIN_COOKIE"),
PrestaShopCookieKey: os.Getenv("PRESTASHOP_COOKIE_KEY"),
PrestaShopCookieIV: os.Getenv("PRESTASHOP_COOKIE_IV"),
PrestaShopCookieName: os.Getenv("PRESTASHOP_COOKIE_NAME"),
PrestaShopDBDSN: firstNonEmpty(os.Getenv("PRESTASHOP_DB_DSN"), dbDSNFromEnv()),
PrestaShopDBDialect: envOr("PRESTASHOP_DB_DIALECT", "mariadb"),
PrestaShopTablePrefix: firstNonEmpty(os.Getenv("PRESTASHOP_TABLE_PREFIX"), os.Getenv("DB_PREFIX"), "ps_"),
PrestaShopProjectRoot: os.Getenv("PRESTASHOP_PROJECT_ROOT"),
PrestaShopBootstrap: os.Getenv("PRESTASHOP_BOOTSTRAP_PATH"),
RouteOwnershipConfig: envOr("ROUTE_OWNERSHIP_CONFIG", "/product/"),
}
if err := cfg.bootstrap(); err != nil {
return Config{}, err
}
if cfg.PrestaShopProxyTarget == "" {
return Config{}, errors.New("PRESTASHOP_PROXY_TARGET is required")
}
if cfg.PrestaShopProjectRoot == "" && cfg.PrestaShopBootstrap == "" && cfg.PrestaShopCookieKey == "" {
return Config{}, errors.New("prestashop cookie configuration is incomplete")
}
if cfg.PrestaShopDBDSN == "" {
return Config{}, errors.New("PRESTASHOP_DB_DSN is required")
}
return cfg, nil
}
func loadDotEnv(path string) error {
file, err := os.Open(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("open %s: %w", path, err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "export ") {
line = strings.TrimSpace(strings.TrimPrefix(line, "export "))
}
key, value, ok := strings.Cut(line, "=")
if !ok {
continue
}
key = strings.TrimSpace(key)
value = strings.TrimSpace(value)
if key == "" {
continue
}
value = strings.Trim(value, `"'`)
if _, exists := os.LookupEnv(key); exists {
continue
}
if err := os.Setenv(key, value); err != nil {
return fmt.Errorf("set env %s: %w", key, err)
}
}
if err := scanner.Err(); err != nil {
return fmt.Errorf("scan %s: %w", path, err)
}
return nil
}
func (c Config) CookieConfig() pscookie.Config {
return pscookie.Config{
Version: c.PrestaShopVersion,
CookieName: c.DeriveCookieName(""),
CookieKey: c.PrestaShopCookieKey,
CookieIV: c.PrestaShopCookieIV,
ProjectRoot: c.PrestaShopProjectRoot,
BootstrapPath: c.PrestaShopBootstrap,
}
}
func (c Config) DeriveCookieName(host string) string {
if c.PrestaShopCookieName != "" {
return c.PrestaShopCookieName
}
domain := fallbackCookieHashDomain(c.DomainCookie)
if domain == "" {
domain = fallbackCookieHashDomain(host)
}
if domain == "" {
domain = fallbackCookieHashDomain(c.PrestaShopBaseURL)
}
if domain == "" {
domain = fallbackCookieHashDomain(c.PrestaShopProxyTarget)
}
sum := md5.Sum([]byte(c.PrestaShopVersion + "ps-s1" + domain))
return fmt.Sprintf("PrestaShop-%x", sum)
}
func (c *Config) bootstrap() error {
if c.PrestaShopProjectRoot == "" && c.PrestaShopBootstrap != "" {
c.PrestaShopProjectRoot = filepath.Dir(filepath.Dir(c.PrestaShopBootstrap))
}
if c.PrestaShopProjectRoot == "" {
return nil
}
settings := filepath.Join(c.PrestaShopProjectRoot, "config", "settings.inc.php")
if data, err := os.ReadFile(settings); err == nil {
if c.PrestaShopCookieKey == "" {
c.PrestaShopCookieKey = parseDefine(string(data), "_COOKIE_KEY_")
}
if c.PrestaShopCookieIV == "" {
c.PrestaShopCookieIV = parseDefine(string(data), "_COOKIE_IV_")
}
if c.PrestaShopTablePrefix == "ps_" {
if prefix := parseDefine(string(data), "_DB_PREFIX_"); prefix != "" {
c.PrestaShopTablePrefix = prefix
}
}
}
parameters := filepath.Join(c.PrestaShopProjectRoot, "app", "config", "parameters.php")
if data, err := os.ReadFile(parameters); err == nil {
values := parsePHPParameters(string(data))
if c.PrestaShopDBDSN == "" {
c.PrestaShopDBDSN = mysqlDSN(values["database_host"], values["database_port"], values["database_name"], values["database_user"], values["database_password"])
}
if c.PrestaShopCookieKey == "" {
c.PrestaShopCookieKey = values["secret"]
}
}
if c.PrestaShopBootstrap == "" {
c.PrestaShopBootstrap = filepath.Join(c.PrestaShopProjectRoot, "config", "config.inc.php")
}
return nil
}
func parseDefine(input, key string) string {
re := regexp.MustCompile(fmt.Sprintf(`define\('%s',\s*'([^']*)'`, regexp.QuoteMeta(key)))
matches := re.FindStringSubmatch(input)
if len(matches) < 2 {
return ""
}
return matches[1]
}
func parsePHPParameters(input string) map[string]string {
out := map[string]string{}
re := regexp.MustCompile(`'([^']+)'\s*=>\s*'([^']*)'`)
for _, match := range re.FindAllStringSubmatch(input, -1) {
out[match[1]] = match[2]
}
return out
}
func mysqlDSN(host, port, db, user, pass string) string {
if host == "" || db == "" || user == "" {
return ""
}
if port == "" {
port = "3306"
}
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=UTC", user, pass, host, port, db)
}
func envOr(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if value != "" {
return value
}
}
return ""
}
func normalizedCookieDomain(input string) string {
value := strings.TrimSpace(input)
if value == "" {
return ""
}
if strings.Contains(value, "://") {
if parsed, err := url.Parse(value); err == nil {
value = parsed.Hostname()
}
}
if host, _, err := net.SplitHostPort(value); err == nil {
value = host
}
value = strings.TrimPrefix(strings.ToLower(value), ".")
value = strings.TrimPrefix(value, "www.")
return value
}
func fallbackCookieHashDomain(input string) string {
value := normalizedCookieDomain(input)
if value == "" {
return ""
}
if net.ParseIP(value) != nil || !strings.Contains(value, ".") {
return ""
}
return value
}
func dbDSNFromEnv() string {
return mysqlDSN(
firstNonEmpty(os.Getenv("PRESTASHOP_DB_HOST"), os.Getenv("DB_HOST")),
firstNonEmpty(os.Getenv("PRESTASHOP_DB_PORT"), os.Getenv("DB_PORT")),
firstNonEmpty(os.Getenv("PRESTASHOP_DB_NAME"), os.Getenv("DB_NAME")),
firstNonEmpty(os.Getenv("PRESTASHOP_DB_USER"), os.Getenv("DB_USER")),
firstNonEmpty(os.Getenv("PRESTASHOP_DB_PASS"), os.Getenv("DB_PASS")),
)
}
type RouteOwnership struct {
ProductPrefixes []string `json:"product_prefixes"`
}
func (c Config) ParseRouteOwnership() RouteOwnership {
if strings.HasPrefix(strings.TrimSpace(c.RouteOwnershipConfig), "{") {
var parsed RouteOwnership
if err := json.Unmarshal([]byte(c.RouteOwnershipConfig), &parsed); err == nil && len(parsed.ProductPrefixes) > 0 {
return parsed
}
}
return RouteOwnership{
ProductPrefixes: []string{c.RouteOwnershipConfig},
}
}