"use strict";
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.logGui = void 0;
exports.serveGuiFiles = serveGuiFiles;
const const_1 = require("./const");
const serveFile_1 = require("./serveFile");
const plugins_1 = require("./plugins");
const api_auth_1 = require("./api.auth");
const apiMiddleware_1 = require("./apiMiddleware");
const path_1 = require("path");
const misc_1 = require("./misc");
const adminApis_1 = require("./adminApis");
const customHtml_1 = require("./customHtml");
const lodash_1 = __importDefault(require("lodash"));
const config_1 = require("./config");
const lang_1 = require("./lang");
const upload_1 = require("./upload");
const icons_1 = require("./icons");
const middlewares_1 = require("./middlewares");
const size1024 = (0, config_1.defineConfig)(misc_1.CFG.size_1024, false, x => misc_1.formatBytes.k = x ? 1024 : 1000); // we both configure formatBytes, and also provide a compiled version (number instead of boolean)
const splitUploads = (0, config_1.defineConfig)(misc_1.CFG.split_uploads, 0);
exports.logGui = (0, config_1.defineConfig)(misc_1.CFG.log_gui, false);
lodash_1.default.each(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.defineConfig)(k, v)); // define default values
function serveStatic(uri) {
    const folder = (const_1.DEV ? 'dist/' : '') + uri.slice(2, -1);
    return async (ctx) => {
        if (!exports.logGui.get())
            ctx.state.dontLog = true;
        if (ctx.method === 'OPTIONS') {
            ctx.status = const_1.HTTP_NO_CONTENT;
            ctx.set({ Allow: 'OPTIONS, GET' });
            return;
        }
        if (ctx.method !== 'GET')
            return ctx.status = const_1.HTTP_METHOD_NOT_ALLOWED;
        const serveApp = shouldServeApp(ctx);
        const fullPath = (0, path_1.join)(__dirname, '..', folder, serveApp ? '/index.html' : ctx.path);
        const content = await (0, misc_1.parseFile)(fullPath, raw => serveApp || !raw.length ? raw : adjustBundlerLinks(ctx, uri, raw), 1000)
            .catch(e => {
            if (e?.code !== 'ENOENT') // not supposed to happen, and yet a user reported a strange behavior
                console.error(`serveStatic/parseFile: ${String(e)}`);
            return null;
        });
        if (content === null)
            return ctx.status = const_1.HTTP_NOT_FOUND;
        if (!serveApp)
            return (0, serveFile_1.serveFile)(ctx, fullPath, const_1.MIME_AUTO, content);
        // we don't cache the index as it's small and may prevent plugins change to apply
        ctx.body = await treatIndex(ctx, uri, String(content));
    };
}
function shouldServeApp(ctx) {
    return ctx.state.serveApp ||= ctx.path.endsWith('/') && !ctx.headers.upgrade; // skip websockets
}
function adjustBundlerLinks(ctx, uri, data) {
    const ext = (0, path_1.extname)(ctx.path);
    return ext && !ext.match(/\.(css|html|js|ts|scss)/) ? data
        : String(data).replace(/((?:import[ (]| from )['"])\//g, `$1${ctx.state.revProxyPath}${uri}`);
}
const getFaviconTimestamp = (0, misc_1.debounceAsync)(async () => {
    const f = adminApis_1.favicon.get();
    return !f ? 0 : (0, misc_1.statWithTimeout)(f).then(x => x?.mtimeMs || 0, () => 0);
}, { retain: 5_000 });
async function treatIndex(ctx, filesUri, body) {
    const session = await api_auth_1.authApis.refresh_session({}, ctx);
    ctx.set('etag', '');
    ctx.set('Cache-Control', 'no-store, no-cache, must-revalidate');
    ctx.type = 'html';
    const isFrontend = filesUri === const_1.FRONTEND_URI ? ' ' : ''; // as a string will allow neater code later
    const pub = ctx.state.revProxyPath + const_1.PLUGINS_PUB_URI;
    // expose plugins' configs that are declared with 'frontend' attribute
    const plugins = Object.fromEntries((0, misc_1.onlyTruthy)((0, plugins_1.mapPlugins)((pl, name) => {
        let configs = (0, misc_1.newObj)((0, plugins_1.getPluginConfigFields)(name), (v, k, skip) => !v.frontend ? skip() :
            adjustValueByConfig(plugins_1.pluginsConfig.get()?.[name]?.[k], pl.getData().config?.[k]));
        configs = (0, plugins_1.getPluginInfo)(name).onFrontendConfig?.(configs) || configs;
        return !lodash_1.default.isEmpty(configs) && [name, configs];
    })));
    const timestamp = await getFaviconTimestamp();
    const lang = await (0, lang_1.getLangData)(ctx);
    return body
        .replace(/((?:src|href) *= *['"])\/?(?!([a-z]+:\/)?\/)(?!\?)/g, '$1' + ctx.state.revProxyPath + filesUri)
        .replace(/<(\/)?(head|body)>/g, (all, isClose, name) => {
        const isHead = name === 'head';
        const isBody = !isHead;
        const isOpen = !isClose;
        if (isHead && isOpen)
            return all + `
                    <script>
                    HFS = ${JSON.stringify({
                VERSION: const_1.VERSION,
                API_VERSION: const_1.API_VERSION,
                SPECIAL_URI: const_1.SPECIAL_URI, PLUGINS_PUB_URI: const_1.PLUGINS_PUB_URI, FRONTEND_URI: const_1.FRONTEND_URI,
                pathSeparator: path_1.sep,
                session: session instanceof apiMiddleware_1.ApiError ? null : session,
                plugins,
                loadScripts: Object.fromEntries((0, plugins_1.mapPlugins)((p, id) => [id, p.frontend_js?.map(f => f.includes('//') ? f : pub + id + '/' + f)])),
                prefixUrl: ctx.state.revProxyPath || '',
                proxyDetected: Boolean((0, middlewares_1.getProxyDetected)()),
                dontOverwriteUploading: upload_1.dontOverwriteUploading.get(),
                splitUploads: splitUploads.get(),
                kb: size1024.compiled(),
                forceTheme: (0, plugins_1.mapPlugins)(p => lodash_1.default.isString(p.isTheme) ? p.isTheme : undefined).find(Boolean),
                customHtml: lodash_1.default.omit((0, customHtml_1.getAllSections)(), ['top', 'bottom', 'htmlHead', 'style']), // exclude the sections we already apply in this phase
                ...(0, misc_1.newObj)(misc_1.FRONTEND_OPTIONS, (v, k) => (0, config_1.getConfig)(k)),
                icons: Object.assign({}, ...(0, plugins_1.mapPlugins)(p => iconsToObj(p.icons, p.id + '/')), iconsToObj(icons_1.customizedIcons)), // name-to-uri 
                lang
            }, null, 4).replace(/<(\/script)/g, '<"+"$1') /*avoid breaking our script container*/}
                    document.documentElement.setAttribute('ver', HFS.VERSION.split('-')[0])
                    document.documentElement.setAttribute('browser', ${JSON.stringify((0, misc_1.shortenAgent)(ctx.get('user-agent')))})
                    </script>
                    ${isFrontend && `
                        <title>${adminApis_1.title.get()}</title>
                        <link rel="shortcut icon" href="${ctx.state.revProxyPath}/favicon.ico?${timestamp}" />
                    ${(0, customHtml_1.getSection)('htmlHead')}`}
                `;
        function iconsToObj(icons, pre = '') {
            return icons && lodash_1.default.mapValues(icons, (v, k) => ctx.state.revProxyPath + const_1.ICONS_URI + pre + k);
        }
        if (isBody && isOpen)
            return `${all}
                    ${isFrontend && (0, customHtml_1.getSection)('top')}
                    <style>
                    :root {
                        ${lodash_1.default.map(plugins, (configs, pluginName) => // make plugin configs accessible via css
             lodash_1.default.map(configs, (v, k) => {
                v = serializeCss(v);
                return typeof v === 'string' && `\n--${pluginName}-${k}: ${v};`;
            }).filter(Boolean).join('')).join('')}
                    }
                    ${isFrontend && (0, customHtml_1.getSection)('style')}
                    </style>
                    ${isFrontend && (0, plugins_1.mapPlugins)((plug, id) => plug.frontend_css?.map(f => `<link rel='stylesheet' type='text/css' href='${f.includes('//') ? f : pub + id + '/' + f}' plugin=${JSON.stringify(id)}/>`))
                .flat().filter(Boolean).join('\n')}
                `;
        if (isBody && isClose)
            return (0, customHtml_1.getSection)('bottom') + all;
        return all; // unchanged
    });
    function adjustValueByConfig(v, cfg) {
        v ??= cfg.defaultValue;
        const { type } = cfg;
        if (v && type === 'vfs_path') {
            v = (0, misc_1.enforceStarting)('/', v);
            const { root } = ctx.state;
            if (root)
                if (v.startsWith(root))
                    v = v.slice(root.length - 1);
                else
                    return;
            if (ctx.state.revProxyPath)
                v = ctx.state.revProxyPath + v;
        }
        else if (type === 'array' && Array.isArray(v))
            v = v.map(x => lodash_1.default.mapValues(x, (xv, xk) => adjustValueByConfig(xv, cfg.fields[xk])));
        return v;
    }
}
function serializeCss(v) {
    return typeof v === 'string' && /^#[0-9a-fA-F]{3,8}|rgba?\(.+\)$/.test(v) ? v // colors
        : (0, misc_1.isPrimitive)(v) ? JSON.stringify(v)?.replace(/</g, '&lt;') : undefined;
}
function serveProxied(port, uri) {
    if (!port)
        return;
    console.debug('Proxied on port', port);
    let proxy;
    import('koa-better-http-proxy').then(lib => // dynamic import to avoid having this in final distribution
     proxy = lib.default('127.0.0.1:' + port, {
        parseReqBody: false, // the dev GUI proxy serves app/assets, so avoid koa-better-http-proxy trying to reread ctx.req
        proxyReqPathResolver: (ctx) => shouldServeApp(ctx) ? '/' : ctx.path,
        userResDecorator(res, data, ctx) {
            return shouldServeApp(ctx) ? treatIndex(ctx, uri, String(data))
                : adjustBundlerLinks(ctx, uri, data);
        }
    }));
    return function (ctx, next) {
        if (!exports.logGui.get())
            ctx.state.dontLog = true;
        return proxy(ctx, async () => { });
    };
}
function serveGuiFiles(proxyPort, uri) {
    return serveProxied(proxyPort, uri) || serveStatic(uri);
}
