@samitouri / QOSami-HFS / commits / 6dc8fdb9

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) {