1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Merge remote-tracking branch 'upstream/dev' into fix-mqtt-keep-subscription-4132

This commit is contained in:
Steve-Mcl 2023-05-25 12:07:33 +01:00
commit 0528c12782
40 changed files with 467 additions and 214 deletions

View File

@ -44,8 +44,8 @@
"express": "4.18.2", "express": "4.18.2",
"express-session": "1.17.3", "express-session": "1.17.3",
"form-data": "4.0.0", "form-data": "4.0.0",
"fs-extra": "10.1.0", "fs-extra": "11.1.1",
"got": "11.8.6", "got": "12.6.0",
"hash-sum": "2.0.0", "hash-sum": "2.0.0",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"https-proxy-agent": "5.0.1", "https-proxy-agent": "5.0.1",
@ -60,7 +60,7 @@
"memorystore": "1.6.7", "memorystore": "1.6.7",
"mime": "3.0.0", "mime": "3.0.0",
"moment": "2.29.4", "moment": "2.29.4",
"moment-timezone": "0.5.41", "moment-timezone": "0.5.43",
"mqtt": "4.3.7", "mqtt": "4.3.7",
"multer": "1.4.5-lts.1", "multer": "1.4.5-lts.1",
"mustache": "4.2.0", "mustache": "4.2.0",
@ -73,13 +73,13 @@
"passport-http-bearer": "1.0.1", "passport-http-bearer": "1.0.1",
"passport-oauth2-client-password": "0.1.2", "passport-oauth2-client-password": "0.1.2",
"raw-body": "2.5.2", "raw-body": "2.5.2",
"semver": "7.3.8", "semver": "7.5.0",
"tar": "6.1.13", "tar": "6.1.13",
"tough-cookie": "4.1.2", "tough-cookie": "4.1.2",
"uglify-js": "3.17.4", "uglify-js": "3.17.4",
"uuid": "9.0.0", "uuid": "9.0.0",
"ws": "7.5.6", "ws": "7.5.6",
"xml2js": "0.4.23" "xml2js": "0.5.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"bcrypt": "5.1.0" "bcrypt": "5.1.0"
@ -108,14 +108,14 @@
"i18next-http-backend": "1.4.1", "i18next-http-backend": "1.4.1",
"jquery-i18next": "1.2.1", "jquery-i18next": "1.2.1",
"jsdoc-nr-template": "github:node-red/jsdoc-nr-template", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template",
"marked": "4.2.12", "marked": "4.3.0",
"mermaid": "^9.3.0", "mermaid": "^9.4.3",
"minami": "1.2.3", "minami": "1.2.3",
"mocha": "9.2.2", "mocha": "9.2.2",
"node-red-node-test-helper": "^0.3.0", "node-red-node-test-helper": "^0.3.1",
"nodemon": "2.0.20", "nodemon": "2.0.20",
"proxy": "^1.0.2", "proxy": "^1.0.2",
"sass": "1.58.3", "sass": "1.62.1",
"should": "13.2.3", "should": "13.2.3",
"sinon": "11.1.2", "sinon": "11.1.2",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",

View File

@ -14,8 +14,6 @@
* limitations under the License. * limitations under the License.
**/ **/
var express = require("express");
var nodes = require("./nodes"); var nodes = require("./nodes");
var flows = require("./flows"); var flows = require("./flows");
var flow = require("./flow"); var flow = require("./flow");
@ -37,18 +35,9 @@ module.exports = {
plugins.init(runtimeAPI); plugins.init(runtimeAPI);
diagnostics.init(settings, runtimeAPI); diagnostics.init(settings, runtimeAPI);
var needsPermission = auth.needsPermission; const needsPermission = auth.needsPermission;
var adminApp = express();
var defaultServerSettings = {
"x-powered-by": false
}
var serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
for (var eOption in serverSettings) {
adminApp.set(eOption, serverSettings[eOption]);
}
const adminApp = apiUtil.createExpressApp(settings)
// Flows // Flows
adminApp.get("/flows",needsPermission("flows.read"),flows.get,apiUtil.errorHandler); adminApp.get("/flows",needsPermission("flows.read"),flows.get,apiUtil.errorHandler);

View File

@ -46,14 +46,15 @@ module.exports = {
runtimeAPI = _runtimeAPI; runtimeAPI = _runtimeAPI;
needsPermission = auth.needsPermission; needsPermission = auth.needsPermission;
if (!settings.disableEditor) { if (!settings.disableEditor) {
info.init(runtimeAPI); info.init(settings, runtimeAPI);
comms.init(server,settings,runtimeAPI); comms.init(server,settings,runtimeAPI);
var ui = require("./ui"); var ui = require("./ui");
ui.init(runtimeAPI); ui.init(runtimeAPI);
var editorApp = express(); const editorApp = apiUtil.createExpressApp(settings)
if (settings.requireHttps === true) { if (settings.requireHttps === true) {
editorApp.enable('trust proxy'); editorApp.enable('trust proxy');
editorApp.use(function (req, res, next) { editorApp.use(function (req, res, next) {
@ -86,7 +87,7 @@ module.exports = {
//Projects //Projects
var projects = require("./projects"); var projects = require("./projects");
projects.init(runtimeAPI); projects.init(settings, runtimeAPI);
editorApp.use("/projects",projects.app()); editorApp.use("/projects",projects.app());
// Locales // Locales

View File

@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
**/ **/
var express = require("express");
var apiUtils = require("../util"); var apiUtils = require("../util");
var settings;
var runtimeAPI; var runtimeAPI;
var needsPermission = require("../auth").needsPermission; var needsPermission = require("../auth").needsPermission;
@ -77,11 +77,12 @@ function getProjectRemotes(req,res) {
}) })
} }
module.exports = { module.exports = {
init: function(_runtimeAPI) { init: function(_settings, _runtimeAPI) {
settings = _settings;
runtimeAPI = _runtimeAPI; runtimeAPI = _runtimeAPI;
}, },
app: function() { app: function() {
var app = express(); var app = apiUtils.createExpressApp(settings)
app.use(function(req,res,next) { app.use(function(req,res,next) {
runtimeAPI.projects.available().then(function(available) { runtimeAPI.projects.available().then(function(available) {

View File

@ -18,9 +18,9 @@ var runtimeAPI;
var sshkeys = require("./sshkeys"); var sshkeys = require("./sshkeys");
module.exports = { module.exports = {
init: function(_runtimeAPI) { init: function(settings, _runtimeAPI) {
runtimeAPI = _runtimeAPI; runtimeAPI = _runtimeAPI;
sshkeys.init(runtimeAPI); sshkeys.init(settings, runtimeAPI);
}, },
userSettings: function(req, res) { userSettings: function(req, res) {
var opts = { var opts = {

View File

@ -17,13 +17,15 @@
var apiUtils = require("../util"); var apiUtils = require("../util");
var express = require("express"); var express = require("express");
var runtimeAPI; var runtimeAPI;
var settings;
module.exports = { module.exports = {
init: function(_runtimeAPI) { init: function(_settings, _runtimeAPI) {
runtimeAPI = _runtimeAPI; runtimeAPI = _runtimeAPI;
settings = _settings;
}, },
app: function() { app: function() {
var app = express(); const app = apiUtils.createExpressApp(settings);
// List all SSH keys // List all SSH keys
app.get("/", function(req,res) { app.get("/", function(req,res) {

View File

@ -19,6 +19,7 @@ var util = require("util");
var path = require("path"); var path = require("path");
var fs = require("fs"); var fs = require("fs");
var clone = require("clone"); var clone = require("clone");
const apiUtil = require("../util")
var defaultContext = { var defaultContext = {
page: { page: {
@ -27,8 +28,7 @@ var defaultContext = {
tabicon: { tabicon: {
icon: "red/images/node-red-icon-black.svg", icon: "red/images/node-red-icon-black.svg",
colour: "#8f0000" colour: "#8f0000"
}, }
version: require(path.join(__dirname,"../../package.json")).version
}, },
header: { header: {
title: "Node-RED", title: "Node-RED",
@ -40,6 +40,7 @@ var defaultContext = {
vendorMonaco: "" vendorMonaco: ""
} }
}; };
var settings;
var theme = null; var theme = null;
var themeContext = clone(defaultContext); var themeContext = clone(defaultContext);
@ -92,7 +93,8 @@ function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) {
} }
module.exports = { module.exports = {
init: function(settings, _runtimeAPI) { init: function(_settings, _runtimeAPI) {
settings = _settings;
runtimeAPI = _runtimeAPI; runtimeAPI = _runtimeAPI;
themeContext = clone(defaultContext); themeContext = clone(defaultContext);
if (process.env.NODE_ENV == "development") { if (process.env.NODE_ENV == "development") {
@ -113,7 +115,15 @@ module.exports = {
var url; var url;
themeSettings = {}; themeSettings = {};
themeApp = express(); themeApp = apiUtil.createExpressApp(settings);
const defaultServerSettings = {
"x-powered-by": false
}
const serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
for (const eOption in serverSettings) {
themeApp.set(eOption, serverSettings[eOption]);
}
if (theme.page) { if (theme.page) {

View File

@ -37,7 +37,6 @@ var adminApp;
var server; var server;
var editor; var editor;
/** /**
* Initialise the module. * Initialise the module.
* @param {Object} settings The runtime settings * @param {Object} settings The runtime settings
@ -49,7 +48,7 @@ var editor;
function init(settings,_server,storage,runtimeAPI) { function init(settings,_server,storage,runtimeAPI) {
server = _server; server = _server;
if (settings.httpAdminRoot !== false) { if (settings.httpAdminRoot !== false) {
adminApp = express(); adminApp = apiUtil.createExpressApp(settings);
var cors = require('cors'); var cors = require('cors');
var corsHandler = cors({ var corsHandler = cors({
@ -64,14 +63,6 @@ function init(settings,_server,storage,runtimeAPI) {
} }
} }
var defaultServerSettings = {
"x-powered-by": false
}
var serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
for (var eOption in serverSettings) {
adminApp.set(eOption, serverSettings[eOption]);
}
auth.init(settings,storage); auth.init(settings,storage);
var maxApiRequestSize = settings.apiMaxLength || '5mb'; var maxApiRequestSize = settings.apiMaxLength || '5mb';
@ -136,10 +127,11 @@ async function stop() {
editor.stop(); editor.stop();
} }
} }
module.exports = { module.exports = {
init: init, init,
start: start, start,
stop: stop, stop,
/** /**
* @memberof @node-red/editor-api * @memberof @node-red/editor-api

View File

@ -14,10 +14,9 @@
* limitations under the License. * limitations under the License.
**/ **/
const express = require("express");
var log = require("@node-red/util").log; // TODO: separate module const { log, i18n } = require("@node-red/util");
var i18n = require("@node-red/util").i18n; // TODO: separate module
module.exports = { module.exports = {
errorHandler: function(err,req,res,next) { errorHandler: function(err,req,res,next) {
@ -64,5 +63,17 @@ module.exports = {
path: req.path, path: req.path,
ip: (req.headers && req.headers['x-forwarded-for']) || (req.connection && req.connection.remoteAddress) || undefined ip: (req.headers && req.headers['x-forwarded-for']) || (req.connection && req.connection.remoteAddress) || undefined
} }
},
createExpressApp: function(settings) {
const app = express();
const defaultServerSettings = {
"x-powered-by": false
}
const serverSettings = Object.assign({},defaultServerSettings,settings.httpServerOptions||{});
for (let eOption in serverSettings) {
app.set(eOption, serverSettings[eOption]);
}
return app
} }
} }

View File

@ -504,6 +504,7 @@
"unassigned": "未割当", "unassigned": "未割当",
"global": "グローバル", "global": "グローバル",
"workspace": "ワークスペース", "workspace": "ワークスペース",
"editor": "編集ダイアログ",
"selectAll": "全てのノードを選択", "selectAll": "全てのノードを選択",
"selectNone": "選択を外す", "selectNone": "選択を外す",
"selectAllConnected": "接続されたノードを選択", "selectAllConnected": "接続されたノードを選択",
@ -1203,7 +1204,7 @@
"fr": "フランス語", "fr": "フランス語",
"ja": "日本語", "ja": "日本語",
"ko": "韓国語", "ko": "韓国語",
"pt-BR":"ポルトガル語", "pt-BR": "ポルトガル語",
"ru": "ロシア語", "ru": "ロシア語",
"zh-CN": "中国語(簡体)", "zh-CN": "中国語(簡体)",
"zh-TW": "中国語(繁体)" "zh-TW": "中国語(繁体)"

View File

@ -263,6 +263,7 @@ var RED = (function() {
setTimeout(() => { setTimeout(() => {
RED.view.reveal(nodeToShow.id) RED.view.reveal(nodeToShow.id)
window.location.hash = currentHash window.location.hash = currentHash
RED.view.select(nodeToShow.id)
if (showEditDialog) { if (showEditDialog) {
RED.editor.edit(nodeToShow) RED.editor.edit(nodeToShow)
} }
@ -273,6 +274,7 @@ var RED = (function() {
if (nodeToShow) { if (nodeToShow) {
RED.view.reveal(nodeToShow.id) RED.view.reveal(nodeToShow.id)
window.location.hash = currentHash window.location.hash = currentHash
RED.view.select(nodeToShow.id)
if (showEditDialog) { if (showEditDialog) {
RED.editor.editGroup(nodeToShow) RED.editor.editGroup(nodeToShow)
} }

View File

@ -37,13 +37,13 @@ RED.clipboard = (function() {
// IE11 workaround // IE11 workaround
// IE does not support data uri scheme for downloading data // IE does not support data uri scheme for downloading data
var blob = new Blob([data], { var blob = new Blob([data], {
type: "data:text/plain;charset=utf-8" type: "data:application/json;charset=utf-8"
}); });
navigator.msSaveBlob(blob, file); navigator.msSaveBlob(blob, file);
} }
else { else {
var element = document.createElement('a'); var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(data)); element.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(data));
element.setAttribute('download', file); element.setAttribute('download', file);
element.style.display = 'none'; element.style.display = 'none';
document.body.appendChild(element); document.body.appendChild(element);

View File

@ -50,7 +50,11 @@ RED.envVar = (function() {
var new_env = []; var new_env = [];
var items = list.editableList('items'); var items = list.editableList('items');
var credentials = gconf ? gconf.credentials : null; var credentials = gconf ? gconf.credentials : null;
if (!gconf && list.editableList('length') === 0) {
// No existing global-config node and nothing in the list,
// so no need to do anything more
return
}
if (!credentials) { if (!credentials) {
credentials = { credentials = {
_ : {}, _ : {},
@ -78,6 +82,12 @@ RED.envVar = (function() {
if (gconf === null) { if (gconf === null) {
gconf = getGlobalConf(true); gconf = getGlobalConf(true);
} }
if (!gconf.credentials) {
gconf.credentials = {
_ : {},
map: {}
};
}
if ((JSON.stringify(new_env) !== JSON.stringify(gconf.env)) || if ((JSON.stringify(new_env) !== JSON.stringify(gconf.env)) ||
(JSON.stringify(credentials) !== JSON.stringify(gconf.credentials))) { (JSON.stringify(credentials) !== JSON.stringify(gconf.credentials))) {
gconf.env = new_env; gconf.env = new_env;

View File

@ -401,7 +401,7 @@ RED.group = (function() {
} }
} }
var existingGroup; var existingGroup;
var mergedEnv = {}
// Second pass, ungroup any groups in the selection and add their contents // Second pass, ungroup any groups in the selection and add their contents
// to the selection // to the selection
for (var i=0; i<selection.nodes.length; i++) { for (var i=0; i<selection.nodes.length; i++) {
@ -410,6 +410,11 @@ RED.group = (function() {
if (!existingGroup) { if (!existingGroup) {
existingGroup = n; existingGroup = n;
} }
if (n.env && n.env.length > 0) {
n.env.forEach(env => {
mergedEnv[env.name] = env
})
}
ungroupHistoryEvent.groups.push(n); ungroupHistoryEvent.groups.push(n);
nodes = nodes.concat(ungroup(n)); nodes = nodes.concat(ungroup(n));
} else { } else {
@ -427,6 +432,7 @@ RED.group = (function() {
group.style = existingGroup.style; group.style = existingGroup.style;
group.name = existingGroup.name; group.name = existingGroup.name;
} }
group.env = Object.values(mergedEnv)
RED.view.select({nodes:[group]}) RED.view.select({nodes:[group]})
} }
historyEvent.events.push({ historyEvent.events.push({

View File

@ -46,7 +46,9 @@ RED.subflow = (function() {
'</script>'; '</script>';
function findAvailableSubflowIOPosition(subflow,isInput) { function findAvailableSubflowIOPosition(subflow,isInput) {
var pos = {x:50,y:30}; const scrollPos = RED.view.scroll()
const scaleFactor = RED.view.scale()
var pos = { x: (scrollPos[0]/scaleFactor)+50, y: (scrollPos[1]/scaleFactor)+30 };
if (!isInput) { if (!isInput) {
pos.x += 110; pos.x += 110;
} }

View File

@ -232,6 +232,63 @@ RED.view = (function() {
return api return api
})(); })();
const selectedGroups = (function() {
let groups = new Set()
const api = {
add: function(g, includeNodes, addToMovingSet) {
groups.add(g)
if (!g.selected) {
g.selected = true;
g.dirty = true;
}
if (addToMovingSet !== false) {
movingSet.add(g);
}
if (includeNodes) {
var currentSet = new Set(movingSet.nodes());
var allNodes = RED.group.getNodes(g,true);
allNodes.forEach(function(n) {
if (!currentSet.has(n)) {
movingSet.add(n)
}
n.dirty = true;
})
}
selectedLinks.clearUnselected()
},
remove: function(g) {
groups.delete(g)
if (g.selected) {
g.selected = false;
g.dirty = true;
}
const allNodes = RED.group.getNodes(g,true);
const nodeSet = new Set(allNodes);
nodeSet.add(g);
for (let i = movingSet.length()-1; i >= 0; i -= 1) {
const msn = movingSet.get(i);
if (nodeSet.has(msn.n) || msn.n === g) {
msn.n.selected = false;
msn.n.dirty = true;
movingSet.remove(msn.n,i)
}
}
selectedLinks.clearUnselected()
},
length: () => groups.length,
forEach: (func) => { groups.forEach(func) },
toArray: () => [...groups],
clear: function () {
groups.forEach(g => {
g.selected = false
g.dirty = true
})
groups.clear()
}
}
return api
})()
function init() { function init() {
@ -1136,7 +1193,7 @@ RED.view = (function() {
var touchTrigger = options.touchTrigger; var touchTrigger = options.touchTrigger;
if (targetGroup) { if (targetGroup) {
selectGroup(targetGroup,false); selectedGroups.add(targetGroup,false);
RED.view.redraw(); RED.view.redraw();
} }
@ -1462,7 +1519,7 @@ RED.view = (function() {
clearSelection(); clearSelection();
nn.selected = true; nn.selected = true;
if (targetGroup) { if (targetGroup) {
selectGroup(targetGroup,false); selectedGroups.add(targetGroup,false);
} }
movingSet.add(nn); movingSet.add(nn);
updateActiveNodes(); updateActiveNodes();
@ -1926,7 +1983,7 @@ RED.view = (function() {
if (!movingSet.has(n) && !n.selected) { if (!movingSet.has(n) && !n.selected) {
// group entirely within lasso // group entirely within lasso
if (n.x > x && n.y > y && n.x + n.w < x2 && n.y + n.h < y2) { if (n.x > x && n.y > y && n.x + n.w < x2 && n.y + n.h < y2) {
selectGroup(n, true) selectedGroups.add(n, true)
} }
} }
}) })
@ -2276,7 +2333,7 @@ RED.view = (function() {
clearSelection(); clearSelection();
activeGroups.forEach(function(g) { activeGroups.forEach(function(g) {
if (!g.g) { if (!g.g) {
selectGroup(g, true); selectedGroups.add(g, true);
if (!g.selected) { if (!g.selected) {
g.selected = true; g.selected = true;
g.dirty = true; g.dirty = true;
@ -2346,10 +2403,7 @@ RED.view = (function() {
} }
movingSet.clear(); movingSet.clear();
selectedLinks.clear(); selectedLinks.clear();
activeGroups.forEach(function(g) { selectedGroups.clear();
g.selected = false;
g.dirty = true;
})
} }
var lastSelection = null; var lastSelection = null;
@ -2606,6 +2660,16 @@ RED.view = (function() {
var result = RED.nodes.removeJunction(node) var result = RED.nodes.removeJunction(node)
removedJunctions.push(node); removedJunctions.push(node);
removedLinks = removedLinks.concat(result.links); removedLinks = removedLinks.concat(result.links);
if (node.g) {
var group = RED.nodes.group(node.g);
if (selectedGroups.indexOf(group) === -1) {
// Don't use RED.group.removeFromGroup as that emits
// a change event on the node - but we're deleting it
var index = group.nodes.indexOf(node);
group.nodes.splice(index,1);
RED.group.markDirty(group);
}
}
} else { } else {
if (node.direction === "out") { if (node.direction === "out") {
removedSubflowOutputs.push(node); removedSubflowOutputs.push(node);
@ -3428,7 +3492,7 @@ RED.view = (function() {
if (!groupNodeSelectPrimed && !d.selected && d.g && RED.nodes.group(d.g).selected) { if (!groupNodeSelectPrimed && !d.selected && d.g && RED.nodes.group(d.g).selected) {
clearSelection(); clearSelection();
selectGroup(RED.nodes.group(d.g), false); selectedGroups.add(RED.nodes.group(d.g), false);
mousedown_node.selected = true; mousedown_node.selected = true;
movingSet.add(mousedown_node); movingSet.add(mousedown_node);
@ -3849,14 +3913,14 @@ RED.view = (function() {
lastClickNode = g; lastClickNode = g;
if (g.selected && (d3.event.ctrlKey||d3.event.metaKey)) { if (g.selected && (d3.event.ctrlKey||d3.event.metaKey)) {
deselectGroup(g); selectedGroups.remove(g);
d3.event.stopPropagation(); d3.event.stopPropagation();
} else { } else {
if (!g.selected) { if (!g.selected) {
if (!d3.event.ctrlKey && !d3.event.metaKey) { if (!d3.event.ctrlKey && !d3.event.metaKey) {
clearSelection(); clearSelection();
} }
selectGroup(g,true);//!wasSelected); selectedGroups.add(g,true);//!wasSelected);
} }
if (d3.event.button != 2) { if (d3.event.button != 2) {
@ -3872,45 +3936,6 @@ RED.view = (function() {
d3.event.stopPropagation(); d3.event.stopPropagation();
} }
function selectGroup(g, includeNodes, addToMovingSet) {
if (!g.selected) {
g.selected = true;
g.dirty = true;
}
if (addToMovingSet !== false) {
movingSet.add(g);
}
if (includeNodes) {
var currentSet = new Set(movingSet.nodes());
var allNodes = RED.group.getNodes(g,true);
allNodes.forEach(function(n) {
if (!currentSet.has(n)) {
movingSet.add(n)
}
n.dirty = true;
})
}
selectedLinks.clearUnselected()
}
function deselectGroup(g) {
if (g.selected) {
g.selected = false;
g.dirty = true;
}
const allNodes = RED.group.getNodes(g,true);
const nodeSet = new Set(allNodes);
nodeSet.add(g);
for (let i = movingSet.length()-1; i >= 0; i -= 1) {
const msn = movingSet.get(i);
if (nodeSet.has(msn.n) || msn.n === g) {
msn.n.selected = false;
msn.n.dirty = true;
movingSet.remove(msn.n,i)
}
}
selectedLinks.clearUnselected()
}
function getGroupAt(x, y, ignoreSelected) { function getGroupAt(x, y, ignoreSelected) {
// x,y expected to be in node-co-ordinate space // x,y expected to be in node-co-ordinate space
var candidateGroups = {}; var candidateGroups = {};
@ -4349,6 +4374,7 @@ RED.view = (function() {
this.__port__.setAttribute("transform","translate(-5,"+((d.h/2)-5)+")"); this.__port__.setAttribute("transform","translate(-5,"+((d.h/2)-5)+")");
this.__outputOutput__.setAttribute("transform","translate(20,"+((d.h/2)-8)+")"); this.__outputOutput__.setAttribute("transform","translate(20,"+((d.h/2)-8)+")");
this.__outputNumber__.setAttribute("transform","translate(20,"+((d.h/2)+7)+")"); this.__outputNumber__.setAttribute("transform","translate(20,"+((d.h/2)+7)+")");
this.__outputNumber__.textContent = d.i+1;
} }
d.dirty = false; d.dirty = false;
} }
@ -5891,11 +5917,10 @@ RED.view = (function() {
if (movingSet.length() > 0) { if (movingSet.length() > 0) {
movingSet.forEach(function(n) { movingSet.forEach(function(n) {
if (n.n.type !== 'group') { if (n.n.type !== 'group') {
allNodes.add(n.n); allNodes.add(n.n);
} }
}); });
} }
var selectedGroups = activeGroups.filter(function(g) { return g.selected });
selectedGroups.forEach(function(g) { selectedGroups.forEach(function(g) {
var groupNodes = RED.group.getNodes(g,true); var groupNodes = RED.group.getNodes(g,true);
groupNodes.forEach(function(n) { groupNodes.forEach(function(n) {
@ -6089,6 +6114,13 @@ RED.view = (function() {
selectedNode.dirty = true; selectedNode.dirty = true;
movingSet.clear(); movingSet.clear();
movingSet.add(selectedNode); movingSet.add(selectedNode);
} else {
selectedNode = RED.nodes.group(selection);
if (selectedNode) {
movingSet.clear();
selectedGroups.clear()
selectedGroups.add(selectedNode)
}
} }
} else if (selection) { } else if (selection) {
if (selection.nodes) { if (selection.nodes) {
@ -6104,7 +6136,7 @@ RED.view = (function() {
n.dirty = true; n.dirty = true;
movingSet.add(n); movingSet.add(n);
} else { } else {
selectGroup(n,true); selectedGroups.add(n,true);
} }
}) })
} }
@ -6272,8 +6304,12 @@ RED.view = (function() {
}) })
}, },
scroll: function(x,y) { scroll: function(x,y) {
chart.scrollLeft(chart.scrollLeft()+x); if (x !== undefined && y !== undefined) {
chart.scrollTop(chart.scrollTop()+y) chart.scrollLeft(chart.scrollLeft()+x);
chart.scrollTop(chart.scrollTop()+y)
} else {
return [chart.scrollLeft(), chart.scrollTop()]
}
}, },
clickNodeButton: function(n) { clickNodeButton: function(n) {
if (n._def.button) { if (n._def.button) {

View File

@ -4,6 +4,7 @@
<label style="width: auto" for="node-input-scope" data-i18n="catch.label.source"></label> <label style="width: auto" for="node-input-scope" data-i18n="catch.label.source"></label>
<select id="node-input-scope-select"> <select id="node-input-scope-select">
<option value="all" data-i18n="catch.scope.all"></option> <option value="all" data-i18n="catch.scope.all"></option>
<option value="group" data-i18n="catch.scope.group"></option>
<option value="target" data-i18n="catch.scope.selected"></option> <option value="target" data-i18n="catch.scope.selected"></option>
</select> </select>
</div> </div>
@ -170,6 +171,8 @@
}); });
if (this.scope === null) { if (this.scope === null) {
$("#node-input-scope-select").val("all"); $("#node-input-scope-select").val("all");
} else if(this.scope === "group"){
$("#node-input-scope-select").val("group");
} else { } else {
$("#node-input-scope-select").val("target"); $("#node-input-scope-select").val("target");
} }
@ -179,6 +182,8 @@
var scope = $("#node-input-scope-select").val(); var scope = $("#node-input-scope-select").val();
if (scope === 'all') { if (scope === 'all') {
this.scope = null; this.scope = null;
} else if(scope === 'group') {
this.scope = "group";
} else { } else {
$("#node-input-uncaught").prop("checked",false); $("#node-input-uncaught").prop("checked",false);
this.scope = $("#node-input-catch-target-container-div").treeList('selected').map(function(i) { return i.node.id}) this.scope = $("#node-input-catch-target-container-div").treeList('selected').map(function(i) { return i.node.id})

View File

@ -4,6 +4,7 @@
<label style="width: auto" for="node-input-scope" data-i18n="status.label.source"></label> <label style="width: auto" for="node-input-scope" data-i18n="status.label.source"></label>
<select id="node-input-scope-select"> <select id="node-input-scope-select">
<option value="all" data-i18n="status.scope.all"></option> <option value="all" data-i18n="status.scope.all"></option>
<option value="group" data-i18n="status.scope.group"></option>
<option value="target" data-i18n="status.scope.selected"></option> <option value="target" data-i18n="status.scope.selected"></option>
</select> </select>
</div> </div>
@ -157,6 +158,8 @@
}); });
if (this.scope === null) { if (this.scope === null) {
$("#node-input-scope-select").val("all"); $("#node-input-scope-select").val("all");
} else if(this.scope === "group"){
$("#node-input-scope-select").val("group");
} else { } else {
$("#node-input-scope-select").val("target"); $("#node-input-scope-select").val("target");
} }
@ -166,6 +169,8 @@
var scope = $("#node-input-scope-select").val(); var scope = $("#node-input-scope-select").val();
if (scope === 'all') { if (scope === 'all') {
this.scope = null; this.scope = null;
} else if(scope === 'group') {
this.scope = "group";
} else { } else {
this.scope = $("#node-input-status-target-container-div").treeList('selected').map(function(i) { return i.node.id}) this.scope = $("#node-input-status-target-container-div").treeList('selected').map(function(i) { return i.node.id})
} }

View File

@ -35,7 +35,11 @@ module.exports = function(RED) {
} }
else { node.previous = {}; } else { node.previous = {}; }
} }
var value = RED.util.getMessageProperty(msg,node.property); var value;
try {
value = RED.util.getMessageProperty(msg,node.property);
}
catch(e) { }
if (value !== undefined) { if (value !== undefined) {
var t = "_no_topic"; var t = "_no_topic";
if (node.septopics) { t = topic || t; } if (node.septopics) { t = topic || t; }

View File

@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
**/ **/
module.exports = function(RED) { module.exports = async function(RED) {
"use strict"; "use strict";
const got = require("got"); const { got } = await import('got')
const {CookieJar} = require("tough-cookie"); const {CookieJar} = require("tough-cookie");
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent'); const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
const FormData = require('form-data'); const FormData = require('form-data');
@ -210,24 +210,24 @@ in your Node-RED user directory (${RED.settings.userDir}).
// set defaultport, else when using HttpsProxyAgent, it's defaultPort of 443 will be used :(. // set defaultport, else when using HttpsProxyAgent, it's defaultPort of 443 will be used :(.
// Had to remove this to get http->https redirect to work // Had to remove this to get http->https redirect to work
// opts.defaultPort = isHttps?443:80; // opts.defaultPort = isHttps?443:80;
opts.timeout = node.reqTimeout; opts.timeout = { request: node.reqTimeout || 5000 };
opts.throwHttpErrors = false; opts.throwHttpErrors = false;
// TODO: add UI option to auto decompress. Setting to false for 1.x compatibility // TODO: add UI option to auto decompress. Setting to false for 1.x compatibility
opts.decompress = false; opts.decompress = false;
opts.method = method; opts.method = method;
opts.retry = 0; opts.retry = { limit: 0 };
opts.responseType = 'buffer'; opts.responseType = 'buffer';
opts.maxRedirects = 21; opts.maxRedirects = 21;
opts.cookieJar = new CookieJar(); opts.cookieJar = new CookieJar();
opts.ignoreInvalidCookies = true; opts.ignoreInvalidCookies = true;
opts.forever = nodeHTTPPersistent; // opts.forever = nodeHTTPPersistent;
if (msg.requestTimeout !== undefined) { if (msg.requestTimeout !== undefined) {
if (isNaN(msg.requestTimeout)) { if (isNaN(msg.requestTimeout)) {
node.warn(RED._("httpin.errors.timeout-isnan")); node.warn(RED._("httpin.errors.timeout-isnan"));
} else if (msg.requestTimeout < 1) { } else if (msg.requestTimeout < 1) {
node.warn(RED._("httpin.errors.timeout-isnegative")); node.warn(RED._("httpin.errors.timeout-isnegative"));
} else { } else {
opts.timeout = msg.requestTimeout; opts.timeout = { request: msg.requestTimeout };
} }
} }
const originalHeaderMap = {}; const originalHeaderMap = {};
@ -245,9 +245,12 @@ in your Node-RED user directory (${RED.settings.userDir}).
delete options.headers[h]; delete options.headers[h];
} }
}) })
if (node.insecureHTTPParser) { if (node.insecureHTTPParser) {
options.insecureHTTPParser = true // Setting the property under _unixOptions as pretty
// much the only hack available to get got to apply
// a core http option it doesn't think we should be
// allowed to set
options._unixOptions = { ...options.unixOptions, insecureHTTPParser: true }
} }
} }
], ],
@ -403,15 +406,16 @@ in your Node-RED user directory (${RED.settings.userDir}).
return response return response
} }
const requestUrl = new URL(response.request.requestUrl); const requestUrl = new URL(response.request.requestUrl);
const options = response.request.options; const options = { headers: {} }
const normalisedHeaders = {}; const normalisedHeaders = {};
Object.keys(response.headers).forEach(k => { Object.keys(response.headers).forEach(k => {
normalisedHeaders[k.toLowerCase()] = response.headers[k] normalisedHeaders[k.toLowerCase()] = response.headers[k]
}) })
if (normalisedHeaders['www-authenticate']) { if (normalisedHeaders['www-authenticate']) {
let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, options.method, requestUrl.pathname, normalisedHeaders['www-authenticate']) let authHeader = buildDigestHeader(digestCreds.user,digestCreds.password, response.request.options.method, requestUrl.pathname, normalisedHeaders['www-authenticate'])
options.headers.Authorization = authHeader; options.headers.Authorization = authHeader;
} }
// response.request.options.merge(options)
sentCreds = true; sentCreds = true;
return retry(options); return retry(options);
} }

View File

@ -33,7 +33,13 @@ module.exports = function(RED) {
parseString(value, options, function (err, result) { parseString(value, options, function (err, result) {
if (err) { done(err); } if (err) { done(err); }
else { else {
value = result; // TODO: With xml2js@0.5.0, they return an object with
// a null prototype. This could cause unexpected
// issues. So for now, we have to reconstruct
// the object with a proper prototype.
// Once https://github.com/Leonidas-from-XIV/node-xml2js/pull/674
// is merged, we can revisit and hopefully remove this hack
value = fixObj(result)
RED.util.setMessageProperty(msg,node.property,value); RED.util.setMessageProperty(msg,node.property,value);
send(msg); send(msg);
done(); done();
@ -46,4 +52,18 @@ module.exports = function(RED) {
}); });
} }
RED.nodes.registerType("xml",XMLNode); RED.nodes.registerType("xml",XMLNode);
function fixObj(obj) {
const res = {}
const keys = Object.keys(obj)
keys.forEach(k => {
if (typeof obj[k] === 'object' && obj[k]) {
res[k] = fixObj(obj[k])
} else {
res[k] = obj[k]
}
})
return res
}
} }

View File

@ -98,6 +98,7 @@
}, },
"scope": { "scope": {
"all": "allen Nodes", "all": "allen Nodes",
"group": "in gleicher Gruppe",
"selected": "ausgewählten Nodes" "selected": "ausgewählten Nodes"
} }
}, },
@ -110,6 +111,7 @@
}, },
"scope": { "scope": {
"all": "allen Nodes", "all": "allen Nodes",
"group": "in gleicher Gruppe",
"selected": "ausgewählten Nodes" "selected": "ausgewählten Nodes"
} }
}, },

View File

@ -103,6 +103,7 @@
}, },
"scope": { "scope": {
"all": "all nodes", "all": "all nodes",
"group": "in same group",
"selected": "selected nodes" "selected": "selected nodes"
} }
}, },
@ -115,6 +116,7 @@
}, },
"scope": { "scope": {
"all": "all nodes", "all": "all nodes",
"group": "in same group",
"selected": "selected nodes" "selected": "selected nodes"
} }
}, },

View File

@ -414,6 +414,7 @@
"port": "ポート", "port": "ポート",
"keepalive": "キープアライブ時間", "keepalive": "キープアライブ時間",
"cleansession": "セッションの初期化", "cleansession": "セッションの初期化",
"autoUnsubscribe": "切断時に購読を自動解除",
"cleanstart": "クリーンスタート", "cleanstart": "クリーンスタート",
"use-tls": "TLSを使用", "use-tls": "TLSを使用",
"tls-config": "TLS設定", "tls-config": "TLS設定",

View File

@ -27,8 +27,8 @@
"cronosjs": "1.7.1", "cronosjs": "1.7.1",
"denque": "2.1.0", "denque": "2.1.0",
"form-data": "4.0.0", "form-data": "4.0.0",
"fs-extra": "10.1.0", "fs-extra": "11.1.1",
"got": "11.8.6", "got": "12.6.0",
"hash-sum": "2.0.0", "hash-sum": "2.0.0",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"https-proxy-agent": "5.0.1", "https-proxy-agent": "5.0.1",
@ -44,7 +44,7 @@
"tough-cookie": "4.1.2", "tough-cookie": "4.1.2",
"uuid": "9.0.0", "uuid": "9.0.0",
"ws": "7.5.6", "ws": "7.5.6",
"xml2js": "0.4.23", "xml2js": "0.5.0",
"iconv-lite": "0.6.3" "iconv-lite": "0.6.3"
} }
} }

View File

@ -242,63 +242,68 @@ async function ensureModuleDir() {
} }
} }
let installLock = Promise.resolve()
async function installModule(moduleDetails) { async function installModule(moduleDetails) {
let installSpec = moduleDetails.module; const result = installLock.then(async () => {
if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) { let installSpec = moduleDetails.module;
const e = new Error("Install not allowed"); if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) {
e.code = "install_not_allowed"; const e = new Error("Install not allowed");
throw e; e.code = "install_not_allowed";
}
if (moduleDetails.version) {
installSpec = installSpec+"@"+moduleDetails.version;
}
log.info(log._("server.install.installing",{name: moduleDetails.module,version: moduleDetails.version||"latest"}));
const installDir = getInstallDir();
await ensureModuleDir();
let triggerPayload = {
"module": moduleDetails.module,
"version": moduleDetails.version,
"dir": installDir,
"args": ["--production","--engine-strict"]
}
return hooks.trigger("preInstall", triggerPayload).then((result) => {
// preInstall passed
// - run install
if (result !== false) {
let extraArgs = triggerPayload.args || [];
let args = ['install', ...extraArgs, installSpec]
log.trace(NPM_COMMAND + JSON.stringify(args));
return exec.run(NPM_COMMAND, args, { cwd: installDir },true)
} else {
log.trace("skipping npm install");
}
}).then(() => {
return hooks.trigger("postInstall", triggerPayload)
}).then(() => {
log.info(log._("server.install.installed", { name: installSpec }));
const runtimeInstalledModules = settings.get("modules") || {};
runtimeInstalledModules[moduleDetails.module] = moduleDetails;
settings.set("modules",runtimeInstalledModules)
}).catch(result => {
var output = result.stderr || result.toString();
var e;
if (/E404/.test(output) || /ETARGET/.test(output)) {
log.error(log._("server.install.install-failed-not-found",{name:installSpec}));
e = new Error("Module not found");
e.code = 404;
throw e;
} else {
log.error(log._("server.install.install-failed-long",{name:installSpec}));
log.error("------------------------------------------");
log.error(output);
log.error("------------------------------------------");
e = new Error(log._("server.install.install-failed"));
e.code = "unexpected_error";
throw e; throw e;
} }
if (moduleDetails.version) {
installSpec = installSpec+"@"+moduleDetails.version;
}
log.info(log._("server.install.installing",{name: moduleDetails.module,version: moduleDetails.version||"latest"}));
const installDir = getInstallDir();
await ensureModuleDir();
let triggerPayload = {
"module": moduleDetails.module,
"version": moduleDetails.version,
"dir": installDir,
"args": ["--production","--engine-strict"]
}
return hooks.trigger("preInstall", triggerPayload).then((result) => {
// preInstall passed
// - run install
if (result !== false) {
let extraArgs = triggerPayload.args || [];
let args = ['install', ...extraArgs, installSpec]
log.trace(NPM_COMMAND + JSON.stringify(args));
return exec.run(NPM_COMMAND, args, { cwd: installDir },true)
} else {
log.trace("skipping npm install");
}
}).then(() => {
return hooks.trigger("postInstall", triggerPayload)
}).then(() => {
log.info(log._("server.install.installed", { name: installSpec }));
const runtimeInstalledModules = settings.get("modules") || {};
runtimeInstalledModules[moduleDetails.module] = moduleDetails;
settings.set("modules",runtimeInstalledModules)
}).catch(result => {
var output = result.stderr || result.toString();
var e;
if (/E404/.test(output) || /ETARGET/.test(output)) {
log.error(log._("server.install.install-failed-not-found",{name:installSpec}));
e = new Error("Module not found");
e.code = 404;
throw e;
} else {
log.error(log._("server.install.install-failed-long",{name:installSpec}));
log.error("------------------------------------------");
log.error(output);
log.error("------------------------------------------");
e = new Error(log._("server.install.install-failed"));
e.code = "unexpected_error";
throw e;
}
})
}) })
installLock = result.catch(() => {})
return result
} }
module.exports = { module.exports = {

View File

@ -18,8 +18,8 @@
"dependencies": { "dependencies": {
"@node-red/util": "3.1.0-beta.2", "@node-red/util": "3.1.0-beta.2",
"clone": "2.1.2", "clone": "2.1.2",
"fs-extra": "10.1.0", "fs-extra": "11.1.1",
"semver": "7.3.8", "semver": "7.5.0",
"tar": "6.1.13", "tar": "6.1.13",
"uglify-js": "3.17.4" "uglify-js": "3.17.4"
} }

View File

@ -606,10 +606,36 @@ class Flow {
} }
handled = true; handled = true;
} else { } else {
this.statusNodes.forEach(function(targetStatusNode) { const candidateNodes = [];
if (targetStatusNode.scope && targetStatusNode.scope.indexOf(reportingNode.id) === -1) { this.statusNodes.forEach(targetStatusNode => {
if (targetStatusNode.g && targetStatusNode.scope === 'group' && !reportingNode.g) {
// Status node inside a group, reporting node not in a group - skip it
return
}
if (Array.isArray(targetStatusNode.scope) && targetStatusNode.scope.indexOf(reportingNode.id) === -1) {
return; return;
} }
let distance = 0
if (reportingNode.g) {
// Reporting node inside a group. Calculate the distance between it and the status node
let containingGroup = this.global.groups[reportingNode.g]
while (containingGroup && containingGroup.id !== targetStatusNode.g) {
distance++
containingGroup = this.global.groups[containingGroup.g]
}
if (!containingGroup && targetStatusNode.g && targetStatusNode.scope === 'group') {
// This status node is in a group, but not in the same hierachy
// the reporting node is in
return
}
}
candidateNodes.push({ d: distance, n: targetStatusNode })
})
candidateNodes.sort((A,B) => {
return A.d - B.d
})
candidateNodes.forEach(candidate => {
const targetStatusNode = candidate.n
var message = { var message = {
status: clone(statusMessage) status: clone(statusMessage)
} }
@ -667,21 +693,46 @@ class Flow {
} }
handled = true; handled = true;
} else { } else {
var handledByUncaught = false; const candidateNodes = [];
this.catchNodes.forEach(targetCatchNode => {
this.catchNodes.forEach(function(targetCatchNode) { if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) {
if (targetCatchNode.scope && targetCatchNode.scope.indexOf(reportingNode.id) === -1) { // Catch node inside a group, reporting node not in a group - skip it
return
}
if (Array.isArray(targetCatchNode.scope) && targetCatchNode.scope.indexOf(reportingNode.id) === -1) {
// Catch node has a scope set and it doesn't include the reporting node
return; return;
} }
if (!targetCatchNode.scope && targetCatchNode.uncaught && !handledByUncaught) { let distance = 0
if (handled) { if (reportingNode.g) {
// This has been handled by a !uncaught catch node // Reporting node inside a group. Calculate the distance between it and the catch node
return; let containingGroup = this.global.groups[reportingNode.g]
while (containingGroup && containingGroup.id !== targetCatchNode.g) {
distance++
containingGroup = this.global.groups[containingGroup.g]
}
if (!containingGroup && targetCatchNode.g && targetCatchNode.scope === 'group') {
// This catch node is in a group, but not in the same hierachy
// the reporting node is in
return
} }
// This is an uncaught error
handledByUncaught = true;
} }
var errorMessage; candidateNodes.push({ d: distance, n: targetCatchNode })
})
candidateNodes.sort((A,B) => {
return A.d - B.d
})
let handledByUncaught = false
candidateNodes.forEach(candidate => {
const targetCatchNode = candidate.n
if (targetCatchNode.uncaught && !handledByUncaught) {
// This node only wants errors that haven't already been handled
if (handled) {
return
}
handledByUncaught = true
}
let errorMessage;
if (msg) { if (msg) {
errorMessage = redUtil.cloneMessage(msg); errorMessage = redUtil.cloneMessage(msg);
} else { } else {

View File

@ -89,6 +89,15 @@ function init(userSettings,httpServer,_adminApi) {
nodeApp = express(); nodeApp = express();
adminApp = express(); adminApp = express();
const defaultServerSettings = {
"x-powered-by": false
}
const serverSettings = Object.assign({},defaultServerSettings,userSettings.httpServerOptions||{});
for (let eOption in serverSettings) {
nodeApp.set(eOption, serverSettings[eOption]);
adminApp.set(eOption, serverSettings[eOption]);
}
if (_adminApi) { if (_adminApi) {
adminApi = _adminApi; adminApi = _adminApi;

View File

@ -21,7 +21,7 @@
"async-mutex": "0.4.0", "async-mutex": "0.4.0",
"clone": "2.1.2", "clone": "2.1.2",
"express": "4.18.2", "express": "4.18.2",
"fs-extra": "10.1.0", "fs-extra": "11.1.1",
"json-stringify-safe": "5.0.1" "json-stringify-safe": "5.0.1"
} }
} }

View File

@ -15,12 +15,12 @@
} }
], ],
"dependencies": { "dependencies": {
"fs-extra": "10.1.0", "fs-extra": "11.1.1",
"i18next": "21.10.0", "i18next": "21.10.0",
"json-stringify-safe": "5.0.1", "json-stringify-safe": "5.0.1",
"jsonata": "1.8.6", "jsonata": "1.8.6",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"moment": "2.29.4", "moment": "2.29.4",
"moment-timezone": "0.5.41" "moment-timezone": "0.5.43"
} }
} }

View File

@ -38,10 +38,10 @@
"basic-auth": "2.0.1", "basic-auth": "2.0.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"express": "4.18.2", "express": "4.18.2",
"fs-extra": "10.1.0", "fs-extra": "11.1.1",
"node-red-admin": "^3.0.0", "node-red-admin": "^3.0.0",
"nopt": "5.0.0", "nopt": "5.0.0",
"semver": "7.3.8" "semver": "7.5.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"bcrypt": "5.1.0" "bcrypt": "5.1.0"

View File

@ -148,7 +148,7 @@ try {
} else { } else {
console.log(err); console.log(err);
} }
process.exit(); process.exit(1);
} }
if (parsedArgs.define) { if (parsedArgs.define) {
@ -182,7 +182,7 @@ if (parsedArgs.define) {
} }
} catch (e) { } catch (e) {
console.log("Error processing -D option: "+e.message); console.log("Error processing -D option: "+e.message);
process.exit(); process.exit(1);
} }
} }
@ -316,10 +316,10 @@ httpsPromise.then(function(startupHttps) {
} else { } else {
continue; continue;
} }
sp.subRoot = formatRoot(sp.root); sp.subRoot = formatRoot(sp.root || "/");
sp.root = formatRoot(path.posix.join(settings.httpStaticRoot,sp.subRoot)); sp.root = formatRoot(path.posix.join(settings.httpStaticRoot,sp.subRoot));
} }
settings.httpStatic = sanitised.length ? sanitised : false; settings.httpStatic = sanitised.length ? sanitised : false;
} }
// if we got a port from command line, use it (even if 0) // if we got a port from command line, use it (even if 0)
@ -427,6 +427,7 @@ httpsPromise.then(function(startupHttps) {
const sp = settings.httpStatic[si]; const sp = settings.httpStatic[si];
const filePath = sp.path; const filePath = sp.path;
const thisRoot = sp.root || "/"; const thisRoot = sp.root || "/";
const options = sp.options;
if(appUseMem[filePath + "::" + thisRoot]) { if(appUseMem[filePath + "::" + thisRoot]) {
continue;// this path and root already registered! continue;// this path and root already registered!
} }
@ -434,7 +435,7 @@ httpsPromise.then(function(startupHttps) {
if (settings.httpStaticAuth) { if (settings.httpStaticAuth) {
app.use(thisRoot, basicAuthMiddleware(settings.httpStaticAuth.user, settings.httpStaticAuth.pass)); app.use(thisRoot, basicAuthMiddleware(settings.httpStaticAuth.user, settings.httpStaticAuth.pass));
} }
app.use(thisRoot, express.static(filePath)); app.use(thisRoot, express.static(filePath, options));
} }
} }

View File

@ -223,10 +223,15 @@ module.exports = {
* to move httpAdminRoot * to move httpAdminRoot
*/ */
//httpStatic: '/home/nol/node-red-static/', //single static source //httpStatic: '/home/nol/node-red-static/', //single static source
/* OR multiple static sources can be created using an array of objects... */ /**
* OR multiple static sources can be created using an array of objects...
* Each object can also contain an options object for further configuration.
* See https://expressjs.com/en/api.html#express.static for available options.
*/
//httpStatic: [ //httpStatic: [
// {path: '/home/nol/pics/', root: "/img/"}, // {path: '/home/nol/pics/', root: "/img/"},
// {path: '/home/nol/reports/', root: "/doc/"}, // {path: '/home/nol/reports/', root: "/doc/"},
// {path: '/home/nol/videos/', root: "/vid/", options: {maxAge: '1d'}}
//], //],
/** /**
@ -431,7 +436,7 @@ module.exports = {
enabled: true enabled: true
} }
}, },
}, },
/******************************************************************************* /*******************************************************************************

View File

@ -854,7 +854,7 @@ describe('inject node', function() {
}); });
n1.on("call:error", function(err) { n1.on("call:error", function(err) {
count++; count++;
if (count == 2) { if (count == 1) {
done(); done();
} }
}); });

View File

@ -223,7 +223,7 @@ describe('HTTP Request Node', function() {
} }
authFields[match[1]] = match[2] || match[3]; authFields[match[1]] = match[2] || match[3];
} }
console.log(JSON.stringify(authFields)); // console.log(JSON.stringify(authFields));
if (qop && authFields['qop'] != qop) { if (qop && authFields['qop'] != qop) {
console.log('test1'); console.log('test1');
@ -250,7 +250,7 @@ describe('HTTP Request Node', function() {
req, algorithm, sess, realm, username, nonce, nc, cnonce, qop req, algorithm, sess, realm, username, nonce, nc, cnonce, qop
); );
if (!response || expectedResponse.toLowerCase() !== response.toLowerCase()) { if (!response || expectedResponse.toLowerCase() !== response.toLowerCase()) {
console.log('test3'); console.log('test3', response, expectedResponse);
res.status(401).end(); res.status(401).end();
return; return;
} }

View File

@ -61,12 +61,14 @@ describe("api/editor/index", function() {
sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/"+m),"init").callsFake(function(){}); sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/"+m),"init").callsFake(function(){});
}); });
sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme"),"app").callsFake(function(){ return express()}); sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme"),"app").callsFake(function(){ return express()});
sinon.stub(NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/settings"),"sshkeys").callsFake(function(){ return express()});
}); });
after(function() { after(function() {
mockList.forEach(function(m) { mockList.forEach(function(m) {
NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/"+m).init.restore(); NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/"+m).init.restore();
}) })
NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme").app.restore(); NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/theme").app.restore();
NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/settings").sshkeys.restore();
auth.needsPermission.restore(); auth.needsPermission.restore();
log.error.restore(); log.error.restore();
}); });

View File

@ -41,7 +41,7 @@ describe("api/editor/settings", function() {
}); });
it('returns the user settings', function(done) { it('returns the user settings', function(done) {
info.init({ info.init({}, {
settings: { settings: {
getUserSettings: function(opts) { getUserSettings: function(opts) {
if (opts.user !== "fred") { if (opts.user !== "fred") {
@ -67,7 +67,7 @@ describe("api/editor/settings", function() {
}); });
it('updates the user settings', function(done) { it('updates the user settings', function(done) {
var update; var update;
info.init({ info.init({}, {
settings: { settings: {
updateUserSettings: function(opts) { updateUserSettings: function(opts) {
if (opts.user !== "fred") { if (opts.user !== "fred") {

View File

@ -34,7 +34,7 @@ describe("api/editor/sshkeys", function() {
} }
} }
before(function() { before(function() {
sshkeys.init(mockRuntime); sshkeys.init({}, mockRuntime);
app = express(); app = express();
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use("/settings/user/keys", sshkeys.app()); app.use("/settings/user/keys", sshkeys.app());

View File

@ -686,6 +686,44 @@ describe('Flow', function() {
},50); },50);
}); });
it.only("passes a status event to the group scoped status node",function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id: "g1", type: "group", g: "g3" },
{id: "g2", type: "group" },
{id: "g3", type: "group" },
{id:"1",x:10,y:10,z:"t1",g:"g1", type:"test",name:"a",wires:["2"]},
// sn - in the same group as source node
{id:"sn",x:10,y:10,z:"t1",g:"g1", type:"status",scope:"group",wires:[]},
// sn2 - in a different group hierarchy to the source node
{id:"sn2",x:10,y:10,z:"t1", g:"g2", type:"status",scope:"group",wires:[]},
// sn3 - in a higher-level group to the source node
{id:"sn3",x:10,y:10,z:"t1", g:"g3", type:"status",scope:"group",wires:[]},
// sn2 - in a different group hierarchy, but not scope to the group
{id:"sn4",x:10,y:10,z:"t1", g:"g2", type:"status",wires:[]},
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
flow.handleStatus(config.flows["t1"].nodes["1"],{text:"my-status"});
setTimeout(function() {
try {
currentNodes["sn"].should.have.a.property("handled",1);
currentNodes["sn2"].should.have.a.property("handled",0);
currentNodes["sn3"].should.have.a.property("handled",1);
currentNodes["sn3"].should.have.a.property("handled",1);
done()
} catch(err) {
done(err)
}
},50);
});
}); });
describe("#handleError",function() { describe("#handleError",function() {
@ -796,6 +834,42 @@ describe('Flow', function() {
},50); },50);
},50); },50);
}); });
it("passes an error event to the group scoped catch node",function(done) {
var config = flowUtils.parseConfig([
{id:"t1",type:"tab"},
{id: "g1", type: "group", g: "g3" },
{id: "g2", type: "group" },
{id: "g3", type: "group" },
{id:"1",x:10,y:10,z:"t1",g:"g1", type:"test",name:"a",wires:["2"]},
// sn - in the same group as source node
{id:"sn",x:10,y:10,z:"t1",g:"g1", type:"catch",scope:"group",wires:[]},
// sn2 - in a different group hierarchy to the source node
{id:"sn2",x:10,y:10,z:"t1", g:"g2", type:"catch",scope:"group",wires:[]},
// sn3 - in a higher-level group to the source node
{id:"sn3",x:10,y:10,z:"t1", g:"g3", type:"catch",scope:"group",wires:[]},
// sn2 - in a different group hierarchy, but not scope to the group
{id:"sn4",x:10,y:10,z:"t1", g:"g2", type:"catch",wires:[]},
]);
var flow = Flow.create({},config,config.flows["t1"]);
flow.start();
var activeNodes = flow.getActiveNodes();
flow.handleError(config.flows["t1"].nodes["1"],"my-error",{a:"foo"});
setTimeout(function() {
try {
currentNodes["sn"].should.have.a.property("handled",1);
currentNodes["sn2"].should.have.a.property("handled",0);
currentNodes["sn3"].should.have.a.property("handled",1);
currentNodes["sn3"].should.have.a.property("handled",1);
done()
} catch(err) {
done(err)
}
},50);
});
it("moves any existing error object sideways",function(done){ it("moves any existing error object sideways",function(done){
var config = flowUtils.parseConfig([ var config = flowUtils.parseConfig([
{id:"t1",type:"tab"}, {id:"t1",type:"tab"},