Source: core/Settings.js

const path      = require('path');
const fs        = require('fs-extra');
const Utils     = require('../Utils.js');

/** Settings that can be set in the settings.json file */
class Settings {

     constructor(jt) {

         this.jt = jt;

         // Admin access, see AdminAuth: without a password, only from the computer running jtree.
         this.admins                 = {}; // extra admin logins, {id: {pwd: '...'}}
         this.allowClientsToCreateParticipants = true; // default for new sessions
         this.askBeforeLeaving       = true; // participants' pages ask before they are closed or reloaded
         this.autoSaveFreq           = 5000; // how often to save the time (internal/serverState.json), in ms; after a crash, restored stage timers are off by up to this much.
         this.logToConsole           = false;
         this.openAdminOnStart       = true; // whether or not the admin page should be opened when the server starts.
         this.multipleUsers          = false;
         this.reloadApps             = true; // load apps every time they are accessed.
         // Whose rules apply where z-Tree, oTree and jtree differ (division by zero, an array index
         // out of range, ...): 'jtree' (they are errors, reported), 'ztree' or 'otree' (as that
         // engine does them). A session may have its own (see Session#engineMode).
         this.mode                   = 'jtree';

         this.clientJSFile           = 'internal/clients/shared/shared.js';
         this.clientJSTemplateFile   = 'internal/sharedTemplate.js';
         this.defaultAdminUI         = 'v2'; // folder in adminUIsPath served at /admin; others at /admin/<folder>
         this.participantUI          = 'internal/clients/participant';
         this.clientUI               = 'internal/clients/participant';
         this.adminUIsPath           = 'internal/clients/admin';
         this.adminUIsSharedPath     = 'internal/clients/admin/shared';
         this.appFolders             = ['apps']; // the folders apps are in: relative to jtree's folder (client/), or absolute
         this.roomsPath              = 'rooms';
         this.usersPath              = 'users';
         this.helpPath               = 'internal/docs'; // location of help documents.
         this.logPath                = 'internal/logs';
         this.sessionsFolder         = 'sessions';
         this.sharedUI               = 'internal/clients/shared/';
         this.serverTimeInfoFilename = 'internal/serverState.json'; // location of file that stores last time server was active.
         this.autoplayDelay          = 'randomInt(4,8)*1000';
         this.outputDelimiter        = ';';
         this.useHTTPS               = false;
         this.basePath               = '/jtree'; // route everything is served under; '' for the root. JAS sets it from the app's route.
         this.httpsCertificateFile   = 'certificate.pem';
         this.defaultAdminPwd        = undefined; // admin password; set it to use the admin from other computers
         this.sessionShowFullLinks   = false;
         this.loadSettings           = false;
         this.session = {};
         this.session.suggestedNumParticipants = 4;
 
         this.valsToSave = {};

         this.server = {};

         this.logMessage = null;

         /**
          * Pre-specified participant IDs.
          */
         this.participantIds         = [];
         for (let i=1; i<100; i++) {
             this.participantIds.push('P' + i);
         }

         // On Linux / Mac, ports below 1024 require sudo access.
         var isWin = process.platform === "win32";
         if (isWin) {
             this.port               = 80;
         } else {
             this.port               = 3000;
         }

         var settings = this; // so that settings can be modified without using 'jt.' prefix.
         try {
            let settingsJSONPath = path.join(this.jt.path, 'settings.json');
            if (fs.existsSync(settingsJSONPath)) {
                var json = fs.readJSONSync(settingsJSONPath);
                for (var i in json) {
                    console.log('loading custom setting: ' + i + ' = ' + (/pwd|password/i.test(i) ? '********' : JSON.stringify(json[i])));
                    this[i] = json[i];
                }
            }
         } catch (err) {
             console.log('Error loading settings JSON file: ' + err);
             this.logMessage = err;
         }
         try {
             let settingsJSPath = path.join(this.jt.path, 'settings.js');
            if (fs.existsSync(settingsJSPath)) {
                var settingsFile = fs.readFileSync(settingsJSPath, 'utf8');
                console.log('loading custom settings from settings.js');
                eval(settingsFile);
            }
        } catch (err) {
            console.log('Error reading settings JS file: ' + err);
            this.logMessage = err;
        }

         // PORT, as JAS and the launchers use, wins over settings.json; JTREE_MODE over its mode.
         if (process.env.PORT) {
             this.port               = parseInt(process.env.PORT);
         }
         if (process.env.JTREE_MODE) {
             this.mode               = process.env.JTREE_MODE;
         }
         if (!Settings.MODES.includes(this.mode)) {
             console.log('Unknown mode ' + JSON.stringify(this.mode) + ' (one of ' + Settings.MODES.join(', ') + '): using jtree');
             this.mode = 'jtree';
         }
     }

     /**
      * Filename of log file. By default, returns YYYYMMDD-HHmmss-SSS log.txt
      */
     getLogFilename() {
         return Utils.getDate(new Date()) + '.txt';
     }

     getConsoleTimeStamp() {
         return Utils.getDate(new Date());
     }

     /**
      * Return the i-th participant ID.
      *
      * @param  {type} num description
      * @return {type}     description
      */
     getParticipantId(num) {
         return this.participantIds[num];
     }

     /** Writes valsToSave into settings.json, keeping the settings already in it. */
     save() {
         let file = path.join(this.jt.path, 'settings.json');
         let json = {};
         try {
             if (fs.existsSync(file)) {
                 json = fs.readJSONSync(file);
             }
         } catch (err) {
             console.log('Error reading settings JSON file: ' + err);
         }
         Utils.writeJSON(file, Object.assign(json, this.valsToSave));
     }

     setDefaultAdminPwd(curPwd, newPwd) {
         if (this.defaultAdminPwd === undefined || curPwd === this.defaultAdminPwd) {
             this.defaultAdminPwd = newPwd;
             this.valsToSave.defaultAdminPwd = newPwd;
             this.save();
         }
     }

     setMultipleUsers(b) {
         this.multipleUsers = b;
         this.valsToSave.multipleUsers = b;
         this.save();
     }

     outputFields() {
         var fields = [];
         for (var prop in this) {
             if (
                 !Utils.isFunction(this[prop]) &&
                 prop !== 'jt'
             )
             fields.push(prop);
         }
         return fields;
     }

     /** The settings sent to admin pages, without passwords. */
     shell() {
         var out = {};
         var fields = this.outputFields();
         for (var i in fields) {
             var field = fields[i];
             out[field] = this[field];
         }
         out.defaultAdminPwd = this.defaultAdminPwd ? '********' : undefined;
         out.admins = {};
         for (var id in this.admins) {
             out.admins[id] = { pwd: '********' };
         }
         out.valsToSave = Object.assign({}, this.valsToSave);
         delete out.valsToSave.defaultAdminPwd;
         return out;
     }

}

/** The modes the server and its sessions can run in (see Settings#mode). */
Settings.MODES = ['jtree', 'ztree', 'otree'];

var exports = module.exports = {};
exports.new = Settings;
exports.MODES = Settings.MODES;