Skip to content

Commit 4793068

Browse files
authored
fix: replace leftover innerHTML assignment with CSP safe approach (#912)
1 parent 2111453 commit 4793068

File tree

8 files changed

+52
-41
lines changed

8 files changed

+52
-41
lines changed

examples/example4-model-html-formatters.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<title>SlickGrid example 4: Model</title>
77
<link rel="stylesheet" href="../dist/styles/css/slick.customtooltip.css" type="text/css" />
88
<link rel="stylesheet" href="../dist/styles/css/slick.columnpicker.css" type="text/css"/>
9+
<link rel="stylesheet" href="../dist/styles/css/slick.gridmenu.css" type="text/css"/>
910
<link rel="stylesheet" href="../dist/styles/css/slick.pager.css" type="text/css"/>
1011
<link rel="stylesheet" href="../dist/styles/css/slick-icons.css" type="text/css"/>
1112
<link rel="stylesheet" href="../dist/styles/css/example-demo.css" type="text/css"/>
@@ -131,7 +132,6 @@ <h2>View Source:</h2>
131132

132133
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
133134
<script src="https://cdn.jsdelivr.net/npm/sortablejs/Sortable.min.js"></script>
134-
<script src="sortable-cdn-fallback.js"></script>
135135

136136
<script type="module">
137137
import {
@@ -142,6 +142,7 @@ <h2>View Source:</h2>
142142
SlickColumnPicker,
143143
SlickDataView,
144144
SlickCustomTooltip,
145+
SlickGridMenu,
145146
SlickGridPager,
146147
SlickGrid,
147148
Utils,
@@ -174,6 +175,9 @@ <h2>View Source:</h2>
174175
forceFitTitle: "Force fit columns",
175176
syncResizeTitle: "Synchronous resize",
176177
},
178+
gridMenu: {
179+
iconCssClass: "sgi sgi-menu sgi-17px", // you can provide iconImage OR iconCssClass
180+
},
177181
editable: true,
178182
enableAddRow: true,
179183
enableCellNavigation: true,
@@ -312,12 +316,13 @@ <h2>View Source:</h2>
312316
d["effortDriven"] = (i % 5 == 0);
313317
}
314318

315-
dataView = new SlickDataView({ inlineFilters: true });
319+
dataView = new SlickDataView({ inlineFilters: true, useCSPSafeFilter: true });
316320
grid = new SlickGrid("#myGrid", dataView, columns, options);
317321
grid.setSelectionModel(new SlickRowSelectionModel());
318322

319323
let pager = new SlickGridPager(dataView, grid, "#pager");
320324
let columnpicker = new SlickColumnPicker(columns, grid, options);
325+
let gridMenu = new SlickGridMenu(columns, grid, options);
321326

322327
const customTooltipPlugin = new SlickCustomTooltip();
323328
grid.registerPlugin(customTooltipPlugin);

examples/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ <h2>Other Features</h2>
128128
<li><a href="./example-column-hidden.html">Grid with Hidden Columns</a></li>
129129
<li><a href="./example-frozen-columns-and-column-group-hidden-col.html">Frozen Grid with Hidden Columns</a></li>
130130
<li><a href="./example-csp-header.html">CSP Header (Content Security Policy)</a></li>
131-
<li><a href="./example4-model-html-formatters.html">Filtered DataView with HTML Formatter - CSP Header (Content Security Policy)</a></li>
131+
<li><a href="./example4-model-html-formatters.html">Custom Formatter using native HTML</a></li>
132132
</ul>
133133
</div>
134134

src/controls/slick.columnmenu.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export class SlickColumnMenu {
157157

158158
const labelElm = document.createElement('label');
159159
labelElm.htmlFor = `${this._gridUid}colpicker-${columnId}`;
160-
labelElm.innerHTML = this.grid.sanitizeHtmlString(columnLabel instanceof HTMLElement ? columnLabel.innerHTML : columnLabel);
160+
this.grid.applyHtmlCode(labelElm, columnLabel);
161161
liElm.appendChild(labelElm);
162162
this._listElm.appendChild(liElm);
163163
}
@@ -249,9 +249,7 @@ export class SlickColumnMenu {
249249

250250
/** Update the Titles of each sections (command, customTitle, ...) */
251251
updateAllTitles(pickerOptions: { columnTitle: string; }) {
252-
if (this._columnTitleElm?.innerHTML) {
253-
this._columnTitleElm.innerHTML = this.grid.sanitizeHtmlString(pickerOptions.columnTitle);
254-
}
252+
this.grid.applyHtmlCode(this._columnTitleElm, pickerOptions.columnTitle);
255253
}
256254

257255
updateColumn(e: DOMMouseOrTouchEvent<HTMLInputElement>) {

src/controls/slick.columnpicker.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export class SlickColumnPicker {
158158

159159
const labelElm = document.createElement('label');
160160
labelElm.htmlFor = `${this._gridUid}colpicker-${columnId}`;
161-
labelElm.innerHTML = this.grid.sanitizeHtmlString(columnLabel instanceof HTMLElement ? columnLabel.innerHTML : columnLabel);
161+
this.grid.applyHtmlCode(labelElm, columnLabel);
162162
liElm.appendChild(labelElm);
163163
this._listElm.appendChild(liElm);
164164
}
@@ -250,9 +250,7 @@ export class SlickColumnPicker {
250250

251251
/** Update the Titles of each sections (command, customTitle, ...) */
252252
updateAllTitles(pickerOptions: { columnTitle: string; }) {
253-
if (this._columnTitleElm?.innerHTML) {
254-
this._columnTitleElm.innerHTML = this.grid.sanitizeHtmlString(pickerOptions.columnTitle);
255-
}
253+
this.grid.applyHtmlCode(this._columnTitleElm, pickerOptions.columnTitle);
256254
}
257255

258256
protected updateColumn(e: DOMMouseOrTouchEvent<HTMLInputElement>) {

src/controls/slick.gridmenu.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ export class SlickGridMenu {
393393
if (!isSubMenu && (this._gridMenuOptions?.commandTitle || this._gridMenuOptions?.customTitle)) {
394394
this._commandTitleElm = document.createElement('div');
395395
this._commandTitleElm.className = 'title';
396-
this._commandTitleElm.innerHTML = this.grid.sanitizeHtmlString((this._gridMenuOptions.commandTitle || this._gridMenuOptions.customTitle) as string);
396+
this.grid.applyHtmlCode(this._commandTitleElm, this.grid.sanitizeHtmlString((this._gridMenuOptions.commandTitle || this._gridMenuOptions.customTitle) as string));
397397
commandListElm.appendChild(this._commandTitleElm);
398398
}
399399

@@ -461,7 +461,7 @@ export class SlickGridMenu {
461461

462462
const textElm = document.createElement('span');
463463
textElm.className = 'slick-gridmenu-content';
464-
textElm.innerHTML = this.grid.sanitizeHtmlString((item as GridMenuItem).title || '');
464+
this.grid.applyHtmlCode(textElm, this.grid.sanitizeHtmlString((item as GridMenuItem).title || ''));
465465

466466
liElm.appendChild(textElm);
467467

@@ -512,7 +512,7 @@ export class SlickGridMenu {
512512
if (this._gridMenuOptions?.columnTitle) {
513513
this._columnTitleElm = document.createElement('div');
514514
this._columnTitleElm.className = 'title';
515-
this._columnTitleElm.innerHTML = this.grid.sanitizeHtmlString(this._gridMenuOptions.columnTitle);
515+
this.grid.applyHtmlCode(this._columnTitleElm, this.grid.sanitizeHtmlString(this._gridMenuOptions.columnTitle));
516516
this._menuElm.appendChild(this._columnTitleElm);
517517
}
518518

@@ -592,7 +592,7 @@ export class SlickGridMenu {
592592

593593
const labelElm = document.createElement('label');
594594
labelElm.htmlFor = `${this._gridUid}-gridmenu-colpicker-${columnId}`;
595-
labelElm.innerHTML = this.grid.sanitizeHtmlString((columnLabel instanceof HTMLElement ? columnLabel.innerHTML : columnLabel) || '');
595+
this.grid.applyHtmlCode(labelElm, this.grid.sanitizeHtmlString((columnLabel instanceof HTMLElement ? columnLabel.innerHTML : columnLabel) || ''));
596596
liElm.appendChild(labelElm);
597597
this._listElm.appendChild(liElm);
598598
}
@@ -760,11 +760,11 @@ export class SlickGridMenu {
760760

761761
/** Update the Titles of each sections (command, commandTitle, ...) */
762762
updateAllTitles(gridMenuOptions: GridMenuOption) {
763-
if (this._commandTitleElm?.innerHTML) {
764-
this._commandTitleElm.innerHTML = this.grid.sanitizeHtmlString(gridMenuOptions.commandTitle || gridMenuOptions.customTitle || '');
763+
if (this._commandTitleElm) {
764+
this.grid.applyHtmlCode(this._commandTitleElm, this.grid.sanitizeHtmlString(gridMenuOptions.commandTitle || gridMenuOptions.customTitle || ''));
765765
}
766-
if (this._columnTitleElm?.innerHTML) {
767-
this._columnTitleElm.innerHTML = this.grid.sanitizeHtmlString(gridMenuOptions.columnTitle || '');
766+
if (this._columnTitleElm) {
767+
this.grid.applyHtmlCode(this._columnTitleElm, this.grid.sanitizeHtmlString(gridMenuOptions.columnTitle || ''));
768768
}
769769
}
770770

src/plugins/slick.cellmenu.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ export class SlickCellMenu implements SlickPlugin {
333333
const spanCloseElm = document.createElement('span');
334334
spanCloseElm.className = 'close';
335335
spanCloseElm.ariaHidden = 'true';
336-
spanCloseElm.innerHTML = '&times;';
336+
spanCloseElm.textContent = '×';
337337
closeButtonElm.appendChild(spanCloseElm);
338338
}
339339

src/plugins/slick.customtooltip.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ export class SlickCustomTooltip {
263263
*/
264264
protected renderRegularTooltip(formatterOrText: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: any) {
265265
const tmpDiv = document.createElement('div');
266-
tmpDiv.innerHTML = this.parseFormatterAndSanitize(formatterOrText, cell, value, columnDef, item);
266+
this._grid.applyHtmlCode(tmpDiv, this.parseFormatterAndSanitize(formatterOrText, cell, value, columnDef, item));
267267
let tooltipText = columnDef.toolTip || '';
268268
let tmpTitleElm;
269269

@@ -427,12 +427,12 @@ export class SlickCustomTooltip {
427427
* Parse the Custom Formatter (when provided) or return directly the text when it is already a string.
428428
* We will also sanitize the text in both cases before returning it so that it can be used safely.
429429
*/
430-
protected parseFormatterAndSanitize(formatterOrText: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: unknown): string {
430+
protected parseFormatterAndSanitize(formatterOrText: Formatter | string | undefined, cell: { row: number; cell: number; }, value: any, columnDef: Column, item: unknown): string | HTMLElement {
431431
if (typeof formatterOrText === 'function') {
432432
const tooltipResult = formatterOrText(cell.row, cell.cell, value, columnDef, item, this._grid);
433-
let formatterText = (Object.prototype.toString.call(tooltipResult) !== '[object Object]' ? tooltipResult : (tooltipResult as FormatterResultWithHtml).html || (tooltipResult as FormatterResultWithText).text);
433+
const formatterText = (Object.prototype.toString.call(tooltipResult) !== '[object Object]' ? tooltipResult : (tooltipResult as FormatterResultWithHtml).html || (tooltipResult as FormatterResultWithText).text);
434434
if (formatterText instanceof HTMLElement) {
435-
formatterText = formatterText.outerHTML;
435+
return formatterText;
436436
}
437437
return this._grid.sanitizeHtmlString(formatterText as string);
438438
} else if (typeof formatterOrText === 'string') {
@@ -450,15 +450,27 @@ export class SlickCustomTooltip {
450450
this._tooltipElm.classList.add('l' + cell.cell);
451451
this._tooltipElm.classList.add('r' + cell.cell);
452452
let outputText = tooltipText || this.parseFormatterAndSanitize(formatter, cell, value, columnDef, item) || '';
453-
outputText = (this._cellTooltipOptions.tooltipTextMaxLength && outputText.length > this._cellTooltipOptions.tooltipTextMaxLength) ? outputText.substring(0, this._cellTooltipOptions.tooltipTextMaxLength - 3) + '...' : outputText;
453+
if (outputText instanceof HTMLElement) {
454+
const content = outputText.textContent || '';
455+
if (this._cellTooltipOptions.tooltipTextMaxLength && content.length > this._cellTooltipOptions.tooltipTextMaxLength) {
456+
outputText.textContent = content.substring(0, this._cellTooltipOptions.tooltipTextMaxLength - 3) + '...';
457+
}
458+
} else {
459+
outputText = (this._cellTooltipOptions.tooltipTextMaxLength && outputText.length > this._cellTooltipOptions.tooltipTextMaxLength) ? outputText.substring(0, this._cellTooltipOptions.tooltipTextMaxLength - 3) + '...' : outputText;
460+
}
454461

455462
let finalOutputText = '';
456463
if (!tooltipText || (this._cellTooltipOptions?.renderRegularTooltipAsHtml)) {
457-
finalOutputText = this._grid.sanitizeHtmlString(outputText);
458-
this._tooltipElm.innerHTML = finalOutputText;
464+
if (outputText instanceof HTMLElement) {
465+
this._grid.applyHtmlCode(this._tooltipElm, outputText);
466+
finalOutputText = this._grid.sanitizeHtmlString(outputText.textContent || '');
467+
} else {
468+
finalOutputText = this._grid.sanitizeHtmlString(outputText);
469+
this._tooltipElm.innerHTML = finalOutputText;
470+
}
459471
this._tooltipElm.style.whiteSpace = this._cellTooltipOptions?.whiteSpace ?? this._defaults.whiteSpace as string;
460472
} else {
461-
finalOutputText = outputText || '';
473+
finalOutputText = (outputText instanceof HTMLElement ? outputText.textContent : outputText) || '';
462474
this._tooltipElm.textContent = finalOutputText;
463475
this._tooltipElm.style.whiteSpace = this._cellTooltipOptions?.regularTooltipWhiteSpace ?? this._defaults.regularTooltipWhiteSpace as string; // use `pre` so that sequences of white space are collapsed. Lines are broken at newline characters
464476
}
@@ -484,7 +496,7 @@ export class SlickCustomTooltip {
484496
}
485497

486498
// also clear any "title" attribute to avoid showing a 2nd browser tooltip
487-
this.swapAndClearTitleAttribute(inputTitleElm, outputText);
499+
this.swapAndClearTitleAttribute(inputTitleElm, (outputText instanceof HTMLElement ? outputText.textContent : outputText) || '');
488500
}
489501
}
490502

src/slick.grid.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -526,13 +526,15 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
526526
* @param val - input value can be either a string or an HTMLElement
527527
*/
528528
applyHtmlCode(target: HTMLElement, val: string | HTMLElement) {
529-
if (val instanceof HTMLElement) {
530-
target.appendChild(val);
531-
} else {
532-
if (this._options.enableHtmlRendering) {
533-
target.innerHTML = this.sanitizeHtmlString(val as string);
529+
if (target) {
530+
if (val instanceof HTMLElement) {
531+
target.appendChild(val);
534532
} else {
535-
target.textContent = this.sanitizeHtmlString(val as string);
533+
if (this._options.enableHtmlRendering) {
534+
target.innerHTML = this.sanitizeHtmlString(val as string);
535+
} else {
536+
target.textContent = this.sanitizeHtmlString(val as string);
537+
}
536538
}
537539
}
538540
}
@@ -3937,11 +3939,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
39373939
// if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
39383940
if (item) {
39393941
const cellResult = (Object.prototype.toString.call(formatterResult) !== '[object Object]' ? formatterResult : (formatterResult as FormatterResultWithHtml).html || (formatterResult as FormatterResultWithText).text);
3940-
if (cellResult instanceof HTMLElement) {
3941-
cellDiv.appendChild(cellResult);
3942-
} else {
3943-
cellDiv.innerHTML = this.sanitizeHtmlString(cellResult as string);
3944-
}
3942+
this.applyHtmlCode(cellDiv, cellResult as string | HTMLElement);
39453943
}
39463944

39473945
divRow.appendChild(cellDiv);
@@ -4625,7 +4623,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
46254623
}
46264624

46274625
const x = document.createElement('div');
4628-
x.innerHTML = this.sanitizeHtmlString(divRow.outerHTML);
4626+
x.appendChild(divRow);
46294627

46304628
let processedRow: number | null | undefined;
46314629
let node: HTMLElement;

0 commit comments

Comments
 (0)