From d7a10328c047b2d9bad9733c79f1c5d536fe7fdf Mon Sep 17 00:00:00 2001 From: NetHans Date: Sun, 14 Aug 2022 20:20:59 +0200 Subject: [PATCH 01/30] function for group analysis added --- .../@node-red/runtime/lib/flows/Flow.js | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index b5685d3ec..6350755c5 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -128,6 +128,65 @@ class Flow { } this.parent.log(msg); } + + /** + * Checks if node A and node B are in the same group. + * Node B can also be placed in a subgroup. + * If node A is not in any group, false is returned + * @param {[type]} nodeIdA [description] + * @param {[type]} nodeIdB [description] + * @returns {[type]} Returns true if all nodes are in the same group. If not, then false or if node A is not in a group then also false. + */ + isNodeInSameGroup(nodeIdA, nodeIdB) { + const groups = this.global.groups; + let result = false; + for(let key in groups) { + let group = groups[key]; + + if(!group.nodes.includes(nodeIdA.id)) { + continue; + } + + if(group.nodes.includes(nodeIdB.id)) { + result = true; + break; + } + + /** + * Subfunction to recursively search the groups for matches + * @param {[type]} targetNode [description] + * @param {[type]} targetGroup [description] + * @returns Returns true if a match was found. Otherwise false. + */ + const isInSubGroup = (targetNode, targetGroup) => { + let _result = false; + if(targetGroup.nodes.includes(targetNode.id)) { + _result = true; + } else { + for(let nodeId of targetGroup.nodes) { + let node = this.getGroupNode(nodeId); + + if(!node){ + continue; + } + + if(node.type === "group"){ + let result = isInSubGroup(targetNode, node); + if(result === true){ + _result = true; + break; + } + } + } + } + + return _result; + }; + + result = isInSubGroup(nodeIdB, group); + } + return result; + } /** * Start this flow. From 08ce6cce97f81a3db9d0ffe1c5f3c9297646ce07 Mon Sep 17 00:00:00 2001 From: NetHans Date: Sun, 14 Aug 2022 20:24:05 +0200 Subject: [PATCH 02/30] status node extended --- .../@node-red/nodes/core/common/25-status.html | 5 +++++ .../node_modules/@node-red/nodes/locales/de/messages.json | 1 + .../@node-red/nodes/locales/en-US/messages.json | 1 + packages/node_modules/@node-red/runtime/lib/flows/Flow.js | 8 +++++++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/common/25-status.html b/packages/node_modules/@node-red/nodes/core/common/25-status.html index 47a3192e4..4c1b9c313 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-status.html +++ b/packages/node_modules/@node-red/nodes/core/common/25-status.html @@ -4,6 +4,7 @@ @@ -157,6 +158,8 @@ }); if (this.scope === null) { $("#node-input-scope-select").val("all"); + } else if(this.scope === "group"){ + $("#node-input-scope-select").val("group"); } else { $("#node-input-scope-select").val("target"); } @@ -166,6 +169,8 @@ var scope = $("#node-input-scope-select").val(); if (scope === 'all') { this.scope = null; + } else if(scope === 'group') { + this.scope = "group"; } else { this.scope = $("#node-input-status-target-container-div").treeList('selected').map(function(i) { return i.node.id}) } diff --git a/packages/node_modules/@node-red/nodes/locales/de/messages.json b/packages/node_modules/@node-red/nodes/locales/de/messages.json index 65f251e98..2ae7ba3b8 100755 --- a/packages/node_modules/@node-red/nodes/locales/de/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/de/messages.json @@ -110,6 +110,7 @@ }, "scope": { "all": "allen Nodes", + "group": "in gleicher Gruppe", "selected": "ausgewählten Nodes" } }, diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index 62d5f351f..8318693b0 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -115,6 +115,7 @@ }, "scope": { "all": "all nodes", + "group": "in same group", "selected": "selected nodes" } }, diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 6350755c5..56c98be0e 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -665,10 +665,16 @@ class Flow { } handled = true; } else { + const flow = this; this.statusNodes.forEach(function(targetStatusNode) { - if (targetStatusNode.scope && targetStatusNode.scope.indexOf(reportingNode.id) === -1) { + if (Array.isArray(targetStatusNode.scope) && targetStatusNode.scope.indexOf(reportingNode.id) === -1) { return; } + + if (targetStatusNode.scope === "group" && flow.isNodeInSameGroup(targetStatusNode, reportingNode) === false){ + return; + } + var message = { status: clone(statusMessage) } From 371253a4f6fff9b11a83fcba0c323ac92e82f641 Mon Sep 17 00:00:00 2001 From: NetHans Date: Sun, 14 Aug 2022 20:28:34 +0200 Subject: [PATCH 03/30] catch node extended --- .../@node-red/nodes/core/common/25-catch.html | 5 +++++ .../@node-red/nodes/locales/de/messages.json | 1 + .../@node-red/nodes/locales/en-US/messages.json | 1 + .../node_modules/@node-red/runtime/lib/flows/Flow.js | 10 ++++++++-- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/nodes/core/common/25-catch.html b/packages/node_modules/@node-red/nodes/core/common/25-catch.html index 0b976ea78..4c591226d 100644 --- a/packages/node_modules/@node-red/nodes/core/common/25-catch.html +++ b/packages/node_modules/@node-red/nodes/core/common/25-catch.html @@ -4,6 +4,7 @@ @@ -170,6 +171,8 @@ }); if (this.scope === null) { $("#node-input-scope-select").val("all"); + } else if(this.scope === "group"){ + $("#node-input-scope-select").val("group"); } else { $("#node-input-scope-select").val("target"); } @@ -179,6 +182,8 @@ var scope = $("#node-input-scope-select").val(); if (scope === 'all') { this.scope = null; + } else if(scope === 'group') { + this.scope = "group"; } else { $("#node-input-uncaught").prop("checked",false); this.scope = $("#node-input-catch-target-container-div").treeList('selected').map(function(i) { return i.node.id}) diff --git a/packages/node_modules/@node-red/nodes/locales/de/messages.json b/packages/node_modules/@node-red/nodes/locales/de/messages.json index 2ae7ba3b8..edc881001 100755 --- a/packages/node_modules/@node-red/nodes/locales/de/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/de/messages.json @@ -98,6 +98,7 @@ }, "scope": { "all": "allen Nodes", + "group": "in gleicher Gruppe", "selected": "ausgewählten Nodes" } }, diff --git a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json index 8318693b0..b600bda91 100644 --- a/packages/node_modules/@node-red/nodes/locales/en-US/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/en-US/messages.json @@ -103,6 +103,7 @@ }, "scope": { "all": "all nodes", + "group": "in same group", "selected": "selected nodes" } }, diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 56c98be0e..7df148b7f 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -734,11 +734,17 @@ class Flow { } else { var handledByUncaught = false; + const flow = this; this.catchNodes.forEach(function(targetCatchNode) { - if (targetCatchNode.scope && targetCatchNode.scope.indexOf(reportingNode.id) === -1) { + if (Array.isArray(targetCatchNode.scope) && targetCatchNode.scope.indexOf(reportingNode.id) === -1) { return; } - if (!targetCatchNode.scope && targetCatchNode.uncaught && !handledByUncaught) { + + if (targetCatchNode.scope === "group" && flow.isNodeInSameGroup(targetCatchNode, reportingNode) === false){ + return; + } + + if ((!targetCatchNode.scope || targetCatchNode.scope === "group") && targetCatchNode.uncaught && !handledByUncaught) { if (handled) { // This has been handled by a !uncaught catch node return; From b7a016edcfde73de6d309edbd50309e948143cdd Mon Sep 17 00:00:00 2001 From: NetHans Date: Tue, 16 Aug 2022 06:37:03 +0200 Subject: [PATCH 04/30] Update Flow.js replace tabs with whitespace --- .../@node-red/runtime/lib/flows/Flow.js | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 7df148b7f..4f87854ea 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -137,32 +137,32 @@ class Flow { * @param {[type]} nodeIdB [description] * @returns {[type]} Returns true if all nodes are in the same group. If not, then false or if node A is not in a group then also false. */ - isNodeInSameGroup(nodeIdA, nodeIdB) { - const groups = this.global.groups; + isNodeInSameGroup(nodeIdA, nodeIdB) { + const groups = this.global.groups; let result = false; - for(let key in groups) { - let group = groups[key]; + for(let key in groups) { + let group = groups[key]; - if(!group.nodes.includes(nodeIdA.id)) { - continue; - } + if(!group.nodes.includes(nodeIdA.id)) { + continue; + } - if(group.nodes.includes(nodeIdB.id)) { - result = true; + if(group.nodes.includes(nodeIdB.id)) { + result = true; break; - } - + } + /** * Subfunction to recursively search the groups for matches * @param {[type]} targetNode [description] * @param {[type]} targetGroup [description] * @returns Returns true if a match was found. Otherwise false. */ - const isInSubGroup = (targetNode, targetGroup) => { + const isInSubGroup = (targetNode, targetGroup) => { let _result = false; - if(targetGroup.nodes.includes(targetNode.id)) { - _result = true; - } else { + if(targetGroup.nodes.includes(targetNode.id)) { + _result = true; + } else { for(let nodeId of targetGroup.nodes) { let node = this.getGroupNode(nodeId); @@ -180,13 +180,13 @@ class Flow { } } - return _result; - }; - - result = isInSubGroup(nodeIdB, group); - } - return result; - } + return _result; + }; + + result = isInSubGroup(nodeIdB, group); + } + return result; + } /** * Start this flow. From e147602a3a587956cf8c42481d3e6a8c4e586223 Mon Sep 17 00:00:00 2001 From: NetHans Date: Thu, 18 Aug 2022 21:27:59 +0200 Subject: [PATCH 05/30] JSdoc documentation fixed --- .../node_modules/@node-red/runtime/lib/flows/Flow.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 4f87854ea..2616b8878 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -133,9 +133,9 @@ class Flow { * Checks if node A and node B are in the same group. * Node B can also be placed in a subgroup. * If node A is not in any group, false is returned - * @param {[type]} nodeIdA [description] - * @param {[type]} nodeIdB [description] - * @returns {[type]} Returns true if all nodes are in the same group. If not, then false or if node A is not in a group then also false. + * @param {Node} nodeIdA Node which defines the first search level + * @param {Node} nodeIdB Node which is to be searched in the group or a subgroup + * @returns {boolean} Returns true if all nodes are in the same group. If not, then false or if node A is not in a group then also false. */ isNodeInSameGroup(nodeIdA, nodeIdB) { const groups = this.global.groups; @@ -154,9 +154,9 @@ class Flow { /** * Subfunction to recursively search the groups for matches - * @param {[type]} targetNode [description] - * @param {[type]} targetGroup [description] - * @returns Returns true if a match was found. Otherwise false. + * @param {Node} Node which is to be searched in the group or a subgroup + * @param {Group} targetGroup group currently under analysis + * @returns {boolean} Returns true if a match was found. Otherwise false. */ const isInSubGroup = (targetNode, targetGroup) => { let _result = false; From 889489e33ecd70400c3a3b3b970d2ae6666d3c3f Mon Sep 17 00:00:00 2001 From: Kevin Godell Date: Mon, 20 Mar 2023 16:32:14 -0500 Subject: [PATCH 06/30] set default when root is not defined --- packages/node_modules/node-red/red.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 2f5ab354c..22b1eb224 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -316,10 +316,10 @@ httpsPromise.then(function(startupHttps) { } else { continue; } - sp.subRoot = formatRoot(sp.root); + sp.subRoot = formatRoot(sp.root || "/"); 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) From 54b2215164ce2e4c6584021ed4f64ac60e2275d4 Mon Sep 17 00:00:00 2001 From: Kevin Godell Date: Mon, 20 Mar 2023 17:31:10 -0500 Subject: [PATCH 07/30] pass options to express.static --- packages/node_modules/node-red/red.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 22b1eb224..d148dd73b 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -427,6 +427,7 @@ httpsPromise.then(function(startupHttps) { const sp = settings.httpStatic[si]; const filePath = sp.path; const thisRoot = sp.root || "/"; + const options = sp.options; if(appUseMem[filePath + "::" + thisRoot]) { continue;// this path and root already registered! } @@ -434,7 +435,7 @@ httpsPromise.then(function(startupHttps) { if (settings.httpStaticAuth) { app.use(thisRoot, basicAuthMiddleware(settings.httpStaticAuth.user, settings.httpStaticAuth.pass)); } - app.use(thisRoot, express.static(filePath)); + app.use(thisRoot, express.static(filePath, options)); } } From 7ae3e32abd11251182cca3b69164c1dd07552b4f Mon Sep 17 00:00:00 2001 From: Kevin Godell Date: Mon, 20 Mar 2023 17:34:12 -0500 Subject: [PATCH 08/30] update example and document feature for httpStatic options --- packages/node_modules/node-red/settings.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/node-red/settings.js b/packages/node_modules/node-red/settings.js index ef76f8680..218baa92d 100644 --- a/packages/node_modules/node-red/settings.js +++ b/packages/node_modules/node-red/settings.js @@ -223,10 +223,15 @@ module.exports = { * to move httpAdminRoot */ //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: [ // {path: '/home/nol/pics/', root: "/img/"}, // {path: '/home/nol/reports/', root: "/doc/"}, + // {path: '/home/nol/videos/', root: "/vid/", options: {maxAge: '1d'}} //], /** @@ -431,7 +436,7 @@ module.exports = { enabled: true } }, - + }, /******************************************************************************* From 12ac260dce0b248784c4583d7c11a369f288a533 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sun, 26 Mar 2023 11:00:28 +0100 Subject: [PATCH 09/30] select the deep link item --- packages/node_modules/@node-red/editor-client/src/js/red.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index ce3b439e4..b45f0378a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -263,6 +263,7 @@ var RED = (function() { setTimeout(() => { RED.view.reveal(nodeToShow.id) window.location.hash = currentHash + RED.view.select(nodeToShow.id) if (showEditDialog) { RED.editor.edit(nodeToShow) } @@ -273,6 +274,7 @@ var RED = (function() { if (nodeToShow) { RED.view.reveal(nodeToShow.id) window.location.hash = currentHash + RED.view.select({ nodes: [nodeToShow] }) if (showEditDialog) { RED.editor.editGroup(nodeToShow) } From b129e11c8f6b93925df535a4335766605f56fc13 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 28 Apr 2023 17:36:55 +0100 Subject: [PATCH 10/30] Avoid creating empty global-config node if not needed --- .../@node-red/editor-client/src/js/ui/env-var.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js b/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js index db1a8e86f..998484858 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/env-var.js @@ -50,7 +50,11 @@ RED.envVar = (function() { var new_env = []; var items = list.editableList('items'); 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) { credentials = { _ : {}, @@ -78,6 +82,12 @@ RED.envVar = (function() { if (gconf === null) { gconf = getGlobalConf(true); } + if (!gconf.credentials) { + gconf.credentials = { + _ : {}, + map: {} + }; + } if ((JSON.stringify(new_env) !== JSON.stringify(gconf.env)) || (JSON.stringify(credentials) !== JSON.stringify(gconf.credentials))) { gconf.env = new_env; From 20abe4a40cfd4d24b71e5bda69c433bf57155448 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 28 Apr 2023 21:37:03 +0100 Subject: [PATCH 11/30] Update dependecies include got --- package.json | 16 ++++++------- .../nodes/core/network/21-httprequest.js | 24 +++++++++++-------- .../node_modules/@node-red/nodes/package.json | 6 ++--- .../@node-red/registry/package.json | 4 ++-- .../@node-red/runtime/package.json | 2 +- .../node_modules/@node-red/util/package.json | 4 ++-- packages/node_modules/node-red/package.json | 4 ++-- .../nodes/core/network/21-httprequest_spec.js | 4 ++-- 8 files changed, 34 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index f4a87961c..a92965e54 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "express": "4.18.2", "express-session": "1.17.3", "form-data": "4.0.0", - "fs-extra": "10.1.0", - "got": "11.8.6", + "fs-extra": "11.1.1", + "got": "12.6.0", "hash-sum": "2.0.0", "hpagent": "1.2.0", "https-proxy-agent": "5.0.1", @@ -60,7 +60,7 @@ "memorystore": "1.6.7", "mime": "3.0.0", "moment": "2.29.4", - "moment-timezone": "0.5.41", + "moment-timezone": "0.5.43", "mqtt": "4.3.7", "multer": "1.4.5-lts.1", "mustache": "4.2.0", @@ -73,13 +73,13 @@ "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", "raw-body": "2.5.2", - "semver": "7.3.8", + "semver": "7.5.0", "tar": "6.1.13", "tough-cookie": "4.1.2", "uglify-js": "3.17.4", "uuid": "9.0.0", "ws": "7.5.6", - "xml2js": "0.4.23" + "xml2js": "0.5.0" }, "optionalDependencies": { "bcrypt": "5.1.0" @@ -108,14 +108,14 @@ "i18next-http-backend": "1.4.1", "jquery-i18next": "1.2.1", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template", - "marked": "4.2.12", - "mermaid": "^9.3.0", + "marked": "4.3.0", + "mermaid": "^9.4.3", "minami": "1.2.3", "mocha": "9.2.2", "node-red-node-test-helper": "^0.3.0", "nodemon": "2.0.20", "proxy": "^1.0.2", - "sass": "1.58.3", + "sass": "1.62.1", "should": "13.2.3", "sinon": "11.1.2", "stoppable": "^1.1.0", diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js index bcf3b72b9..4543c3655 100644 --- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js +++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js @@ -14,9 +14,9 @@ * limitations under the License. **/ -module.exports = function(RED) { +module.exports = async function(RED) { "use strict"; - const got = require("got"); + const { got } = await import('got') const {CookieJar} = require("tough-cookie"); const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent'); 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 :(. // Had to remove this to get http->https redirect to work // opts.defaultPort = isHttps?443:80; - opts.timeout = node.reqTimeout; + opts.timeout = { request: node.reqTimeout || 5000 }; opts.throwHttpErrors = false; // TODO: add UI option to auto decompress. Setting to false for 1.x compatibility opts.decompress = false; opts.method = method; - opts.retry = 0; + opts.retry = { limit: 0 }; opts.responseType = 'buffer'; opts.maxRedirects = 21; opts.cookieJar = new CookieJar(); opts.ignoreInvalidCookies = true; - opts.forever = nodeHTTPPersistent; + // opts.forever = nodeHTTPPersistent; if (msg.requestTimeout !== undefined) { if (isNaN(msg.requestTimeout)) { node.warn(RED._("httpin.errors.timeout-isnan")); } else if (msg.requestTimeout < 1) { node.warn(RED._("httpin.errors.timeout-isnegative")); } else { - opts.timeout = msg.requestTimeout; + opts.timeout = { request: msg.requestTimeout }; } } const originalHeaderMap = {}; @@ -245,9 +245,12 @@ in your Node-RED user directory (${RED.settings.userDir}). delete options.headers[h]; } }) - 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 } const requestUrl = new URL(response.request.requestUrl); - const options = response.request.options; + const options = { headers: {} } const normalisedHeaders = {}; Object.keys(response.headers).forEach(k => { normalisedHeaders[k.toLowerCase()] = response.headers[k] }) 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; } + // response.request.options.merge(options) sentCreds = true; return retry(options); } diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index 197223089..6bed85bd9 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -27,8 +27,8 @@ "cronosjs": "1.7.1", "denque": "2.1.0", "form-data": "4.0.0", - "fs-extra": "10.1.0", - "got": "11.8.6", + "fs-extra": "11.1.1", + "got": "12.6.0", "hash-sum": "2.0.0", "hpagent": "1.2.0", "https-proxy-agent": "5.0.1", @@ -44,7 +44,7 @@ "tough-cookie": "4.1.2", "uuid": "9.0.0", "ws": "7.5.6", - "xml2js": "0.4.23", + "xml2js": "0.5.0", "iconv-lite": "0.6.3" } } diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index 205f97631..fae0d624a 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -18,8 +18,8 @@ "dependencies": { "@node-red/util": "3.1.0-beta.2", "clone": "2.1.2", - "fs-extra": "10.1.0", - "semver": "7.3.8", + "fs-extra": "11.1.1", + "semver": "7.5.0", "tar": "6.1.13", "uglify-js": "3.17.4" } diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index e5fcf52a8..7504a7497 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -21,7 +21,7 @@ "async-mutex": "0.4.0", "clone": "2.1.2", "express": "4.18.2", - "fs-extra": "10.1.0", + "fs-extra": "11.1.1", "json-stringify-safe": "5.0.1" } } diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index 1ef2dcf08..184e96f38 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -15,12 +15,12 @@ } ], "dependencies": { - "fs-extra": "10.1.0", + "fs-extra": "11.1.1", "i18next": "21.10.0", "json-stringify-safe": "5.0.1", "jsonata": "1.8.6", "lodash.clonedeep": "^4.5.0", "moment": "2.29.4", - "moment-timezone": "0.5.41" + "moment-timezone": "0.5.43" } } diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index 3ea18092c..0b899f655 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -38,10 +38,10 @@ "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "express": "4.18.2", - "fs-extra": "10.1.0", + "fs-extra": "11.1.1", "node-red-admin": "^3.0.0", "nopt": "5.0.0", - "semver": "7.3.8" + "semver": "7.5.0" }, "optionalDependencies": { "bcrypt": "5.1.0" diff --git a/test/nodes/core/network/21-httprequest_spec.js b/test/nodes/core/network/21-httprequest_spec.js index 37e282bcf..f02ba8153 100644 --- a/test/nodes/core/network/21-httprequest_spec.js +++ b/test/nodes/core/network/21-httprequest_spec.js @@ -223,7 +223,7 @@ describe('HTTP Request Node', function() { } authFields[match[1]] = match[2] || match[3]; } - console.log(JSON.stringify(authFields)); + // console.log(JSON.stringify(authFields)); if (qop && authFields['qop'] != qop) { console.log('test1'); @@ -250,7 +250,7 @@ describe('HTTP Request Node', function() { req, algorithm, sess, realm, username, nonce, nc, cnonce, qop ); if (!response || expectedResponse.toLowerCase() !== response.toLowerCase()) { - console.log('test3'); + console.log('test3', response, expectedResponse); res.status(401).end(); return; } From 46ae66c8b2ff2c070800e07a3826695cb91af611 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Fri, 28 Apr 2023 21:42:36 +0100 Subject: [PATCH 12/30] Bump test helper version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a92965e54..f53cad7c6 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "mermaid": "^9.4.3", "minami": "1.2.3", "mocha": "9.2.2", - "node-red-node-test-helper": "^0.3.0", + "node-red-node-test-helper": "^0.3.1", "nodemon": "2.0.20", "proxy": "^1.0.2", "sass": "1.62.1", From c0650cc0f5b9071269c1208f9633bdc0c1693bdf Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Mon, 1 May 2023 13:53:54 +0900 Subject: [PATCH 13/30] Add Japanese translation for keyboard shortcut scope --- .../node_modules/@node-red/editor-client/locales/ja/editor.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index b5939cec0..6022b5e81 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -491,6 +491,7 @@ "unassigned": "未割当", "global": "グローバル", "workspace": "ワークスペース", + "editor": "編集ダイアログ", "selectAll": "全てのノードを選択", "selectNone": "選択を外す", "selectAllConnected": "接続されたノードを選択", From aff0bd3f6a35634b51414d26a26ac553ca01b52b Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Mon, 1 May 2023 14:28:02 +0900 Subject: [PATCH 14/30] Add Japanese translation for auto unsubscribe in MQTT node --- packages/node_modules/@node-red/nodes/locales/ja/messages.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/node_modules/@node-red/nodes/locales/ja/messages.json b/packages/node_modules/@node-red/nodes/locales/ja/messages.json index 3b5e3d6a4..a5736c60e 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/ja/messages.json @@ -414,6 +414,7 @@ "port": "ポート", "keepalive": "キープアライブ時間", "cleansession": "セッションの初期化", + "autoUnsubscribe": "切断時に購読を自動解除", "cleanstart": "クリーンスタート", "use-tls": "TLSを使用", "tls-config": "TLS設定", From fb5ffa1c318e6ab04901f448f61abe134085f73b Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Mon, 1 May 2023 14:44:14 +0900 Subject: [PATCH 15/30] Add Japanese translation for keyboard shortcut scope --- .../@node-red/editor-client/locales/ja/editor.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json index 84a1f4dad..1d08e9dd8 100644 --- a/packages/node_modules/@node-red/editor-client/locales/ja/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/ja/editor.json @@ -504,6 +504,7 @@ "unassigned": "未割当", "global": "グローバル", "workspace": "ワークスペース", + "editor": "編集ダイアログ", "selectAll": "全てのノードを選択", "selectNone": "選択を外す", "selectAllConnected": "接続されたノードを選択", @@ -1203,7 +1204,7 @@ "fr": "フランス語", "ja": "日本語", "ko": "韓国語", - "pt-BR":"ポルトガル語", + "pt-BR": "ポルトガル語", "ru": "ロシア語", "zh-CN": "中国語(簡体)", "zh-TW": "中国語(繁体)" From be4eab65f66fb98025f53633dabbcad45428eecd Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Sat, 6 May 2023 19:40:52 +0900 Subject: [PATCH 16/30] Fix content type for downloading flows.json --- .../@node-red/editor-client/src/js/ui/clipboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index f547203d4..b185a90c1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -37,13 +37,13 @@ RED.clipboard = (function() { // IE11 workaround // IE does not support data uri scheme for downloading data var blob = new Blob([data], { - type: "data:text/plain;charset=utf-8" + type: "data:json/application;charset=utf-8" }); navigator.msSaveBlob(blob, file); } else { var element = document.createElement('a'); - element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(data)); + element.setAttribute('href', 'data:json/application;charset=utf-8,' + encodeURIComponent(data)); element.setAttribute('download', file); element.style.display = 'none'; document.body.appendChild(element); From e8ddee24a9944e57d186f02b081295c56a45b67c Mon Sep 17 00:00:00 2001 From: Kazuhito Yokoi Date: Sat, 6 May 2023 21:10:49 +0900 Subject: [PATCH 17/30] Use correct content type for downloading flows.json --- .../@node-red/editor-client/src/js/ui/clipboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index b185a90c1..af82f1b14 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -37,13 +37,13 @@ RED.clipboard = (function() { // IE11 workaround // IE does not support data uri scheme for downloading data var blob = new Blob([data], { - type: "data:json/application;charset=utf-8" + type: "data:application/json;charset=utf-8" }); navigator.msSaveBlob(blob, file); } else { var element = document.createElement('a'); - element.setAttribute('href', 'data:json/application;charset=utf-8,' + encodeURIComponent(data)); + element.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(data)); element.setAttribute('download', file); element.style.display = 'none'; document.body.appendChild(element); From 67c5a248ad74f88d7fb5c044af288447c3e393a2 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Mon, 8 May 2023 09:28:35 +0100 Subject: [PATCH 18/30] Fix RBE for missing "payload" To close #4165 --- packages/node_modules/@node-red/nodes/core/function/rbe.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/function/rbe.js b/packages/node_modules/@node-red/nodes/core/function/rbe.js index eb526a441..34a8e91ed 100644 --- a/packages/node_modules/@node-red/nodes/core/function/rbe.js +++ b/packages/node_modules/@node-red/nodes/core/function/rbe.js @@ -35,7 +35,11 @@ module.exports = function(RED) { } 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) { var t = "_no_topic"; if (node.septopics) { t = topic || t; } From 7e9042e9f713eec981adeb8ff6af226a40efb5af Mon Sep 17 00:00:00 2001 From: wooferguy Date: Wed, 17 May 2023 05:14:18 +1200 Subject: [PATCH 19/30] Check for group Remove junction from groups node list if it is present. --- .../@node-red/editor-client/src/js/ui/view.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 4131f5dbf..84bf02332 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -2646,6 +2646,16 @@ RED.view = (function() { var result = RED.nodes.removeJunction(node) removedJunctions.push(node); 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 { if (node.direction === "out") { removedSubflowOutputs.push(node); From 9e3f1482735cfb5b5b46ad03debfc159b61694c6 Mon Sep 17 00:00:00 2001 From: wooferguy Date: Wed, 17 May 2023 18:56:07 +1200 Subject: [PATCH 20/30] Invalid JSONata Inject node test passing condition This test would sometimes run twice, causing the author to increase its catch count to 2 before considering the test complete. However even one pass proves the node is behaving as expected, and it always runs at least once. I have left the conditional statement in so it can be changed in future. --- test/nodes/core/common/20-inject_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nodes/core/common/20-inject_spec.js b/test/nodes/core/common/20-inject_spec.js index a4dbdd9e4..3e9a35391 100644 --- a/test/nodes/core/common/20-inject_spec.js +++ b/test/nodes/core/common/20-inject_spec.js @@ -854,7 +854,7 @@ describe('inject node', function() { }); n1.on("call:error", function(err) { count++; - if (count == 2) { + if (count == 1) { done(); } }); From 57359d1659e2404291c9acd216119dc0ec191228 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 10:54:37 +0100 Subject: [PATCH 21/30] Ensure express server options are applied consistently Fixes #4169 --- .../@node-red/editor-api/lib/admin/index.js | 15 ++------------- .../@node-red/editor-api/lib/editor/index.js | 7 ++++--- .../editor-api/lib/editor/projects.js | 7 ++++--- .../editor-api/lib/editor/settings.js | 4 ++-- .../@node-red/editor-api/lib/editor/sshkeys.js | 6 ++++-- .../@node-red/editor-api/lib/editor/theme.js | 15 +++++++++++++-- .../@node-red/editor-api/lib/index.js | 18 +++++------------- .../@node-red/editor-api/lib/util.js | 17 ++++++++++++++--- .../@node-red/runtime/lib/index.js | 9 +++++++++ .../editor-api/lib/editor/index_spec.js | 2 ++ .../editor-api/lib/editor/settings_spec.js | 4 ++-- .../editor-api/lib/editor/sshkeys_spec.js | 2 +- 12 files changed, 62 insertions(+), 44 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/index.js b/packages/node_modules/@node-red/editor-api/lib/admin/index.js index 8406fa8e9..26eabe65b 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/index.js @@ -14,8 +14,6 @@ * limitations under the License. **/ -var express = require("express"); - var nodes = require("./nodes"); var flows = require("./flows"); var flow = require("./flow"); @@ -37,18 +35,9 @@ module.exports = { plugins.init(runtimeAPI); diagnostics.init(settings, runtimeAPI); - var 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 needsPermission = auth.needsPermission; + const adminApp = apiUtil.createExpressApp(settings) // Flows adminApp.get("/flows",needsPermission("flows.read"),flows.get,apiUtil.errorHandler); diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/index.js b/packages/node_modules/@node-red/editor-api/lib/editor/index.js index f210d90fe..42be1f270 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/index.js @@ -46,14 +46,15 @@ module.exports = { runtimeAPI = _runtimeAPI; needsPermission = auth.needsPermission; if (!settings.disableEditor) { - info.init(runtimeAPI); + info.init(settings, runtimeAPI); comms.init(server,settings,runtimeAPI); var ui = require("./ui"); ui.init(runtimeAPI); - var editorApp = express(); + const editorApp = apiUtil.createExpressApp(settings) + if (settings.requireHttps === true) { editorApp.enable('trust proxy'); editorApp.use(function (req, res, next) { @@ -86,7 +87,7 @@ module.exports = { //Projects var projects = require("./projects"); - projects.init(runtimeAPI); + projects.init(settings, runtimeAPI); editorApp.use("/projects",projects.app()); // Locales diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/projects.js b/packages/node_modules/@node-red/editor-api/lib/editor/projects.js index ad505a46e..5d1b2ff26 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/projects.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/projects.js @@ -14,9 +14,9 @@ * limitations under the License. **/ -var express = require("express"); var apiUtils = require("../util"); +var settings; var runtimeAPI; var needsPermission = require("../auth").needsPermission; @@ -77,11 +77,12 @@ function getProjectRemotes(req,res) { }) } module.exports = { - init: function(_runtimeAPI) { + init: function(_settings, _runtimeAPI) { + settings = _settings; runtimeAPI = _runtimeAPI; }, app: function() { - var app = express(); + var app = apiUtils.createExpressApp(settings) app.use(function(req,res,next) { runtimeAPI.projects.available().then(function(available) { diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/settings.js b/packages/node_modules/@node-red/editor-api/lib/editor/settings.js index 5fa2476e1..200ddf2c2 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/settings.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/settings.js @@ -18,9 +18,9 @@ var runtimeAPI; var sshkeys = require("./sshkeys"); module.exports = { - init: function(_runtimeAPI) { + init: function(settings, _runtimeAPI) { runtimeAPI = _runtimeAPI; - sshkeys.init(runtimeAPI); + sshkeys.init(settings, runtimeAPI); }, userSettings: function(req, res) { var opts = { diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js index 6d1c62e11..08097571f 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js @@ -17,13 +17,15 @@ var apiUtils = require("../util"); var express = require("express"); var runtimeAPI; +var settings; module.exports = { - init: function(_runtimeAPI) { + init: function(_settings, _runtimeAPI) { runtimeAPI = _runtimeAPI; + settings = _settings; }, app: function() { - var app = express(); + const app = apiUtils.createExpressApp(settings); // List all SSH keys app.get("/", function(req,res) { diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js index c21a7e6e7..b3f6ef922 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js @@ -19,6 +19,7 @@ var util = require("util"); var path = require("path"); var fs = require("fs"); var clone = require("clone"); +const apiUtil = require("../util") var defaultContext = { page: { @@ -40,6 +41,7 @@ var defaultContext = { vendorMonaco: "" } }; +var settings; var theme = null; var themeContext = clone(defaultContext); @@ -92,7 +94,8 @@ function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) { } module.exports = { - init: function(settings, _runtimeAPI) { + init: function(_settings, _runtimeAPI) { + settings = _settings; runtimeAPI = _runtimeAPI; themeContext = clone(defaultContext); if (process.env.NODE_ENV == "development") { @@ -113,7 +116,15 @@ module.exports = { var url; 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) { diff --git a/packages/node_modules/@node-red/editor-api/lib/index.js b/packages/node_modules/@node-red/editor-api/lib/index.js index 56f52a222..d9f34eafd 100644 --- a/packages/node_modules/@node-red/editor-api/lib/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/index.js @@ -37,7 +37,6 @@ var adminApp; var server; var editor; - /** * Initialise the module. * @param {Object} settings The runtime settings @@ -49,7 +48,7 @@ var editor; function init(settings,_server,storage,runtimeAPI) { server = _server; if (settings.httpAdminRoot !== false) { - adminApp = express(); + adminApp = apiUtil.createExpressApp(settings); var cors = require('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); var maxApiRequestSize = settings.apiMaxLength || '5mb'; @@ -136,10 +127,11 @@ async function stop() { editor.stop(); } } + module.exports = { - init: init, - start: start, - stop: stop, + init, + start, + stop, /** * @memberof @node-red/editor-api diff --git a/packages/node_modules/@node-red/editor-api/lib/util.js b/packages/node_modules/@node-red/editor-api/lib/util.js index 621fd9e33..f1420235a 100644 --- a/packages/node_modules/@node-red/editor-api/lib/util.js +++ b/packages/node_modules/@node-red/editor-api/lib/util.js @@ -14,10 +14,9 @@ * limitations under the License. **/ +const express = require("express"); -var log = require("@node-red/util").log; // TODO: separate module -var i18n = require("@node-red/util").i18n; // TODO: separate module - +const { log, i18n } = require("@node-red/util"); module.exports = { errorHandler: function(err,req,res,next) { @@ -64,5 +63,17 @@ module.exports = { path: req.path, 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 } } diff --git a/packages/node_modules/@node-red/runtime/lib/index.js b/packages/node_modules/@node-red/runtime/lib/index.js index a886cd2ca..74f03c55c 100644 --- a/packages/node_modules/@node-red/runtime/lib/index.js +++ b/packages/node_modules/@node-red/runtime/lib/index.js @@ -89,6 +89,15 @@ function init(userSettings,httpServer,_adminApi) { nodeApp = 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) { adminApi = _adminApi; diff --git a/test/unit/@node-red/editor-api/lib/editor/index_spec.js b/test/unit/@node-red/editor-api/lib/editor/index_spec.js index 8ed4d88f3..2c11acbc3 100644 --- a/test/unit/@node-red/editor-api/lib/editor/index_spec.js +++ b/test/unit/@node-red/editor-api/lib/editor/index_spec.js @@ -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/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() { 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/theme").app.restore(); + NR_TEST_UTILS.require("@node-red/editor-api/lib/editor/settings").sshkeys.restore(); auth.needsPermission.restore(); log.error.restore(); }); diff --git a/test/unit/@node-red/editor-api/lib/editor/settings_spec.js b/test/unit/@node-red/editor-api/lib/editor/settings_spec.js index 171dca564..19f72977a 100644 --- a/test/unit/@node-red/editor-api/lib/editor/settings_spec.js +++ b/test/unit/@node-red/editor-api/lib/editor/settings_spec.js @@ -41,7 +41,7 @@ describe("api/editor/settings", function() { }); it('returns the user settings', function(done) { - info.init({ + info.init({}, { settings: { getUserSettings: function(opts) { if (opts.user !== "fred") { @@ -67,7 +67,7 @@ describe("api/editor/settings", function() { }); it('updates the user settings', function(done) { var update; - info.init({ + info.init({}, { settings: { updateUserSettings: function(opts) { if (opts.user !== "fred") { diff --git a/test/unit/@node-red/editor-api/lib/editor/sshkeys_spec.js b/test/unit/@node-red/editor-api/lib/editor/sshkeys_spec.js index 1647cd99d..75e8063f6 100644 --- a/test/unit/@node-red/editor-api/lib/editor/sshkeys_spec.js +++ b/test/unit/@node-red/editor-api/lib/editor/sshkeys_spec.js @@ -34,7 +34,7 @@ describe("api/editor/sshkeys", function() { } } before(function() { - sshkeys.init(mockRuntime); + sshkeys.init({}, mockRuntime); app = express(); app.use(bodyParser.json()); app.use("/settings/user/keys", sshkeys.app()); From 0e52271ba9b585bf044270cf6c70111965c20f32 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 11:00:15 +0100 Subject: [PATCH 22/30] Remove version info from theme endpoint Fixes #4170 --- packages/node_modules/@node-red/editor-api/lib/editor/theme.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js index c21a7e6e7..a3467a66c 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js @@ -27,8 +27,7 @@ var defaultContext = { tabicon: { icon: "red/images/node-red-icon-black.svg", colour: "#8f0000" - }, - version: require(path.join(__dirname,"../../package.json")).version + } }, header: { title: "Node-RED", From 6e1b298282d57cc882ae2475943618b1852c3ce9 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 13:57:12 +0100 Subject: [PATCH 23/30] Reconstruct xml2js output as proper object --- .../@node-red/nodes/core/parsers/70-XML.js | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/nodes/core/parsers/70-XML.js b/packages/node_modules/@node-red/nodes/core/parsers/70-XML.js index a778c4d72..538368730 100644 --- a/packages/node_modules/@node-red/nodes/core/parsers/70-XML.js +++ b/packages/node_modules/@node-red/nodes/core/parsers/70-XML.js @@ -33,7 +33,13 @@ module.exports = function(RED) { parseString(value, options, function (err, result) { if (err) { done(err); } 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); send(msg); done(); @@ -46,4 +52,18 @@ module.exports = function(RED) { }); } 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 + } } From 18610bb54094e56f6a35e250b7223f392a7271b6 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 14:24:11 +0100 Subject: [PATCH 24/30] Ensure external modules are installed synchronously Fixes #4168 --- .../@node-red/registry/lib/externalModules.js | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/packages/node_modules/@node-red/registry/lib/externalModules.js b/packages/node_modules/@node-red/registry/lib/externalModules.js index b61392d9b..b76f748a4 100644 --- a/packages/node_modules/@node-red/registry/lib/externalModules.js +++ b/packages/node_modules/@node-red/registry/lib/externalModules.js @@ -242,63 +242,68 @@ async function ensureModuleDir() { } } +let installLock = Promise.resolve() async function installModule(moduleDetails) { - let installSpec = moduleDetails.module; - if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) { - const e = new Error("Install not allowed"); - e.code = "install_not_allowed"; - 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"; + const result = installLock.then(async () => { + let installSpec = moduleDetails.module; + if (!registryUtil.checkModuleAllowed( moduleDetails.module, moduleDetails.version,installAllowList,installDenyList)) { + const e = new Error("Install not allowed"); + e.code = "install_not_allowed"; 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 = { From e6c454bba544ca51df492772e52cf4882a0c96d7 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 15:11:57 +0100 Subject: [PATCH 25/30] Ensure non-zero exit codes for errors --- packages/node_modules/node-red/red.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/node_modules/node-red/red.js b/packages/node_modules/node-red/red.js index 2f5ab354c..72f8a4169 100755 --- a/packages/node_modules/node-red/red.js +++ b/packages/node_modules/node-red/red.js @@ -148,7 +148,7 @@ try { } else { console.log(err); } - process.exit(); + process.exit(1); } if (parsedArgs.define) { @@ -182,7 +182,7 @@ if (parsedArgs.define) { } } catch (e) { console.log("Error processing -D option: "+e.message); - process.exit(); + process.exit(1); } } From 2ae2ec2578a668c2751556a2ad5c7b0c79b84dca Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 16:07:14 +0100 Subject: [PATCH 26/30] Combine existing env vars when merging groups Closes #4101 --- .../editor-client/src/js/ui/group.js | 8 +- .../@node-red/editor-client/src/js/ui/view.js | 120 ++++++++++-------- 2 files changed, 74 insertions(+), 54 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js index 738b6e38d..13056f09b 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/group.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/group.js @@ -401,7 +401,7 @@ RED.group = (function() { } } var existingGroup; - + var mergedEnv = {} // Second pass, ungroup any groups in the selection and add their contents // to the selection for (var i=0; i 0) { + n.env.forEach(env => { + mergedEnv[env.name] = env + }) + } ungroupHistoryEvent.groups.push(n); nodes = nodes.concat(ungroup(n)); } else { @@ -427,6 +432,7 @@ RED.group = (function() { group.style = existingGroup.style; group.name = existingGroup.name; } + group.env = Object.values(mergedEnv) RED.view.select({nodes:[group]}) } historyEvent.events.push({ diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 2909e8182..674648747 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -232,6 +232,63 @@ RED.view = (function() { 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() { @@ -1136,7 +1193,7 @@ RED.view = (function() { var touchTrigger = options.touchTrigger; if (targetGroup) { - selectGroup(targetGroup,false); + selectedGroups.add(targetGroup,false); RED.view.redraw(); } @@ -1462,7 +1519,7 @@ RED.view = (function() { clearSelection(); nn.selected = true; if (targetGroup) { - selectGroup(targetGroup,false); + selectedGroups.add(targetGroup,false); } movingSet.add(nn); updateActiveNodes(); @@ -1926,7 +1983,7 @@ RED.view = (function() { if (!movingSet.has(n) && !n.selected) { // group entirely within lasso 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(); activeGroups.forEach(function(g) { if (!g.g) { - selectGroup(g, true); + selectedGroups.add(g, true); if (!g.selected) { g.selected = true; g.dirty = true; @@ -2346,10 +2403,7 @@ RED.view = (function() { } movingSet.clear(); selectedLinks.clear(); - activeGroups.forEach(function(g) { - g.selected = false; - g.dirty = true; - }) + selectedGroups.clear(); } var lastSelection = null; @@ -3438,7 +3492,7 @@ RED.view = (function() { if (!groupNodeSelectPrimed && !d.selected && d.g && RED.nodes.group(d.g).selected) { clearSelection(); - selectGroup(RED.nodes.group(d.g), false); + selectedGroups.add(RED.nodes.group(d.g), false); mousedown_node.selected = true; movingSet.add(mousedown_node); @@ -3859,14 +3913,14 @@ RED.view = (function() { lastClickNode = g; if (g.selected && (d3.event.ctrlKey||d3.event.metaKey)) { - deselectGroup(g); + selectedGroups.remove(g); d3.event.stopPropagation(); } else { if (!g.selected) { if (!d3.event.ctrlKey && !d3.event.metaKey) { clearSelection(); } - selectGroup(g,true);//!wasSelected); + selectedGroups.add(g,true);//!wasSelected); } if (d3.event.button != 2) { @@ -3882,45 +3936,6 @@ RED.view = (function() { 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) { // x,y expected to be in node-co-ordinate space var candidateGroups = {}; @@ -5901,11 +5916,10 @@ RED.view = (function() { if (movingSet.length() > 0) { movingSet.forEach(function(n) { 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) { var groupNodes = RED.group.getNodes(g,true); groupNodes.forEach(function(n) { @@ -6114,7 +6128,7 @@ RED.view = (function() { n.dirty = true; movingSet.add(n); } else { - selectGroup(n,true); + selectedGroups.add(n,true); } }) } From 7a4f48e4fb083f876cdb34035b55ccddb37810c0 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 16:26:38 +0100 Subject: [PATCH 27/30] Place subflow outputs/inputs relative to current view Fixes #4111 --- .../@node-red/editor-client/src/js/ui/subflow.js | 4 +++- .../@node-red/editor-client/src/js/ui/view.js | 9 +++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index ef9da4f96..da38e6054 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -46,7 +46,9 @@ RED.subflow = (function() { ''; 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) { pos.x += 110; } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 2909e8182..f15c09f82 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -4359,6 +4359,7 @@ RED.view = (function() { this.__port__.setAttribute("transform","translate(-5,"+((d.h/2)-5)+")"); this.__outputOutput__.setAttribute("transform","translate(20,"+((d.h/2)-8)+")"); this.__outputNumber__.setAttribute("transform","translate(20,"+((d.h/2)+7)+")"); + this.__outputNumber__.textContent = d.i+1; } d.dirty = false; } @@ -6282,8 +6283,12 @@ RED.view = (function() { }) }, scroll: function(x,y) { - chart.scrollLeft(chart.scrollLeft()+x); - chart.scrollTop(chart.scrollTop()+y) + if (x !== undefined && y !== undefined) { + chart.scrollLeft(chart.scrollLeft()+x); + chart.scrollTop(chart.scrollTop()+y) + } else { + return [chart.scrollLeft(), chart.scrollTop()] + } }, clickNodeButton: function(n) { if (n._def.button) { From 90e32f52c9535e340c67279aeac1f04d97535797 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 16:51:17 +0100 Subject: [PATCH 28/30] Enable RED.view.select to select group by id --- .../node_modules/@node-red/editor-client/src/js/red.js | 2 +- .../node_modules/@node-red/editor-client/src/js/ui/view.js | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/red.js b/packages/node_modules/@node-red/editor-client/src/js/red.js index b45f0378a..89464bd6c 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/red.js +++ b/packages/node_modules/@node-red/editor-client/src/js/red.js @@ -274,7 +274,7 @@ var RED = (function() { if (nodeToShow) { RED.view.reveal(nodeToShow.id) window.location.hash = currentHash - RED.view.select({ nodes: [nodeToShow] }) + RED.view.select(nodeToShow.id) if (showEditDialog) { RED.editor.editGroup(nodeToShow) } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 04fcf1335..9fdbc3dde 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -6114,6 +6114,13 @@ RED.view = (function() { selectedNode.dirty = true; movingSet.clear(); movingSet.add(selectedNode); + } else { + selectedNode = RED.nodes.group(selection); + if (selectedNode) { + movingSet.clear(); + selectedGroups.clear() + selectedGroups.add(selectedNode) + } } } else if (selection) { if (selection.nodes) { From 2388232179f59c71066f4cf5eaea881885946088 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 22:33:31 +0100 Subject: [PATCH 29/30] Fix catch/status group scoping to handle group hierarchies --- .../@node-red/runtime/lib/flows/Flow.js | 83 ++++++++++++++----- .../@node-red/runtime/lib/flows/Flow_spec.js | 74 +++++++++++++++++ 2 files changed, 135 insertions(+), 22 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 0c0d1e61e..4430e59ed 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -665,16 +665,36 @@ class Flow { } handled = true; } else { - const flow = this; - this.statusNodes.forEach(function(targetStatusNode) { + const candidateNodes = []; + 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; } - - if (targetStatusNode.scope === "group" && flow.isNodeInSameGroup(targetStatusNode, reportingNode) === false){ - 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 = { status: clone(statusMessage) } @@ -732,27 +752,46 @@ class Flow { } handled = true; } else { - var handledByUncaught = false; - - const flow = this; - this.catchNodes.forEach(function(targetCatchNode) { + const candidateNodes = []; + this.catchNodes.forEach(targetCatchNode => { + if (targetCatchNode.g && targetCatchNode.scope === 'group' && !reportingNode.g) { + // 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; } - - if (targetCatchNode.scope === "group" && flow.isNodeInSameGroup(targetCatchNode, reportingNode) === false){ - return; - } - - if ((!targetCatchNode.scope || targetCatchNode.scope === "group") && targetCatchNode.uncaught && !handledByUncaught) { - if (handled) { - // This has been handled by a !uncaught catch node - return; + let distance = 0 + if (reportingNode.g) { + // Reporting node inside a group. Calculate the distance between it and the catch node + 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) { errorMessage = redUtil.cloneMessage(msg); } else { diff --git a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js index ca30868d7..6d53747ef 100644 --- a/test/unit/@node-red/runtime/lib/flows/Flow_spec.js +++ b/test/unit/@node-red/runtime/lib/flows/Flow_spec.js @@ -686,6 +686,44 @@ describe('Flow', function() { },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() { @@ -796,6 +834,42 @@ describe('Flow', function() { },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){ var config = flowUtils.parseConfig([ {id:"t1",type:"tab"}, From 991f13e704f0ae2f979bd7a4ef1d4ca92cceb8de Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Mon, 22 May 2023 23:14:31 +0100 Subject: [PATCH 30/30] Remove unused function --- .../@node-red/runtime/lib/flows/Flow.js | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js index 4430e59ed..489ec42cb 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/Flow.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/Flow.js @@ -128,65 +128,6 @@ class Flow { } this.parent.log(msg); } - - /** - * Checks if node A and node B are in the same group. - * Node B can also be placed in a subgroup. - * If node A is not in any group, false is returned - * @param {Node} nodeIdA Node which defines the first search level - * @param {Node} nodeIdB Node which is to be searched in the group or a subgroup - * @returns {boolean} Returns true if all nodes are in the same group. If not, then false or if node A is not in a group then also false. - */ - isNodeInSameGroup(nodeIdA, nodeIdB) { - const groups = this.global.groups; - let result = false; - for(let key in groups) { - let group = groups[key]; - - if(!group.nodes.includes(nodeIdA.id)) { - continue; - } - - if(group.nodes.includes(nodeIdB.id)) { - result = true; - break; - } - - /** - * Subfunction to recursively search the groups for matches - * @param {Node} Node which is to be searched in the group or a subgroup - * @param {Group} targetGroup group currently under analysis - * @returns {boolean} Returns true if a match was found. Otherwise false. - */ - const isInSubGroup = (targetNode, targetGroup) => { - let _result = false; - if(targetGroup.nodes.includes(targetNode.id)) { - _result = true; - } else { - for(let nodeId of targetGroup.nodes) { - let node = this.getGroupNode(nodeId); - - if(!node){ - continue; - } - - if(node.type === "group"){ - let result = isInSubGroup(targetNode, node); - if(result === true){ - _result = true; - break; - } - } - } - } - - return _result; - }; - - result = isInSubGroup(nodeIdB, group); - } - return result; - } /** * Start this flow.