Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .deps/EXCLUDED/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This file contains a manual contribution to .deps/dev.md and it's needed because
| `@eclipse-che/api@7.86.0` | ecd.che |
| `@eclipse-che/license-tool@2.0.0` | ecd.che |
| `axios@1.15.2` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/axios/1.15.2) |
| `jsbn@0.1.1` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/jsbn/0.1.1) |
| `postcss@8.5.13` | [clearlydefined](https://clearlydefined.io/definitions/npm/npmjs/-/postcss/8.5.13) |


Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from '@/components/WorkspaceProgress/CreatingSteps/Apply/Devfile/getGitRemotes';
import { buildFactoryLoaderPath } from '@/preload/main';
import { FactoryLocationAdapter } from '@/services/factory-location-adapter';
import { REVISION } from '@/services/helpers/factoryFlow/buildFactoryParams';
import { REVISION_ATTR } from '@/services/helpers/factoryFlow/buildFactoryParams';

const BR_NAME_REGEX = /^[0-9A-Za-z-./_]{1,256}$/;

Expand Down Expand Up @@ -189,7 +189,7 @@ function setBranchToAzureDevOpsLocation(location: string, branch: string | undef

export function setBranchToLocation(location: string, branch: string | undefined): string {
if (!FactoryLocationAdapter.isHttpLocation(location)) {
return branch ? `${location}?revision=${branch}` : location;
return branch ? `${location}?${REVISION_ATTR}=${branch}` : location;
}
const url = new URL(location);
const pathname = url.pathname.replace(/^\//, '').replace(/\/$/, '');
Expand Down Expand Up @@ -326,7 +326,7 @@ export function getGitRepoOptionsFromLocation(location: string): {
console.log(`Unable to get branch from '${location}'.${common.helpers.errors.getMessage(e)}`);
}
} else if (!FactoryLocationAdapter.isHttpLocation(location)) {
gitBranch = searchParams.get(REVISION) || undefined;
gitBranch = searchParams.get(REVISION_ATTR) || undefined;
}
return { location, gitBranch, remotes, devfilePath, hasSupportedGitService };
}
Expand Down Expand Up @@ -449,7 +449,7 @@ export function setGitRepoOptionsToLocation(
state.gitBranch = newOptions.gitBranch;
}
if (!FactoryLocationAdapter.isHttpLocation(location) && newOptions.gitBranch) {
searchParams.set(REVISION, newOptions.gitBranch);
searchParams.set(REVISION_ATTR, newOptions.gitBranch);
}
// update the location with the new gitBranch value
let searchParamsStr = decodeURIComponent(searchParamsToString(searchParams));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
*/

.sampleCard {
border: var(--pf-v6-c-card--BorderWidth) var(--pf-v6-c-card--BorderStyle) var(--pf-t--global--border--color--default);
border: var(--pf-v6-c-card--BorderWidth) var(--pf-v6-c-card--BorderStyle)
var(--pf-t--global--border--color--default);
}

.sampleCardIcon {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,44 @@ describe('GitRepoURL', () => {
expect(screen.queryByRole('button', { name: copyButtonName })).toBeTruthy;
expect(screen.queryByRole('button', { name: copyButtonNameAfter })).toBeFalsy;
});

test('airgap sample URL: percent-encoded query string decoded correctly', async () => {
// Regression for CRW-9659: the old split('=')[1] parser truncated the URL at the
// second '=' sign. For an airgap URL stored as
// url=http://…/download%3Fid%3Dnodejs-express&che-editor=…&storageType=per-user
// it produced "…/download%3Fid%3Dnodejs-express?che-editor=…" — a double-'?' URL
// with the '=nodejs-express' value missing.
// URLSearchParams.get('url') correctly decodes %3F→? and %3D→=, giving
// "…/download?id=nodejs-express", and appends other params with '&'.
const devWorkspace = new DevWorkspaceBuilder()
.withMetadata({
name: 'test-workspace',
annotations: {
[DEVWORKSPACE_DEVFILE_SOURCE]: dump({
factory: {
params:
'url=http://che-dashboard.eclipse-che.svc:8080/dashboard/api/airgap-sample/devfile/download%3Fid%3Dnodejs-express&che-editor=che-incubator%2Fche-code%2Flatest&storageType=per-user',
},
}),
},
})
.build();

const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
renderComponent(constructWorkspace(devWorkspace));

const copyButton = screen.getByRole('button', { name: 'Copy to clipboard' });
await user.click(copyButton);

const copied = mockClipboard.mock.calls[0][0] as string;
// The URL must not contain a double '?' — that was the symptom of the bug.
expect(copied.split('?').length - 1).toBe(1);
// The decoded query param must be preserved.
expect(copied).toContain('id=nodejs-express');
// Factory params are still included but appended with '&', not '?'.
expect(copied).toContain('&che-editor=');
expect(copied).toContain('&storageType=per-user');
});
});

function getComponent(workspace: Workspace) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,37 +79,43 @@ class GitRepoFormGroup extends React.PureComponent<Props, State> {
};
};

const factoryParams = devfileSource?.factory?.params;
if (factoryParams === undefined) {
const rawFactoryParams = devfileSource?.factory?.params;
if (rawFactoryParams === undefined) {
return source;
}

const paramsArr = factoryParams.split('&');
if (paramsArr.length === 0) {
// Use URLSearchParams to correctly extract the 'url' value.
// The old split('=')[1] approach truncated the URL at the second '=' character,
// producing malformed display URLs for airgap sample URLs whose query string
// contains '=' signs (e.g. %3Fid%3Dnodejs-express decoded to ?id=nodejs-express).
const parsedParams = new URLSearchParams(rawFactoryParams);
const urlValue = parsedParams.get('url');
if (!urlValue) {
return source;
}

paramsArr.forEach(param => {
const [key, value] = param.split('=');
if (key === 'url') {
if (!source.gitRepo && paramsArr.length === 1) {
source.gitRepo = value;
} else {
source.gitRepo = value + '?' + source.gitRepo;
}
source.isUrl = new RegExp('^https?://').test(value);
source.fieldName = source.isUrl ? new URL(value).pathname.replace(/^\//, '') : value;
if (source.fieldName.length > 50) {
source.fieldName = source.fieldName.substring(0, 50) + '...';
}
} else {
if (source.gitRepo.length !== 0 && !source.gitRepo.endsWith('?')) {
source.gitRepo = source.gitRepo + '&';
}
source.gitRepo = source.gitRepo + key + '=' + value;
source.isUrl = /^https?:\/\//.test(urlValue);
source.fieldName = source.isUrl ? new URL(urlValue).pathname.replace(/^\//, '') : urlValue;
if (source.fieldName.length > 50) {
source.fieldName = source.fieldName.substring(0, 50) + '...';
}

// Reconstruct the full display URL: base URL + any additional factory params
// (editor image, storage type, etc.) so the link reflects the full workspace config.
const otherParams: string[] = [];
parsedParams.forEach((value, key) => {
if (key !== 'url') {
otherParams.push(`${key}=${value}`);
}
});

if (otherParams.length === 0) {
source.gitRepo = urlValue;
} else {
const separator = urlValue.includes('?') ? '&' : '?';
source.gitRepo = urlValue + separator + otherParams.join('&');
}

return source;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,24 @@ export function buildFactoryParams(searchParams: URLSearchParams): FactoryParams

function getSourceUrl(searchParams: URLSearchParams): string {
const devworkspaceResourcesUrl = getDevworkspaceResourcesUrl(searchParams);
return devworkspaceResourcesUrl !== undefined
? devworkspaceResourcesUrl
: getFactoryUrl(searchParams);
if (devworkspaceResourcesUrl !== undefined) {
return devworkspaceResourcesUrl;
}
const factoryUrl = getFactoryUrl(searchParams);
if (!factoryUrl) {
return factoryUrl;
}
// Decode percent-encoded characters so this value matches workspace.source, which is
// read via URLSearchParams.get() from the DevWorkspace annotation and therefore has
// one additional level of decoding applied (e.g. %3F → ?, %3D → =).
// This fixes comparison failures for airgap sample URLs that are double-encoded in
// the factory hash (%253Fid%253Dnodejs-express → %3Fid%3Dnodejs-express after first
// decode, then ?id=nodejs-express after the second decode in workspace.source).
try {
return decodeURIComponent(factoryUrl);
} catch {
return factoryUrl;
}
}

function getDevworkspaceResourcesUrl(searchParams: URLSearchParams): string | undefined {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,12 +185,12 @@ export class WorkspaceAdapter<T extends devfileApi.DevWorkspace> implements Work
* It can be an HTTPS or SSH URL.
*/
get source(): string | undefined {
const devfileSourseStr = this.workspace.metadata.annotations?.[DEVWORKSPACE_DEVFILE_SOURCE];
if (!devfileSourseStr) {
const devfileSourceStr = this.workspace.metadata.annotations?.[DEVWORKSPACE_DEVFILE_SOURCE];
if (!devfileSourceStr) {
return undefined;
}
// Parse the devfile source annotation to extract the repository URL
const devfileSourse = load(devfileSourseStr) as {
const devfileSource = load(devfileSourceStr) as {
factory?: {
params?: string;
};
Expand All @@ -202,26 +202,27 @@ export class WorkspaceAdapter<T extends devfileApi.DevWorkspace> implements Work
location?: string;
};
};
// Check if the devfile source has a factory with parameters
const factoryParams = devfileSourse?.factory?.params;
if (factoryParams) {
// Split the factory params string into an array of parameters
const paramsArr = factoryParams.split('&');
if (paramsArr.length > 0) {
// Find the URL parameter in the factory params
const targetParam = paramsArr.find(param => param.startsWith('url='));
if (targetParam) {
return targetParam.split('=')[1];
}
// Check if the devfile source has a factory with parameters.
// Use URLSearchParams.get() so that '=' characters inside the URL value are
// handled correctly (the old split('=')[1] approach truncates the value at the
// second '=' which breaks URLs containing query parameters like ?id=foo).
// URLSearchParams.get() also decodes percent-encoded characters (%3F → ?, %3D → =)
// matching the decoding applied in buildFactoryParams.getSourceUrl().
const rawFactoryParams = devfileSource?.factory?.params;
if (rawFactoryParams) {
const factoryParams = new URLSearchParams(rawFactoryParams);
const location = factoryParams.get('url');
if (location) {
return location;
}
}
// Check if the devfile source has a repository URL
const repo = devfileSourse?.scm?.repo;
const repo = devfileSource?.scm?.repo;
if (repo) {
return repo;
}
// Check if the devfile source has a URL location
const location = devfileSourse?.url?.location;
const location = devfileSource?.url?.location;
if (location) {
return location;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,33 @@ function getRepoPattern(url: string): RegExp | undefined {
}
}

/**
* Regex pattern to match airgap sample URLs that should be automatically trusted.
* Matches URLs containing: /dashboard/api/airgap-sample/devfile/download
* Examples:
* - http://che-dashboard.eclipse-che.svc:8080/dashboard/api/airgap-sample/devfile/download?name=JBoss+EAP+8
* - http://localhost:8080/dashboard/api/airgap-sample/devfile/download?id=java-lombok&che-editor=...
*/
export const AIRGAP_SAMPLE_PATTERN = /\/dashboard\/api\/airgap-sample\/devfile\/download/;

export function isTrustedRepo(
trustedSources: api.TrustedSources | undefined,
url: string | URL,
): boolean {
const urlString = url.toString();

// Always trust airgap sample URLs (internal samples)
if (AIRGAP_SAMPLE_PATTERN.test(urlString)) {
return true;
}

if (trustedSources === undefined) {
return false;
}
if (trustedSources === '*') {
return true;
}

const urlString = url.toString();
const urlPattern = getRepoPattern(urlString);
const urlRepo = extractRepo(urlString, urlPattern);

Expand Down
Loading