diff --git a/CHANGELOG.md b/CHANGELOG.md index 747f78261..9eabbaaac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,35 @@ +#### 2.2.2: Maintenance Release + +Nodes + + - Fix "close timed out" error when performing full deploy or modifying broker node. (#3451) @Steve-Mcl + + +#### 2.2.1: Maintenance Release + +Editor + + - Handle mixed-cased filter terms in keyboard shortcut dialog (#3444) @knolleary + - Prevent duplicate links being added between nodes (#3442) @knolleary + - Fix to hide tooltip after removing subflow tab (#3391) @HiroyasuNishiyama + - Fix node validation to be applied to config node (#3397) @HiroyasuNishiyama + - Fix: Dont add wires to undo buffer twice (#3437) @Steve-Mcl + +Runtime + + - Improve module location parsing (of stack info) when adding hook (#3447) @Steve-Mcl + - Fix substitution of NR_NODE_PATH (#3445) @HiroyasuNishiyama + - Remove console.log when ignoring disabled module (#3439) @knolleary + - Improve "Unexpected Node Error" logging (#3446) @Steve-Mcl + +Nodes + + - Debug: Fix no-prototype-builtins bug in debug node and utils (#3394) @Alkarex + - Delay: Fix Japanese message of delay node (#3434) + - Allow nbRateUnits to be undefined when validating (#3443) @knolleary + - Coding help for recently added node-red Predefined Environment Variables (#3440) @Steve-Mcl + + #### 2.2.0: Milestone Release Editor diff --git a/package.json b/package.json index 91bbb684d..df031ca2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "2.2.0", + "version": "2.2.2", "description": "Low-code programming for event-driven applications", "homepage": "http://nodered.org", "license": "Apache-2.0", @@ -28,7 +28,7 @@ "dependencies": { "acorn": "8.7.0", "acorn-walk": "8.2.0", - "ajv": "8.9.0", + "ajv": "8.10.0", "async-mutex": "0.3.2", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", @@ -36,7 +36,7 @@ "cheerio": "1.0.0-rc.10", "clone": "2.1.2", "content-type": "1.0.4", - "cookie": "0.4.1", + "cookie": "0.4.2", "cookie-parser": "1.4.6", "cors": "2.8.5", "cronosjs": "1.7.1", @@ -50,32 +50,32 @@ "hash-sum": "2.0.0", "hpagent": "0.1.2", "https-proxy-agent": "5.0.0", - "i18next": "21.6.10", + "i18next": "21.6.11", "iconv-lite": "0.6.3", "is-utf8": "0.2.1", "js-yaml": "3.14.1", "json-stringify-safe": "5.0.1", - "jsonata": "1.8.5", + "jsonata": "1.8.6", "lodash.clonedeep": "^4.5.0", "media-typer": "1.1.0", - "memorystore": "1.6.6", + "memorystore": "1.6.7", "mime": "3.0.0", "moment-timezone": "0.5.34", - "mqtt": "4.3.4", + "mqtt": "4.3.5", "multer": "1.4.4", "mustache": "4.2.0", - "node-red-admin": "^2.2.1", + "node-red-admin": "^2.2.3", "nopt": "5.0.0", "oauth2orize": "1.11.1", "on-headers": "1.0.2", "passport": "0.5.2", "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", - "raw-body": "2.4.2", + "raw-body": "2.4.3", "semver": "7.3.5", "tar": "6.1.11", "tough-cookie": "4.0.0", - "uglify-js": "3.15.0", + "uglify-js": "3.15.1", "uuid": "8.3.2", "ws": "7.5.6", "xml2js": "0.4.23" @@ -84,7 +84,7 @@ "bcrypt": "5.0.1" }, "devDependencies": { - "dompurify": "2.3.4", + "dompurify": "2.3.5", "grunt": "1.4.1", "grunt-chmod": "~1.1.1", "grunt-cli": "~1.4.3", @@ -113,11 +113,11 @@ "node-red-node-test-helper": "^0.2.7", "nodemon": "2.0.15", "proxy": "^1.0.2", - "sass": "1.49.0", + "sass": "1.49.7", "should": "13.2.3", "sinon": "11.1.2", "stoppable": "^1.1.0", - "supertest": "6.2.1" + "supertest": "6.2.2" }, "engines": { "node": ">=12" diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index 0436f2871..468585284 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "2.2.0", + "version": "2.2.2", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,15 +16,15 @@ } ], "dependencies": { - "@node-red/util": "2.2.0", - "@node-red/editor-client": "2.2.0", + "@node-red/util": "2.2.2", + "@node-red/editor-client": "2.2.2", "bcryptjs": "2.4.3", "body-parser": "1.19.1", "clone": "2.1.2", "cors": "2.8.5", "express-session": "1.17.2", "express": "4.17.2", - "memorystore": "1.6.6", + "memorystore": "1.6.7", "mime": "3.0.0", "multer": "1.4.4", "mustache": "4.2.0", diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index 03bba0d2a..347fe9112 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "2.2.0", + "version": "2.2.2", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index 78b9deae6..ab7fae401 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -600,6 +600,14 @@ RED.nodes = (function() { RED.events.emit('nodes:add',n); } function addLink(l) { + if (nodeLinks[l.source.id]) { + const isUnique = nodeLinks[l.source.id].out.every(function(link) { + return link.sourcePort !== l.sourcePort || link.target.id !== l.target.id + }) + if (!isUnique) { + return + } + } links.push(l); if (l.source) { // Possible the node hasn't been added yet diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index f060af37d..5871abcde 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -350,6 +350,15 @@ RED.popover = (function() { } } + target.on("remove", function (ev) { + if (timer) { + clearTimeout(timer); + } + if (active) { + active = false; + setTimeout(closePopup,delay.hide); + } + }); if (trigger === 'hover') { target.on('mouseenter',function(e) { clearTimeout(timer); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js index a407fc6cd..592075dfd 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js @@ -334,6 +334,9 @@ RED.deploy = (function() { var invalidNodes = []; RED.nodes.eachConfig(function(node) { + if (node.valid === undefined) { + RED.editor.validateNode(node); + } if (!node.valid && !node.d) { invalidNodes.push(getNodeInfo(node)); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js index 35ca607a0..0e17fec75 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/monaco.js @@ -577,7 +577,7 @@ RED.editor.codeEditor.monaco = (function() { createMonacoCompletionItem("set (flow context)", 'flow.set("${1:name}", ${1:value});','Set a value in flow context',range), createMonacoCompletionItem("get (global context)", 'global.get("${1:name}");','Get a value from global context',range), createMonacoCompletionItem("set (global context)", 'global.set("${1:name}", ${1:value});','Set a value in global context',range), - createMonacoCompletionItem("get (env)", 'env.get("${1:name}");','Get env variable value',range), + createMonacoCompletionItem("get (env)", 'env.get("${1|NR_NODE_ID,NR_NODE_NAME,NR_NODE_PATH,NR_GROUP_ID,NR_GROUP_NAME,NR_FLOW_ID,NR_FLOW_NAME|}");','Get env variable value',range), createMonacoCompletionItem("cloneMessage (RED.util)", 'RED.util.cloneMessage(${1:msg});', ["```typescript", "RED.util.cloneMessage(msg: T): T", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/keyboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/keyboard.js index cecb408bd..d9511269d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/keyboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/keyboard.js @@ -625,7 +625,7 @@ RED.keyboard = (function() { pane.find("#red-ui-settings-tab-keyboard-filter").searchBox({ delay: 100, change: function() { - var filterValue = $(this).val().trim(); + var filterValue = $(this).val().trim().toLowerCase(); if (filterValue === "") { shortcutList.editableList('filter', null); } else { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/search.js b/packages/node_modules/@node-red/editor-client/src/js/ui/search.js index e460224c1..6834679e2 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/search.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/search.js @@ -119,6 +119,7 @@ RED.search = (function() { val = extractFlag(val,"config",flags); val = extractFlag(val,"subflow",flags); val = extractFlag(val,"hidden",flags); + val = extractFlag(val,"modified",flags); // uses: val = extractValue(val,"uses",flags); @@ -164,6 +165,11 @@ RED.search = (function() { continue; } } + if (flags.hasOwnProperty("modified")) { + if (!node.node.changed && !node.node.moved) { + continue; + } + } if (flags.hasOwnProperty("hidden")) { // Only tabs can be hidden if (node.node.type !== 'tab') { 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 5b9285152..a3c838ce1 100755 --- 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 @@ -2338,14 +2338,21 @@ RED.view = (function() { var removedSubflowStatus; var subflowInstances = []; var historyEvents = []; - + var addToRemovedLinks = function(links) { + if(!links) { return; } + var _links = Array.isArray(links) ? links : [links]; + _links.forEach(function(l) { + removedLinks.push(l); + selectedLinks.remove(l); + }) + } if (reconnectWires) { var reconnectResult = RED.nodes.detachNodes(movingSet.nodes()) var addedLinks = reconnectResult.newLinks; if (addedLinks.length > 0) { historyEvents.push({ t:'add', links: addedLinks }) } - removedLinks = removedLinks.concat(reconnectResult.removedLinks) + addToRemovedLinks(reconnectResult.removedLinks) } var startDirty = RED.nodes.dirty(); @@ -2377,7 +2384,7 @@ RED.view = (function() { var removedEntities = RED.nodes.remove(node.id); removedNodes.push(node); removedNodes = removedNodes.concat(removedEntities.nodes); - removedLinks = removedLinks.concat(removedEntities.links); + addToRemovedLinks(removedNodes.removedLinks); if (node.g) { var group = RED.nodes.group(node.g); if (selectedGroups.indexOf(group) === -1) { @@ -2410,20 +2417,20 @@ RED.view = (function() { if (removedSubflowOutputs.length > 0) { result = RED.subflow.removeOutput(removedSubflowOutputs); if (result) { - removedLinks = removedLinks.concat(result.links); + addToRemovedLinks(result.links); } } // Assume 0/1 inputs if (removedSubflowInputs.length == 1) { result = RED.subflow.removeInput(); if (result) { - removedLinks = removedLinks.concat(result.links); + addToRemovedLinks(result.links); } } if (removedSubflowStatus) { result = RED.subflow.removeStatus(); if (result) { - removedLinks = removedLinks.concat(result.links); + addToRemovedLinks(result.links); } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js index d3fc939a6..c899403eb 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js @@ -428,6 +428,9 @@ RED.workspaces = (function() { } } }) + RED.actions.add("core:list-modified-nodes",function() { + RED.actions.invoke("core:search","is:modified "); + }) RED.actions.add("core:list-hidden-flows",function() { RED.actions.invoke("core:search","is:hidden "); }) diff --git a/packages/node_modules/@node-red/editor-client/src/types/node-red/func.d.ts b/packages/node_modules/@node-red/editor-client/src/types/node-red/func.d.ts index 6ac0d8b02..ae411f33c 100644 --- a/packages/node_modules/@node-red/editor-client/src/types/node-red/func.d.ts +++ b/packages/node_modules/@node-red/editor-client/src/types/node-red/func.d.ts @@ -263,6 +263,20 @@ declare class global { static keys(store: string, callback: Function); } declare class env { - /** Get an environment variable value */ - static get(name:string); + /** + * Get an environment variable value + * + * Predefined node-red variables... + * * `NR_NODE_ID` - the ID of the node + * * `NR_NODE_NAME` - the Name of the node + * * `NR_NODE_PATH` - the Path of the node + * * `NR_GROUP_ID` - the ID of the containing group + * * `NR_GROUP_NAME` - the Name of the containing group + * * `NR_FLOW_ID` - the ID of the flow the node is on + * * `NR_FLOW_NAME` - the Name of the flow the node is on + * @param name Name of the environment variable to get + * @example + * ```const flowName = env.get("NR_FLOW_NAME");``` + */ + static get(name:string) :string; } diff --git a/packages/node_modules/@node-red/nodes/core/common/21-debug.js b/packages/node_modules/@node-red/nodes/core/common/21-debug.js index 73d364e43..92fa46967 100644 --- a/packages/node_modules/@node-red/nodes/core/common/21-debug.js +++ b/packages/node_modules/@node-red/nodes/core/common/21-debug.js @@ -7,6 +7,7 @@ module.exports = function(RED) { var debuglength = RED.settings.debugMaxLength || 1000; var useColors = RED.settings.debugUseColors || false; util.inspect.styles.boolean = "red"; + const { hasOwnProperty } = Object.prototype; function DebugNode(n) { var hasEditExpression = (n.targetType === "jsonata"); @@ -107,7 +108,7 @@ module.exports = function(RED) { } }) this.on("input", function(msg, send, done) { - if (msg.hasOwnProperty("status") && msg.status.hasOwnProperty("source") && msg.status.source.hasOwnProperty("id") && (msg.status.source.id === node.id)) { + if (hasOwnProperty.call(msg, "status") && hasOwnProperty.call(msg.status, "source") && hasOwnProperty.call(msg.status.source, "id") && (msg.status.source.id === node.id)) { done(); return; } @@ -118,17 +119,17 @@ module.exports = function(RED) { var st = (typeof output === 'string') ? output : util.inspect(output); var fill = "grey"; var shape = "dot"; - if (typeof output === 'object' && output.hasOwnProperty("fill") && output.hasOwnProperty("shape") && output.hasOwnProperty("text")) { + if (typeof output === 'object' && hasOwnProperty.call(output, "fill") && hasOwnProperty.call(output, "shape") && hasOwnProperty.call(output, "text")) { fill = output.fill; shape = output.shape; st = output.text; } if (node.statusType === "auto") { - if (msg.hasOwnProperty("error")) { + if (hasOwnProperty.call(msg, "error")) { fill = "red"; st = msg.error.message; } - if (msg.hasOwnProperty("status")) { + if (hasOwnProperty.call(msg, "status")) { fill = msg.status.fill || "grey"; shape = msg.status.shape || "ring"; st = msg.status.text || ""; @@ -194,7 +195,7 @@ module.exports = function(RED) { function sendDebug(msg) { // don't put blank errors in sidebar (but do add to logs) - //if ((msg.msg === "") && (msg.hasOwnProperty("level")) && (msg.level === 20)) { return; } + //if ((msg.msg === "") && (hasOwnProperty.call(msg, "level")) && (msg.level === 20)) { return; } msg = RED.util.encodeObject(msg,{maxLength:debuglength}); RED.comms.publish("debug",msg); } diff --git a/packages/node_modules/@node-red/nodes/core/function/10-switch.js b/packages/node_modules/@node-red/nodes/core/function/10-switch.js index aa1972221..fdc345f47 100644 --- a/packages/node_modules/@node-red/nodes/core/function/10-switch.js +++ b/packages/node_modules/@node-red/nodes/core/function/10-switch.js @@ -55,6 +55,7 @@ module.exports = function(RED) { catch(e) { return false;} } else if (b === "null") { return a === null; } + else if (b === "number") { return typeof a === b && !isNaN(a) } else { return typeof a === b && !Array.isArray(a) && !Buffer.isBuffer(a) && a !== null; } }, 'head': function(a, b, c, d, parts) { diff --git a/packages/node_modules/@node-red/nodes/core/function/89-delay.html b/packages/node_modules/@node-red/nodes/core/function/89-delay.html index 8ee404924..d5b69dffd 100644 --- a/packages/node_modules/@node-red/nodes/core/function/89-delay.html +++ b/packages/node_modules/@node-red/nodes/core/function/89-delay.html @@ -115,7 +115,7 @@ timeoutUnits: {value:"seconds"}, rate: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }}, nbRateUnits: {value:"1", required:false, - validate:function(v) { return RED.validators.number(v) && (v >= 0); }}, + validate:function(v) { return v === undefined || (RED.validators.number(v) && (v >= 0)); }}, rateUnits: {value: "second"}, randomFirst: {value:"1", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }}, randomLast: {value:"5", required:true, validate:function(v) { return RED.validators.number(v) && (v >= 0); }}, diff --git a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js index 5546dc868..bdf2b0e5b 100644 --- a/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js +++ b/packages/node_modules/@node-red/nodes/core/network/10-mqtt.js @@ -637,24 +637,8 @@ module.exports = function(RED) { node.deregister = function(mqttNode,done) { delete node.users[mqttNode.id]; - if (node.closing) { - return done(); - } - if (Object.keys(node.users).length === 0) { - if (node.client && node.client.connected) { - // Send close message - if (node.closeMessage) { - node.publish(node.closeMessage,function(err) { - node.client.end(done); - }); - } else { - node.client.end(done); - } - return; - } else { - if (node.client) { node.client.end(); } - return done(); - } + if (!node.closing && node.connected && Object.keys(node.users).length === 0) { + node.disconnect(); } done(); }; @@ -663,6 +647,7 @@ module.exports = function(RED) { } node.connect = function (callback) { if (node.canConnect()) { + node.closing = false; node.connecting = true; setStatusConnecting(node, true); try { @@ -672,6 +657,7 @@ module.exports = function(RED) { let callbackDone = false; //prevent re-connects causing node.client.on('connect' firing callback multiple times // Register successful connect or reconnect handler node.client.on('connect', function (connack) { + node.closing = false; node.connecting = false; node.connected = true; if(!callbackDone && typeof callback == "function") { @@ -740,6 +726,7 @@ module.exports = function(RED) { reasonCode: rc, reasonString: rs } + node.connected = false; node.log(RED._("mqtt.state.broker-disconnected", details)); setStatusDisconnected(node, true); }); @@ -764,25 +751,31 @@ module.exports = function(RED) { } }; node.disconnect = function (callback) { - const _callback = function () { + const _callback = function (resetNodeConnectedState) { setStatusDisconnected(node, true); - node.connecting = false; - node.connected = false; + if(resetNodeConnectedState) { + node.closing = true; + node.connecting = false; + node.connected = false; + } callback && typeof callback == "function" && callback(); }; - if(node.client) { - if(node.client.connected && node.closeMessage) { - node.publish(node.closeMessage, function (err) { - node.client.end(_callback); - }); - } else if(node.client.connected || node.client.reconnecting) { - node.client.end(_callback); - } else if(node.client.disconnecting || node.client.connected === false) { - _callback(); - } + if(node.closing) { + return _callback(false); + } + var endCallBack = function endCallBack() { + } + if(node.connected && node.closeMessage) { + node.publish(node.closeMessage, function (err) { + node.client.end(endCallBack); + _callback(true); + }); + } else if(node.connected) { + node.client.end(endCallBack); + _callback(true); } else { - _callback(); + _callback(false); } } node.subscriptionIds = {}; @@ -1074,6 +1067,8 @@ module.exports = function(RED) { node.brokerConn.unsubscribe(node.topic,node.id, removed); } node.brokerConn.deregister(node, done); + } else { + done(); } }); } else { @@ -1134,7 +1129,11 @@ module.exports = function(RED) { } node.brokerConn.register(node); node.on('close', function(done) { - node.brokerConn.deregister(node,done); + if (node.brokerConn) { + node.brokerConn.deregister(node,done); + } else { + done(); + } }); } else { node.error(RED._("mqtt.errors.missing-config")); 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 70b813554..7784dbe4b 100644 --- a/packages/node_modules/@node-red/nodes/locales/ja/messages.json +++ b/packages/node_modules/@node-red/nodes/locales/ja/messages.json @@ -291,8 +291,8 @@ "hour": "時間", "days": "日", "day": "日", - "between": "頻度", - "and": "回/", + "between": "範囲", + "and": "〜", "rate": "流量", "msgper": "メッセージ/", "queuemsg": "中間メッセージをキューに追加", diff --git a/packages/node_modules/@node-red/nodes/package.json b/packages/node_modules/@node-red/nodes/package.json index 93a8c9593..2c6aeb307 100644 --- a/packages/node_modules/@node-red/nodes/package.json +++ b/packages/node_modules/@node-red/nodes/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/nodes", - "version": "2.2.0", + "version": "2.2.2", "license": "Apache-2.0", "repository": { "type": "git", @@ -17,12 +17,12 @@ "dependencies": { "acorn": "8.7.0", "acorn-walk": "8.2.0", - "ajv": "8.9.0", + "ajv": "8.10.0", "body-parser": "1.19.1", "cheerio": "1.0.0-rc.10", "content-type": "1.0.4", "cookie-parser": "1.4.6", - "cookie": "0.4.1", + "cookie": "0.4.2", "cors": "2.8.5", "cronosjs": "1.7.1", "denque": "2.0.1", @@ -36,11 +36,11 @@ "is-utf8": "0.2.1", "js-yaml": "3.14.1", "media-typer": "1.1.0", - "mqtt": "4.3.4", + "mqtt": "4.3.5", "multer": "1.4.4", "mustache": "4.2.0", "on-headers": "1.0.2", - "raw-body": "2.4.2", + "raw-body": "2.4.3", "tough-cookie": "4.0.0", "uuid": "8.3.2", "ws": "7.5.6", diff --git a/packages/node_modules/@node-red/registry/lib/loader.js b/packages/node_modules/@node-red/registry/lib/loader.js index 6e2e4d8d8..fc508aa57 100644 --- a/packages/node_modules/@node-red/registry/lib/loader.js +++ b/packages/node_modules/@node-red/registry/lib/loader.js @@ -353,7 +353,6 @@ async function loadPluginConfig(fileInfo) { */ function loadNodeSet(node) { if (!node.enabled) { - console.log("BAIL ON",node.id) return Promise.resolve(node); } else { } diff --git a/packages/node_modules/@node-red/registry/package.json b/packages/node_modules/@node-red/registry/package.json index d332591bc..e96d8e396 100644 --- a/packages/node_modules/@node-red/registry/package.json +++ b/packages/node_modules/@node-red/registry/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/registry", - "version": "2.2.0", + "version": "2.2.2", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,11 +16,11 @@ } ], "dependencies": { - "@node-red/util": "2.2.0", + "@node-red/util": "2.2.2", "clone": "2.1.2", "fs-extra": "10.0.0", "semver": "7.3.5", "tar": "6.1.11", - "uglify-js": "3.15.0" + "uglify-js": "3.15.1" } } diff --git a/packages/node_modules/@node-red/runtime/lib/flows/util.js b/packages/node_modules/@node-red/runtime/lib/flows/util.js index be436eb22..0f3435b77 100644 --- a/packages/node_modules/@node-red/runtime/lib/flows/util.js +++ b/packages/node_modules/@node-red/runtime/lib/flows/util.js @@ -77,15 +77,16 @@ function createNode(flow,config) { if (typeof nodeTypeConstructor === "function") { var conf = clone(config); delete conf.credentials; - for (var p in conf) { - if (conf.hasOwnProperty(p)) { - mapEnvVarProperties(conf,p,flow,conf); - } - } try { Object.defineProperty(conf,'_module', {value: typeRegistry.getNodeInfo(type), enumerable: false, writable: true }) Object.defineProperty(conf,'_flow', {value: flow, enumerable: false, writable: true }) Object.defineProperty(conf,'_path', {value: `${flow.path}/${config._alias||config.id}`, enumerable: false, writable: true }) + + for (var p in conf) { + if (conf.hasOwnProperty(p)) { + mapEnvVarProperties(conf,p,flow,conf); + } + } newNode = new nodeTypeConstructor(conf); } catch (err) { Log.log({ diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js index 837e36386..f20359b8a 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/Node.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/Node.js @@ -503,10 +503,25 @@ function log_helper(self, level, msg) { o.name = self.name; } // See https://github.com/node-red/node-red/issues/3327 + // See https://github.com/node-red/node-red/issues/3389 + + let srcError; + if (msg instanceof Error) { + srcError = msg;//use existing err object for actual stack + } else { + srcError = new Error(msg);//generate a new error for generate a stack + } try { - self._flow.log(o); + if(self instanceof Node && self._flow) { + self._flow.log(o); + } else { + //if self._flow is not present, this is not a node-red Node + //Set info to "Node object is not a node-red Node" to point out the `Node type` problem in log + logUnexpectedError(self, srcError, "Node object is not a node-red Node") + } } catch(err) { - logUnexpectedError(self, err) + //build an unexpected error report indicating using the original error (for better stack trace) + logUnexpectedError(self, srcError, `An error occured attempting to make a log entry: ${err}`) } } /** @@ -531,7 +546,7 @@ Node.prototype.error = function(logMessage,msg) { logMessage = logMessage || ""; } var handled = false; - if (msg && typeof msg === 'object') { + if (this._flow && msg && typeof msg === 'object') { handled = this._flow.handleError(this,logMessage,msg); } if (!handled) { @@ -619,27 +634,34 @@ function inspectObject(flow) { } } -function logUnexpectedError(node, error) { - let moduleInfo = node._module?`${node._module.module}@${node._module.version}`:"undefined" - Log.error(` +function logUnexpectedError(node, error, info) { + const header = ` ******************************************************************** Unexpected Node Error -${error.stack} -Node: - Type: ${node.type} - Module: ${moduleInfo} - ID: ${node._alias||node.id} - Properties: - ${inspectObject(node)} -Flow: ${node._flow?node._flow.path:'undefined'} - Type: ${node._flow?node._flow.TYPE:'undefined'} - Properties: - ${node._flow?inspectObject(node._flow):'undefined'} +********************************************************************`; + const footer = ` Please report this issue, including the information logged above: https://github.com/node-red/node-red/issues/ -******************************************************************** -`) +********************************************************************`; + + let detail = [`Info:\n ${info || 'No additional info'}`]; + + //Include Error info? + if(error && error.stack){ + detail.push(`Stack:\n ${error.stack}`) + } + //Include Node info? + if(node && (node._module || node.type)){ + const moduleInfo = node._module?`${node._module.module}@${node._module.version}`:"undefined"; + const id = node._alias||node.id||"undefined"; + detail.push(`Node:\n Type: ${node.type}\n Module: ${moduleInfo}\n ID: ${id}\n Properties:\n ${inspectObject(node)}`) + } + //Include Flow info? + if(node && node._flow){ + detail.push(`Flow: ${node._flow.path}\n Type: ${node._flow.TYPE}\n Properties:\n ${inspectObject(node._flow)}`) + } + Log.error(`${header}\n${detail.join("\n")}\n${footer}`); } module.exports = Node; diff --git a/packages/node_modules/@node-red/runtime/package.json b/packages/node_modules/@node-red/runtime/package.json index 3062b17f9..c06ab74bf 100644 --- a/packages/node_modules/@node-red/runtime/package.json +++ b/packages/node_modules/@node-red/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/runtime", - "version": "2.2.0", + "version": "2.2.2", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/registry": "2.2.0", - "@node-red/util": "2.2.0", + "@node-red/registry": "2.2.2", + "@node-red/util": "2.2.2", "async-mutex": "0.3.2", "clone": "2.1.2", "express": "4.17.2", diff --git a/packages/node_modules/@node-red/util/lib/hooks.js b/packages/node_modules/@node-red/util/lib/hooks.js index 6dc1a37c9..d8fb6541c 100644 --- a/packages/node_modules/@node-red/util/lib/hooks.js +++ b/packages/node_modules/@node-red/util/lib/hooks.js @@ -67,8 +67,25 @@ function add(hookId, callback) { throw new Error("Hook "+hookId+" already registered") } // Get location of calling code + let callModule; const stack = new Error().stack; - const callModule = stack.split("\n")[2].split("(")[1].slice(0,-1); + const stackEntries = stack.split("\n").slice(1);//drop 1st line (error message) + const stackEntry2 = stackEntries[1];//get 2nd stack entry + if (stackEntry2) { + try { + if (stackEntry2.indexOf(" (") >= 0) { + callModule = stackEntry2.split("(")[1].slice(0, -1); + } else { + callModule = stackEntry2.split(" ").slice(-1)[0]; + } + } catch (error) { + Log.debug(`Unable to determined module when adding hook '${hookId}'. Stack:\n${stackEntries.join("\n")}`); + callModule = "unknown:0:0"; + } + } else { + Log.debug(`Unable to determined module when adding hook '${hookId}'. Stack:\n${stackEntries.join("\n")}`); + callModule = "unknown:0:0"; + } Log.debug(`Adding hook '${hookId}' from ${callModule}`); const hookItem = {cb:callback, location: callModule, previousHook: null, nextHook: null } diff --git a/packages/node_modules/@node-red/util/lib/util.js b/packages/node_modules/@node-red/util/lib/util.js index 15f6b44d9..a6fec8f98 100644 --- a/packages/node_modules/@node-red/util/lib/util.js +++ b/packages/node_modules/@node-red/util/lib/util.js @@ -24,6 +24,17 @@ const jsonata = require("jsonata"); const moment = require("moment-timezone"); const safeJSONStringify = require("json-stringify-safe"); const util = require("util"); +const { hasOwnProperty } = Object.prototype; + +/** + * Safely returns the object construtor name. + * @return {String} the name of the object constructor if it exists, empty string otherwise. + */ +function constructorName(obj) { + // Note: This function could be replaced by optional chaining in Node.js 14+: + // obj?.constructor?.name + return obj && obj.constructor ? obj.constructor.name : ''; +} /** * Generates a psuedo-unique-random id. @@ -171,7 +182,7 @@ function compareObjects(obj1,obj2) { } for (var k in obj1) { /* istanbul ignore else */ - if (obj1.hasOwnProperty(k)) { + if (hasOwnProperty.call(obj1, k)) { if (!compareObjects(obj1[k],obj2[k])) { return false; } @@ -462,7 +473,7 @@ function setObjectProperty(msg,prop,value,createMissing) { for (var i=0;i 1 && ((typeof obj[key] !== "object" && typeof obj[key] !== "function") || obj[key] === null)) { // Break out early as we cannot create a property beneath // this type of value @@ -561,7 +572,7 @@ function getSetting(node, name, flow_) { * @memberof @node-red/util_util */ function evaluateEnvProperty(value, node) { - var flow = (node && node.hasOwnProperty("_flow")) ? node._flow : null; + var flow = (node && hasOwnProperty.call(node, "_flow")) ? node._flow : null; var result; if (/^\${[^}]+}$/.test(value)) { // ${ENV_VAR} @@ -785,7 +796,7 @@ function normaliseNodeTypeName(name) { function encodeObject(msg,opts) { try { var debuglength = 1000; - if (opts && opts.hasOwnProperty('maxLength')) { + if (opts && hasOwnProperty.call(opts, 'maxLength')) { debuglength = opts.maxLength; } var msgType = typeof msg.msg; @@ -795,7 +806,7 @@ function encodeObject(msg,opts) { if (msg.msg.name) { errorMsg.name = msg.msg.name; } - if (msg.msg.hasOwnProperty('message')) { + if (hasOwnProperty.call(msg.msg, 'message')) { errorMsg.message = msg.msg.message; } else { errorMsg.message = msg.msg.toString(); @@ -809,7 +820,7 @@ function encodeObject(msg,opts) { } } else if (msg.msg && msgType === 'object') { try { - msg.format = msg.msg.constructor.name || "Object"; + msg.format = constructorName(msg.msg) || "Object"; // Handle special case of msg.req/res objects from HTTP In node if (msg.format === "IncomingMessage" || msg.format === "ServerResponse") { msg.format = "Object"; @@ -836,7 +847,7 @@ function encodeObject(msg,opts) { length: msg.msg.length } } - } else if (msg.msg && msg.msg.constructor.name === "Set") { + } else if (constructorName(msg.msg) === "Set") { msg.format = "set["+msg.msg.size+"]"; msg.msg = { __enc__: true, @@ -845,7 +856,7 @@ function encodeObject(msg,opts) { length: msg.msg.size } needsStringify = true; - } else if (msg.msg && msg.msg.constructor.name === "Map") { + } else if (constructorName(msg.msg) === "Map") { msg.format = "map"; msg.msg = { __enc__: true, @@ -854,7 +865,7 @@ function encodeObject(msg,opts) { length: msg.msg.size } needsStringify = true; - } else if (msg.msg && msg.msg.constructor.name === "RegExp") { + } else if (constructorName(msg.msg) === "RegExp") { msg.format = 'regexp'; msg.msg = msg.msg.toString(); } @@ -904,25 +915,25 @@ function encodeObject(msg,opts) { if (value.length > debuglength) { value.data = value.data.slice(0,debuglength); } - } else if (value.constructor.name === "ServerResponse") { + } else if (constructorName(value) === "ServerResponse") { value = "[internal]" - } else if (value.constructor.name === "Socket") { + } else if (constructorName(value) === "Socket") { value = "[internal]" - } else if (value.constructor.name === "Set") { + } else if (constructorName(value) === "Set") { value = { __enc__: true, type: "set", data: Array.from(value).slice(0,debuglength), length: value.size } - } else if (value.constructor.name === "Map") { + } else if (constructorName(value) === "Map") { value = { __enc__: true, type: "map", data: Object.fromEntries(Array.from(value.entries()).slice(0,debuglength)), length: value.size } - } else if (value.constructor.name === "RegExp") { + } else if (constructorName(value) === "RegExp") { value = { __enc__: true, type: "regexp", @@ -974,7 +985,7 @@ function encodeObject(msg,opts) { if (e.name) { errorMsg.name = e.name; } - if (e.hasOwnProperty('message')) { + if (hasOwnProperty.call(e, 'message')) { errorMsg.message = 'encodeObject Error: ['+e.message + '] Value: '+util.inspect(msg.msg); } else { errorMsg.message = 'encodeObject Error: ['+e.toString() + '] Value: '+util.inspect(msg.msg); diff --git a/packages/node_modules/@node-red/util/package.json b/packages/node_modules/@node-red/util/package.json index e2cc4e3c2..b7d817f07 100644 --- a/packages/node_modules/@node-red/util/package.json +++ b/packages/node_modules/@node-red/util/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/util", - "version": "2.2.0", + "version": "2.2.2", "license": "Apache-2.0", "repository": { "type": "git", @@ -16,9 +16,9 @@ ], "dependencies": { "fs-extra": "10.0.0", - "i18next": "21.6.10", + "i18next": "21.6.11", "json-stringify-safe": "5.0.1", - "jsonata": "1.8.5", + "jsonata": "1.8.6", "lodash.clonedeep": "^4.5.0", "moment-timezone": "0.5.34" } diff --git a/packages/node_modules/node-red/package.json b/packages/node_modules/node-red/package.json index 8e80a3a10..cae0721af 100644 --- a/packages/node_modules/node-red/package.json +++ b/packages/node_modules/node-red/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "2.2.0", + "version": "2.2.2", "description": "Low-code programming for event-driven applications", "homepage": "http://nodered.org", "license": "Apache-2.0", @@ -31,15 +31,15 @@ "flow" ], "dependencies": { - "@node-red/editor-api": "2.2.0", - "@node-red/runtime": "2.2.0", - "@node-red/util": "2.2.0", - "@node-red/nodes": "2.2.0", + "@node-red/editor-api": "2.2.2", + "@node-red/runtime": "2.2.2", + "@node-red/util": "2.2.2", + "@node-red/nodes": "2.2.2", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", "express": "4.17.2", "fs-extra": "10.0.0", - "node-red-admin": "^2.2.1", + "node-red-admin": "^2.2.3", "nopt": "5.0.0", "semver": "7.3.5" }, diff --git a/test/nodes/core/common/20-inject_spec.js b/test/nodes/core/common/20-inject_spec.js index 8b0143ce9..cb14690d0 100644 --- a/test/nodes/core/common/20-inject_spec.js +++ b/test/nodes/core/common/20-inject_spec.js @@ -242,6 +242,142 @@ describe('inject node', function() { }); + it('inject name of node as environment variable by substitution ', function (done) { + var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_NODE_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper"}]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload", "NAME"); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject id of node as environment variable by substitution ', function (done) { + var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_NODE_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper"}]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload", "n1"); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject path of node as environment variable by substitution ', function (done) { + var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_NODE_PATH}", payloadType: "str", wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper"}]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload", "flow/n1"); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + + it('inject name of flow as environment variable by substitution ', function (done) { + var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_FLOW_NAME}", payloadType: "str", wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper"}, + {id: "flow", type: "tab", label: "FLOW" }, + ]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload", "FLOW"); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject id of flow as environment variable ', function (done) { + var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_FLOW_ID}", payloadType: "str", wires: [["n2"]], z: "flow"}, + {id: "n2", type: "helper"}, + {id: "flow", type: "tab", name: "FLOW" }, + ]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload", "flow"); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject name of group as environment variable by substitution ', function (done) { + var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_GROUP_NAME}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper"}, + {id: "g0", type: "group", name: "GROUP" }, + ]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload", "GROUP"); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('inject id of group as environment variable by substitution ', function (done) { + var flow = [{id: "n1", type: "inject", name: "NAME", topnic: "t1", payload: "${NR_GROUP_ID}", payloadType: "str", wires: [["n2"]], z: "flow", g: "g0"}, + {id: "n2", type: "helper"}, + {id: "g0", type: "group", name: "GROUP" }, + ]; + helper.load(injectNode, flow, function () { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n2.on("input", function (msg) { + try { + msg.should.have.property("payload", "g0"); + done(); + } catch (err) { + done(err); + } + }); + n1.receive({}); + }); + }); + + it('sets the value of flow context property', function (done) { var flow = [{id: "n1", type: "inject", topic: "t1", payload: "flowValue", payloadType: "flow", wires: [["n2"]], z: "flow"}, {id: "n2", type: "helper"}]; diff --git a/test/nodes/core/common/21-debug_spec.js b/test/nodes/core/common/21-debug_spec.js index ae0d9a48e..2931ea1e4 100644 --- a/test/nodes/core/common/21-debug_spec.js +++ b/test/nodes/core/common/21-debug_spec.js @@ -265,6 +265,38 @@ describe('debug node', function() { }); }); + it('should publish an object with no-prototype-builtins', function(done) { + const flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + const n1 = helper.getNode("n1"); + const payload = Object.create(null); + payload.type = 'foo'; + websocket_test(function() { + n1.emit("input", {payload: payload}); + }, function(msg) { + JSON.parse(msg).should.eql([{ + topic:"debug", + data:{id:"n1",msg:'{"type":"foo"}',property:"payload",format:"Object",path:"global"} + }]); + }, done); + }); + }); + + it('should publish an object with overriden hasOwnProperty', function(done) { + const flow = [{id:"n1", type:"debug" }]; + helper.load(debugNode, flow, function() { + const n1 = helper.getNode("n1"); + websocket_test(function() { + n1.emit("input", {payload: {type:'foo', hasOwnProperty: null}}); + }, function(msg) { + JSON.parse(msg).should.eql([{ + topic:"debug", + data:{id:"n1",msg:'{"type":"foo","hasOwnProperty":null}',property:"payload",format:"Object",path:"global"} + }]); + }, done); + }); + }); + it('should publish an array', function(done) { var flow = [{id:"n1", type:"debug" }]; helper.load(debugNode, flow, function() { diff --git a/test/nodes/core/function/10-switch_spec.js b/test/nodes/core/function/10-switch_spec.js index 180ec9d36..07896a9a4 100644 --- a/test/nodes/core/function/10-switch_spec.js +++ b/test/nodes/core/function/10-switch_spec.js @@ -310,6 +310,12 @@ describe('switch Node', function() { it('should check if payload if of type number 0', function(done) { genericSwitchTest("istype", "number", true, true, 0, done); }); + it('should check if payload if of type number NaN', function(done) { + genericSwitchTest("istype", "number", true, false, parseInt("banana"), done); + }); + it('should check if payload if of type number Infinity', function(done) { + genericSwitchTest("istype", "number", true, true, 1/0, done); + }); it('should check if payload if of type boolean true', function(done) { genericSwitchTest("istype", "boolean", true, true, true, done); }); diff --git a/test/unit/@node-red/util/lib/util_spec.js b/test/unit/@node-red/util/lib/util_spec.js index 5007fe5c5..2013f143e 100644 --- a/test/unit/@node-red/util/lib/util_spec.js +++ b/test/unit/@node-red/util/lib/util_spec.js @@ -830,6 +830,24 @@ describe("@node-red/util/util", function() { resultJson.b.should.have.property("__enc__", true); resultJson.b.should.have.property("type", "undefined"); }); + it('object with no prototype builtins', function() { + const payload = new Object(null); + payload.c = 3; + var msg = { msg:{b:payload} }; + var result = util.encodeObject(msg); + result.format.should.eql("Object"); + var resultJson = JSON.parse(result.msg); + resultJson.should.have.property("b"); + resultJson.b.should.have.property("c", 3); + }); + it('object with overriden hasOwnProperty', function() { + var msg = { msg:{b:{hasOwnProperty:null}} }; + var result = util.encodeObject(msg); + result.format.should.eql("Object"); + var resultJson = JSON.parse(result.msg); + resultJson.should.have.property("b"); + resultJson.b.should.have.property("hasOwnProperty"); + }); it('object with Map property', function() { const m = new Map(); m.set("a",1);