import path from 'path'
/**
* @module Helper
*/
/**
* Write HTML content to the reply object. It will read the template file,
* compile it with the provided payload, and send the resulting HTML as the response.
*
* @param {object} req - The request object
* @param {object} reply - The reply object
* @param {string} tpl - The template file path
* @param {object} payload - The data to be injected into the template
* @returns {string} - The compiled HTML content
*/
export function writeHtml (req, reply, tpl, payload) {
const { fs } = this.app.lib
const { template } = this.app.lib._
reply.header('Content-Type', 'text/html')
reply.header('Content-Language', req.lang)
const file = this.app.getPluginFile(tpl)
const content = fs.readFileSync(file, 'utf8')
const compiled = template(content)
return compiled(payload)
}
/**
* Interceptor for handling errors and delegating to specific web app handlers if available.
*
* @async
* @param {string} name - The name of the error handler to invoke
* @param {Error} err - The error object
* @param {object} req - The request object
* @param {object} reply - The reply object
* @returns {Promise<any>} - The result of the invoked handler, if any
*/
export async function interceptor (name, err, req, reply) {
const { get, trim } = this.app.lib._
const { pascalCase } = this.app.lib.aneka
let webApp = get(req, 'routeOptions.config.webApp')
const all = this.webApps.map(item => item.ns)
if (!webApp) {
const url = req.url ?? req.raw.url
const [prefix] = trim(url, '/').split('/')
const ns = this.getPluginByPrefix(prefix, true)
if (all.includes(ns)) webApp = ns
}
if (!webApp) {
const wa = this.webApps.find(item => item.prefix === '')
if (wa) webApp = wa.ns
}
if (webApp) {
const plugin = this.app[webApp]
const handler = plugin['_handle' + pascalCase(name)] ?? get(plugin, `webAppFactory.${name}Handler`)
if (handler) return await handler.call(plugin, err, req, reply)
}
}
/**
* Determine the redirect route based on the request object.
*
* @private
* @param {object} req - The request object
* @returns {string|boolean} - The redirect route or false if not found
*/
function redirSvc (req) {
const { trim, get } = this.app.lib._
const { outmatch } = this.app.lib
const matchRoute = (path, items) => {
let match = false
for (const k in items) {
const isMatch = outmatch(k)
if (isMatch(path)) {
match = items[k]
const parts = path.split('/')
const patterns = k.split('/')
for (const idx in patterns) {
if (patterns[idx] === '*') match = match.replace(`{${idx}}`, parts[idx])
}
break
}
}
return match
}
const [prefix, subPrefix, ...args] = trim(req.url.split('?')[0].split('#')[0], '/').split('/')
let plugin = this.getPluginByPrefix(prefix)
const subPlugin = this.getPluginByPrefix(subPrefix)
if (!plugin && !subPlugin) plugin = this.app.main
let route = false
if (plugin && subPlugin) {
const items = get(this, `app.${subPlugin.ns}.config.waibuMpa.redirectSubRoute.${plugin.ns}`, {})
route = matchRoute(`/${args.join('/')}`, items)
if (route) return route
}
if (plugin) {
const items = get(this, `app.${plugin.ns}.config.waibuMpa.redirect`, {})
route = matchRoute(subPrefix ? `/${subPrefix}/${args.join('/')}` : '/', items)
if (route) return route
}
return route
}
/**
* Handle 404 Not Found errors. It will check for a redirect route, and if found, redirect the user.
* If no redirect route is found, it will render a 404 error page using the specified template.
*
* @async
* @param {Error} err - The error object
* @param {object} req - The request object
* @param {object} reply - The reply object
* @returns {Promise<any>} - The result of the handler, if any
*/
export async function notFound (err, req, reply) {
const { getMethod } = this.app.bajo
let redirectTo = await redirSvc.call(this, req, reply)
if (redirectTo !== false) {
const fn = getMethod(redirectTo, false)
if (fn) redirectTo = await fn(req)
if (redirectTo) return reply.redirectTo(redirectTo)
}
reply.code(404)
const resp = await interceptor.call(this, 'notFound', err, req, reply)
if (resp) return resp
if (err && err.noContent) return ''
const payload = {
text: req.t('notFound%s%s', req.t('route'), req.url),
title: req.t('pageNotFound')
}
return writeHtml.call(this, req, reply, `${this.ns}:/lib/template/404.html`, payload)
}
/**
* Handle redirection based on the error object. It will redirect to the specified path or URL.
*
* @param {Error} err - The error object
* @param {object} req - The request object
* @param {object} reply - The reply object
* @returns {object} - The reply object
*/
export function redirect (err = {}, req, reply) {
if (err.path.startsWith('http') || path.isAbsolute(err.path)) reply.redirect(err.path)
else reply.redirect(this.routePath(err.path, err.options), err.redirectCode)
return reply
}
/**
* Collect web apps by scanning for files that match the specified glob pattern. It will import the modules and gather their metadata.
*
* @async
* @param {string} glob - The glob pattern to match files
* @param {string} baseNs - The base namespace
* @returns {Promise<Array<TWaibuWebappBoot>>} - The collected web apps
*/
export async function collectWebApps (glob = 'webapp-boot.js', baseNs) {
const { eachPlugins, importModule } = this.app.bajo
const { orderBy, get } = this.app.lib._
if (!baseNs) baseNs = this.ns
const mods = []
await eachPlugins(async function ({ file }) {
const { ns, alias, config } = this
const mod = await importModule(file, { asHandler: true })
mod.prefix = get(config, 'waibu.prefix', alias)
if (get(config, 'intl.detectors', []).includes('path')) mod.prefix = `:lang${mod.prefix === '' ? '' : ('/' + mod.prefix)}`
mod.ns = ns
mod.alias = alias
mods.push(mod)
}, { glob, prefix: baseNs })
const prefixes = {}
mods.forEach(m => {
if (!prefixes[m.prefix]) prefixes[m.prefix] = []
prefixes[m.prefix].push(m.plugin)
if (prefixes[m.prefix].length > 1) this.fatal('pluginPrefixConflic%s%s%s', m.prefix, prefixes[m.prefix][0], prefixes[m.prefix][1])
})
return orderBy(mods, ['level'])
}
/**
* Decorate the request object with additional properties and methods for handling language, translation, formatting, and settings retrieval.
*/
export function decorate () {
const me = this
this.instance.decorateRequest('lang', null)
this.instance.decorateRequest('t', () => {})
this.instance.decorateRequest('te', () => {})
this.instance.decorateRequest('format', () => {})
this.instance.decorateRequest('langDetector', null)
this.instance.decorateRequest('site', null)
this.instance.decorateRequest('ns', null)
this.instance.decorateRequest('webApp', null)
this.instance.decorateRequest('getSetting', function (key, defValue) {
return me.app.waibu.getSetting(key, { req: this, defValue })
})
}
/**
* Helper to handle file download. It will set proper content-type based on file extension.
*
* @async
* @param {string|Function} input - File path or function that returns file path. If it is a function, it will be called with ```this``` context and ```req``` as parameter
* @param {object} req - The request object_handleNot
* @param {object} reply - The reply object
* @returns {Promise<object>} - The reply object
*/
export async function download (input, req, reply) {
const { importPkg } = this.app.bajo
const { fs } = this.app.lib
const { isFunction } = this.app.lib._
const mime = await importPkg('waibu:mime')
const file = isFunction(input) ? (await input.call(this, req)) : input
if (!fs.existsSync(file)) throw this.error('_notFound')
const mimeType = mime.getType(path.extname(file))
reply.header('Content-Type', mimeType)
const stream = fs.createReadStream(file)
reply.send(stream)
return reply
}