Skip to content

Commit

Permalink
fix: SlickEmptyWarningComponent should accept native HTML for CSP safe (
Browse files Browse the repository at this point in the history
#1333)

* fix: SlickEmptyWarningComponent should accept native HTML
- we should be able to pass `HTMLElement`/`DocumentFragment` to empty warning `message` option
  • Loading branch information
ghiscoding authored Jan 16, 2024
1 parent 2b9216d commit 4740f96
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 31 deletions.
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

0 comments on commit 4740f96

Please sign in to comment.