Skip to content
This repository was archived by the owner on Dec 30, 2022. It is now read-only.

Commit e78cacd

Browse files
committed
feat(sanitize-results): add a module to sanitize results
1 parent a65116b commit e78cacd

File tree

2 files changed

+326
-0
lines changed

2 files changed

+326
-0
lines changed

src/__tests__/sanitize-results.js

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import sanitizeResults from '../sanitize-results';
2+
3+
test('it ensures results are provided in the correct format', () => {
4+
expect(() => {
5+
sanitizeResults();
6+
}).toThrow(new TypeError('Results should be provided as an array.'));
7+
8+
expect(() => {
9+
sanitizeResults({});
10+
}).toThrow(new TypeError('Results should be provided as an array.'));
11+
});
12+
13+
test('it ensures tags are provided in the correct format', () => {
14+
expect(() => {
15+
sanitizeResults([]);
16+
}).toThrow(
17+
new TypeError('preTag and postTag should be provided as strings.')
18+
);
19+
20+
expect(() => {
21+
sanitizeResults([], 'pre');
22+
}).toThrow(
23+
new TypeError('preTag and postTag should be provided as strings.')
24+
);
25+
26+
expect(() => {
27+
sanitizeResults([], {}, 'post');
28+
}).toThrow(
29+
new TypeError('preTag and postTag should be provided as strings.')
30+
);
31+
});
32+
33+
test('it should escape HTML of highlighted values', () => {
34+
const results = [
35+
{
36+
name: 'name',
37+
_highlightResult: {
38+
name: {
39+
value: "<script>alert('Yay')</script>",
40+
matchLevel: 'full',
41+
},
42+
},
43+
},
44+
];
45+
46+
const sanitized = sanitizeResults(results, 'pre', 'post');
47+
48+
expect(sanitized).toEqual([
49+
{
50+
name: 'name',
51+
_highlightResult: {
52+
name: {
53+
value: '&lt;script&gt;alert(&#39;Yay&#39;)&lt;/script&gt;',
54+
matchLevel: 'full',
55+
},
56+
},
57+
},
58+
]);
59+
});
60+
61+
test('it should escape HTML of snippetted values', () => {
62+
const results = [
63+
{
64+
name: 'name',
65+
_snippetResult: {
66+
name: {
67+
value: "<script>alert('Yay')</script>",
68+
matchLevel: 'full',
69+
},
70+
},
71+
},
72+
];
73+
74+
const sanitized = sanitizeResults(results, 'pre', 'post');
75+
76+
expect(sanitized).toEqual([
77+
{
78+
name: 'name',
79+
_snippetResult: {
80+
name: {
81+
value: '&lt;script&gt;alert(&#39;Yay&#39;)&lt;/script&gt;',
82+
matchLevel: 'full',
83+
},
84+
},
85+
},
86+
]);
87+
});
88+
89+
test('it should not escape HTML of non highlighted attributes', () => {
90+
const results = [
91+
{
92+
name: '<h1>Test</h1>',
93+
},
94+
];
95+
96+
const sanitized = sanitizeResults(results, 'pre', 'post');
97+
expect(sanitized).toEqual(results);
98+
});
99+
100+
test('it should replace pre-post tags in highlighted values', () => {
101+
const results = [
102+
{
103+
name: 'name',
104+
_snippetResult: {
105+
name: {
106+
value: 'My __ais-highlight__resu__/ais-highlight__lt',
107+
matchLevel: 'full',
108+
},
109+
},
110+
},
111+
];
112+
113+
const sanitized = sanitizeResults(
114+
results,
115+
'__ais-highlight__',
116+
'__/ais-highlight__'
117+
);
118+
expect(sanitized).toEqual([
119+
{
120+
name: 'name',
121+
_snippetResult: {
122+
name: {
123+
value: 'My <em>resu</em>lt',
124+
matchLevel: 'full',
125+
},
126+
},
127+
},
128+
]);
129+
});
130+
131+
test('it should replace multiple occurences of pre-post tags in highlighted values', () => {
132+
const results = [
133+
{
134+
name: 'name',
135+
_snippetResult: {
136+
name: {
137+
value:
138+
'__ais-highlight__My__/ais-highlight__ __ais-highlight__resu__/ais-highlight__lt',
139+
matchLevel: 'full',
140+
},
141+
},
142+
},
143+
];
144+
145+
const sanitized = sanitizeResults(
146+
results,
147+
'__ais-highlight__',
148+
'__/ais-highlight__'
149+
);
150+
expect(sanitized).toEqual([
151+
{
152+
name: 'name',
153+
_snippetResult: {
154+
name: {
155+
value: '<em>My</em> <em>resu</em>lt',
156+
matchLevel: 'full',
157+
},
158+
},
159+
},
160+
]);
161+
});
162+
163+
test('it should handle nested attributes', () => {
164+
const results = [
165+
{
166+
name: 'name',
167+
_snippetResult: {
168+
tags: [
169+
{
170+
value: '__ais-highlight__Ta__/ais-highlight__g 2',
171+
matchLevel: 'full',
172+
},
173+
{
174+
value: '__ais-highlight__Ta__/ais-highlight__g 2',
175+
matchLevel: 'full',
176+
},
177+
],
178+
},
179+
},
180+
];
181+
182+
const sanitized = sanitizeResults(
183+
results,
184+
'__ais-highlight__',
185+
'__/ais-highlight__'
186+
);
187+
expect(sanitized).toEqual([
188+
{
189+
name: 'name',
190+
_snippetResult: {
191+
tags: [
192+
{
193+
value: '<em>Ta</em>g 2',
194+
matchLevel: 'full',
195+
},
196+
{
197+
value: '<em>Ta</em>g 2',
198+
matchLevel: 'full',
199+
},
200+
],
201+
},
202+
},
203+
]);
204+
});
205+
206+
test('it should handle deeply nested attributes', () => {
207+
const results = [
208+
{
209+
name: 'name',
210+
_snippetResult: {
211+
info: {
212+
tags: [
213+
{
214+
value: '__ais-highlight__Ta__/ais-highlight__g 2',
215+
matchLevel: 'full',
216+
},
217+
{
218+
value: '__ais-highlight__Ta__/ais-highlight__g 2',
219+
matchLevel: 'full',
220+
},
221+
],
222+
},
223+
},
224+
},
225+
];
226+
227+
const sanitized = sanitizeResults(
228+
results,
229+
'__ais-highlight__',
230+
'__/ais-highlight__'
231+
);
232+
expect(sanitized).toEqual([
233+
{
234+
name: 'name',
235+
_snippetResult: {
236+
info: {
237+
tags: [
238+
{
239+
value: '<em>Ta</em>g 2',
240+
matchLevel: 'full',
241+
},
242+
{
243+
value: '<em>Ta</em>g 2',
244+
matchLevel: 'full',
245+
},
246+
],
247+
},
248+
},
249+
},
250+
]);
251+
});

src/sanitize-results.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import escapeHtml from 'escape-html';
2+
3+
export default function(results, preTag, postTag, tagName = 'em') {
4+
if (!Array.isArray(results)) {
5+
throw new TypeError('Results should be provided as an array.');
6+
}
7+
8+
if (typeof preTag !== 'string' || typeof postTag !== 'string') {
9+
throw new TypeError('preTag and postTag should be provided as strings.');
10+
}
11+
12+
const sanitized = [];
13+
for (const result of results) {
14+
if ('_highlightResult' in result) {
15+
result._highlightResult = sanitizeHighlights(
16+
result._highlightResult,
17+
preTag,
18+
postTag,
19+
tagName
20+
);
21+
}
22+
23+
if ('_snippetResult' in result) {
24+
result._snippetResult = sanitizeHighlights(
25+
result._snippetResult,
26+
preTag,
27+
postTag,
28+
tagName
29+
);
30+
}
31+
32+
sanitized.push(result);
33+
}
34+
35+
return sanitized;
36+
}
37+
38+
const sanitizeHighlights = function(data, preTag, postTag, tagName) {
39+
if (containsValue(data)) {
40+
const sanitized = Object.assign({}, data, {
41+
value: escapeHtml(data.value)
42+
.replace(new RegExp(preTag, 'g'), `<${tagName}>`)
43+
.replace(new RegExp(postTag, 'g'), `</${tagName}>`),
44+
});
45+
46+
return sanitized;
47+
}
48+
49+
if (Array.isArray(data)) {
50+
const child = [];
51+
data.forEach(item => {
52+
child.push(sanitizeHighlights(item, preTag, postTag, tagName));
53+
});
54+
55+
return child;
56+
}
57+
58+
if (isObject(data)) {
59+
const keys = Object.keys(data);
60+
const child = {};
61+
keys.forEach(key => {
62+
child[key] = sanitizeHighlights(data[key], preTag, postTag, tagName);
63+
});
64+
65+
return child;
66+
}
67+
68+
return data;
69+
};
70+
71+
const containsValue = function(data) {
72+
return isObject(data) && 'matchLevel' in data && 'value' in data;
73+
};
74+
75+
const isObject = value => typeof value === 'object' && value !== null;

0 commit comments

Comments
 (0)