Skip to content

Commit e667957

Browse files
Enhance configuration file handling, validate filename input and adjust paths (#554)
1 parent d52b4c4 commit e667957

18 files changed

Lines changed: 291 additions & 120 deletions

File tree

src/main/frontend/app/actions/navigationActions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type NavigateFunction } from 'react-router'
22
import useTabStore from '~/stores/tab-store'
33
import useEditorTabStore from '~/stores/editor-tab-store'
4+
import { getBaseName } from '~/utils/path-utils'
45

56
export function openInStudio(
67
navigate: NavigateFunction,
@@ -46,7 +47,7 @@ export function openInEditorAtElement(
4647
{ subtype, filepath, name }: { subtype: string; filepath: string; name?: string },
4748
) {
4849
const editorStore = useEditorTabStore.getState()
49-
const fileName = filepath.split(/[/\\]/).pop() ?? filepath
50+
const fileName = getBaseName(filepath)
5051

5152
if (!editorStore.getTab(filepath)) {
5253
editorStore.setTabData(filepath, {

src/main/frontend/app/components/directory-picker/directory-picker.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { filesystemService } from '~/services/filesystem-service'
55
import type { FilesystemEntry } from '~/types/filesystem.types'
66
import { ApiError } from '~/utils/api'
77
import { useDirectoryWatcher } from '~/hooks/use-file-watcher'
8+
import { normalizePath } from '~/utils/path-utils'
89
import Button from '../inputs/button'
910
import CloseButton from '../inputs/close-button'
1011

@@ -36,9 +37,9 @@ export default function DirectoryPicker({
3637
setIsCreatingFolder(false)
3738
try {
3839
const result = await filesystemService.browse(path)
39-
setEntries(result.entries)
40-
setCurrentPath(result.resolvedPath)
41-
setParentPath(result.parentPath)
40+
setEntries(result.entries.map((entry) => ({ ...entry, path: normalizePath(entry.path) })))
41+
setCurrentPath(normalizePath(result.resolvedPath))
42+
setParentPath(normalizePath(result.parentPath))
4243
} catch (error) {
4344
if (error instanceof ApiError && error.httpCode === 403) {
4445
setError('Access denied')

src/main/frontend/app/components/file-structure/name-input-dialog.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export const CONFIGURATION_NAME_PATTERNS: Record<string, RegExp> = {
2222

2323
export const FOLDER_OR_ADAPTER_NAME_PATTERNS: Record<string, RegExp> = BASE_NAME_PATTERNS
2424

25+
export const PROJECT_NAME_PATTERNS: Record<string, RegExp> = {
26+
...BASE_NAME_PATTERNS,
27+
'Cannot have a file extension': /^(?!.*\.[^.\\/]+$).*$/,
28+
}
29+
2530
type NameInputDialogProps = {
2631
title: string
2732
initialValue?: string

src/main/frontend/app/components/file-structure/studio-file-structure.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ function studioTabItemId(activeTab: string, dataProvider: StudioFilesDataProvide
4747
const lastSep = rest.lastIndexOf('::')
4848
const adapterName = lastSep === -1 ? rest : rest.slice(0, lastSep)
4949
const position = lastSep === -1 ? '0' : rest.slice(lastSep + 2)
50-
const rootPath = dataProvider.getRootPath().replace(/[/\\]$/, '')
51-
return `${toTreeItemId(configPath, rootPath)}/${adapterName}::${position}`
50+
return `${toTreeItemId(configPath, dataProvider.getRootPath())}/${adapterName}::${position}`
5251
}
5352

5453
function getItemTitle(item: TreeItem<StudioItemData>): string {

src/main/frontend/app/components/file-structure/tree-utilities.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import MailIcon from '../../../icons/solar/Mailbox.svg?react'
55
import FolderIcon from '../../../icons/solar/Folder.svg?react'
66
import type { FileTreeNode } from '~/types/filesystem.types'
77
import type { TreeRef } from 'react-complex-tree'
8+
import { relativeTo } from '~/utils/path-utils'
89

910
export function getListenerIcon(listenerType: string | null) {
1011
if (!listenerType) return CodeIcon
@@ -31,8 +32,8 @@ export function getAncestorIds(itemId: string): string[] {
3132
}
3233

3334
export function toTreeItemId(absolutePath: string, rootPath: string): string {
34-
const relativePath = absolutePath.slice(rootPath.length).replace(/^[/\\]/, '')
35-
return `root/${relativePath.split(/[/\\]/).join('/')}`
35+
const relativePath = relativeTo(rootPath, absolutePath)
36+
return relativePath ? `root/${relativePath}` : 'root'
3637
}
3738

3839
export function isVisibleInTree(itemId: string | null, expandedItems: string[]): boolean {

src/main/frontend/app/components/file-structure/use-studio-context-menu.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
FILE_NAME_PATTERNS,
1414
FOLDER_OR_ADAPTER_NAME_PATTERNS,
1515
} from '~/components/file-structure/name-input-dialog'
16+
import { getBaseName, getParentPath, joinPath, relativeTo } from '~/utils/path-utils'
1617
import { openInStudio } from '~/actions/navigationActions'
1718

1819
export type StudioItemType = 'root' | 'folder' | 'configuration' | 'adapter' | 'file'
@@ -58,7 +59,7 @@ export function detectItemType(data: StudioItemData, isFolder?: boolean): Studio
5859

5960
if (isFolder) return 'folder'
6061

61-
const lastSegment = path.split(/[/\\]/).at(-1) ?? path
62+
const lastSegment = getBaseName(path)
6263
if (lastSegment.includes('.')) return 'file'
6364
return 'folder'
6465
}
@@ -70,11 +71,6 @@ export function getItemName(data: StudioItemData): string {
7071
return 'Unnamed'
7172
}
7273

73-
function getParentDir(filePath: string): string {
74-
const lastSep = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'))
75-
return lastSep > 0 ? filePath.slice(0, lastSep) : filePath
76-
}
77-
7874
function ensureXmlExtension(name: string): string {
7975
if (name.endsWith('.xml')) return name
8076
return `${name}.xml`
@@ -92,12 +88,12 @@ export function resolveItemPaths(
9288

9389
if (itemType === 'adapter') {
9490
const configPath = (data as StudioAdapterData).configPath
95-
return { path: configPath, folderPath: getParentDir(configPath) }
91+
return { path: configPath, folderPath: getParentPath(configPath) }
9692
}
9793

9894
const folderData = data as StudioFolderData
9995
if (itemType === 'configuration' || itemType === 'file') {
100-
return { path: folderData.path, folderPath: getParentDir(folderData.path) }
96+
return { path: folderData.path, folderPath: getParentPath(folderData.path) }
10197
}
10298

10399
return { path: folderData.path, folderPath: folderData.path }
@@ -179,13 +175,13 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
179175
onSubmit: async (name: string) => {
180176
const fileName = ensureXmlExtension(name)
181177
try {
182-
const folderPath = menu.folderPath.replace(/[/\\]$/, '')
183-
const absoluteFilePath = `${folderPath}/${fileName}`
184-
const { adapterName, adapterPosition } = await createConfigurationFile(projectName, absoluteFilePath)
178+
const relativeFolder = relativeTo(dataProvider.getRootPath(), menu.folderPath)
179+
const relativePath = relativeFolder ? joinPath(relativeFolder, fileName) : fileName
180+
const { adapterName, adapterPosition } = await createConfigurationFile(projectName, relativePath)
185181
await dataProvider.reloadDirectory('root')
186182

187183
if (adapterName) {
188-
openInStudio(navigate, { adapterName, filepath: absoluteFilePath, adapterPosition })
184+
openInStudio(navigate, { adapterName, filepath: relativePath, adapterPosition })
189185
}
190186
} catch (error) {
191187
logApiError('Failed to create configuration', error as Error)
@@ -273,7 +269,7 @@ export function useStudioContextMenu({ projectName, dataProvider }: UseStudioCon
273269
clearConfigurationFileCache(projectName, menu.path)
274270
useTabStore.getState().renameTabsForConfig(menu.path, newPath)
275271
} else {
276-
await renameFile(projectName, menu.path, `${getParentDir(menu.path)}/${newName}`)
272+
await renameFile(projectName, menu.path, `${getParentPath(menu.path)}/${newName}`)
277273
}
278274
await dataProvider.reloadDirectory('root')
279275
} catch (error) {

src/main/frontend/app/routes/configurations/add-configuration-modal.tsx

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import CloseButton from '~/components/inputs/close-button'
77
import Input from '~/components/inputs/input'
88
import DirectoryPicker from '~/components/directory-picker/directory-picker'
99
import { fetchProject } from '~/services/project-service'
10+
import { containsPathSeparator, joinPath, relativeTo } from '~/utils/path-utils'
1011

1112
type AddConfigurationModalProperties = {
1213
isOpen: boolean
@@ -47,14 +48,21 @@ export default function AddConfigurationModal({
4748
setLoading(false)
4849
return
4950
}
50-
// Ensure .xml suffix
51+
52+
if (containsPathSeparator(configname) || configname.includes('..')) {
53+
setError(String.raw`Filename cannot contain "/", "\" or ".."`)
54+
setLoading(false)
55+
return
56+
}
57+
5158
if (!configname.toLowerCase().endsWith('.xml')) {
5259
configname = `${configname}.xml`
5360
}
5461

55-
const folderPath = rootLocationName.replace(/[/\\]$/, '')
56-
const absoluteFilePath = `${folderPath}/${configname}`
57-
await createConfigurationFile(currentConfiguration.name, absoluteFilePath)
62+
const relativeFolder = relativeTo(currentConfiguration.rootPath, rootLocationName)
63+
const relativePath = relativeFolder ? joinPath(relativeFolder, configname) : configname
64+
65+
await createConfigurationFile(currentConfiguration.name, relativePath)
5866
const updatedProject = await fetchProject(currentConfiguration.name)
5967
setProject(updatedProject)
6068
onSuccess?.()
@@ -85,10 +93,13 @@ export default function AddConfigurationModal({
8593
setIsOpenPickerOpen(false)
8694
}
8795

96+
const trimmedFilename = filename.trim()
97+
const filenameHasInvalidChars = containsPathSeparator(trimmedFilename) || trimmedFilename.includes('..')
98+
const isFilenameValid = trimmedFilename.length > 0 && !filenameHasInvalidChars
99+
88100
const displayFilename = (() => {
89-
const trimmed = filename.trim()
90-
if (!trimmed) return ''
91-
return trimmed.toLowerCase().endsWith('.xml') ? trimmed : `${trimmed}.xml`
101+
if (!trimmedFilename) return ''
102+
return trimmedFilename.toLowerCase().endsWith('.xml') ? trimmedFilename : `${trimmedFilename}.xml`
92103
})()
93104

94105
return (
@@ -122,19 +133,25 @@ export default function AddConfigurationModal({
122133
<label className="text-sm font-medium" htmlFor="configuration-filename-input">
123134
Filename
124135
</label>
125-
<div className="ml-2 flex w-full items-center">
126-
<Input
127-
id="configuration-filename-input"
128-
value={filename}
129-
onChange={(event) => setFilename(event.target.value)}
130-
placeholder="Choose a filename"
131-
aria-label="configuration filename"
132-
/>
136+
<div className="ml-2 flex w-full flex-col">
137+
<div className="flex w-full items-center gap-1">
138+
<Input
139+
id="configuration-filename-input"
140+
value={filename}
141+
onChange={(event) => setFilename(event.target.value)}
142+
placeholder="Choose a filename"
143+
aria-label="configuration filename"
144+
/>
145+
<span className="text-foreground-muted text-sm">.xml</span>
146+
</div>
147+
{filenameHasInvalidChars && (
148+
<p className="mt-1 text-xs text-red-500">{String.raw`Filename cannot contain "/", "\" or ".."`}</p>
149+
)}
133150
</div>
134151
</div>
135152

136153
<div className="flex gap-2">
137-
<Button onClick={handleAdd} disabled={loading} className="disabled:opacity-50">
154+
<Button onClick={handleAdd} disabled={loading || !isFilenameValid} className="disabled:opacity-50">
138155
{loading ? 'Adding...' : `Add ${displayFilename || 'configuration file'} to ${currentConfiguration.name}`}
139156
</Button>
140157

src/main/frontend/app/routes/configurations/configuration-file-tile.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import IconButton from '~/components/inputs/icon-button'
77
import IconLabelButton from '~/components/inputs/icon-label-button'
88
import ConfirmDeleteDialog from '~/components/file-structure/confirm-delete-dialog'
99
import { useState } from 'react'
10+
import { getBaseName } from '~/utils/path-utils'
1011

1112
type ConfigurationFileTileProperties = {
1213
filepath: string
@@ -29,7 +30,7 @@ export default function ConfigurationFileTile({
2930
}
3031

3132
const handleOpenInEditor = () => {
32-
const fileName = relativePath.split(/[/\\]/).pop()
33+
const fileName = getBaseName(relativePath)
3334
if (!fileName) return
3435
openInEditor(fileName, filepath, navigate)
3536
}
@@ -82,7 +83,7 @@ export default function ConfigurationFileTile({
8283

8384
{showDeleteDialog && (
8485
<ConfirmDeleteDialog
85-
name={relativePath.split(/[/\\]/).pop() ?? relativePath}
86+
name={getBaseName(relativePath)}
8687
isFolder={false}
8788
onCancel={() => setShowDeleteDialog(false)}
8889
onConfirm={handleConfirmDelete}

src/main/frontend/app/routes/configurations/configuration-overview.tsx

Lines changed: 4 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,33 +11,14 @@ import type { FileTreeNode } from '~/types/filesystem.types'
1111
import { fetchProjectTree } from '~/services/file-tree-service'
1212
import Button from '~/components/inputs/button'
1313
import Search from '~/components/search/search'
14-
import { normalizePath, toRelativePath } from '~/utils/path-utils'
14+
import { relativeTo } from '~/utils/path-utils'
1515

1616
type ConfigurationFile = {
1717
path: string
1818
relativePath: string
1919
adapterNames: string[]
2020
}
2121

22-
function findConfigurationsDir(node: FileTreeNode | undefined | null): FileTreeNode | null {
23-
if (!node || !node.path) return null
24-
25-
const normalizedPath = normalizePath(node.path)
26-
27-
if (node.type === 'DIRECTORY' && normalizedPath.endsWith(`/src/main/configurations/${node.name}`)) {
28-
return node
29-
}
30-
31-
if (!node.children) return null
32-
33-
for (const child of node.children) {
34-
const found = findConfigurationsDir(child)
35-
if (found) return found
36-
}
37-
38-
return null
39-
}
40-
4122
function collectXmlFiles(node: FileTreeNode | undefined | null): FileTreeNode[] {
4223
let result: FileTreeNode[] = []
4324
if (!node) return result
@@ -61,7 +42,6 @@ export default function ConfigurationOverview() {
6142
const [showModal, setShowModal] = useState(false)
6243
const [tree, setTree] = useState<FileTreeNode | null>(null)
6344
const [isLoading, setIsLoading] = useState(false)
64-
const [configurationsDir, setConfigurationsDir] = useState<FileTreeNode | null>(null)
6545
const [searchQuery, setSearchQuery] = useState('')
6646
const [debouncedQuery, setDebouncedQuery] = useState(searchQuery)
6747

@@ -91,13 +71,6 @@ export default function ConfigurationOverview() {
9171
return () => controller.abort()
9272
}, [loadTree])
9373

94-
useEffect(() => {
95-
if (tree) {
96-
const configDir = findConfigurationsDir(tree)
97-
setConfigurationsDir(configDir)
98-
}
99-
}, [tree])
100-
10174
const handleConfigAdded = useCallback(() => {
10275
setShowModal(false)
10376
loadTree()
@@ -112,12 +85,9 @@ export default function ConfigurationOverview() {
11285
const configFiles = useMemo(() => {
11386
if (!tree || !currentConfigurationProject) return []
11487

115-
const configurationDirectory = findConfigurationsDir(tree)
116-
if (!configurationDirectory) return []
117-
118-
const xmlFiles = collectXmlFiles(configurationDirectory)
88+
const xmlFiles = collectXmlFiles(tree)
11989
return xmlFiles.map((file) => {
120-
const relativePath = toRelativePath(file.path, `${configurationDirectory.path}/`) ?? file.name
90+
const relativePath = relativeTo(tree.path, file.path) || file.name
12191
return { ...file, relativePath, path: file.path }
12292
})
12393
}, [tree, currentConfigurationProject])
@@ -208,7 +178,7 @@ export default function ConfigurationOverview() {
208178
onClose={() => setShowModal(false)}
209179
onSuccess={handleConfigAdded}
210180
currentConfiguration={currentConfigurationProject}
211-
configurationsDirPath={configurationsDir?.path ?? ''}
181+
configurationsDirPath={tree?.path ?? ''}
212182
/>
213183
</div>
214184
)

src/main/frontend/app/routes/projectlanding/clone-configuration-modal.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Button from '~/components/inputs/button'
44
import CloseButton from '~/components/inputs/close-button'
55
import Input from '~/components/inputs/input'
66
import { filesystemService } from '~/services/filesystem-service'
7+
import { joinPath } from '~/utils/path-utils'
78

89
type CloneProjectModalProperties = {
910
isLocal: boolean
@@ -39,6 +40,9 @@ export default function CloneConfigurationModal({
3940
.pop()
4041
?.replace(/\.git$/i, '')
4142

43+
const targetName = repoName || 'cloned-project'
44+
const targetPath = location ? joinPath(location, targetName) : targetName
45+
4246
const handleClone = () => {
4347
if (!repoUrl.trim()) return
4448
if (isLocal && !location) return
@@ -113,14 +117,7 @@ export default function CloneConfigurationModal({
113117
</div>
114118
)}
115119

116-
{repoName && (
117-
<p className="text-foreground-muted mb-4 text-xs">
118-
Will clone to:{' '}
119-
{isLocal
120-
? `${location}${location.includes('/') ? '/' : '\\'}${repoName}`
121-
: `${location ? `${location}/` : ''}${repoName}`}
122-
</p>
123-
)}
120+
{repoName && <p className="text-foreground-muted mb-4 text-xs">Will clone to: {targetPath}</p>}
124121

125122
<div className="flex gap-2">
126123
<Button

0 commit comments

Comments
 (0)