diff --git a/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js b/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js index 2787a5c36..b78de9d75 100644 --- a/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js +++ b/packages/node_modules/@node-red/editor-api/lib/admin/nodes.js @@ -44,6 +44,7 @@ module.exports = { user: req.user, module: req.body.module, version: req.body.version, + url: req.body.url, req: apiUtils.getRequestLogObject(req) } runtimeAPI.nodes.addModule(opts).then(function(info) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js index 074a54469..a89003856 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js @@ -75,13 +75,16 @@ RED.palette.editor = (function() { }); }) } - function installNodeModule(id,version,callback) { + function installNodeModule(id,version,url,callback) { var requestBody = { module: id }; if (version) { requestBody.version = version; } + if (url) { + requestBody.url = url; + } $.ajax({ url:"nodes", type: "POST", @@ -622,7 +625,7 @@ RED.palette.editor = (function() { if ($(this).hasClass('disabled')) { return; } - update(entry,loadedIndex[entry.name].version,container,function(err){}); + update(entry,loadedIndex[entry.name].version,loadedIndex[entry.name].pkg_url,container,function(err){}); }) @@ -872,7 +875,7 @@ RED.palette.editor = (function() { $('
').appendTo(installTab); } - function update(entry,version,container,done) { + function update(entry,version,url,container,done) { if (RED.settings.theme('palette.editable') === false) { done(new Error('Palette not editable')); return; @@ -898,7 +901,7 @@ RED.palette.editor = (function() { RED.actions.invoke("core:show-event-log"); }); RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.name+" "+version); - installNodeModule(entry.name,version,function(xhr) { + installNodeModule(entry.name,version,url,function(xhr) { spinner.remove(); if (xhr) { if (xhr.responseJSON) { @@ -1023,7 +1026,7 @@ RED.palette.editor = (function() { RED.actions.invoke("core:show-event-log"); }); RED.eventLog.startEvent(RED._("palette.editor.confirm.button.install")+" : "+entry.id+" "+entry.version); - installNodeModule(entry.id,entry.version,function(xhr) { + installNodeModule(entry.id,entry.version,entry.pkg_url,function(xhr) { spinner.remove(); if (xhr) { if (xhr.responseJSON) { diff --git a/packages/node_modules/@node-red/registry/lib/installer.js b/packages/node_modules/@node-red/registry/lib/installer.js index 3562dfb47..e70952832 100644 --- a/packages/node_modules/@node-red/registry/lib/installer.js +++ b/packages/node_modules/@node-red/registry/lib/installer.js @@ -32,6 +32,7 @@ var paletteEditorEnabled = false; var settings; var moduleRe = /^(@[^/]+?[/])?[^/]+?$/; var slashRe = process.platform === "win32" ? /\\|[/]/ : /[/]/; +var pkgurlRe = /^(https?|git(|\+https?|\+ssh|\+file)):\/\//; function init(runtime) { events = runtime.events; @@ -76,14 +77,17 @@ function checkExistingModule(module,version) { } return false; } -function installModule(module,version) { +function installModule(module,version,url) { activePromise = activePromise.then(() => { //TODO: ensure module is 'safe' return new Promise((resolve,reject) => { var installName = module; var isUpgrade = false; try { - if (moduleRe.test(module)) { + if (url && pkgurlRe.test(url)) { + // Git remote url or Tarball url - check the valid package url + installName = url; + } else if (moduleRe.test(module)) { // Simple module name - assume it can be npm installed if (version) { installName += "@"+version; diff --git a/packages/node_modules/@node-red/runtime/lib/api/nodes.js b/packages/node_modules/@node-red/runtime/lib/api/nodes.js index ee4d3bc1a..fb16cc631 100644 --- a/packages/node_modules/@node-red/runtime/lib/api/nodes.js +++ b/packages/node_modules/@node-red/runtime/lib/api/nodes.js @@ -159,6 +159,7 @@ var api = module.exports = { * @param {User} opts.user - the user calling the api * @param {String} opts.module - the id of the module to install * @param {String} opts.version - (optional) the version of the module to install + * @param {String} opts.url - (optional) url to install * @param {Object} opts.req - the request to log (optional) * @return {Promise} - the node module info * @memberof @node-red/runtime_nodes @@ -183,20 +184,20 @@ var api = module.exports = { return reject(err); } } - runtime.nodes.installModule(opts.module,opts.version).then(function(info) { - runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version}, opts.req); + runtime.nodes.installModule(opts.module,opts.version,opts.url).then(function(info) { + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url}, opts.req); return resolve(info); }).catch(function(err) { if (err.code === 404) { - runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:"not_found"}, opts.req); + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url,error:"not_found"}, opts.req); // TODO: code/status err.status = 404; } else if (err.code) { err.status = 400; - runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code}, opts.req); + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url,error:err.code}, opts.req); } else { err.status = 400; - runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code||"unexpected_error",message:err.toString()}, opts.req); + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,url:opts.url,error:err.code||"unexpected_error",message:err.toString()}, opts.req); } return reject(err); }) diff --git a/packages/node_modules/@node-red/runtime/lib/nodes/index.js b/packages/node_modules/@node-red/runtime/lib/nodes/index.js index fcb153fe1..09aebe6eb 100644 --- a/packages/node_modules/@node-red/runtime/lib/nodes/index.js +++ b/packages/node_modules/@node-red/runtime/lib/nodes/index.js @@ -150,10 +150,10 @@ function reportNodeStateChange(info,enabled) { } } -function installModule(module,version) { +function installModule(module,version,url) { var existingModule = registry.getModuleInfo(module); var isUpgrade = !!existingModule; - return registry.installModule(module,version).then(function(info) { + return registry.installModule(module,version,url).then(function(info) { if (isUpgrade) { events.emit("runtime-event",{id:"node/upgraded",retain:false,payload:{module:module,version:version}}); } else { diff --git a/test/unit/@node-red/editor-api/lib/admin/nodes_spec.js b/test/unit/@node-red/editor-api/lib/admin/nodes_spec.js index ef98fb677..0d373b8d0 100644 --- a/test/unit/@node-red/editor-api/lib/admin/nodes_spec.js +++ b/test/unit/@node-red/editor-api/lib/admin/nodes_spec.js @@ -227,7 +227,7 @@ describe("api/admin/nodes", function() { }); request(app) .post('/nodes') - .send({module: 'foo',version:"1.2.3"}) + .send({module: 'foo',version:"1.2.3",url:"https://example/foo-1.2.3.tgz"}) .expect(200) .end(function(err,res) { if (err) { @@ -238,6 +238,7 @@ describe("api/admin/nodes", function() { res.body.nodes[0].should.have.property("id","123"); opts.should.have.property("module","foo"); opts.should.have.property("version","1.2.3"); + opts.should.have.property("url","https://example/foo-1.2.3.tgz"); done(); }); }); @@ -256,7 +257,7 @@ describe("api/admin/nodes", function() { }); request(app) .post('/nodes') - .send({module: 'foo',version:"1.2.3"}) + .send({module: 'foo',version:"1.2.3",url:"https://example/foo-1.2.3.tgz"}) .expect(400) .end(function(err,res) { if (err) { diff --git a/test/unit/@node-red/registry/lib/installer_spec.js b/test/unit/@node-red/registry/lib/installer_spec.js index f3bae8b3a..045a81e0d 100644 --- a/test/unit/@node-red/registry/lib/installer_spec.js +++ b/test/unit/@node-red/registry/lib/installer_spec.js @@ -121,6 +121,17 @@ describe('nodes/registry/installer', function() { done(); }); }); + it("rejects when update requested to existing version and url", function(done) { + sinon.stub(typeRegistry,"getModuleInfo", function() { + return { + version: "0.1.1" + } + }); + installer.installModule("this_wont_exist","0.1.1","https://example/foo-0.1.1.tgz").catch(function(err) { + err.code.should.be.eql('module_already_loaded'); + done(); + }); + }); it("rejects with generic error", function(done) { var res = { code: 1, @@ -201,6 +212,29 @@ describe('nodes/registry/installer', function() { done(err); }); }); + it("succeeds when url is valid node-red module", function(done) { + var nodeInfo = {nodes:{module:"foo",types:["a"]}}; + + var res = { + code: 0, + stdout:"", + stderr:"" + } + var p = Promise.resolve(res); + p.catch((err)=>{}); + initInstaller(p) + + var addModule = sinon.stub(registry,"addModule",function(md) { + return when.resolve(nodeInfo); + }); + + installer.installModule("this_wont_exist",null,"https://example/foo-0.1.1.tgz").then(function(info) { + info.should.eql(nodeInfo); + done(); + }).catch(function(err) { + done(err); + }); + }); }); describe("uninstalls module", function() {