Skip to content

Commit 1d60871

Browse files
Jimmy-Joseph19github-actions[bot]Klaus-Keller
authored
reusable xml id validation method (#4584)
* reusable xml id validation method * removed async method and moved the source and test to project folder --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Klaus Keller <[email protected]>
1 parent dc9e096 commit 1d60871

7 files changed

Lines changed: 252 additions & 18 deletions

File tree

.changeset/funny-socks-rescue.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@sap-ux/project-access': minor
3+
'@sap-ux/fe-fpm-writer': patch
4+
---
5+
6+
Added isUI5IdUnique function to check UI5 control ID uniqueness in XML views/fragments.
7+
Exported findFilesByExtension from @sap-ux/project-access and removed deep imports.

packages/fe-fpm-writer/src/common/file.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import type { CopyOptions, Editor } from 'mem-fs-editor';
22
import type { TabInfo } from '../common/types';
33
import { sep, normalize } from 'node:path';
4-
import { findFilesByExtension } from '@sap-ux/project-access/dist/file';
5-
import { DOMParser } from '@xmldom/xmldom';
4+
import { findFilesByExtension, isUI5IdUnique } from '@sap-ux/project-access';
65

76
/**
87
* Options for creating an ID generator with cached file contents.
@@ -168,25 +167,16 @@ export const CONFIG = {
168167
function generateUniqueElementId(baseId: string, filteredFilesContent: string[], validatedIds: string[] = []): string {
169168
const maxAttempts = 1000;
170169

171-
function checkElementIdAvailable(id: string, xmlContent: string): boolean {
172-
const xmlDocument = new DOMParser({ errorHandler: (): void => {} }).parseFromString(xmlContent);
173-
return xmlDocument.documentElement ? !xmlDocument.getElementById(id) : true;
174-
}
175-
176-
if (
177-
filteredFilesContent.every((content) => content === '' || checkElementIdAvailable(baseId, content)) &&
178-
!validatedIds.includes(baseId)
179-
) {
170+
// Check both in-memory validatedIds and filesystem files
171+
if (!validatedIds.includes(baseId) && isUI5IdUnique(baseId, filteredFilesContent)) {
180172
return baseId;
181173
}
182174

183175
for (let counter = 1; counter < maxAttempts; counter++) {
184176
const candidateId = `${baseId}${counter}`;
185177

186-
if (
187-
filteredFilesContent.every((content) => content === '' || checkElementIdAvailable(candidateId, content)) &&
188-
!validatedIds.includes(candidateId)
189-
) {
178+
// Check both in-memory validatedIds and filesystem files
179+
if (!validatedIds.includes(candidateId) && isUI5IdUnique(candidateId, filteredFilesContent)) {
190180
return candidateId;
191181
}
192182
}

packages/fiori-docs-embeddings/data_local/ux-ui5-tooling-README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,27 @@ Executing `npx fiori run` in your project with the configuration below in the `u
7777
url: https://my.backend.com:1234
7878
```
7979

80+
#### [Using connectPath for credential retrieval](#using-connectpath-for-credential-retrieval)
81+
82+
When credentials are saved against a full service URL (for example, `https://my.backend.com:1234/sap/opu/odata/example/service`) rather than just the base URL, you need to specify the `connectPath` parameter to ensure the correct credentials are retrieved. The `connectPath` should match the path portion of the URL against which the credentials were saved.
83+
84+
```yaml
85+
- name: fiori-tools-proxy
86+
afterMiddleware: compression
87+
configuration:
88+
backend:
89+
- path: /sap
90+
url: https://my.backend.com:1234
91+
connectPath: /sap/opu/odata/UI5/ABAP_REPOSITORY_SRV
92+
```
93+
94+
**When to use `connectPath`:**
95+
- Your credentials are stored in the SAP System configuration with a full service URL including the path
96+
- You need to connect to a specific service endpoint for authentication
97+
- The stored system URL differs from the base server URL
98+
99+
**Note:** If credentials are saved against the base URL only (e.g., `https://my.backend.com:1234`), the `connectPath` parameter is not needed.
100+
80101
#### [Connecting to a back-end system with destination](#connecting-to-a-back-end-system-with-destination)
81102

82103
If the back-end is hidden behind a destination then you can also provide the `destination` in the configuration.
@@ -170,6 +191,21 @@ Let's that you want to configure the proxy to send requests from a certain path
170191
destination: my_backend
171192
```
172193
194+
#### [Add Query Parameters](#add-query-parameters)
195+
Add query parameters to the proxied request by using the `params` configuration option, e.g.
196+
197+
```
198+
- name: fiori-tools-proxy
199+
afterMiddleware: compression
200+
configuration:
201+
backend:
202+
- path: /sap
203+
url: https://my.backend.com:1234
204+
params:
205+
saml2: 'disabled'
206+
```
207+
208+
173209
#### [Providing Proxy Configuration](#providing-proxy-configuration)
174210
By the default the `fiori-tools-proxy` will read the proxy configuration from the Node.js environment variables `proxy`, `https-proxy` and `noproxy`. If those variables are not set, then you can also provide the proxy configuration in the `ui5.yaml` file. **Please note: if you want to exclude any domains from the proxy then you will need to set the `noproxy` variable, e.g. `npm config set noproxy "sap.com"`**.
175211
@@ -220,6 +256,7 @@ Here is the full list of the available configuration options for the backend pro
220256
- `ignorePath` (available with version 1.8.5): true/false, Default: false - specify whether you want to ignore the proxy path of the incoming request (note: you will have to append / manually if required)
221257
- `localAddress` (available with version 1.8.5): Local interface string to bind for outgoing connections
222258
- `changeOrigin` (available with version 1.8.5): true/false, Default: true - changes the origin of the host header to the target URL
259+
- `params` (available with version 1.23.0): object, adds query parameters to the proxied request
223260
- `preserveHeaderKeyCase` (available with version 1.8.5): true/false, Default: false - specify whether you want to keep letter case of response header key
224261
- `auth` (available with version 1.8.5): Basic authentication i.e. 'user:password' to compute an Authorization header
225262
- `hostRewrite` (available with version 1.8.5): rewrites the location hostname on (301/302/307/308) redirects

packages/project-access/src/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export {
66
MinCdsVersion,
77
fioriToolsDirectory
88
} from './constants';
9-
export { getFilePaths } from './file';
9+
export { getFilePaths, findFilesByExtension } from './file';
1010
export { normalizePath } from './path';
1111
export {
1212
addPackageDevDependency,
@@ -67,10 +67,11 @@ export {
6767
readFlexChanges,
6868
processServices,
6969
getMainService,
70-
getGlobalCdsHomePath
70+
getGlobalCdsHomePath,
71+
isUI5IdUnique
7172
} from './project';
7273
export { execNpmCommand } from './command/npm-command';
7374
export * from './types';
74-
export * from './library';
75+
export { checkDependencies, getReuseLibs } from './library';
7576
export { findRecursiveHierarchyKey, getTableCapabilitiesByEntitySet } from './odata';
7677
export { hasDependency } from './project';

packages/project-access/src/project/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ export {
6161
refreshSpecificationDistTags
6262
} from './specification';
6363
export { readFlexChanges } from './flex-changes';
64+
export { isUI5IdUnique } from './ui5-xml-id-validator';
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { XMLParser } from 'fast-xml-parser';
2+
3+
/**
4+
* Recursively checks if an object (parsed XML) contains an element with the specified id attribute.
5+
*
6+
* @param obj - parsed XML object to search in
7+
* @param id - id to search for
8+
* @param attrPrefix - attribute prefix used by the parser (default: '@_')
9+
* @returns true if an element with the id is found
10+
*/
11+
function hasElementWithId(obj: unknown, id: string, attrPrefix = '@_'): boolean {
12+
if (typeof obj !== 'object' || obj === null) {
13+
return false;
14+
}
15+
16+
const objRecord = obj as Record<string, unknown>;
17+
const idAttr = `${attrPrefix}id`;
18+
19+
// Check if this element has the id attribute
20+
if (objRecord[idAttr] === id) {
21+
return true;
22+
}
23+
24+
for (const key in objRecord) {
25+
if (key.startsWith(attrPrefix)) {
26+
continue; // Skip attributes
27+
}
28+
29+
if (checkIdInValue(objRecord[key], id, attrPrefix)) {
30+
return true;
31+
}
32+
}
33+
34+
return false;
35+
}
36+
37+
/**
38+
* Checks if a value (object or array) contains an element with the specified id.
39+
*
40+
* @param value - value to check (can be array or object)
41+
* @param id - id to search for
42+
* @param attrPrefix - attribute prefix used by the parser
43+
* @returns true if id is found in the value
44+
*/
45+
function checkIdInValue(value: unknown, id: string, attrPrefix: string): boolean {
46+
if (Array.isArray(value)) {
47+
return value.some((item) => hasElementWithId(item, id, attrPrefix));
48+
}
49+
if (typeof value === 'object' && value !== null) {
50+
return hasElementWithId(value, id, attrPrefix);
51+
}
52+
return false;
53+
}
54+
55+
/**
56+
* Checks if an element with the specified id is available (does not exist) in the XML content.
57+
*
58+
* @param id - id to check for availability
59+
* @param xmlContent - XML content as string
60+
* @returns true if the id is available (not found), false if it exists
61+
*/
62+
function checkElementIdAvailable(id: string, xmlContent: string): boolean {
63+
const parser = new XMLParser({
64+
ignoreAttributes: false,
65+
attributeNamePrefix: '@_',
66+
parseAttributeValue: false
67+
});
68+
69+
try {
70+
const xmlDocument: unknown = parser.parse(xmlContent);
71+
return xmlDocument ? !hasElementWithId(xmlDocument, id) : true;
72+
} catch {
73+
// Parse error = no valid document = no element with id
74+
return true;
75+
}
76+
}
77+
78+
/**
79+
* Checks if a UI5 control ID is unique across XML files (fragments and views).
80+
*
81+
* @param id - ID to check
82+
* @param files - Array of XML file contents to check
83+
* @returns true if the id is unique (available), false if it already exists
84+
*/
85+
export function isUI5IdUnique(id: string, files: string[]): boolean {
86+
return files.every((content) => content === '' || checkElementIdAvailable(id, content));
87+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { isUI5IdUnique } from '../../src/project/ui5-xml-id-validator';
2+
3+
describe('isUI5IdUnique', () => {
4+
const sampleView = `<mvc:View
5+
xmlns:mvc="sap.ui.core.mvc"
6+
xmlns="sap.m"
7+
controllerName="my.app.controller.Main">
8+
<Page id="mainPage" title="Main View">
9+
<content>
10+
<Button id = "submitButton" text="Submit" />
11+
<Input id="nameInput" placeholder="Enter name" />
12+
<Table id ="dataTable">
13+
<columns>
14+
<Column>
15+
<Text text="Name" />
16+
</Column>
17+
</columns>
18+
</Table>
19+
</content>
20+
</Page>
21+
</mvc:View>`;
22+
23+
const sampleFragment = `<core:FragmentDefinition
24+
xmlns="sap.m"
25+
xmlns:core="sap.ui.core">
26+
<Dialog id="confirmDialog" title="Confirm Action">
27+
<content>
28+
<Text id= "dialogText" text="Are you sure?" />
29+
</content>
30+
<beginButton>
31+
<Button id="confirmButton" text="Confirm" press="onConfirm" />
32+
</beginButton>
33+
<endButton>
34+
<Button id="cancelButton" text="Cancel" press="onCancel" />
35+
</endButton>
36+
</Dialog>
37+
</core:FragmentDefinition>`;
38+
39+
const sampleViewWithNamespace = `<mvc:View
40+
xmlns:mvc="sap.ui.core.mvc"
41+
xmlns="sap.m"
42+
xmlns:f="sap.ui.layout.form">
43+
<f:SimpleForm id="detailForm">
44+
<f:content>
45+
<Label text="Title" />
46+
<Input id="titleInput" />
47+
</f:content>
48+
</f:SimpleForm>
49+
</mvc:View>`;
50+
51+
test('should return true when id does not exist in any files', () => {
52+
const result = isUI5IdUnique('newButton', [sampleView, sampleFragment]);
53+
expect(result).toBe(true);
54+
});
55+
56+
test('should return false when id exists in view', () => {
57+
const result = isUI5IdUnique('submitButton', [sampleView, sampleFragment]);
58+
expect(result).toBe(false);
59+
});
60+
61+
test('should return false when id exists in fragment', () => {
62+
const result = isUI5IdUnique('confirmDialog', [sampleView, sampleFragment]);
63+
expect(result).toBe(false);
64+
});
65+
66+
test('should return true when id is unique across multiple files', () => {
67+
const result = isUI5IdUnique('uniqueId', [sampleView, sampleFragment, sampleViewWithNamespace]);
68+
expect(result).toBe(true);
69+
});
70+
71+
test('should return false when id exists in nested elements', () => {
72+
const result = isUI5IdUnique('dataTable', [sampleView]);
73+
expect(result).toBe(false);
74+
});
75+
76+
test('should return false when id exists in fragment dialog content', () => {
77+
const result = isUI5IdUnique('dialogText', [sampleFragment]);
78+
expect(result).toBe(false);
79+
});
80+
81+
test('should return true for empty files array', () => {
82+
const result = isUI5IdUnique('anyId', []);
83+
expect(result).toBe(true);
84+
});
85+
86+
test('should return true when XML parsing fails', () => {
87+
// fast-xml-parser is lenient, but completely invalid content should fail
88+
const invalidXml = '<<<>>><invalid';
89+
const result = isUI5IdUnique('test', [invalidXml]);
90+
expect(result).toBe(true);
91+
});
92+
93+
test('should return true when files contain empty strings', () => {
94+
const result = isUI5IdUnique('testId', ['', '', sampleView]);
95+
expect(result).toBe(true);
96+
});
97+
98+
test('should handle ids with special characters', () => {
99+
const xmlWithSpecialId = `<?xml version="1.0" encoding="UTF-8"?>
100+
<mvc:View xmlns:mvc="sap.ui.core.mvc" xmlns="sap.m">
101+
<Button id="button-with-dash" text="Test" />
102+
<Button id="button_with_underscore" text="Test" />
103+
<Button id="button.with.dot" text="Test" />
104+
</mvc:View>`;
105+
106+
expect(isUI5IdUnique('button-with-dash', [xmlWithSpecialId])).toBe(false);
107+
expect(isUI5IdUnique('button_with_underscore', [xmlWithSpecialId])).toBe(false);
108+
expect(isUI5IdUnique('button.with.dot', [xmlWithSpecialId])).toBe(false);
109+
expect(isUI5IdUnique('button-not-exists', [xmlWithSpecialId])).toBe(true);
110+
});
111+
});

0 commit comments

Comments
 (0)