Files
Nitro-V3/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx
T
DuckieTM 7ffb213ce7 ㊙️ Security Fixes
- XSS fix: Created SanitizeHtml.ts utility using DOMPurify (already in package.json but never used). Wrapped all 21 dangerouslySetInnerHTML calls in catalog views with SanitizeHtml() — only allows safe tags (b, i, u, br, span, div, p, a, strong, em, img)

- Race condition fix: Added 10-second timeout fallbacks on purchase flags in CatalogPurchaseWidgetView and CatalogGiftView so the flag auto-resets even if the server never responds
2026-03-23 22:14:03 +01:00

114 lines
5.2 KiB
TypeScript

import { GetOfficialSongIdMessageComposer, GetSoundManager, MusicPriorities, OfficialSongIdMessageEvent } from '@nitrots/nitro-renderer';
import { FC, useEffect, useState } from 'react';
import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml, SendMessageComposer } from '../../../../../api';
import { Button, Column, Grid, LayoutImage, Text } from '../../../../../common';
import { useCatalog, useMessageEvent } from '../../../../../hooks';
import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView';
import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView';
import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView';
import { CatalogLimitedItemWidgetView } from '../widgets/CatalogLimitedItemWidgetView';
import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView';
import { CatalogSpinnerWidgetView } from '../widgets/CatalogSpinnerWidgetView';
import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget';
import { CatalogViewProductWidgetView } from '../widgets/CatalogViewProductWidgetView';
import { CatalogLayoutProps } from './CatalogLayout.types';
export const CatalogLayoutSoundMachineView: FC<CatalogLayoutProps> = props =>
{
const { page = null } = props;
const [ songId, setSongId ] = useState(-1);
const [ officialSongId, setOfficialSongId ] = useState('');
const { currentOffer = null, currentPage = null } = useCatalog();
const previewSong = (previewSongId: number) => GetSoundManager().musicController?.playSong(previewSongId, MusicPriorities.PRIORITY_PURCHASE_PREVIEW, 15, 0, 0, 0);
useMessageEvent<OfficialSongIdMessageEvent>(OfficialSongIdMessageEvent, event =>
{
const parser = event.getParser();
if(parser.officialSongId !== officialSongId) return;
setSongId(parser.songId);
});
useEffect(() =>
{
if(!currentOffer) return;
const product = currentOffer.product;
if(!product) return;
if(product.extraParam.length > 0)
{
const id = parseInt(product.extraParam);
if(id > 0)
{
setSongId(id);
}
else
{
setOfficialSongId(product.extraParam);
SendMessageComposer(new GetOfficialSongIdMessageComposer(product.extraParam));
}
}
else
{
setOfficialSongId('');
setSongId(-1);
}
return () => GetSoundManager().musicController?.stop(MusicPriorities.PRIORITY_PURCHASE_PREVIEW);
}, [ currentOffer ]);
useEffect(() =>
{
return () => GetSoundManager().musicController?.stop(MusicPriorities.PRIORITY_PURCHASE_PREVIEW);
}, []);
return (
<>
<Grid>
<Column overflow="hidden" size={ 7 }>
{ GetConfigurationValue('catalog.headers') &&
<CatalogHeaderView imageUrl={ currentPage.localization.getImage(0) } /> }
<CatalogItemGridWidgetView />
</Column>
<Column center={ !currentOffer } overflow="hidden" size={ 5 }>
{ !currentOffer &&
<>
{ !!page.localization.getImage(1) &&
<LayoutImage imageUrl={ page.localization.getImage(1) } /> }
<Text center dangerouslySetInnerHTML={ { __html: SanitizeHtml(page.localization.getText(0)) } } />
</> }
{ currentOffer &&
<>
<div className="flex items-center justify-center overflow-hidden" style={ { height: 140 } }>
{ (currentOffer.product.productType !== ProductTypeEnum.BADGE) &&
<>
<CatalogViewProductWidgetView />
<CatalogAddOnBadgeWidgetView className="bg-muted rounded bottom-1 inset-e-1" />
</> }
{ (currentOffer.product.productType === ProductTypeEnum.BADGE) && <CatalogAddOnBadgeWidgetView className="scale-2" /> }
</div>
<Column grow gap={ 1 }>
<CatalogLimitedItemWidgetView />
<Text grow truncate>{ currentOffer.localizationName }</Text>
{ songId > -1 && <Button onClick={ () => previewSong(songId) }>{ LocalizeText('play_preview_button') }</Button>
}
<div className="flex justify-between">
<div className="flex flex-col gap-1">
<CatalogSpinnerWidgetView />
</div>
<CatalogTotalPriceWidget alignItems="end" justifyContent="end" />
</div>
<CatalogPurchaseWidgetView />
</Column>
</> }
</Column>
</Grid>
</>
);
};