fix: having <body> in custom-html made web interface inaccessible
Massimo Melina committed
Mar 18, 2024 at 12:18 UTC
6dc8fdb934df79868f4d44d1c36eb2ae7991d2db
1 file changed
+50
-46
src/serveGuiFiles.ts
+50
-46
@@ -90,53 +90,57 @@ async function treatIndex(ctx: Koa.Context, filesUri: string, body: string) {
90
})))
91
const timestamp = await getFaviconTimestamp()
92
const lang = await getLangData(ctx)
93
- let ret = body
93
+ return body
94
.replace(/((?:src|href) *= *['"])\/?(?!([a-z]+:\/)?\/)/g, '$1' + ctx.state.revProxyPath + filesUri)
95
- .replace('<head>', () => `<head>
96
- ${!isFrontend ? '' : `
97
- <title>${title.get()}</title>
98
- <link rel="shortcut icon" href="/favicon.ico?${timestamp}" />
99
- `}
100
- <script>
101
- HFS = ${JSON.stringify({
102
- VERSION,
103
- API_VERSION,
104
- SPECIAL_URI, PLUGINS_PUB_URI, FRONTEND_URI,
105
- session: session instanceof ApiError ? null : session,
106
- plugins,
107
- prefixUrl: ctx.state.revProxyPath,
108
- dontOverwriteUploading: dontOverwriteUploading.get(),
109
- customHtml: _.omit(Object.fromEntries(customHtmlState.sections),
110
- ['top','bottom']), // exclude the sections we already apply in this phase
111
- ...newObj(FRONTEND_OPTIONS, (v, k) => getConfig(k)),
112
- lang
113
- }, null, 4)
114
- .replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
115
- document.documentElement.setAttribute('ver', '${VERSION.split('-')[0] /*for style selectors*/}')
116
- </script>
117
- `)
118
- .replace('<body>', () => `<body>
119
- <style>
120
- :root {
121
- ${_.map(plugins, (configs, pluginName) =>
122
- _.map(configs, (v,k) => `--${pluginName}-${k}: ${serializeCss(v)};`).join('\n')).join('')}
123
- }
124
- </style>
125
- ${!isFrontend ? '' : mapPlugins((plug,id) =>
126
- plug.frontend_css?.map(f =>
127
- `<link rel='stylesheet' type='text/css' href='${f.includes('//') ? f : pub + id + '/' + f}' plugin=${JSON.stringify(id)}/>`))
128
- .flat().filter(Boolean).join('\n')}
129
- ${!isFrontend ? '' : mapPlugins((plug,id) =>
130
- plug.frontend_js?.map(f =>
131
- `<script defer plugin=${JSON.stringify(id)} src='${f.includes('//') ? f : pub + id + '/' + f}'></script>`))
132
- .flat().filter(Boolean).join('\n')}
133
- `)
134
- if (isFrontend)
135
- ret = ret
136
- .replace('<head>', '<head>' + getSection('htmlHead'))
137
- .replace('<body>', '<body>' + getSection('top'))
138
- .replace('</body>', getSection('bottom') + '</body>')
139
- return ret
95
+ .replace(/<(\/)?(head|body)>/g, (all, isClose, name) => { // must make these changes in one .replace call, otherwise we may encounter head/body tags due to customHtml. This simple trick makes html parsing unnecessary.
96
+ const isHead = name === 'head'
97
+ const isBody = !isHead
98
+ const isOpen = !isClose
99
+ if (isHead && isOpen)
100
+ return all + `
101
+ ${!isFrontend ? '' : `
102
+ <title>${title.get()}</title>
103
+ <link rel="shortcut icon" href="/favicon.ico?${timestamp}" />
104
+ ` + getSection('htmlHead')}
105
+ <script>
106
+ HFS = ${JSON.stringify({
107
+ VERSION,
108
+ API_VERSION,
109
+ SPECIAL_URI, PLUGINS_PUB_URI, FRONTEND_URI,
110
+ session: session instanceof ApiError ? null : session,
111
+ plugins,
112
+ prefixUrl: ctx.state.revProxyPath,
113
+ dontOverwriteUploading: dontOverwriteUploading.get(),
114
+ customHtml: _.omit(Object.fromEntries(customHtmlState.sections),
115
+ ['top', 'bottom']), // exclude the sections we already apply in this phase
116
+ ...newObj(FRONTEND_OPTIONS, (v, k) => getConfig(k)),
117
+ lang
118
+ }, null, 4).replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
119
+ document.documentElement.setAttribute('ver', '${VERSION.split('-')[0] /*for style selectors*/}')
120
+ </script>
121
+ `
122
+ if (isBody && isOpen)
123
+ return all + `
124
+ ${!isFrontend ? '' : getSection('top')}
125
+ <style>
126
+ :root {
127
+ ${_.map(plugins, (configs, pluginName) =>
128
+ _.map(configs, (v,k) => `--${pluginName}-${k}: ${serializeCss(v)};`).join('\n')).join('')}
129
+ }
130
+ </style>
131
+ ${!isFrontend ? '' : mapPlugins((plug,id) =>
132
+ plug.frontend_css?.map(f =>
133
+ `<link rel='stylesheet' type='text/css' href='${f.includes('//') ? f : pub + id + '/' + f}' plugin=${JSON.stringify(id)}/>`))
134
+ .flat().filter(Boolean).join('\n')}
135
+ ${!isFrontend ? '' : mapPlugins((plug,id) =>
136
+ plug.frontend_js?.map(f =>
137
+ `<script defer plugin=${JSON.stringify(id)} src='${f.includes('//') ? f : pub + id + '/' + f}'></script>`))
138
+ .flat().filter(Boolean).join('\n')}
139
+ `
140
+ if (isBody && isClose)
141
+ return getSection('bottom') + all
142
+ return all // unchanged
143
+ })
144
}
145
146
function serializeCss(v: any) {