Skip to content

Commit

Permalink
fix: port escapeKuery from 8.16->7.17
Browse files Browse the repository at this point in the history
  • Loading branch information
logeekal committed Oct 30, 2024
1 parent 9fb1723 commit 0184821
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 92 deletions.
1 change: 1 addition & 0 deletions packages/kbn-es-query/src/kuery/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export { KQLSyntaxError } from './kuery_syntax_error';
export { nodeTypes, nodeBuilder } from './node_types';
export { fromKueryExpression } from './ast';
export type { DslQuery, KueryNode, KueryQueryOptions } from './types';
export { escapeKuery } from './utils/escape_kuery';
42 changes: 42 additions & 0 deletions packages/kbn-es-query/src/kuery/utils/escape_kuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { flow } from 'lodash';

/**
* Escapes backslashes and double-quotes. (Useful when putting a string in quotes to use as a value
* in a KQL expression. See the QuotedCharacter rule in kuery.peg.)
*/
export function escapeQuotes(str: string) {
return str.replace(/[\\"]/g, '\\$&');
}

/**
* Escapes a Kuery node value to ensure that special characters, operators, and whitespace do not result in a parsing error or unintended
* behavior when using the value as an argument for the `buildNode` function.
*/
export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace);

// See the SpecialCharacter rule in kuery.peg
function escapeSpecialCharacters(str: string) {
return str.replace(/[\\():<>"*]/g, '\\$&'); // $& means the whole matched string
}

// See the Keyword rule in kuery.peg
function escapeAndOr(str: string) {
return str.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3');
}

function escapeNot(str: string) {
return str.replace(/not(\s+)/gi, '\\$&');
}

// See the Space rule in kuery.peg
function escapeWhitespace(str: string) {
return str.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n');
}
144 changes: 67 additions & 77 deletions x-pack/plugins/timelines/public/components/utils/keury/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,84 +6,74 @@
*/

import expect from '@kbn/expect';
import { escapeKuery } from '.';
import { escapeQueryValue } from '.';

describe('Kuery escape', () => {
it('should not remove white spaces quotes', () => {
const value = ' netcat';
const expected = ' netcat';
expect(escapeKuery(value)).to.be(expected);
});

it('should escape quotes', () => {
const value = 'I said, "Hello."';
const expected = 'I said, \\"Hello.\\"';
expect(escapeKuery(value)).to.be(expected);
});

it('should escape special characters', () => {
const value = `This \\ has (a lot of) <special> characters, don't you *think*? "Yes."`;
const expected = `This \\ has (a lot of) <special> characters, don't you *think*? \\"Yes.\\"`;
expect(escapeKuery(value)).to.be(expected);
});

it('should NOT escape keywords', () => {
const value = 'foo and bar or baz not qux';
const expected = 'foo and bar or baz not qux';
expect(escapeKuery(value)).to.be(expected);
});

it('should NOT escape keywords next to each other', () => {
const value = 'foo and bar or not baz';
const expected = 'foo and bar or not baz';
expect(escapeKuery(value)).to.be(expected);
});

it('should not escape keywords without surrounding spaces', () => {
const value = 'And this has keywords, or does it not?';
const expected = 'And this has keywords, or does it not?';
expect(escapeKuery(value)).to.be(expected);
});
const TEST_QUERIES = [
{
description: 'should not remove white spaces quotes',
value: ' netcat',
expected: ' netcat',
},
{
description: 'should escape quotes',
value: 'I said, "Hello."',
expected: 'I said, \\"Hello.\\"',
},
{
description: 'should escape special characters',
value: `This \\ has (a lot of) <special> characters, don't you *think*? "Yes."`,
expected: `This \\\\ has \\(a lot of\\) \\<special\\> characters, don't you \\*think\\*? \\"Yes.\\"`,
},
{
description: 'should NOT escape keywords',
value: 'foo and bar or baz not qux',
expected: 'foo and bar or baz not qux',
},
{
description: 'should NOT escape keywords next to each other',
value: 'foo and bar or not baz',
expected: 'foo and bar or not baz',
},
{
description: 'should not escape keywords without surrounding spaces',
value: 'And this has keywords, or does it not?',
expected: 'And this has keywords, or does it not?',
},
{
description: 'should NOT escape uppercase keywords',
value: 'foo AND bar',
expected: 'foo AND bar',
},
{
description: 'should escape special characters and NOT keywords',
value: 'Hello, "world", and <nice> to meet you!',
expected: 'Hello, \\"world\\", and <nice> to meet you!',
},
{
description: 'should escape newlines and tabs',
value: 'This\nhas\tnewlines\r\nwith\ttabs',
expected: 'This\\nhas\\tnewlines\\r\\nwith\\ttabs',
},
{
description: 'should escape backslashes',
value: 'This\\has\\backslashes',
expected: 'This\\\\has\\\\backslashes',
},
{
description: 'should escape multiple backslashes and quotes',
value: 'This\\ has 2" quotes & \\ 2 "backslashes',
expected: 'This\\\\ has 2\\" quotes & \\\\ 2 \\"backslashes',
},
{
description: 'should escape all special character according to kuery.peg SpecialCharacter rule',
value: '\\():"*',
expected: '\\\\\\(\\)\\:\\"\\*',
},
];

it('should NOT escape uppercase keywords', () => {
const value = 'foo AND bar';
const expected = 'foo AND bar';
expect(escapeKuery(value)).to.be(expected);
});

it('should escape special characters and NOT keywords', () => {
const value = 'Hello, "world", and <nice> to meet you!';
const expected = 'Hello, \\"world\\", and <nice> to meet you!';
expect(escapeKuery(value)).to.be(expected);
});

it('should escape newlines and tabs', () => {
const value = 'This\nhas\tnewlines\r\nwith\ttabs';
const expected = 'This\\nhas\\tnewlines\\r\\nwith\\ttabs';
expect(escapeKuery(value)).to.be(expected);
});

it('should escape backslashes', () => {
const value = 'This\\has\\backslashes';
const expected = 'This\\\\has\\\\backslashes';
expect(escapeKuery(value)).to.be(expected);
});

it('should escape multiple backslashes and quotes', () => {
const value = 'This\\ has 2" quotes & \\ 2 "backslashes';
const expected = 'This\\\\ has 2\\" quotes & \\\\ 2 \\"backslashes';
expect(escapeKuery(value)).to.be(expected);
});

it('should escape all special character according to kuery.peg SpecialCharacter rule', () => {
/*
* Ref: packages/kbn-es-query/grammar/grammar.peggy
*
* SpecialCharacter
* = [\\():<>"*{}]
*/
const value = `\\():"*{}`;
const expected = `\\\\\\(\\)\\:\\"\\*\\{\\}`;
expect(escapeKuery(value)).to.be(expected);
describe('Kuery escape', () => {
it.each(TEST_QUERIES)('$description', ({ description, value, expected }) => {
const result = escapeQueryValue(value);
expect(result).to.be(`"${expected}"`);
});
});
17 changes: 2 additions & 15 deletions x-pack/plugins/timelines/public/components/utils/keury/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { isEmpty, isString, flow } from 'lodash/fp';
import { isEmpty, isString } from 'lodash/fp';
import {
buildEsQuery,
EsQueryConfig,
Expand All @@ -14,6 +14,7 @@ import {
IndexPatternBase,
Query,
toElasticsearchQuery,
escapeKuery,
} from '@kbn/es-query';

export const convertKueryToElasticSearchQuery = (
Expand Down Expand Up @@ -53,20 +54,6 @@ export const escapeQueryValue = (val: number | string = ''): string | number =>
return val;
};

const escapeWhitespace = (val: string) =>
val.replace(/\t/g, '\\t').replace(/\r/g, '\\r').replace(/\n/g, '\\n');

// See the SpecialCharacter rule in kuery.peg
const escapeSpecialCharacters = (val: string) => val.replace(/["\\\(\)\:\<\>\*\{\}]/g, '\\$&'); // $& means the whole matched string

// See the Keyword rule in kuery.peg
// I do not think that we need that anymore since we are doing a full match_phrase all the time now => return `"${escapeKuery(val)}"`;
// const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3');

// const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&');

export const escapeKuery = flow(escapeSpecialCharacters, escapeWhitespace);

export const convertToBuildEsQuery = ({
config,
indexPattern,
Expand Down

0 comments on commit 0184821

Please sign in to comment.