Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: SlickEmptyWarningComponent should accept native HTML for CSP safe #1333

Merged
merged 3 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/common/src/interfaces/emptyWarning.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export interface EmptyWarning {
/** Empty data warning message, defaults to "No data to display." */
message: string;
message: string | HTMLElement | DocumentFragment;

/** Empty data warning message translation key, defaults to "EMPTY_DATA_WARNING_MESSAGE" */
messageKey?: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
import type {
ContainerService,
EmptyWarning,
ExternalResource,
GridOption,
SlickGrid,
TranslaterService
import {
classNameToList,
type ContainerService,
type EmptyWarning,
type ExternalResource,
type GridOption,
type SlickGrid,
type TranslaterService
} from '@slickgrid-universal/common';

export class SlickEmptyWarningComponent implements ExternalResource {
protected _grid!: SlickGrid;
protected _isPreviouslyShown = false;
protected _translaterService?: TranslaterService | null;
protected _warningLeftElement: HTMLDivElement | null = null;
protected _warningRightElement: HTMLDivElement | null = null;
protected grid!: SlickGrid;
protected isPreviouslyShown = false;
protected translaterService?: TranslaterService | null;


/** Getter for the Grid Options pulled through the Grid Object */
get gridOptions(): GridOption {
return this.grid?.getOptions() ?? {};
return this._grid?.getOptions() ?? {};
}

constructor() { }

init(grid: SlickGrid, containerService: ContainerService) {
this.grid = grid;
this.translaterService = containerService.get<TranslaterService>('TranslaterService');
this._grid = grid;
this._translaterService = containerService.get<TranslaterService>('TranslaterService');
}

dispose() {
Expand All @@ -41,14 +39,14 @@ export class SlickEmptyWarningComponent implements ExternalResource {
* @param options - any styling options you'd like to pass like the text color
*/
showEmptyDataMessage(isShowing = true, options?: EmptyWarning): boolean {
if (!this.grid || !this.gridOptions || this.isPreviouslyShown === isShowing) {
if (!this._grid || !this.gridOptions || this._isPreviouslyShown === isShowing) {
return false;
}

// keep reference so that we won't re-render the warning if the status is the same
this.isPreviouslyShown = isShowing;
this._isPreviouslyShown = isShowing;

const gridUid = this.grid.getUID();
const gridUid = this._grid.getUID();
const defaultMessage = 'No data to display.';
const mergedOptions: EmptyWarning = { message: defaultMessage, ...this.gridOptions.emptyDataWarning, ...options };
const emptyDataClassName = mergedOptions?.className ?? 'slick-empty-data-warning';
Expand Down Expand Up @@ -89,15 +87,15 @@ export class SlickEmptyWarningComponent implements ExternalResource {

// warning message could come from a translation key or by the warning options
let warningMessage = mergedOptions.message;
if (this.gridOptions.enableTranslate && this.translaterService && mergedOptions?.messageKey) {
warningMessage = this.translaterService.translate(mergedOptions.messageKey);
if (this.gridOptions.enableTranslate && this._translaterService && mergedOptions?.messageKey) {
warningMessage = this._translaterService.translate(mergedOptions.messageKey);
}

if (!this._warningLeftElement && gridCanvasLeftElm && gridCanvasRightElm) {
this._warningLeftElement = document.createElement('div');
this._warningLeftElement.classList.add(emptyDataClassName);
this._warningLeftElement.classList.add(...classNameToList(emptyDataClassName));
this._warningLeftElement.classList.add('left');
this.grid.applyHtmlCode(this._warningLeftElement, warningMessage);
this._grid.applyHtmlCode(this._warningLeftElement, warningMessage);

// clone the warning element and add the "right" class to it so we can distinguish
this._warningRightElement = this._warningLeftElement.cloneNode(true) as HTMLDivElement;
Expand Down
39 changes: 34 additions & 5 deletions packages/empty-warning-component/src/slick-empty-warning.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { EmptyWarning, GridOption, SlickGrid } from '@slickgrid-universal/common';
import { createDomElement, type EmptyWarning, type GridOption, type SlickGrid } from '@slickgrid-universal/common';
import { SlickEmptyWarningComponent } from './slick-empty-warning.component';
import { ContainerServiceStub } from '../../../test/containerServiceStub';
import { TranslateServiceStub } from '../../../test/translateServiceStub';
import * as DOMPurify from 'dompurify';

const GRID_UID = 'slickgrid_123456';

Expand All @@ -12,7 +11,13 @@ const mockGridOptions = {
} as GridOption;

const gridStub = {
applyHtmlCode: (elm, val) => elm.innerHTML = DOMPurify.sanitize(val || ''),
applyHtmlCode: (elm, val) => {
if (val instanceof HTMLElement || val instanceof DocumentFragment) {
elm.appendChild(val)
} else {
elm.innerHTML = val || ''
}
},
getGridPosition: () => mockGridOptions,
getOptions: () => mockGridOptions,
getUID: () => GRID_UID,
Expand Down Expand Up @@ -352,8 +357,12 @@ describe('Slick-Empty-Warning Component', () => {
expect(componentElm.innerHTML).toBe('<span class="fa fa-warning"></span> No Record found.');
});

it('should expect the Slick-Empty-Warning provide html text and expect script to be sanitized out of the final html', () => {
const mockOptions = { message: `<script>alert('test')></script><span class="fa fa-warning"></span> No Record found.`, className: 'custom-class', marginTop: 22, marginLeft: 11 };
it('should expect the Slick-Empty-Warning to change some options and display a different message is provided as a DocumentFragment', () => {
const emptyWarningElm = new DocumentFragment();
emptyWarningElm.appendChild(createDomElement('span', { className: 'fa fa-warning' }));
emptyWarningElm.appendChild(document.createTextNode(' No Record found.'));

const mockOptions = { message: emptyWarningElm, className: 'custom-class', marginTop: 22, marginLeft: 11 };
component = new SlickEmptyWarningComponent();
component.init(gridStub, container);
component.showEmptyDataMessage(true, mockOptions);
Expand All @@ -368,6 +377,26 @@ describe('Slick-Empty-Warning Component', () => {
expect(componentElm.innerHTML).toBe('<span class="fa fa-warning"></span> No Record found.');
});

it('should expect the Slick-Empty-Warning to change some options and display a different message is provided as an HTMLElement', () => {
const emptyWarningElm = createDomElement('div', { className: 'container' });
emptyWarningElm.appendChild(createDomElement('span', { className: 'fa fa-warning' }));
emptyWarningElm.appendChild(document.createTextNode(' No Record found.'));

const mockOptions = { message: emptyWarningElm, className: 'custom-class', marginTop: 22, marginLeft: 11 };
component = new SlickEmptyWarningComponent();
component.init(gridStub, container);
component.showEmptyDataMessage(true, mockOptions);

const componentElm = document.querySelector<HTMLSelectElement>('div.slickgrid_123456 .grid-canvas .custom-class') as HTMLSelectElement;

expect(component).toBeTruthy();
expect(component.constructor).toBeDefined();
expect(componentElm).toBeTruthy();
expect(componentElm.style.display).toBe('block');
expect(componentElm.classList.contains('custom-class')).toBeTruthy();
expect(componentElm.innerHTML).toBe('<div class="container"><span class="fa fa-warning"></span> No Record found.</div>');
});

it('should expect the Slick-Empty-Warning message to be translated to French when providing a Translater Service and "messageKey" property', () => {
container.registerInstance('TranslaterService', translateService);
mockGridOptions.enableTranslate = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { GridOption } from '@slickgrid-universal/common';
import { createDomElement, type GridOption } from '@slickgrid-universal/common';
import { EventNamingStyle } from '@slickgrid-universal/event-pub-sub';

// create empty warning message as Document Fragment to be CSP safe
const emptyWarningElm = new DocumentFragment();
emptyWarningElm.appendChild(createDomElement('span', { className: 'mdi mdi-alert color-warning' }));
emptyWarningElm.appendChild(document.createTextNode(' No data to display.'));

/** Global Grid Options Defaults for Salesforce */
export const SalesforceGlobalGridOptions = {
autoEdit: true, // true single click (false for double-click)
Expand All @@ -20,7 +25,7 @@ export const SalesforceGlobalGridOptions = {
},
datasetIdPropertyName: 'Id',
emptyDataWarning: {
message: `<span class="mdi mdi-alert color-warning"></span> No data to display.`,
message: emptyWarningElm
},
enableDeepCopyDatasetOnPageLoad: true,
enableTextExport: true,
Expand Down