Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(pageAPI): Apply Regex and sanitization correctly #31422

Merged
merged 14 commits into from
Feb 20, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -329,9 +329,9 @@ describe('DotEmaShellComponent', () => {
);

spectator.detectChanges();
expect(spyloadPageAsset).toHaveBeenCalledWith({ ...params, url: 'index' });
expect(spyloadPageAsset).toHaveBeenCalledWith({ ...params, url: '/index' });
expect(spyLocation).toHaveBeenCalledWith(
'/?language_id=1&url=index&variantName=DEFAULT&mode=EDIT_MODE'
'/?language_id=1&url=%2Findex&variantName=DEFAULT&mode=EDIT_MODE'
);
});

Expand All @@ -342,7 +342,7 @@ describe('DotEmaShellComponent', () => {

const params = {
...INITIAL_PAGE_PARAMS,
url: '/some-url/some-nested-url/'
url: '/some-url/some-nested-url'
};

overrideRouteSnashot(
Expand All @@ -353,10 +353,10 @@ describe('DotEmaShellComponent', () => {
spectator.detectChanges();
expect(spyloadPageAsset).toHaveBeenCalledWith({
...params,
url: 'some-url/some-nested-url'
url: '/some-url/some-nested-url'
});
expect(spyLocation).toHaveBeenCalledWith(
'/?language_id=1&url=some-url%2Fsome-nested-url&variantName=DEFAULT&mode=EDIT_MODE'
'/?language_id=1&url=%2Fsome-url%2Fsome-nested-url&variantName=DEFAULT&mode=EDIT_MODE'
);
});

Expand All @@ -375,9 +375,12 @@ describe('DotEmaShellComponent', () => {
);

spectator.detectChanges();
expect(spyloadPageAsset).toHaveBeenCalledWith({ ...params, url: 'some-url/' });
expect(spyloadPageAsset).toHaveBeenCalledWith({
...params,
url: '/some-url/index'
});
expect(spyLocation).toHaveBeenCalledWith(
'/?language_id=1&url=some-url%2F&variantName=DEFAULT&mode=EDIT_MODE'
'/?language_id=1&url=%2Fsome-url%2Findex&variantName=DEFAULT&mode=EDIT_MODE'
);
});

Expand All @@ -392,7 +395,7 @@ describe('DotEmaShellComponent', () => {
};

const expectedParams = {
url: 'some-url/',
url: '/some-url/index',
[PERSONA_KEY]: 'someCoolDude',
mode: UVE_MODE.EDIT,
language_id: 1
Expand All @@ -406,7 +409,7 @@ describe('DotEmaShellComponent', () => {
spectator.detectChanges();
expect(spyloadPageAsset).toHaveBeenCalledWith(expectedParams);
expect(spyLocation).toHaveBeenCalledWith(
'/?url=some-url%2F&language_id=1&mode=EDIT_MODE&personaId=someCoolDude'
'/?url=%2Fsome-url%2Findex&language_id=1&mode=EDIT_MODE&personaId=someCoolDude'
);
});
});
Expand Down Expand Up @@ -657,7 +660,7 @@ describe('DotEmaShellComponent', () => {
const baseClientHost = 'http://localhost:3000/';
const params = {
...INITIAL_PAGE_PARAMS,
clientHost: 'http://localhost:3000' // No trailing slash
clientHost: 'http://localhost:3000/' // No trailing slash
};

// Set up route with uveConfig.url that has trailing slash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ describe('DotUveWorkflowActionsComponent', () => {
expect(spySetWorkflowActionLoading).toHaveBeenCalledWith(true);
expect(spyLoadPageAsset).toHaveBeenCalledWith({
language_id: dotcmsContentletMock.languageId.toString(),
url: dotcmsContentletMock.url
url: '/'
});
expect(spyMessage).toHaveBeenCalledTimes(2);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ describe('EditEmaEditorComponent', () => {

beforeEach(() => {
spectator = createComponent({
queryParams: { language_id: 1, url: 'page-one' },
queryParams: { language_id: 1, url: 'index' },
data: {
data: {
url: 'http://localhost:3000'
Expand Down Expand Up @@ -2589,7 +2589,7 @@ describe('EditEmaEditorComponent', () => {
const iframe = spectator.debugElement.query(By.css('[data-testId="iframe"]'));

expect(iframe.nativeElement.src).toBe(
'http://localhost:3000/page-one?language_id=1'
'http://localhost:3000/index?language_id=1'
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,12 @@ describe('EditEmaGuard', () => {
expect(didEnteredPortlet).toBe(true);
});

it('should navigate to "edit-page" with url as "index" when the initial url queryParam is "/"', () => {
it('should navigate to "edit-page" with url as "/" when the initial url queryParam is ""', () => {
const route: ActivatedRouteSnapshot = {
firstChild: {
url: [{ path: 'content' }]
},
queryParams: { url: '/' }
queryParams: { url: '' }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;

Expand All @@ -110,7 +110,7 @@ describe('EditEmaGuard', () => {
queryParams: {
[PERSONA_KEY]: 'modes.persona.no.persona',
language_id: 1,
url: 'index'
url: '/'
},
replaceUrl: true
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ function confirmQueryParams(queryParams: Params): {
} {
const { missing, ...missingQueryParams } = DEFAULT_QUERY_PARAMS.reduce(
(acc, { key, value }) => {
if (!queryParams[key]) {
acc[key] = value;
if (key === 'url' && queryParams[key]?.trim()?.length === 0) {
acc[key] = '/';
acc.missing = true;

return acc;
}

if (key === 'url' && queryParams[key] === '/') {
acc[key] = 'index';
if (!queryParams[key]) {
acc[key] = value;
acc.missing = true;

return acc;
Expand Down Expand Up @@ -77,7 +77,7 @@ const DEFAULT_QUERY_PARAMS = [
},
{
key: 'url',
value: 'index'
value: '/'
},
{
key: 'com.dotmarketing.persona.id',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,17 @@ describe('withEditor', () => {
...MOCK_RESPONSE_HEADLESS.vanityUrl,
url: 'first'
}
},
pageParams: {
language_id: '1',
variantName: 'DEFAULT',
url: 'first',
[PERSONA_KEY]: 'dot:persona'
}
});

expect(store.$iframeURL()).toBe(
'http://localhost:3000/first?language_id=1&variantName=DEFAULT&personaId=dot%3Apersona'
'http://localhost/first?language_id=1&variantName=DEFAULT&personaId=dot%3Apersona'
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,9 +209,7 @@ export function withEditor() {
};
}),
$iframeURL: computed<string | InstanceType<typeof String>>(() => {
const page = store.pageAPIResponse().page;
const vanityURL = store.pageAPIResponse().vanityUrl?.url;
const sanitizedURL = sanitizeURL(vanityURL ?? page?.pageURI);
const sanitizedURL = sanitizeURL(store.pageParams().url);

const url = buildIframeURL({
url: sanitizedURL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,9 @@ export function withLoad() {
}

// Maybe we can use retryWhen() instead of this navigate.
const url = vanityUrl.forwardTo.replace('/', '');
router.navigate([], {
queryParamsHandling: 'merge',
queryParams: { url }
queryParams: { url: vanityUrl.forwardTo }
});

// EMPTY is a simple Observable that only emits the complete notification.
Expand Down
21 changes: 14 additions & 7 deletions core-web/libs/portlets/edit-ema/portlet/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,19 @@ function insertPositionedContentletInContainer(payload: ActionPayload): {
}

/**
* Remove the index from the end of the url if it's nested and also remove extra slashes
* Sanitizes a URL by:
* 1. Removing extra leading/trailing slashes
* 2. Preserving 'index' in the URL path
*
* @param {string} url
* @return {*} {string}
*/
export function sanitizeURL(url?: string): string {
return url
?.replace(/^\/+|\/+$/g, '') // Remove starting and trailing slashes
?.replace(/^(index)$|(.*?)(?:\/)?index\/?$/, (_, g1, g2) => g1 || `${g2}/`); // Keep 'index' or add slash for paths
if (!url || url === '/') {
return '/';
}

return url.replace(/\/+/g, '/'); // Convert multiple slashes to single slash
}

/**
Expand Down Expand Up @@ -292,7 +296,10 @@ export function normalizeQueryParams(params, baseClientHost?: string) {
delete queryParams[PERSONA_KEY];
}

if (baseClientHost && sanitizeURL(baseClientHost) === sanitizeURL(params.clientHost)) {
if (
baseClientHost &&
new URL(baseClientHost).toString() === new URL(params.clientHost).toString()
) {
delete queryParams.clientHost;
}

Expand Down Expand Up @@ -633,8 +640,8 @@ export const checkClientHostAccess = (
}

// Most IDEs and terminals add a / at the end of the URL, so we need to sanitize it
const sanitizedClientHost = sanitizeURL(clientHost);
const sanitizedAllowedDevURLs = allowedDevURLs.map(sanitizeURL);
const sanitizedClientHost = new URL(clientHost).toString();
const sanitizedAllowedDevURLs = allowedDevURLs.map((url) => new URL(url).toString());

return sanitizedAllowedDevURLs.includes(sanitizedClientHost);
};
Expand Down
42 changes: 11 additions & 31 deletions core-web/libs/portlets/edit-ema/portlet/src/lib/utils/utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,50 +309,30 @@ describe('utils functions', () => {
});

describe('url sanitize', () => {
it('should remove the slash from the start', () => {
expect(sanitizeURL('/cool')).toEqual('cool');
it('should left the / as /', () => {
expect(sanitizeURL('/')).toEqual('/');
});

it("should remove the slash from the end if it's not the only character", () => {
expect(sanitizeURL('super-cool/')).toEqual('super-cool');
it('should clean multiple slashes', () => {
expect(sanitizeURL('//////')).toEqual('/');
});

it('should remove the slash from the end and the beggining', () => {
expect(sanitizeURL('/hello-there/')).toEqual('hello-there');
});

it('should leave as it is for valid url', () => {
expect(sanitizeURL('this-is-where-the-fun-begins')).toEqual(
'this-is-where-the-fun-begins'
);
});

it('should return index if the url is index without nested path', () => {
expect(sanitizeURL('index')).toEqual('index');
expect(sanitizeURL('index/')).toEqual('index');
expect(sanitizeURL('/index/')).toEqual('index');
it('should clean multiple slashes', () => {
expect(sanitizeURL('//index////')).toEqual('/index/');
});

describe('nested url', () => {
it('should leave as it is for a nested valid url', () => {
expect(sanitizeURL('hello-there/general-kenobi')).toEqual(
'hello-there/general-kenobi'
expect(sanitizeURL('hello-there/general-kenobi/')).toEqual(
'hello-there/general-kenobi/'
);
});

it('should remove index from the end of the url but keep the slash if is a nested path', () => {
expect(sanitizeURL('my-nested-path/index/')).toEqual('my-nested-path/');
});

it('should remove the index if a nested path', () => {
expect(sanitizeURL('i-have-the-high-ground/index')).toEqual(
'i-have-the-high-ground/'
it('should clean multiple slashes in a nested url', () => {
expect(sanitizeURL('hello-there////general-kenobi//////')).toEqual(
'hello-there/general-kenobi/'
);
});

it('should remove the index if a nested path with slash', () => {
expect(sanitizeURL('no-index-please/index/')).toEqual('no-index-please/');
});
});
});

Expand Down
21 changes: 20 additions & 1 deletion core-web/libs/sdk/client/src/lib/editor/sdk-editor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,26 @@ describe('DotCMSPageEditor', () => {
expect(postMessageToEditor).toHaveBeenCalledWith({
action: CLIENT_ACTIONS.NAVIGATION_UPDATE,
payload: {
url: 'index'
url: '/'
}
});
});

it('should update navigation if its empty', () => {
updateNavigation('');
expect(postMessageToEditor).toHaveBeenCalledWith({
action: CLIENT_ACTIONS.NAVIGATION_UPDATE,
payload: {
url: '/'
}
});
});
it('should update navigation if its undefined', () => {
updateNavigation(undefined as unknown as string);
expect(postMessageToEditor).toHaveBeenCalledWith({
action: CLIENT_ACTIONS.NAVIGATION_UPDATE,
payload: {
url: '/'
}
});
});
Expand Down
2 changes: 1 addition & 1 deletion core-web/libs/sdk/client/src/lib/editor/sdk-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function updateNavigation(pathname: string): void {
postMessageToEditor({
action: CLIENT_ACTIONS.NAVIGATION_UPDATE,
payload: {
url: pathname === '/' ? 'index' : pathname?.replace('/', '')
url: pathname || '/'
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -404,8 +404,16 @@ private Response getPageRender(final PageRenderParams renderParams) throws DotDa
.isPermanentRedirect()) {
Logger.debug(this, () -> String.format("Incoming Vanity URL is a %d Redirect",
cachedVanityUrlOpt.get().response));

final CachedVanityUrl cachedVanityUrl = cachedVanityUrlOpt.get();
final String finalForwardTo = cachedVanityUrlOpt.get().handle(renderParams.uri()).getRewrite();
final CachedVanityUrl finalCachedVanityUrl = new CachedVanityUrl( cachedVanityUrl.vanityUrlId,
cachedVanityUrl.url, cachedVanityUrl.languageId, cachedVanityUrl.siteId, finalForwardTo, cachedVanityUrl.response, cachedVanityUrl.order);


final EmptyPageView emptyPageView =
new EmptyPageView.Builder().vanityUrl(cachedVanityUrlOpt.get()).build();
new EmptyPageView.Builder().vanityUrl(finalCachedVanityUrl).build();

return Response.ok(new ResponseEntityView<>(emptyPageView)).build();
} else {
final VanityUrlResult vanityUrlResult = cachedVanityUrlOpt.get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -605,8 +605,9 @@ public Optional<CachedVanityUrl> resolveVanityUrlIfPresent(final HttpServletRequ
final Optional<CachedVanityUrl> vanityUrlOpt =
this.vanityUrlAPI.resolveVanityUrl(correctedUri, site, language);
if (vanityUrlOpt.isPresent()) {
DotPreconditions.checkArgument(!this.vanityUrlAPI.isSelfReferenced(vanityUrlOpt.get()
, correctedUri));
if (this.vanityUrlAPI.isSelfReferenced(vanityUrlOpt.get(), correctedUri)){
return Optional.empty();
}
return vanityUrlOpt;
}
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ public CachedVanityUrl(final String vanityUrlId, final String url, final long la
/**
* rewrites the vanity with the matching groups if needed, returns
* the rewritten url, parameters from the request
* @param urlIn
* @param url
* @return
*/
final Tuple2<String, String> processForward(final String urlIn) {
final Tuple2<String, String> processForward(final String url) {
final String urlIn = !url.startsWith(StringPool.SLASH) ? StringPool.SLASH + url : url;
String newForward = this.forwardTo;
String queryString = null;
if(pattern!=null) {
Expand All @@ -72,6 +73,10 @@ final Tuple2<String, String> processForward(final String urlIn) {
}
}
}

//check to handle the cases where forwardTo is empty, which means that it should redirect to the root
newForward = newForward.isEmpty() ? "/" : newForward;

if (UtilMethods.isSet(newForward) && newForward.contains("?")) {
String[] arr = newForward.split("\\?", 2);
newForward = arr[0];
Expand Down
Loading
Loading