Source: editors/index.js

const express = require('express');
const fs = require('fs-extra');
const path = require('path');
const { fail } = require('./util.js');

/*
 * What the admin interfaces' app editors ask the server, at /api/editor/<dialect>/<action> (POST,
 * JSON; admins only, as all of /api): jtree's own editor (admin/v2) for .jtt apps, the z-Tree
 * editor (admin/ztree) for z-Tree treatments, and the oTree Studio editor (admin/otree) for oTree
 * projects. Each dialect's module reads its files into what its editor shows (a model), changes
 * them as its editor asks (apply), and writes them. Files are read and written only in the app
 * folders (settings.appFolders), and after a write the app catalogue is loaded again.
 */

const DIALECTS = {
    jtree: require('./jtree.js'),
    ztree: require('./ztree.js'),
    otree: require('./otree.js'),
};

/** The folders apps are in, absolute. */
function appRoots(jt) {
    return (jt.settings.appFolders || []).map((f) => path.resolve(jt.path, f));
}

/** p (absolute, or relative to the first app folder), if it is in an app folder; else throws. */
function inAppFolders(jt, p) {
    if (typeof p !== 'string' || p === '') fail('no path given');
    const roots = appRoots(jt);
    if (!roots.length) fail('jtree has no app folders (settings.appFolders)');
    const abs = path.resolve(roots[0], p);
    if (!roots.some((r) => abs === r || abs.startsWith(r + path.sep))) fail('not in an app folder: ' + p);
    return abs;
}

/** What the editors are given to work with: where files may go, and loading the apps again. */
function context(jt) {
    return {
        roots: () => appRoots(jt),
        resolve: (p) => inAppFolders(jt, p),
        /** Writes text to file, making its folder if need be. */
        write(file, text, encoding = 'utf8') {
            fs.mkdirpSync(path.dirname(file));
            fs.writeFileSync(file, text, encoding);
        },
        /** Loads the app catalogue again, and tells the admins. */
        reload() {
            jt.data.reloadApps();
            if (jt.socketServer) jt.socketServer.refreshAdmins(null);
        },
        jt,
    };
}

/** The router for /api/editor. */
function router(jt) {
    const r = express.Router();
    r.use(express.json({ limit: '50mb' }));
    r.post('/:dialect/:action', async (req, res) => {
        const dialect = DIALECTS[req.params.dialect];
        const action = dialect && dialect.actions && dialect.actions[req.params.action];
        if (!action) return res.status(404).send({ error: 'no editor action ' + req.params.dialect + '/' + req.params.action });
        try {
            res.send(await action(req.body || {}, context(jt)));
        } catch (err) {
            if (!err.editorMessage) console.log('editor ' + req.params.dialect + '/' + req.params.action + ': ' + err.stack);
            res.status(400).send({ error: err.message });
        }
    });
    return r;
}

module.exports = { router, inAppFolders };