diff --git a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 06ad924a6c97..0357715bb15c 100644 --- a/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/grid/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -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 diff --git a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index c98f167da2f5..0231d8bb4d41 100644 --- a/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/grid/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -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 diff --git a/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx index afd746f9581f..22e3860741c3 100644 --- a/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx +++ b/packages/grid/x-data-grid/src/DataGrid/DataGrid.tsx @@ -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 diff --git a/packages/grid/x-data-grid/src/DataGrid/useDataGridProps.ts b/packages/grid/x-data-grid/src/DataGrid/useDataGridProps.ts index 79454bb420b2..bbe85470742a 100644 --- a/packages/grid/x-data-grid/src/DataGrid/useDataGridProps.ts +++ b/packages/grid/x-data-grid/src/DataGrid/useDataGridProps.ts @@ -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, diff --git a/packages/grid/x-data-grid/src/hooks/features/filter/gridFilterUtils.ts b/packages/grid/x-data-grid/src/hooks/features/filter/gridFilterUtils.ts index d8975d41461c..8e3465613c3d 100644 --- a/packages/grid/x-data-grid/src/hooks/features/filter/gridFilterUtils.ts +++ b/packages/grid/x-data-grid/src/hooks/features/filter/gridFilterUtils.ts @@ -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; @@ -229,6 +237,7 @@ export const buildAggregatedFilterItemsApplier = ( getRowId: GridRowIdGetter | undefined, filterModel: GridFilterModel, apiRef: React.MutableRefObject, + disableEval: boolean, ): GridFilterItemApplierNotAggregated | null => { const { items } = filterModel; @@ -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 }`. @@ -409,8 +420,14 @@ export const buildAggregatedFilterApplier = ( getRowId: GridRowIdGetter | undefined, filterModel: GridFilterModel, apiRef: React.MutableRefObject, + 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) { diff --git a/packages/grid/x-data-grid/src/hooks/features/filter/useGridFilter.tsx b/packages/grid/x-data-grid/src/hooks/features/filter/useGridFilter.tsx index f936e76775df..a35e9d1adc43 100644 --- a/packages/grid/x-data-grid/src/hooks/features/filter/useGridFilter.tsx +++ b/packages/grid/x-data-grid/src/hooks/features/filter/useGridFilter.tsx @@ -89,6 +89,7 @@ export const useGridFilter = ( | 'slots' | 'slotProps' | 'disableColumnFilter' + | 'disableEval' >, ): void => { const logger = useGridLogger(apiRef, 'useGridFilter'); @@ -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', { @@ -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>( (columnMenuItems, colDef) => { diff --git a/packages/grid/x-data-grid/src/models/props/DataGridProps.ts b/packages/grid/x-data-grid/src/models/props/DataGridProps.ts index 381a52984aec..647e04f9dae3 100644 --- a/packages/grid/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/grid/x-data-grid/src/models/props/DataGridProps.ts @@ -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 diff --git a/packages/grid/x-data-grid/src/tests/filtering.DataGrid.test.tsx b/packages/grid/x-data-grid/src/tests/filtering.DataGrid.test.tsx index e8e47766d543..e5ab37fa5443 100644 --- a/packages/grid/x-data-grid/src/tests/filtering.DataGrid.test.tsx +++ b/packages/grid/x-data-grid/src/tests/filtering.DataGrid.test.tsx @@ -41,10 +41,22 @@ describe(' - Filter', () => { columns: [{ field: 'brand' }], }; + let disableEval = false; + + function testEval(fn: Function) { + return () => { + disableEval = false; + fn(); + disableEval = true; + fn(); + disableEval = false; + }; + } + function TestCase(props: Partial) { return (
- +
); } @@ -278,25 +290,27 @@ describe(' - 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"', () => {