Skip to content

Commit 20c3932

Browse files
sc-ruslanmatkovskyiRuslan Matkovskyi
and
Ruslan Matkovskyi
authored
[sitecore-jss-nextjs]: Updated logic to properly handle cases like [/]? #SXA-6320 (#1999)
* [sitecore-jss-nextjs]: Updated logic to properly handle cases like [/]? * CHANGELOG has been changed --------- Co-authored-by: Ruslan Matkovskyi <[email protected]>
1 parent 3b5b780 commit 20c3932

File tree

3 files changed

+107
-3
lines changed

3 files changed

+107
-3
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ Our versioning strategy is as follows:
1919

2020
* `[templates/nextjs-sxa]` Fixed font-awesome import issue in custom workspace configuration ([#1998](https://github.com/Sitecore/jss/pull/1998))
2121

22+
### 🐛 Bug Fixes
23+
24+
* `[sitecore-jss-nextjs]` Fixed handling of ? inside square brackets [] in regex patterns to prevent incorrect escaping ([#1999](https://github.com/Sitecore/jss/pull/1999))
25+
2226
## 22.3.0 / 22.3.1
2327

2428
### 🐛 Bug Fixes

packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.test.ts

+51
Original file line numberDiff line numberDiff line change
@@ -1411,6 +1411,57 @@ describe('RedirectsMiddleware', () => {
14111411
expect(finalRes).to.deep.equal(res);
14121412
expect(finalRes.status).to.equal(res.status);
14131413
});
1414+
1415+
it('should return 301 redirect when pattern has special symbols "?"', async () => {
1416+
const cloneUrl = () => Object.assign({}, req.nextUrl);
1417+
const url = {
1418+
clone: cloneUrl,
1419+
href: 'http://localhost:3000/found?a=1&w=1',
1420+
locale: 'en',
1421+
origin: 'http://localhost:3000',
1422+
search: '?a=1&w=1',
1423+
pathname: '/found',
1424+
};
1425+
1426+
const { res, req } = createTestRequestResponse({
1427+
response: { url },
1428+
request: {
1429+
nextUrl: {
1430+
pathname: '/not-found/',
1431+
search: '?a=1&w=1',
1432+
href: 'http://localhost:3000/not-found/?a=1&w=1',
1433+
locale: 'en',
1434+
origin: 'http://localhost:3000',
1435+
clone: cloneUrl,
1436+
},
1437+
},
1438+
});
1439+
setupRedirectStub(301);
1440+
1441+
const { finalRes, fetchRedirects, siteResolver } = await runTestWithRedirect(
1442+
{
1443+
pattern: '/[/]?not-found?a=1&w=1/',
1444+
target: '/found',
1445+
redirectType: REDIRECT_TYPE_301,
1446+
isQueryStringPreserved: true,
1447+
locale: 'en',
1448+
},
1449+
req
1450+
);
1451+
1452+
validateEndMessageDebugLog('redirects middleware end in %dms: %o', {
1453+
headers: {},
1454+
redirected: undefined,
1455+
status: 301,
1456+
url,
1457+
});
1458+
1459+
expect(siteResolver.getByHost).to.be.calledWith(hostname);
1460+
// eslint-disable-next-line no-unused-expressions
1461+
expect(fetchRedirects.called).to.be.true;
1462+
expect(finalRes).to.deep.equal(res);
1463+
expect(finalRes.status).to.equal(res.status);
1464+
});
14141465
});
14151466

14161467
describe('should redirect to normalized path when nextjs specific "path" query string parameter is provided', () => {

packages/sitecore-jss-nextjs/src/middleware/redirects-middleware.ts

+52-3
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,16 @@ export class RedirectsMiddleware extends MiddlewareBase {
199199
return modifyRedirects.length
200200
? modifyRedirects.find((redirect: RedirectInfo & { matchedQueryString?: string }) => {
201201
// Modify the redirect pattern to ignore the language prefix in the path
202-
redirect.pattern = redirect.pattern.replace(RegExp(`^[^]?/${language}/`, 'gi'), '');
202+
// And escapes non-special "?" characters in a string or regex.
203+
redirect.pattern = this.escapeNonSpecialQuestionMarks(
204+
redirect.pattern.replace(RegExp(`^[^]?/${language}/`, 'gi'), '')
205+
);
203206

204207
// Prepare the redirect pattern as a regular expression, making it more flexible for matching URLs
205208
redirect.pattern = `/^\/${redirect.pattern
206209
.replace(/^\/|\/$/g, '') // Removes leading and trailing slashes
207210
.replace(/^\^\/|\/\$$/g, '') // Removes unnecessary start (^) and end ($) anchors
208211
.replace(/^\^|\$$/g, '') // Further cleans up anchors
209-
.replace(/(?<!\\)\?/g, '\\?') // Escapes question marks in the pattern
210212
.replace(/\$\/gi$/g, '')}[\/]?$/i`; // Ensures the pattern allows an optional trailing slash
211213

212214
/**
@@ -272,7 +274,7 @@ export class RedirectsMiddleware extends MiddlewareBase {
272274
*/
273275
const splittedPathname = url.pathname
274276
.split('/')
275-
.filter((route) => route)
277+
.filter((route: string) => route)
276278
.map((route) => `path=${route}`);
277279

278280
/**
@@ -362,4 +364,51 @@ export class RedirectsMiddleware extends MiddlewareBase {
362364
].some(Boolean)
363365
);
364366
}
367+
368+
/**
369+
* Escapes non-special "?" characters in a string or regex.
370+
*
371+
* - For regular strings, it escapes all unescaped "?" characters by adding a backslash (`\`).
372+
* - For regex patterns (strings enclosed in `/.../`), it analyzes each "?" to determine if it has special meaning
373+
* (e.g., `?` in `(abc)?`, `.*?`) or is just a literal character. Only literal "?" characters are escaped.
374+
* @param {string} input - The input string or regex pattern.
375+
* @returns {string} - The modified string or regex with non-special "?" characters escaped.
376+
**/
377+
private escapeNonSpecialQuestionMarks(input: string): string {
378+
const regexPattern = /(?<!\\)\?/g; // Find unescaped "?" characters
379+
const isRegex = input.startsWith('/') && input.endsWith('/'); // Check if the string is a regex
380+
381+
if (!isRegex) {
382+
// If not a regex, escape all unescaped "?" characters
383+
return input.replace(regexPattern, '\\?');
384+
}
385+
386+
// If it's a regex, analyze each "?" character
387+
let result = '';
388+
let lastIndex = 0;
389+
390+
let match;
391+
while ((match = regexPattern.exec(input)) !== null) {
392+
const index = match.index; // Position of "?" in the string
393+
const before = input.slice(0, index).replace(/\s+$/, ''); // Context before "?"
394+
const lastChar = before.slice(-1); // Last character before "?"
395+
396+
// Determine if the "?" is a special regex symbol
397+
const isSpecialRegexSymbol = /[\.\*\+\)\[\]]$/.test(lastChar);
398+
399+
if (isSpecialRegexSymbol) {
400+
// If it's special, keep it as is
401+
result += input.slice(lastIndex, index + 1);
402+
} else {
403+
// If it's not special, escape it
404+
result += input.slice(lastIndex, index) + '\\?';
405+
}
406+
lastIndex = index + 1;
407+
}
408+
409+
// Append the remaining part of the string
410+
result += input.slice(lastIndex);
411+
412+
return result;
413+
}
365414
}

0 commit comments

Comments
 (0)