const fs = require('fs')
const path = require('path')
const { tables: defaultTables } = require('../data/tables')
const STATIC_DIR = path.join(__dirname, 'static')
const ICON_PATH = path.join(__dirname, '..', '..', 'public', 'images', 'banmanager-icon.png')
const safeRead = (filePath, encoding) => {
try {
return fs.readFileSync(filePath, encoding)
} catch (e) {
console.warn(`[installer] Could not load ${filePath}: ${e.message}`)
return null
}
}
const HTML_TEMPLATE = safeRead(path.join(STATIC_DIR, 'installer.html'), 'utf8')
const CSS = safeRead(path.join(STATIC_DIR, 'installer.css'), 'utf8')
const JS = safeRead(path.join(STATIC_DIR, 'installer.js'), 'utf8')
const ICON = safeRead(ICON_PATH)
const FALLBACK_HTML = '
' +
'BanManager WebUI Setup' +
'' +
'Setup assets are missing
' +
'The installer static files could not be loaded. This usually means the WebUI was deployed without ' +
'the server/setup/static/ directory or the build step did not include it.
' +
'Please run npx bmwebui setup from a shell on this host instead, or re-deploy the WebUI ' +
'with all bundled files intact.
' +
''
const escapeHtml = (str) =>
String(str == null ? '' : str).replace(/[&<>"']/g, (ch) => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[ch])
const renderTemplate = (tpl, vars) =>
tpl.replace(/\{\{(\w+)\}\}/g, (_, key) => (key in vars && vars[key] != null ? vars[key] : ''))
const escapeForScript = (json) =>
json
.replace(//g, '\\u003e')
.replace(/&/g, '\\u0026')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029')
// BASE_PATH is operator-controlled via env, but we still validate the format
// before splicing it into HTML attributes / JS strings. Anything that doesn't
// match a strict subpath (e.g. /admin or /admin/webui) is treated as empty
// to avoid breaking attributes or smuggling characters into the markup.
const BASE_PATH_REGEX = /^\/[a-zA-Z0-9_-]+(\/[a-zA-Z0-9_-]+)*$/
const sanitiseBasePath = (basePath) => {
if (!basePath) return ''
if (typeof basePath !== 'string') return ''
if (!BASE_PATH_REGEX.test(basePath)) return ''
return basePath
}
const buildHtml = ({ clientIp, isSecure, isLoopback, requireToken, version, basePath } = {}) => {
if (!HTML_TEMPLATE) return FALLBACK_HTML
const safeBasePath = sanitiseBasePath(basePath)
const insecureBanner = (!isSecure && !isLoopback)
? 'Your connection is not encrypted. Passwords entered here could be intercepted. Use HTTPS or run setup over localhost (an SSH tunnel works).
'
: ''
const setupConfig = escapeForScript(JSON.stringify({
requireToken: Boolean(requireToken),
isSecure: Boolean(isSecure),
isLoopback: Boolean(isLoopback),
clientIp: clientIp || '',
basePath: safeBasePath,
defaultTables
}))
return renderTemplate(HTML_TEMPLATE, {
basePath: escapeHtml(safeBasePath),
clientIp: escapeHtml(clientIp || 'unknown'),
versionLabel: version ? 'v' + escapeHtml(version) : '',
insecureBanner,
setupConfig
})
}
module.exports = {
buildHtml,
escapeHtml,
hasAssets: () => Boolean(HTML_TEMPLATE && CSS && JS),
getCss: () => CSS || '/* installer.css missing */',
getJs: () => JS || '/* installer.js missing */',
getIcon: () => ICON
}