diff --git a/src/app/components/product-card/product-card.component.html b/src/app/components/product-card/product-card.component.html index ad90796..38c121b 100644 --- a/src/app/components/product-card/product-card.component.html +++ b/src/app/components/product-card/product-card.component.html @@ -39,7 +39,7 @@

{{ title || item.name }}

@if (showDescription && description) { -

{{ description }}

+

{{ cleanDescription(description) }}

} @if (showRating) { diff --git a/src/app/components/product-card/product-card.component.ts b/src/app/components/product-card/product-card.component.ts index 3b2a9bb..b8311d5 100644 --- a/src/app/components/product-card/product-card.component.ts +++ b/src/app/components/product-card/product-card.component.ts @@ -4,7 +4,7 @@ import { RouterLink } from '@angular/router'; import { Product } from '../../core/products/models/product-domain.model'; import { LangRoutePipe } from '../../pipes/lang-route.pipe'; import { TranslatePipe } from '../../i18n/translate.pipe'; -import { getBadgeClass, getDiscountedPrice, getMainImage } from '../../utils/item.utils'; +import { cleanDescription, getBadgeClass, getDiscountedPrice, getMainImage } from '../../utils/item.utils'; const STOCK_LABEL_KEYS: Record = { high: 'catalog.stockHigh', @@ -55,6 +55,7 @@ export class ProductCardComponent { readonly getMainImage = getMainImage; readonly getDiscountedPrice = getDiscountedPrice; readonly getBadgeClass = getBadgeClass; + readonly cleanDescription = cleanDescription; onAddToCart(event: Event): void { this.addToCart.emit({ itemID: this.item.itemID, event }); diff --git a/src/app/features/website/product/components/product-description/product-description.component.html b/src/app/features/website/product/components/product-description/product-description.component.html index cedea4c..e61d5ac 100644 --- a/src/app/features/website/product/components/product-description/product-description.component.html +++ b/src/app/features/website/product/components/product-description/product-description.component.html @@ -2,8 +2,8 @@

{{ 'itemDetail.description' | translate }}

@if (simpleDescription) { -

{{ simpleDescription }}

+

{{ cleanDescription(simpleDescription) }}

} @else if (product.description) { -

{{ product.description }}

+

{{ cleanDescription(product.description) }}

} diff --git a/src/app/features/website/product/components/product-description/product-description.component.ts b/src/app/features/website/product/components/product-description/product-description.component.ts index 968ecbc..ffda596 100644 --- a/src/app/features/website/product/components/product-description/product-description.component.ts +++ b/src/app/features/website/product/components/product-description/product-description.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { Product } from '../../../../../core/products/models/product-domain.model'; import { TranslatePipe } from '../../../../../i18n/translate.pipe'; +import { cleanDescription } from '../../../../../utils/item.utils'; @Component({ selector: 'app-product-description', @@ -13,4 +14,6 @@ import { TranslatePipe } from '../../../../../i18n/translate.pipe'; export class ProductDescriptionComponent { @Input({ required: true }) product!: Product; @Input() simpleDescription = ''; + + readonly cleanDescription = cleanDescription; } diff --git a/src/app/utils/item.utils.ts b/src/app/utils/item.utils.ts index 4e9e09b..aaee42c 100644 --- a/src/app/utils/item.utils.ts +++ b/src/app/utils/item.utils.ts @@ -45,6 +45,30 @@ export function getBadgeClass(badge: string): string { return map[badge.toLowerCase()] || 'badge-custom'; } +const HTML_ENTITY_MAP: Record = { + '<': '<', + '>': '>', + '"': '"', + ''': "'", + ''': "'", + ' ': ' ', + '&': '&', +}; + +/** + * Cleans up seller-supplied description text for safe plain-text display: decodes + * HTML entities (some catalog listings arrive HTML-escaped, e.g. "<attention>") + * and strips any resulting tag-like markup. Never renders as HTML — output stays a + * plain string for text interpolation, so this cannot introduce an XSS surface. + */ +export function cleanDescription(text: string | undefined | null): string { + if (!text) { + return ''; + } + const decoded = text.replace(/<|>|"|'|'| |&/g, match => HTML_ENTITY_MAP[match]); + return decoded.replace(/<\/?[a-zA-Z][^>]*>/g, '').trim(); +} + /** * Get the translated name/description for the current language. * Checks translations map first, then names[]/descriptions[] arrays,