Skip to content

Commit 6e2cc4f

Browse files
kapral18cursoragent
andcommitted
[Console] Preserve spaces in URL query strings
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 960eee9 commit 6e2cc4f

2 files changed

Lines changed: 139 additions & 19 deletions

File tree

src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ describe('tokens_utils', () => {
2121
const result = removeTrailingWhitespaces(url);
2222
expect(result).toBe(url);
2323
});
24+
it(`doesn't strip if the first character is whitespace`, () => {
25+
const url = ' _search';
26+
const result = removeTrailingWhitespaces(url);
27+
expect(result).toBe(url);
28+
});
2429
it(`removes any text after the first whitespace`, () => {
2530
const url = '_search some_text';
2631
const result = removeTrailingWhitespaces(url);
@@ -31,6 +36,49 @@ describe('tokens_utils', () => {
3136
const result = removeTrailingWhitespaces(url);
3237
expect(result).toBe(url);
3338
});
39+
it(`doesn't treat a question mark inside quotes as query string start`, () => {
40+
const url = '_search/"?literal" trailing_text';
41+
const result = removeTrailingWhitespaces(url);
42+
expect(result).toBe('_search/"?literal"');
43+
});
44+
it(`does not strip unquoted spaces inside query values`, () => {
45+
const url = 'myindex/_search?q=type:organisation AND elastic';
46+
const result = removeTrailingWhitespaces(url);
47+
expect(result).toBe(url);
48+
});
49+
it.each([
50+
[
51+
'keeps slashes inside query values',
52+
'myindex/_search?q=http://example.com/path AND elastic',
53+
],
54+
['keeps hashes inside query values', 'myindex/_search?q=tag#1 AND elastic'],
55+
[
56+
'keeps comment markers inside quoted query values',
57+
'myindex/_search?q="organisation // elastic" AND kibana',
58+
],
59+
[
60+
'uses the first question mark outside quotes as query string start',
61+
'myindex/"?literal"/_search?q=type:organisation AND elastic',
62+
],
63+
])('%s', (_, url) => {
64+
const result = removeTrailingWhitespaces(url);
65+
expect(result).toBe(url);
66+
});
67+
it(`strips inline comment after unquoted query spaces`, () => {
68+
const url = 'myindex/_search?q=type:organisation AND elastic // filter orgs';
69+
const result = removeTrailingWhitespaces(url);
70+
expect(result).toBe('myindex/_search?q=type:organisation AND elastic');
71+
});
72+
it(`strips inline comment after mixed whitespace in query values`, () => {
73+
const url = 'myindex/_search?q=type:organisation AND elastic \t// filter orgs';
74+
const result = removeTrailingWhitespaces(url);
75+
expect(result).toBe('myindex/_search?q=type:organisation AND elastic');
76+
});
77+
it(`strips hash comment after unquoted query spaces`, () => {
78+
const url = 'myindex/_search?q=type:organisation AND elastic # filter orgs';
79+
const result = removeTrailingWhitespaces(url);
80+
expect(result).toBe('myindex/_search?q=type:organisation AND elastic');
81+
});
3482
});
3583

3684
describe('parseLine', () => {
@@ -48,6 +96,24 @@ describe('tokens_utils', () => {
4896
expect(urlPathTokens).toEqual(['_search']);
4997
expect(urlParamsTokens[0]).toEqual(['query', '"test1 test2 test3"']);
5098
});
99+
it('preserves unquoted spaces inside query values', () => {
100+
const { method, url, urlPathTokens, urlParamsTokens } = parseLine(
101+
'GET myindex/_search?q=type:organisation AND elastic'
102+
);
103+
expect(method).toBe('GET');
104+
expect(url).toBe('myindex/_search?q=type:organisation AND elastic');
105+
expect(urlPathTokens).toEqual(['myindex', '_search']);
106+
expect(urlParamsTokens[0]).toEqual(['q', 'type:organisation AND elastic']);
107+
});
108+
it('uses the first question mark outside quotes to parse url params', () => {
109+
const { method, url, urlPathTokens, urlParamsTokens } = parseLine(
110+
'GET myindex/"?literal"/_search?q=type:organisation AND elastic'
111+
);
112+
expect(method).toBe('GET');
113+
expect(url).toBe('myindex/"?literal"/_search?q=type:organisation AND elastic');
114+
expect(urlPathTokens).toEqual(['myindex', '"?literal"', '_search']);
115+
expect(urlParamsTokens[0]).toEqual(['q', 'type:organisation AND elastic']);
116+
});
51117
it('works with multiple whitespaces', () => {
52118
const { method, url, urlPathTokens, urlParamsTokens } = parseLine(
53119
' GET _search?query="test1 test2 test3" // comment'
@@ -197,5 +263,12 @@ describe('tokens_utils', () => {
197263
const result = parseUrl(url);
198264
expect(result.urlPathTokens).toEqual(['_search', 'test']);
199265
});
266+
267+
it('uses the first question mark outside quotes for url params', () => {
268+
const url = 'myindex/"?literal"/_search?q=type:organisation AND elastic';
269+
const result = parseUrl(url);
270+
expect(result.urlPathTokens).toEqual(['myindex', '"?literal"', '_search']);
271+
expect(result.urlParamsTokens[0]).toEqual(['q', 'type:organisation AND elastic']);
272+
});
200273
});
201274
});

src/platform/plugins/shared/console/public/application/containers/editor/utils/tokens_utils.ts

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ export const parseUrl = (
5454
} => {
5555
let urlPathTokens: ParsedLineTokens['urlPathTokens'] = [];
5656
let urlParamsTokens: ParsedLineTokens['urlParamsTokens'] = [];
57-
const urlParts = url.split(questionMarkRegex);
57+
const queryStringStart = getQueryStringStart(url);
5858
// 1st part is the url path
59-
const urlPath = urlParts[0];
59+
const urlPath = queryStringStart >= 0 ? url.slice(0, queryStringStart) : url;
6060
// try to parse into url path tokens (split on slashes, only keep non-empty tokens)
6161
if (urlPath) {
6262
urlPathTokens = urlPath.split(slashesRegex).filter(Boolean);
6363
}
6464
// 2nd part is the url params
65-
const urlParams = urlParts[1];
65+
const urlParams = queryStringStart >= 0 ? url.slice(queryStringStart + 1) : undefined;
6666
// try to parse into url param tokens
6767
if (urlParams) {
6868
urlParamsTokens = urlParams.split(ampersandRegex).map((urlParamsPart) => {
@@ -409,28 +409,43 @@ export const parseBody = (value: string): string[] => {
409409
};
410410

411411
/*
412-
* This functions removes any trailing inline comments, for example
412+
* This function removes trailing text after the request URL while preserving
413+
* spaces in query values.
414+
* For example:
413415
* "_search // comment" -> "_search"
414-
* Ideally the parser would do that, but currently they are included in url.
416+
* "_search?q=foo AND bar // comment" -> "_search?q=foo AND bar"
417+
* Ideally the parser would do that, but currently inline comments are included in url.
415418
*/
416419
export const removeTrailingWhitespaces = (url: string): string => {
417-
let index = 0;
418-
let whitespaceIndex = -1;
419-
let isQueryParam = false;
420-
let char = url[index];
421-
while (char) {
420+
let isInQuotes = false;
421+
let isInQueryString = false;
422+
423+
for (let index = 0; index < url.length; index++) {
424+
const char = url[index];
425+
422426
if (char === '"') {
423-
isQueryParam = !isQueryParam;
424-
} else if (char === ' ' && !isQueryParam) {
425-
whitespaceIndex = index;
426-
break;
427+
isInQuotes = !isInQuotes;
428+
continue;
429+
}
430+
431+
if (isInQuotes) {
432+
continue;
433+
}
434+
435+
if (char === '?') {
436+
isInQueryString = true;
437+
continue;
438+
}
439+
440+
if (char !== ' ') {
441+
continue;
442+
}
443+
444+
if (index > 0 && (!isInQueryString || isInlineCommentStart(url, index))) {
445+
return url.slice(0, index);
427446
}
428-
index++;
429-
char = url[index];
430-
}
431-
if (whitespaceIndex > 0) {
432-
return url.slice(0, whitespaceIndex);
433447
}
448+
434449
return url;
435450
};
436451

@@ -476,6 +491,38 @@ const isHashChar = (char: string): boolean => {
476491
const isSlash = (char: string): boolean => {
477492
return char === '/';
478493
};
494+
const isWhitespaceChar = (char: string | undefined): boolean => {
495+
return Boolean(char && whitespacesRegex.test(char));
496+
};
497+
const getQueryStringStart = (url: string): number => {
498+
let isInQuotes = false;
499+
500+
for (let index = 0; index < url.length; index++) {
501+
const char = url[index];
502+
if (isDoubleQuote(char)) {
503+
isInQuotes = !isInQuotes;
504+
} else if (char === '?' && !isInQuotes) {
505+
return index;
506+
}
507+
}
508+
509+
return -1;
510+
};
511+
const isInlineCommentStart = (url: string, index: number): boolean => {
512+
let commentIndex = index;
513+
while (isWhitespaceChar(url[commentIndex])) {
514+
commentIndex++;
515+
}
516+
const firstCommentChar = url[commentIndex];
517+
if (!firstCommentChar) {
518+
return false;
519+
}
520+
const secondCommentChar = url[commentIndex + 1];
521+
return (
522+
isHashChar(firstCommentChar) ||
523+
(isSlash(firstCommentChar) && Boolean(secondCommentChar && isSlash(secondCommentChar)))
524+
);
525+
};
479526
const isStar = (char: string): boolean => {
480527
return char === '*';
481528
};

0 commit comments

Comments
 (0)