Skip to content

Commit

Permalink
[DataGrid] Fix eval blocked by CSP (#9863)
Browse files Browse the repository at this point in the history
  • Loading branch information
romgrk authored Aug 16, 2023
1 parent 8e4846f commit 0032290
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@ DataGridPremiumRaw.propTypes = {
* @default false
*/
disableDensitySelector: PropTypes.bool,
/**
* If `true`, `eval()` is not used for performance optimization.
* @default false
* @ignore - do not document
*/
disableEval: PropTypes.bool,
/**
* If `true`, filtering with multiple columns is disabled.
* @default false
Expand Down
6 changes: 6 additions & 0 deletions packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,12 @@ DataGridProRaw.propTypes = {
* @default false
*/
disableDensitySelector: PropTypes.bool,
/**
* If `true`, `eval()` is not used for performance optimization.
* @default false
* @ignore - do not document
*/
disableEval: PropTypes.bool,
/**
* If `true`, filtering with multiple columns is disabled.
* @default false
Expand Down
6 changes: 6 additions & 0 deletions packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ DataGridRaw.propTypes = {
* @default false
*/
disableDensitySelector: PropTypes.bool,
/**
* If `true`, `eval()` is not used for performance optimization.
* @default false
* @ignore - do not document
*/
disableEval: PropTypes.bool,
/**
* If `true`, the selection on click on a row or cell is disabled.
* @default false
Expand Down
1 change: 1 addition & 0 deletions packages/grid/x-data-grid/src/DataGrid/useDataGridProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const DATA_GRID_PROPS_DEFAULT_VALUES: DataGridPropsWithDefaultValues = {
disableColumnMenu: false,
disableColumnSelector: false,
disableDensitySelector: false,
disableEval: false,
disableMultipleColumnsFiltering: false,
disableMultipleRowSelection: false,
disableMultipleColumnsSorting: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ import {
gridVisibleColumnFieldsSelector,
} from '../columns';

let hasEval: boolean;
try {
// eslint-disable-next-line no-eval
hasEval = eval('true');
} catch (_: unknown) {
hasEval = false;
}

type GridFilterItemApplier =
| {
v7: false;
Expand Down Expand Up @@ -229,6 +237,7 @@ export const buildAggregatedFilterItemsApplier = (
getRowId: GridRowIdGetter | undefined,
filterModel: GridFilterModel,
apiRef: React.MutableRefObject<GridApiCommunity>,
disableEval: boolean,
): GridFilterItemApplierNotAggregated | null => {
const { items } = filterModel;

Expand All @@ -240,21 +249,23 @@ export const buildAggregatedFilterItemsApplier = (
return null;
}

// Original logic:
// return (row, shouldApplyFilter) => {
// const resultPerItemId: GridFilterItemResult = {};
//
// for (let i = 0; i < appliers.length; i += 1) {
// const applier = appliers[i];
// if (!shouldApplyFilter || shouldApplyFilter(applier.item.field)) {
// resultPerItemId[applier.item.id!] = applier.v7
// ? applier.fn(row)
// : applier.fn(getRowId ? getRowId(row) : row.id);
// }
// }
//
// return resultPerItemId;
// };
if (!hasEval || disableEval) {
// This is the original logic, which is used if `eval()` is not supported (aka prevented by CSP).
return (row, shouldApplyFilter) => {
const resultPerItemId: GridFilterItemResult = {};

for (let i = 0; i < appliers.length; i += 1) {
const applier = appliers[i];
if (!shouldApplyFilter || shouldApplyFilter(applier.item.field)) {
resultPerItemId[applier.item.id!] = applier.v7
? applier.fn(row)
: applier.fn(getRowId ? getRowId(row) : row.id);
}
}

return resultPerItemId;
};
}

// We generate a new function with `eval()` to avoid expensive patterns for JS engines
// such as a dynamic object assignment, e.g. `{ [dynamicKey]: value }`.
Expand Down Expand Up @@ -409,8 +420,14 @@ export const buildAggregatedFilterApplier = (
getRowId: GridRowIdGetter | undefined,
filterModel: GridFilterModel,
apiRef: React.MutableRefObject<GridApiCommunity>,
disableEval: boolean,
): GridAggregatedFilterItemApplier => {
const isRowMatchingFilterItems = buildAggregatedFilterItemsApplier(getRowId, filterModel, apiRef);
const isRowMatchingFilterItems = buildAggregatedFilterItemsApplier(
getRowId,
filterModel,
apiRef,
disableEval,
);
const isRowMatchingQuickFilter = buildAggregatedQuickFilterApplier(getRowId, filterModel, apiRef);

return function isRowMatchingFilters(row, shouldApplyFilter, result) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const useGridFilter = (
| 'slots'
| 'slotProps'
| 'disableColumnFilter'
| 'disableEval'
>,
): void => {
const logger = useGridLogger(apiRef, 'useGridFilter');
Expand All @@ -106,7 +107,7 @@ export const useGridFilter = (
const filterModel = gridFilterModelSelector(state, apiRef.current.instanceId);
const isRowMatchingFilters =
props.filterMode === 'client'
? buildAggregatedFilterApplier(props.getRowId, filterModel, apiRef)
? buildAggregatedFilterApplier(props.getRowId, filterModel, apiRef, props.disableEval)
: null;

const filteringResult = apiRef.current.applyStrategyProcessor('filtering', {
Expand All @@ -130,7 +131,7 @@ export const useGridFilter = (
};
});
apiRef.current.publishEvent('filteredRowsSet');
}, [apiRef, props.filterMode, props.getRowId]);
}, [apiRef, props.filterMode, props.getRowId, props.disableEval]);

const addColumnMenuItem = React.useCallback<GridPipeProcessor<'columnMenu'>>(
(columnMenuItems, colDef) => {
Expand Down
6 changes: 6 additions & 0 deletions packages/grid/x-data-grid/src/models/props/DataGridProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ export interface DataGridPropsWithDefaultValues {
* @default false
*/
disableDensitySelector: boolean;
/**
* If `true`, `eval()` is not used for performance optimization.
* @default false
* @ignore - do not document
*/
disableEval: boolean;
/**
* If `true`, filtering with multiple columns is disabled.
* @default false
Expand Down
44 changes: 29 additions & 15 deletions packages/grid/x-data-grid/src/tests/filtering.DataGrid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,22 @@ describe('<DataGrid /> - Filter', () => {
columns: [{ field: 'brand' }],
};

let disableEval = false;

function testEval(fn: Function) {
return () => {
disableEval = false;
fn();
disableEval = true;
fn();
disableEval = false;
};
}

function TestCase(props: Partial<DataGridProps>) {
return (
<div style={{ width: 300, height: 300 }}>
<DataGrid {...baselineProps} {...props} />
<DataGrid {...baselineProps} {...props} disableEval={disableEval} />
</div>
);
}
Expand Down Expand Up @@ -278,25 +290,27 @@ describe('<DataGrid /> - Filter', () => {
const ALL_ROWS = ['', '', '', 'France (fr)', 'Germany', '0', '1'];

it('should filter with operator "contains"', () => {
expect(getRows({ operator: 'contains', value: 'Fra' })).to.deep.equal(['France (fr)']);
testEval(() => {
expect(getRows({ operator: 'contains', value: 'Fra' })).to.deep.equal(['France (fr)']);

// Trim value
expect(getRows({ operator: 'contains', value: ' Fra ' })).to.deep.equal(['France (fr)']);
// Trim value
expect(getRows({ operator: 'contains', value: ' Fra ' })).to.deep.equal(['France (fr)']);

// Case-insensitive
expect(getRows({ operator: 'contains', value: 'fra' })).to.deep.equal(['France (fr)']);
// Case-insensitive
expect(getRows({ operator: 'contains', value: 'fra' })).to.deep.equal(['France (fr)']);

// Number casting
expect(getRows({ operator: 'contains', value: '0' })).to.deep.equal(['0']);
expect(getRows({ operator: 'contains', value: '1' })).to.deep.equal(['1']);
// Number casting
expect(getRows({ operator: 'contains', value: '0' })).to.deep.equal(['0']);
expect(getRows({ operator: 'contains', value: '1' })).to.deep.equal(['1']);

// Empty values
expect(getRows({ operator: 'contains', value: undefined })).to.deep.equal(ALL_ROWS);
expect(getRows({ operator: 'contains', value: '' })).to.deep.equal(ALL_ROWS);
// Empty values
expect(getRows({ operator: 'contains', value: undefined })).to.deep.equal(ALL_ROWS);
expect(getRows({ operator: 'contains', value: '' })).to.deep.equal(ALL_ROWS);

// Value with regexp special literal
expect(getRows({ operator: 'contains', value: '[-[]{}()*+?.,\\^$|#s]' })).to.deep.equal([]);
expect(getRows({ operator: 'contains', value: '(fr)' })).to.deep.equal(['France (fr)']);
// Value with regexp special literal
expect(getRows({ operator: 'contains', value: '[-[]{}()*+?.,\\^$|#s]' })).to.deep.equal([]);
expect(getRows({ operator: 'contains', value: '(fr)' })).to.deep.equal(['France (fr)']);
});
});

it('should filter with operator "equals"', () => {
Expand Down

0 comments on commit 0032290

Please sign in to comment.