const path = require('path'), send = require('koa-send'), nunjucks = require('nunjucks'), hljs = require('highlight.js'), MarkdownIt = require('markdown-it'), katex = require('markdown-it-katex'), options = require('../options'), perm = require('../permission'), builtin = require('../model/builtin'), md5 = require('../lib/md5'), { MIDDLEWARE } = require('../service/server'), { NotFoundError } = require('../error'); class Markdown extends MarkdownIt { constructor() { super({ linkify: true, highlight: function (str, lang) { if (lang && hljs.getLanguage(lang)) try { return hljs.highlight(lang, str).value; } catch (__) { } // eslint-disable-line no-empty return ''; } }); this.linkify.tlds('.py', false); this.use(katex); } } const md = new Markdown(); function datetime_span(dt, { relative = true } = {}) { if (dt.generationTime) dt = new Date(dt.generationTime * 1000); else if (typeof dt == 'number' || typeof dt == 'string') dt = new Date(dt); return '{2}'.format( relative ? ' relative' : '', dt.getTime() / 1000, dt.toLocaleString() ); } class Nunjucks extends nunjucks.Environment { constructor() { super( new nunjucks.FileSystemLoader(path.resolve(options.template.path)), { autoescape: true, trimBlocks: true } ); this.addFilter('json', function (self) { return JSON.stringify(self); }, false); this.addFilter('assign', function (self, data) { return Object.assign(self, data); }); this.addFilter('markdown', function (self) { return md.render(self); }); this.addFilter('gravatar_url', function (email, size) { return `//gravatar.loli.net/avatar/${md5(email.toString().trim().toLowerCase())}?d=mm&s=${size}`; }); this.addFilter('format_size', function (size, base = 1) { size *= base; let unit = 1024; let unit_names = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; for (let unit_name of unit_names) { if (size < unit) return '{0} {1}'.format(Math.round(size), unit_name); size /= unit; } return '{0} {1}'.format(Math.round(size * unit), unit_names[unit_names.length - 1]); }); this.addGlobal('typeof', o => typeof o); this.addGlobal('console', console); this.addGlobal('static_url', str => `/${str}`); this.addGlobal('reverse_url', str => str); this.addGlobal('datetime_span', datetime_span); this.addGlobal('perm', perm); this.addGlobal('builtin', builtin); this.addGlobal('status', builtin.STATUS); } } let env = new Nunjucks(); MIDDLEWARE(async (ctx, next) => { let done = false; if (ctx.method === 'HEAD' || ctx.method === 'GET') try { done = await send(ctx, ctx.path, { root: path.resolve(process.cwd(), '.uibuild'), index: 'index.html' }); } catch (err) { if (err.status !== 404) throw err; } if (!done) await next(); }); MIDDLEWARE(async (ctx, next) => { ctx.render_title = str => str; ctx.UIContext = { cdn_prefix: '/', url_prefix: '/' }; ctx.preferJson = (ctx.request.headers['accept'] || '').includes('application/json'); ctx.renderHTML = (name, context) => { ctx.user = ctx.state.user; ctx.translate = str => { if (!str) return ''; return str.toString().translate(ctx.state.user.language); }; ctx.has_perm = perm => ctx.state.user.hasPerm(perm); return new Promise((resolve, reject) => { env.render(name, Object.assign(ctx.state, context, { handler: ctx, _: ctx.translate, user: ctx.state.user }), (error, res) => { if (error) reject(error); else resolve(res); }); }); }; ctx.render = async (name, context) => { ctx.body = await ctx.renderHTML(name, context); ctx.response.type = 'text/html'; }; try { try { await next(); if (ctx.body || ctx.templateName) { if (ctx.preferJson) return; if (ctx.query.template || ctx.templateName) { ctx.body = ctx.body || {}; Object.assign(ctx.body, JSON.parse(ctx.query.data || '{}')); await ctx.render(ctx.query.template || ctx.templateName, ctx.body); } else if (ctx.setRedirect) { ctx.response.type = 'application/octet-stream'; ctx.redirect(ctx.setRedirect); } } else { throw new NotFoundError(); } } catch (error) { if (error instanceof NotFoundError) ctx.status = 404; if (error.toString().startsWith('NotFoundError')) console.log(error); if (error.toString().startsWith('Template render error')) throw error; if (ctx.preferJson) ctx.body = { error }; else await ctx.render('error.html', { error }); } } catch (error) { if (ctx.preferJson) ctx.body = { error }; else await ctx.render('bsod.html', { error }); } }, true);