Files
ps_shop/internal/prestashop/session/service.go
T
2026-05-12 05:08:39 +02:00

374 lines
9.1 KiB
Go

package session
import (
"context"
"fmt"
"hash/crc32"
"net"
"net/http"
"strconv"
"strings"
"time"
pscookie "git.ma-al.com/goc_marek/ps_shop/internal/prestashop/cookie"
"gorm.io/gorm"
)
type Service struct {
db *gorm.DB
prefix string
}
type defaults struct {
LanguageID int64
CurrencyID int64
ShopID int64
ShopGroupID int64
CountryISO string
}
func NewService(db *gorm.DB, prefix string) *Service {
return &Service{db: db, prefix: prefix}
}
func (s *Service) NewAnonymous(ctx context.Context, req *http.Request, cookieName string) (*pscookie.SessionContext, error) {
if s == nil || s.db == nil {
return nil, fmt.Errorf("prestashop session service is not initialized")
}
def, err := s.loadDefaults(ctx)
if err != nil {
return nil, err
}
guestID, err := s.insertGuest(ctx)
if err != nil {
return nil, err
}
connectionID, err := s.insertConnection(ctx, def, guestID, req)
if err != nil {
return nil, err
}
now := time.Now().UTC()
values := map[string]string{
"checksum": anonymousChecksum(guestID, connectionID, def.LanguageID, def.CurrencyID, def.ShopID),
"date_add": now.Format("2006-01-02 15:04:05"),
"id_cart": "",
"id_connections": strconv.FormatInt(connectionID, 10),
"id_currency": strconv.FormatInt(def.CurrencyID, 10),
"id_guest": strconv.FormatInt(guestID, 10),
"id_lang": strconv.FormatInt(def.LanguageID, 10),
"id_language": strconv.FormatInt(def.LanguageID, 10),
"iso_code_country": def.CountryISO,
}
orderedKeys := []string{
"date_add",
"id_lang",
"id_cart",
"id_language",
"iso_code_country",
"id_currency",
"id_guest",
"id_connections",
"checksum",
}
if def.ShopID > 0 {
values["id_shop"] = strconv.FormatInt(def.ShopID, 10)
orderedKeys = append(orderedKeys[:6], append([]string{"id_shop"}, orderedKeys[6:]...)...)
}
return &pscookie.SessionContext{
CookieName: cookieName,
LanguageID: int64Ptr(def.LanguageID),
CurrencyID: int64Ptr(def.CurrencyID),
ShopID: int64Ptr(def.ShopID),
GuestID: int64Ptr(guestID),
IsLoggedIn: false,
Values: values,
OrderedKeys: orderedKeys,
ParseStatus: pscookie.ParseStatusAnonymous,
}, nil
}
func (s *Service) loadDefaults(ctx context.Context) (*defaults, error) {
def := &defaults{
LanguageID: 1,
CurrencyID: 1,
ShopID: 1,
ShopGroupID: 1,
CountryISO: "US",
}
configTable := s.prefix + "configuration"
shopTable := s.prefix + "shop"
countryTable := s.prefix + "country"
var configs []struct {
Name string
Value string
}
configQuery := fmt.Sprintf("SELECT name, value FROM %s WHERE name IN ('PS_LANG_DEFAULT', 'PS_CURRENCY_DEFAULT', 'PS_COUNTRY_DEFAULT')", configTable)
if err := s.db.WithContext(ctx).Raw(configQuery).Scan(&configs).Error; err != nil {
return nil, err
}
countryID := int64(0)
for _, cfg := range configs {
switch cfg.Name {
case "PS_LANG_DEFAULT":
if parsed, err := strconv.ParseInt(cfg.Value, 10, 64); err == nil && parsed > 0 {
def.LanguageID = parsed
}
case "PS_CURRENCY_DEFAULT":
if parsed, err := strconv.ParseInt(cfg.Value, 10, 64); err == nil && parsed > 0 {
def.CurrencyID = parsed
}
case "PS_COUNTRY_DEFAULT":
if parsed, err := strconv.ParseInt(cfg.Value, 10, 64); err == nil && parsed > 0 {
countryID = parsed
}
}
}
var shop struct {
ID int64 `gorm:"column:id_shop"`
GroupID int64 `gorm:"column:id_shop_group"`
}
shopQuery := fmt.Sprintf("SELECT id_shop, id_shop_group FROM %s ORDER BY id_shop LIMIT 1", shopTable)
if err := s.db.WithContext(ctx).Raw(shopQuery).Scan(&shop).Error; err != nil {
return nil, err
}
if shop.ID > 0 {
def.ShopID = shop.ID
}
if shop.GroupID > 0 {
def.ShopGroupID = shop.GroupID
}
if countryID > 0 {
var country struct {
ISOCode string `gorm:"column:iso_code"`
}
countryQuery := fmt.Sprintf("SELECT iso_code FROM %s WHERE id_country = ? LIMIT 1", countryTable)
if err := s.db.WithContext(ctx).Raw(countryQuery, countryID).Scan(&country).Error; err != nil {
return nil, err
}
if country.ISOCode != "" {
def.CountryISO = country.ISOCode
}
}
return def, nil
}
func (s *Service) insertGuest(ctx context.Context) (int64, error) {
sqlDB, err := s.db.DB()
if err != nil {
return 0, fmt.Errorf("resolve sql db for guest insert: %w", err)
}
tableName := s.prefix + "guest"
columns, values, err := s.guestInsert(ctx)
if err != nil {
return 0, err
}
query := insertQuery(tableName, columns)
result, err := sqlDB.ExecContext(ctx, query, values...)
if err != nil {
return 0, fmt.Errorf("insert guest: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("guest last insert id: %w", err)
}
return id, nil
}
func (s *Service) insertConnection(ctx context.Context, def *defaults, guestID int64, req *http.Request) (int64, error) {
sqlDB, err := s.db.DB()
if err != nil {
return 0, fmt.Errorf("resolve sql db for connection insert: %w", err)
}
tableName := s.prefix + "connections"
columns, values, err := s.connectionInsert(ctx, def, guestID, req)
if err != nil {
return 0, err
}
query := insertQuery(tableName, columns)
result, err := sqlDB.ExecContext(ctx, query, values...)
if err != nil {
return 0, fmt.Errorf("insert connection: %w", err)
}
id, err := result.LastInsertId()
if err != nil {
return 0, fmt.Errorf("connection last insert id: %w", err)
}
return id, nil
}
func (s *Service) guestInsert(ctx context.Context) ([]string, []any, error) {
available, err := s.tableColumns(ctx, s.prefix+"guest")
if err != nil {
return nil, nil, fmt.Errorf("load guest columns: %w", err)
}
columns := make([]string, 0)
values := make([]any, 0)
addColumn := func(name string, value any) {
if available[name] {
columns = append(columns, name)
values = append(values, value)
}
}
addColumn("id_customer", 0)
addColumn("id_operating_system", 0)
addColumn("id_web_browser", 0)
addColumn("javascript", 0)
addColumn("screen_resolution_x", 0)
addColumn("screen_resolution_y", 0)
addColumn("screen_color", 0)
addColumn("sun_java", 0)
addColumn("adobe_flash", 0)
addColumn("adobe_director", 0)
addColumn("apple_quicktime", 0)
addColumn("real_player", 0)
addColumn("windows_media", 0)
addColumn("accept_language", "")
addColumn("mobile_theme", 0)
return columns, values, nil
}
func (s *Service) connectionInsert(ctx context.Context, def *defaults, guestID int64, req *http.Request) ([]string, []any, error) {
available, err := s.tableColumns(ctx, s.prefix+"connections")
if err != nil {
return nil, nil, fmt.Errorf("load connections columns: %w", err)
}
now := time.Now().UTC().Format("2006-01-02 15:04:05")
columns := make([]string, 0)
values := make([]any, 0)
addColumn := func(name string, value any) {
if available[name] {
columns = append(columns, name)
values = append(values, value)
}
}
addColumn("id_guest", guestID)
addColumn("id_shop", def.ShopID)
addColumn("id_shop_group", def.ShopGroupID)
addColumn("id_page", 0)
addColumn("ip_address", ipAsUint32(req))
addColumn("date_add", now)
addColumn("date_upd", now)
addColumn("http_referer", referer(req))
addColumn("request_uri", requestURI(req))
return columns, values, nil
}
func (s *Service) tableColumns(ctx context.Context, tableName string) (map[string]bool, error) {
type columnRow struct {
ColumnName string `gorm:"column:COLUMN_NAME"`
}
var rows []columnRow
query := `
SELECT COLUMN_NAME
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND table_name = ?
`
if err := s.db.WithContext(ctx).Raw(query, tableName).Scan(&rows).Error; err != nil {
return nil, err
}
columns := make(map[string]bool, len(rows))
for _, row := range rows {
columns[row.ColumnName] = true
}
return columns, nil
}
func insertQuery(tableName string, columns []string) string {
if len(columns) == 0 {
return fmt.Sprintf("INSERT INTO %s () VALUES ()", tableName)
}
return fmt.Sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
tableName,
strings.Join(columns, ", "),
placeholders(len(columns)),
)
}
func placeholders(n int) string {
parts := make([]string, n)
for i := range parts {
parts[i] = "?"
}
return strings.Join(parts, ", ")
}
func referer(req *http.Request) string {
if req == nil {
return ""
}
return req.Referer()
}
func requestURI(req *http.Request) string {
if req == nil || req.URL == nil {
return ""
}
return req.URL.RequestURI()
}
func ipAsUint32(req *http.Request) uint32 {
if req == nil {
return 0
}
raw := req.Header.Get("X-Forwarded-For")
if raw == "" {
raw = req.RemoteAddr
}
if strings.Contains(raw, ",") {
raw = strings.TrimSpace(strings.Split(raw, ",")[0])
}
host := raw
if parsedHost, _, err := net.SplitHostPort(raw); err == nil {
host = parsedHost
}
ip := net.ParseIP(host)
if ip == nil {
return 0
}
ip = ip.To4()
if ip == nil {
return 0
}
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
}
func anonymousChecksum(values ...int64) string {
buf := make([]byte, 0, len(values)*8)
for _, v := range values {
buf = strconv.AppendInt(buf, v, 10)
buf = append(buf, '|')
}
return strconv.FormatUint(uint64(crc32.ChecksumIEEE(buf)), 10)
}
func int64Ptr(value int64) *int64 {
if value == 0 {
return nil
}
v := value
return &v
}