From 7264a11ba67b8a8911da5a0c0c3fbaa874e9fee4 Mon Sep 17 00:00:00 2001 From: Daniel Goc Date: Fri, 3 Apr 2026 14:58:50 +0200 Subject: [PATCH] sanitize and save URL slugs --- app/model/productDescription.go | 2 +- .../productDescriptionRepo.go | 10 +-- .../productTranslationService.go | 25 ++++++- .../sanitizeURLSlug.go | 69 +++++++++++++++++++ app/utils/const_data/consts.go | 25 +++++++ app/utils/responseErrors/responseErrors.go | 4 ++ bruno/b2b-daniel/save-product-description.yml | 39 +++++++++++ .../translate-product-description.yml | 28 ++++++++ 8 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 app/service/productTranslationService/sanitizeURLSlug.go create mode 100644 bruno/b2b-daniel/save-product-description.yml create mode 100644 bruno/b2b-daniel/translate-product-description.yml diff --git a/app/model/productDescription.go b/app/model/productDescription.go index 985b819..2080b0b 100644 --- a/app/model/productDescription.go +++ b/app/model/productDescription.go @@ -18,7 +18,7 @@ type ProductDescription struct { AvailableLater string `gorm:"column:available_later;type:varchar(255)" json:"available_later" form:"available_later"` DeliveryInStock string `gorm:"column:delivery_in_stock;type:varchar(255)" json:"delivery_in_stock" form:"delivery_in_stock"` DeliveryOutStock string `gorm:"column:delivery_out_stock;type:varchar(255)" json:"delivery_out_stock" form:"delivery_out_stock"` - Usage string `gorm:"column:_usage_;type:text" json:"usage" form:"usage"` + Usage string `gorm:"column:usage;type:text" json:"usage" form:"usage"` ImageLink string `gorm:"column:image_link" json:"image_link"` ExistsInDatabase bool `gorm:"-" json:"exists_in_database"` diff --git a/app/repos/productDescriptionRepo/productDescriptionRepo.go b/app/repos/productDescriptionRepo/productDescriptionRepo.go index ae26f6b..5083a42 100644 --- a/app/repos/productDescriptionRepo/productDescriptionRepo.go +++ b/app/repos/productDescriptionRepo/productDescriptionRepo.go @@ -52,7 +52,7 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid `+dbmodel.PsProductLangCols.AvailableLater.TabCol()+` AS available_later, `+dbmodel.PsProductLangCols.DeliveryInStock.TabCol()+` AS delivery_in_stock, `+dbmodel.PsProductLangCols.DeliveryOutStock.TabCol()+` AS delivery_out_stock, - `+dbmodel.PsProductLangCols.Usage.TabCol()+` AS _usage_, + `+dbmodel.PsProductLangCols.Usage.TabCol()+` AS `+"`usage`"+`, CONCAT(?, '/', `+dbmodel.PsImageShopCols.IDImage.TabCol()+`, '-large_default/', `+dbmodel.PsProductLangCols.LinkRewrite.TabCol()+`, '.webp') AS image_link `, config.Get().Image.ImagePrefix). Joins("JOIN " + dbmodel.TableNamePsImageShop + @@ -74,10 +74,10 @@ func (r *ProductDescriptionRepo) GetProductDescription(productID uint, productid // If it doesn't exist, returns an error. func (r *ProductDescriptionRepo) CreateIfDoesNotExist(productID uint, productid_lang uint) error { - record := model.ProductDescription{ - ProductID: productID, - ShopID: constdata.SHOP_ID, - LangID: productid_lang, + record := dbmodel.PsProductLang{ + IDProduct: int32(productID), + IDShop: int32(constdata.SHOP_ID), + IDLang: int32(productid_lang), } err := db.Get(). diff --git a/app/service/productTranslationService/productTranslationService.go b/app/service/productTranslationService/productTranslationService.go index 1b0a747..0ad8cd7 100644 --- a/app/service/productTranslationService/productTranslationService.go +++ b/app/service/productTranslationService/productTranslationService.go @@ -89,13 +89,24 @@ func (s *ProductTranslationService) GetProductDescription(userID uint, productID // Updates relevant fields with the "updates" map func (s *ProductTranslationService) SaveProductDescription(userID uint, productID uint, productLangID uint, updates map[string]string) error { // only some fields can be affected - allowedFields := []string{"description", "description_short", "meta_description", "meta_title", "name", "available_now", "available_later", "usage"} + allowedFields := []string{"description", "description_short", "link_rewrite", "meta_description", "meta_keywords", "meta_title", "name", + "available_now", "available_later", "delivery_in_stock", "delivery_out_stock", "usage"} for key := range updates { if !slices.Contains(allowedFields, key) { return responseErrors.ErrBadField } } + if text, exists := updates["link_rewrite"]; exists { + // sanitize and check that link_rewrite is a valid url slug + sanitized := SanitizeSlug(text) + if !IsValidSlug(sanitized) { + return responseErrors.ErrInvalidURLSlug + } + + updates["link_rewrite"] = sanitized + } + // check that fields description, description_short and usage, if they exist, have a valid html format mustBeHTML := []string{"description", "description_short", "usage"} for i := 0; i < len(mustBeHTML); i++ { @@ -136,20 +147,28 @@ func (s *ProductTranslationService) TranslateProductDescription(userID uint, pro fields := []*string{&productDescription.Description, &productDescription.DescriptionShort, + &productDescription.LinkRewrite, &productDescription.MetaDescription, + &productDescription.MetaKeywords, &productDescription.MetaTitle, &productDescription.Name, &productDescription.AvailableNow, &productDescription.AvailableLater, + &productDescription.DeliveryInStock, + &productDescription.DeliveryOutStock, &productDescription.Usage, } keys := []string{"translation_of_product_description", "translation_of_product_short_description", + "translation_of_product_url_link", "translation_of_product_meta_description", + "translation_of_product_meta_keywords", "translation_of_product_meta_title", "translation_of_product_name", - "translation_of_product_available_now", - "translation_of_product_available_later", + "translation_of_product_available_now_message", + "translation_of_product_available_later_message", + "translation_of_product_delivery_in_stock_message", + "translation_of_product_delivery_out_stock_message", "translation_of_product_usage", } diff --git a/app/service/productTranslationService/sanitizeURLSlug.go b/app/service/productTranslationService/sanitizeURLSlug.go new file mode 100644 index 0000000..ea69d7c --- /dev/null +++ b/app/service/productTranslationService/sanitizeURLSlug.go @@ -0,0 +1,69 @@ +package productTranslationService + +import ( + "strings" + "unicode" + + constdata "git.ma-al.com/goc_daniel/b2b/app/utils/const_data" + "github.com/dlclark/regexp2" + "golang.org/x/text/runes" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +func IsValidSlug(s string) bool { + var slug_regex2 = regexp2.MustCompile(constdata.SLUG_REGEX, regexp2.None) + + ok, _ := slug_regex2.MatchString(s) + return ok +} + +func SanitizeSlug(s string) string { + s = strings.TrimSpace(strings.ToLower(s)) + + // First apply explicit transliteration for language-specific letters. + s = transliterateWithTable(s) + + // Then normalize and strip any remaining combining marks. + s = removeDiacritics(s) + + // Replace all non-alphanumeric runs with "-" + var non_alphanum_regex2 = regexp2.MustCompile(constdata.NON_ALNUM_REGEX, regexp2.None) + s, _ = non_alphanum_regex2.Replace(s, "-", -1, -1) + + // Collapse repeated "-" and trim edges + var multi_dash_regex2 = regexp2.MustCompile(constdata.MULTI_DASH_REGEX, regexp2.None) + s, _ = multi_dash_regex2.Replace(s, "-", -1, -1) + + s = strings.Trim(s, "-") + + return s +} + +func transliterateWithTable(s string) string { + var b strings.Builder + b.Grow(len(s)) + + for _, r := range s { + if repl, ok := constdata.TRANSLITERATION_TABLE[r]; ok { + b.WriteString(repl) + } else { + b.WriteRune(r) + } + } + + return b.String() +} + +func removeDiacritics(s string) string { + t := transform.Chain( + norm.NFD, + runes.Remove(runes.In(unicode.Mn)), + norm.NFC, + ) + out, _, err := transform.String(t, s) + if err != nil { + return s + } + return out +} diff --git a/app/utils/const_data/consts.go b/app/utils/const_data/consts.go index b3790c8..cbd5657 100644 --- a/app/utils/const_data/consts.go +++ b/app/utils/const_data/consts.go @@ -12,3 +12,28 @@ const MAX_AMOUNT_OF_CARTS_PER_USER = 10 const DEFAULT_NEW_CART_NAME = "new cart" const USER_LOCALE = "user" + +// Slug sanitization +const NON_ALNUM_REGEX = `[^a-z0-9]+` +const MULTI_DASH_REGEX = `-+` +const SLUG_REGEX = `^[a-z0-9]+(?:-[a-z0-9]+)*$` + +// Currently supports only German+Polish specific cases +var TRANSLITERATION_TABLE = map[rune]string{ + // German + 'ä': "ae", + 'ö': "oe", + 'ü': "ue", + 'ß': "ss", + + // Polish + 'ą': "a", + 'ć': "c", + 'ę': "e", + 'ł': "l", + 'ń': "n", + 'ó': "o", + 'ś': "s", + 'ż': "z", + 'ź': "z", +} diff --git a/app/utils/responseErrors/responseErrors.go b/app/utils/responseErrors/responseErrors.go index c4247ea..d20c173 100644 --- a/app/utils/responseErrors/responseErrors.go +++ b/app/utils/responseErrors/responseErrors.go @@ -42,6 +42,7 @@ var ( // Typed errors for product description handler ErrBadAttribute = errors.New("bad or missing attribute value in header") ErrBadField = errors.New("this field can not be updated") + ErrInvalidURLSlug = errors.New("URL slug does not obey the industry standard") ErrInvalidXHTML = errors.New("text is not in xhtml format") ErrAIResponseFail = errors.New("AI responded with failure") ErrAIBadOutput = errors.New("AI response does not obey the format") @@ -136,6 +137,8 @@ func GetErrorCode(c fiber.Ctx, err error) string { return i18n.T_(c, "error.err_bad_attribute") case errors.Is(err, ErrBadField): return i18n.T_(c, "error.err_bad_field") + case errors.Is(err, ErrInvalidURLSlug): + return i18n.T_(c, "error.invalid_url_slug") case errors.Is(err, ErrInvalidXHTML): return i18n.T_(c, "error.err_invalid_html") case errors.Is(err, ErrAIResponseFail): @@ -195,6 +198,7 @@ func GetErrorStatus(err error) int { errors.Is(err, ErrInvalidPassword), errors.Is(err, ErrBadAttribute), errors.Is(err, ErrBadField), + errors.Is(err, ErrInvalidURLSlug), errors.Is(err, ErrInvalidXHTML), errors.Is(err, ErrBadPaging), errors.Is(err, ErrNoRootFound), diff --git a/bruno/b2b-daniel/save-product-description.yml b/bruno/b2b-daniel/save-product-description.yml new file mode 100644 index 0000000..e843995 --- /dev/null +++ b/bruno/b2b-daniel/save-product-description.yml @@ -0,0 +1,39 @@ +info: + name: save-product-description + type: http + seq: 19 + +http: + method: POST + url: http://localhost:3000/api/v1/restricted/product-translation/save-product-description?productID=1&productLangID=1 + params: + - name: productID + value: "1" + type: query + - name: productLangID + value: "1" + type: query + body: + type: json + data: |- + { + "description": "

Zastosowanie wałków rehabilitacyjnych w różnego rodzaju ćwiczeniach oraz zabiegach wpływa pozytywnie na łagodzenie urazów oraz zwiększa szanse na powrót pacjenta do pełnej sprawności fizycznej. Stosowane są w rehabilitacji ruchowej, podczas gimnastyki korekcyjnej, masaży tradycyjnych i sportowych, gdyż idealnie nadają się do unoszenia i separacji kończyn. Można je wykorzystać także do podpierania kolan, stóp, ramion, a także barków pacjenta. Wałki rehabilitacyjne polecane są także dla dzieci, wykorzystanie ich podczas zabawy znacznie wspiera rozwój dużej motoryki.

\n

Dzięki szerokiej ofercie kolorystycznej oraz zróżnicowanym rozmiarom, możliwe jest skomponowanie zestawu do ćwiczeń niezbędnego w każdym gabinecie fizjoterapeutycznym, gabinecie masażu czy też szkole i przedszkolu. 

\n

Wałek rehabilitacyjny  jest wyrobem medycznym zgodnie z wymaganiami zasadniczymi dla wyrobów medycznych i w rozumieniu ustawy o wyrobach medycznych, zgłoszonym do Rejestru Wyrobów Medycznych prowadzonego przez Urząd Rejestracji Produktów Leczniczych, Wyrobów Medycznych i Produktów Biobójczych, wyposażonym w deklarację zgodności producenta i opatrzonym znakiem CE.

\n

\n

\"Wyrób

\n

Polecane zastosowanie:

\n\n

\n

Specyfikacja materiału:

\n

Pokrowiec: materiał z powłoką PCV przeznaczony dla wyrobów medycznych, dzięki czemu jest bardzo łatwy w czyszczeniu oraz dezynfekcji:

\n\n

\"REACH\"\"Certyfikat\"Nie\"Ognioodporny\"\"Odporny\"Odporny\"Przeznaczony\"Odporny\"Olejoodporny\"

\n

Wypełnienie: średnio twarda pianka poliuretanowa o podwyższonej odporności na odkształcenia:

\n\n

\"Certyfikat\"Atest\"Atest

\n

\n

", + "description_short": "

Wałki rehabilitacyjne znajdują swoje zastosowanie w różnego rodzaju ćwiczeniach. Stosowane są w rehabilitacji ruchowej, podczas gimnastyki korekcyjnej, masaży tradycyjnych i sportowych, gdyż idealnie nadają się do unoszenia i separacji kończyn. Można je wykorzystać także do podpierania kolan, stóp, ramion, a także barków pacjenta. Wałki rehabilitacyjne polecane są także dla dzieci, wykorzystanie ich podczas zabawy, znacznie wspiera rozwój dużej motoryki. Produkt posiada certyfikację jako wyrób medyczny. 

", + "link_rewrite": " Wałek-Rehabilitacyjny-10x30-cm ", + "meta_description": "", + "meta_keywords": "", + "meta_title": "", + "name": "Wałek rehabilitacyjny 10 x 30 cm", + "available_now": "dostępny", + "available_later": "na zamówienie", + "delivery_in_stock": "Czas realizacji 3-7 dni roboczych", + "delivery_out_stock": "Czas realizacji 3-7 dni roboczych", + "usage": "

I. Czyszczenie i konserwacja

\r\n

Tapicerkę należy czyścić powierzchniowo stosując dozwolone środki:

\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
\r\n

Rodzaj zabrudzenia

\r\n
\r\n

Dozwolone środki

\r\n
\r\n

Postępowanie

\r\n
\r\n

Codzienne zabrudzenia

\r\n

 

\r\n
\r\n

Łagodny detergent najlepiej roztwór szarego mydła

\r\n
\r\n

Czyścić regularnie z użyciem gąbki lub miękkiej szczotki. Na koniec przetrzeć czyszczone miejsce wilgotną szmatką po czym wytrzeć do sucha (w celu usunięcia pozostałości detergentu).

\r\n
\r\n

Miejscowe, silniejsze zabrudzenia

\r\n
\r\n

25% roztwór alkoholu etylowego

\r\n
\r\n

Delikatnie przecierać nasączonym tamponem z gazy. Na koniec przetrzeć czyszczone miejsce wilgotną szmatką po czym wytrzeć do sucha (w celu usunięcia pozostałości detergentu).

\r\n
\r\n

Dezynfekcja

\r\n
\r\n

Ogólnodostępne środki do dezynfekcji zawierające:

\r\n

- aktywny chlor – dichloroizocyjanuran sodu, max stężenie 10000 ppm 

\r\n

- aktywny chlor - dwutlenek chloru w roztworze do 20 000 ppm 

\r\n

- alkohol izopropylowy max stężenie 70 % 

\r\n

\r\n
\r\n

Dezynfekować zgodnie z zaleceniami producenta używanego środka.

\r\n
\r\n

Przed użyciem środka innego niż łagodny detergent trzeba sprawdzić efekt w niewidocznym miejscu, a samo czyszczenie wykonać bardzo ostrożnie.

\r\n
\r\n


II. Informacje

\r\n

\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n
\"\"\r\n

Szamponować przy użyciu gąbki

\r\n
\r\n

 \"\"

\r\n
\r\n

Nie prać!!! (delikatne wyroby)   

\r\n
\r\n

\r\n
\r\n

Nie chlorować!!! (nie stosować do bielenia związków wydzielających wolny chlor)

\r\n
\r\n

 \"\"

\r\n
\r\n

Nie prasować!!! (nie dopuszczać do kontaktu z nagrzanymi powierzchniami np. kaloryfer)

\r\n
\r\n

 \"\"

\r\n
\r\n

Nie czyścić chemicznie!!!

\r\n
\r\n

\r\n

III. Warunki gwarancji

\r\n

Gwarancji nie podlegają:

\r\n" + } + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/bruno/b2b-daniel/translate-product-description.yml b/bruno/b2b-daniel/translate-product-description.yml new file mode 100644 index 0000000..c914958 --- /dev/null +++ b/bruno/b2b-daniel/translate-product-description.yml @@ -0,0 +1,28 @@ +info: + name: translate-product-description + type: http + seq: 20 + +http: + method: GET + url: http://localhost:3000/api/v1/restricted/product-translation/translate-product-description?productID=51&productFromLangID=1&productToLangID=3&model=Google + params: + - name: productID + value: "51" + type: query + - name: productFromLangID + value: "1" + type: query + - name: productToLangID + value: "3" + type: query + - name: model + value: Google + type: query + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5