fix(kiosk): escape data-derived strings in innerHTML (RG-T15) (#20)
This commit is contained in:
parent
ee14186a19
commit
9ddb4ccb27
4 changed files with 41 additions and 21 deletions
|
|
@ -17,7 +17,7 @@
|
||||||
* requires prices shown to end-consumers to include all taxes.
|
* requires prices shown to end-consumers to include all taxes.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getCart, removeFromCart, updateQuantity, getTotalCents, computeMenuLineCents, clearCart, formatPrice } from './state.js';
|
import { getCart, removeFromCart, updateQuantity, getTotalCents, computeMenuLineCents, clearCart, formatPrice, escHtml } from './state.js';
|
||||||
import { refreshCartBadge } from './nav.js';
|
import { refreshCartBadge } from './nav.js';
|
||||||
|
|
||||||
/* TVA rate used for display breakdown only — stored prices are already TTC */
|
/* TVA rate used for display breakdown only — stored prices are already TTC */
|
||||||
|
|
@ -62,27 +62,27 @@ function renderCart() {
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<img
|
<img
|
||||||
class="cart-line__image"
|
class="cart-line__image"
|
||||||
src="${item.image}"
|
src="${escHtml(item.image)}"
|
||||||
alt="${item.libelle}"
|
alt="${escHtml(item.libelle)}"
|
||||||
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
|
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
|
||||||
>
|
>
|
||||||
<div class="cart-line__info">
|
<div class="cart-line__info">
|
||||||
<span class="cart-line__name">${item.libelle}</span>
|
<span class="cart-line__name">${escHtml(item.libelle)}</span>
|
||||||
<span class="cart-line__unit-price">${formatPrice(item.prix_cents)} / unite${isMenu && (item.supplement_cents ?? 0) > 0 ? ` + ${formatPrice(item.supplement_cents)} suppl.` : ''}</span>
|
<span class="cart-line__unit-price">${formatPrice(item.prix_cents)} / unite${isMenu && (item.supplement_cents ?? 0) > 0 ? ` + ${formatPrice(item.supplement_cents)} suppl.` : ''}</span>
|
||||||
${isMenu && item.composition ? renderCompositionBlock(item) : ''}
|
${isMenu && item.composition ? renderCompositionBlock(item) : ''}
|
||||||
</div>
|
</div>
|
||||||
<div class="cart-line__qty" role="group" aria-label="Quantite de ${item.libelle}">
|
<div class="cart-line__qty" role="group" aria-label="Quantite de ${escHtml(item.libelle)}">
|
||||||
<button
|
<button
|
||||||
class="qty-btn qty-btn--minus"
|
class="qty-btn qty-btn--minus"
|
||||||
data-index="${index}"
|
data-index="${index}"
|
||||||
aria-label="Diminuer la quantite de ${item.libelle}"
|
aria-label="Diminuer la quantite de ${escHtml(item.libelle)}"
|
||||||
type="button"
|
type="button"
|
||||||
>-</button>
|
>-</button>
|
||||||
<span class="qty-value" aria-live="polite">${item.quantite}</span>
|
<span class="qty-value" aria-live="polite">${item.quantite}</span>
|
||||||
<button
|
<button
|
||||||
class="qty-btn qty-btn--plus"
|
class="qty-btn qty-btn--plus"
|
||||||
data-index="${index}"
|
data-index="${index}"
|
||||||
aria-label="Augmenter la quantite de ${item.libelle}"
|
aria-label="Augmenter la quantite de ${escHtml(item.libelle)}"
|
||||||
type="button"
|
type="button"
|
||||||
>+</button>
|
>+</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -90,7 +90,7 @@ function renderCart() {
|
||||||
<button
|
<button
|
||||||
class="cart-line__remove"
|
class="cart-line__remove"
|
||||||
data-index="${index}"
|
data-index="${index}"
|
||||||
aria-label="Supprimer ${item.libelle} du panier"
|
aria-label="Supprimer ${escHtml(item.libelle)} du panier"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<img src="assets/images/ui/trash.png" alt="" aria-hidden="true" width="24" height="24">
|
<img src="assets/images/ui/trash.png" alt="" aria-hidden="true" width="24" height="24">
|
||||||
|
|
@ -161,10 +161,10 @@ function renderCompositionBlock(item) {
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<ul class="cart-line__composition" aria-label="Composition du menu">
|
<ul class="cart-line__composition" aria-label="Composition du menu">
|
||||||
<li class="cart-line__comp-item">+ ${c.burger.libelle}${burgerOpts}</li>
|
<li class="cart-line__comp-item">+ ${escHtml(c.burger.libelle)}${burgerOpts}</li>
|
||||||
<li class="cart-line__comp-item">+ ${c.accompagnement.libelle}${accompTailleLabel}</li>
|
<li class="cart-line__comp-item">+ ${escHtml(c.accompagnement.libelle)}${accompTailleLabel}</li>
|
||||||
<li class="cart-line__comp-item">+ ${c.boisson.libelle}${boissonTailleLabel}</li>
|
<li class="cart-line__comp-item">+ ${escHtml(c.boisson.libelle)}${boissonTailleLabel}</li>
|
||||||
<li class="cart-line__comp-item">+ ${c.sauce.libelle}</li>
|
<li class="cart-line__comp-item">+ ${escHtml(c.sauce.libelle)}</li>
|
||||||
${supplTotal > 0 ? `<li class="cart-line__comp-suppl">Supplement ${nbGrandes} grande(s) : +${formatPrice(supplTotal)}</li>` : ''}
|
${supplTotal > 0 ? `<li class="cart-line__comp-suppl">Supplement ${nbGrandes} grande(s) : +${formatPrice(supplTotal)}</li>` : ''}
|
||||||
</ul>
|
</ul>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { findProduct } from './data.js';
|
import { findProduct } from './data.js';
|
||||||
import { addToCart, formatPrice } from './state.js';
|
import { addToCart, formatPrice, escHtml } from './state.js';
|
||||||
import { refreshCartBadge } from './nav.js';
|
import { refreshCartBadge } from './nav.js';
|
||||||
import { openMenuComposer } from './page-product-menu.js';
|
import { openMenuComposer } from './page-product-menu.js';
|
||||||
|
|
||||||
|
|
@ -59,18 +59,18 @@ async function renderProduct() {
|
||||||
<div class="product-detail__image-wrap">
|
<div class="product-detail__image-wrap">
|
||||||
<img
|
<img
|
||||||
class="product-detail__image"
|
class="product-detail__image"
|
||||||
src="${product.image}"
|
src="${escHtml(product.image)}"
|
||||||
alt="${product.nom}"
|
alt="${escHtml(product.nom)}"
|
||||||
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
|
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-detail__info">
|
<div class="product-detail__info">
|
||||||
<h1 class="product-detail__name">${product.nom}</h1>
|
<h1 class="product-detail__name">${escHtml(product.nom)}</h1>
|
||||||
<p class="product-detail__price">${formatPrice(product.prix)}</p>
|
<p class="product-detail__price">${formatPrice(product.prix)}</p>
|
||||||
<button
|
<button
|
||||||
class="btn btn--primary btn--large product-detail__add"
|
class="btn btn--primary btn--large product-detail__add"
|
||||||
id="add-to-cart-btn"
|
id="add-to-cart-btn"
|
||||||
aria-label="Ajouter ${product.nom} au panier"
|
aria-label="Ajouter ${escHtml(product.nom)} au panier"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Ajouter au panier
|
Ajouter au panier
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG } from './data.js';
|
import { getProductsByCategory, getCategoryById, CATEGORY_ID_TO_SLUG } from './data.js';
|
||||||
import { formatPrice } from './state.js';
|
import { formatPrice, escHtml } from './state.js';
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const categoryId = parseInt(params.get('category'), 10) || 1;
|
const categoryId = parseInt(params.get('category'), 10) || 1;
|
||||||
|
|
@ -60,14 +60,14 @@ async function renderProducts() {
|
||||||
<div class="product-card__image-wrap">
|
<div class="product-card__image-wrap">
|
||||||
<img
|
<img
|
||||||
class="product-card__image"
|
class="product-card__image"
|
||||||
src="${product.image}"
|
src="${escHtml(product.image)}"
|
||||||
alt="${product.nom}"
|
alt="${escHtml(product.nom)}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
|
onerror="this.src='assets/images/ui/logo.png'; this.alt='Image non disponible';"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-card__body">
|
<div class="product-card__body">
|
||||||
<span class="product-card__name">${product.nom}</span>
|
<span class="product-card__name">${escHtml(product.nom)}</span>
|
||||||
<span class="product-card__price">${formatPrice(product.prix)}</span>
|
<span class="product-card__price">${formatPrice(product.prix)}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
||||||
|
|
@ -172,3 +172,23 @@ export function formatPrice(cents) {
|
||||||
export function getCartCount() {
|
export function getCartCount() {
|
||||||
return getCart().reduce((sum, item) => sum + item.quantite, 0);
|
return getCart().reduce((sum, item) => sum + item.quantite, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- HTML escaping ------------------------------------------------------- */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal HTML escaping for data-derived strings (product names, image paths,
|
||||||
|
* libelles) injected into innerHTML. RG-T15 (anti-XSS) requires every catalogue
|
||||||
|
* value rendered as HTML to be escaped. Centralised here so all page modules
|
||||||
|
* that build markup from data escape identically; mirrors the helper that was
|
||||||
|
* local to page-product-menu.js.
|
||||||
|
* @param {*} str
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function escHtml(str) {
|
||||||
|
return String(str)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue