diff --git a/packages/common/src/helpers/__tests__/sanitize.spec.ts b/packages/common/src/helpers/__tests__/sanitize.spec.ts new file mode 100644 index 000000000..557b59839 --- /dev/null +++ b/packages/common/src/helpers/__tests__/sanitize.spec.ts @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2018-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { + sanitizeLocation, + sanitizeSearchParams, + sanitizePathname, +} from '../sanitize'; + +describe('sanitize', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('sanitizeLocation', () => { + it('sanitizeLocation', () => { + const location = { + search: + 'url=https%3A%2F%2Fgithub.com%2Ftest-samples&storageType=persistent', + pathname: '/f', + }; + + const sanitizedLocation = sanitizeLocation(location); + + expect(sanitizedLocation).toEqual( + expect.objectContaining({ + search: + 'url=https%3A%2F%2Fgithub.com%2Ftest-samples&storageType=persistent', + pathname: '/f', + }), + ); + }); + }); + + describe('sanitizeSearchParams', () => { + it('should return sanitized value of location.search if it is without encoding)', () => { + const search = + 'url=https://github.com/test-samples&state=9284564475&session=98765&session_state=45645654567&code=9844646765&storageType=persistent&new'; + + const searchParams = new URLSearchParams(search); + const sanitizedSearchParams = sanitizeSearchParams(searchParams); + + expect(sanitizedSearchParams.toString()).toEqual( + 'url=https%3A%2F%2Fgithub.com%2Ftest-samples&storageType=persistent&new=', + ); + }); + + it('should return sanitized value of location.search if it is encoded', () => { + const search = + 'url=https%3A%2F%2Fgithub.com%2Ftest-samples%26state%3D9284564475%26session%3D98765%26session_state%3D45645654567%26code%3D9844646765%26storageType%3Dpersistent'; + + const searchParams = new URLSearchParams(search); + const sanitizedSearchParams = sanitizeSearchParams(searchParams); + + expect(sanitizedSearchParams.toString()).toEqual( + 'url=https%3A%2F%2Fgithub.com%2Ftest-samples%26storageType%3Dpersistent', + ); + }); + }); + + describe('sanitizePathname', () => { + it('should remove oauth redirect leftovers', () => { + const pathname = + '/f&code=12345&session=67890&session_state=13579&state=24680'; + + const newPathname = sanitizePathname(pathname); + + expect(newPathname).toEqual('/f'); + }); + }); +}); diff --git a/packages/common/src/helpers/index.ts b/packages/common/src/helpers/index.ts index e6e839b43..16704b310 100644 --- a/packages/common/src/helpers/index.ts +++ b/packages/common/src/helpers/index.ts @@ -11,7 +11,15 @@ */ import * as errors from './errors'; +import { + sanitizeLocation, + sanitizePathname, + sanitizeSearchParams, +} from './sanitize'; export default { errors, + sanitizeLocation, + sanitizePathname, + sanitizeSearchParams, }; diff --git a/packages/common/src/helpers/sanitize.ts b/packages/common/src/helpers/sanitize.ts new file mode 100644 index 000000000..7f314aea2 --- /dev/null +++ b/packages/common/src/helpers/sanitize.ts @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2018-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +const oauthParams = ['state', 'session', 'session_state', 'code']; + +/** + * Remove oauth params. + */ +export function sanitizeLocation< + T extends { search: string; pathname: string } = Location, +>(location: T, removeParams: string[] = []): T { + const sanitizedSearchParams = sanitizeSearchParams( + new URLSearchParams(location.search), + removeParams, + ); + const sanitizedPathname = sanitizePathname(location.pathname, removeParams); + + return { + ...location, + search: sanitizedSearchParams.toString(), + searchParams: sanitizedSearchParams, + pathname: sanitizedPathname, + }; +} + +export function sanitizeSearchParams( + searchParams: URLSearchParams, + removeParams: string[] = [], +): URLSearchParams { + const toRemove = [...oauthParams, ...removeParams]; + + // remove oauth params + toRemove.forEach(val => searchParams.delete(val)); + + // sanitize each query param + const sanitizedSearchParams = new URLSearchParams(); + searchParams.forEach((value, param) => { + if (!value) { + sanitizedSearchParams.set(param, value); + return; + } + + const sanitizedValue = sanitizeStr(value, toRemove); + sanitizedSearchParams.set(param, sanitizedValue); + }); + + return sanitizedSearchParams; +} + +export function sanitizePathname( + pathname: string, + removeParams: string[] = [], +): string { + const toRemove = [...oauthParams, ...removeParams]; + + // sanitize pathname + const sanitizedPathname = sanitizeStr(pathname, toRemove); + + return sanitizedPathname; +} + +function sanitizeStr(str: string, removeParams: string[] = []): string { + removeParams.forEach(param => { + const re = new RegExp('&' + param + '=.+?(?=(?:[?&/#]|$))', 'i'); + str = str.replace(re, ''); + }); + + return str; +} diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index a03a10910..158f19073 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -18,6 +18,8 @@ export * from './dto/cluster-config'; export { helpers, api }; +export const FACTORY_LINK_ATTR = 'factoryLink'; + const common = { helpers, api, diff --git a/packages/dashboard-backend/src/routes/__tests__/factoryAcceptanceRedirect.spec.ts b/packages/dashboard-backend/src/routes/__tests__/factoryAcceptanceRedirect.spec.ts index c0fbc9ad9..52e3efa35 100644 --- a/packages/dashboard-backend/src/routes/__tests__/factoryAcceptanceRedirect.spec.ts +++ b/packages/dashboard-backend/src/routes/__tests__/factoryAcceptanceRedirect.spec.ts @@ -33,6 +33,15 @@ describe('Factory Acceptance Redirect', () => { expect(res.headers.location).toEqual(`/dashboard/#/load-factory?url=${factoryUrl}`); }); + it('should redirect "/f?url=factoryUrl"', async () => { + const factoryUrl = 'factoryUrl'; + const res = await app.inject({ + url: `/f?factoryLink=${encodeURIComponent('url=' + factoryUrl)}`, + }); + expect(res.statusCode).toEqual(302); + expect(res.headers.location).toEqual(`/dashboard/#/load-factory?url=${factoryUrl}`); + }); + it('should redirect "/dashboard/f?url=factoryUrl"', async () => { const factoryUrl = 'factoryUrl'; const res = await app.inject({ diff --git a/packages/dashboard-backend/src/routes/factoryAcceptanceRedirect.ts b/packages/dashboard-backend/src/routes/factoryAcceptanceRedirect.ts index cadbc06ab..b9421edf0 100644 --- a/packages/dashboard-backend/src/routes/factoryAcceptanceRedirect.ts +++ b/packages/dashboard-backend/src/routes/factoryAcceptanceRedirect.ts @@ -10,15 +10,27 @@ * Red Hat, Inc. - initial API and implementation */ +import { FACTORY_LINK_ATTR } from '@eclipse-che/common'; +import { sanitizeSearchParams } from '@eclipse-che/common/src/helpers/sanitize'; import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'; +import querystring from 'querystring'; export function registerFactoryAcceptanceRedirect(instance: FastifyInstance): void { // redirect to the Dashboard factory flow function redirectFactoryFlow(path: string) { instance.register(async server => { server.get(path, async (request: FastifyRequest, reply: FastifyReply) => { - const queryStr = request.url.replace(path, ''); - return reply.redirect('/dashboard/#/load-factory' + queryStr); + let queryStr = request.url.replace(path, '').replace(/^\?/, ''); + + const query = querystring.parse(queryStr); + if (query[FACTORY_LINK_ATTR] !== undefined) { + // restore the factory link from the query string + queryStr = querystring.unescape(query[FACTORY_LINK_ATTR] as string); + } + + const sanitizedQueryParams = sanitizeSearchParams(new URLSearchParams(queryStr)); + + return reply.redirect('/dashboard/#/load-factory?' + sanitizedQueryParams.toString()); }); }); } diff --git a/packages/dashboard-frontend/jest.config.js b/packages/dashboard-frontend/jest.config.js index 0ce15e802..5df44f6c5 100644 --- a/packages/dashboard-frontend/jest.config.js +++ b/packages/dashboard-frontend/jest.config.js @@ -51,10 +51,10 @@ module.exports = { ], coverageThreshold: { global: { - statements: 80, + statements: 81, branches: 85, functions: 77, - lines: 80, + lines: 81, }, }, }; diff --git a/packages/dashboard-frontend/src/Routes/index.tsx b/packages/dashboard-frontend/src/Routes/index.tsx index c870b2a19..e91ec0e24 100644 --- a/packages/dashboard-frontend/src/Routes/index.tsx +++ b/packages/dashboard-frontend/src/Routes/index.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { Redirect, Route, RouteComponentProps, Switch } from 'react-router'; -import { buildFactoryLoaderPath } from '../preload'; +import { buildFactoryLoaderPath } from '../preload/main'; import { ROUTE } from './routes'; const CreateWorkspace = React.lazy(() => import('../pages/GetStarted')); diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getProjectFromLocation.ts b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getProjectFromLocation.ts index faf27ae26..620a003c4 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getProjectFromLocation.ts +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getProjectFromLocation.ts @@ -25,7 +25,7 @@ export function getProjectFromLocation( git: { remotes: { [remoteName]: origin } }, name, }; - } else if (FactoryLocationAdapter.isFullPathUrl(location)) { + } else if (FactoryLocationAdapter.isHttpLocation(location)) { const sourceUrl = new URL(location); if (sourceUrl.pathname.endsWith('.git')) { const origin = `${sourceUrl.origin}${sourceUrl.pathname}`; diff --git a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Fetch/Devfile/__tests__/index.spec.tsx b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Fetch/Devfile/__tests__/index.spec.tsx index 32d544ecc..8fba37e02 100644 --- a/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Fetch/Devfile/__tests__/index.spec.tsx +++ b/packages/dashboard-frontend/src/components/WorkspaceProgress/CreatingSteps/Fetch/Devfile/__tests__/index.spec.tsx @@ -12,26 +12,28 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { FACTORY_LINK_ATTR } from '@eclipse-che/common'; import { cleanup, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { createMemoryHistory } from 'history'; +import { MemoryHistory, createMemoryHistory } from 'history'; import React from 'react'; import { Provider } from 'react-redux'; import { Action, Store } from 'redux'; import CreatingStepFetchDevfile from '..'; -import ExpandableWarning from '../../../../../ExpandableWarning'; +import getComponentRenderer from '../../../../../../services/__mocks__/getComponentRenderer'; import devfileApi from '../../../../../../services/devfileApi'; +import { getDefer } from '../../../../../../services/helpers/deferred'; import { FACTORY_URL_ATTR, OVERRIDE_ATTR_PREFIX, REMOTES_ATTR, } from '../../../../../../services/helpers/factoryFlow/buildFactoryParams'; -import { getDefer } from '../../../../../../services/helpers/deferred'; import { AlertItem } from '../../../../../../services/helpers/types'; -import getComponentRenderer from '../../../../../../services/__mocks__/getComponentRenderer'; +import OAuthService from '../../../../../../services/oauth'; import { AppThunk } from '../../../../../../store'; import { ActionCreators, OAuthResponse } from '../../../../../../store/FactoryResolver'; import { FakeStoreBuilder } from '../../../../../../store/__mocks__/storeBuilder'; +import ExpandableWarning from '../../../../../ExpandableWarning'; import { MIN_STEP_DURATION_MS, TIMEOUT_TO_RESOLVE_SEC } from '../../../../const'; jest.mock('../../../../TimeLimit'); @@ -524,6 +526,8 @@ describe('Creating steps, fetching a devfile', () => { const host = 'che-host'; const protocol = 'http://'; let spyWindowLocation: jest.SpyInstance; + let spyOpenOAuthPage: jest.SpyInstance; + let history: MemoryHistory; beforeEach(() => { mockIsOAuthResponse.mockReturnValue(true); @@ -535,25 +539,39 @@ describe('Creating steps, fetching a devfile', () => { } as OAuthResponse); spyWindowLocation = createWindowLocationSpy(host, protocol); + spyOpenOAuthPage = jest + .spyOn(OAuthService, 'openOAuthPage') + .mockImplementation(() => jest.fn()); + + history = createMemoryHistory({ + initialEntries: [ + { + search: searchParams.toString(), + }, + ], + }); }); afterEach(() => { sessionStorage.clear(); spyWindowLocation.mockClear(); + spyOpenOAuthPage.mockClear(); }); test('redirect to an authentication URL', async () => { const emptyStore = new FakeStoreBuilder().build(); - renderComponent(emptyStore, searchParams); + renderComponent(emptyStore, searchParams, history); await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS); - const expectedRedirectUrl = `${oauthAuthenticationUrl}/&redirect_after_login=${protocol}${host}/f?url=${encodeURIComponent( - factoryUrl, + const expectedRedirectUrl = `${protocol}${host}/f?${FACTORY_LINK_ATTR}=${encodeURIComponent( + 'url=' + encodeURIComponent(factoryUrl), )}`; - await waitFor(() => expect(spyWindowLocation).toHaveBeenCalledWith(expectedRedirectUrl)); + await waitFor(() => + expect(spyOpenOAuthPage).toHaveBeenCalledWith(oauthAuthenticationUrl, expectedRedirectUrl), + ); expect(mockOnError).not.toHaveBeenCalled(); expect(mockOnNextStep).not.toHaveBeenCalled(); @@ -562,25 +580,29 @@ describe('Creating steps, fetching a devfile', () => { test('authentication fails', async () => { const emptyStore = new FakeStoreBuilder().build(); - renderComponent(emptyStore, searchParams); + renderComponent(emptyStore, searchParams, history); await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS); - const expectedRedirectUrl = `${oauthAuthenticationUrl}/&redirect_after_login=${protocol}${host}/f?url=${encodeURIComponent( - factoryUrl, + const expectedRedirectUrl = `${protocol}${host}/f?${FACTORY_LINK_ATTR}=${encodeURIComponent( + 'url=' + encodeURIComponent(factoryUrl), )}`; - await waitFor(() => expect(spyWindowLocation).toHaveBeenCalledWith(expectedRedirectUrl)); + await waitFor(() => + expect(spyOpenOAuthPage).toHaveBeenCalledWith(oauthAuthenticationUrl, expectedRedirectUrl), + ); // cleanup previous render cleanup(); // first unsuccessful try to resolve devfile after authentication - renderComponent(emptyStore, searchParams); + renderComponent(emptyStore, searchParams, history); await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS); - await waitFor(() => expect(spyWindowLocation).toHaveBeenCalledWith(expectedRedirectUrl)); + await waitFor(() => + expect(spyOpenOAuthPage).toHaveBeenCalledWith(oauthAuthenticationUrl, expectedRedirectUrl), + ); await waitFor(() => expect(mockOnError).not.toHaveBeenCalled()); @@ -588,11 +610,13 @@ describe('Creating steps, fetching a devfile', () => { cleanup(); // second unsuccessful try to resolve devfile after authentication - renderComponent(emptyStore, searchParams); + renderComponent(emptyStore, searchParams, history); await jest.advanceTimersByTimeAsync(MIN_STEP_DURATION_MS); - await waitFor(() => expect(spyWindowLocation).toHaveBeenCalledWith(expectedRedirectUrl)); + await waitFor(() => + expect(spyOpenOAuthPage).toHaveBeenCalledWith(oauthAuthenticationUrl, expectedRedirectUrl), + ); const expectAlertItem = expect.objectContaining({ title: 'Failed to create the workspace', @@ -667,6 +691,7 @@ function createWindowLocationSpy(host: string, protocol: string): jest.SpyInstan (window.location as any) = { host, protocol, + origin: protocol + host, }; Object.defineProperty(window.location, 'href', { set: () => { @@ -677,8 +702,11 @@ function createWindowLocationSpy(host: string, protocol: string): jest.SpyInstan return jest.spyOn(window.location, 'href', 'set'); } -function getComponent(store: Store, searchParams: URLSearchParams): React.ReactElement { - const history = createMemoryHistory(); +function getComponent( + store: Store, + searchParams: URLSearchParams, + history = createMemoryHistory(), +): React.ReactElement { return ( { this.checkNumberOfTries(factoryUrl); this.increaseNumberOfTries(factoryUrl); - // open authentication page - const env = getEnvironment(); - // build redirect URL - let redirectHost = window.location.protocol + '//' + window.location.host; - if (isDevEnvironment(env)) { - redirectHost = env.server; - } - const redirectUrl = new URL('/f', redirectHost); - redirectUrl.searchParams.set('url', factoryUrl); + /* open authentication page */ + + const redirectUrl = new URL('/f', window.location.origin); + redirectUrl.searchParams.set( + FACTORY_LINK_ATTR, + this.props.history.location.search.replace(/^\?/, ''), + ); + OAuthService.openOAuthPage(e.attributes.oauth_authentication_url, redirectUrl.toString()); + return false; } diff --git a/packages/dashboard-frontend/src/containers/Loader/index.tsx b/packages/dashboard-frontend/src/containers/Loader/index.tsx index a3f204514..3091c974c 100644 --- a/packages/dashboard-frontend/src/containers/Loader/index.tsx +++ b/packages/dashboard-frontend/src/containers/Loader/index.tsx @@ -10,13 +10,13 @@ * Red Hat, Inc. - initial API and implementation */ +import { helpers } from '@eclipse-che/common'; import React from 'react'; import { connect, ConnectedProps } from 'react-redux'; import { RouteComponentProps } from 'react-router-dom'; import { LoaderPage } from '../../pages/Loader'; import { findTargetWorkspace } from '../../services/helpers/factoryFlow/findTargetWorkspace'; import { getLoaderMode } from '../../services/helpers/factoryFlow/getLoaderMode'; -import { sanitizeLocation } from '../../services/helpers/location'; import { LoaderTab } from '../../services/helpers/types'; import { Workspace } from '../../services/workspace-adapter'; import { AppState } from '../../store'; @@ -33,7 +33,7 @@ class LoaderContainer extends React.Component { super(props); const { location: dirtyLocation } = this.props.history; - const { search } = sanitizeLocation(dirtyLocation); + const { search } = helpers.sanitizeLocation(dirtyLocation); const searchParams = new URLSearchParams(search); const tabParam = searchParams.get('tab') || undefined; diff --git a/packages/dashboard-frontend/src/pages/GetStarted/GetStartedTab/ImportFromGit/GitRepoLocationInput.tsx b/packages/dashboard-frontend/src/pages/GetStarted/GetStartedTab/ImportFromGit/GitRepoLocationInput.tsx index 817bf7daf..4ea52209f 100644 --- a/packages/dashboard-frontend/src/pages/GetStarted/GetStartedTab/ImportFromGit/GitRepoLocationInput.tsx +++ b/packages/dashboard-frontend/src/pages/GetStarted/GetStartedTab/ImportFromGit/GitRepoLocationInput.tsx @@ -64,7 +64,7 @@ export class GitRepoLocationInput extends React.PureComponent { private handleChange(location: string): void { const isValid = - FactoryLocationAdapter.isFullPathUrl(location) || + FactoryLocationAdapter.isHttpLocation(location) || FactoryLocationAdapter.isSshLocation(location); if (isValid) { diff --git a/packages/dashboard-frontend/src/preload/__tests__/index.spec.ts b/packages/dashboard-frontend/src/preload/__tests__/index.spec.ts index eccfbbdfe..5d2951752 100644 --- a/packages/dashboard-frontend/src/preload/__tests__/index.spec.ts +++ b/packages/dashboard-frontend/src/preload/__tests__/index.spec.ts @@ -10,145 +10,12 @@ * Red Hat, Inc. - initial API and implementation */ -import SessionStorageService, { SessionStorageKey } from '../../services/session-storage'; -import { buildFactoryLoaderPath, storePathIfNeeded } from '..'; - -describe('Location test', () => { - describe('SSHLocation', () => { - test('new policy', () => { - const result = buildFactoryLoaderPath('git@github.com:eclipse-che/che-dashboard.git?new='); - expect(result).toEqual( - '/f?policies.create=perclick&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', - ); - }); - - test('che-editor parameter', () => { - const result = buildFactoryLoaderPath( - 'git@github.com:eclipse-che/che-dashboard.git?che-editor=che-incubator/checode/insiders', - ); - expect(result).toEqual( - '/f?che-editor=che-incubator%2Fchecode%2Finsiders&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', - ); - }); - - test('devfilePath parameter', () => { - const result = buildFactoryLoaderPath( - 'git@github.com:eclipse-che/che-dashboard.git?devfilePath=devfilev2.yaml', - ); - expect(result).toEqual( - '/f?override.devfileFilename=devfilev2.yaml&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', - ); - }); - - test('devWorkspace parameter', () => { - const result = buildFactoryLoaderPath( - 'git@github.com:eclipse-che/che-dashboard.git?devWorkspace=/devfiles/devworkspace-che-theia-latest.yaml', - ); - expect(result).toEqual( - '/f?devWorkspace=%2Fdevfiles%2Fdevworkspace-che-theia-latest.yaml&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', - ); - }); - - test('storageType parameter', () => { - const result = buildFactoryLoaderPath( - 'git@github.com:eclipse-che/che-dashboard.git?storageType=ephemeral', - ); - expect(result).toEqual( - '/f?storageType=ephemeral&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', - ); - }); - - test('unsupported parameter', () => { - const result = buildFactoryLoaderPath( - 'git@github.com:eclipse-che/che-dashboard.git?unsupportedParameter=foo', - ); - expect(result).toEqual( - '/f?url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git%3FunsupportedParameter%3Dfoo', - ); - }); - }); - - describe('FullPathUrl', () => { - test('new policy', () => { - const result = buildFactoryLoaderPath( - 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?new=', - ); - expect(result).toEqual( - '/f?policies.create=perclick&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', - ); - }); - - test('che-editor parameter', () => { - const result = buildFactoryLoaderPath( - 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?che-editor=che-incubator/checode/insiders', - ); - expect(result).toEqual( - '/f?che-editor=che-incubator%2Fchecode%2Finsiders&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', - ); - }); - - test('devfilePath parameter', () => { - const result = buildFactoryLoaderPath( - 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?devfilePath=devfilev2.yaml', - ); - expect(result).toEqual( - '/f?override.devfileFilename=devfilev2.yaml&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', - ); - }); - - test('devWorkspace parameter', () => { - const result = buildFactoryLoaderPath( - 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?devWorkspace=/devfiles/devworkspace-che-theia-latest.yaml', - ); - expect(result).toEqual( - '/f?devWorkspace=%2Fdevfiles%2Fdevworkspace-che-theia-latest.yaml&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', - ); - }); - - test('storageType parameter', () => { - const result = buildFactoryLoaderPath( - 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?storageType=ephemeral', - ); - expect(result).toEqual( - '/f?storageType=ephemeral&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', - ); - }); - - test('image parameter', () => { - const result = buildFactoryLoaderPath( - 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?image=quay.io/devfile/universal-developer-image:latest', - ); - expect(result).toEqual( - '/f?image=quay.io%2Fdevfile%2Funiversal-developer-image%3Alatest&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', - ); - }); - - test('unsupported parameter', () => { - const result = buildFactoryLoaderPath( - 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?unsupportedParameter=foo', - ); - expect(result).toEqual( - '/f?url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2%3FunsupportedParameter%3Dfoo', - ); - }); - }); -}); - -describe('storePathnameIfNeeded test', () => { - let mockUpdate: jest.Mock; - - beforeAll(() => { - mockUpdate = jest.fn(); - SessionStorageService.update = mockUpdate; - }); - - test('empty path', () => { - storePathIfNeeded('/'); - expect(mockUpdate).toBeCalledTimes(0); - }); - - test('regular path', () => { - storePathIfNeeded('/test'); - expect(mockUpdate).toBeCalledWith(SessionStorageKey.ORIGINAL_LOCATION_PATH, '/test'); - }); +const mockRedirectToDashboard = jest.fn(); +jest.mock('../main.ts', () => ({ + redirectToDashboard: mockRedirectToDashboard, +})); + +it('should call main()', () => { + require('../index.ts'); + expect(mockRedirectToDashboard).toHaveBeenCalled(); }); diff --git a/packages/dashboard-frontend/src/preload/__tests__/main.spec.ts b/packages/dashboard-frontend/src/preload/__tests__/main.spec.ts new file mode 100644 index 000000000..edc974979 --- /dev/null +++ b/packages/dashboard-frontend/src/preload/__tests__/main.spec.ts @@ -0,0 +1,238 @@ +/* + * Copyright (c) 2018-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { REMOTES_ATTR } from '../../services/helpers/factoryFlow/buildFactoryParams'; +import SessionStorageService, { SessionStorageKey } from '../../services/session-storage'; +import { redirectToDashboard, buildFactoryLoaderPath, storePathIfNeeded } from '../main'; + +describe('test buildFactoryLoaderPath()', () => { + describe('SSHLocation', () => { + test('new policy', () => { + const result = buildFactoryLoaderPath('git@github.com:eclipse-che/che-dashboard.git?new='); + expect(result).toEqual( + '/f?policies.create=perclick&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', + ); + }); + + test('che-editor parameter', () => { + const result = buildFactoryLoaderPath( + 'git@github.com:eclipse-che/che-dashboard.git?che-editor=che-incubator/checode/insiders', + ); + expect(result).toEqual( + '/f?che-editor=che-incubator%2Fchecode%2Finsiders&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', + ); + }); + + test('devfilePath parameter', () => { + const result = buildFactoryLoaderPath( + 'git@github.com:eclipse-che/che-dashboard.git?devfilePath=devfilev2.yaml', + ); + expect(result).toEqual( + '/f?override.devfileFilename=devfilev2.yaml&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', + ); + }); + + test('devWorkspace parameter', () => { + const result = buildFactoryLoaderPath( + 'git@github.com:eclipse-che/che-dashboard.git?devWorkspace=/devfiles/devworkspace-che-theia-latest.yaml', + ); + expect(result).toEqual( + '/f?devWorkspace=%2Fdevfiles%2Fdevworkspace-che-theia-latest.yaml&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', + ); + }); + + test('storageType parameter', () => { + const result = buildFactoryLoaderPath( + 'git@github.com:eclipse-che/che-dashboard.git?storageType=ephemeral', + ); + expect(result).toEqual( + '/f?storageType=ephemeral&url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git', + ); + }); + + test('unsupported parameter', () => { + const result = buildFactoryLoaderPath( + 'git@github.com:eclipse-che/che-dashboard.git?unsupportedParameter=foo', + ); + expect(result).toEqual( + '/f?url=git%40github.com%3Aeclipse-che%2Fche-dashboard.git%3FunsupportedParameter%3Dfoo', + ); + }); + }); + + describe('FullPathUrl', () => { + test('new policy', () => { + const result = buildFactoryLoaderPath( + 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?new=', + ); + expect(result).toEqual( + '/f?policies.create=perclick&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', + ); + }); + + test('che-editor parameter', () => { + const result = buildFactoryLoaderPath( + 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?che-editor=che-incubator/checode/insiders', + ); + expect(result).toEqual( + '/f?che-editor=che-incubator%2Fchecode%2Finsiders&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', + ); + }); + + test('devfilePath parameter', () => { + const result = buildFactoryLoaderPath( + 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?devfilePath=devfilev2.yaml', + ); + expect(result).toEqual( + '/f?override.devfileFilename=devfilev2.yaml&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', + ); + }); + + test('devWorkspace parameter', () => { + const result = buildFactoryLoaderPath( + 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?devWorkspace=/devfiles/devworkspace-che-theia-latest.yaml', + ); + expect(result).toEqual( + '/f?devWorkspace=%2Fdevfiles%2Fdevworkspace-che-theia-latest.yaml&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', + ); + }); + + test('storageType parameter', () => { + const result = buildFactoryLoaderPath( + 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?storageType=ephemeral', + ); + expect(result).toEqual( + '/f?storageType=ephemeral&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', + ); + }); + + test('image parameter', () => { + const result = buildFactoryLoaderPath( + 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?image=quay.io/devfile/universal-developer-image:latest', + ); + expect(result).toEqual( + '/f?image=quay.io%2Fdevfile%2Funiversal-developer-image%3Alatest&url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2', + ); + }); + + test('unsupported parameter', () => { + const result = buildFactoryLoaderPath( + 'https://github.com/che-samples/java-spring-petclinic/tree/devfilev2?unsupportedParameter=foo', + ); + expect(result).toEqual( + '/f?url=https%3A%2F%2Fgithub.com%2Fche-samples%2Fjava-spring-petclinic%2Ftree%2Fdevfilev2%3FunsupportedParameter%3Dfoo', + ); + }); + }); +}); + +describe('test storePathnameIfNeeded()', () => { + let mockUpdate: jest.Mock; + + beforeAll(() => { + mockUpdate = jest.fn(); + SessionStorageService.update = mockUpdate; + }); + + afterEach(() => { + mockUpdate.mockClear(); + }); + + test('regular path', () => { + storePathIfNeeded('/test'); + expect(mockUpdate).toBeCalledWith(SessionStorageKey.ORIGINAL_LOCATION_PATH, '/test'); + }); + + test('empty path', () => { + storePathIfNeeded('/'); + expect(mockUpdate).toBeCalledTimes(0); + }); +}); + +describe('test redirectToDashboard()', () => { + const origin = 'https://che-host'; + let spyWindowLocation: jest.SpyInstance; + + afterEach(() => { + spyWindowLocation.mockRestore(); + }); + + describe('wrong pathname', () => { + it('should redirect to home', () => { + spyWindowLocation = createWindowLocationSpy(origin + '/test'); + + redirectToDashboard(); + expect(spyWindowLocation).toHaveBeenCalledWith(origin + '/dashboard/'); + }); + }); + + describe('factory url', () => { + test('with HTTP protocol', () => { + const repoUrl = 'https://repo-url'; + const query = 'new'; + spyWindowLocation = createWindowLocationSpy(origin + '#' + repoUrl + '&' + query); + + redirectToDashboard(); + expect(spyWindowLocation).toHaveBeenCalledWith( + origin + '/dashboard/f?policies.create=perclick&url=' + encodeURIComponent(repoUrl), + ); + }); + + test('with SHH protocol', () => { + const repoUrl = 'git@github.com:namespace/myrepo.git'; + const query = 'devfilePath=my-devfile.yaml'; + spyWindowLocation = createWindowLocationSpy(origin + '#' + repoUrl + '&' + query); + + redirectToDashboard(); + expect(spyWindowLocation).toHaveBeenCalledWith( + origin + + '/dashboard/f?override.devfileFilename=my-devfile.yaml&url=' + + encodeURIComponent(repoUrl), + ); + }); + }); + + describe('redirect after authentication', () => { + it('should redirect to the workspace creation flow', () => { + const remoteUrl = '{https://origin-url,https://upstream-url}'; + spyWindowLocation = createWindowLocationSpy(origin + '?' + REMOTES_ATTR + '=' + remoteUrl); + + redirectToDashboard(); + expect(spyWindowLocation).toHaveBeenCalledWith( + origin + '/dashboard/f?remotes=' + encodeURIComponent(remoteUrl), + ); + }); + }); +}); + +function createWindowLocationSpy(href: string): jest.SpyInstance { + delete (window as any).location; + const url = new URL(href); + (window.location as Partial) = { + protocol: url.protocol, + host: url.host, + hostname: url.hostname, + port: url.port, + pathname: url.pathname, + search: url.search, + hash: url.hash, + origin: url.origin, + }; + Object.defineProperty(window.location, 'href', { + set: () => { + // no-op + }, + configurable: true, + get: () => href, + }); + return jest.spyOn(window.location, 'href', 'set'); +} diff --git a/packages/dashboard-frontend/src/preload/index.ts b/packages/dashboard-frontend/src/preload/index.ts index 904a94dc3..527a62a0c 100644 --- a/packages/dashboard-frontend/src/preload/index.ts +++ b/packages/dashboard-frontend/src/preload/index.ts @@ -10,88 +10,8 @@ * Red Hat, Inc. - initial API and implementation */ -import { FactoryLocation, FactoryLocationAdapter } from '../services/factory-location-adapter'; -import { - PROPAGATE_FACTORY_ATTRS, - REMOTES_ATTR, -} from '../services/helpers/factoryFlow/buildFactoryParams'; -import { sanitizeLocation } from '../services/helpers/location'; -import SessionStorageService, { SessionStorageKey } from '../services/session-storage'; +import { redirectToDashboard } from './main'; -(function acceptNewFactoryLink(): void { - if (window.location.pathname.startsWith('/dashboard/')) { - return; - } - - storePathIfNeeded(window.location.pathname); - - const hash = window.location.hash.replace(/(\/?)#(\/?)/, ''); - if (FactoryLocationAdapter.isFullPathUrl(hash) || FactoryLocationAdapter.isSshLocation(hash)) { - window.location.href = window.location.origin + '/dashboard' + buildFactoryLoaderPath(hash); - } else if ( - window.location.search.startsWith(`?${REMOTES_ATTR}=`) || - window.location.search.includes(`&${REMOTES_ATTR}=`) - ) { - // allow starting workspaces when no project url, but remotes are provided - window.location.href = - window.origin + '/dashboard' + buildFactoryLoaderPath(window.location.href, false); - } else { - window.location.href = window.location.origin + '/dashboard/'; - } +(function (): void { + redirectToDashboard(); })(); - -export function storePathIfNeeded(path: string) { - if (path !== '/') { - SessionStorageService.update(SessionStorageKey.ORIGINAL_LOCATION_PATH, path); - } -} - -export function buildFactoryLoaderPath(location: string, appendUrl = true): string { - let factory: FactoryLocation | URL; - if (appendUrl) { - try { - factory = new FactoryLocationAdapter(location); - } catch (e) { - console.error(e); - return '/'; - } - } else { - factory = sanitizeLocation(new window.URL(location)); - } - - const initParams = PROPAGATE_FACTORY_ATTRS.map(paramName => { - const paramValue = extractUrlParam(factory.searchParams, paramName); - return [paramName, paramValue]; - }).filter(([, paramValue]) => paramValue); - - const devfilePath = - extractUrlParam(factory.searchParams, 'devfilePath') || - extractUrlParam(factory.searchParams, 'df'); - if (devfilePath) { - initParams.push(['override.devfileFilename', devfilePath]); - } - const newWorkspace = extractUrlParam(factory.searchParams, 'new'); - if (newWorkspace) { - initParams.push(['policies.create', 'perclick']); - } - - const searchParams = new URLSearchParams(initParams); - - if (appendUrl) { - searchParams.append('url', factory.toString()); - } - - return '/f?' + searchParams.toString(); -} - -function extractUrlParam(params: URLSearchParams, paramName: string): string { - const param = params.get(paramName); - let value = ''; - if (param) { - value = param.slice(); - } else if (params.has(paramName)) { - value = 'true'; - } - params.delete(paramName); - return value; -} diff --git a/packages/dashboard-frontend/src/preload/main.ts b/packages/dashboard-frontend/src/preload/main.ts new file mode 100644 index 000000000..f6567a51c --- /dev/null +++ b/packages/dashboard-frontend/src/preload/main.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2018-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ + +import { helpers } from '@eclipse-che/common'; +import { FactoryLocation, FactoryLocationAdapter } from '../services/factory-location-adapter'; +import { + PROPAGATE_FACTORY_ATTRS, + REMOTES_ATTR, +} from '../services/helpers/factoryFlow/buildFactoryParams'; +import SessionStorageService, { SessionStorageKey } from '../services/session-storage'; + +export function redirectToDashboard(): void { + if (window.location.pathname.startsWith('/dashboard/')) { + // known location, do nothing + return; + } + + storePathIfNeeded(window.location.pathname); + + // check if project url is provided (#) + const hash = window.location.hash.replace(/(\/?)#(\/?)/, ''); + if (FactoryLocationAdapter.isHttpLocation(hash) || FactoryLocationAdapter.isSshLocation(hash)) { + // project url found, redirect to the workspaces creation page + window.location.href = window.location.origin + '/dashboard' + buildFactoryLoaderPath(hash); + return; + } + + // check if remotes are provided without a project url + if ( + window.location.search.startsWith(`?${REMOTES_ATTR}=`) || + window.location.search.includes(`&${REMOTES_ATTR}=`) + ) { + // allow starting workspaces when no project url, but remotes are provided + window.location.href = + window.location.origin + '/dashboard' + buildFactoryLoaderPath(window.location.href, false); + return; + } + + // redirect to the dashboard home page + window.location.href = window.location.origin + '/dashboard/'; +} + +export function storePathIfNeeded(path: string) { + if (path !== '/') { + SessionStorageService.update(SessionStorageKey.ORIGINAL_LOCATION_PATH, path); + } +} + +export function buildFactoryLoaderPath(location: string, appendUrl = true): string { + let factory: FactoryLocation | URL; + if (appendUrl) { + try { + factory = new FactoryLocationAdapter(location); + } catch (e) { + console.error(e); + return '/'; + } + } else { + factory = helpers.sanitizeLocation(new window.URL(location)); + } + + const initParams = PROPAGATE_FACTORY_ATTRS.map(paramName => { + const paramValue = extractUrlParam(factory.searchParams, paramName); + return [paramName, paramValue]; + }).filter(([, paramValue]) => paramValue); + + const devfilePath = + extractUrlParam(factory.searchParams, 'devfilePath') || + extractUrlParam(factory.searchParams, 'df'); + if (devfilePath) { + initParams.push(['override.devfileFilename', devfilePath]); + } + const newWorkspace = extractUrlParam(factory.searchParams, 'new'); + if (newWorkspace) { + initParams.push(['policies.create', 'perclick']); + } + + const searchParams = new URLSearchParams(initParams); + + if (appendUrl) { + searchParams.append('url', factory.toString()); + } + + return '/f?' + searchParams.toString(); +} + +function extractUrlParam(params: URLSearchParams, paramName: string): string { + const param = params.get(paramName); + let value = ''; + if (param) { + value = param.slice(); + } else if (params.has(paramName)) { + value = 'true'; + } + params.delete(paramName); + return value; +} diff --git a/packages/dashboard-frontend/src/services/factory-location-adapter/__tests__/factoryLocationAdapter.spec.ts b/packages/dashboard-frontend/src/services/factory-location-adapter/__tests__/factoryLocationAdapter.spec.ts index 9c6f6fd95..3d94d310c 100644 --- a/packages/dashboard-frontend/src/services/factory-location-adapter/__tests__/factoryLocationAdapter.spec.ts +++ b/packages/dashboard-frontend/src/services/factory-location-adapter/__tests__/factoryLocationAdapter.spec.ts @@ -22,7 +22,7 @@ describe('FactoryLocationAdapter Service', () => { factoryLocation = new FactoryLocationAdapter(location); - expect(factoryLocation.isFullPathUrl).toBeTruthy(); + expect(factoryLocation.isHttpLocation).toBeTruthy(); expect(factoryLocation.isSshLocation).toBeFalsy(); }); it('should determine the SSH location', () => { @@ -30,7 +30,7 @@ describe('FactoryLocationAdapter Service', () => { factoryLocation = new FactoryLocationAdapter(location); - expect(factoryLocation.isFullPathUrl).toBeFalsy(); + expect(factoryLocation.isHttpLocation).toBeFalsy(); expect(factoryLocation.isSshLocation).toBeTruthy(); }); it('should determine the Bitbucket-Server SSH location', () => { @@ -38,7 +38,7 @@ describe('FactoryLocationAdapter Service', () => { factoryLocation = new FactoryLocationAdapter(location); - expect(factoryLocation.isFullPathUrl).toBeFalsy(); + expect(factoryLocation.isHttpLocation).toBeFalsy(); expect(factoryLocation.isSshLocation).toBeTruthy(); }); it('should determine unsupported factory location', () => { diff --git a/packages/dashboard-frontend/src/services/factory-location-adapter/index.ts b/packages/dashboard-frontend/src/services/factory-location-adapter/index.ts index 5140d93ff..04d406fbc 100644 --- a/packages/dashboard-frontend/src/services/factory-location-adapter/index.ts +++ b/packages/dashboard-frontend/src/services/factory-location-adapter/index.ts @@ -10,12 +10,12 @@ * Red Hat, Inc. - initial API and implementation */ -import { sanitizeLocation } from '../helpers/location'; +import { helpers } from '@eclipse-che/common'; import { Location } from 'history'; export interface FactoryLocation { readonly searchParams: URLSearchParams; - readonly isFullPathUrl: boolean; + readonly isHttpLocation: boolean; readonly isSshLocation: boolean; readonly toString: () => string; } @@ -31,12 +31,12 @@ export class FactoryLocationAdapter implements FactoryLocation { href = href.replace('&', '?'); } const [pathname, search] = href.split('?'); - const sanitizedLocation = sanitizeLocation({ search, pathname } as Location); + const sanitizedLocation = helpers.sanitizeLocation({ search, pathname } as Location); this.pathname = sanitizedLocation.pathname; this.search = new window.URLSearchParams(sanitizedLocation.search); - if (FactoryLocationAdapter.isFullPathUrl(sanitizedLocation.pathname)) { + if (FactoryLocationAdapter.isHttpLocation(sanitizedLocation.pathname)) { this.fullPathUrl = sanitizedLocation.pathname; if (sanitizedLocation.search) { this.fullPathUrl += sanitizedLocation.search; @@ -51,7 +51,7 @@ export class FactoryLocationAdapter implements FactoryLocation { } } - public static isFullPathUrl(href: string): boolean { + public static isHttpLocation(href: string): boolean { return /^(https?:\/\/.)[-a-zA-Z0-9@:%._+~#=]{2,}\b([-a-zA-Z0-9@:%_+.~#?&/=]*)$/.test(href); } @@ -63,7 +63,7 @@ export class FactoryLocationAdapter implements FactoryLocation { return this.search; } - get isFullPathUrl(): boolean { + get isHttpLocation(): boolean { return this.fullPathUrl !== undefined; } diff --git a/packages/dashboard-frontend/src/services/helpers/__tests__/location.spec.ts b/packages/dashboard-frontend/src/services/helpers/__tests__/location.spec.ts deleted file mode 100644 index b605f812c..000000000 --- a/packages/dashboard-frontend/src/services/helpers/__tests__/location.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2018-2023 Red Hat, Inc. - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - */ - -import { sanitizeLocation } from '../location'; -import { Location } from 'history'; - -describe('location/sanitizeLocation', () => { - it("should return the same values if location variables don't have OAuth params included", () => { - const search = '?url=https%3A%2F%2Fgithub.com%2Ftest-samples&storageType=persistent'; - const pathname = '/f'; - - const newLocation = sanitizeLocation({ search, pathname } as Location); - - expect(newLocation.search).toEqual(search); - expect(newLocation.pathname).toEqual(pathname); - }); - - it('should return sanitized value of location.search if it is without encoding)', () => { - const search = - '?url=https://github.com/test-samples&state=9284564475&session=98765&session_state=45645654567&code=9844646765&storageType=persistent'; - const pathname = '/f'; - - const newLocation = sanitizeLocation({ search, pathname } as Location); - - expect(newLocation.search).not.toEqual(search); - expect(newLocation.search).toEqual( - '?url=https%3A%2F%2Fgithub.com%2Ftest-samples&storageType=persistent', - ); - expect(newLocation.pathname).toEqual(pathname); - }); - - it('should return sanitized value of location.search if it is encoded', () => { - const search = - '?url=https%3A%2F%2Fgithub.com%2Ftest-samples%26state%3D9284564475%26session%3D98765%26session_state%3D45645654567%26code%3D9844646765%26storageType%3Dpersistent'; - const pathname = '/f'; - - const newLocation = sanitizeLocation({ search, pathname } as Location); - - expect(newLocation.search).not.toEqual(search); - expect(newLocation.search).toEqual( - '?url=https%3A%2F%2Fgithub.com%2Ftest-samples%26storageType%3Dpersistent', - ); - expect(newLocation.pathname).toEqual(pathname); - }); - - it('should return sanitized value of location.pathname', () => { - const search = '?url=https%3A%2F%2Fgithub.com%2Ftest-samples'; - const pathname = '/f&code=1239844646765'; - - const newLocation = sanitizeLocation({ search, pathname } as Location); - - expect(newLocation.search).toEqual(search); - expect(newLocation.pathname).not.toEqual(pathname); - expect(newLocation.pathname).toEqual('/f'); - }); -}); diff --git a/packages/dashboard-frontend/src/services/helpers/environment.ts b/packages/dashboard-frontend/src/services/helpers/environment.ts deleted file mode 100644 index ba70a94d0..000000000 --- a/packages/dashboard-frontend/src/services/helpers/environment.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2018-2023 Red Hat, Inc. - * This program and the accompanying materials are made - * available under the terms of the Eclipse Public License 2.0 - * which is available at https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Red Hat, Inc. - initial API and implementation - */ - -export type EnvironmentType = 'production' | 'development'; -export type Environment = { - type: EnvironmentType; - server?: string; -}; -export type ProdEnvironment = { - type: 'production'; -}; -export type DevEnvironment = { - type: 'development'; - server: string; -}; - -export function getEnvironment(): Environment { - const env: Environment = { - type: - (process && process.env && (process.env.ENVIRONMENT as EnvironmentType)) === 'development' - ? 'development' - : 'production', - }; - return env; -} -export function isDevEnvironment(env: Environment): env is DevEnvironment { - if (env.type === 'development') { - return true; - } - return false; -} -export function isProdEnvironment(env: Environment): env is ProdEnvironment { - return isDevEnvironment(env) === false; -} diff --git a/packages/dashboard-frontend/src/services/helpers/location.ts b/packages/dashboard-frontend/src/services/helpers/location.ts index fb67637ba..d5ee14019 100644 --- a/packages/dashboard-frontend/src/services/helpers/location.ts +++ b/packages/dashboard-frontend/src/services/helpers/location.ts @@ -97,43 +97,3 @@ function _buildLocationObject(pathAndQuery: string): Location { export function toHref(history: History, location: Location): string { return history.createHref(location); } - -const oauthParams = ['state', 'session', 'session_state', 'code']; -/** - * Removes oauth params. - */ -export function sanitizeLocation( - location: T, - removeParams: string[] = [], -): T { - const toRemove = [...oauthParams, ...removeParams]; - // clear search params - if (location.search) { - const searchParams = new window.URLSearchParams(location.search); - - // sanitize the URL inside searchParams - const targetParam = 'url'; - let targetValue = searchParams.get(targetParam); - if (targetValue !== null) { - toRemove.forEach(param => { - const re = new RegExp('[&|?]' + param + '=[^&]+', 'i'); - if (targetValue) { - targetValue = targetValue.replace(re, ''); - } - }); - searchParams.set(targetParam, targetValue); - } - - toRemove.forEach(val => searchParams.delete(val)); - location.search = '?' + searchParams.toString(); - } - - // clear pathname - toRemove.forEach(param => { - const re = new RegExp('&' + param + '=[^&]+', 'i'); - const newPathname = location.pathname.replace(re, ''); - location.pathname = newPathname; - }); - - return location; -}