Skip to content

Commit 9b383ab

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

2 files changed

Lines changed: 130 additions & 20 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 trailing';
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: 57 additions & 20 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,41 @@ 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) {
422-
if (char === '"') {
423-
isQueryParam = !isQueryParam;
424-
} else if (char === ' ' && !isQueryParam) {
425-
whitespaceIndex = index;
426-
break;
427-
}
428-
index++;
429-
char = url[index];
420+
if (url.startsWith(' ')) {
421+
return url;
430422
}
431-
if (whitespaceIndex > 0) {
432-
return url.slice(0, whitespaceIndex);
423+
424+
const queryStringStart = getQueryStringStart(url);
425+
let isInQuotes = false;
426+
427+
for (let index = 0; index < url.length; index++) {
428+
const char = url[index];
429+
430+
if (isDoubleQuote(char)) {
431+
isInQuotes = !isInQuotes;
432+
continue;
433+
}
434+
435+
if (isInQuotes || char !== ' ') {
436+
continue;
437+
}
438+
439+
const isOutsideQueryString = queryStringStart < 0 || index < queryStringStart;
440+
const shouldTrimTrailingText =
441+
index > 0 && (isOutsideQueryString || isInlineCommentStart(url, index));
442+
if (shouldTrimTrailingText) {
443+
return url.slice(0, index);
444+
}
433445
}
446+
434447
return url;
435448
};
436449

@@ -476,6 +489,30 @@ const isHashChar = (char: string): boolean => {
476489
const isSlash = (char: string): boolean => {
477490
return char === '/';
478491
};
492+
const isWhitespaceChar = (char: string | undefined): boolean => {
493+
return Boolean(char && whitespacesRegex.test(char));
494+
};
495+
const getQueryStringStart = (url: string): number => {
496+
let isInQuotes = false;
497+
498+
for (let index = 0; index < url.length; index++) {
499+
const char = url[index];
500+
if (isDoubleQuote(char)) {
501+
isInQuotes = !isInQuotes;
502+
} else if (char === '?' && !isInQuotes) {
503+
return index;
504+
}
505+
}
506+
507+
return -1;
508+
};
509+
const isInlineCommentStart = (url: string, index: number): boolean => {
510+
let commentIndex = index;
511+
while (isWhitespaceChar(url[commentIndex])) {
512+
commentIndex++;
513+
}
514+
return url.startsWith('#', commentIndex) || url.startsWith('//', commentIndex);
515+
};
479516
const isStar = (char: string): boolean => {
480517
return char === '*';
481518
};

0 commit comments

Comments
 (0)