Skip to content

Commit

Permalink
fix: change dynamic html string w/CSP safe code to fix scroll (#1210)
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding authored Nov 23, 2023
1 parent 25ffca0 commit cd03907
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 51 deletions.
82 changes: 32 additions & 50 deletions packages/common/src/core/slickGrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
if (typeof this._options?.sanitizer === 'function') {
sanitizedText = this._options.sanitizer(val || '');
} else if (typeof DOMPurify?.sanitize === 'function') {
sanitizedText = DOMPurify.sanitize(val || '', sanitizerOptions || { RETURN_TRUSTED_TYPE: true });
sanitizedText = DOMPurify.sanitize(val || '', sanitizerOptions || { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true });
}

if (this._options.enableHtmlRendering) {
Expand Down Expand Up @@ -3270,7 +3270,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
return item[columnDef.field as keyof TData];
}

protected appendRowHtml(stringArrayL: string[], stringArrayR: string[], row: number, range: CellViewportRange, dataLength: number) {
protected appendRowHtml(divArrayL: HTMLElement[], divArrayR: HTMLElement[], row: number, range: CellViewportRange, dataLength: number) {
const d = this.getDataItem(row);
const dataLoading = row < dataLength && !d;
let rowCss = 'slick-row' +
Expand All @@ -3291,12 +3291,15 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e

const frozenRowOffset = this.getFrozenRowOffset(row);

const rowHtml = `<div class="ui-widget-content ${rowCss}" data-top="${(this.getRowTop(row) - frozenRowOffset)}px">`;

stringArrayL.push(rowHtml);
const rowDiv = createDomElement('div', { className: `ui-widget-content ${rowCss}`, style: { top: `${this.getRowTop(row) - frozenRowOffset}px` } });
let rowDivR: HTMLElement | undefined;
divArrayL.push(rowDiv);

if (this.hasFrozenColumns()) {
stringArrayR.push(rowHtml);
// it has to be a deep copy otherwise we will have issues with pass by reference in js since
// attempting to add the same element to 2 different arrays will just move 1 item to the other array
rowDivR = rowDiv.cloneNode(true) as HTMLElement;
divArrayR.push(rowDivR);
}

let colspan: number | string;
Expand All @@ -3322,28 +3325,22 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
}

if (this.hasFrozenColumns() && (i > this._options.frozenColumn!)) {
this.appendCellHtml(stringArrayR, row, i, (colspan as number), d);
this.appendCellHtml(rowDivR!, row, i, (colspan as number), d);
} else {
this.appendCellHtml(stringArrayL, row, i, (colspan as number), d);
this.appendCellHtml(rowDiv, row, i, (colspan as number), d);
}
} else if (m.alwaysRenderColumn || (this.hasFrozenColumns() && i <= this._options.frozenColumn!)) {
this.appendCellHtml(stringArrayL, row, i, (colspan as number), d);
this.appendCellHtml(rowDiv, row, i, (colspan as number), d);
}

if ((colspan as number) > 1) {
i += ((colspan as number) - 1);
}
}

stringArrayL.push('</div>');

if (this.hasFrozenColumns()) {
stringArrayR.push('</div>');
}
}

protected appendCellHtml(stringArray: string[], row: number, cell: number, colspan: number, item: TData) {
// stringArray: stringBuilder containing the HTML parts
protected appendCellHtml(divRow: HTMLElement, row: number, cell: number, colspan: number, item: TData) {
// divRow: the html element to append items too
// row, cell: row and column index
// colspan: HTML colspan
// item: grid data for row
Expand Down Expand Up @@ -3384,27 +3381,27 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
if ((formatterResult as FormatterResultObject)?.addClasses) {
addlCssClasses += (addlCssClasses ? ' ' : '') + (formatterResult as FormatterResultObject).addClasses;
}
const toolTip = (formatterResult as FormatterResultObject)?.toolTip ? `title="${(formatterResult as FormatterResultObject).toolTip}"` : '';

let customAttrStr = '';
const toolTipText = (formatterResult as FormatterResultObject)?.toolTip ? `${(formatterResult as FormatterResultObject).toolTip}` : '';
const cellDiv = document.createElement('div');
cellDiv.className = `${cellCss} ${addlCssClasses || ''}`.trim();
cellDiv.setAttribute('title', toolTipText);

if (m.hasOwnProperty('cellAttrs') && m.cellAttrs instanceof Object) {
for (const key in m.cellAttrs) {
if (m.cellAttrs.hasOwnProperty(key)) {
customAttrStr += ` ${key}="${m.cellAttrs[key]}" `;
cellDiv.setAttribute(key, m.cellAttrs[key]);
}
}
}

stringArray.push(`<div class="${cellCss + (addlCssClasses ? ' ' + addlCssClasses : '')}" ${toolTip + customAttrStr}>`);

// if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
if (item) {
const cellResult = (Object.prototype.toString.call(formatterResult) !== '[object Object]' ? formatterResult : (formatterResult as FormatterResultWithHtml).html || (formatterResult as FormatterResultWithText).text);
const formattedCellResult = (cellResult instanceof HTMLElement) ? cellResult.outerHTML : cellResult;
stringArray.push(formattedCellResult as string);
this.applyHtmlCode(cellDiv, cellResult as string | HTMLElement);
}

stringArray.push('</div>');
divRow.appendChild(cellDiv);

this.rowsCache[row].cellRenderQueue.push(cell);
this.rowsCache[row].cellColSpans[cell] = colspan;
Expand Down Expand Up @@ -4018,7 +4015,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e

protected cleanUpAndRenderCells(range: CellViewportRange) {
let cacheEntry;
const stringArray: string[] = [];
const divRow: HTMLElement = document.createElement('div');
const processedRows: number[] = [];
let cellsAdded: number;
let totalCellsAdded = 0;
Expand Down Expand Up @@ -4069,7 +4066,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e

const colspanNb = colspan as number; // at this point colspan is for sure a number
if (this.columnPosRight[Math.min(ii - 1, i + colspanNb - 1)] > range.leftPx) {
this.appendCellHtml(stringArray, row, i, colspanNb, d);
this.appendCellHtml(divRow, row, i, colspanNb, d);
cellsAdded++;
}

Expand All @@ -4082,20 +4079,17 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
processedRows.push(row);
}
}
if (!stringArray.length) {
if (!divRow.children.length) {
return;
}

const x = document.createElement('div');
x.innerHTML = this.sanitizeHtmlString(stringArray.join(''));

let processedRow: number | null | undefined;
let node: HTMLElement;
while (isDefined(processedRow = processedRows.pop())) {
cacheEntry = this.rowsCache[processedRow];
let columnIdx;
while (isDefined(columnIdx = cacheEntry.cellRenderQueue.pop())) {
node = x.lastChild as HTMLElement;
node = divRow.lastChild as HTMLElement;

// no idea why node would be null here but apparently it is..
if (!node) {
Expand All @@ -4112,8 +4106,8 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
}

protected renderRows(range: { top: number; bottom: number; leftPx: number; rightPx: number; }) {
const stringArrayL: string[] = [];
const stringArrayR: string[] = [];
const divArrayL: HTMLElement[] = [];
const divArrayR: HTMLElement[] = [];
const rows: number[] = [];
let needToReselectCell = false;
const dataLength = this.getDataLength();
Expand Down Expand Up @@ -4143,7 +4137,7 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
cellRenderQueue: []
};

this.appendRowHtml(stringArrayL, stringArrayR, i, range, dataLength);
this.appendRowHtml(divArrayL, divArrayR, i, range, dataLength);
if (this.activeCellNode && this.activeRow === i) {
needToReselectCell = true;
}
Expand All @@ -4154,12 +4148,9 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e

const x = document.createElement('div');
const xRight = document.createElement('div');
x.innerHTML = this.sanitizeHtmlString(stringArrayL.join(''));
xRight.innerHTML = this.sanitizeHtmlString(stringArrayR.join(''));
const elements1 = x.querySelectorAll('[data-top]') as NodeListOf<HTMLElement>;
const elements2 = xRight.querySelectorAll('[data-top]') as NodeListOf<HTMLElement>;
this.applyTopStyling(elements1);
this.applyTopStyling(elements2);
divArrayL.forEach(elm => x.appendChild(elm as HTMLElement));
divArrayR.forEach(elm => xRight.appendChild(elm as HTMLElement));

for (let i = 0, ii = rows.length; i < ii; i++) {
if ((this.hasFrozenRows) && (rows[i] >= this.actualFrozenRow)) {
if (this.hasFrozenColumns()) {
Expand Down Expand Up @@ -4193,15 +4184,6 @@ export class SlickGrid<TData = any, C extends Column<TData> = Column<TData>, O e
}
}

protected applyTopStyling(elements: NodeListOf<HTMLElement>) {
elements?.forEach((element: HTMLElement) => {
const top = element.dataset.top;
if (top !== undefined) {
element.style.top = top;
}
});
}

protected startPostProcessing() {
if (!this._options.enableAsyncPostRender) {
return;
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/services/domUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ export function sanitizeTextByAvailableSanitizer(gridOptions: GridOption, dirtyH
if (typeof gridOptions?.sanitizer === 'function') {
sanitizedText = gridOptions.sanitizer(dirtyHtml || '');
} else if (typeof DOMPurify?.sanitize === 'function') {
sanitizedText = (DOMPurify.sanitize(dirtyHtml || '', sanitizerOptions || { RETURN_TRUSTED_TYPE: true }) || '').toString();
sanitizedText = (DOMPurify.sanitize(dirtyHtml || '', sanitizerOptions || { ADD_ATTR: ['level'], RETURN_TRUSTED_TYPE: true }) || '').toString();
}

return sanitizedText;
Expand Down

0 comments on commit cd03907

Please sign in to comment.