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

Merge branch 'master' into 0.16

This commit is contained in:
Nick O'Leary 2017-01-06 14:30:13 +00:00 committed by GitHub
commit e73216d4c1
27 changed files with 354 additions and 160 deletions

View File

@ -1,3 +1,38 @@
#### 0.15.3: Maintenance Release
- Tcpgetfix: Another small check (#1070)
- TCPGet: Ensure done() is called only once (#1068)
- Allow $ and _ at start of property identifiers Fixes #1063
- TCPGet: Separated the node.connected property for each instance (#1062)
- Corrected 'overide' typo in XML node help (#1061)
- TCPGet: Last property check (hopefully) (#1059)
- Add additional safety checks to avoid acting on non-existent objects (#1057)
- add --title for process name to command line options
- add indicator for fire once on inject node
- reimplement $(env var) replace to share common code.
- Fix error message for missing node html file, and add test.
- Let credentials also use $(...) substitutions from ENV
- Rename insecureRedirect to requireHttps
- Add setting to cause insecure redirect (#1054)
- Palette editor fixes (#1033)
- Close comms on stopServer in test helper (#1020)
- Tcpgetfix (#1050)
- TCPget: Store incoming messages alongside the client object to keep reference
- Merge remote-tracking branch 'upstream/master' into tcpgetfix
- TCPget can now handle concurrent sessions (#1042)
- Better scope handling
- Add security checks
- small change to udp httpadmin
- Fix comparison to "" in tcpin
- Change scope of clients object
- Works when connection is left open
- First release of multi connection tcpget
- Fix node.error() not printing when passed false (#1037)
- fix test for CSV array input
- different test for Pi (rather than use serial port name)
- Fix missing 0 handling for css node with array input
#### 0.15.2: Maintenance Release #### 0.15.2: Maintenance Release
- Revert bidi changes to nodes and hide menu option until fixed Fixes #1024 - Revert bidi changes to nodes and hide menu option until fixed Fixes #1024

View File

@ -38,21 +38,13 @@ If you want to raise a pull-request with a new feature, or a refactoring
of existing code, it may well get rejected if you haven't discussed it on of existing code, it may well get rejected if you haven't discussed it on
the [mailing list](https://groups.google.com/forum/#!forum/node-red) first. the [mailing list](https://groups.google.com/forum/#!forum/node-red) first.
### Contributor License Agreement All contributors need to sign the JS Foundation's Contributor License Agreement.
It is an online process and quick to do. You can read the details of the agreement
here: https://cla.js.foundation/node-red/node-red.
In order for us to accept pull-requests, the contributor must first complete If you raise a pull-request without having signed the CLA, you will be prompted
a Contributor License Agreement (CLA). This clarifies the intellectual to do so automatically.
property license granted with any contribution. It is for your protection as a
Contributor as well as the protection of IBM and its customers; it does not
change your rights to use your own Contributions for any other purpose.
You can download the CLAs here:
- [individual](http://nodered.org/cla/node-red-cla-individual.pdf)
- [corporate](http://nodered.org/cla/node-red-cla-corporate.pdf)
If you are an IBMer, please contact us directly as the contribution process is
slightly different.
### Coding standards ### Coding standards

View File

@ -14,9 +14,6 @@
* limitations under the License. * limitations under the License.
**/ **/
(function($) { (function($) {
var allOptions = { var allOptions = {
msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression}, msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression},
flow: {value:"flow",label:"flow.",validate:RED.utils.validatePropertyExpression}, flow: {value:"flow",label:"flow.",validate:RED.utils.validatePropertyExpression},

View File

@ -255,7 +255,9 @@ RED.palette.editor = (function() {
nodeEntry.removeButton.hide(); nodeEntry.removeButton.hide();
} else { } else {
nodeEntry.enableButton.removeClass('disabled'); nodeEntry.enableButton.removeClass('disabled');
if (moduleInfo.local) {
nodeEntry.removeButton.show(); nodeEntry.removeButton.show();
}
if (activeTypeCount === 0) { if (activeTypeCount === 0) {
nodeEntry.enableButton.html(RED._('palette.editor.enableall')); nodeEntry.enableButton.html(RED._('palette.editor.enableall'));
} else { } else {

View File

@ -201,6 +201,11 @@
icon: "inject.png", icon: "inject.png",
label: function() { label: function() {
var suffix = ""; var suffix = "";
// if fire once then add small indication
if (this.once) {
suffix = " ¹";
}
// but replace with repeat one if set to repeat
if (this.repeat || this.crontab) { if (this.repeat || this.crontab) {
suffix = " ↻"; suffix = " ↻";
} }

View File

@ -23,9 +23,9 @@ module.exports = function(RED) {
var gpioCommand = __dirname+'/nrgpio'; var gpioCommand = __dirname+'/nrgpio';
try { try {
fs.statSync("/dev/ttyAMA0"); // unlikely if not on a Pi var cpuinfo = fs.readFileSync("/proc/cpuinfo").toString();
if (cpuinfo.indexOf(": BCM") === -1) { throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); }
} catch(err) { } catch(err) {
//RED.log.info(RED._("rpi-gpio.errors.ignorenode"));
throw "Info : "+RED._("rpi-gpio.errors.ignorenode"); throw "Info : "+RED._("rpi-gpio.errors.ignorenode");
} }

View File

@ -83,7 +83,7 @@ module.exports = function(RED) {
} }
}); });
client.on('end', function() { client.on('end', function() {
if (!node.stream || (node.datatype == "utf8" && node.newline != "" && buffer.length > 0)) { if (!node.stream || (node.datatype == "utf8" && node.newline !== "" && buffer.length > 0)) {
var msg = {topic:node.topic, payload:buffer}; var msg = {topic:node.topic, payload:buffer};
msg._session = {type:"tcp",id:id}; msg._session = {type:"tcp",id:id};
if (buffer.length !== 0) { if (buffer.length !== 0) {
@ -407,6 +407,28 @@ module.exports = function(RED) {
} // jshint ignore:line } // jshint ignore:line
} }
var node = this;
var clients = {};
this.on("input", function(msg) {
var i = 0;
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
msg.payload = msg.payload.toString();
}
var host = node.server || msg.host;
var port = node.port || msg.port;
// Store client information independently
// the clients object will have:
// clients[id].client, clients[id].msg, clients[id].timeout
var connection_id = host + ":" + port;
clients[connection_id] = clients[connection_id] || {};
clients[connection_id].msg = msg;
clients[connection_id].connected = clients[connection_id].connected || false;
if (!clients[connection_id].connected) {
var buf; var buf;
if (this.out == "count") { if (this.out == "count") {
if (this.splitc === 0) { buf = new Buffer(1); } if (this.splitc === 0) { buf = new Buffer(1); }
@ -414,136 +436,178 @@ module.exports = function(RED) {
} }
else { buf = new Buffer(65536); } // set it to 64k... hopefully big enough for most TCP packets.... but only hopefully else { buf = new Buffer(65536); } // set it to 64k... hopefully big enough for most TCP packets.... but only hopefully
this.connected = false; clients[connection_id].client = net.Socket();
var node = this; if (socketTimeout !== null) { clients[connection_id].client.setTimeout(socketTimeout);}
var client;
var m;
this.on("input", function(msg) {
m = msg;
var i = 0;
if ((!Buffer.isBuffer(msg.payload)) && (typeof msg.payload !== "string")) {
msg.payload = msg.payload.toString();
}
if (!node.connected) {
client = net.Socket();
if (socketTimeout !== null) { client.setTimeout(socketTimeout); }
var host = node.server || msg.host;
var port = node.port || msg.port;
if (host && port) { if (host && port) {
client.connect(port, host, function() { clients[connection_id].client.connect(port, host, function() {
//node.log(RED._("tcpin.errors.client-connected")); //node.log(RED._("tcpin.errors.client-connected"));
node.status({fill:"green",shape:"dot",text:"common.status.connected"}); node.status({fill:"green",shape:"dot",text:"common.status.connected"});
node.connected = true; if (clients[connection_id] && clients[connection_id].client) {
client.write(msg.payload); clients[connection_id].connected = true;
clients[connection_id].client.write(clients[connection_id].msg.payload);
}
}); });
} }
else { else {
node.warn(RED._("tcpin.errors.no-host")); node.warn(RED._("tcpin.errors.no-host"));
} }
client.on('data', function(data) { clients[connection_id].client.on('data', function(data) {
if (node.out == "sit") { // if we are staying connected just send the buffer if (node.out == "sit") { // if we are staying connected just send the buffer
m.payload = data; if (clients[connection_id]) {
node.send(m); clients[connection_id].msg.payload = data;
node.send(clients[connection_id].msg);
}
} }
else if (node.splitc === 0) { else if (node.splitc === 0) {
msg.payload = data; clients[connection_id].msg.payload = data;
node.send(msg); node.send(clients[connection_id].msg);
} }
else { else {
for (var j = 0; j < data.length; j++ ) { for (var j = 0; j < data.length; j++ ) {
if (node.out === "time") { if (node.out === "time") {
if (clients[connection_id]) {
// do the timer thing // do the timer thing
if (node.tout) { if (clients[connection_id].timeout) {
i += 1; i += 1;
buf[i] = data[j]; buf[i] = data[j];
} }
else { else {
node.tout = setTimeout(function () { clients[connection_id].timeout = setTimeout(function () {
node.tout = null; if (clients[connection_id]) {
msg.payload = new Buffer(i+1); clients[connection_id].timeout = null;
buf.copy(msg.payload,0,0,i+1); clients[connection_id].msg.payload = new Buffer(i+1);
node.send(msg); buf.copy(clients[connection_id].msg.payload,0,0,i+1);
if (client) { node.status({}); client.destroy(); } node.send(clients[connection_id].msg);
if (clients[connection_id].client) {
node.status({}); clients[connection_id].client.destroy();
delete clients[connection_id];
}
}
}, node.splitc); }, node.splitc);
i = 0; i = 0;
buf[0] = data[j]; buf[0] = data[j];
} }
} }
}
// count bytes into a buffer... // count bytes into a buffer...
else if (node.out == "count") { else if (node.out == "count") {
buf[i] = data[j]; buf[i] = data[j];
i += 1; i += 1;
if ( i >= node.splitc) { if ( i >= node.splitc) {
msg.payload = new Buffer(i); if (clients[connection_id]) {
buf.copy(msg.payload,0,0,i); clients[connection_id].msg.payload = new Buffer(i);
node.send(msg); buf.copy(clients[connection_id].msg.payload,0,0,i);
if (client) { node.status({}); client.destroy(); } node.send(clients[connection_id].msg);
if (clients[connection_id].client) {
node.status({}); clients[connection_id].client.destroy();
delete clients[connection_id];
}
i = 0; i = 0;
} }
} }
}
// look for a char // look for a char
else { else {
buf[i] = data[j]; buf[i] = data[j];
i += 1; i += 1;
if (data[j] == node.splitc) { if (data[j] == node.splitc) {
msg.payload = new Buffer(i); if (clients[connection_id]) {
buf.copy(msg.payload,0,0,i); clients[connection_id].msg.payload = new Buffer(i);
node.send(msg); buf.copy(clients[connection_id].msg.payload,0,0,i);
if (client) { node.status({}); client.destroy(); } node.send(clients[connection_id].msg);
if (clients[connection_id].client) {
node.status({}); clients[connection_id].client.destroy();
delete clients[connection_id];
}
i = 0; i = 0;
} }
} }
} }
} }
}
}); });
client.on('end', function() { clients[connection_id].client.on('end', function() {
//console.log("END"); //console.log("END");
node.connected = false;
node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"}); node.status({fill:"grey",shape:"ring",text:"common.status.disconnected"});
client = null; if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = false;
clients[connection_id].client = null;
}
}); });
client.on('close', function() { clients[connection_id].client.on('close', function() {
//console.log("CLOSE"); //console.log("CLOSE");
node.connected = false; if (clients[connection_id]) {
if (node.done) { node.done(); } clients[connection_id].connected = false;
}
var anyConnected = false;
for (var client in clients) {
if (clients[client].connected) {
anyConnected = true;
break;
}
}
if (node.done && !anyConnected) {
clients = {};
node.done();
}
}); });
client.on('error', function() { clients[connection_id].client.on('error', function() {
//console.log("ERROR"); //console.log("ERROR");
node.connected = false;
node.status({fill:"red",shape:"ring",text:"common.status.error"}); node.status({fill:"red",shape:"ring",text:"common.status.error"});
node.error(RED._("tcpin.errors.connect-fail"),msg); node.error(RED._("tcpin.errors.connect-fail") + " " + connection_id, msg);
if (client) { client.destroy(); } if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].connected = false;
clients[connection_id].client.destroy();
delete clients[connection_id];
}
}); });
client.on('timeout',function() { clients[connection_id].client.on('timeout',function() {
//console.log("TIMEOUT"); //console.log("TIMEOUT");
node.connected = false; clients[connection_id].connected = false;
node.status({fill:"grey",shape:"dot",text:"tcpin.errors.connect-timeout"}); node.status({fill:"grey",shape:"dot",text:"tcpin.errors.connect-timeout"});
//node.warn(RED._("tcpin.errors.connect-timeout")); //node.warn(RED._("tcpin.errors.connect-timeout"));
if (client) { if (clients[connection_id] && clients[connection_id].client) {
client.connect(port, host, function() { clients[connection_id].client.connect(port, host, function() {
node.connected = true; clients[connection_id].connected = true;
node.status({fill:"green",shape:"dot",text:"common.status.connected"}); node.status({fill:"green",shape:"dot",text:"common.status.connected"});
}); });
} }
}); });
} }
else { client.write(msg.payload); } else {
if (clients[connection_id] && clients[connection_id].client) {
clients[connection_id].client.write(clients[connection_id].msg.payload);
}
}
}); });
this.on("close", function(done) { this.on("close", function(done) {
node.done = done; node.done = done;
if (client) { for (var client in clients) {
client.destroy(); clients[client].client.destroy();
} }
node.status({}); node.status({});
if (!node.connected) { done(); }
var anyConnected = false;
for (var c in clients) {
if (clients[c].connected) {
anyConnected = true;
break;
}
}
if (!anyConnected) {
clients = {};
done();
}
}); });
} }

View File

@ -102,7 +102,7 @@ module.exports = function(RED) {
try { server.bind(node.port,node.iface); } try { server.bind(node.port,node.iface); }
catch(e) { } // Don't worry if already bound catch(e) { } // Don't worry if already bound
} }
RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-in.read'), function(req,res) { RED.httpAdmin.get('/udp-ports/:id', RED.auth.needsPermission('udp-ports.read'), function(req,res) {
res.json(Object.keys(udpInputPortsInUse)); res.json(Object.keys(udpInputPortsInUse));
}); });
RED.nodes.registerType("udp in",UDPin); RED.nodes.registerType("udp in",UDPin);

View File

@ -36,7 +36,7 @@
<p>A function that parses the <code>msg.payload</code> to convert xml to/from a javascript object. Places the result in the payload.</p> <p>A function that parses the <code>msg.payload</code> to convert xml to/from a javascript object. Places the result in the payload.</p>
<p>If the input is a string it tries to parse it as XML and creates a javascript object.</p> <p>If the input is a string it tries to parse it as XML and creates a javascript object.</p>
<p>If the input is a javascript object it tries to build an XML string.</p> <p>If the input is a javascript object it tries to build an XML string.</p>
<p>You can also pass in a <code>msg.options</code> object to overide all the multitude of parameters. See <p>You can also pass in a <code>msg.options</code> object to override all the multitude of parameters. See
<a href="https://github.com/Leonidas-from-XIV/node-xml2js/blob/master/README.md#options" target="_blank">the xml2js docs</a> <a href="https://github.com/Leonidas-from-XIV/node-xml2js/blob/master/README.md#options" target="_blank">the xml2js docs</a>
for more information.</p> for more information.</p>
<p>If set, options in the edit dialogue override those passed in on the msg.options object.</p> <p>If set, options in the edit dialogue override those passed in on the msg.options object.</p>

View File

@ -1,6 +1,6 @@
{ {
"name" : "node-red", "name" : "node-red",
"version" : "0.15.2", "version" : "0.15.3",
"description" : "A visual tool for wiring the Internet of Things", "description" : "A visual tool for wiring the Internet of Things",
"homepage" : "http://nodered.org", "homepage" : "http://nodered.org",
"license" : "Apache-2.0", "license" : "Apache-2.0",

36
red.js
View File

@ -33,17 +33,20 @@ var settingsFile;
var flowFile; var flowFile;
var knownOpts = { var knownOpts = {
"settings":[path], "help": Boolean,
"userDir":[path],
"port": Number, "port": Number,
"v": Boolean, "settings": [path],
"help": Boolean "title": String,
"userDir": [path],
"verbose": Boolean
}; };
var shortHands = { var shortHands = {
"s":["--settings"], "?":["--help"],
"u":["--userDir"],
"p":["--port"], "p":["--port"],
"?":["--help"] "s":["--settings"],
"t":["--help"],
"u":["--userDir"],
"v":["--verbose"]
}; };
nopt.invalidHandler = function(k,v,t) { nopt.invalidHandler = function(k,v,t) {
// TODO: console.log(k,v,t); // TODO: console.log(k,v,t);
@ -54,14 +57,15 @@ var parsedArgs = nopt(knownOpts,shortHands,process.argv,2)
if (parsedArgs.help) { if (parsedArgs.help) {
console.log("Node-RED v"+RED.version()); console.log("Node-RED v"+RED.version());
console.log("Usage: node-red [-v] [-?] [--settings settings.js] [--userDir DIR]"); console.log("Usage: node-red [-v] [-?] [--settings settings.js] [--userDir DIR]");
console.log(" [--port PORT] [flows.json]"); console.log(" [--port PORT] [--title TITLE] [flows.json]");
console.log(""); console.log("");
console.log("Options:"); console.log("Options:");
console.log(" -s, --settings FILE use specified settings file");
console.log(" -u, --userDir DIR use specified user directory");
console.log(" -p, --port PORT port to listen on"); console.log(" -p, --port PORT port to listen on");
console.log(" -v enable verbose output"); console.log(" -s, --settings FILE use specified settings file");
console.log(" -?, --help show usage"); console.log(" --title TITLE process window title");
console.log(" -u, --userDir DIR use specified user directory");
console.log(" -v, --verbose enable verbose output");
console.log(" -?, --help show this help");
console.log(""); console.log("");
console.log("Documentation can be found at http://nodered.org"); console.log("Documentation can be found at http://nodered.org");
process.exit(); process.exit();
@ -121,9 +125,9 @@ if (parsedArgs.v) {
} }
if (settings.https) { if (settings.https) {
server = https.createServer(settings.https,function(req,res){app(req,res);}); server = https.createServer(settings.https,function(req,res) {app(req,res);});
} else { } else {
server = http.createServer(function(req,res){app(req,res);}); server = http.createServer(function(req,res) {app(req,res);});
} }
server.setMaxListeners(0); server.setMaxListeners(0);
@ -224,7 +228,6 @@ if (settings.httpNodeRoot !== false && settings.httpNodeAuth) {
if (settings.httpNodeRoot !== false) { if (settings.httpNodeRoot !== false) {
app.use(settings.httpNodeRoot,RED.httpNode); app.use(settings.httpNodeRoot,RED.httpNode);
} }
if (settings.httpStatic) { if (settings.httpStatic) {
settings.httpStaticAuth = settings.httpStaticAuth || settings.httpAuth; settings.httpStaticAuth = settings.httpStaticAuth || settings.httpAuth;
if (settings.httpStaticAuth) { if (settings.httpStaticAuth) {
@ -265,7 +268,7 @@ RED.start().then(function() {
if (settings.httpAdminRoot === false) { if (settings.httpAdminRoot === false) {
RED.log.info(RED.log._("server.admin-ui-disabled")); RED.log.info(RED.log._("server.admin-ui-disabled"));
} }
process.title = 'node-red'; process.title = parsedArgs.title || 'node-red';
RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()})); RED.log.info(RED.log._("server.now-running", {listenpath:getListenPath()}));
}); });
} else { } else {
@ -280,7 +283,6 @@ RED.start().then(function() {
} }
}); });
process.on('uncaughtException',function(err) { process.on('uncaughtException',function(err) {
util.log('[red] Uncaught Exception:'); util.log('[red] Uncaught Exception:');
if (err.stack) { if (err.stack) {

View File

@ -87,6 +87,16 @@ function init(_server,_runtime) {
if (!settings.disableEditor) { if (!settings.disableEditor) {
ui.init(runtime); ui.init(runtime);
var editorApp = express(); var editorApp = express();
if (settings.requireHttps === true) {
editorApp.enable('trust proxy');
editorApp.use(function (req, res, next) {
if (req.secure) {
next();
} else {
res.redirect('https://' + req.headers.host + req.originalUrl);
}
});
}
editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor); editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor);
editorApp.get("/icons/:icon",ui.icon); editorApp.get("/icons/:icon",ui.icon);
theme.init(runtime); theme.init(runtime);

View File

@ -236,7 +236,9 @@ Node.prototype.warn = function(msg) {
}; };
Node.prototype.error = function(logMessage,msg) { Node.prototype.error = function(logMessage,msg) {
if (typeof logMessage != 'boolean') {
logMessage = logMessage || ""; logMessage = logMessage || "";
}
log_helper(this, Log.ERROR, logMessage); log_helper(this, Log.ERROR, logMessage);
/* istanbul ignore else */ /* istanbul ignore else */
if (msg) { if (msg) {

View File

@ -259,32 +259,6 @@ function Flow(global,flow) {
} }
} }
} }
}
var EnvVarPropertyRE = /^\$\((\S+)\)$/;
function mapEnvVarProperties(obj,prop) {
if (Buffer.isBuffer(obj[prop])) {
return;
} else if (Array.isArray(obj[prop])) {
for (var i=0;i<obj[prop].length;i++) {
mapEnvVarProperties(obj[prop],i);
}
} else if (typeof obj[prop] === 'string') {
var m;
if ( (m = EnvVarPropertyRE.exec(obj[prop])) !== null) {
if (process.env.hasOwnProperty(m[1])) {
obj[prop] = process.env[m[1]];
}
}
} else {
for (var p in obj[prop]) {
if (obj[prop].hasOwnProperty(p)) {
mapEnvVarProperties(obj[prop],p);
}
}
}
} }
function createNode(type,config) { function createNode(type,config) {
@ -295,7 +269,7 @@ function createNode(type,config) {
delete conf.credentials; delete conf.credentials;
for (var p in conf) { for (var p in conf) {
if (conf.hasOwnProperty(p)) { if (conf.hasOwnProperty(p)) {
mapEnvVarProperties(conf,p); flowUtil.mapEnvVarProperties(conf,p);
} }
} }
try { try {

View File

@ -37,9 +37,35 @@ function diffNodes(oldNode,newNode) {
return false; return false;
} }
var EnvVarPropertyRE = /^\$\((\S+)\)$/;
function mapEnvVarProperties(obj,prop) {
if (Buffer.isBuffer(obj[prop])) {
return;
} else if (Array.isArray(obj[prop])) {
for (var i=0;i<obj[prop].length;i++) {
mapEnvVarProperties(obj[prop],i);
}
} else if (typeof obj[prop] === 'string') {
var m;
if ( (m = EnvVarPropertyRE.exec(obj[prop])) !== null) {
if (process.env.hasOwnProperty(m[1])) {
obj[prop] = process.env[m[1]];
}
}
} else {
for (var p in obj[prop]) {
if (obj[prop].hasOwnProperty(p)) {
mapEnvVarProperties(obj[prop],p);
}
}
}
}
module.exports = { module.exports = {
diffNodes: diffNodes, diffNodes: diffNodes,
mapEnvVarProperties: mapEnvVarProperties,
parseConfig: function(config) { parseConfig: function(config) {
var flow = {}; var flow = {};
@ -301,7 +327,7 @@ module.exports = {
} }
} }
} }
} while(madeChange===true) } while (madeChange===true)
// Find any nodes that exist on a subflow template and remove from changed // Find any nodes that exist on a subflow template and remove from changed
// list as the parent subflow will now be marked as containing a change // list as the parent subflow will now be marked as containing a change
@ -316,7 +342,7 @@ module.exports = {
// Recursively mark all instances of changed subflows as changed // Recursively mark all instances of changed subflows as changed
var changedSubflowStack = Object.keys(changedSubflows); var changedSubflowStack = Object.keys(changedSubflows);
while(changedSubflowStack.length > 0) { while (changedSubflowStack.length > 0) {
var subflowId = changedSubflowStack.pop(); var subflowId = changedSubflowStack.pop();
for (id in newConfig.allNodes) { for (id in newConfig.allNodes) {
if (newConfig.allNodes.hasOwnProperty(id)) { if (newConfig.allNodes.hasOwnProperty(id)) {
@ -350,7 +376,7 @@ module.exports = {
// Traverse the links of all modified nodes to mark the connected nodes // Traverse the links of all modified nodes to mark the connected nodes
var modifiedNodes = diff.added.concat(diff.changed).concat(diff.removed).concat(diff.rewired); var modifiedNodes = diff.added.concat(diff.changed).concat(diff.removed).concat(diff.rewired);
var visited = {}; var visited = {};
while(modifiedNodes.length > 0) { while (modifiedNodes.length > 0) {
node = modifiedNodes.pop(); node = modifiedNodes.pop();
if (!visited[node]) { if (!visited[node]) {
visited[node] = true; visited[node] = true;

View File

@ -21,6 +21,7 @@ var fs = require("fs");
var registry = require("./registry"); var registry = require("./registry");
var credentials = require("./credentials"); var credentials = require("./credentials");
var flows = require("./flows"); var flows = require("./flows");
var flowUtil = require("./flows/util")
var context = require("./context"); var context = require("./context");
var Node = require("./Node"); var Node = require("./Node");
var log = require("../log"); var log = require("../log");
@ -69,6 +70,12 @@ function createNode(node,def) {
var creds = credentials.get(id); var creds = credentials.get(id);
if (creds) { if (creds) {
//console.log("Attaching credentials to ",node.id); //console.log("Attaching credentials to ",node.id);
// allow $(foo) syntax to substitute env variables for credentials also...
for (var p in creds) {
if (creds.hasOwnProperty(p)) {
flowUtil.mapEnvVarProperties(creds,p);
}
}
node.credentials = creds; node.credentials = creds;
} else if (credentials.getDefinition(node.type)) { } else if (credentials.getDefinition(node.type)) {
node.credentials = {}; node.credentials = {};
@ -146,7 +153,6 @@ module.exports = {
// disableFlow: flows.disableFlow, // disableFlow: flows.disableFlow,
// enableFlow: flows.enableFlow, // enableFlow: flows.enableFlow,
// Credentials // Credentials
addCredentials: credentials.add, addCredentials: credentials.add,
getCredentials: credentials.get, getCredentials: credentials.get,

View File

@ -87,10 +87,10 @@ function createNodeApi(node) {
red.server = runtime.adminApi.server; red.server = runtime.adminApi.server;
} else { } else {
red.comms = { red.comms = {
publish: function(){} publish: function() {}
}; };
red.library = { red.library = {
register: function(){} register: function() {}
}; };
red.auth = { red.auth = {
needsPermission: function() {} needsPermission: function() {}
@ -203,7 +203,7 @@ function loadNodeConfig(fileInfo) {
if (!node.types) { if (!node.types) {
node.types = []; node.types = [];
} }
node.err = "Error: "+file+" does not exist"; node.err = "Error: "+node.template+" does not exist";
} else { } else {
node.types = []; node.types = [];
node.err = err.toString(); node.err = err.toString();
@ -215,7 +215,7 @@ function loadNodeConfig(fileInfo) {
var regExp = /<script ([^>]*)data-template-name=['"]([^'"]*)['"]/gi; var regExp = /<script ([^>]*)data-template-name=['"]([^'"]*)['"]/gi;
var match = null; var match = null;
while((match = regExp.exec(content)) !== null) { while ((match = regExp.exec(content)) !== null) {
types.push(match[2]); types.push(match[2]);
} }
node.types = types; node.types = types;
@ -226,7 +226,7 @@ function loadNodeConfig(fileInfo) {
var mainContent = ""; var mainContent = "";
var helpContent = {}; var helpContent = {};
var index = 0; var index = 0;
while((match = regExp.exec(content)) !== null) { while ((match = regExp.exec(content)) !== null) {
mainContent += content.substring(index,regExp.lastIndex-match[1].length); mainContent += content.substring(index,regExp.lastIndex-match[1].length);
index = regExp.lastIndex; index = regExp.lastIndex;
var help = content.substring(regExp.lastIndex-match[1].length,regExp.lastIndex); var help = content.substring(regExp.lastIndex-match[1].length,regExp.lastIndex);
@ -382,7 +382,6 @@ function getNodeHelp(node,lang) {
} else { } else {
node.help[lang] = node.help[runtime.i18n.defaultLang]; node.help[lang] = node.help[runtime.i18n.defaultLang];
} }
} }
return node.help[lang]; return node.help[lang];
} }

View File

@ -281,6 +281,7 @@ function getModuleFiles(module) {
} }
nodeModuleFiles.forEach(function(node) { nodeModuleFiles.forEach(function(node) {
nodeList[moduleFile.package.name].nodes[node.name] = node; nodeList[moduleFile.package.name].nodes[node.name] = node;
nodeList[moduleFile.package.name].nodes[node.name].local = moduleFile.local || false;
}); });
}); });
return nodeList; return nodeList;

View File

@ -165,7 +165,7 @@ function normalisePropertyExpression(str) {
throw new Error("Invalid property expression: unterminated expression"); throw new Error("Invalid property expression: unterminated expression");
} }
// Next char is a-z // Next char is a-z
if (!/[a-z0-9]/i.test(str[i+1])) { if (!/[a-z0-9\$\_]/i.test(str[i+1])) {
throw new Error("Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1)); throw new Error("Invalid property expression: unexpected "+str[i+1]+" at position "+(i+1));
} }
start = i+1; start = i+1;

View File

@ -129,6 +129,10 @@ module.exports = {
// cert: fs.readFileSync('certificate.pem') // cert: fs.readFileSync('certificate.pem')
//}, //},
// The following property can be used to cause insecure HTTP connections to
// be redirected to HTTPS.
//requireHttps: true
// The following property can be used to disable the editor. The admin API // The following property can be used to disable the editor. The admin API
// is not affected by this option. To disable both the editor and the admin // is not affected by this option. To disable both the editor and the admin
// API, use either the httpRoot or httpAdminRoot properties // API, use either the httpRoot or httpAdminRoot properties

View File

@ -227,12 +227,12 @@ describe('CSV node', function() {
var n2 = helper.getNode("n2"); var n2 = helper.getNode("n2");
n2.on("input", function(msg) { n2.on("input", function(msg) {
try { try {
msg.should.have.property('payload', '1,2,3,4\n'); msg.should.have.property('payload', '0,1,2,3,4\n');
done(); done();
} }
catch(e) { done(e); } catch(e) { done(e); }
}); });
var testJson = [1,2,3,4]; var testJson = [0,1,2,3,4];
n1.emit("input", {payload:testJson}); n1.emit("input", {payload:testJson});
}); });
}); });
@ -245,12 +245,12 @@ describe('CSV node', function() {
var n2 = helper.getNode("n2"); var n2 = helper.getNode("n2");
n2.on("input", function(msg) { n2.on("input", function(msg) {
try { try {
msg.should.have.property('payload', '1,2,3,4\n4,3,2,1\n'); msg.should.have.property('payload', '0,1,2,3,4\n4,3,2,1,0\n');
done(); done();
} }
catch(e) { done(e); } catch(e) { done(e); }
}); });
var testJson = [[1,2,3,4],[4,3,2,1]]; var testJson = [[0,1,2,3,4],[4,3,2,1,0]];
n1.emit("input", {payload:testJson}); n1.emit("input", {payload:testJson});
}); });
}); });

View File

@ -149,6 +149,9 @@ module.exports = {
stopServer: function(done) { stopServer: function(done) {
if (server) { if (server) {
try { try {
server.on('close', function() {
comms.stop();
});
server.close(done); server.close(done);
} catch(e) { } catch(e) {
done(); done();

View File

@ -34,7 +34,20 @@ describe('flows/util', function() {
getType.restore(); getType.restore();
}); });
describe('#mapEnvVarProperties',function() {
it('handles ENV substitutions in an object', function() {
process.env.foo1 = "bar1";
process.env.foo2 = "bar2";
process.env.foo3 = "bar3";
var foo = {a:"$(foo1)",b:"$(foo2)",c:{d:"$(foo3)"}};
for (var p in foo) {
if (foo.hasOwnProperty(p)) {
flowUtil.mapEnvVarProperties(foo,p);
}
}
foo.should.eql({ a: 'bar1', b: 'bar2', c: { d: 'bar3' } } );
});
});
describe('#diffNodes',function() { describe('#diffNodes',function() {
it('handles a null old node', function() { it('handles a null old node', function() {

View File

@ -38,8 +38,9 @@ describe("red/nodes/index", function() {
index.clearRegistry(); index.clearRegistry();
}); });
process.env.foo="bar";
var testFlows = [{"type":"test","id":"tab1","label":"Sheet 1"}]; var testFlows = [{"type":"test","id":"tab1","label":"Sheet 1"}];
var testCredentials = {"tab1":{"b":1,"c":2}}; var testCredentials = {"tab1":{"b":1, "c":"2", "d":"$(foo)"}};
var storage = { var storage = {
getFlows: function() { getFlows: function() {
return when({red:123,flows:testFlows,credentials:testCredentials}); return when({red:123,flows:testFlows,credentials:testCredentials});
@ -58,7 +59,7 @@ describe("red/nodes/index", function() {
var runtime = { var runtime = {
settings: settings, settings: settings,
storage: storage, storage: storage,
log: {debug:function(){},warn:function(){}} log: {debug:function() {}, warn:function() {}}
}; };
function TestNode(n) { function TestNode(n) {
@ -75,7 +76,8 @@ describe("red/nodes/index", function() {
index.loadFlows().then(function() { index.loadFlows().then(function() {
var testnode = new TestNode({id:'tab1',type:'test',name:'barney'}); var testnode = new TestNode({id:'tab1',type:'test',name:'barney'});
testnode.credentials.should.have.property('b',1); testnode.credentials.should.have.property('b',1);
testnode.credentials.should.have.property('c',2); testnode.credentials.should.have.property('c',"2");
testnode.credentials.should.have.property('d',"bar");
done(); done();
}).otherwise(function(err) { }).otherwise(function(err) {
done(err); done(err);

View File

@ -317,6 +317,53 @@ describe("red/nodes/registry/loader",function() {
done(err); done(err);
}); });
}); });
it("load core node files scanned by lfs - missing html file", function(done) {
stubs.push(sinon.stub(localfilesystem,"getNodeFiles", function(){
var result = {};
result["node-red"] = {
"name": "node-red",
"nodes": {
"DuffNode": {
"file": path.join(resourcesDir,"DuffNode","DuffNode.js"),
"module": "node-red",
"name": "DuffNode"
}
}
};
return result;
}));
stubs.push(sinon.stub(registry,"saveNodeList", function(){ return }));
stubs.push(sinon.stub(registry,"addNodeSet", function(){ return }));
// This module isn't already loaded
stubs.push(sinon.stub(registry,"getNodeInfo", function(){ return null; }));
stubs.push(sinon.stub(nodes,"registerType"));
loader.init({nodes:nodes,i18n:{defaultLang:"en-US"},events:{on:function(){},removeListener:function(){}},log:{info:function(){},_:function(){}},settings:{available:function(){return true;}}});
loader.load().then(function(result) {
registry.addNodeSet.called.should.be.true();
registry.addNodeSet.lastCall.args[0].should.eql("node-red/DuffNode");
registry.addNodeSet.lastCall.args[1].should.have.a.property('id',"node-red/DuffNode");
registry.addNodeSet.lastCall.args[1].should.have.a.property('module',"node-red");
registry.addNodeSet.lastCall.args[1].should.have.a.property('enabled',true);
registry.addNodeSet.lastCall.args[1].should.have.a.property('loaded',false);
registry.addNodeSet.lastCall.args[1].should.have.a.property('version',undefined);
registry.addNodeSet.lastCall.args[1].should.have.a.property('types');
registry.addNodeSet.lastCall.args[1].types.should.have.a.length(0);
registry.addNodeSet.lastCall.args[1].should.not.have.a.property('config');
registry.addNodeSet.lastCall.args[1].should.not.have.a.property('help');
registry.addNodeSet.lastCall.args[1].should.not.have.a.property('namespace','node-red');
registry.addNodeSet.lastCall.args[1].should.have.a.property('err');
registry.addNodeSet.lastCall.args[1].err.should.endWith("DuffNode.html does not exist");
nodes.registerType.calledOnce.should.be.false();
done();
}).otherwise(function(err) {
done(err);
});
});
}); });
describe("#addModule",function() { describe("#addModule",function() {

View File

@ -0,0 +1,5 @@
// A test node that exports a function
module.exports = function(RED) {
function DuffNode(n) {}
RED.nodes.registerType("duff-node",DuffNode);
}

View File

@ -337,6 +337,11 @@ describe("red/util", function() {
it("pass 'a.b'.c",function() { testABC("'a.b'.c",['a.b','c']); }) it("pass 'a.b'.c",function() { testABC("'a.b'.c",['a.b','c']); })
it('pass a.$b.c',function() { testABC('a.$b.c',['a','$b','c']); })
it('pass a["$b"].c',function() { testABC('a["$b"].c',['a','$b','c']); })
it('pass a._b.c',function() { testABC('a._b.c',['a','_b','c']); })
it('pass a["_b"].c',function() { testABC('a["_b"].c',['a','_b','c']); })
it("fail a'b'.c",function() { testInvalid("a'b'.c"); }) it("fail a'b'.c",function() { testInvalid("a'b'.c"); })
it("fail a['b'.c",function() { testInvalid("a['b'.c"); }) it("fail a['b'.c",function() { testInvalid("a['b'.c"); })
it("fail a[]",function() { testInvalid("a[]"); }) it("fail a[]",function() { testInvalid("a[]"); })