diff --git a/src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.test.ts b/src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.test.ts index 7ccb46093f9c2..3a71746630537 100644 --- a/src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.test.ts +++ b/src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.test.ts @@ -21,6 +21,11 @@ describe('tokens_utils', () => { const result = removeTrailingWhitespaces(url); expect(result).toBe(url); }); + it(`doesn't strip if the first character is whitespace`, () => { + const url = ' _search trailing'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe(url); + }); it(`removes any text after the first whitespace`, () => { const url = '_search some_text'; const result = removeTrailingWhitespaces(url); @@ -31,6 +36,49 @@ describe('tokens_utils', () => { const result = removeTrailingWhitespaces(url); expect(result).toBe(url); }); + it(`doesn't treat a question mark inside quotes as query string start`, () => { + const url = '_search/"?literal" trailing_text'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe('_search/"?literal"'); + }); + it(`does not strip unquoted spaces inside query values`, () => { + const url = 'myindex/_search?q=type:organisation AND elastic'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe(url); + }); + it.each([ + [ + 'keeps slashes inside query values', + 'myindex/_search?q=http://example.com/path AND elastic', + ], + ['keeps hashes inside query values', 'myindex/_search?q=tag#1 AND elastic'], + [ + 'keeps comment markers inside quoted query values', + 'myindex/_search?q="organisation // elastic" AND kibana', + ], + [ + 'uses the first question mark outside quotes as query string start', + 'myindex/"?literal"/_search?q=type:organisation AND elastic', + ], + ])('%s', (_, url) => { + const result = removeTrailingWhitespaces(url); + expect(result).toBe(url); + }); + it(`strips inline comment after unquoted query spaces`, () => { + const url = 'myindex/_search?q=type:organisation AND elastic // filter orgs'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe('myindex/_search?q=type:organisation AND elastic'); + }); + it(`strips inline comment after mixed whitespace in query values`, () => { + const url = 'myindex/_search?q=type:organisation AND elastic \t// filter orgs'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe('myindex/_search?q=type:organisation AND elastic'); + }); + it(`strips hash comment after unquoted query spaces`, () => { + const url = 'myindex/_search?q=type:organisation AND elastic # filter orgs'; + const result = removeTrailingWhitespaces(url); + expect(result).toBe('myindex/_search?q=type:organisation AND elastic'); + }); }); describe('parseLine', () => { @@ -48,6 +96,24 @@ describe('tokens_utils', () => { expect(urlPathTokens).toEqual(['_search']); expect(urlParamsTokens[0]).toEqual(['query', '"test1 test2 test3"']); }); + it('preserves unquoted spaces inside query values', () => { + const { method, url, urlPathTokens, urlParamsTokens } = parseLine( + 'GET myindex/_search?q=type:organisation AND elastic' + ); + expect(method).toBe('GET'); + expect(url).toBe('myindex/_search?q=type:organisation AND elastic'); + expect(urlPathTokens).toEqual(['myindex', '_search']); + expect(urlParamsTokens[0]).toEqual(['q', 'type:organisation AND elastic']); + }); + it('uses the first question mark outside quotes to parse url params', () => { + const { method, url, urlPathTokens, urlParamsTokens } = parseLine( + 'GET myindex/"?literal"/_search?q=type:organisation AND elastic' + ); + expect(method).toBe('GET'); + expect(url).toBe('myindex/"?literal"/_search?q=type:organisation AND elastic'); + expect(urlPathTokens).toEqual(['myindex', '"?literal"', '_search']); + expect(urlParamsTokens[0]).toEqual(['q', 'type:organisation AND elastic']); + }); it('works with multiple whitespaces', () => { const { method, url, urlPathTokens, urlParamsTokens } = parseLine( ' GET _search?query="test1 test2 test3" // comment' @@ -197,5 +263,12 @@ describe('tokens_utils', () => { const result = parseUrl(url); expect(result.urlPathTokens).toEqual(['_search', 'test']); }); + + it('uses the first question mark outside quotes for url params', () => { + const url = 'myindex/"?literal"/_search?q=type:organisation AND elastic'; + const result = parseUrl(url); + expect(result.urlPathTokens).toEqual(['myindex', '"?literal"', '_search']); + expect(result.urlParamsTokens[0]).toEqual(['q', 'type:organisation AND elastic']); + }); }); }); diff --git a/src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.ts b/src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.ts index 7db1b19977f33..062ba3731e5c0 100644 --- a/src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.ts +++ b/src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.ts @@ -54,15 +54,15 @@ export const parseUrl = ( } => { let urlPathTokens: ParsedLineTokens['urlPathTokens'] = []; let urlParamsTokens: ParsedLineTokens['urlParamsTokens'] = []; - const urlParts = url.split(questionMarkRegex); + const queryStringStart = getQueryStringStart(url); // 1st part is the url path - const urlPath = urlParts[0]; + const urlPath = queryStringStart >= 0 ? url.slice(0, queryStringStart) : url; // try to parse into url path tokens (split on slashes, only keep non-empty tokens) if (urlPath) { urlPathTokens = urlPath.split(slashesRegex).filter(Boolean); } // 2nd part is the url params - const urlParams = urlParts[1]; + const urlParams = queryStringStart >= 0 ? url.slice(queryStringStart + 1) : undefined; // try to parse into url param tokens if (urlParams) { urlParamsTokens = urlParams.split(ampersandRegex).map((urlParamsPart) => { @@ -409,28 +409,41 @@ export const parseBody = (value: string): string[] => { }; /* - * This functions removes any trailing inline comments, for example + * This function removes trailing text after the request URL while preserving + * spaces in query values. + * For example: * "_search // comment" -> "_search" - * Ideally the parser would do that, but currently they are included in url. + * "_search?q=foo AND bar // comment" -> "_search?q=foo AND bar" + * Ideally the parser would do that, but currently inline comments are included in url. */ export const removeTrailingWhitespaces = (url: string): string => { - let index = 0; - let whitespaceIndex = -1; - let isQueryParam = false; - let char = url[index]; - while (char) { - if (char === '"') { - isQueryParam = !isQueryParam; - } else if (char === ' ' && !isQueryParam) { - whitespaceIndex = index; - break; - } - index++; - char = url[index]; + if (url.startsWith(' ')) { + return url; } - if (whitespaceIndex > 0) { - return url.slice(0, whitespaceIndex); + + const queryStringStart = getQueryStringStart(url); + let isInQuotes = false; + + for (let index = 0; index < url.length; index++) { + const char = url[index]; + + if (isDoubleQuote(char)) { + isInQuotes = !isInQuotes; + continue; + } + + if (isInQuotes || char !== ' ') { + continue; + } + + const isOutsideQueryString = queryStringStart < 0 || index < queryStringStart; + const shouldTrimTrailingText = + index > 0 && (isOutsideQueryString || isInlineCommentStart(url, index)); + if (shouldTrimTrailingText) { + return url.slice(0, index); + } } + return url; }; @@ -476,6 +489,30 @@ const isHashChar = (char: string): boolean => { const isSlash = (char: string): boolean => { return char === '/'; }; +const isWhitespaceChar = (char: string | undefined): boolean => { + return Boolean(char && whitespacesRegex.test(char)); +}; +const getQueryStringStart = (url: string): number => { + let isInQuotes = false; + + for (let index = 0; index < url.length; index++) { + const char = url[index]; + if (isDoubleQuote(char)) { + isInQuotes = !isInQuotes; + } else if (char === '?' && !isInQuotes) { + return index; + } + } + + return -1; +}; +const isInlineCommentStart = (url: string, index: number): boolean => { + let commentIndex = index; + while (isWhitespaceChar(url[commentIndex])) { + commentIndex++; + } + return url.startsWith('#', commentIndex) || url.startsWith('//', commentIndex); +}; const isStar = (char: string): boolean => { return char === '*'; };