Commit 05cb85fe by Hugo Häggmark Committed by GitHub

Table: Matches column names with unescaped regex characters (#21164)

* Table: Matches column names with unescaped regex characters
Fixes #21106

* Chore: Cleans up unused code
parent 26aa1f0c
import { stringToJsRegex, stringToMs } from './string';
import { escapeStringForRegex, stringToJsRegex, stringToMs, unEscapeStringFromRegex } from './string';
describe('stringToJsRegex', () => {
it('should just return string as RegEx if it does not start as a regex', () => {
const output = stringToJsRegex('validRegexp');
expect(output).toBeInstanceOf(RegExp);
});
it('should parse the valid regex value', () => {
const output = stringToJsRegex('/validRegexp/');
expect(output).toBeInstanceOf(RegExp);
......@@ -51,3 +56,35 @@ describe('stringToMs', () => {
}).toThrow();
});
});
describe('escapeStringForRegex', () => {
describe('when using a string with special chars', () => {
it('then all special chars should be escaped', () => {
const result = escapeStringForRegex('([{}])|*+-.?<>#&^$');
expect(result).toBe('\\(\\[\\{\\}\\]\\)\\|\\*\\+\\-\\.\\?\\<\\>\\#\\&\\^\\$');
});
});
describe('when using a string without special chars', () => {
it('then nothing should change', () => {
const result = escapeStringForRegex('some string 123');
expect(result).toBe('some string 123');
});
});
});
describe('unEscapeStringFromRegex', () => {
describe('when using a string with escaped special chars', () => {
it('then all special chars should be unescaped', () => {
const result = unEscapeStringFromRegex('\\(\\[\\{\\}\\]\\)\\|\\*\\+\\-\\.\\?\\<\\>\\#\\&\\^\\$');
expect(result).toBe('([{}])|*+-.?<>#&^$');
});
});
describe('when using a string without escaped special chars', () => {
it('then nothing should change', () => {
const result = unEscapeStringFromRegex('some string 123');
expect(result).toBe('some string 123');
});
});
});
const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$'];
export const escapeStringForRegex = (value: string) => {
if (!value) {
return value;
}
return specialChars.reduce((escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar), value);
};
export const unEscapeStringFromRegex = (value: string) => {
if (!value) {
return value;
}
return specialChars.reduce((escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar), value);
};
export function stringStartsAsRegEx(str: string): boolean {
if (!str) {
return false;
}
return str[0] === '/';
}
export function stringToJsRegex(str: string): RegExp {
if (str[0] !== '/') {
return new RegExp('^' + str + '$');
if (!stringStartsAsRegEx(str)) {
return new RegExp(`^${str}$`);
}
const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
......
import React, { forwardRef } from 'react';
const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$'];
export const escapeStringForRegex = (value: string) => {
if (!value) {
return value;
}
const newValue = specialChars.reduce(
(escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar),
value
);
return newValue;
};
export const unEscapeStringFromRegex = (value: string) => {
if (!value) {
return value;
}
const newValue = specialChars.reduce(
(escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar),
value
);
return newValue;
};
import { escapeStringForRegex, unEscapeStringFromRegex } from '@grafana/data';
export interface Props {
value: string | undefined;
......
......@@ -4,12 +4,11 @@ import React from 'react';
import { components } from '@torkelo/react-select';
// @ts-ignore
import AsyncSelect from '@torkelo/react-select/lib/Async';
import { escapeStringForRegex } from '@grafana/data';
// Components
import { TagOption } from './TagOption';
import { TagBadge } from './TagBadge';
import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui';
import { escapeStringForRegex } from '../FilterInput/FilterInput';
import { IndicatorsContainer, NoOptionsMessage, resetSelectStyles } from '@grafana/ui';
export interface TermCount {
term: string;
......
import _ from 'lodash';
import {
dateTime,
getValueFormat,
escapeStringForRegex,
formattedValueToString,
getColorFromHexRgbOrName,
getValueFormat,
GrafanaThemeType,
stringToJsRegex,
ScopedVars,
formattedValueToString,
stringStartsAsRegEx,
stringToJsRegex,
unEscapeStringFromRegex,
} from '@grafana/data';
import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { TableRenderModel, ColumnRender } from './types';
import { ColumnRender, TableRenderModel } from './types';
export class TableRenderer {
formatters: any[];
......@@ -44,7 +47,10 @@ export class TableRenderer {
for (let i = 0; i < this.panel.styles.length; i++) {
const style = this.panel.styles[i];
const regex = stringToJsRegex(style.pattern);
const escapedPattern = stringStartsAsRegEx(style.pattern)
? style.pattern
: escapeStringForRegex(unEscapeStringFromRegex(style.pattern));
const regex = stringToJsRegex(escapedPattern);
if (column.text.match(regex)) {
column.style = style;
......
import _ from 'lodash';
import TableModel from 'app/core/table_model';
import { TableRenderer } from '../renderer';
import { getColorDefinitionByName } from '@grafana/data';
import { ScopedVars } from '@grafana/data';
import { getColorDefinitionByName, ScopedVars } from '@grafana/data';
import { ColumnRender } from '../types';
const sanitize = (value: any): string => {
return 'sanitized';
};
const templateSrv = {
replace: (value: any, scopedVars: ScopedVars) => {
if (scopedVars) {
// For testing variables replacement in link
_.each(scopedVars, (val, key) => {
value = value.replace('$' + key, val.value);
});
}
return value;
},
};
describe('when rendering table', () => {
const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
......@@ -173,22 +188,6 @@ describe('when rendering table', () => {
],
};
const sanitize = (value: any): string => {
return 'sanitized';
};
const templateSrv = {
replace: (value: any, scopedVars: ScopedVars) => {
if (scopedVars) {
// For testing variables replacement in link
_.each(scopedVars, (val, key) => {
value = value.replace('$' + key, val.value);
});
}
return value;
},
};
//@ts-ignore
const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv);
......@@ -407,6 +406,51 @@ describe('when rendering table', () => {
});
});
describe('when rendering table with different patterns', () => {
it.each`
column | pattern | expected
${'Requests (Failed)'} | ${'/Requests \\(Failed\\)/'} | ${'<td>1.230 s</td>'}
${'Requests (Failed)'} | ${'/(Req)uests \\(Failed\\)/'} | ${'<td>1.230 s</td>'}
${'Requests (Failed)'} | ${'Requests (Failed)'} | ${'<td>1.230 s</td>'}
${'Requests (Failed)'} | ${'Requests \\(Failed\\)'} | ${'<td>1.230 s</td>'}
${'Requests (Failed)'} | ${'/.*/'} | ${'<td>1.230 s</td>'}
${'Some other column'} | ${'/.*/'} | ${'<td>1.230 s</td>'}
${'Requests (Failed)'} | ${'/Requests (Failed)/'} | ${'<td>1230</td>'}
${'Requests (Failed)'} | ${'Response (Failed)'} | ${'<td>1230</td>'}
`(
'number column should be formatted for a column:$column with the pattern:$pattern',
({ column, pattern, expected }) => {
const table = new TableModel();
table.columns = [{ text: 'Time' }, { text: column }];
table.rows = [[1388556366666, 1230]];
const panel = {
pageSize: 10,
styles: [
{
pattern: 'Time',
type: 'date',
format: 'LLL',
alias: 'Timestamp',
},
{
pattern: pattern,
type: 'number',
unit: 'ms',
decimals: 3,
alias: pattern,
},
],
};
//@ts-ignore
const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv);
const html = renderer.renderCell(1, 0, 1230);
expect(html).toBe(expected);
}
);
});
function normalize(str: string) {
return str.replace(/\s+/gm, ' ').trim();
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment