fix: admin/shared: link field not working for roots with special chars; also fall back to other roots that are not baseUrl
Massimo Melina committed
May 28, 2026 at 10:17 UTC
eab409e7cef4ecae0d128ae3d292817853a6bbc5
2 files changed
+29
-7
admin/src/FileForm.ts
+28
-6
@@ -8,7 +8,7 @@ import {
8
} from '@hfs/mui-grid-form'
9
import { apiCall, UseApi } from './api'
10
import {
11
- basename, defaultPerms, formatBytes, formatTimestamp, isWhoObject, newDialog, objSameKeys,
11
+ basename, defaultPerms, formatBytes, formatTimestamp, isWhoObject, newDialog, objSameKeys, pathEncode,
12
onlyTruthy, prefix, VfsPerms, wantArray, Who, WhoObject, matches, xlate, md, Callback, MASK_IN_TESTS,
13
useRequestRender, splitAt, IMAGE_FILEMASK, copyTextToClipboard, normalizeHost, CFG, try_, WHO_ANY_ACCOUNT,
14
} from './misc'
@@ -349,12 +349,28 @@ function LinkField({ value, statusApi }: LinkFieldProps) {
349
const data = statusApi.getData()
350
351
const urls: string[] = data && (data.urls.https || data.urls.http || [data.base_url])
352
- const baseHost = try_(() => normalizeHost(new URL(data?.baseUrl).host)) // URL can throw on malformed data
353
- const root = useMemo(() => baseHost && _.find(data.roots, (_root, host) => matches(baseHost, host)),
354
- [data])
352
+ const baseHost = try_(() => new URL(data?.baseUrl).host) // URL can throw on malformed data
353
+ const roots = data?.roots || {}
354
+ const root = baseHost && _.find(roots, (_root, host) => matches(baseHost, host))
355
+ const originalValue = value
356
if (root)
356
- value &&= value.indexOf(root) === 1 ? value.slice(root.length) : undefined
357
- const link = prefix(data?.baseUrl || '', value)
357
+ value = pathInRoot(value, root)
358
+ let linkBase = data?.baseUrl || ''
359
+ if (value === undefined) { // baseUrl didn't match, but other hosts in roots may
360
+ const base = try_(() => new URL(linkBase))
361
+ if (base) {
362
+ const sorted = _.sortBy(Object.entries(roots), ([, root]) => -String(root).length) // prioritize longer roots because are more specific
363
+ for (const [hostMask, root] of sorted) {
364
+ if (typeof root !== 'string') continue
365
+ value = pathInRoot(originalValue, root)
366
+ const host = value && hostMask.split('|').find(x => x && !/[*?]/.test(x) && x !== baseHost)
367
+ if (!host) continue
368
+ linkBase = base.protocol + '//' + host
369
+ break
370
+ }
371
+ }
372
+ }
373
+ const link = prefix(linkBase, value)
374
const RenderLink = useMemo(() => forwardRef((props: any, ref) =>
375
h(Link, {
376
ref,
@@ -414,6 +430,12 @@ function LinkField({ value, statusApi }: LinkFieldProps) {
430
console.error('Error generating QR code:', error);
431
}
432
}
433
+
434
+ function pathInRoot(uri: string | undefined, root: string | undefined) {
435
+ if (!root || root === '/') return uri
436
+ root = pathEncode(root)
437
+ return uri?.startsWith(root, 1) ? uri.slice(root.length) : undefined
438
+ }
439
}
440
441
export async function changeBaseUrl() {
shared/index.ts
+1
-1
@@ -129,7 +129,7 @@ export function getHFS() {
129
}
130
131
export function getPrefixUrl() {
132
- return getHFS().prefixUrl
132
+ return getHFS().prefixUrl || ''
133
}
134
135
export function makeSessionRefresher(state: any) {