Skip to content

Commit

Permalink
fix: move innerHTML as separate assignment outside of createDomElement
Browse files Browse the repository at this point in the history
- to further improve CSP support for issue #878, we need to move `innerHTML` as separate assignment and not use it directly within a `createDomElement`, so for example this line `const elm = createDomElement('div', { innerHTML: '' })` should be split in 2 lines `const elm = createDomElement('div'); elm.innerHTML = '';`
  • Loading branch information
ghiscoding-SE committed Oct 30, 2023
1 parent 1e13388 commit 25ebd19
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 9 deletions.
5 changes: 3 additions & 2 deletions cypress/e2e/example-colspan.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ describe('Example - Column Span & Header Grouping', { retries: 1 }, () => {
}

it('should display Example title', () => {
cy.visit(`${Cypress.config('baseUrl')}/examples/example-colspan.html`);
cy.get('h2').contains('Demonstrates');
cy.visit(`${Cypress.config('baseUrl')}/examples/example-colspan.html`);
cy.get('h2').contains('Demonstrates');
cy.get('h2 + ul > li').first().contains('column span');
});

it('should have exact column titles', () => {
Expand Down
52 changes: 52 additions & 0 deletions cypress/e2e/example-csp-header.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
describe('Example CSP Header - with Column Span & Header Grouping', () => {
// NOTE: everywhere there's a * 2 is because we have a top+bottom (frozen rows) containers even after Unfreeze Columns/Rows
const GRID_ROW_HEIGHT = 25;
const fullTitles = ['Title', 'Duration', '% Complete', 'Start', 'Finish', 'Effort Driven'];
for (let i = 0; i < 30; i++) {
fullTitles.push(`Mock${i}`);
}

beforeEach(() => {
// create a console.log spy for later use
cy.window().then((win) => {
cy.spy(win.console, "log");
});
});

it('should display Example title', () => {
cy.visit(`${Cypress.config('baseUrl')}/examples/example-csp-header.html`);
cy.get('h2').contains('Demonstrates');
cy.get('h2 + ul > li').first().contains('column span');
});

it('should have exact column titles', () => {
cy.get('#myGrid')
.find('.slick-header-columns')
.children()
.each(($child, index) => expect($child.text()).to.eq(fullTitles[index]));
});

it('should expect 1st row to be 1 column spanned to the entire width', () => {
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(0)`).should('contain', 'Task 0');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 0}px"] > .slick-cell:nth(1)`).should('not.exist');
});

it('should expect 2nd row to be 4 columns and not be spanned', () => {
cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(0)`).should('contain', 'Task 1');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(1)`).should('contain', '5 days');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(2)`).should('contain', '01/05/2009');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 1}px"] > .slick-cell:nth(3)`).contains(/(true|false)/);
});

it('should expect 3rd row to be 1 column spanned to the entire width', () => {
cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(0)`).should('contain', 'Task 2');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 2}px"] > .slick-cell:nth(1)`).should('not.exist');
});

it('should expect 4th row to be 4 columns and not be spanned', () => {
cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(0)`).should('contain', 'Task 3');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(1)`).should('contain', '5 days');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(2)`).should('contain', '01/05/2009');
cy.get(`[style="top:${GRID_ROW_HEIGHT * 3}px"] > .slick-cell:nth(3)`).contains(/(true|false)/);
});
});
4 changes: 3 additions & 1 deletion examples/example-csp-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
<link rel="shortcut icon" type="image/ico" href="favicon.ico" />
<link rel="stylesheet" href="../dist/styles/css/example-demo.css" type="text/css"/>
<link rel="stylesheet" href="../dist/styles/css/slick-alpine-theme.css" type="text/css"/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https://cdn.jsdelivr.net 'nonce-random-string' 'nonce-browser-sync'">
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self' https://cdn.jsdelivr.net 'nonce-random-string' 'nonce-browser-sync'; require-trusted-types-for 'script'; trusted-types dompurify"> -->
<meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script'; trusted-types dompurify">
</head>
<body>
<table width="100%">
Expand All @@ -30,6 +31,7 @@ <h2>View Source:</h2>

<script src="https://cdn.jsdelivr.net/npm/sortablejs/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js"></script>
<!-- <script src="./example-csp-policy.js"></script> -->

<script src="../dist/browser/slick.core.js"></script>
<script src="../dist/browser/slick.interactions.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion examples/example-csp-header.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ var columns = [
var options = {
enableCellNavigation: true,
enableColumnReorder: false,
sanitizer: (dirtyHtml) => DOMPurify.sanitize(dirtyHtml, { RETURN_TRUSTED_TYPE: true })
sanitizer: (html) => DOMPurify.sanitize(html, { RETURN_TRUSTED_TYPE: true })
};

document.addEventListener("DOMContentLoaded", function () {
Expand Down
5 changes: 5 additions & 0 deletions examples/example-csp-policy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
trustedTypes.createPolicy('sanitizeWithDomPurify', {
createHTML: string => DOMPurify.sanitize(string, { RETURN_TRUSTED_TYPE: true })
});
}
5 changes: 5 additions & 0 deletions src/slick.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,11 @@ export class Utils {

if (elementOptions) {
Object.keys(elementOptions).forEach((elmOptionKey) => {
if (elmOptionKey === 'innerHTML') {
console.warn(`[SlickGrid] For better CSP (Content Security Policy) support, do not use "innerHTML" directly in "createDomElement('${tagName}', { innerHTML: 'some html'})"` +
`, it is better as separate assignment: "const elm = createDomElement('span'); elm.innerHTML = 'some html';"`);
}

const elmValue = elementOptions[elmOptionKey as keyof typeof elementOptions];
if (typeof elmValue === 'object') {
Object.assign(elm[elmOptionKey as K] as object, elmValue);
Expand Down
17 changes: 12 additions & 5 deletions src/slick.grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1537,7 +1537,9 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
const headerRowTarget = this.hasFrozenColumns() ? ((i <= this._options.frozenColumn!) ? this._headerRowL : this._headerRowR) : this._headerRowL;

const header = Utils.createDomElement('div', { id: `${this.uid + m.id}`, dataset: { id: String(m.id) }, className: 'ui-state-default slick-state-default slick-header-column', title: m.toolTip || '' }, headerTarget);
Utils.createDomElement('span', { className: 'slick-column-name', innerHTML: this.sanitizeHtmlString(m.name as string) }, header);
const colNameElm = Utils.createDomElement('span', { className: 'slick-column-name' }, header);
colNameElm.innerHTML = this.sanitizeHtmlString(m.name as string);

Utils.width(header, m.width! - this.headerColumnWidthDiff);

let classname = m.headerCssClass || null;
Expand Down Expand Up @@ -3017,7 +3019,8 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
// headers have not yet been created, create a new node
const header = this.getHeader(columnDef) as HTMLElement;
headerColEl = Utils.createDomElement('div', { id: dummyHeaderColElId, className: 'ui-state-default slick-state-default slick-header-column' }, header);
Utils.createDomElement('span', { className: 'slick-column-name', innerHTML: this.sanitizeHtmlString(String(columnDef.name)) }, headerColEl);
const colNameElm = Utils.createDomElement('span', { className: 'slick-column-name' }, headerColEl);
colNameElm.innerHTML = this.sanitizeHtmlString(String(columnDef.name));
clone.style.cssText = 'position: absolute; visibility: hidden;right: auto;text-overflow: initial;white-space: nowrap;';
if (columnDef.headerCssClass) {
headerColEl.classList.add(...(columnDef.headerCssClass || '').split(' '));
Expand Down Expand Up @@ -4552,7 +4555,9 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
return;
}

const x = Utils.createDomElement('div', { innerHTML: this.sanitizeHtmlString(stringArray.join('')) });
const x = document.createElement('div');
x.innerHTML = this.sanitizeHtmlString(stringArray.join(''));

let processedRow: number | null | undefined;
let node: HTMLElement;
while (Utils.isDefined(processedRow = processedRows.pop())) {
Expand Down Expand Up @@ -4612,8 +4617,10 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e

if (!rows.length) { return; }

const x = Utils.createDomElement('div', { innerHTML: this.sanitizeHtmlString(stringArrayL.join('')) });
const xRight = Utils.createDomElement('div', { innerHTML: this.sanitizeHtmlString(stringArrayR.join('')) });
const x = document.createElement('div');
const xRight = document.createElement('div');
x.innerHTML = this.sanitizeHtmlString(stringArrayL.join(''));
xRight.innerHTML = this.sanitizeHtmlString(stringArrayR.join(''));

for (let i = 0, ii = rows.length; i < ii; i++) {
if ((this.hasFrozenRows) && (rows[i] >= this.actualFrozenRow)) {
Expand Down

0 comments on commit 25ebd19

Please sign in to comment.