diff --git a/red/api/nodes.js b/red/api/nodes.js index fa26e0a1e..47e42be32 100644 --- a/red/api/nodes.js +++ b/red/api/nodes.js @@ -59,20 +59,17 @@ module.exports = { comms.publish("node/added",info.nodes,false); if (node.module) { log.audit({event: "nodes.install",module:node.module},req); - res.json(redNodes.getModuleInfo(node.module)); - } else if (node.file) { - log.audit({event: "nodes.install",file:node.file},req); - res.json(info.nodes[0]); + res.json(info); } }).otherwise(function(err) { if (err.code === 404) { - log.audit({event: "nodes.install",module:node.module,file:node.file,error:"not_found"},req); + log.audit({event: "nodes.install",module:node.module,error:"not_found"},req); res.status(404).end(); } else if (err.code) { log.audit({event: "nodes.install",module:node.module,error:err.code},req); res.status(400).json({error:err.code, message:err.message}); } else { - log.audit({event: "nodes.install",module:node.module,file:node.file,error:err.code||"unexpected_error",message:err.toString()},req); + log.audit({event: "nodes.install",module:node.module,error:err.code||"unexpected_error",message:err.toString()},req); res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); } }); diff --git a/red/nodes/registry/installer.js b/red/nodes/registry/installer.js index 7c6740677..ba78281f0 100644 --- a/red/nodes/registry/installer.js +++ b/red/nodes/registry/installer.js @@ -28,37 +28,74 @@ var child_process = require('child_process'); var settings; +var moduleRe = /^[^/]+$/; +var slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/; + function init(_settings) { settings = _settings; } +function checkModulePath(folder) { + var moduleName; + var err; + var fullPath = path.resolve(folder); + var packageFile = path.join(fullPath,'package.json'); + if (fs.existsSync(packageFile)) { + var pkg = require(packageFile); + moduleName = pkg.name; + if (!pkg['node-red']) { + // TODO: nls + err = new Error("Invalid Node-RED module"); + err.code = 'invalid_module'; + throw err; + } + } else { + err = new Error("Module not found"); + err.code = 404; + throw err; + } + return moduleName; +} + +function checkExistingModule(module) { + if (registry.getModuleInfo(module)) { + // TODO: nls + var err = new Error("Module already loaded"); + err.code = "module_already_loaded"; + throw err; + } +} + function installModule(module) { //TODO: ensure module is 'safe' return when.promise(function(resolve,reject) { - if (/[\s;]/.test(module)) { - reject(new Error(log._("server.install.invalid"))); - return; - } - if (registry.getModuleInfo(module)) { - // TODO: nls - var err = new Error("Module already loaded"); - err.code = "module_already_loaded"; - reject(err); - return; + var installName = module; + + try { + if (moduleRe.test(module)) { + // Simple module name - assume it can be npm installed + } else if (slashRe.test(module)) { + // A path - check if there's a valid package.json + installName = module; + module = checkModulePath(module); + } + checkExistingModule(module); + } catch(err) { + return reject(err); } log.info(log._("server.install.installing",{name: module})); var installDir = settings.userDir || process.env.NODE_RED_HOME || "."; - var child = child_process.execFile('npm',['install','--production',module], + var child = child_process.execFile('npm',['install','--production',installName], { cwd: installDir }, function(err, stdin, stdout) { if (err) { - var lookFor404 = new RegExp(" 404 .*"+module+"$","m"); + var lookFor404 = new RegExp(" 404 .*"+installName+"$","m"); if (lookFor404.test(stdout)) { log.warn(log._("server.install.install-failed-not-found",{name:module})); - var e = new Error(); + var e = new Error("Module not found"); e.code = 404; reject(e); } else { diff --git a/red/nodes/registry/loader.js b/red/nodes/registry/loader.js index 5d6f84baf..265abe71f 100644 --- a/red/nodes/registry/loader.js +++ b/red/nodes/registry/loader.js @@ -321,34 +321,6 @@ function addModule(module) { } } -function addFile(file) { - if (!settings.available()) { - throw new Error("Settings unavailable"); - } - var info = registry.getNodeInfo("node-red/"+path.basename(file).replace(/^\d+-/,"").replace(/\.js$/,"")); - if (info) { - var err = new Error("File already loaded"); - err.code = "file_already_loaded"; - return when.reject(err); - } - var nodeFiles = localfilesystem.getLocalFile(file); - if (nodeFiles) { - var fileObj = {}; - fileObj[nodeFiles.module] = { - name: nodeFiles.module, - version: nodeFiles.version, - nodes: {} - }; - fileObj[nodeFiles.module].nodes[nodeFiles.name] = nodeFiles; - - return loadNodeFiles(fileObj); - } else { - var e = new Error(); - e.code = 404; - return when.reject(e); - } -} - function loadNodeHelp(node,lang) { var dir = path.dirname(node.template); var base = path.basename(node.template); @@ -385,7 +357,6 @@ module.exports = { init: init, load: load, addModule: addModule, - addFile: addFile, loadNodeSet: loadNodeSet, getNodeHelp: getNodeHelp } diff --git a/test/red/api/nodes_spec.js b/test/red/api/nodes_spec.js index ee0772ae5..f82e39a65 100644 --- a/test/red/api/nodes_spec.js +++ b/test/red/api/nodes_spec.js @@ -208,12 +208,11 @@ describe("nodes api", function() { }); var getModuleInfo = sinon.stub(redNodes,'getModuleInfo'); getModuleInfo.onCall(0).returns(null); - getModuleInfo.onCall(1).returns({ - name:"foo", - nodes:[{id:"123"}] - }); var installModule = sinon.stub(redNodes,'installModule', function() { - return when.resolve({id:"123"}); + return when.resolve({ + name:"foo", + nodes:[{id:"123"}] + }); }); request(app) diff --git a/test/red/nodes/registry/installer_spec.js b/test/red/nodes/registry/installer_spec.js index be6b2f9a2..1e65cfa4a 100644 --- a/test/red/nodes/registry/installer_spec.js +++ b/test/red/nodes/registry/installer_spec.js @@ -17,6 +17,7 @@ var should = require("should"); var sinon = require("sinon"); var when = require("when"); +var path = require("path"); var child_process = require('child_process'); var installer = require("../../../../red/nodes/registry/installer"); @@ -28,33 +29,25 @@ describe('nodes/registry/installer', function() { before(function() { installer.init({}); }); + afterEach(function() { + if (child_process.execFile.restore) { + child_process.execFile.restore(); + } + }) describe("installs module", function() { - it("rejects invalid module names", function(done) { - var promises = []; - promises.push(installer.installModule("this_wont_exist ")); - promises.push(installer.installModule("this_wont_exist;no_it_really_wont")); - when.settle(promises).then(function(results) { - results[0].state.should.be.eql("rejected"); - results[1].state.should.be.eql("rejected"); - done(); - }); - }); - it("rejects when npm returns a 404", function(done) { - var exec = sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { + sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { cb(new Error(),""," 404 this_wont_exist"); }); installer.installModule("this_wont_exist").otherwise(function(err) { err.code.should.be.eql(404); done(); - }).finally(function() { - exec.restore(); }); }); it("rejects with generic error", function(done) { - var exec = sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { + sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { cb(new Error("test_error"),"",""); }); @@ -62,13 +55,11 @@ describe('nodes/registry/installer', function() { done(new Error("Unexpected success")); }).otherwise(function(err) { done(); - }).finally(function() { - exec.restore(); }); }); it("succeeds when module is found", function(done) { var nodeInfo = {nodes:{module:"foo",types:["a"]}}; - var exec = sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { + sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { cb(null,"",""); }); var addModule = sinon.stub(registry,"addModule",function(md) { @@ -84,10 +75,37 @@ describe('nodes/registry/installer', function() { }).otherwise(function(err) { done(err); }).finally(function() { - exec.restore(); addModule.restore(); }); }); + it("rejects when non-existant path is provided", function(done) { + var resourcesDir = path.resolve(path.join(__dirname,"..","resources","TestNodeModule","node_modules","NonExistant")); + installer.installModule(resourcesDir).then(function() { + done(new Error("Unexpected success")); + }).otherwise(function(err) { + err.code.should.eql(404); + done(); + }); + }); + it("succeeds when path is valid node-red module", function(done) { + var nodeInfo = {nodes:{module:"foo",types:["a"]}}; + var addModule = sinon.stub(registry,"addModule",function(md) { + return when.resolve(nodeInfo); + }); + var resourcesDir = path.resolve(path.join(__dirname,"..","resources","TestNodeModule","node_modules","TestNodeModule")); + sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { + cb(null,"",""); + }); + installer.installModule(resourcesDir).then(function(info) { + info.should.eql(nodeInfo); + done(); + }).otherwise(function(err) { + done(err); + }).finally(function() { + addModule.restore(); + }); + }); + }); describe("uninstalls module", function() { it("rejects invalid module names", function(done) { @@ -106,7 +124,7 @@ describe('nodes/registry/installer', function() { var removeModule = sinon.stub(registry,"removeModule",function(md) { return when.resolve(nodeInfo); }); - var exec = sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { + sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { cb(new Error("test_error"),"",""); }); @@ -115,7 +133,6 @@ describe('nodes/registry/installer', function() { }).otherwise(function(err) { done(); }).finally(function() { - exec.restore(); removeModule.restore(); }); }); @@ -127,7 +144,7 @@ describe('nodes/registry/installer', function() { var getModuleInfo = sinon.stub(registry,"getModuleInfo",function(md) { return {nodes:[]}; }); - var exec = sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { + sinon.stub(child_process,"execFile",function(cmd,args,opt,cb) { cb(null,"",""); }); @@ -142,7 +159,6 @@ describe('nodes/registry/installer', function() { }).otherwise(function(err) { done(err); }).finally(function() { - exec.restore(); removeModule.restore(); exists.restore(); getModuleInfo.restore();