Skip to content

Commit db592e7

Browse files
authored
fix: sanitize data to match edge delivery markup (#244)
* fix: sanitize data to match edge delivery markup * fix: sanitize data to match edge delivery markup * fix: check for string * fix: add jsdoc
1 parent 1163589 commit db592e7

5 files changed

Lines changed: 326 additions & 15 deletions

File tree

actions/pdp-renderer/lib.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,32 @@ function getImageList(primary, images) {
178178
return imageList;
179179
}
180180

181-
module.exports = { extractPathDetails, findDescription, getPrimaryImage, prepareBaseTemplate, generatePriceString, getImageList };
181+
/**
182+
* Sanitizes HTML content by removing disallowed or unbalanced tags.
183+
* Suppoorts three modes: 'all', 'inline', 'no'.
184+
* 'all': allows all block and inline tags supported by edge delivery.
185+
* 'inline': allows all inline tags supported by edge delivery.
186+
* 'no': allows no tags
187+
*
188+
* @param {string} html - HTML string to sanitize
189+
* @param {string} [mode='all'] - Sanitization mode
190+
* @returns {string} Sanitized HTML string
191+
*/
192+
function sanitize(html, mode = 'all') {
193+
const allowedInlineTags = [ 'a', 'br', 'code', 'del', 'em', 'img', 'strong', 'sub', 'sup', 'u' ];
194+
const allowedAllTags = [
195+
'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'pre',
196+
...allowedInlineTags,
197+
'table', 'tbody', 'td', 'th', 'thead', 'tr',
198+
];
199+
200+
if (mode === 'all') {
201+
return striptags(html, allowedAllTags);
202+
} else if (mode === 'inline') {
203+
return striptags(html, allowedInlineTags);
204+
} else if (mode === 'no') {
205+
return striptags(html);
206+
}
207+
}
208+
209+
module.exports = { extractPathDetails, findDescription, getPrimaryImage, prepareBaseTemplate, generatePriceString, getImageList, sanitize };

actions/pdp-renderer/render.js

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
11
const fs = require('fs');
22
const path = require('path');
33
const Handlebars = require('handlebars');
4-
const { findDescription, prepareBaseTemplate, getPrimaryImage, generatePriceString, getImageList } = require('./lib');
4+
const { findDescription, prepareBaseTemplate, getPrimaryImage, generatePriceString, getImageList, sanitize } = require('./lib');
55
const { generateLdJson } = require('./ldJson');
66
const { requestSaaS, getProductUrl } = require('../utils');
77
const { ProductQuery, ProductByUrlKeyQuery } = require('../queries');
88

99
const productTemplateCache = {};
10+
// according to https://www.aem.live/developer/markup-reference
1011

1112
function toTemplateProductData(baseProduct) {
12-
const templateProductData = { ...baseProduct };
1313
const primaryImage = getPrimaryImage(baseProduct)?.url;
1414

15-
templateProductData.hasImages = baseProduct.images?.length > 0;
16-
templateProductData.imageList = getImageList(primaryImage, baseProduct.images);
17-
templateProductData.priceString = generatePriceString(baseProduct);
18-
templateProductData.metaDescription = findDescription(baseProduct);
19-
templateProductData.metaImage = primaryImage;
20-
templateProductData.primaryImage = primaryImage;
21-
templateProductData.metaTitle = baseProduct.metaTitle || baseProduct.name || 'Product Details';
22-
23-
return templateProductData;
15+
return {
16+
name: sanitize(baseProduct.name, 'inline'),
17+
description: sanitize(baseProduct.description, 'all'),
18+
externalId: sanitize(baseProduct.externalId, 'no'),
19+
sku: sanitize(baseProduct.sku, 'no'),
20+
__typename: sanitize(baseProduct.__typename, 'no'),
21+
metaDescription: findDescription(baseProduct),
22+
metaKeyword: sanitize(baseProduct.metaKeyword, 'no'),
23+
metaTitle: sanitize(baseProduct.metaTitle, 'no') || sanitize(baseProduct.name, 'no') || 'Product Details',
24+
metaImage: primaryImage,
25+
primaryImage: primaryImage,
26+
options: baseProduct.options ? [...baseProduct.options] : null,
27+
hasImages: baseProduct.images?.length > 0,
28+
imageList: getImageList(primaryImage, baseProduct.images),
29+
priceString: generatePriceString(baseProduct),
30+
};
2431
}
2532

2633
async function generateProductHtml(sku, urlKey, context) {
@@ -55,12 +62,17 @@ async function generateProductHtml(sku, urlKey, context) {
5562
const baseUrl = getProductUrl(baseProduct, context);
5663
if (Array.isArray(option.values)) {
5764
option.values = option.values.map((value) => ({
58-
...value,
65+
title: sanitize(value.title, 'inline'),
5966
url: baseUrl.toLowerCase() + '?optionsUIDs=' + value.id,
6067
}));
6168
option.values.sort((a, b) => a.title.localeCompare(b.title));
6269
}
63-
return option;
70+
return {
71+
title: sanitize(option.title, 'inline'),
72+
id: sanitize(option.id, 'no'),
73+
required: sanitize(option.required, 'no'),
74+
values: option.values
75+
};
6476
});
6577
}
6678

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
{
2+
"data": {
3+
"products": [
4+
{
5+
"__typename": "SimpleProductView",
6+
"id": "TWpRdFRVSXdNdwBaR1ZtWVhWc2RBAFpqTTRZVEJrWlRBdE56WTBZaTAwTVdaaExXSmtNbU10TldKak1tWXpZemRpTXpsaABiV0ZwYmw5M1pXSnphWFJsWDNOMGIzSmwAWW1GelpRAFRVRkhNREExTXpZeE56STU",
7+
"sku": "25-MB03",
8+
"name": "Crown Summit Backpack",
9+
"url": "http://www.aemshop.net/crown-summit-backpack.html",
10+
"description": "<div><p>The Crown Summit Backpack is equally at home in a gym locker, study cube or a pup tent, so be sure yours is packed with books, a bag lunch, water bottles, yoga block, laptop, or whatever else you want in hand. Rugged enough for day hikes and camping trips, it has two large zippered compartments and padded, adjustable shoulder straps.</p>\r\n<ul>\r\n<li>Top handle.</li>\r\n<li>Grommet holes.</li>\r\n<li>Two-way zippers.</li>\r\n<li>H 20\" x W 14\" x D 12\".</li>\r\n<li>Weight: 2 lbs, 8 oz. Volume: 29 L.</li>\r\n<ul></div></div></p>",
11+
"shortDescription": "<div><p>The Crown Summit Backpack is equal</p></div></p>",
12+
"metaDescription": "",
13+
"metaKeyword": "backpack, hiking, camping",
14+
"metaTitle": "<h1>Crown Summit Backpack</h1>",
15+
"urlKey": "crown-summit-backpack",
16+
"inStock": true,
17+
"externalId": "7",
18+
"lastModifiedAt": "2024-10-03T15:26:48.850Z",
19+
"images": [
20+
{
21+
"url": "http://www.aemshop.net/media/catalog/product/m/b/mb03-black-0.jpg",
22+
"label": "Image",
23+
"roles": [
24+
"image",
25+
"small_image",
26+
"thumbnail"
27+
]
28+
},
29+
{
30+
"url": "http://www.aemshop.net/media/catalog/product/m/b/mb03-black-0_alt1.jpg",
31+
"label": "Image",
32+
"roles": []
33+
}
34+
],
35+
"attributes": [
36+
{
37+
"name": "activity",
38+
"label": "Activity",
39+
"value": [
40+
"Gym",
41+
"Hiking",
42+
"Overnight",
43+
"School",
44+
"Trail",
45+
"Travel",
46+
"Urban"
47+
],
48+
"roles": [
49+
"visible_in_pdp",
50+
"visible_in_compare_list",
51+
"visible_in_search"
52+
]
53+
},
54+
{
55+
"name": "color",
56+
"label": "Color",
57+
"value": "",
58+
"roles": [
59+
"visible_in_pdp",
60+
"visible_in_compare_list",
61+
"visible_in_plp",
62+
"visible_in_search"
63+
]
64+
},
65+
{
66+
"name": "cost",
67+
"label": "Cost",
68+
"value": "",
69+
"roles": [
70+
"visible_in_pdp"
71+
]
72+
},
73+
{
74+
"name": "eco_collection",
75+
"label": "Eco Collection",
76+
"value": "no",
77+
"roles": [
78+
"visible_in_pdp"
79+
]
80+
},
81+
{
82+
"name": "erin_recommends",
83+
"label": "Erin Recommends",
84+
"value": "no",
85+
"roles": [
86+
"visible_in_pdp"
87+
]
88+
},
89+
{
90+
"name": "features_bags",
91+
"label": "Features",
92+
"value": [
93+
"Audio Pocket",
94+
"Waterproof",
95+
"Lightweight",
96+
"Reflective",
97+
"Laptop Sleeve"
98+
],
99+
"roles": [
100+
"visible_in_pdp",
101+
"visible_in_search"
102+
]
103+
},
104+
{
105+
"name": "material",
106+
"label": "Material",
107+
"value": [
108+
"Nylon",
109+
"Polyester"
110+
],
111+
"roles": [
112+
"visible_in_pdp",
113+
"visible_in_search"
114+
]
115+
},
116+
{
117+
"name": "new",
118+
"label": "New",
119+
"value": "no",
120+
"roles": [
121+
"visible_in_pdp"
122+
]
123+
},
124+
{
125+
"name": "performance_fabric",
126+
"label": "Performance Fabric",
127+
"value": "no",
128+
"roles": [
129+
"visible_in_pdp"
130+
]
131+
},
132+
{
133+
"name": "sale",
134+
"label": "Sale",
135+
"value": "no",
136+
"roles": [
137+
"visible_in_pdp"
138+
]
139+
},
140+
{
141+
"name": "strap_bags",
142+
"label": "Strap/Handle",
143+
"value": [
144+
"Adjustable",
145+
"Double",
146+
"Padded"
147+
],
148+
"roles": [
149+
"visible_in_pdp",
150+
"visible_in_search"
151+
]
152+
},
153+
{
154+
"name": "style_bags",
155+
"label": "Style",
156+
"value": "Backpack",
157+
"roles": [
158+
"visible_in_pdp",
159+
"visible_in_search"
160+
]
161+
}
162+
],
163+
"price": {
164+
"roles": [
165+
"visible"
166+
],
167+
"regular": {
168+
"amount": {
169+
"currency": "USD",
170+
"value": 38
171+
}
172+
},
173+
"final": {
174+
"amount": {
175+
"currency": "USD",
176+
"value": 38
177+
}
178+
}
179+
}
180+
}
181+
]
182+
}
183+
}

test/mock-server.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,20 @@ const mockVariants = require('./mock-responses/mock-variants.json');
1919
const mockProduct = require('./mock-responses/mock-product.json');
2020
const mockProductLs = require('./mock-responses/mock-product-ls.json');
2121
const mockComplexProduct = require('./mock-responses/mock-complex-product.json');
22+
const mockProductWithBrokenMarkupJson = require('./mock-responses/mock-product-with-broken-markup.json');
2223
const mockProductTemplate = fs.readFileSync(path.resolve(__dirname, './mock-responses/product-default.html'), 'utf8');
2324

2425
const handlers = {
26+
defaultProductWithBrokenMarkup: (matcher) => graphql.query('ProductQuery', (req) => {
27+
matcher?.(req);
28+
return HttpResponse.json(mockProductWithBrokenMarkupJson);
29+
}),
30+
givenProduct: function(product) {
31+
return (matcher) => graphql.query('ProductQuery', (req) => {
32+
matcher?.(req);
33+
return HttpResponse.json(product);
34+
});
35+
},
2536
defaultProduct: (matcher) => graphql.query('ProductQuery', (req) => {
2637
matcher?.(req);
2738
return HttpResponse.json(mockProduct);

0 commit comments

Comments
 (0)