* SEXYZ: XModem & YModem
* Explicit sort avail to protocols * MenuView.removeItem() * Natural sort for more things * Fix some issues with HorizontalMenuView redraw/update * Sanatize non-blind upload filename (security) * Validator on non-blind upload filename
This commit is contained in:
parent
8261881e3e
commit
ff64a7aed5
|
@ -364,9 +364,48 @@ function getDefaultConfig() {
|
||||||
},
|
},
|
||||||
|
|
||||||
fileTransferProtocols : {
|
fileTransferProtocols : {
|
||||||
|
zmodem8kSexyz : {
|
||||||
|
name : 'ZModem 8k (SEXYZ)',
|
||||||
|
type : 'external',
|
||||||
|
sort : 1,
|
||||||
|
external : {
|
||||||
|
// :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems
|
||||||
|
sendCmd : 'sexyz',
|
||||||
|
sendArgs : [ '-telnet', '-8', 'sz', '@{fileListPath}' ],
|
||||||
|
recvCmd : 'sexyz',
|
||||||
|
recvArgs : [ '-telnet', '-8', 'rz', '{uploadDir}' ],
|
||||||
|
recvArgsNonBatch : [ '-telnet', '-8', 'rz', '{fileName}' ],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
xmodemSexyz : {
|
||||||
|
name : 'XModem (SEXYZ)',
|
||||||
|
type : 'external',
|
||||||
|
sort : 3,
|
||||||
|
external : {
|
||||||
|
sendCmd : 'sexyz',
|
||||||
|
sendArgs : [ '-telnet', 'sX', '@{fileListPath}' ],
|
||||||
|
recvCmd : 'sexyz',
|
||||||
|
recvArgsNonBatch : [ '-telnet', 'rC', '{fileName}' ]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
ymodemSexyz : {
|
||||||
|
name : 'YModem (SEXYZ)',
|
||||||
|
type : 'external',
|
||||||
|
sort : 4,
|
||||||
|
external : {
|
||||||
|
sendCmd : 'sexyz',
|
||||||
|
sendArgs : [ '-telnet', 'sY', '@{fileListPath}' ],
|
||||||
|
recvCmd : 'sexyz',
|
||||||
|
recvArgs : [ '-telnet', 'ry', '{uploadDir}' ],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
zmodem8kSz : {
|
zmodem8kSz : {
|
||||||
name : 'ZModem 8k',
|
name : 'ZModem 8k',
|
||||||
type : 'external',
|
type : 'external',
|
||||||
|
sort : 2,
|
||||||
external : {
|
external : {
|
||||||
sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
|
sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz"
|
||||||
sendArgs : [
|
sendArgs : [
|
||||||
|
@ -380,28 +419,7 @@ function getDefaultConfig() {
|
||||||
// :TODO: can we not just use --escape ?
|
// :TODO: can we not just use --escape ?
|
||||||
escapeTelnet : true, // set to true to escape Telnet codes such as IAC
|
escapeTelnet : true, // set to true to escape Telnet codes such as IAC
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
|
||||||
zmodem8kSexyz : {
|
|
||||||
name : 'ZModem 8k (SEXYZ)',
|
|
||||||
type : 'external',
|
|
||||||
external : {
|
|
||||||
// :TODO: Look into shipping sexyz binaries or at least hosting them somewhere for common systems
|
|
||||||
sendCmd : 'sexyz',
|
|
||||||
sendArgs : [
|
|
||||||
'-telnet', '-8', 'sz', '@{fileListPath}'
|
|
||||||
],
|
|
||||||
recvCmd : 'sexyz',
|
|
||||||
recvArgs : [
|
|
||||||
'-telnet', '-8', 'rz', '{uploadDir}'
|
|
||||||
],
|
|
||||||
recvArgsNonBatch : [
|
|
||||||
'-telnet', '-8', 'rz', '{fileName}'
|
|
||||||
],
|
|
||||||
escapeTelnet : false, // -telnet option does this for us
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
messageAreaDefaults : {
|
messageAreaDefaults : {
|
||||||
|
|
|
@ -77,6 +77,20 @@ MenuView.prototype.setItems = function(items) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MenuView.prototype.removeItem = function(index) {
|
||||||
|
this.items.splice(index, 1);
|
||||||
|
|
||||||
|
if(this.focusItems) {
|
||||||
|
this.focusItems.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.focusedItemIndex >= index) {
|
||||||
|
this.focusedItemIndex = Math.max(this.focusedItemIndex - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.positionCacheExpired = true;
|
||||||
|
};
|
||||||
|
|
||||||
MenuView.prototype.getCount = function() {
|
MenuView.prototype.getCount = function() {
|
||||||
return this.items.length;
|
return this.items.length;
|
||||||
};
|
};
|
||||||
|
|
|
@ -79,7 +79,7 @@ exports.getModule = class NewScanModule extends MenuModule {
|
||||||
if('system_internal' === a.confTag) {
|
if('system_internal' === a.confTag) {
|
||||||
return -1;
|
return -1;
|
||||||
} else {
|
} else {
|
||||||
return a.conf.name.localeCompare(b.conf.name);
|
return a.conf.name.localeCompare(b.conf.name, { sensitivity : false, numeric : true } );
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -195,12 +195,14 @@ function getPredefinedMCIValue(client, code) {
|
||||||
// :TODO: System stat log for total ul/dl, total ul/dl bytes
|
// :TODO: System stat log for total ul/dl, total ul/dl bytes
|
||||||
|
|
||||||
// :TODO: PT - Messages posted *today* (Obv/2)
|
// :TODO: PT - Messages posted *today* (Obv/2)
|
||||||
|
// -> Include FTN/etc.
|
||||||
// :TODO: NT - New users today (Obv/2)
|
// :TODO: NT - New users today (Obv/2)
|
||||||
// :TODO: CT - Calls *today* (Obv/2)
|
// :TODO: CT - Calls *today* (Obv/2)
|
||||||
// :TODO: TF - Total files on the system (Obv/2)
|
// :TODO: TF - Total files on the system (Obv/2)
|
||||||
// :TODO: FT - Files uploaded/added *today* (Obv/2)
|
// :TODO: FT - Files uploaded/added *today* (Obv/2)
|
||||||
// :TODO: DD - Files downloaded *today* (iNiQUiTY)
|
// :TODO: DD - Files downloaded *today* (iNiQUiTY)
|
||||||
// :TODO: TP - total message/posts on the system (Obv/2)
|
// :TODO: TP - total message/posts on the system (Obv/2)
|
||||||
|
// -> Include FTN/etc.
|
||||||
// :TODO: LC - name of last caller to system (Obv/2)
|
// :TODO: LC - name of last caller to system (Obv/2)
|
||||||
// :TODO: TZ - Average *system* post/call ratio (iNiQUiTY)
|
// :TODO: TZ - Average *system* post/call ratio (iNiQUiTY)
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ function VerticalMenuView(options) {
|
||||||
|
|
||||||
self.viewWindow = {
|
self.viewWindow = {
|
||||||
top : self.focusedItemIndex,
|
top : self.focusedItemIndex,
|
||||||
bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1
|
bottom : Math.min(self.focusedItemIndex + self.maxVisibleItems, self.items.length) - 1,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -107,12 +107,14 @@ VerticalMenuView.prototype.redraw = function() {
|
||||||
delete this.oldDimens;
|
delete this.oldDimens;
|
||||||
}
|
}
|
||||||
|
|
||||||
let row = this.position.row;
|
if(this.items.length) {
|
||||||
for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) {
|
let row = this.position.row;
|
||||||
this.items[i].row = row;
|
for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) {
|
||||||
row += this.itemSpacing + 1;
|
this.items[i].row = row;
|
||||||
this.items[i].focused = this.focusedItemIndex === i;
|
row += this.itemSpacing + 1;
|
||||||
this.drawItem(i);
|
this.items[i].focused = this.focusedItemIndex === i;
|
||||||
|
this.drawItem(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -171,7 +173,7 @@ VerticalMenuView.prototype.getData = function() {
|
||||||
VerticalMenuView.prototype.setItems = function(items) {
|
VerticalMenuView.prototype.setItems = function(items) {
|
||||||
// if we have items already, save off their drawing area so we don't leave fragments at redraw
|
// if we have items already, save off their drawing area so we don't leave fragments at redraw
|
||||||
if(this.items && this.items.length) {
|
if(this.items && this.items.length) {
|
||||||
this.oldDimens = this.dimens;
|
this.oldDimens = Object.assign({}, this.dimens);
|
||||||
}
|
}
|
||||||
|
|
||||||
VerticalMenuView.super_.prototype.setItems.call(this, items);
|
VerticalMenuView.super_.prototype.setItems.call(this, items);
|
||||||
|
@ -179,6 +181,14 @@ VerticalMenuView.prototype.setItems = function(items) {
|
||||||
this.positionCacheExpired = true;
|
this.positionCacheExpired = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
VerticalMenuView.prototype.removeItem = function(index) {
|
||||||
|
if(this.items && this.items.length) {
|
||||||
|
this.oldDimens = Object.assign({}, this.dimens);
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalMenuView.super_.prototype.removeItem.call(this, index);
|
||||||
|
};
|
||||||
|
|
||||||
// :TODO: Apply draw optimizaitons when only two items need drawn vs entire view!
|
// :TODO: Apply draw optimizaitons when only two items need drawn vs entire view!
|
||||||
|
|
||||||
VerticalMenuView.prototype.focusNext = function() {
|
VerticalMenuView.prototype.focusNext = function() {
|
||||||
|
|
|
@ -454,7 +454,7 @@ ViewController.prototype.resetInitialFocus = function() {
|
||||||
if(this.formInitialFocusId) {
|
if(this.formInitialFocusId) {
|
||||||
return this.switchFocus(this.formInitialFocusId);
|
return this.switchFocus(this.formInitialFocusId);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
ViewController.prototype.switchFocus = function(id) {
|
ViewController.prototype.switchFocus = function(id) {
|
||||||
//
|
//
|
||||||
|
@ -480,15 +480,19 @@ ViewController.prototype.switchFocus = function(id) {
|
||||||
};
|
};
|
||||||
|
|
||||||
ViewController.prototype.nextFocus = function() {
|
ViewController.prototype.nextFocus = function() {
|
||||||
var nextId;
|
let nextFocusView = this.focusedView ? this.focusedView : this.views[this.firstId];
|
||||||
|
|
||||||
if(!this.focusedView) {
|
// find the next view that accepts focus
|
||||||
nextId = this.views[this.firstId].id;
|
while(nextFocusView && nextFocusView.nextId) {
|
||||||
} else {
|
nextFocusView = this.getView(nextFocusView.nextId);
|
||||||
nextId = this.views[this.focusedView.id].nextId;
|
if(!nextFocusView || nextFocusView.acceptsFocus) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.switchFocus(nextId);
|
if(nextFocusView && this.focusedView !== nextFocusView) {
|
||||||
|
this.switchFocus(nextFocusView.id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ViewController.prototype.setViewOrder = function(order) {
|
ViewController.prototype.setViewOrder = function(order) {
|
||||||
|
@ -507,7 +511,6 @@ ViewController.prototype.setViewOrder = function(order) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(viewIdOrder.length > 0) {
|
if(viewIdOrder.length > 0) {
|
||||||
var view;
|
|
||||||
var count = viewIdOrder.length - 1;
|
var count = viewIdOrder.length - 1;
|
||||||
for(var i = 0; i < count; ++i) {
|
for(var i = 0; i < count; ++i) {
|
||||||
this.views[viewIdOrder[i]].nextId = viewIdOrder[i + 1];
|
this.views[viewIdOrder[i]].nextId = viewIdOrder[i + 1];
|
||||||
|
|
|
@ -69,13 +69,13 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
||||||
this.dlQueue.removeItems(selectedItem.fileId);
|
this.dlQueue.removeItems(selectedItem.fileId);
|
||||||
|
|
||||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||||
return this.updateDownloadQueueView(cb);
|
return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb);
|
||||||
},
|
},
|
||||||
clearQueue : (formData, extraArgs, cb) => {
|
clearQueue : (formData, extraArgs, cb) => {
|
||||||
this.dlQueue.clear();
|
this.dlQueue.clear();
|
||||||
|
|
||||||
// :TODO: broken: does not redraw menu properly - needs fixed!
|
// :TODO: broken: does not redraw menu properly - needs fixed!
|
||||||
return this.updateDownloadQueueView(cb);
|
return this.removeItemsFromDownloadQueueView('all', cb);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -108,6 +108,23 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeItemsFromDownloadQueueView(itemIndex, cb) {
|
||||||
|
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
||||||
|
if(!queueView) {
|
||||||
|
return cb(Errors.DoesNotExist('Queue view does not exist'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if('all' === itemIndex) {
|
||||||
|
queueView.setItems([]);
|
||||||
|
queueView.setFocusItems([]);
|
||||||
|
} else {
|
||||||
|
queueView.removeItem(itemIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
queueView.redraw();
|
||||||
|
return cb(null);
|
||||||
|
}
|
||||||
|
|
||||||
updateDownloadQueueView(cb) {
|
updateDownloadQueueView(cb) {
|
||||||
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue);
|
||||||
if(!queueView) {
|
if(!queueView) {
|
||||||
|
|
|
@ -135,6 +135,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
||||||
name : protInfo.name,
|
name : protInfo.name,
|
||||||
hasBatch : _.has(protInfo, 'external.recvArgs'),
|
hasBatch : _.has(protInfo, 'external.recvArgs'),
|
||||||
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'),
|
hasNonBatch : _.has(protInfo, 'external.recvArgsNonBatch'),
|
||||||
|
sort : protInfo.sort,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -145,6 +146,13 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule {
|
||||||
this.protocols = this.protocols.filter( prot => prot.hasBatch );
|
this.protocols = this.protocols.filter( prot => prot.hasBatch );
|
||||||
}
|
}
|
||||||
|
|
||||||
this.protocols.sort( (a, b) => a.name.localeCompare(b.name) );
|
// natural sort taking explicit orders into consideration
|
||||||
|
this.protocols.sort( (a, b) => {
|
||||||
|
if(_.isNumber(a.sort) && _.isNumber(b.sort)) {
|
||||||
|
return a.sort - b.sort;
|
||||||
|
} else {
|
||||||
|
return a.name.localeCompare(b.name, { sensitivity : false, numeric : true } );
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,6 +18,7 @@ const async = require('async');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const temptmp = require('temptmp').createTrackedSession('upload');
|
const temptmp = require('temptmp').createTrackedSession('upload');
|
||||||
const paths = require('path');
|
const paths = require('path');
|
||||||
|
const sanatizeFilename = require('sanitize-filename');
|
||||||
|
|
||||||
exports.moduleInfo = {
|
exports.moduleInfo = {
|
||||||
name : 'Upload',
|
name : 'Upload',
|
||||||
|
@ -38,6 +39,7 @@ const MciViewIds = {
|
||||||
uploadType : 2, // blind vs specify filename
|
uploadType : 2, // blind vs specify filename
|
||||||
fileName : 3, // for non-blind; not editable for blind
|
fileName : 3, // for non-blind; not editable for blind
|
||||||
navMenu : 4, // next/cancel/etc.
|
navMenu : 4, // next/cancel/etc.
|
||||||
|
errMsg : 5, // errors (e.g. filename cannot be blank)
|
||||||
},
|
},
|
||||||
|
|
||||||
processing : {
|
processing : {
|
||||||
|
@ -77,6 +79,37 @@ exports.getModule = class UploadModule extends MenuModule {
|
||||||
// see displayFileDetailsPageForUploadEntry() for this hackery:
|
// see displayFileDetailsPageForUploadEntry() for this hackery:
|
||||||
cb(null);
|
cb(null);
|
||||||
return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any
|
return this.fileDetailsCurrentEntrySubmitCallback(null, formData.value); // move on to the next entry, if any
|
||||||
|
},
|
||||||
|
|
||||||
|
// validation
|
||||||
|
validateNonBlindFileName : (fileName, cb) => {
|
||||||
|
fileName = sanatizeFilename(fileName); // remove unsafe chars, path info, etc.
|
||||||
|
if(0 === fileName.length) {
|
||||||
|
return cb(new Error('Invalid filename'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if(0 === fileName.length) {
|
||||||
|
return cb(new Error('Filename cannot be empty'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// At least SEXYZ doesn't like non-blind names that start with a number - it becomes confused
|
||||||
|
if(/^[0-9].*$/.test(fileName)) {
|
||||||
|
return cb(new Error('Invalid filename'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
|
},
|
||||||
|
viewValidationListener : (err, cb) => {
|
||||||
|
const errView = this.viewControllers.options.getView(MciViewIds.options.errMsg);
|
||||||
|
if(errView) {
|
||||||
|
if(err) {
|
||||||
|
errView.setText(err.message);
|
||||||
|
} else {
|
||||||
|
errView.clearText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -156,6 +189,7 @@ exports.getModule = class UploadModule extends MenuModule {
|
||||||
};
|
};
|
||||||
|
|
||||||
if(!this.isBlindUpload()) {
|
if(!this.isBlindUpload()) {
|
||||||
|
// data has been sanatized at this point
|
||||||
modOpts.extraArgs.recvFileName = this.viewControllers.options.getView(MciViewIds.options.fileName).getData();
|
modOpts.extraArgs.recvFileName = this.viewControllers.options.getView(MciViewIds.options.fileName).getData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -463,8 +497,17 @@ exports.getModule = class UploadModule extends MenuModule {
|
||||||
|
|
||||||
if(self.isBlindUpload()) {
|
if(self.isBlindUpload()) {
|
||||||
fileNameView.setText(blindFileNameText);
|
fileNameView.setText(blindFileNameText);
|
||||||
|
fileNameView.acceptsFocus = false;
|
||||||
|
} else {
|
||||||
|
fileNameView.clearText();
|
||||||
|
fileNameView.acceptsFocus = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// :TODO: when blind, fileNameView should not be focus/editable
|
// sanatize filename for display when leaving the view
|
||||||
|
self.viewControllers.options.on('leave', prevView => {
|
||||||
|
if(prevView.id === MciViewIds.options.fileName) {
|
||||||
|
fileNameView.setText(sanatizeFilename(fileNameView.getData()));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,8 @@
|
||||||
"ptyw.js": "NuSkooler/ptyw.js",
|
"ptyw.js": "NuSkooler/ptyw.js",
|
||||||
"sqlite3": "^3.1.1",
|
"sqlite3": "^3.1.1",
|
||||||
"ssh2": "^0.5.1",
|
"ssh2": "^0.5.1",
|
||||||
"temptmp" : "^1.0.0"
|
"temptmp" : "^1.0.0",
|
||||||
|
"sanitize-filename" : "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue