Sync with master

This commit is contained in:
Bryan Ashby
2022-04-08 17:38:28 -06:00
177 changed files with 3729 additions and 1905 deletions

View File

@@ -11,12 +11,14 @@ const {
trackDoorRunBegin,
trackDoorRunEnd
} = require('./door_util.js');
const Log = require('./logger').log;
// deps
const async = require('async');
const assert = require('assert');
const _ = require('lodash');
const paths = require('path');
const fs = require('graceful-fs');
const activeDoorNodeInstances = {};
@@ -70,20 +72,12 @@ exports.getModule = class AbracadabraModule extends MenuModule {
// :TODO: MenuModule.validateConfig(cb) -- validate config section gracefully instead of asserts! -- { key : type, key2 : type2, ... }
// .. and/or EnigAssert
assert(_.isString(this.config.name, 'Config \'name\' is required'));
assert(_.isString(this.config.dropFileType, 'Config \'dropFileType\' is required'));
assert(_.isString(this.config.cmd, 'Config \'cmd\' is required'));
this.config.nodeMax = this.config.nodeMax || 0;
this.config.args = this.config.args || [];
}
/*
:TODO:
* disconnecting while door is open leaves dosemu
* http://bbslink.net/sysop.php support
* Font support ala all other menus... or does this just work?
*/
incrementActiveDoorNodeInstances() {
if(activeDoorNodeInstances[this.config.name]) {
activeDoorNodeInstances[this.config.name] += 1;
@@ -141,11 +135,15 @@ exports.getModule = class AbracadabraModule extends MenuModule {
return self.doorInstance.prepare(self.config.io || 'stdio', callback);
},
function generateDropfile(callback) {
const dropFileOpts = {
fileType : self.config.dropFileType,
};
if (!self.config.dropFileType || self.config.dropFileType.toLowerCase() === 'none') {
return callback(null);
}
self.dropFile = new DropFile(
self.client,
{ fileType : self.config.dropFileType }
);
self.dropFile = new DropFile(self.client, dropFileOpts);
return self.dropFile.createFile(callback);
}
],
@@ -170,17 +168,30 @@ exports.getModule = class AbracadabraModule extends MenuModule {
args : this.config.args,
io : this.config.io || 'stdio',
encoding : this.config.encoding || 'cp437',
dropFile : this.dropFile.fileName,
dropFilePath : this.dropFile.fullPath,
node : this.client.node,
env : this.config.env,
};
if (this.dropFile) {
exeInfo.dropFile = this.dropFile.fileName;
exeInfo.dropFilePath = this.dropFile.fullPath;
}
const doorTracking = trackDoorRunBegin(this.client, this.config.name);
this.doorInstance.run(exeInfo, () => {
trackDoorRunEnd(doorTracking);
this.decrementActiveDoorNodeInstances();
// Clean up dropfile, if any
if (exeInfo.dropFilePath) {
fs.unlink(exeInfo.dropFilePath, err => {
if (err) {
Log.warn({ error : err, path : exeInfo.dropFilePath }, 'Failed to remove drop file.');
}
});
}
// client may have disconnected while process was active -
// we're done here if so.
if(!this.client.term.output) {
@@ -199,7 +210,7 @@ exports.getModule = class AbracadabraModule extends MenuModule {
'\r\n\r\n'
);
this.prevMenu();
this.autoNextMenu();
});
}

View File

@@ -21,8 +21,6 @@ function ANSIEscapeParser(options) {
events.EventEmitter.call(this);
this.column = 1;
this.row = 1;
this.scrollBack = 0;
this.graphicRendition = {};
this.parseState = {
@@ -36,11 +34,15 @@ function ANSIEscapeParser(options) {
trailingLF : 'default', // default|omit|no|yes, ...
});
this.mciReplaceChar = miscUtil.valueWithDefault(options.mciReplaceChar, '');
this.termHeight = miscUtil.valueWithDefault(options.termHeight, 25);
this.termWidth = miscUtil.valueWithDefault(options.termWidth, 80);
this.trailingLF = miscUtil.valueWithDefault(options.trailingLF, 'default');
this.row = Math.min(options?.startRow ?? 1, this.termHeight);
self.moveCursor = function(cols, rows) {
self.column += cols;
self.row += rows;
@@ -69,14 +71,11 @@ function ANSIEscapeParser(options) {
};
self.clearScreen = function() {
// :TODO: should be doing something with row/column?
self.column = 1;
self.row = 1;
self.emit('clear screen');
};
/*
self.rowUpdated = function() {
self.emit('row update', self.row + self.scrollBack);
};*/
self.positionUpdated = function() {
self.emit('position update', self.row, self.column);
@@ -190,10 +189,11 @@ function ANSIEscapeParser(options) {
self.emit('mci', {
mci : mciCode,
id : id ? parseInt(id, 10) : null,
args : args,
SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true)
position : [self.row, self.column],
mci : mciCode,
id : id ? parseInt(id, 10) : null,
args : args,
SGR : ansi.getSGRFromGraphicRendition(self.graphicRendition, true)
});
if(self.mciReplaceChar.length > 0) {
@@ -215,6 +215,9 @@ function ANSIEscapeParser(options) {
}
self.reset = function(input) {
self.column = 1;
self.row = Math.min(options?.startRow ?? 1, self.termHeight);
self.parseState = {
// ignore anything past EOF marker, if any
buffer : input.split(String.fromCharCode(0x1a), 1)[0],

View File

@@ -309,7 +309,8 @@ const FONT_ALIAS_TO_SYNCTERM_MAP = {
'mo_soul' : 'mo_soul',
'mosoul' : 'mo_soul',
'mO\'sOul' : 'mo_soul',
'mo\'soul' : 'mo_soul',
'amiga_mosoul' : 'mo_soul',
'amiga_microknight' : 'microknight',
'amiga_microknight+' : 'microknight_plus',

View File

@@ -269,19 +269,16 @@ function display(client, art, options, cb) {
termHeight : client.term.termHeight,
termWidth : client.term.termWidth,
trailingLF : options.trailingLF,
startRow : options.startRow,
});
let parseComplete = false;
let cprListener;
let mciMap;
const mciCprQueue = [];
let artHash;
let mciMapFromCache;
function completed() {
if(cprListener) {
client.removeListener('cursor position report', cprListener);
}
if(!options.disableMciCache && !mciMapFromCache) {
// cache our MCI findings...
@@ -314,18 +311,6 @@ function display(client, art, options, cb) {
// no cached MCI info
mciMap = {};
cprListener = function(pos) {
if(mciCprQueue.length > 0) {
mciMap[mciCprQueue.shift()].position = pos;
if(parseComplete && 0 === mciCprQueue.length) {
return completed();
}
}
};
client.on('cursor position report', cprListener);
let generatedId = 100;
ansiParser.on('mci', mciInfo => {
@@ -339,18 +324,16 @@ function display(client, art, options, cb) {
mapEntry.focusArgs = mciInfo.args;
} else {
mciMap[mapKey] = {
args : mciInfo.args,
SGR : mciInfo.SGR,
code : mciInfo.mci,
id : id,
position : mciInfo.position,
args : mciInfo.args,
SGR : mciInfo.SGR,
code : mciInfo.mci,
id : id,
};
if(!mciInfo.id) {
++generatedId;
}
mciCprQueue.push(mapKey);
client.term.rawWrite(ansi.queryPos());
}
});

View File

@@ -21,7 +21,7 @@ function ButtonView(options) {
util.inherits(ButtonView, TextView);
ButtonView.prototype.onKeyPress = function(ch, key) {
if(this.isKeyMapped('accept', key.name) || ' ' === ch) {
if(this.isKeyMapped('accept', (key ? key.name : ch)) || ' ' === ch) {
this.submitData = 'accept';
this.emit('action', 'accept');
delete this.submitData;
@@ -29,16 +29,6 @@ ButtonView.prototype.onKeyPress = function(ch, key) {
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
}
};
/*
ButtonView.prototype.onKeyPress = function(ch, key) {
// allow space = submit
if(' ' === ch) {
this.emit('action', 'accept');
}
ButtonView.super_.prototype.onKeyPress.call(this, ch, key);
};
*/
ButtonView.prototype.getData = function() {
return this.submitData || null;

View File

@@ -4,11 +4,12 @@
// ENiGMA½
var Log = require('./logger.js').log;
var renegadeToAnsi = require('./color_codes.js').renegadeToAnsi;
const Config = require('./config.js').get;
var iconv = require('iconv-lite');
var assert = require('assert');
var _ = require('lodash');
exports.ClientTerminal = ClientTerminal;
function ClientTerminal(output) {
@@ -115,7 +116,8 @@ ClientTerminal.prototype.isNixTerm = function() {
return true;
}
return [ 'xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator' ].includes(this.termType);
const utf8TermList = Config().term.utf8TermList;
return utf8TermList.includes(this.termType);
};
ClientTerminal.prototype.isANSI = function() {
@@ -153,7 +155,8 @@ ClientTerminal.prototype.isANSI = function() {
// linux:
// * JuiceSSH (note: TERM=linux also)
//
return [ 'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color' ].includes(this.termType);
const cp437TermList = Config().term.cp437TermList;
return cp437TermList.includes(this.termType);
};
// :TODO: probably need to update these to convert IAC (0xff) -> IACIAC (escape it)

View File

@@ -126,7 +126,7 @@ function renegadeToAnsi(s, client) {
//
// Converts various control codes popular in BBS packages
// to ANSI escape sequences. Additionaly supports ENiGMA style
// to ANSI escape sequences. Additionally supports ENiGMA style
// MCI codes.
//
// Supported control code formats:
@@ -134,16 +134,17 @@ function renegadeToAnsi(s, client) {
// * PCBoard : @X## where the first number/char is BG color, and second is FG
// * WildCat! : @##@ the same as PCBoard without the X prefix, but with a @ suffix
// * WWIV : ^#
// * CNET Y-Style : 0x19## where ## is a specific set of codes -- this is the older format
// * CNET Q-style : 0x11##} where ## is a specific set of codes -- this is the newer format
// * CNET Control-Y: AKA Y-Style -- 0x19## where ## is a specific set of codes (older format)
// * CNET Control-Q: AKA Q-style -- 0x11##} where ## is a specific set of codes (newer format)
//
// TODO: Add Synchronet and Celerity format support
//
// Resources:
// * http://wiki.synchro.net/custom:colors
// * https://archive.org/stream/C-Net_Pro_3.0_1994_Perspective_Software/C-Net_Pro_3.0_1994_Perspective_Software_djvu.txt
//
function controlCodesToAnsi(s, client) {
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1)}|\x11)/g; // eslint-disable-line no-control-regex
const RE = /(\|([A-Z0-9]{2})|\|)|(@X([0-9A-F]{2}))|(@([0-9A-F]{2})@)|(\x03[0-9]|\x03)|(\x19(c[0-9a-f]|z[0-7]|n1|f1|q1)|\x19)|(\x11(c[0-9a-f]|z[0-7]|n1|f1|q1)}|\x11)/g; // eslint-disable-line no-control-regex
let m;
let result = '';

View File

@@ -17,6 +17,24 @@ module.exports = () => {
achievementFile : 'achievements.hjson',
},
term : {
// checkUtf8Encoding requires the use of cursor position reports, which are not supported on all terminals.
// Using this with a terminal that does not support cursor position reports results in a 2 second delay
// during the connect process, but provides better autoconfiguration of utf-8
checkUtf8Encoding : true,
// Checking the ANSI home position also requires the use of cursor position reports, which are not
// supported on all terminals. Using this with a terminal that does not support cursor position reports
// results in a 3 second delay during the connect process, but works around positioning problems with
// non-standard terminals.
checkAnsiHomePosition: true,
// List of terms that should be assumed to use cp437 encoding
cp437TermList : ['ansi', 'pcansi', 'pc-ansi', 'ansi-bbs', 'qansi', 'scoansi', 'syncterm', 'ansi-256color', 'ansi-256color-rgb'],
// List of terms that should be assumed to use utf8 encoding
utf8TermList : ['xterm', 'linux', 'screen', 'dumb', 'rxvt', 'konsole', 'gnome', 'x11 terminal emulator'],
},
users : {
usernameMin : 2,
usernameMax : 16, // Note that FidoNet wants 36 max
@@ -166,10 +184,11 @@ module.exports = () => {
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group14-sha1',
'diffie-hellman-group-exchange-sha1',
'diffie-hellman-group1-sha1',
// Group exchange not currnetly supported
// 'diffie-hellman-group-exchange-sha256',
// 'diffie-hellman-group-exchange-sha1',
],
cipher : [
'aes128-ctr',
@@ -263,7 +282,7 @@ module.exports = () => {
port : 8070,
publicHostname : 'another-fine-enigma-bbs.org',
publicPort : 8070, // adjust if behind NAT/etc.
bannerFile : 'gopher_banner.asc',
staticRoot : paths.join(__dirname, './../gopher'),
//
// Set messageConferences{} to maps of confTag -> [ areaTag1, areaTag2, ... ]
@@ -492,7 +511,7 @@ module.exports = () => {
},
decompress : {
cmd : '7za',
args : [ 'e', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
args : [ 'e', '-y', '-o{extractPath}', '{archivePath}' ] // :TODO: should be 'x'?
},
list : {
cmd : '7za',
@@ -501,7 +520,7 @@ module.exports = () => {
},
extract : {
cmd : '7za',
args : [ 'e', '-o{extractPath}', '{archivePath}', '{fileList}' ],
args : [ 'e', '-y', '-o{extractPath}', '{archivePath}', '{fileList}' ],
},
},

View File

@@ -4,6 +4,7 @@
// ENiGMA½
const ansi = require('./ansi_term.js');
const Events = require('./events.js');
const Config = require('./config.js').get;
const { Errors } = require('./enig_error.js');
// deps
@@ -18,6 +19,13 @@ function ansiDiscoverHomePosition(client, cb) {
// think of home as 0,0. If this is the case, we need to offset
// our positioning to accommodate for such.
//
if( !Config().term.checkAnsiHomePosition ) {
// Skip (and assume 1,1) if the home position check is disabled.
return cb(null);
}
const done = (err) => {
client.removeListener('cursor position report', cprListener);
clearTimeout(giveUpTimer);
@@ -68,8 +76,9 @@ function ansiAttemptDetectUTF8(client, cb) {
//
// We currently only do this if the term hasn't already been ID'd as a
// "*nix" terminal -- that is, xterm, etc.
//
if(!client.term.isNixTerm()) {
// Also skip this check if checkUtf8Encoding is disabled in the config
if(!client.term.isNixTerm() || !Config().term.checkUtf8Encoding) {
return cb(null);
}
@@ -119,6 +128,8 @@ function ansiAttemptDetectUTF8(client, cb) {
return giveUp();
}, 2000);
client.once('cursor position report', cprListener);
client.term.rawWrite(ansi.goHome() + ansi.queryPos());
}
@@ -199,7 +210,7 @@ function displayBanner(term) {
// note: intentional formatting:
term.pipeWrite(`
|06Connected to |02EN|10i|02GMA|10½ |06BBS version |12|VN
|06Copyright (c) 2014-2020 Bryan Ashby |14- |12http://l33t.codes/
|06Copyright (c) 2014-2022 Bryan Ashby |14- |12http://l33t.codes/
|06Updates & source |14- |12https://github.com/NuSkooler/enigma-bbs/
|00`
);
@@ -216,7 +227,6 @@ function connectEntry(client, nextMenu) {
},
function discoverHomePosition(callback) {
ansiDiscoverHomePosition(client, () => {
// :TODO: If CPR for home fully fails, we should bail out on the connection with an error, e.g. ANSI support required
return callback(null); // we try to continue anyway
});
},

View File

@@ -421,7 +421,8 @@ const DB_INIT_TABLE = {
hash_tag_id INTEGER NOT NULL,
file_id INTEGER NOT NULL,
UNIQUE(hash_tag_id, file_id)
UNIQUE(hash_tag_id, file_id),
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
);`
);
@@ -431,7 +432,10 @@ const DB_INIT_TABLE = {
user_id INTEGER NOT NULL,
rating INTEGER NOT NULL,
UNIQUE(file_id, user_id)
UNIQUE(file_id, user_id),
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
-- Note that we cannot CASCADE if user_id is removed from user.db
-- See processing in oputil's removeUser()
);`
);
@@ -447,7 +451,8 @@ const DB_INIT_TABLE = {
hash_id VARCHAR NOT NULL,
file_id INTEGER NOT NULL,
UNIQUE(hash_id, file_id)
UNIQUE(hash_id, file_id),
FOREIGN KEY(file_id) REFERENCES file(file_id) ON DELETE CASCADE
);`
);

View File

@@ -6,27 +6,45 @@ const TextView = require('./text_view.js').TextView;
const miscUtil = require('./misc_util.js');
const strUtil = require('./string_util.js');
const VIEW_SPECIAL_KEY_MAP_DEFAULT = require('./view').VIEW_SPECIAL_KEY_MAP_DEFAULT;
// deps
const _ = require('lodash');
exports.EditTextView = EditTextView;
const EDIT_TEXT_VIEW_KEY_MAP = Object.assign({}, VIEW_SPECIAL_KEY_MAP_DEFAULT, {
delete : [ 'delete', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
});
function EditTextView(options) {
options.acceptsFocus = miscUtil.valueWithDefault(options.acceptsFocus, true);
options.acceptsInput = miscUtil.valueWithDefault(options.acceptsInput, true);
options.cursorStyle = miscUtil.valueWithDefault(options.cursorStyle, 'steady block');
options.resizable = false;
if(!_.isObject(options.specialKeyMap)) {
options.specialKeyMap = EDIT_TEXT_VIEW_KEY_MAP;
}
TextView.call(this, options);
this.initDefaultWidth();
this.cursorPos = { row : 0, col : 0 };
this.clientBackspace = function() {
const fillCharSGR = this.getStyleSGR(1) || this.getSGR();
this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`);
};
this.text = this.text.substr(0, this.text.length - 1);
if(this.text.length >= this.dimens.width) {
this.redraw();
} else {
this.cursorPos.col -= 1;
if(this.cursorPos.col >= 0) {
const fillCharSGR = this.getStyleSGR(1) || this.getSGR();
this.client.term.write(`\b${fillCharSGR}${this.fillChar}\b${this.getFocusSGR()}`);
}
}
}
}
require('util').inherits(EditTextView, TextView);
@@ -35,19 +53,16 @@ EditTextView.prototype.onKeyPress = function(ch, key) {
if(key) {
if(this.isKeyMapped('backspace', key.name)) {
if(this.text.length > 0) {
this.text = this.text.substr(0, this.text.length - 1);
if(this.text.length >= this.dimens.width) {
this.redraw();
} else {
this.cursorPos.col -= 1;
if(this.cursorPos.col >= 0) {
this.clientBackspace();
}
}
this.clientBackspace();
}
return EditTextView.super_.prototype.onKeyPress.call(this, ch, key);
} else if (this.isKeyMapped('delete', key.name)) {
// Some (mostly older) terms send 'delete' for Backspace.
// if we're at the end of the line, go ahead and treat them the same
if (this.text.length > 0 && this.cursorPos.col === this.text.length) {
this.clientBackspace();
}
} else if(this.isKeyMapped('clearLine', key.name)) {
this.text = '';
this.cursorPos.col = 0;

View File

@@ -144,7 +144,10 @@ exports.getModule = class FileAreaList extends MenuModule {
},
displayHelp : (formData, extraArgs, cb) => {
return this.displayHelpPage(cb);
}
},
movementKeyPressed : (formData, extraArgs, cb) => {
return this._handleMovementKeyPress(_.get(formData, 'key.name'), cb);
},
};
}
@@ -201,7 +204,7 @@ exports.getModule = class FileAreaList extends MenuModule {
},
function display(callback) {
return self.displayBrowsePage(false, err => {
if(err && 'NORESULTS' === err.reasonCode) {
if(err) {
self.gotoMenu(self.menuConfig.config.noResultsMenu || 'fileBaseListEntriesNoResults');
}
return callback(err);
@@ -505,6 +508,23 @@ exports.getModule = class FileAreaList extends MenuModule {
);
}
_handleMovementKeyPress(keyName, cb) {
const descView = this.viewControllers.browse.getView(MciViewIds.browse.desc);
if (!descView) {
return cb(null);
}
switch (keyName) {
case 'down arrow' : descView.scrollDocumentUp(); break;
case 'up arrow' : descView.scrollDocumentDown(); break;
case 'page up' : descView.keyPressPageUp(); break;
case 'page down' : descView.keyPressPageDown(); break;
}
this.viewControllers.browse.switchFocus(MciViewIds.browse.navMenu);
return cb(null);
}
fetchAndDisplayWebDownloadLink(cb) {
const self = this;
@@ -720,7 +740,7 @@ exports.getModule = class FileAreaList extends MenuModule {
}
FileEntry.findFiles(filterCriteria, (err, fileIds) => {
this.fileList = fileIds;
this.fileList = fileIds || [];
return cb(err);
});
}

View File

@@ -45,7 +45,7 @@ exports.getFileAreasByTagWildcardRule = getFileAreasByTagWildcardRule;
exports.getFileEntryPath = getFileEntryPath;
exports.changeFileAreaWithOptions = changeFileAreaWithOptions;
exports.scanFile = scanFile;
exports.scanFileAreaForChanges = scanFileAreaForChanges;
//exports.scanFileAreaForChanges = scanFileAreaForChanges;
exports.getDescFromFileName = getDescFromFileName;
exports.getAreaStats = getAreaStats;
exports.cleanUpTempSessionItems = cleanUpTempSessionItems;
@@ -139,7 +139,14 @@ function getDefaultFileAreaTag(client, disableAcsCheck) {
function getFileAreaByTag(areaTag) {
const areaInfo = Config().fileBase.areas[areaTag];
if(areaInfo) {
areaInfo.areaTag = areaTag; // convienence!
// normalize |hashTags|
if (_.isString(areaInfo.hashTags)) {
areaInfo.hashTags = areaInfo.hashTags.trim().split(',');
}
if (Array.isArray(areaInfo.hashTags)) {
areaInfo.hashTags = new Set(areaInfo.hashTags.map(t => t.trim()));
}
areaInfo.areaTag = areaTag; // convenience!
areaInfo.storage = getAreaStorageLocations(areaInfo);
return areaInfo;
}
@@ -794,7 +801,7 @@ function scanFile(filePath, options, iterator, cb) {
stepInfo.calcHashPercent = Math.round(((stepInfo.bytesProcessed / stepInfo.byteSize) * 100));
//
// Only send 'hash_update' step update if we have a noticable percentage change in progress
// Only send 'hash_update' step update if we have a noticeable percentage change in progress
//
const data = bytesRead < chunkSize ? buffer.slice(0, bytesRead) : buffer;
if(!iterator || stepInfo.calcHashPercent === lastCalcHashPercent) {
@@ -871,90 +878,91 @@ function scanFile(filePath, options, iterator, cb) {
);
}
function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
if(3 === arguments.length && _.isFunction(iterator)) {
cb = iterator;
iterator = null;
} else if(2 === arguments.length && _.isFunction(options)) {
cb = options;
iterator = null;
options = {};
}
// :TODO: this stuff needs cleaned up
// function scanFileAreaForChanges(areaInfo, options, iterator, cb) {
// if(3 === arguments.length && _.isFunction(iterator)) {
// cb = iterator;
// iterator = null;
// } else if(2 === arguments.length && _.isFunction(options)) {
// cb = options;
// iterator = null;
// options = {};
// }
const storageLocations = getAreaStorageLocations(areaInfo);
// const storageLocations = getAreaStorageLocations(areaInfo);
async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
async.series(
[
function scanPhysFiles(callback) {
const physDir = storageLoc.dir;
// async.eachSeries(storageLocations, (storageLoc, nextLocation) => {
// async.series(
// [
// function scanPhysFiles(callback) {
// const physDir = storageLoc.dir;
fs.readdir(physDir, (err, files) => {
if(err) {
return callback(err);
}
// fs.readdir(physDir, (err, files) => {
// if(err) {
// return callback(err);
// }
async.eachSeries(files, (fileName, nextFile) => {
const fullPath = paths.join(physDir, fileName);
// async.eachSeries(files, (fileName, nextFile) => {
// const fullPath = paths.join(physDir, fileName);
fs.stat(fullPath, (err, stats) => {
if(err) {
// :TODO: Log me!
return nextFile(null); // always try next file
}
// fs.stat(fullPath, (err, stats) => {
// if(err) {
// // :TODO: Log me!
// return nextFile(null); // always try next file
// }
if(!stats.isFile()) {
return nextFile(null);
}
// if(!stats.isFile()) {
// return nextFile(null);
// }
scanFile(
fullPath,
{
areaTag : areaInfo.areaTag,
storageTag : storageLoc.storageTag
},
iterator,
(err, fileEntry, dupeEntries) => {
if(err) {
// :TODO: Log me!!!
return nextFile(null); // try next anyway
}
// scanFile(
// fullPath,
// {
// areaTag : areaInfo.areaTag,
// storageTag : storageLoc.storageTag
// },
// iterator,
// (err, fileEntry, dupeEntries) => {
// if(err) {
// // :TODO: Log me!!!
// return nextFile(null); // try next anyway
// }
if(dupeEntries.length > 0) {
// :TODO: Handle duplidates -- what to do here???
} else {
if(Array.isArray(options.tags)) {
options.tags.forEach(tag => {
fileEntry.hashTags.add(tag);
});
}
addNewFileEntry(fileEntry, fullPath, err => {
// pass along error; we failed to insert a record in our DB or something else bad
return nextFile(err);
});
}
}
);
});
}, err => {
return callback(err);
});
});
},
function scanDbEntries(callback) {
// :TODO: Look @ db entries for area that were *not* processed above
return callback(null);
}
],
err => {
return nextLocation(err);
}
);
},
err => {
return cb(err);
});
}
// if(dupeEntries.length > 0) {
// // :TODO: Handle duplicates -- what to do here???
// } else {
// if(Array.isArray(options.tags)) {
// options.tags.forEach(tag => {
// fileEntry.hashTags.add(tag);
// });
// }
// addNewFileEntry(fileEntry, fullPath, err => {
// // pass along error; we failed to insert a record in our DB or something else bad
// return nextFile(err);
// });
// }
// }
// );
// });
// }, err => {
// return callback(err);
// });
// });
// },
// function scanDbEntries(callback) {
// // :TODO: Look @ db entries for area that were *not* processed above
// return callback(null);
// }
// ],
// err => {
// return nextLocation(err);
// }
// );
// },
// err => {
// return cb(err);
// });
// }
function getDescFromFileName(fileName) {
//

View File

@@ -243,6 +243,15 @@ module.exports = class FileEntry {
);
}
static removeUserRatings(userId, cb) {
return fileDb.run(
`DELETE FROM file_user_rating
WHERE user_id = ?;`,
[ userId ],
cb
);
}
static persistMetaValue(fileId, name, value, transOrDb, cb) {
if(!_.isFunction(cb) && _.isFunction(transOrDb)) {
cb = transOrDb;
@@ -457,6 +466,16 @@ module.exports = class FileEntry {
);
}
//
// Find file(s) by |filter|
//
// - sort: sort results by any well known name, file_id, or user_rating
// - terms: one or more search terms to search within filenames as well
// as short and long descriptions. We attempt to use the FTS ability when
// possible, but want to allow users to search for wildcard matches in
// which some cases we'll use multiple LIKE queries.
// See _normalizeFileSearchTerms()
//
static findFiles(filter, cb) {
filter = filter || {};
@@ -500,8 +519,8 @@ module.exports = class FileEntry {
if('user_rating' === filter.sort) {
sql =
`SELECT DISTINCT f.file_id,
(SELECT IFNULL(AVG(rating), 0) rating
FROM file_user_rating
(SELECT IFNULL(AVG(rating), 0) rating
FROM file_user_rating
WHERE file_id = f.file_id)
AS avg_rating
FROM file f`;
@@ -540,16 +559,16 @@ module.exports = class FileEntry {
mp.value = mp.value.replace(/\*/g, '%').replace(/\?/g, '_');
appendWhereClause(
`f.file_id IN (
SELECT file_id
FROM file_meta
SELECT file_id
FROM file_meta
WHERE meta_name = "${mp.name}" AND meta_value LIKE "${mp.value}"
)`
);
} else {
appendWhereClause(
`f.file_id IN (
SELECT file_id
FROM file_meta
SELECT file_id
FROM file_meta
WHERE meta_name = "${mp.name}" AND meta_value = "${mp.value}"
)`
);
@@ -562,14 +581,29 @@ module.exports = class FileEntry {
}
if(filter.terms && filter.terms.length > 0) {
// note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
appendWhereClause(
`f.file_id IN (
SELECT rowid
FROM file_fts
WHERE file_fts MATCH ":${sanitizeString(filter.terms)}"
)`
);
const [terms, queryType] = FileEntry._normalizeFileSearchTerms(filter.terms);
if ('fts_match' === queryType) {
// note the ':' in MATCH expr., see https://www.sqlite.org/cvstrac/wiki?p=FullTextIndex
appendWhereClause(
`f.file_id IN (
SELECT rowid
FROM file_fts
WHERE file_fts MATCH ":${terms}"
)`
);
} else {
appendWhereClause(
`(f.file_name LIKE "${terms}" OR
f.desc LIKE "${terms}" OR
f.desc_long LIKE "${terms}")`
);
}
}
// handle e.g. 1998 -> "1998"
if (_.isNumber(filter.tags)) {
filter.tags = filter.tags.toString();
}
if(filter.tags && filter.tags.length > 0) {
@@ -693,4 +727,46 @@ module.exports = class FileEntry {
}
);
}
static _normalizeFileSearchTerms(terms) {
// ensure we have reasonable input to start with
terms = sanitizeString(terms.toString());
// No wildcards?
const hasSingleCharWC = terms.indexOf('?') > -1;
if (terms.indexOf('*') === -1 && !hasSingleCharWC) {
return [ terms, 'fts_match' ];
}
const prepareLike = () => {
// Convert * and ? to SQL LIKE style
terms = terms.replace(/\*/g, '%').replace(/\?/g, '_');
return terms;
};
// Any ? wildcards?
if (hasSingleCharWC) {
return [ prepareLike(terms), 'like' ];
}
const split = terms.replace(/\s+/g, ' ').split(' ');
const useLike = split.some(term => {
if (term.indexOf('?') > -1) {
return true;
}
const wcPos = term.indexOf('*');
if (wcPos > -1 && wcPos !== term.length - 1) {
return true;
}
return false;
});
if (useLike) {
return [ prepareLike(terms), 'like' ];
}
return [ terms, 'fts_match' ];
}
};

View File

@@ -21,7 +21,10 @@ const {
isAnsi, stripAnsiControlCodes,
insert
} = require('./string_util.js');
const { stripMciColorCodes } = require('./color_codes.js');
const {
stripMciColorCodes,
controlCodesToAnsi,
} = require('./color_codes.js');
const Config = require('./config.js').get;
const { getAddressedToInfo } = require('./mail_util.js');
const Events = require('./events.js');
@@ -418,7 +421,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
//
// Find tearline - we want to color it differently.
//
const tearLinePos = this.message.getTearLinePosition(msg);
const tearLinePos = Message.getTearLinePosition(msg);
if(tearLinePos > -1) {
msg = insert(msg, tearLinePos, bodyMessageView.getSGRFor('text'));
@@ -432,7 +435,55 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}
);
} else {
bodyMessageView.setText(stripAnsiControlCodes(msg));
msg = stripAnsiControlCodes(msg); // start clean
const styleToArray = (style, len) => {
if (!Array.isArray(style)) {
style = [ style ];
}
while (style.length < len) {
style.push(style[0]);
}
return style;
};
//
// In *View* mode, if enabled, do a little prep work so we can stylize:
// - Quote indicators
// - Tear lines
// - Origins
//
if (this.menuConfig.config.quoteStyleLevel1) {
// can be a single style to cover 'XX> TEXT' or an array to cover 'XX', '>', and TEXT
// Non-standard (as for BBSes) single > TEXT, omitting space before XX, etc. are allowed
const styleL1 = styleToArray(this.menuConfig.config.quoteStyleLevel1, 3);
const QuoteRegex = /^([ ]?)([!-~]{0,2})>([ ]*)([^\r\n]*\r?\n)/gm;
msg = msg.replace(QuoteRegex, (m, spc1, initials, spc2, text) => {
return `${spc1}${styleL1[0]}${initials}${styleL1[1]}>${spc2}${styleL1[2]}${text}${bodyMessageView.styleSGR1}`;
});
}
if (this.menuConfig.config.tearLineStyle) {
// '---' and TEXT
const style = styleToArray(this.menuConfig.config.tearLineStyle, 2);
const TearLineRegex = /^--- (.+)$(?![\s\S]*^--- .+$)/m;
msg = msg.replace(TearLineRegex, (m, text) => {
return `${style[0]}--- ${style[1]}${text}${bodyMessageView.styleSGR1}`;
});
}
if (this.menuConfig.config.originStyle) {
const style = styleToArray(this.menuConfig.config.originStyle, 3);
const OriginRegex = /^([ ]{1,2})\* Origin: (.+)$/m;
msg = msg.replace(OriginRegex, (m, spc, text) => {
return `${spc}${style[0]}* ${style[1]}Origin: ${style[2]}${text}${bodyMessageView.styleSGR1}`;
});
}
bodyMessageView.setText(controlCodesToAnsi(msg));
}
}
}
@@ -552,7 +603,7 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
theme.displayThemedAsset(
footerArt,
self.client,
{ font : self.menuConfig.font },
{ font : self.menuConfig.font, startRow: self.header.height + self.body.height },
function displayed(err, artData) {
callback(err, artData);
}
@@ -575,19 +626,34 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
async.series(
[
function displayHeaderAndBody(callback) {
async.eachSeries( comps, function dispArt(n, next) {
theme.displayThemedAsset(
art[n],
self.client,
{ font : self.menuConfig.font },
function displayed(err) {
next(err);
async.waterfall(
[
function displayHeader(callback) {
theme.displayThemedAsset(
art['header'],
self.client,
{ font : self.menuConfig.font },
function displayed(err, artInfo) {
return callback(err, artInfo);
}
);
},
function displayBody(artInfo, callback) {
theme.displayThemedAsset(
art['header'],
self.client,
{ font : self.menuConfig.font, startRow: artInfo.height + 1 },
function displayed(err, artInfo) {
return callback(err, artInfo);
}
);
}
);
}, function complete(err) {
//self.body.height = self.client.term.termHeight - self.header.height - 1;
callback(err);
});
],
function complete(err) {
//self.body.height = self.client.term.termHeight - self.header.height - 1;
callback(err);
}
);
},
function displayFooter(callback) {
// we have to treat the footer special
@@ -649,31 +715,39 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
assert(_.isObject(art));
async.series(
async.waterfall(
[
function beforeDisplayArt(callback) {
self.beforeArt(callback);
},
function displayHeaderAndBodyArt(callback) {
async.eachSeries( [ 'header', 'body' ], function dispArt(n, next) {
theme.displayThemedAsset(
art[n],
self.client,
{ font : self.menuConfig.font },
function displayed(err, artData) {
if(artData) {
mciData[n] = artData;
self[n] = { height : artData.height };
}
next(err);
function displayHeader(callback) {
theme.displayThemedAsset(
art.header,
self.client,
{ font : self.menuConfig.font },
function displayed(err, artInfo) {
if(artInfo) {
mciData['header'] = artInfo;
self.header = {height: artInfo.height};
}
);
}, function complete(err) {
callback(err);
});
return callback(err, artInfo);
}
);
},
function displayFooter(callback) {
function displayBody(artInfo, callback) {
theme.displayThemedAsset(
art.body,
self.client,
{ font : self.menuConfig.font, startRow: artInfo.height + 1 },
function displayed(err, artInfo) {
if(artInfo) {
mciData['body'] = artInfo;
self.body = {height: artInfo.height - self.header.height};
}
return callback(err, artInfo);
});
},
function displayFooter(artInfo, callback) {
self.setInitialFooterMode();
var footerName = self.getFooterName();
@@ -740,10 +814,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
});
},
function prepareViewStates(callback) {
var header = self.viewControllers.header;
var from = header.getView(MciViewIds.header.from);
from.acceptsFocus = false;
//from.setText(self.client.user.username);
let from = self.viewControllers.header.getView(MciViewIds.header.from);
if (from) {
from.acceptsFocus = false;
}
// :TODO: make this a method
var body = self.viewControllers.body.getView(MciViewIds.body.message);
@@ -774,10 +848,12 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
{
const fromView = self.viewControllers.header.getView(MciViewIds.header.from);
const area = getMessageAreaByTag(self.messageAreaTag);
if(area && area.realNames) {
fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username);
} else {
fromView.setText(self.client.user.username);
if(fromView !== undefined) {
if(area && area.realNames) {
fromView.setText(self.client.user.properties[UserProps.RealName] || self.client.user.username);
} else {
fromView.setText(self.client.user.username);
}
}
if(self.replyToMessage) {
@@ -863,7 +939,10 @@ exports.FullScreenEditorModule = exports.getModule = class FullScreenEditorModul
}
initHeaderViewMode() {
this.setHeaderText(MciViewIds.header.from, this.message.fromUserName);
// Only set header text for from view if it is on the form
if (this.viewControllers.header.getView(MciViewIds.header.from) !== undefined) {
this.setHeaderText(MciViewIds.header.from, this.message.fromUserName);
}
this.setHeaderText(MciViewIds.header.to, this.message.toUserName);
this.setHeaderText(MciViewIds.header.subject, this.message.subject);

511
core/full_menu_view.js Normal file
View File

@@ -0,0 +1,511 @@
/* jslint node: true */
'use strict';
// ENiGMA½
const MenuView = require('./menu_view.js').MenuView;
const ansi = require('./ansi_term.js');
const strUtil = require('./string_util.js');
const formatString = require('./string_format');
const pipeToAnsi = require('./color_codes.js').pipeToAnsi;
// deps
const util = require('util');
const _ = require('lodash');
exports.FullMenuView = FullMenuView;
function FullMenuView(options) {
options.cursor = options.cursor || 'hide';
options.justify = options.justify || 'left';
MenuView.call(this, options);
// Initialize paging
this.pages = [];
this.currentPage = 0;
this.initDefaultWidth();
// we want page up/page down by default
if (!_.isObject(options.specialKeyMap)) {
Object.assign(this.specialKeyMap, {
'page up': ['page up'],
'page down': ['page down'],
});
}
this.autoAdjustHeightIfEnabled = () => {
if (this.autoAdjustHeight) {
this.dimens.height = (this.items.length * (this.itemSpacing + 1)) - (this.itemSpacing);
this.dimens.height = Math.min(this.dimens.height, this.client.term.termHeight - this.position.row);
}
this.positionCacheExpired = true;
};
this.autoAdjustHeightIfEnabled();
this.clearPage = () => {
let width = this.dimens.width;
if (this.oldDimens) {
if (this.oldDimens.width > width) {
width = this.oldDimens.width;
}
delete this.oldDimens;
}
for (let i = 0; i < this.dimens.height; i++) {
const text = `${strUtil.pad(this.fillChar, width, this.fillChar, 'left')}`;
this.client.term.write(`${ansi.goto(this.position.row + i, this.position.col)}${this.getSGR()}${text}`);
}
}
this.cachePositions = () => {
if (this.positionCacheExpired) {
// first, clear the page
this.clearPage();
this.autoAdjustHeightIfEnabled();
this.pages = []; // reset
// Calculate number of items visible per column
this.itemsPerRow = Math.floor(this.dimens.height / (this.itemSpacing + 1));
// handle case where one can fit at the end
if (this.dimens.height > (this.itemsPerRow * (this.itemSpacing + 1))) {
this.itemsPerRow++;
}
// Final check to make sure we don't try to display more than we have
if (this.itemsPerRow > this.items.length) {
this.itemsPerRow = this.items.length;
}
let col = this.position.col;
let row = this.position.row;
const spacer = new Array(this.itemHorizSpacing + 1).join(this.fillChar);
let itemInRow = 0;
let itemInCol = 0;
let pageStart = 0;
for (let i = 0; i < this.items.length; ++i) {
itemInRow++;
this.items[i].row = row;
this.items[i].col = col;
this.items[i].itemInRow = itemInRow;
row += this.itemSpacing + 1;
// have to calculate the max length on the last entry
if (i == this.items.length - 1) {
let maxLength = 0;
for (let j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != this.items[i].col) {
break;
}
const itemLength = this.items[i - j].text.length;
if (itemLength > maxLength) {
maxLength = itemLength;
}
}
// set length on each item in the column
for (let j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != this.items[i].col) {
break;
}
this.items[i - j].fixedLength = maxLength;
}
// Check if we have room for this column
// skip for column 0, we need at least one
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
// save previous page
this.pages.push({ start: pageStart, end: i - itemInRow });
// fix the last column processed
for (let j = 0; j < this.itemsPerRow; j++) {
if (this.items[i - j].col != col) {
break;
}
this.items[i - j].col = this.position.col;
pageStart = i - j;
}
}
// Since this is the last page, save the current page as well
this.pages.push({ start: pageStart, end: i });
}
// also handle going to next column
else if (itemInRow == this.itemsPerRow) {
itemInRow = 0;
// restart row for next column
row = this.position.row;
let maxLength = 0;
for (let j = 0; j < this.itemsPerRow; j++) {
// TODO: handle complex items
let itemLength = this.items[i - j].text.length;
if (itemLength > maxLength) {
maxLength = itemLength;
}
}
// set length on each item in the column
for (let j = 0; j < this.itemsPerRow; j++) {
this.items[i - j].fixedLength = maxLength;
}
// Check if we have room for this column in the current page
// skip for first column, we need at least one
if (itemInCol != 0 && (col + maxLength > this.dimens.width)) {
// save previous page
this.pages.push({ start: pageStart, end: i - this.itemsPerRow });
// restart page start for next page
pageStart = i - this.itemsPerRow + 1;
// reset
col = this.position.col;
itemInRow = 0;
// fix the last column processed
for (let j = 0; j < this.itemsPerRow; j++) {
this.items[i - j].col = col;
}
}
// increment the column
col += maxLength + spacer.length;
itemInCol++;
}
// Set the current page if the current item is focused.
if (this.focusedItemIndex === i) {
this.currentPage = this.pages.length;
}
}
}
this.positionCacheExpired = false;
};
this.drawItem = (index) => {
const item = this.items[index];
if (!item) {
return;
}
const cached = this.getRenderCacheItem(index, item.focused);
if (cached) {
return this.client.term.write(`${ansi.goto(item.row, item.col)}${cached}`);
}
let text;
let sgr;
if (item.focused && this.hasFocusItems()) {
const focusItem = this.focusItems[index];
text = focusItem ? focusItem.text : item.text;
sgr = '';
} else if (this.complexItems) {
text = pipeToAnsi(formatString(item.focused && this.focusItemFormat ? this.focusItemFormat : this.itemFormat, item));
sgr = this.focusItemFormat ? '' : (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
} else {
text = strUtil.stylizeString(item.text, item.focused ? this.focusTextStyle : this.textStyle);
sgr = (index === this.focusedItemIndex ? this.getFocusSGR() : this.getSGR());
}
let renderLength = strUtil.renderStringLength(text);
if (this.hasTextOverflow() && (item.col + renderLength) > this.dimens.width) {
text = strUtil.renderSubstr(text, 0, this.dimens.width - (item.col + this.textOverflow.length)) + this.textOverflow;
}
let padLength = Math.min(item.fixedLength + 1, this.dimens.width);
text = `${sgr}${strUtil.pad(text, padLength, this.fillChar, this.justify)}${this.getSGR()}`;
this.client.term.write(`${ansi.goto(item.row, item.col)}${text}`);
this.setRenderCacheItem(index, text, item.focused);
};
}
util.inherits(FullMenuView, MenuView);
FullMenuView.prototype.redraw = function() {
FullMenuView.super_.prototype.redraw.call(this);
this.cachePositions();
if (this.items.length) {
for (let i = this.pages[this.currentPage].start; i <= this.pages[this.currentPage].end; ++i) {
this.items[i].focused = this.focusedItemIndex === i;
this.drawItem(i);
}
}
};
FullMenuView.prototype.setHeight = function(height) {
this.oldDimens = Object.assign({}, this.dimens);
FullMenuView.super_.prototype.setHeight.call(this, height);
this.positionCacheExpired = true;
this.autoAdjustHeight = false;
};
FullMenuView.prototype.setWidth = function(width) {
this.oldDimens = Object.assign({}, this.dimens);
FullMenuView.super_.prototype.setWidth.call(this, width);
this.positionCacheExpired = true;
};
FullMenuView.prototype.setTextOverflow = function(overflow) {
FullMenuView.super_.prototype.setTextOverflow.call(this, overflow);
this.positionCacheExpired = true;
}
FullMenuView.prototype.setPosition = function(pos) {
FullMenuView.super_.prototype.setPosition.call(this, pos);
this.positionCacheExpired = true;
};
FullMenuView.prototype.setFocus = function(focused) {
FullMenuView.super_.prototype.setFocus.call(this, focused);
this.positionCacheExpired = true;
this.autoAdjustHeight = false;
this.redraw();
};
FullMenuView.prototype.setFocusItemIndex = function(index) {
FullMenuView.super_.prototype.setFocusItemIndex.call(this, index); // sets this.focusedItemIndex
};
FullMenuView.prototype.onKeyPress = function(ch, key) {
if (key) {
if (this.isKeyMapped('up', key.name)) {
this.focusPrevious();
} else if (this.isKeyMapped('down', key.name)) {
this.focusNext();
} else if (this.isKeyMapped('left', key.name)) {
this.focusPreviousColumn();
} else if (this.isKeyMapped('right', key.name)) {
this.focusNextColumn();
} else if (this.isKeyMapped('page up', key.name)) {
this.focusPreviousPageItem();
} else if (this.isKeyMapped('page down', key.name)) {
this.focusNextPageItem();
} else if (this.isKeyMapped('home', key.name)) {
this.focusFirst();
} else if (this.isKeyMapped('end', key.name)) {
this.focusLast();
}
}
FullMenuView.super_.prototype.onKeyPress.call(this, ch, key);
};
FullMenuView.prototype.getData = function() {
const item = this.getItem(this.focusedItemIndex);
return _.isString(item.data) ? item.data : this.focusedItemIndex;
};
FullMenuView.prototype.setItems = function(items) {
// if we have items already, save off their drawing area so we don't leave fragments at redraw
if (this.items && this.items.length) {
this.oldDimens = Object.assign({}, this.dimens);
}
FullMenuView.super_.prototype.setItems.call(this, items);
this.positionCacheExpired = true;
};
FullMenuView.prototype.removeItem = function(index) {
if (this.items && this.items.length) {
this.oldDimens = Object.assign({}, this.dimens);
}
FullMenuView.super_.prototype.removeItem.call(this, index);
this.positionCacheExpired = true;
};
FullMenuView.prototype.focusNext = function() {
if (this.items.length - 1 === this.focusedItemIndex) {
this.clearPage();
this.focusedItemIndex = 0;
this.currentPage = 0;
}
else {
this.focusedItemIndex++;
if (this.focusedItemIndex > this.pages[this.currentPage].end) {
this.clearPage();
this.currentPage++;
}
}
this.redraw();
FullMenuView.super_.prototype.focusNext.call(this);
};
FullMenuView.prototype.focusPrevious = function() {
if (0 === this.focusedItemIndex) {
this.clearPage();
this.focusedItemIndex = this.items.length - 1;
this.currentPage = this.pages.length - 1;
}
else {
this.focusedItemIndex--;
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.clearPage();
this.currentPage--;
}
}
this.redraw();
FullMenuView.super_.prototype.focusPrevious.call(this);
};
FullMenuView.prototype.focusPreviousColumn = function() {
const currentRow = this.items[this.focusedItemIndex].itemInRow;
this.focusedItemIndex = this.focusedItemIndex - this.itemsPerRow;
if (this.focusedItemIndex < 0) {
this.clearPage();
const lastItemRow = this.items[this.items.length - 1].itemInRow;
if (lastItemRow > currentRow) {
this.focusedItemIndex = this.items.length - (lastItemRow - currentRow) - 1;
}
else {
// can't go to same column, so go to last item
this.focusedItemIndex = this.items.length - 1;
}
// set to last page
this.currentPage = this.pages.length - 1;
}
else {
if (this.focusedItemIndex < this.pages[this.currentPage].start) {
this.clearPage();
this.currentPage--;
}
}
this.redraw();
// TODO: This isn't specific to Previous, may want to replace in the future
FullMenuView.super_.prototype.focusPrevious.call(this);
};
FullMenuView.prototype.focusNextColumn = function() {
const currentRow = this.items[this.focusedItemIndex].itemInRow;
this.focusedItemIndex = this.focusedItemIndex + this.itemsPerRow;
if (this.focusedItemIndex > this.items.length - 1) {
this.focusedItemIndex = currentRow - 1;
this.currentPage = 0;
this.clearPage();
}
else if (this.focusedItemIndex > this.pages[this.currentPage].end) {
this.clearPage();
this.currentPage++;
}
this.redraw();
// TODO: This isn't specific to Next, may want to replace in the future
FullMenuView.super_.prototype.focusNext.call(this);
};
FullMenuView.prototype.focusPreviousPageItem = function() {
// handle first page
if (this.currentPage == 0) {
// Do nothing, page up shouldn't go down on last page
return;
}
this.currentPage--;
this.focusedItemIndex = this.pages[this.currentPage].start;
this.clearPage();
this.redraw();
return FullMenuView.super_.prototype.focusPreviousPageItem.call(this);
};
FullMenuView.prototype.focusNextPageItem = function() {
// handle last page
if (this.currentPage == this.pages.length - 1) {
// Do nothing, page up shouldn't go down on last page
return;
}
this.currentPage++;
this.focusedItemIndex = this.pages[this.currentPage].start;
this.clearPage();
this.redraw();
return FullMenuView.super_.prototype.focusNextPageItem.call(this);
};
FullMenuView.prototype.focusFirst = function() {
this.currentPage = 0;
this.focusedItemIndex = 0;
this.clearPage();
this.redraw();
return FullMenuView.super_.prototype.focusFirst.call(this);
};
FullMenuView.prototype.focusLast = function() {
this.currentPage = this.pages.length - 1;
this.focusedItemIndex = this.pages[this.currentPage].end;
this.clearPage();
this.redraw();
return FullMenuView.super_.prototype.focusLast.call(this);
};
FullMenuView.prototype.setFocusItems = function(items) {
FullMenuView.super_.prototype.setFocusItems.call(this, items);
this.positionCacheExpired = true;
};
FullMenuView.prototype.setItemSpacing = function(itemSpacing) {
FullMenuView.super_.prototype.setItemSpacing.call(this, itemSpacing);
this.positionCacheExpired = true;
};
FullMenuView.prototype.setJustify = function(justify) {
FullMenuView.super_.prototype.setJustify.call(this, justify);
this.positionCacheExpired = true;
};
FullMenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
FullMenuView.super_.prototype.setItemHorizSpacing.call(this, itemHorizSpacing);
this.positionCacheExpired = true;
};

View File

@@ -132,7 +132,7 @@ MaskEditTextView.prototype.onKeyPress = function(ch, key) {
this.text = this.text.substr(0, this.text.length - 1);
this.clientBackspace();
} else {
while(this.patternArrayPos > 0) {
while(this.patternArrayPos >= 0) {
if(_.isRegExp(this.patternArray[this.patternArrayPos])) {
this.text = this.text.substr(0, this.text.length - 1);
this.client.term.write(ansi.goto(this.position.row, this.getEndOfTextColumn() + 1));

View File

@@ -8,6 +8,7 @@ const EditTextView = require('./edit_text_view.js').EditTextView;
const ButtonView = require('./button_view.js').ButtonView;
const VerticalMenuView = require('./vertical_menu_view.js').VerticalMenuView;
const HorizontalMenuView = require('./horizontal_menu_view.js').HorizontalMenuView;
const FullMenuView = require('./full_menu_view.js').FullMenuView;
const SpinnerMenuView = require('./spinner_menu_view.js').SpinnerMenuView;
const ToggleMenuView = require('./toggle_menu_view.js').ToggleMenuView;
const MaskEditTextView = require('./mask_edit_text_view.js').MaskEditTextView;
@@ -27,7 +28,7 @@ function MCIViewFactory(client) {
}
MCIViewFactory.UserViewCodes = [
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'SM', 'TM', 'KE',
'TL', 'ET', 'ME', 'MT', 'PL', 'BT', 'VM', 'HM', 'FM', 'SM', 'TM', 'KE',
//
// XY is a special MCI code that allows finding positions
@@ -164,6 +165,18 @@ MCIViewFactory.prototype.createFromMCI = function(mci) {
view = new HorizontalMenuView(options);
break;
// Full Menu
case 'FM' :
setOption(0, 'itemSpacing');
setOption(1, 'itemHorizSpacing');
setOption(2, 'justify');
setOption(3, 'textStyle');
setFocusOption(0, 'focusTextStyle');
view = new FullMenuView(options);
break;
case 'SM' :
setOption(0, 'textStyle');
setOption(1, 'justify');

View File

@@ -56,14 +56,14 @@ exports.MenuModule = class MenuModule extends PluginModule {
initSequence() {
const self = this;
const mciData = {};
let pausePosition;
let pausePosition = {row: 0, column: 0};
const hasArt = () => {
return _.isString(self.menuConfig.art) ||
(Array.isArray(self.menuConfig.art) && _.has(self.menuConfig.art[0], 'acs'));
};
async.series(
async.waterfall(
[
function beforeArtInterrupt(callback) {
return self.displayQueuedInterruptions(callback);
@@ -73,7 +73,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
},
function displayMenuArt(callback) {
if(!hasArt()) {
return callback(null);
return callback(null, null);
}
self.displayAsset(
@@ -86,18 +86,15 @@ exports.MenuModule = class MenuModule extends PluginModule {
mciData.menu = artData.mciMap;
}
return callback(null); // any errors are non-fatal
if(artData) {
pausePosition.row = artData.height + 1;
}
return callback(null, artData); // any errors are non-fatal
}
);
},
function moveToPromptLocation(callback) {
if(self.menuConfig.prompt) {
// :TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
}
return callback(null);
},
function displayPromptArt(callback) {
function displayPromptArt(artData, callback) {
if(!_.isString(self.menuConfig.prompt)) {
return callback(null);
}
@@ -106,41 +103,41 @@ exports.MenuModule = class MenuModule extends PluginModule {
return callback(Errors.MissingConfig('Prompt specified but no "promptConfig" block found'));
}
const options = Object.assign({}, self.menuConfig.config);
if(_.isNumber(artData?.height)) {
options.startRow = artData.height + 1;
}
self.displayAsset(
self.menuConfig.promptConfig.art,
self.menuConfig.config,
options,
(err, artData) => {
if(artData) {
mciData.prompt = artData.mciMap;
pausePosition.row = artData.height + 1;
}
return callback(err); // pass err here; prompts *must* have art
}
);
},
function recordCursorPosition(callback) {
if(!self.shouldPause()) {
return callback(null); // cursor position not needed
}
self.client.once('cursor position report', pos => {
pausePosition = { row : pos[0], col : 1 };
self.client.log.trace('After art position recorded', pausePosition );
return callback(null);
});
self.client.term.rawWrite(ansi.queryPos());
},
function afterArtDisplayed(callback) {
return self.mciReady(mciData, callback);
},
function displayPauseIfRequested(callback) {
if(!self.shouldPause()) {
return callback(null);
return callback(null, null);
}
if(self.client.term.termHeight > 0 && pausePosition.row > self.client.termHeight) {
// If this scrolled, the prompt will go to the bottom of the screen
pausePosition.row = self.client.termHeight;
}
return self.pausePrompt(pausePosition, callback);
},
function finishAndNext(callback) {
function finishAndNext(artInfo, callback) {
self.finishedLoading();
self.realTimeInterrupt = 'allowed';
return self.autoNextMenu(callback);
@@ -512,7 +509,7 @@ exports.MenuModule = class MenuModule extends PluginModule {
this.optionalMoveToPosition(position);
return theme.displayThemedPause(this.client, cb);
return theme.displayThemedPause(this.client, {position}, cb);
}
promptForInput( { formName, formId, promptName, prevFormName, position } = {}, options, cb) {

View File

@@ -38,14 +38,14 @@ function MenuView(options) {
this.focusedItemIndex = options.focusedItemIndex || 0;
this.focusedItemIndex = this.items.length >= this.focusedItemIndex ? this.focusedItemIndex : 0;
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
this.itemSpacing = _.isNumber(options.itemSpacing) ? options.itemSpacing : 0;
this.itemHorizSpacing = _.isNumber(options.itemHorizSpacing) ? options.itemHorizSpacing : 0;
// :TODO: probably just replace this with owner draw / pipe codes / etc. more control, less specialization
this.focusPrefix = options.focusPrefix || '';
this.focusSuffix = options.focusSuffix || '';
this.fillChar = miscUtil.valueWithDefault(options.fillChar, ' ').substr(0, 1);
this.justify = options.justify || 'none';
this.hasFocusItems = function() {
return !_.isUndefined(self.focusItems);
@@ -68,6 +68,15 @@ function MenuView(options) {
util.inherits(MenuView, View);
MenuView.prototype.setTextOverflow = function(overflow) {
this.textOverflow = overflow;
this.invalidateRenderCache();
}
MenuView.prototype.hasTextOverflow = function() {
return this.textOverflow != undefined;
}
MenuView.prototype.setItems = function(items) {
if(Array.isArray(items)) {
this.sorted = false;
@@ -253,19 +262,32 @@ MenuView.prototype.setItemSpacing = function(itemSpacing) {
this.positionCacheExpired = true;
};
MenuView.prototype.setItemHorizSpacing = function(itemHorizSpacing) {
itemHorizSpacing = parseInt(itemHorizSpacing);
assert(_.isNumber(itemHorizSpacing));
this.itemHorizSpacing = itemHorizSpacing;
this.positionCacheExpired = true;
};
MenuView.prototype.setPropertyValue = function(propName, value) {
switch(propName) {
case 'itemSpacing' : this.setItemSpacing(value); break;
case 'itemHorizSpacing' : this.setItemHorizSpacing(value); break;
case 'items' : this.setItems(value); break;
case 'focusItems' : this.setFocusItems(value); break;
case 'hotKeys' : this.setHotKeys(value); break;
case 'textOverflow' : this.setTextOverflow(value); break;
case 'hotKeySubmit' : this.hotKeySubmit = value; break;
case 'justify' : this.justify = value; break;
case 'justify' : this.setJustify(value); break;
case 'fillChar' : this.setFillChar(value); break;
case 'focusItemIndex' : this.focusedItemIndex = value; break;
case 'itemFormat' :
case 'focusItemFormat' :
this[propName] = value;
// if there is a cache currently, invalidate it
this.invalidateRenderCache();
break;
case 'sort' : this.setSort(value); break;
@@ -274,6 +296,17 @@ MenuView.prototype.setPropertyValue = function(propName, value) {
MenuView.super_.prototype.setPropertyValue.call(this, propName, value);
};
MenuView.prototype.setFillChar = function(fillChar) {
this.fillChar = miscUtil.valueWithDefault(fillChar, ' ').substr(0, 1);
this.invalidateRenderCache();
}
MenuView.prototype.setJustify = function(justify) {
this.justify = justify;
this.invalidateRenderCache();
this.positionCacheExpired = true;
}
MenuView.prototype.setHotKeys = function(hotKeys) {
if(_.isObject(hotKeys)) {
if(this.caseInsensitiveHotKeys) {

View File

@@ -790,7 +790,7 @@ module.exports = class Message {
return ftnUtil.getQuotePrefix(this[source]);
}
getTearLinePosition(input) {
static getTearLinePosition(input) {
const m = input.match(/^--- .+$(?![\s\S]*^--- .+$)/m);
return m ? m.index : -1;
}
@@ -886,12 +886,12 @@ module.exports = class Message {
}
);
} else {
const QUOTE_RE = /^ ((?:[A-Za-z0-9]{2}> )+(?:[A-Za-z0-9]{2}>)*) */;
const QUOTE_RE = /^ ((?:[A-Za-z0-9]{1,2}> )+(?:[A-Za-z0-9]{1,2}>)*) */;
const quoted = [];
const input = _.trimEnd(this.message).replace(/\x08/g, ''); // eslint-disable-line no-control-regex
// find *last* tearline
let tearLinePos = this.getTearLinePosition(input);
let tearLinePos = Message.getTearLinePosition(input);
tearLinePos = -1 === tearLinePos ? input.length : tearLinePos; // we just want the index or the entire string
input.slice(0, tearLinePos).split(/\r\n\r\n|\n\n/).forEach(paragraph => {
@@ -910,7 +910,7 @@ module.exports = class Message {
if(quoted.length > 0) {
//
// Preserve paragraph seperation.
// Preserve paragraph separation.
//
// FSC-0032 states something about leaving blank lines fully blank
// (without a prefix) but it seems nicer (and more consistent with other systems)

View File

@@ -426,7 +426,8 @@ exports.getModule = class mrcModule extends MenuModule {
switch (cmd[0]) {
case 'pm':
this.processOutgoingMessage(cmd[2], cmd[1]);
const newmsg = cmd.slice(2).join(' ');
this.processOutgoingMessage(newmsg, cmd[1]);
break;
case 'rainbow': {

View File

@@ -65,7 +65,7 @@ const _ = require('lodash');
const SPECIAL_KEY_MAP_DEFAULT = {
'line feed' : [ 'return' ],
exit : [ 'esc' ],
backspace : [ 'backspace' ],
backspace : [ 'backspace', 'ctrl + d' ], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
delete : [ 'delete' ],
tab : [ 'tab' ],
up : [ 'up arrow' ],
@@ -74,7 +74,7 @@ const SPECIAL_KEY_MAP_DEFAULT = {
home : [ 'home' ],
left : [ 'left arrow' ],
right : [ 'right arrow' ],
'delete line' : [ 'ctrl + y' ],
'delete line' : [ 'ctrl + y', 'ctrl + u' ], // https://en.wikipedia.org/wiki/Backspace
'page up' : [ 'page up' ],
'page down' : [ 'page down' ],
insert : [ 'insert', 'ctrl + v' ],
@@ -265,11 +265,10 @@ function MultiLineEditTextView(options) {
this.getRenderText = function(index) {
let text = self.getVisibleText(index);
const remain = self.dimens.width - text.length;
const remain = self.dimens.width - strUtil.renderStringLength(text);
if(remain > 0) {
text += ' '.repeat(remain + 1);
// text += new Array(remain + 1).join(' ');
text += ' '.repeat(remain);// + 1);
}
return text;

View File

@@ -135,7 +135,7 @@ exports.getModule = class OnelinerzModule extends MenuModule {
self.db.each(
`SELECT *
FROM (
SELECT *
SELECT *
FROM onelinerz
ORDER BY timestamp DESC
LIMIT ${limit}
@@ -248,8 +248,9 @@ exports.getModule = class OnelinerzModule extends MenuModule {
async.series(
[
function openDatabase(callback) {
const dbSuffix = self.menuConfig.config.dbSuffix;
self.db = getTransactionDatabase(new sqlite3.Database(
getModDatabasePath(exports.moduleInfo),
getModDatabasePath(exports.moduleInfo, dbSuffix),
err => {
return callback(err);
}
@@ -260,7 +261,7 @@ exports.getModule = class OnelinerzModule extends MenuModule {
`CREATE TABLE IF NOT EXISTS onelinerz (
id INTEGER PRIMARY KEY,
user_id INTEGER_NOT NULL,
user_name VARCHAR NOT NULL,
user_name VARCHAR NOT NULL,
oneliner VARCHAR NOT NULL,
timestamp DATETIME NOT NULL
);`

View File

@@ -153,6 +153,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
function updateTags(fe) {
if(Array.isArray(options.tags)) {
fe.hashTags = new Set(options.tags);
} else if (areaInfo.hashTags) { // no explicit tags; merge in defaults, if any
fe.hashTags = areaInfo.hashTags;
}
}
@@ -227,7 +229,8 @@ function scanFileAreaForChanges(areaInfo, options, cb) {
fullPath,
{
areaTag : areaInfo.areaTag,
storageTag : storageLoc.storageTag
storageTag : storageLoc.storageTag,
hashTags : areaInfo.hashTags,
},
(stepInfo, next) => {
if(argv.verbose) {
@@ -549,7 +552,7 @@ function scanFileAreas() {
console.info(`Processing area "${areaInfo.name}":`);
scanFileAreaForChanges(areaInfo, options, err => {
return callback(err);
return nextAreaTag(err);
});
}, err => {
return callback(err);

View File

@@ -57,7 +57,7 @@ Actions:
lock USERNAME Set a user's status to "locked"
group USERNAME [+|-]GROUP Adds (+) or removes (-) user from a group
group USERNAME [+|~]GROUP Adds (+) or removes (~) user from a group
list [FILTER] List users with optional FILTER.

View File

@@ -172,6 +172,7 @@ function removeUser(user) {
message : [ 'user_message_area_last_read' ],
system : [ 'user_event_log', ],
user : [ 'user_group_member', 'user' ],
file : [ 'file_user_rating']
};
async.eachSeries(Object.keys(DeleteFrom), (dbName, nextDbName) => {
@@ -275,7 +276,7 @@ function modUserGroups(user) {
let groupName = argv._[argv._.length - 1].toString().replace(/["']/g, ''); // remove any quotes - necessary to allow "-foo"
let action = groupName[0]; // + or -
if('-' === action || '+' === action) {
if('-' === action || '+' === action || '~' === action) {
groupName = groupName.substr(1);
}
@@ -286,7 +287,7 @@ function modUserGroups(user) {
}
//
// Groups are currently arbritary, so do a slight validation
// Groups are currently arbitrary, so do a slight validation
//
if(!/[A-Za-z0-9]+/.test(groupName)) {
process.exitCode = ExitCodes.BAD_ARGS;
@@ -303,7 +304,7 @@ function modUserGroups(user) {
}
const UserGroup = require('../../core/user_group.js');
if('-' === action) {
if('-' === action || '~' === action) {
UserGroup.removeUserFromGroup(user.userId, groupName, done);
} else {
UserGroup.addUserToGroup(user.userId, groupName, done);

View File

@@ -941,7 +941,7 @@ class QWKPacketWriter extends EventEmitter {
}
// First block is a space padded ID
const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2020 Bryan Ashby`;
const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2022 Bryan Ashby`;
this.messagesStream.write(id.padEnd(QWKMessageBlockSize, ' '), 'ascii');
this.currentMessageOffset = QWKMessageBlockSize;

View File

@@ -81,14 +81,22 @@ exports.getModule = class GopherModule extends ServerModule {
this.publicHostname = config.contentServers.gopher.publicHostname;
this.publicPort = config.contentServers.gopher.publicPort;
this.addRoute(/^\/?\r\n$/, this.defaultGenerator);
this.addRoute(/^\/msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator);
this.addRoute(/^\/?msgarea(\/[a-z0-9_-]+(\/[a-z0-9_-]+)?(\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(_raw)?)?)?\/?\r\n$/, this.messageAreaGenerator);
this.addRoute(/^(\/?[^\t\r\n]*)\r\n$/, this.staticGenerator);
this.server = net.createServer( socket => {
socket.setEncoding('ascii');
socket.on('data', data => {
this.routeRequest(data, socket);
// sanitize a bit - bots like to inject garbage
data = data.replace(/[^ -~\t\r\n]/g, '');
if (data) {
this.routeRequest(data, socket);
} else {
this.notFoundGenerator('**invalid selector**', res => {
return socket.end(`${res}`);
});
}
});
socket.on('error', err => {
@@ -161,22 +169,56 @@ exports.getModule = class GopherModule extends ServerModule {
return `${itemType}${text}\t${selector}\t${hostname}\t${port}\r\n`;
}
defaultGenerator(selectorMatch, cb) {
this.log.debug( { selector : selectorMatch[0] }, 'Serving default content');
staticGenerator(selectorMatch, cb) {
this.log.debug( { selector : selectorMatch[1] || '(gophermap)' }, 'Serving static content');
let bannerFile = _.get(Config(), 'contentServers.gopher.bannerFile', 'gopher_banner.asc');
bannerFile = paths.isAbsolute(bannerFile) ? bannerFile : paths.join(__dirname, '../../../misc', bannerFile);
fs.readFile(bannerFile, 'utf8', (err, banner) => {
if(err) {
return cb('You have reached an ENiGMA½ Gopher server!');
const requestedPath = selectorMatch[1];
let path = this.resolveContentPath(requestedPath);
if (!path) {
return cb('Not found');
}
fs.stat(path, (err, stats) => {
if (err) {
return cb('Not found');
}
banner = splitTextAtTerms(banner).map(l => this.makeItem(ItemTypes.InfoMessage, l)).join('');
banner += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea');
return cb(banner);
let isGopherMap = false;
if (stats.isDirectory()) {
path = paths.join(path, 'gophermap');
isGopherMap = true;
}
fs.readFile(path, isGopherMap ? 'utf8' : null, (err, content) => {
if (err) {
let content = 'You have reached an ENiGMA½ Gopher server!\r\n';
content += this.makeItem(ItemTypes.SubMenu, 'Public Message Area', '/msgarea');
return cb(content);
}
if (isGopherMap) {
// Convert any UNIX style LF's to DOS CRLF's
content = content.replace(/\r?\n/g, '\r\n');
// variable support
content = content
.replace(/{publicHostname}/g, this.publicHostname)
.replace(/{publicPort}/g, this.publicPort);
}
return cb(content);
});
});
}
resolveContentPath(requestPath) {
const staticRoot = _.get(Config(), 'contentServers.gopher.staticRoot');
const path = paths.resolve(staticRoot, `.${requestPath}`);
if (path.startsWith(staticRoot)) {
return path;
}
}
notFoundGenerator(selector, cb) {
this.log.debug( { selector }, 'Serving not found content');
return cb('Not found');

View File

@@ -125,7 +125,7 @@ class NNTPServer extends NNTPServerBase {
const config = Config();
this.groupCache = new LRU({
max : _.get(config, 'contentServers.nntp.cache.maxItems', 200),
maxAge : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s
ttl : _.get(config, 'contentServers.nntp.cache.maxAge', 1000 * 30), // default=30s
});
}

View File

@@ -215,20 +215,22 @@ exports.getModule = class WebServerModule extends ServerModule {
routeIndex(req, resp) {
const filePath = paths.join(Config().contentServers.web.staticRoot, 'index.html');
return this.returnStaticPage(filePath, resp);
}
routeStaticFile(req, resp) {
const fileName = req.url.substr(req.url.indexOf('/', 1));
const filePath = paths.join(Config().contentServers.web.staticRoot, fileName);
const filePath = this.resolveStaticPath(fileName);
return this.returnStaticPage(filePath, resp);
}
returnStaticPage(filePath, resp) {
const self = this;
if (!filePath) {
return this.fileNotFound(resp);
}
fs.stat(filePath, (err, stats) => {
if(err || !stats.isFile()) {
return self.fileNotFound(resp);
@@ -245,6 +247,14 @@ exports.getModule = class WebServerModule extends ServerModule {
});
}
resolveStaticPath(requestPath) {
const staticRoot = _.get(Config(), 'contentServers.web.staticRoot');
const path = paths.resolve(staticRoot, `.${requestPath}`);
if (path.startsWith(staticRoot)) {
return path;
}
}
routeTemplateFilePage(templatePath, preprocessCallback, resp) {
const self = this;

View File

@@ -357,9 +357,11 @@ exports.getModule = class SSHServerModule extends LoginServerModule {
// However, as of this writing, NetRunner and SyncTERM both
// fail to respond to OpenSSH keep-alive pings (keepalive@openssh.com)
//
ssh2.Server.KEEPALIVE_INTERVAL = 0;
// See also #399
//
ssh2.Server.KEEPALIVE_CLIENT_INTERVAL = 0;
this.server = ssh2.Server(serverConf);
this.server = new ssh2.Server(serverConf);
this.server.on('connection', (conn, info) => {
Log.info(info, 'New SSH connection');
this.handleNewClient(new SSHClient(conn), conn._sock, ModuleInfo);

View File

@@ -103,14 +103,16 @@ class TelnetClient {
case Options.NEW_ENVIRON :
{
this._logDebug(
{ vars : command.optionData.vars, userVars : command.optionData.userVars },
{ vars : command.optionData.vars, uservars : command.optionData.uservars },
'New environment received'
);
// get a value from vars with fallback of user vars
const getValue = (name) => {
return command.optionData.vars.find(nv => nv.name === name) ||
command.optionData.userVars.find(nv => nv.name === name);
return command.optionData.vars &&
(command.optionData.vars.find(nv => nv.name === name) ||
command.optionData.uservars.find(nv => nv.name === name)
);
};
if ('unknown' === this.term.termType) {

View File

@@ -171,22 +171,15 @@ exports.getModule = class ShowArtModule extends MenuModule {
return callback(err);
}
const mciData = { menu : artData.mciMap };
return callback(null, mciData);
if(self.client.term.termHeight > 0 && artData.height > self.client.term.termHeight) {
// We must have scrolled, adjust the positioning for pause
artData.height = self.client.term.termHeight;
}
const pausePosition = { row: artData.height + 1, col: 1};
return callback(null, mciData, pausePosition);
}
);
},
function recordCursorPosition(mciData, callback) {
if(!options.pause) {
return callback(null, mciData, null); // cursor position not needed
}
self.client.once('cursor position report', pos => {
const pausePosition = { row : pos[0], col : 1 };
return callback(null, mciData, pausePosition);
});
self.client.term.rawWrite(ANSI.queryPos());
},
function afterArtDisplayed(mciData, pausePosition, callback) {
self.mciReady(mciData, err => {
return callback(err, pausePosition);

View File

@@ -20,6 +20,7 @@ exports.stringFromNullTermBuffer = stringFromNullTermBuffer;
exports.stringToNullTermBuffer = stringToNullTermBuffer;
exports.renderSubstr = renderSubstr;
exports.renderStringLength = renderStringLength;
exports.ansiRenderStringLength = ansiRenderStringLength;
exports.formatByteSizeAbbr = formatByteSizeAbbr;
exports.formatByteSize = formatByteSize;
exports.formatCountAbbr = formatCountAbbr;
@@ -297,7 +298,7 @@ function renderStringLength(s) {
let len = 0;
const re = ANSI_OR_PIPE_REGEXP;
re.lastIndex = 0; // we recycle the rege; reset
re.lastIndex = 0; // we recycle the regex; reset
//
// Loop counting only literal (non-control) sequences
@@ -312,7 +313,41 @@ function renderStringLength(s) {
len += s.slice(pos, m.index).length;
}
if('C' === m[3]) { // ESC[<N>C is foward/right
if('C' === m[3]) { // ESC[<N>C is forward/right
len += parseInt(m[2], 10) || 0;
}
}
} while(0 !== re.lastIndex);
if(pos < s.length) {
len += s.slice(pos).length;
}
return len;
}
// Like renderStringLength() but ANSI only (no pipe codes accounted for)
function ansiRenderStringLength(s) {
let m;
let pos;
let len = 0;
const re = ANSI.getFullMatchRegExp();
//
// Loop counting only literal (non-control) sequences
// paying special attention to ESC[<N>C which means forward <N>
//
do {
pos = re.lastIndex;
m = re.exec(s);
if(m) {
if(m.index > pos) {
len += s.slice(pos, m.index).length;
}
if('C' === m[3]) { // ESC[<N>C is forward/right
len += parseInt(m[2], 10) || 0;
}
}

View File

@@ -26,6 +26,7 @@ exports.nextConf = nextConf;
exports.prevArea = prevArea;
exports.nextArea = nextArea;
exports.sendForgotPasswordEmail = sendForgotPasswordEmail;
exports.optimizeDatabases = optimizeDatabases;
const handleAuthFailures = (callingMenu, err, cb) => {
// already logged in with this user?
@@ -205,3 +206,25 @@ function sendForgotPasswordEmail(callingMenu, formData, extraArgs, cb) {
return logoff(callingMenu, formData, extraArgs, cb);
});
}
function optimizeDatabases(callingMenu, formData, extraArgs, cb) {
const dbs = require('./database').dbs;
const client = callingMenu.client;
client.term.write('\r\n\r\n');
Object.keys(dbs).forEach(dbName => {
client.log.info({ dbName }, 'Optimizing database');
client.term.write(`Optimizing ${dbName}. Please wait...\r\n`);
// https://www.sqlite.org/pragma.html#pragma_optimize
dbs[dbName].run('PRAGMA optimize;', err => {
if (err) {
client.log.error({ error : err, dbName }, 'Error attempting to optimize database');
}
});
});
return callingMenu.prevMenu(cb);
}

View File

@@ -44,49 +44,6 @@ function TextView(options) {
this.textMaskChar = options.textMaskChar;
}
/*
this.drawText = function(s) {
//
// |<- this.maxLength
// ABCDEFGHIJK
// |ABCDEFG| ^_ this.text.length
// ^-- this.dimens.width
//
let textToDraw = _.isString(this.textMaskChar) ?
new Array(s.length + 1).join(this.textMaskChar) :
stylizeString(s, this.hasFocus ? this.focusTextStyle : this.textStyle);
if(textToDraw.length > this.dimens.width) {
if(this.hasFocus) {
if(this.horizScroll) {
textToDraw = textToDraw.substr(textToDraw.length - this.dimens.width, textToDraw.length);
}
} else {
if(textToDraw.length > this.dimens.width) {
if(this.textOverflow &&
this.dimens.width > this.textOverflow.length &&
textToDraw.length - this.textOverflow.length >= this.textOverflow.length)
{
textToDraw = textToDraw.substr(0, this.dimens.width - this.textOverflow.length) + this.textOverflow;
} else {
textToDraw = textToDraw.substr(0, this.dimens.width);
}
}
}
}
this.client.term.write(padStr(
textToDraw,
this.dimens.width + 1,
this.fillChar,
this.justify,
this.hasFocus ? this.getFocusSGR() : this.getSGR(),
this.getStyleSGR(1) || this.getSGR()
), false);
};
*/
this.drawText = function(s) {
//
@@ -125,7 +82,7 @@ function TextView(options) {
this.client.term.write(
padStr(
textToDraw,
this.dimens.width + 1,
this.dimens.width,
renderedFillChar, //this.fillChar,
this.justify,
this.hasFocus ? this.getFocusSGR() : this.getSGR(),

View File

@@ -495,6 +495,7 @@ function displayPreparedArt(options, artInfo, cb) {
sauce : artInfo.sauce,
font : options.font,
trailingLF : options.trailingLF,
startRow : options.startRow,
};
art.display(options.client, artInfo.data, displayOpts, (err, mciMap, extraInfo) => {
return cb(err, { mciMap : mciMap, artInfo : artInfo, extraInfo : extraInfo } );
@@ -551,6 +552,7 @@ function displayThemedPrompt(name, client, options, cb) {
if(options.clearScreen) {
client.term.rawWrite(ansi.resetScreen());
options.position = {row: 1, column: 1};
}
//
@@ -560,9 +562,9 @@ function displayThemedPrompt(name, client, options, cb) {
//
const dispOptions = Object.assign( {}, options, promptConfig.config );
// :TODO: We can use term detection to do nifty things like avoid this kind of kludge:
if(!options.clearScreen) {
dispOptions.font = 'not_really_a_font!'; // kludge :)
}
// if(!options.clearScreen) {
// dispOptions.font = 'not_really_a_font!'; // kludge :)
// }
displayThemedAsset(
promptConfig.art,
@@ -583,12 +585,15 @@ function displayThemedPrompt(name, client, options, cb) {
return callback(null, promptConfig, artInfo);
}
client.once('cursor position report', pos => {
artInfo.startRow = pos[0] - artInfo.height;
return callback(null, promptConfig, artInfo);
});
if(_.isNumber(options?.position?.row)) {
artInfo.startRow = options.position.row;
if(client.term.termHeight > 0 && artInfo.startRow + artInfo.height > client.term.termHeight) {
// in this case, we will have scrolled
artInfo.startRow = client.term.termHeight - artInfo.height;
}
}
client.term.rawWrite(ansi.queryPos());
return callback(null, promptConfig, artInfo);
},
function createMCIViews(promptConfig, artInfo, callback) {
const assocViewController = usingTempViewController ? new ViewController( { client : client } ) : options.viewController;
@@ -614,7 +619,9 @@ function displayThemedPrompt(name, client, options, cb) {
});
},
function clearPauseArt(artInfo, assocViewController, callback) {
if(options.clearPrompt) {
// Only clear with height if clearPrompt is true and if we were able
// to determine the row
if(options.clearPrompt && artInfo.startRow) {
if(artInfo.startRow && artInfo.height) {
client.term.rawWrite(ansi.goto(artInfo.startRow, 1));

View File

@@ -332,6 +332,7 @@ exports.getModule = class UploadModule extends MenuModule {
const scanOpts = {
areaTag : self.areaInfo.areaTag,
storageTag : self.areaInfo.storageTags[0],
hashTags : self.areaInfo.hashTags,
};
function handleScanStep(stepInfo, nextScanStep) {

View File

@@ -17,7 +17,7 @@ exports.View = View;
const VIEW_SPECIAL_KEY_MAP_DEFAULT = {
accept : [ 'return' ],
exit : [ 'esc' ],
backspace : [ 'backspace', 'del' ],
backspace : [ 'backspace', 'del', 'ctrl + d'], // https://www.tecmint.com/linux-command-line-bash-shortcut-keys/
del : [ 'del' ],
next : [ 'tab' ],
up : [ 'up arrow' ],
@@ -154,7 +154,7 @@ View.prototype.setHeight = function(height) {
View.prototype.setWidth = function(width) {
width = parseInt(width) || 1;
width = Math.min(width, this.client.term.termWidth);
width = Math.min(width, this.client.term.termWidth - this.position.col);
this.dimens.width = width;
};
@@ -186,7 +186,7 @@ View.prototype.setPropertyValue = function(propName, value) {
case 'height' : this.setHeight(value); break;
case 'width' : this.setWidth(value); break;
case 'focus' : this.setFocus(value); break;
case 'focus' : this.setFocusProperty(value); break;
case 'text' :
if('setText' in this) {
@@ -252,10 +252,16 @@ View.prototype.redraw = function() {
this.client.term.write(ansi.goto(this.position.row, this.position.col));
};
View.prototype.setFocus = function(focused) {
enigAssert(this.acceptsFocus, 'View does not accept focus');
View.prototype.setFocusProperty = function(focused) {
// Either this should accept focus, or the focus should be false
enigAssert(this.acceptsFocus || !focused, 'View does not accept focus');
this.hasFocus = focused;
};
View.prototype.setFocus = function(focused) {
// Call separate method to differentiate between a value set as a
// property vs focus programmatically called.
this.setFocusProperty(focused);
this.restoreCursor();
};

View File

@@ -1,7 +1,9 @@
/* jslint node: true */
'use strict';
const renderStringLength = require('./string_util.js').renderStringLength;
const {
ansiRenderStringLength,
} = require('./string_util');
// deps
const assert = require('assert');
@@ -28,7 +30,7 @@ function wordWrapText(text, options) {
//const REGEXP_GOBBLE = new RegExp(`.{0,${options.width}}`, 'g');
//
// For a given word, match 0->options.width chars -- alwasy include a full trailing ESC
// For a given word, match 0->options.width chars -- always include a full trailing ESC
// sequence if present!
//
// :TODO: Need to create ansi.getMatchRegex or something - this is used all over
@@ -49,7 +51,7 @@ function wordWrapText(text, options) {
function appendWord() {
word.match(REGEXP_GOBBLE).forEach( w => {
renderLen = renderStringLength(w);
renderLen = ansiRenderStringLength(w);
if(result.renderLen[i] + renderLen > options.width) {
if(0 === i) {
@@ -70,7 +72,7 @@ function wordWrapText(text, options) {
//
// * Sublime Text 3 for example considers spaces after a word
// part of said word. For example, "word " would be wraped
// in it's entirity.
// in it's entirety.
//
// * Tabs in Sublime Text 3 are also treated as a word, so, e.g.
// "\t" may resolve to " " and must fit within the space.