diff --git a/core/config.js b/core/config.js index 816149e1..32335803 100644 --- a/core/config.js +++ b/core/config.js @@ -364,9 +364,48 @@ function getDefaultConfig() { }, 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 : { name : 'ZModem 8k', type : 'external', + sort : 2, external : { sendCmd : 'sz', // Avail on Debian/Ubuntu based systems as the package "lrzsz" sendArgs : [ @@ -380,28 +419,7 @@ function getDefaultConfig() { // :TODO: can we not just use --escape ? 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 : { diff --git a/core/menu_view.js b/core/menu_view.js index fe4954f2..07d19e8a 100644 --- a/core/menu_view.js +++ b/core/menu_view.js @@ -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() { return this.items.length; }; diff --git a/core/new_scan.js b/core/new_scan.js index abcc43b2..0483e8d1 100644 --- a/core/new_scan.js +++ b/core/new_scan.js @@ -79,7 +79,7 @@ exports.getModule = class NewScanModule extends MenuModule { if('system_internal' === a.confTag) { return -1; } else { - return a.conf.name.localeCompare(b.conf.name); + return a.conf.name.localeCompare(b.conf.name, { sensitivity : false, numeric : true } ); } }); diff --git a/core/predefined_mci.js b/core/predefined_mci.js index 69463f4d..58f56cf4 100644 --- a/core/predefined_mci.js +++ b/core/predefined_mci.js @@ -195,12 +195,14 @@ function getPredefinedMCIValue(client, code) { // :TODO: System stat log for total ul/dl, total ul/dl bytes // :TODO: PT - Messages posted *today* (Obv/2) + // -> Include FTN/etc. // :TODO: NT - New users today (Obv/2) // :TODO: CT - Calls *today* (Obv/2) // :TODO: TF - Total files on the system (Obv/2) // :TODO: FT - Files uploaded/added *today* (Obv/2) // :TODO: DD - Files downloaded *today* (iNiQUiTY) // :TODO: TP - total message/posts on the system (Obv/2) + // -> Include FTN/etc. // :TODO: LC - name of last caller to system (Obv/2) // :TODO: TZ - Average *system* post/call ratio (iNiQUiTY) diff --git a/core/vertical_menu_view.js b/core/vertical_menu_view.js index f72095e1..445f5f4a 100644 --- a/core/vertical_menu_view.js +++ b/core/vertical_menu_view.js @@ -44,7 +44,7 @@ function VerticalMenuView(options) { self.viewWindow = { 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; } - let row = this.position.row; - for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) { - this.items[i].row = row; - row += this.itemSpacing + 1; - this.items[i].focused = this.focusedItemIndex === i; - this.drawItem(i); + if(this.items.length) { + let row = this.position.row; + for(let i = this.viewWindow.top; i <= this.viewWindow.bottom; ++i) { + this.items[i].row = row; + row += this.itemSpacing + 1; + this.items[i].focused = this.focusedItemIndex === i; + this.drawItem(i); + } } }; @@ -171,7 +173,7 @@ VerticalMenuView.prototype.getData = function() { VerticalMenuView.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 = this.dimens; + this.oldDimens = Object.assign({}, this.dimens); } VerticalMenuView.super_.prototype.setItems.call(this, items); @@ -179,6 +181,14 @@ VerticalMenuView.prototype.setItems = function(items) { 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! VerticalMenuView.prototype.focusNext = function() { diff --git a/core/view_controller.js b/core/view_controller.js index 392471cc..51c63b3a 100644 --- a/core/view_controller.js +++ b/core/view_controller.js @@ -454,7 +454,7 @@ ViewController.prototype.resetInitialFocus = function() { if(this.formInitialFocusId) { return this.switchFocus(this.formInitialFocusId); } -} +}; ViewController.prototype.switchFocus = function(id) { // @@ -480,15 +480,19 @@ ViewController.prototype.switchFocus = function(id) { }; ViewController.prototype.nextFocus = function() { - var nextId; + let nextFocusView = this.focusedView ? this.focusedView : this.views[this.firstId]; - if(!this.focusedView) { - nextId = this.views[this.firstId].id; - } else { - nextId = this.views[this.focusedView.id].nextId; + // find the next view that accepts focus + while(nextFocusView && nextFocusView.nextId) { + nextFocusView = this.getView(nextFocusView.nextId); + if(!nextFocusView || nextFocusView.acceptsFocus) { + break; + } } - this.switchFocus(nextId); + if(nextFocusView && this.focusedView !== nextFocusView) { + this.switchFocus(nextFocusView.id); + } }; ViewController.prototype.setViewOrder = function(order) { @@ -507,7 +511,6 @@ ViewController.prototype.setViewOrder = function(order) { } if(viewIdOrder.length > 0) { - var view; var count = viewIdOrder.length - 1; for(var i = 0; i < count; ++i) { this.views[viewIdOrder[i]].nextId = viewIdOrder[i + 1]; diff --git a/mods/file_base_download_manager.js b/mods/file_base_download_manager.js index f6b0c1b0..8ddcb735 100644 --- a/mods/file_base_download_manager.js +++ b/mods/file_base_download_manager.js @@ -69,13 +69,13 @@ exports.getModule = class FileBaseDownloadQueueManager extends MenuModule { this.dlQueue.removeItems(selectedItem.fileId); // :TODO: broken: does not redraw menu properly - needs fixed! - return this.updateDownloadQueueView(cb); + return this.removeItemsFromDownloadQueueView(formData.value.queueItem, cb); }, clearQueue : (formData, extraArgs, cb) => { this.dlQueue.clear(); // :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) { const queueView = this.viewControllers.queueManager.getView(MciViewIds.queueManager.queue); if(!queueView) { diff --git a/mods/file_transfer_protocol_select.js b/mods/file_transfer_protocol_select.js index a25b8709..6efa5a93 100644 --- a/mods/file_transfer_protocol_select.js +++ b/mods/file_transfer_protocol_select.js @@ -135,6 +135,7 @@ exports.getModule = class FileTransferProtocolSelectModule extends MenuModule { name : protInfo.name, hasBatch : _.has(protInfo, 'external.recvArgs'), 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.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 } ); + } + }); } }; diff --git a/mods/upload.js b/mods/upload.js index 4169f34b..c260d1f3 100644 --- a/mods/upload.js +++ b/mods/upload.js @@ -18,6 +18,7 @@ const async = require('async'); const _ = require('lodash'); const temptmp = require('temptmp').createTrackedSession('upload'); const paths = require('path'); +const sanatizeFilename = require('sanitize-filename'); exports.moduleInfo = { name : 'Upload', @@ -38,6 +39,7 @@ const MciViewIds = { uploadType : 2, // blind vs specify filename fileName : 3, // for non-blind; not editable for blind navMenu : 4, // next/cancel/etc. + errMsg : 5, // errors (e.g. filename cannot be blank) }, processing : { @@ -77,6 +79,37 @@ exports.getModule = class UploadModule extends MenuModule { // see displayFileDetailsPageForUploadEntry() for this hackery: cb(null); 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()) { + // data has been sanatized at this point modOpts.extraArgs.recvFileName = this.viewControllers.options.getView(MciViewIds.options.fileName).getData(); } @@ -463,10 +497,19 @@ exports.getModule = class UploadModule extends MenuModule { if(self.isBlindUpload()) { fileNameView.setText(blindFileNameText); - - // :TODO: when blind, fileNameView should not be focus/editable + fileNameView.acceptsFocus = false; + } else { + fileNameView.clearText(); + fileNameView.acceptsFocus = true; } - }); + }); + + // 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())); + } + }); self.uploadType = 'blind'; uploadTypeView.setFocusItemIndex(0); // default to blind diff --git a/package.json b/package.json index dac2e394..11a3cbc8 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "ptyw.js": "NuSkooler/ptyw.js", "sqlite3": "^3.1.1", "ssh2": "^0.5.1", - "temptmp" : "^1.0.0" + "temptmp" : "^1.0.0", + "sanitize-filename" : "^1.6.1" }, "devDependencies": { },