@sitecore-content-sdk/content 2.2.1 → 2.2.2-canary.20260727010537
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/site/index.js +15 -0
- package/dist/cjs/site/redirect-utils.js +208 -0
- package/dist/esm/site/index.js +1 -0
- package/dist/esm/site/redirect-utils.js +197 -0
- package/package.json +1 -1
- package/types/site/index.d.ts +1 -0
- package/types/site/index.d.ts.map +1 -1
- package/types/site/redirect-utils.d.ts +99 -0
- package/types/site/redirect-utils.d.ts.map +1 -0
package/dist/cjs/site/index.js
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
2
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
17
|
exports.SiteResolver = exports.getHostnameFromHostHeader = exports.SITE_KEY = exports.SITE_PREFIX = exports.normalizeSiteRewrite = exports.getSiteRewriteData = exports.getSiteRewrite = exports.SitePathService = exports.SiteInfoService = exports.ErrorPagesService = exports.SitemapXmlService = exports.RedirectsService = exports.REDIRECT_TYPE_SERVER_TRANSFER = exports.REDIRECT_TYPE_302 = exports.REDIRECT_TYPE_301 = exports.RobotsService = void 0;
|
|
4
18
|
var robots_service_1 = require("./robots-service");
|
|
@@ -23,5 +37,6 @@ Object.defineProperty(exports, "normalizeSiteRewrite", { enumerable: true, get:
|
|
|
23
37
|
Object.defineProperty(exports, "SITE_PREFIX", { enumerable: true, get: function () { return utils_1.SITE_PREFIX; } });
|
|
24
38
|
Object.defineProperty(exports, "SITE_KEY", { enumerable: true, get: function () { return utils_1.SITE_KEY; } });
|
|
25
39
|
Object.defineProperty(exports, "getHostnameFromHostHeader", { enumerable: true, get: function () { return utils_1.getHostnameFromHostHeader; } });
|
|
40
|
+
__exportStar(require("./redirect-utils"), exports);
|
|
26
41
|
var site_resolver_1 = require("./site-resolver");
|
|
27
42
|
Object.defineProperty(exports, "SiteResolver", { enumerable: true, get: function () { return site_resolver_1.SiteResolver; } });
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.processRelativeUrlTarget = exports.processAbsoluteUrlTarget = exports.resolveRedirectTarget = exports.isAbsoluteTarget = exports.safeCompileRedirectPattern = exports.matchRedirectItemRedirect = exports.matchFromRedirectMapRedirect = exports.breakDownPath = void 0;
|
|
4
|
+
const tools_1 = require("@sitecore-content-sdk/core/tools");
|
|
5
|
+
const REGEXP_CONTEXT_SITE_LANG = /\$siteLang/i;
|
|
6
|
+
const REGEXP_ABSOLUTE_URL = /^(?:[a-z]+:)?\/\//i;
|
|
7
|
+
/**
|
|
8
|
+
* Splits a URL path into its (optional) leading locale segment, the remaining
|
|
9
|
+
* locale-less path, and the query string. Locales are compared case-insensitively.
|
|
10
|
+
* A leading slash is always ensured on `nonLocalePath`; any trailing slash is preserved
|
|
11
|
+
* so slash-sensitive regex redirect rules can still be matched downstream.
|
|
12
|
+
* @param {string[]} configuredLocales configured site locales
|
|
13
|
+
* @param {string} urlPath path (optionally including a `?query`)
|
|
14
|
+
* @returns {ProcessedPath} broken-down path parts
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
const breakDownPath = (configuredLocales, urlPath) => {
|
|
18
|
+
const urlArray = urlPath.split('?');
|
|
19
|
+
const urlQs = urlArray[1];
|
|
20
|
+
// collapse duplicate slashes so degenerate inputs (`//`, `///`) reduce to `/`,
|
|
21
|
+
// while a single meaningful trailing slash is preserved
|
|
22
|
+
const nonQsPath = urlArray[0].replace(/\/{2,}/g, '/');
|
|
23
|
+
const pathArray = nonQsPath.startsWith('/')
|
|
24
|
+
? nonQsPath.slice(1).split('/')
|
|
25
|
+
: nonQsPath.split('/');
|
|
26
|
+
const [maybeLocale, maybeNonLocale] = [pathArray[0], pathArray.slice(1).join('/')];
|
|
27
|
+
const lowerCaseMaybeLocale = maybeLocale.toLowerCase();
|
|
28
|
+
// locales must be compared case insensitively, variations are abundant and typos can be there
|
|
29
|
+
return configuredLocales.find((configuredLocale) => configuredLocale.toLowerCase() === lowerCaseMaybeLocale)
|
|
30
|
+
? {
|
|
31
|
+
nonLocalePath: `/${maybeNonLocale}`,
|
|
32
|
+
queryString: urlQs,
|
|
33
|
+
locale: maybeLocale,
|
|
34
|
+
}
|
|
35
|
+
: {
|
|
36
|
+
nonLocalePath: nonQsPath.startsWith('/') ? nonQsPath : `/${nonQsPath}`,
|
|
37
|
+
queryString: urlQs,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
exports.breakDownPath = breakDownPath;
|
|
41
|
+
/**
|
|
42
|
+
* Matches redirect-map rules (rules without a `locale` field) against the incoming request.
|
|
43
|
+
* The incoming path is provided pre-split as {@link ProcessedPath}, so matching starts from the
|
|
44
|
+
* guaranteed locale-less `nonLocalePath` and rebuilds the locale-prefixed variant from it. This
|
|
45
|
+
* avoids the ambiguity of a raw pathname that may or may not already carry a locale segment.
|
|
46
|
+
* @param {RedirectResult[]} redirects All redirects from the service (locale-scoped entries are filtered out).
|
|
47
|
+
* @param {string} requestLocale locale of the current request
|
|
48
|
+
* @param {ProcessedPath} incomingPathData broken-down incoming path (locale-less path and query)
|
|
49
|
+
* @returns {RedirectResult | undefined} First matching redirect or undefined.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
const matchFromRedirectMapRedirect = (redirects, requestLocale, incomingPathData) => {
|
|
53
|
+
const redirectMapRedirects = redirects.filter((redirect) => !redirect.locale);
|
|
54
|
+
// start from the locale-less path and rebuild the locale-prefixed variant from it
|
|
55
|
+
// (keep root as `/` so anchored `^/...` patterns still match). Trailing slashes are preserved.
|
|
56
|
+
const nonLocalePath = incomingPathData.nonLocalePath.toLowerCase() || '/';
|
|
57
|
+
const localePath = `/${requestLocale.toLowerCase()}${nonLocalePath}`;
|
|
58
|
+
const incomingQS = incomingPathData.queryString || '';
|
|
59
|
+
// regex rules are matched against the raw candidates (trailing slash preserved so slash-sensitive
|
|
60
|
+
// patterns still match); static rules ignore trailing slashes, so they use trimmed candidates
|
|
61
|
+
const pathCandidates = [nonLocalePath, localePath];
|
|
62
|
+
const staticCandidates = pathCandidates.map((path) => path.replace(/\/+$/g, '') || '/');
|
|
63
|
+
return redirectMapRedirects.find((redirect) => {
|
|
64
|
+
// process static URL (non-regex) rules
|
|
65
|
+
if ((0, tools_1.isRegexOrUrl)(redirect.pattern) === 'url') {
|
|
66
|
+
const [rawPatternPath, patternQS] = (redirect.pattern.endsWith('/') ? redirect.pattern.slice(0, -1) : redirect.pattern).split('?');
|
|
67
|
+
// candidates are already lowercased; lowercase the pattern path to keep locale (and route)
|
|
68
|
+
// comparison case-insensitive
|
|
69
|
+
const patternPath = rawPatternPath.toLowerCase();
|
|
70
|
+
return (staticCandidates.includes(patternPath) &&
|
|
71
|
+
(!patternQS ||
|
|
72
|
+
(0, tools_1.areURLSearchParamsEqual)(new URLSearchParams(patternQS), new URLSearchParams(incomingQS))));
|
|
73
|
+
}
|
|
74
|
+
// process regex rules
|
|
75
|
+
const regex = (0, exports.safeCompileRedirectPattern)(redirect.pattern);
|
|
76
|
+
if (!regex) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const testRegex = (value) => {
|
|
80
|
+
regex.lastIndex = 0;
|
|
81
|
+
return regex.test(value);
|
|
82
|
+
};
|
|
83
|
+
// remove duplicates
|
|
84
|
+
const regexCandidates = [...pathCandidates, ...staticCandidates].filter((candidate, index, array) => array.indexOf(candidate) === index);
|
|
85
|
+
const matchedPath = regexCandidates.find((candidate) => testRegex(candidate));
|
|
86
|
+
const matchedPathWithQuery = incomingQS
|
|
87
|
+
? regexCandidates.find((candidate) => testRegex(`${candidate}?${incomingQS}`))
|
|
88
|
+
: undefined;
|
|
89
|
+
// Save the matched path/query (if found) into the redirect object
|
|
90
|
+
redirect.matchedQueryString = matchedPathWithQuery ? `?${incomingQS}` : '';
|
|
91
|
+
redirect.matchedPath = matchedPath || matchedPathWithQuery || '';
|
|
92
|
+
return !!(matchedPath || matchedPathWithQuery);
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
exports.matchFromRedirectMapRedirect = matchFromRedirectMapRedirect;
|
|
96
|
+
/**
|
|
97
|
+
* Processes redirect rules from redirect items (language-versioned)
|
|
98
|
+
* @param {RedirectResult[]} redirects redirect entries from Edge
|
|
99
|
+
* @param {string} locale current request locale
|
|
100
|
+
* @param {string} nonLocalePath current request path with locale prefix stripped
|
|
101
|
+
* @returns {RedirectResult | undefined} matched redirect item redirect result or undefined
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
const matchRedirectItemRedirect = (redirects, locale, nonLocalePath) => {
|
|
105
|
+
return redirects.find((redirect) => {
|
|
106
|
+
const patternPath = redirect.pattern.replace(/\/+$/g, '').toLowerCase();
|
|
107
|
+
// locale rules are easy and nice
|
|
108
|
+
return redirect.locale === locale && patternPath === nonLocalePath;
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
exports.matchRedirectItemRedirect = matchRedirectItemRedirect;
|
|
112
|
+
/**
|
|
113
|
+
* Compiles a redirect pattern to RegExp; returns null if Sitecore produced a malformed rule
|
|
114
|
+
* so one bad entry does not fail the entire redirect chain.
|
|
115
|
+
* Supports both JS literal form (`/pattern/i`) and plain regex source (`^/path$`).
|
|
116
|
+
* @param {string} pattern redirect pattern from redirect map
|
|
117
|
+
* @returns {RegExp | null} normalized regex instance, or null when invalid
|
|
118
|
+
* @internal
|
|
119
|
+
*/
|
|
120
|
+
const safeCompileRedirectPattern = (pattern) => {
|
|
121
|
+
try {
|
|
122
|
+
const normalizedPattern = (0, tools_1.escapeNonSpecialQuestionMarks)(pattern);
|
|
123
|
+
const literalMatch = normalizedPattern.match(/^\/(.+)\/([a-z]*)$/i);
|
|
124
|
+
if (literalMatch) {
|
|
125
|
+
const [, source, flags] = literalMatch;
|
|
126
|
+
const safeFlags = flags || 'i';
|
|
127
|
+
return new RegExp(source, safeFlags);
|
|
128
|
+
}
|
|
129
|
+
return new RegExp(normalizedPattern, 'i');
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
console.warn(`[RedirectsProxy] Invalid redirect regex; skipping rule. pattern=${pattern} (${message})`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
exports.safeCompileRedirectPattern = safeCompileRedirectPattern;
|
|
138
|
+
/**
|
|
139
|
+
* Detects absolute (external or protocol-relative) target URLs.
|
|
140
|
+
* @param {string} target redirect target
|
|
141
|
+
* @returns {boolean} true when the target is an absolute URL
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
144
|
+
const isAbsoluteTarget = (target) => REGEXP_ABSOLUTE_URL.test(target);
|
|
145
|
+
exports.isAbsoluteTarget = isAbsoluteTarget;
|
|
146
|
+
/**
|
|
147
|
+
* Resolves the redirect target string: replaces the `$siteLang` token and applies
|
|
148
|
+
* regex capture-group substitutions (`$1`, `$2`, …) when the rule pattern is a regex.
|
|
149
|
+
* @param {RedirectResult} existsRedirect matched redirect
|
|
150
|
+
* @param {string} siteLanguage site language used for the `$siteLang` token
|
|
151
|
+
* @param {string} requestPath incoming request path, used when the rule stored no matched path
|
|
152
|
+
* @returns {string} resolved target
|
|
153
|
+
* @internal
|
|
154
|
+
*/
|
|
155
|
+
const resolveRedirectTarget = (existsRedirect, siteLanguage, requestPath) => {
|
|
156
|
+
let target = existsRedirect.target.replace(REGEXP_CONTEXT_SITE_LANG, siteLanguage);
|
|
157
|
+
// Apply regex replacements to the target URL if the pattern is a regex
|
|
158
|
+
if ((0, tools_1.isRegexOrUrl)(existsRedirect.pattern) === 'regex') {
|
|
159
|
+
const sourcePath = existsRedirect.matchedPath || requestPath;
|
|
160
|
+
const pathForCaptureMatch = sourcePath.replace(/\/+$/g, '') || '/';
|
|
161
|
+
const redirectRegex = (0, exports.safeCompileRedirectPattern)(existsRedirect.pattern);
|
|
162
|
+
const matched = redirectRegex ? pathForCaptureMatch.match(redirectRegex) : null;
|
|
163
|
+
if (matched) {
|
|
164
|
+
target = target.replace(/\$(\d+)/g, (_, index) => matched[parseInt(index, 10)] || '');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return target;
|
|
168
|
+
};
|
|
169
|
+
exports.resolveRedirectTarget = resolveRedirectTarget;
|
|
170
|
+
/**
|
|
171
|
+
* Resolves the absolute redirect target (external redirects), optionally preseriving query string
|
|
172
|
+
* @param {ProcessedPath} incomingPathData non locale path, locale prefix and query string (if present)
|
|
173
|
+
* @param {RedirectResult} existsRedirect matched redirect to resolve final result from
|
|
174
|
+
* @returns {string} URL to redirect to
|
|
175
|
+
* @internal
|
|
176
|
+
*/
|
|
177
|
+
const processAbsoluteUrlTarget = (incomingPathData, existsRedirect) => {
|
|
178
|
+
if (!incomingPathData.queryString || !existsRedirect.isQueryStringPreserved) {
|
|
179
|
+
return existsRedirect.target;
|
|
180
|
+
}
|
|
181
|
+
const [targetMainUrl, targetQS] = existsRedirect.target.split('?');
|
|
182
|
+
const mergedQueryString = (0, tools_1.mergeURLSearchParams)(new URLSearchParams(incomingPathData.queryString), new URLSearchParams(targetQS || ''));
|
|
183
|
+
return `${targetMainUrl}?${mergedQueryString}`;
|
|
184
|
+
};
|
|
185
|
+
exports.processAbsoluteUrlTarget = processAbsoluteUrlTarget;
|
|
186
|
+
/**
|
|
187
|
+
* Resolves the locale-less path and target locale for a relative redirect URL.
|
|
188
|
+
* The framework layer is responsible for placing the locale (pathname vs. locale property).
|
|
189
|
+
* @param {ProcessedPath} incomingPathData broken-down incoming request path
|
|
190
|
+
* @param {RedirectResult} existsRedirect matched redirect
|
|
191
|
+
* @param {string[]} configuredLocales configured site locales
|
|
192
|
+
* @param {string} reqLocale current request locale (used when `isLanguagePreserved`)
|
|
193
|
+
* @returns {{ targetLocale: string; targetPath: string }} resolved locale and locale-less path (with query)
|
|
194
|
+
* @internal
|
|
195
|
+
*/
|
|
196
|
+
const processRelativeUrlTarget = (incomingPathData, existsRedirect, configuredLocales, reqLocale) => {
|
|
197
|
+
const { locale, nonLocalePath, queryString } = (0, exports.breakDownPath)(configuredLocales, existsRedirect.target);
|
|
198
|
+
// preserve the request locale when the target has no locale prefix and the rule opts in
|
|
199
|
+
const targetLocale = locale || (existsRedirect.isLanguagePreserved ? reqLocale : '');
|
|
200
|
+
const targetQS = incomingPathData.queryString && existsRedirect.isQueryStringPreserved
|
|
201
|
+
? (0, tools_1.mergeURLSearchParams)(new URLSearchParams(incomingPathData.queryString), new URLSearchParams(queryString || ''))
|
|
202
|
+
: queryString || '';
|
|
203
|
+
return {
|
|
204
|
+
targetLocale,
|
|
205
|
+
targetPath: `${nonLocalePath}${targetQS ? '?' + targetQS : ''}`,
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
exports.processRelativeUrlTarget = processRelativeUrlTarget;
|
package/dist/esm/site/index.js
CHANGED
|
@@ -5,4 +5,5 @@ export { ErrorPagesService } from './error-pages-service';
|
|
|
5
5
|
export { SiteInfoService } from './siteinfo-service';
|
|
6
6
|
export { SitePathService } from './sitepath-service';
|
|
7
7
|
export { getSiteRewrite, getSiteRewriteData, normalizeSiteRewrite, SITE_PREFIX, SITE_KEY, getHostnameFromHostHeader, } from './utils';
|
|
8
|
+
export * from './redirect-utils';
|
|
8
9
|
export { SiteResolver } from './site-resolver';
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { areURLSearchParamsEqual, escapeNonSpecialQuestionMarks, isRegexOrUrl, mergeURLSearchParams, } from '@sitecore-content-sdk/core/tools';
|
|
2
|
+
const REGEXP_CONTEXT_SITE_LANG = /\$siteLang/i;
|
|
3
|
+
const REGEXP_ABSOLUTE_URL = /^(?:[a-z]+:)?\/\//i;
|
|
4
|
+
/**
|
|
5
|
+
* Splits a URL path into its (optional) leading locale segment, the remaining
|
|
6
|
+
* locale-less path, and the query string. Locales are compared case-insensitively.
|
|
7
|
+
* A leading slash is always ensured on `nonLocalePath`; any trailing slash is preserved
|
|
8
|
+
* so slash-sensitive regex redirect rules can still be matched downstream.
|
|
9
|
+
* @param {string[]} configuredLocales configured site locales
|
|
10
|
+
* @param {string} urlPath path (optionally including a `?query`)
|
|
11
|
+
* @returns {ProcessedPath} broken-down path parts
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export const breakDownPath = (configuredLocales, urlPath) => {
|
|
15
|
+
const urlArray = urlPath.split('?');
|
|
16
|
+
const urlQs = urlArray[1];
|
|
17
|
+
// collapse duplicate slashes so degenerate inputs (`//`, `///`) reduce to `/`,
|
|
18
|
+
// while a single meaningful trailing slash is preserved
|
|
19
|
+
const nonQsPath = urlArray[0].replace(/\/{2,}/g, '/');
|
|
20
|
+
const pathArray = nonQsPath.startsWith('/')
|
|
21
|
+
? nonQsPath.slice(1).split('/')
|
|
22
|
+
: nonQsPath.split('/');
|
|
23
|
+
const [maybeLocale, maybeNonLocale] = [pathArray[0], pathArray.slice(1).join('/')];
|
|
24
|
+
const lowerCaseMaybeLocale = maybeLocale.toLowerCase();
|
|
25
|
+
// locales must be compared case insensitively, variations are abundant and typos can be there
|
|
26
|
+
return configuredLocales.find((configuredLocale) => configuredLocale.toLowerCase() === lowerCaseMaybeLocale)
|
|
27
|
+
? {
|
|
28
|
+
nonLocalePath: `/${maybeNonLocale}`,
|
|
29
|
+
queryString: urlQs,
|
|
30
|
+
locale: maybeLocale,
|
|
31
|
+
}
|
|
32
|
+
: {
|
|
33
|
+
nonLocalePath: nonQsPath.startsWith('/') ? nonQsPath : `/${nonQsPath}`,
|
|
34
|
+
queryString: urlQs,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Matches redirect-map rules (rules without a `locale` field) against the incoming request.
|
|
39
|
+
* The incoming path is provided pre-split as {@link ProcessedPath}, so matching starts from the
|
|
40
|
+
* guaranteed locale-less `nonLocalePath` and rebuilds the locale-prefixed variant from it. This
|
|
41
|
+
* avoids the ambiguity of a raw pathname that may or may not already carry a locale segment.
|
|
42
|
+
* @param {RedirectResult[]} redirects All redirects from the service (locale-scoped entries are filtered out).
|
|
43
|
+
* @param {string} requestLocale locale of the current request
|
|
44
|
+
* @param {ProcessedPath} incomingPathData broken-down incoming path (locale-less path and query)
|
|
45
|
+
* @returns {RedirectResult | undefined} First matching redirect or undefined.
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
48
|
+
export const matchFromRedirectMapRedirect = (redirects, requestLocale, incomingPathData) => {
|
|
49
|
+
const redirectMapRedirects = redirects.filter((redirect) => !redirect.locale);
|
|
50
|
+
// start from the locale-less path and rebuild the locale-prefixed variant from it
|
|
51
|
+
// (keep root as `/` so anchored `^/...` patterns still match). Trailing slashes are preserved.
|
|
52
|
+
const nonLocalePath = incomingPathData.nonLocalePath.toLowerCase() || '/';
|
|
53
|
+
const localePath = `/${requestLocale.toLowerCase()}${nonLocalePath}`;
|
|
54
|
+
const incomingQS = incomingPathData.queryString || '';
|
|
55
|
+
// regex rules are matched against the raw candidates (trailing slash preserved so slash-sensitive
|
|
56
|
+
// patterns still match); static rules ignore trailing slashes, so they use trimmed candidates
|
|
57
|
+
const pathCandidates = [nonLocalePath, localePath];
|
|
58
|
+
const staticCandidates = pathCandidates.map((path) => path.replace(/\/+$/g, '') || '/');
|
|
59
|
+
return redirectMapRedirects.find((redirect) => {
|
|
60
|
+
// process static URL (non-regex) rules
|
|
61
|
+
if (isRegexOrUrl(redirect.pattern) === 'url') {
|
|
62
|
+
const [rawPatternPath, patternQS] = (redirect.pattern.endsWith('/') ? redirect.pattern.slice(0, -1) : redirect.pattern).split('?');
|
|
63
|
+
// candidates are already lowercased; lowercase the pattern path to keep locale (and route)
|
|
64
|
+
// comparison case-insensitive
|
|
65
|
+
const patternPath = rawPatternPath.toLowerCase();
|
|
66
|
+
return (staticCandidates.includes(patternPath) &&
|
|
67
|
+
(!patternQS ||
|
|
68
|
+
areURLSearchParamsEqual(new URLSearchParams(patternQS), new URLSearchParams(incomingQS))));
|
|
69
|
+
}
|
|
70
|
+
// process regex rules
|
|
71
|
+
const regex = safeCompileRedirectPattern(redirect.pattern);
|
|
72
|
+
if (!regex) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const testRegex = (value) => {
|
|
76
|
+
regex.lastIndex = 0;
|
|
77
|
+
return regex.test(value);
|
|
78
|
+
};
|
|
79
|
+
// remove duplicates
|
|
80
|
+
const regexCandidates = [...pathCandidates, ...staticCandidates].filter((candidate, index, array) => array.indexOf(candidate) === index);
|
|
81
|
+
const matchedPath = regexCandidates.find((candidate) => testRegex(candidate));
|
|
82
|
+
const matchedPathWithQuery = incomingQS
|
|
83
|
+
? regexCandidates.find((candidate) => testRegex(`${candidate}?${incomingQS}`))
|
|
84
|
+
: undefined;
|
|
85
|
+
// Save the matched path/query (if found) into the redirect object
|
|
86
|
+
redirect.matchedQueryString = matchedPathWithQuery ? `?${incomingQS}` : '';
|
|
87
|
+
redirect.matchedPath = matchedPath || matchedPathWithQuery || '';
|
|
88
|
+
return !!(matchedPath || matchedPathWithQuery);
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Processes redirect rules from redirect items (language-versioned)
|
|
93
|
+
* @param {RedirectResult[]} redirects redirect entries from Edge
|
|
94
|
+
* @param {string} locale current request locale
|
|
95
|
+
* @param {string} nonLocalePath current request path with locale prefix stripped
|
|
96
|
+
* @returns {RedirectResult | undefined} matched redirect item redirect result or undefined
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
export const matchRedirectItemRedirect = (redirects, locale, nonLocalePath) => {
|
|
100
|
+
return redirects.find((redirect) => {
|
|
101
|
+
const patternPath = redirect.pattern.replace(/\/+$/g, '').toLowerCase();
|
|
102
|
+
// locale rules are easy and nice
|
|
103
|
+
return redirect.locale === locale && patternPath === nonLocalePath;
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Compiles a redirect pattern to RegExp; returns null if Sitecore produced a malformed rule
|
|
108
|
+
* so one bad entry does not fail the entire redirect chain.
|
|
109
|
+
* Supports both JS literal form (`/pattern/i`) and plain regex source (`^/path$`).
|
|
110
|
+
* @param {string} pattern redirect pattern from redirect map
|
|
111
|
+
* @returns {RegExp | null} normalized regex instance, or null when invalid
|
|
112
|
+
* @internal
|
|
113
|
+
*/
|
|
114
|
+
export const safeCompileRedirectPattern = (pattern) => {
|
|
115
|
+
try {
|
|
116
|
+
const normalizedPattern = escapeNonSpecialQuestionMarks(pattern);
|
|
117
|
+
const literalMatch = normalizedPattern.match(/^\/(.+)\/([a-z]*)$/i);
|
|
118
|
+
if (literalMatch) {
|
|
119
|
+
const [, source, flags] = literalMatch;
|
|
120
|
+
const safeFlags = flags || 'i';
|
|
121
|
+
return new RegExp(source, safeFlags);
|
|
122
|
+
}
|
|
123
|
+
return new RegExp(normalizedPattern, 'i');
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
console.warn(`[RedirectsProxy] Invalid redirect regex; skipping rule. pattern=${pattern} (${message})`);
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Detects absolute (external or protocol-relative) target URLs.
|
|
133
|
+
* @param {string} target redirect target
|
|
134
|
+
* @returns {boolean} true when the target is an absolute URL
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
export const isAbsoluteTarget = (target) => REGEXP_ABSOLUTE_URL.test(target);
|
|
138
|
+
/**
|
|
139
|
+
* Resolves the redirect target string: replaces the `$siteLang` token and applies
|
|
140
|
+
* regex capture-group substitutions (`$1`, `$2`, …) when the rule pattern is a regex.
|
|
141
|
+
* @param {RedirectResult} existsRedirect matched redirect
|
|
142
|
+
* @param {string} siteLanguage site language used for the `$siteLang` token
|
|
143
|
+
* @param {string} requestPath incoming request path, used when the rule stored no matched path
|
|
144
|
+
* @returns {string} resolved target
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
147
|
+
export const resolveRedirectTarget = (existsRedirect, siteLanguage, requestPath) => {
|
|
148
|
+
let target = existsRedirect.target.replace(REGEXP_CONTEXT_SITE_LANG, siteLanguage);
|
|
149
|
+
// Apply regex replacements to the target URL if the pattern is a regex
|
|
150
|
+
if (isRegexOrUrl(existsRedirect.pattern) === 'regex') {
|
|
151
|
+
const sourcePath = existsRedirect.matchedPath || requestPath;
|
|
152
|
+
const pathForCaptureMatch = sourcePath.replace(/\/+$/g, '') || '/';
|
|
153
|
+
const redirectRegex = safeCompileRedirectPattern(existsRedirect.pattern);
|
|
154
|
+
const matched = redirectRegex ? pathForCaptureMatch.match(redirectRegex) : null;
|
|
155
|
+
if (matched) {
|
|
156
|
+
target = target.replace(/\$(\d+)/g, (_, index) => matched[parseInt(index, 10)] || '');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return target;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Resolves the absolute redirect target (external redirects), optionally preseriving query string
|
|
163
|
+
* @param {ProcessedPath} incomingPathData non locale path, locale prefix and query string (if present)
|
|
164
|
+
* @param {RedirectResult} existsRedirect matched redirect to resolve final result from
|
|
165
|
+
* @returns {string} URL to redirect to
|
|
166
|
+
* @internal
|
|
167
|
+
*/
|
|
168
|
+
export const processAbsoluteUrlTarget = (incomingPathData, existsRedirect) => {
|
|
169
|
+
if (!incomingPathData.queryString || !existsRedirect.isQueryStringPreserved) {
|
|
170
|
+
return existsRedirect.target;
|
|
171
|
+
}
|
|
172
|
+
const [targetMainUrl, targetQS] = existsRedirect.target.split('?');
|
|
173
|
+
const mergedQueryString = mergeURLSearchParams(new URLSearchParams(incomingPathData.queryString), new URLSearchParams(targetQS || ''));
|
|
174
|
+
return `${targetMainUrl}?${mergedQueryString}`;
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Resolves the locale-less path and target locale for a relative redirect URL.
|
|
178
|
+
* The framework layer is responsible for placing the locale (pathname vs. locale property).
|
|
179
|
+
* @param {ProcessedPath} incomingPathData broken-down incoming request path
|
|
180
|
+
* @param {RedirectResult} existsRedirect matched redirect
|
|
181
|
+
* @param {string[]} configuredLocales configured site locales
|
|
182
|
+
* @param {string} reqLocale current request locale (used when `isLanguagePreserved`)
|
|
183
|
+
* @returns {{ targetLocale: string; targetPath: string }} resolved locale and locale-less path (with query)
|
|
184
|
+
* @internal
|
|
185
|
+
*/
|
|
186
|
+
export const processRelativeUrlTarget = (incomingPathData, existsRedirect, configuredLocales, reqLocale) => {
|
|
187
|
+
const { locale, nonLocalePath, queryString } = breakDownPath(configuredLocales, existsRedirect.target);
|
|
188
|
+
// preserve the request locale when the target has no locale prefix and the rule opts in
|
|
189
|
+
const targetLocale = locale || (existsRedirect.isLanguagePreserved ? reqLocale : '');
|
|
190
|
+
const targetQS = incomingPathData.queryString && existsRedirect.isQueryStringPreserved
|
|
191
|
+
? mergeURLSearchParams(new URLSearchParams(incomingPathData.queryString), new URLSearchParams(queryString || ''))
|
|
192
|
+
: queryString || '';
|
|
193
|
+
return {
|
|
194
|
+
targetLocale,
|
|
195
|
+
targetPath: `${nonLocalePath}${targetQS ? '?' + targetQS : ''}`,
|
|
196
|
+
};
|
|
197
|
+
};
|
package/package.json
CHANGED
package/types/site/index.d.ts
CHANGED
|
@@ -6,5 +6,6 @@ export { SiteInfoService, SiteInfoServiceConfig } from './siteinfo-service';
|
|
|
6
6
|
export { SitePathService, SitePathServiceConfig } from './sitepath-service';
|
|
7
7
|
export { SiteInfo } from './models';
|
|
8
8
|
export { getSiteRewrite, getSiteRewriteData, normalizeSiteRewrite, SiteRewriteData, SITE_PREFIX, SITE_KEY, getHostnameFromHostHeader, } from './utils';
|
|
9
|
+
export * from './redirect-utils';
|
|
9
10
|
export { SiteResolver } from './site-resolver';
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/site/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,6BAA6B,EAC7B,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAE/F,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,QAAQ,EACR,yBAAyB,GAC1B,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/site/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,6BAA6B,EAC7B,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAE/F,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAE5E,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EACpB,eAAe,EACf,WAAW,EACX,QAAQ,EACR,yBAAyB,GAC1B,MAAM,SAAS,CAAC;AACjB,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { RedirectInfo } from '../site';
|
|
2
|
+
/**
|
|
3
|
+
* Expanded redirect info to contain matchers for regexp match
|
|
4
|
+
* @internal
|
|
5
|
+
*/
|
|
6
|
+
export type RedirectResult = RedirectInfo & {
|
|
7
|
+
matchedQueryString?: string;
|
|
8
|
+
matchedPath?: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Broken down path with all parts needed to make a match for a redirect rule
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
export type ProcessedPath = {
|
|
15
|
+
nonLocalePath: string;
|
|
16
|
+
locale?: string;
|
|
17
|
+
queryString?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Splits a URL path into its (optional) leading locale segment, the remaining
|
|
21
|
+
* locale-less path, and the query string. Locales are compared case-insensitively.
|
|
22
|
+
* A leading slash is always ensured on `nonLocalePath`; any trailing slash is preserved
|
|
23
|
+
* so slash-sensitive regex redirect rules can still be matched downstream.
|
|
24
|
+
* @param {string[]} configuredLocales configured site locales
|
|
25
|
+
* @param {string} urlPath path (optionally including a `?query`)
|
|
26
|
+
* @returns {ProcessedPath} broken-down path parts
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export declare const breakDownPath: (configuredLocales: string[], urlPath: string) => ProcessedPath;
|
|
30
|
+
/**
|
|
31
|
+
* Matches redirect-map rules (rules without a `locale` field) against the incoming request.
|
|
32
|
+
* The incoming path is provided pre-split as {@link ProcessedPath}, so matching starts from the
|
|
33
|
+
* guaranteed locale-less `nonLocalePath` and rebuilds the locale-prefixed variant from it. This
|
|
34
|
+
* avoids the ambiguity of a raw pathname that may or may not already carry a locale segment.
|
|
35
|
+
* @param {RedirectResult[]} redirects All redirects from the service (locale-scoped entries are filtered out).
|
|
36
|
+
* @param {string} requestLocale locale of the current request
|
|
37
|
+
* @param {ProcessedPath} incomingPathData broken-down incoming path (locale-less path and query)
|
|
38
|
+
* @returns {RedirectResult | undefined} First matching redirect or undefined.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export declare const matchFromRedirectMapRedirect: (redirects: RedirectResult[], requestLocale: string, incomingPathData: ProcessedPath) => RedirectResult | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Processes redirect rules from redirect items (language-versioned)
|
|
44
|
+
* @param {RedirectResult[]} redirects redirect entries from Edge
|
|
45
|
+
* @param {string} locale current request locale
|
|
46
|
+
* @param {string} nonLocalePath current request path with locale prefix stripped
|
|
47
|
+
* @returns {RedirectResult | undefined} matched redirect item redirect result or undefined
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
export declare const matchRedirectItemRedirect: (redirects: RedirectResult[], locale: string, nonLocalePath: string) => RedirectResult | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Compiles a redirect pattern to RegExp; returns null if Sitecore produced a malformed rule
|
|
53
|
+
* so one bad entry does not fail the entire redirect chain.
|
|
54
|
+
* Supports both JS literal form (`/pattern/i`) and plain regex source (`^/path$`).
|
|
55
|
+
* @param {string} pattern redirect pattern from redirect map
|
|
56
|
+
* @returns {RegExp | null} normalized regex instance, or null when invalid
|
|
57
|
+
* @internal
|
|
58
|
+
*/
|
|
59
|
+
export declare const safeCompileRedirectPattern: (pattern: string) => RegExp | null;
|
|
60
|
+
/**
|
|
61
|
+
* Detects absolute (external or protocol-relative) target URLs.
|
|
62
|
+
* @param {string} target redirect target
|
|
63
|
+
* @returns {boolean} true when the target is an absolute URL
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
66
|
+
export declare const isAbsoluteTarget: (target: string) => boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Resolves the redirect target string: replaces the `$siteLang` token and applies
|
|
69
|
+
* regex capture-group substitutions (`$1`, `$2`, …) when the rule pattern is a regex.
|
|
70
|
+
* @param {RedirectResult} existsRedirect matched redirect
|
|
71
|
+
* @param {string} siteLanguage site language used for the `$siteLang` token
|
|
72
|
+
* @param {string} requestPath incoming request path, used when the rule stored no matched path
|
|
73
|
+
* @returns {string} resolved target
|
|
74
|
+
* @internal
|
|
75
|
+
*/
|
|
76
|
+
export declare const resolveRedirectTarget: (existsRedirect: RedirectResult, siteLanguage: string, requestPath: string) => string;
|
|
77
|
+
/**
|
|
78
|
+
* Resolves the absolute redirect target (external redirects), optionally preseriving query string
|
|
79
|
+
* @param {ProcessedPath} incomingPathData non locale path, locale prefix and query string (if present)
|
|
80
|
+
* @param {RedirectResult} existsRedirect matched redirect to resolve final result from
|
|
81
|
+
* @returns {string} URL to redirect to
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
export declare const processAbsoluteUrlTarget: (incomingPathData: ProcessedPath, existsRedirect: RedirectResult) => string;
|
|
85
|
+
/**
|
|
86
|
+
* Resolves the locale-less path and target locale for a relative redirect URL.
|
|
87
|
+
* The framework layer is responsible for placing the locale (pathname vs. locale property).
|
|
88
|
+
* @param {ProcessedPath} incomingPathData broken-down incoming request path
|
|
89
|
+
* @param {RedirectResult} existsRedirect matched redirect
|
|
90
|
+
* @param {string[]} configuredLocales configured site locales
|
|
91
|
+
* @param {string} reqLocale current request locale (used when `isLanguagePreserved`)
|
|
92
|
+
* @returns {{ targetLocale: string; targetPath: string }} resolved locale and locale-less path (with query)
|
|
93
|
+
* @internal
|
|
94
|
+
*/
|
|
95
|
+
export declare const processRelativeUrlTarget: (incomingPathData: ProcessedPath, existsRedirect: RedirectResult, configuredLocales: string[], reqLocale: string) => {
|
|
96
|
+
targetLocale: string;
|
|
97
|
+
targetPath: string;
|
|
98
|
+
};
|
|
99
|
+
//# sourceMappingURL=redirect-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redirect-utils.d.ts","sourceRoot":"","sources":["../../src/site/redirect-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAWvC;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG;IAC1C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,GAAI,mBAAmB,MAAM,EAAE,EAAE,SAAS,MAAM,KAAG,aAwB5E,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACvC,WAAW,cAAc,EAAE,EAC3B,eAAe,MAAM,EACrB,kBAAkB,aAAa,KAC9B,cAAc,GAAG,SAqDnB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,GACpC,WAAW,cAAc,EAAE,EAC3B,QAAQ,MAAM,EACd,eAAe,MAAM,KACpB,cAAc,GAAG,SAMnB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,GAAI,SAAS,MAAM,KAAG,MAAM,GAAG,IAiBrE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,MAAM,KAAG,OAA2C,CAAC;AAE9F;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,GAChC,gBAAgB,cAAc,EAC9B,cAAc,MAAM,EACpB,aAAa,MAAM,KAClB,MAkBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,GACnC,kBAAkB,aAAa,EAC/B,gBAAgB,cAAc,KAC7B,MAUF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,GACnC,kBAAkB,aAAa,EAC/B,gBAAgB,cAAc,EAC9B,mBAAmB,MAAM,EAAE,EAC3B,WAAW,MAAM,KAChB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAoB5C,CAAC"}
|