From e8e8f70c27c17becc690774bdb4c73b021035dad Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Sun, 15 Apr 2018 11:18:10 +0100 Subject: [PATCH 01/51] WIP: create new runtime-api --- jsdoc.json | 17 + package.json | 226 +++++------ red/api/admin/flow.js | 88 ++-- red/api/admin/flows.js | 92 ++--- red/api/admin/index.js | 8 +- red/api/admin/nodes.js | 255 ++++-------- red/api/editor/credentials.js | 42 +- red/api/editor/index.js | 17 +- red/api/editor/settings.js | 129 ++---- red/api/editor/sshkeys.js | 101 ++--- red/api/editor/theme.js | 8 +- red/api/index.js | 9 +- red/api/util.js | 10 + red/red.js | 7 +- red/runtime-api/auth.js | 19 + red/runtime-api/comms.js | 19 + red/runtime-api/flows.js | 248 ++++++++++++ red/runtime-api/index.js | 64 +++ red/runtime-api/library.js | 35 ++ red/runtime-api/nodes.js | 377 ++++++++++++++++++ red/runtime-api/settings.js | 257 ++++++++++++ .../localfilesystem/projects/ssh/index.js | 5 + 22 files changed, 1422 insertions(+), 611 deletions(-) create mode 100644 jsdoc.json create mode 100644 red/runtime-api/auth.js create mode 100644 red/runtime-api/comms.js create mode 100644 red/runtime-api/flows.js create mode 100644 red/runtime-api/index.js create mode 100644 red/runtime-api/library.js create mode 100644 red/runtime-api/nodes.js create mode 100644 red/runtime-api/settings.js diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 000000000..ca6d6cdb1 --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,17 @@ +{ + "tags": { + "allowUnknownTags": false, + "dictionaries": ["jsdoc"] + }, + "source": { + "include": [ + "./red/runtime-api" + ] + }, + "templates": { + "systemName": "Node-RED Runtime API", + "theme":"yeti", + "footer": "", + "copyright": "Released under the Apache License v2.0" + } +} diff --git a/package.json b/package.json index 07ec5e84d..5e4eaac68 100644 --- a/package.json +++ b/package.json @@ -1,115 +1,119 @@ { - "name": "node-red", - "version": "0.18.4", - "description": "A visual tool for wiring the Internet of Things", - "homepage": "http://nodered.org", - "license": "Apache-2.0", - "repository": { - "type": "git", - "url": "https://github.com/node-red/node-red.git" - }, - "main": "red/red.js", - "scripts": { - "start": "node red.js", - "test": "grunt", - "build": "grunt build" - }, - "bin": { - "node-red": "./red.js", - "node-red-pi": "bin/node-red-pi" - }, - "contributors": [ - { - "name": "Nick O'Leary" + "name": "node-red", + "version": "0.18.4", + "description": "A visual tool for wiring the Internet of Things", + "homepage": "http://nodered.org", + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/node-red/node-red.git" }, - { - "name": "Dave Conway-Jones" + "main": "red/red.js", + "scripts": { + "start": "node red.js", + "test": "grunt", + "build": "grunt build", + "doc": "jsdoc --pedantic --recurse -c jsdoc.json -t ./node_modules/ink-docstrap/template" + }, + "bin": { + "node-red": "./red.js", + "node-red-pi": "bin/node-red-pi" + }, + "contributors": [ + { + "name": "Nick O'Leary" + }, + { + "name": "Dave Conway-Jones" + } + ], + "keywords": [ + "editor", + "messaging", + "iot", + "flow" + ], + "dependencies": { + "basic-auth": "2.0.0", + "bcryptjs": "2.4.3", + "body-parser": "1.18.2", + "cheerio": "0.22.0", + "clone": "2.1.1", + "cookie": "0.3.1", + "cookie-parser": "1.4.3", + "cors": "2.8.4", + "cron": "1.3.0", + "express": "4.16.2", + "express-session": "1.15.6", + "follow-redirects": "1.3.0", + "fs-extra": "5.0.0", + "fs.notify": "0.0.4", + "hash-sum": "1.0.2", + "i18next": "1.10.6", + "ink-docstrap": "^1.3.2", + "is-utf8": "0.2.1", + "js-yaml": "3.10.0", + "json-stringify-safe": "5.0.1", + "jsonata": "1.5.0", + "media-typer": "0.3.0", + "memorystore": "1.6.0", + "mime": "1.4.1", + "mqtt": "2.15.1", + "multer": "1.3.0", + "mustache": "2.3.0", + "node-red-node-email": "0.1.*", + "node-red-node-feedparser": "0.1.*", + "node-red-node-rbe": "0.2.*", + "node-red-node-twitter": "0.1.*", + "nopt": "4.0.1", + "oauth2orize": "1.11.0", + "on-headers": "1.0.1", + "passport": "0.4.0", + "passport-http-bearer": "1.0.1", + "passport-oauth2-client-password": "0.1.2", + "raw-body": "2.3.2", + "semver": "5.4.1", + "sentiment": "2.1.0", + "uglify-js": "3.3.6", + "when": "3.7.8", + "ws": "1.1.5", + "xml2js": "0.4.19" + }, + "optionalDependencies": { + "bcrypt": "~1.0.3" + }, + "devDependencies": { + "chromedriver": "^2.33.2", + "grunt": "~1.0.1", + "grunt-chmod": "~1.1.1", + "grunt-cli": "~1.2.0", + "grunt-concurrent": "~2.3.1", + "grunt-contrib-clean": "~1.1.0", + "grunt-contrib-compress": "~1.4.0", + "grunt-contrib-concat": "~1.0.1", + "grunt-contrib-copy": "~1.0.0", + "grunt-contrib-jshint": "~1.1.0", + "grunt-contrib-uglify": "~3.3.0", + "grunt-contrib-watch": "~1.0.0", + "grunt-jsonlint": "~1.1.0", + "grunt-mocha-istanbul": "5.0.2", + "grunt-nodemon": "~0.4.2", + "grunt-sass": "~2.0.0", + "grunt-simple-mocha": "~0.4.1", + "grunt-webdriver": "^2.0.3", + "istanbul": "0.4.5", + "jsdoc": "3.5.5", + "mocha": "^5.1.1", + "should": "^8.4.0", + "sinon": "1.17.7", + "stoppable": "^1.0.6", + "supertest": "3.0.0", + "wdio-chromedriver-service": "^0.1.1", + "wdio-mocha-framework": "^0.5.11", + "wdio-spec-reporter": "^0.1.3", + "webdriverio": "^4.9.11" + }, + "engines": { + "node": ">=4" } - ], - "keywords": [ - "editor", - "messaging", - "iot", - "flow" - ], - "dependencies": { - "basic-auth": "2.0.0", - "bcryptjs": "2.4.3", - "body-parser": "1.18.2", - "cheerio": "0.22.0", - "clone": "2.1.1", - "cookie": "0.3.1", - "cookie-parser": "1.4.3", - "cors": "2.8.4", - "cron": "1.3.0", - "express": "4.16.2", - "express-session": "1.15.6", - "follow-redirects": "1.3.0", - "fs-extra": "5.0.0", - "fs.notify": "0.0.4", - "hash-sum": "1.0.2", - "i18next": "1.10.6", - "is-utf8": "0.2.1", - "js-yaml": "3.10.0", - "json-stringify-safe": "5.0.1", - "jsonata": "1.5.0", - "media-typer": "0.3.0", - "memorystore": "1.6.0", - "mqtt": "2.15.1", - "multer": "1.3.0", - "mustache": "2.3.0", - "node-red-node-email": "0.1.*", - "node-red-node-feedparser": "0.1.*", - "node-red-node-rbe": "0.2.*", - "node-red-node-twitter": "0.1.*", - "nopt": "4.0.1", - "oauth2orize": "1.11.0", - "on-headers": "1.0.1", - "passport": "0.4.0", - "passport-http-bearer": "1.0.1", - "passport-oauth2-client-password": "0.1.2", - "raw-body": "2.3.2", - "semver": "5.4.1", - "sentiment": "2.1.0", - "uglify-js": "3.3.6", - "when": "3.7.8", - "ws": "1.1.5", - "xml2js": "0.4.19" - }, - "optionalDependencies": { - "bcrypt": "~1.0.3" - }, - "devDependencies": { - "chromedriver": "^2.33.2", - "grunt": "~1.0.1", - "grunt-chmod": "~1.1.1", - "grunt-cli": "~1.2.0", - "grunt-concurrent": "~2.3.1", - "grunt-contrib-clean": "~1.1.0", - "grunt-contrib-compress": "~1.4.0", - "grunt-contrib-concat": "~1.0.1", - "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-jshint": "~1.1.0", - "grunt-contrib-uglify": "~3.3.0", - "grunt-contrib-watch": "~1.0.0", - "grunt-jsonlint": "~1.1.0", - "grunt-mocha-istanbul": "5.0.2", - "grunt-nodemon": "~0.4.2", - "grunt-sass": "~2.0.0", - "grunt-simple-mocha": "~0.4.1", - "grunt-webdriver": "^2.0.3", - "istanbul": "0.4.5", - "mocha": "^5.1.1", - "should": "^8.4.0", - "sinon": "1.17.7", - "stoppable": "^1.0.6", - "supertest": "3.0.0", - "wdio-chromedriver-service": "^0.1.1", - "wdio-mocha-framework": "^0.5.11", - "wdio-spec-reporter": "^0.1.3", - "webdriverio": "^4.9.11" - }, - "engines": { - "node": ">=4" - } } diff --git a/red/api/admin/flow.js b/red/api/admin/flow.js index df1cf2d0c..5ba5d7a04 100644 --- a/red/api/admin/flow.js +++ b/red/api/admin/flow.js @@ -14,72 +14,56 @@ * limitations under the License. **/ -var log; -var redNodes; +var runtimeAPI; +var apiUtils = require("../util"); module.exports = { - init: function(runtime) { - redNodes = runtime.nodes; - log = runtime.log; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, get: function(req,res) { - var id = req.params.id; - var flow = redNodes.getFlow(id); - if (flow) { - log.audit({event: "flow.get",id:id},req); - res.json(flow); - } else { - log.audit({event: "flow.get",id:id,error:"not_found"},req); - res.status(404).end(); + var opts = { + user: req.user, + id: req.params.id } + runtimeAPI.flows.getFlow(opts).then(function(result) { + return res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, post: function(req,res) { - var flow = req.body; - redNodes.addFlow(flow).then(function(id) { - log.audit({event: "flow.add",id:id},req); - res.json({id:id}); + var opts = { + user: req.user, + flow: req.body + } + runtimeAPI.flows.addFlow(opts).then(function(id) { + return res.json({id:id}); }).catch(function(err) { - log.audit({event: "flow.add",error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + apiUtils.rejectHandler(req,res,err); }) - }, put: function(req,res) { - var id = req.params.id; - var flow = req.body; - try { - redNodes.updateFlow(id,flow).then(function() { - log.audit({event: "flow.update",id:id},req); - res.json({id:id}); - }).catch(function(err) { - log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - }) - } catch(err) { - if (err.code === 404) { - log.audit({event: "flow.update",id:id,error:"not_found"},req); - res.status(404).end(); - } else { - log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - } + var opts = { + user: req.user, + id: req.params.id, + flow: req.body } + runtimeAPI.flows.updateFlow(opts).then(function(id) { + return res.json({id:id}); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, delete: function(req,res) { - var id = req.params.id; - try { - redNodes.removeFlow(id).then(function() { - log.audit({event: "flow.remove",id:id},req); - res.status(204).end(); - }) - } catch(err) { - if (err.code === 404) { - log.audit({event: "flow.remove",id:id,error:"not_found"},req); - res.status(404).end(); - } else { - log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - } + var opts = { + user: req.user, + id: req.params.id } + runtimeAPI.flows.deleteFlow(opts).then(function() { + res.status(204).end(); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } } diff --git a/red/api/admin/flows.js b/red/api/admin/flows.js index d3eceecb5..35b234dfa 100644 --- a/red/api/admin/flows.js +++ b/red/api/admin/flows.js @@ -14,72 +14,56 @@ * limitations under the License. **/ -var log; -var redNodes; +var runtimeAPI; module.exports = { - init: function(runtime) { - redNodes = runtime.nodes; - log = runtime.log; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, get: function(req,res) { var version = req.get("Node-RED-API-Version")||"v1"; - if (version === "v1") { - log.audit({event: "flows.get",version:"v1"},req); - res.json(redNodes.getFlows().flows); - } else if (version === "v2") { - log.audit({event: "flows.get",version:"v2"},req); - res.json(redNodes.getFlows()); - } else { - log.audit({event: "flows.get",version:version,error:"invalid_api_version"},req); - res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"}); + if (!/^v[12]$/.test(version)) { + return res.status(500).json({code:"invalid_api_version", message:"Invalid API Version requested"}); } + var opts = { + user: req.user + } + runtimeAPI.flows.getFlows(opts).then(function(result) { + if (version === "v1") { + res.json(result.flows); + } else if (version === "v2") { + res.json(result); + } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, post: function(req,res) { var version = req.get("Node-RED-API-Version")||"v1"; if (!/^v[12]$/.test(version)) { - log.audit({event: "flows.set",version:version,error:"invalid_api_version"},req); - res.status(400).json({code:"invalid_api_version", message:"Invalid API Version requested"}); - return; + return res.status(500).json({code:"invalid_api_version", message:"Invalid API Version requested"}); } - var flows = req.body; - var deploymentType = req.get("Node-RED-Deployment-Type")||"full"; - log.audit({event: "flows.set",type:deploymentType,version:version},req); - if (deploymentType === 'reload') { - redNodes.loadFlows().then(function(flowId) { - if (version === "v1") { - res.status(204).end(); - } else { - res.json({rev:flowId}); - } - }).catch(function(err) { - log.warn(log._("api.flows.error-reload",{message:err.message})); - log.warn(err.stack); - res.status(500).json({error:"unexpected_error", message:err.message}); - }); - } else { - var flowConfig = flows; - if (version === "v2") { - flowConfig = flows.flows; - if (flows.hasOwnProperty('rev')) { - var currentVersion = redNodes.getFlows().rev; - if (currentVersion !== flows.rev) { - //TODO: log warning - return res.status(409).json({code:"version_mismatch"}); - } - } + var opts = { + user: req.user, + deploymentType: req.get("Node-RED-Deployment-Type")||"full" + } + + if (opts.deploymentType !== 'reload') { + if (version === "v1") { + opts.flows = {flows: req.body} + } else { + opts.flows = req.body; } - redNodes.setFlows(flowConfig,deploymentType).then(function(flowId) { - if (version === "v1") { - res.status(204).end(); - } else if (version === "v2") { - res.json({rev:flowId}); - } - }).catch(function(err) { - log.warn(log._("api.flows.error-save",{message:err.message})); - log.warn(err.stack); - res.status(500).json({error:err.code || "unexpected_error", message:err.message}); - }); } + + runtimeAPI.flows.setFlows(opts).then(function(result) { + if (version === "v1") { + res.status(204).end(); + } else { + res.json(result); + } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } } diff --git a/red/api/admin/index.js b/red/api/admin/index.js index 57c79eb62..9679eaefc 100644 --- a/red/api/admin/index.js +++ b/red/api/admin/index.js @@ -24,10 +24,10 @@ var auth = require("../auth"); var apiUtil = require("../util"); module.exports = { - init: function(runtime) { - flows.init(runtime); - flow.init(runtime); - nodes.init(runtime); + init: function(runtimeAPI) { + flows.init(runtimeAPI); + flow.init(runtimeAPI); + nodes.init(runtimeAPI); var needsPermission = auth.needsPermission; diff --git a/red/api/admin/nodes.js b/red/api/admin/nodes.js index 032623bb4..6dbe1cc05 100644 --- a/red/api/admin/nodes.js +++ b/red/api/admin/nodes.js @@ -14,230 +14,133 @@ * limitations under the License. **/ -var when = require("when"); var apiUtils = require("../util"); -var redNodes; -var log; -var settings; + +var runtimeAPI; module.exports = { - init: function(runtime) { - redNodes = runtime.nodes; - log = runtime.log; - settings = runtime.settings; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, getAll: function(req,res) { + var opts = { + user: req.user + } if (req.get("accept") == "application/json") { - log.audit({event: "nodes.list.get"},req); - res.json(redNodes.getNodeList()); + runtimeAPI.nodes.getNodeList(opts).then(function(list) { + res.json(list); + }) } else { - var lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); - log.audit({event: "nodes.configs.get"},req); - res.send(redNodes.getNodeConfigs(lang)); + opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); + runtimeAPI.nodes.getNodeConfigs(opts).then(function(configs) { + res.send(configs); + }) } }, post: function(req,res) { - if (!settings.available()) { - log.audit({event: "nodes.install",error:"settings_unavailable"},req); - res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"}); - return; + var opts = { + user: req.user, + module: req.body.module, + version: req.body.version } - var node = req.body; - var promise; - var isUpgrade = false; - if (node.module) { - var module = redNodes.getModuleInfo(node.module); - if (module) { - if (!node.version || module.version === node.version) { - log.audit({event: "nodes.install",module:node.module, version:node.version, error:"module_already_loaded"},req); - res.status(400).json({error:"module_already_loaded", message:"Module already loaded"}); - return; - } - if (!module.local) { - log.audit({event: "nodes.install",module:node.module, version:node.version, error:"module_not_local"},req); - res.status(400).json({error:"module_not_local", message:"Module not locally installed"}); - return; - } - isUpgrade = true; - } - promise = redNodes.installModule(node.module,node.version); - } else { - log.audit({event: "nodes.install",module:node.module,error:"invalid_request"},req); - res.status(400).json({error:"invalid_request", message:"Invalid request"}); - return; - } - promise.then(function(info) { - if (node.module) { - log.audit({event: "nodes.install",module:node.module,version:node.version},req); - res.json(info); - } + runtimeAPI.nodes.addModule(opts).then(function(info) { + res.json(info); }).catch(function(err) { - if (err.code === 404) { - log.audit({event: "nodes.install",module:node.module,version:node.version,error:"not_found"},req); - res.status(404).end(); - } else if (err.code) { - log.audit({event: "nodes.install",module:node.module,version:node.version,error:err.code},req); - res.status(400).json({error:err.code, message:err.message}); - } else { - log.audit({event: "nodes.install",module:node.module,version:node.version,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - } - }); + apiUtils.rejectHandler(req,res,err); + }) }, delete: function(req,res) { - if (!settings.available()) { - log.audit({event: "nodes.remove",error:"settings_unavailable"},req); - res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"}); - return; - } - var mod = req.params[0]; - try { - var promise = null; - var module = redNodes.getModuleInfo(mod); - if (!module) { - log.audit({event: "nodes.remove",module:mod,error:"not_found"},req); - res.status(404).end(); - return; - } else { - promise = redNodes.uninstallModule(mod); - } - - promise.then(function(list) { - log.audit({event: "nodes.remove",module:mod},req); - res.status(204).end(); - }).catch(function(err) { - log.audit({event: "nodes.remove",module:mod,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - }); - } catch(err) { - log.audit({event: "nodes.remove",module:mod,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + var opts = { + user: req.user, + module: req.params[0] } + runtimeAPI.nodes.removeModule(opts).then(function(info) { + res.status(204).end(); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, getSet: function(req,res) { - var id = req.params[0] + "/" + req.params[2]; - var result = null; + var opts = { + user: req.user, + id: req.params[0] + "/" + req.params[2] + } if (req.get("accept") === "application/json") { - result = redNodes.getNodeInfo(id); - if (result) { - log.audit({event: "nodes.info.get",id:id},req); - delete result.loaded; + runtimeAPI.nodes.getNodeInfo(opts).then(function(result) { res.send(result); - } else { - log.audit({event: "nodes.info.get",id:id,error:"not_found"},req); - res.status(404).end(); - } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } else { - var lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); - result = redNodes.getNodeConfig(id,lang); - if (result) { - log.audit({event: "nodes.config.get",id:id},req); - res.send(result); - } else { - log.audit({event: "nodes.config.get",id:id,error:"not_found"},req); - res.status(404).end(); - } + opts.lang = apiUtils.determineLangFromHeaders(req.acceptsLanguages()); + runtimeAPI.nodes.getNodeConfig(opts).then(function(result) { + return res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } }, getModule: function(req,res) { - var module = req.params[0]; - var result = redNodes.getModuleInfo(module); - if (result) { - log.audit({event: "nodes.module.get",module:module},req); - res.json(result); - } else { - log.audit({event: "nodes.module.get",module:module,error:"not_found"},req); - res.status(404).end(); + var opts = { + user: req.user, + module: req.params[0] } + runtimeAPI.nodes.getModuleInfo(opts).then(function(result) { + res.send(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, putSet: function(req,res) { - if (!settings.available()) { - log.audit({event: "nodes.info.set",error:"settings_unavailable"},req); - res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"}); - return; - } var body = req.body; if (!body.hasOwnProperty("enabled")) { - log.audit({event: "nodes.info.set",error:"invalid_request"},req); + // log.audit({event: "nodes.module.set",error:"invalid_request"},req); res.status(400).json({error:"invalid_request", message:"Invalid request"}); return; } - var id = req.params[0] + "/" + req.params[2]; - try { - var node = redNodes.getNodeInfo(id); - var info; - if (!node) { - log.audit({event: "nodes.info.set",id:id,error:"not_found"},req); - res.status(404).end(); - } else { - delete node.loaded; - putNode(node, body.enabled).then(function(result) { - log.audit({event: "nodes.info.set",id:id,enabled:body.enabled},req); - res.json(result); - }); - } - } catch(err) { - log.audit({event: "nodes.info.set",id:id,enabled:body.enabled,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + var opts = { + user: req.user, + id: req.params[0] + "/" + req.params[2], + enabled: body.enabled } + runtimeAPI.nodes.setNodeSetState(opts).then(function(result) { + res.send(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) }, putModule: function(req,res) { - if (!settings.available()) { - log.audit({event: "nodes.module.set",error:"settings_unavailable"},req); - res.status(400).json({error:"settings_unavailable", message:"Settings unavailable"}); - return; - } var body = req.body; if (!body.hasOwnProperty("enabled")) { - log.audit({event: "nodes.module.set",error:"invalid_request"},req); + // log.audit({event: "nodes.module.set",error:"invalid_request"},req); res.status(400).json({error:"invalid_request", message:"Invalid request"}); return; } - var mod = req.params[0]; - try { - var module = redNodes.getModuleInfo(mod); - if (!module) { - log.audit({event: "nodes.module.set",module:mod,error:"not_found"},req); - return res.status(404).end(); - } - - var nodes = module.nodes; - var promises = []; - for (var i = 0; i < nodes.length; ++i) { - promises.push(putNode(nodes[i],body.enabled)); - } - when.settle(promises).then(function() { - res.json(redNodes.getModuleInfo(mod)); - }); - } catch(err) { - log.audit({event: "nodes.module.set",module:mod,enabled:body.enabled,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + var opts = { + user: req.user, + module: req.params[0], + enabled: body.enabled } + runtimeAPI.nodes.setModuleState(opts).then(function(result) { + res.send(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) + }, getIcons: function(req,res) { - log.audit({event: "nodes.icons.get"},req); - res.json(redNodes.getNodeIcons()); + var opts = { + user: req.user + } + runtimeAPI.nodes.getIconList(opts).then(function(list) { + res.json(list); + }); } }; - -function putNode(node, enabled) { - var info; - var promise; - if (!node.err && node.enabled === enabled) { - promise = when.resolve(node); - } else { - if (enabled) { - promise = redNodes.enableNode(node.id); - } else { - promise = redNodes.disableNode(node.id); - } - } - return promise; -} diff --git a/red/api/editor/credentials.js b/red/api/editor/credentials.js index ae7429bcd..4cd8d3c9b 100644 --- a/red/api/editor/credentials.js +++ b/red/api/editor/credentials.js @@ -14,39 +14,23 @@ * limitations under the License. **/ -var log; -var api; +var runtimeAPI; +var apiUtils = require("../util"); module.exports = { - init: function(runtime) { - log = runtime.log; - api = runtime.nodes; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI }, get: function (req, res) { - // TODO: It should verify the given node id is of the type specified - - // but that would add a dependency from this module to the - // registry module that knows about node types. - var nodeType = req.params.type; - var nodeID = req.params.id; - log.audit({event: "credentials.get",type:nodeType,id:nodeID},req); - var credentials = api.getCredentials(nodeID); - if (!credentials) { - res.json({}); - return; + var opts = { + user: req.user, + type: req.params.type, + id: req.params.id } - var definition = api.getCredentialDefinition(nodeType); - - var sendCredentials = {}; - for (var cred in definition) { - if (definition.hasOwnProperty(cred)) { - if (definition[cred].type == "password") { - var key = 'has_' + cred; - sendCredentials[key] = credentials[cred] != null && credentials[cred] !== ''; - continue; - } - sendCredentials[cred] = credentials[cred] || ''; - } - } - res.json(sendCredentials); + runtimeAPI.flows.getNodeCredentials(opts).then(function(result) { + res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }) } } diff --git a/red/api/editor/index.js b/red/api/editor/index.js index e27d03d6c..6412c2411 100644 --- a/red/api/editor/index.js +++ b/red/api/editor/index.js @@ -25,6 +25,7 @@ var auth = require("../auth"); var nodes = require("../admin/nodes"); // TODO: move /icons into here var needsPermission; var runtime; +var runtimeAPI; var log; var apiUtil = require("../util"); @@ -38,16 +39,17 @@ var ensureRuntimeStarted = function(req,res,next) { } module.exports = { - init: function(server, _runtime) { + init: function(server, settings, _runtime, _runtimeAPI) { runtime = _runtime; + runtimeAPI = _runtimeAPI; log = runtime.log; needsPermission = auth.needsPermission; - var settings = runtime.settings; if (!settings.disableEditor) { - info.init(runtime); + info.init(runtimeAPI); comms.init(server,runtime); var ui = require("./ui"); + // ui is passed runtime so it get access runtime.nodes.getNodeIconPath ui.init(runtime); var editorApp = express(); if (settings.requireHttps === true) { @@ -67,7 +69,7 @@ module.exports = { editorApp.get("/icons/:scope/:module/:icon",ui.icon); var theme = require("./theme"); - theme.init(runtime); + theme.init(settings, runtime.version()); editorApp.use("/theme",theme.app()); editorApp.use("/",ui.editorResources); @@ -91,7 +93,7 @@ module.exports = { // Credentials var credentials = require("./credentials"); - credentials.init(runtime); + credentials.init(runtimeAPI); editorApp.get('/credentials/:type/:id', needsPermission("credentials.read"),credentials.get,apiUtil.errorHandler); // Settings @@ -100,11 +102,8 @@ module.exports = { editorApp.get("/settings/user",needsPermission("settings.read"),info.userSettings,apiUtil.errorHandler); // User Settings editorApp.post("/settings/user",needsPermission("settings.write"),info.updateUserSettings,apiUtil.errorHandler); - // SSH keys - var sshkeys = require("./sshkeys"); - sshkeys.init(runtime); - editorApp.use("/settings/user/keys",sshkeys.app()); + editorApp.use("/settings/user/keys",info.sshkeys()); return editorApp; } diff --git a/red/api/editor/settings.js b/red/api/editor/settings.js index 8b45d7271..4c2ea5847 100644 --- a/red/api/editor/settings.js +++ b/red/api/editor/settings.js @@ -13,117 +13,48 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -var theme = require("../editor/theme"); -var util = require('util'); -var runtime; -var settings; -var log; +var apiUtils = require("../util"); +var runtimeAPI; +var sshkeys = require("./sshkeys"); +var theme = require("./theme"); module.exports = { - init: function(_runtime) { - runtime = _runtime; - settings = runtime.settings; - log = runtime.log; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; + sshkeys.init(runtimeAPI); }, runtimeSettings: function(req,res) { - var safeSettings = { - httpNodeRoot: settings.httpNodeRoot||"/", - version: settings.version, + var opts = { user: req.user } - - var themeSettings = theme.settings(); - if (themeSettings) { - safeSettings.editorTheme = themeSettings; - } - - if (util.isArray(settings.paletteCategories)) { - safeSettings.paletteCategories = settings.paletteCategories; - } - - if (settings.flowFilePretty) { - safeSettings.flowFilePretty = settings.flowFilePretty; - } - - if (!runtime.nodes.paletteEditorEnabled()) { - safeSettings.editorTheme = safeSettings.editorTheme || {}; - safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {}; - safeSettings.editorTheme.palette.editable = false; - } - if (runtime.storage.projects) { - var activeProject = runtime.storage.projects.getActiveProject(); - if (activeProject) { - safeSettings.project = activeProject; - } else if (runtime.storage.projects.flowFileExists()) { - safeSettings.files = { - flow: runtime.storage.projects.getFlowFilename(), - credentials: runtime.storage.projects.getCredentialsFilename() - } + runtimeAPI.settings.getRuntimeSettings(opts).then(function(result) { + var themeSettings = theme.settings(); + if (themeSettings) { + result.editorTheme = themeSettings; } - safeSettings.git = { - globalUser: runtime.storage.projects.getGlobalGitUser() - } - } - - safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType(); - - settings.exportNodeSettings(safeSettings); - res.json(safeSettings); + res.json(result); + }); }, userSettings: function(req, res) { - var username; - if (!req.user || req.user.anonymous) { - username = '_'; - } else { - username = req.user.username; + var opts = { + user: req.user } - res.json(settings.getUserSettings(username)||{}); + runtimeAPI.settings.getUserSettings(opts).then(function(result) { + res.json(result); + }); }, updateUserSettings: function(req,res) { - var username; - if (!req.user || req.user.anonymous) { - username = '_'; - } else { - username = req.user.username; - } - var currentSettings = settings.getUserSettings(username)||{}; - currentSettings = extend(currentSettings, req.body); - try { - settings.setUserSettings(username, currentSettings).then(function() { - log.audit({event: "settings.update",username:username},req); - res.status(204).end(); - }).catch(function(err) { - log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); - }); - } catch(err) { - log.warn(log._("settings.user-not-available",{message:log._("settings.not-available")})); - log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()},req); - res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + var opts = { + user: req.user, + settings: req.body } + runtimeAPI.settings.updateUserSettings(opts).then(function(result) { + res.status(204).end(); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }); + }, + sshkeys: function() { + return sshkeys.app() } } - -function extend(target, source) { - var keys = Object.keys(source); - var i = keys.length; - while(i--) { - var value = source[keys[i]] - var type = typeof value; - if (type === 'string' || type === 'number' || type === 'boolean' || Array.isArray(value)) { - target[keys[i]] = value; - } else if (value === null) { - if (target.hasOwnProperty(keys[i])) { - delete target[keys[i]]; - } - } else { - // Object - if (target.hasOwnProperty(keys[i])) { - target[keys[i]] = extend(target[keys[i]],value); - } else { - target[keys[i]] = value; - } - } - } - return target; -} diff --git a/red/api/editor/sshkeys.js b/red/api/editor/sshkeys.js index 6f34d2b2d..62ccdc574 100644 --- a/red/api/editor/sshkeys.js +++ b/red/api/editor/sshkeys.js @@ -15,8 +15,7 @@ **/ var express = require("express"); -var os = require("os"); -var runtime; +var runtimeAPI; var needsPermission = require("../auth").needsPermission; function getUsername(userObj) { @@ -28,94 +27,66 @@ function getUsername(userObj) { } module.exports = { - init: function(_runtime) { - runtime = _runtime; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, app: function() { var app = express(); - // SSH keys - // List all SSH keys app.get("/", needsPermission("settings.read"), function(req,res) { - var username = getUsername(req.user); - runtime.storage.projects.ssh.listSSHKeys(username) - .then(function(list) { + var opts = { + user: req.user + } + runtimeAPI.settings.getUserKeys(opts).then(function(list) { res.json({ keys: list }); - }) - .catch(function(err) { - // console.log(err.stack); - if (err.code) { - res.status(400).json({error:err.code, message: err.message}); - } else { - res.status(400).json({error:"unexpected_error", message:err.toString()}); - } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); }); }); // Get SSH key detail app.get("/:id", needsPermission("settings.read"), function(req,res) { - var username = getUsername(req.user); - // console.log('username:', username); - runtime.storage.projects.ssh.getSSHKey(username, req.params.id) - .then(function(data) { - if (data) { - res.json({ - publickey: data - }); - } else { - res.status(404).end(); - } - }) - .catch(function(err) { - if (err.code) { - res.status(400).json({error:err.code, message: err.message}); - } else { - res.status(400).json({error:"unexpected_error", message:err.toString()}); - } + var opts = { + user: req.user, + id: req.params.id + } + runtimeAPI.settings.getUserKey(opts).then(function(data) { + res.json({ + publickey: data + }); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); }); }); // Generate a SSH key app.post("/", needsPermission("settings.write"), function(req,res) { - var username = getUsername(req.user); - // console.log('req.body:', req.body); - if ( req.body && req.body.name && /^[a-zA-Z0-9\-_]+$/.test(req.body.name)) { - runtime.storage.projects.ssh.generateSSHKey(username, req.body) - .then(function(name) { - // console.log('generate key --- success name:', name); - res.json({ - name: name - }); - }) - .catch(function(err) { - if (err.code) { - res.status(400).json({error:err.code, message: err.message}); - } else { - res.status(400).json({error:"unexpected_error", message:err.toString()}); - } + var opts = { + user: req.user, + id: req.params.id + } + runtimeAPI.settings.generateUserKey(opts).then(function(name) { + res.json({ + name: name }); - } - else { - res.status(400).json({error:"unexpected_error", message:"You need to have body or body.name"}); - } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }); }); // Delete a SSH key app.delete("/:id", needsPermission("settings.write"), function(req,res) { - var username = getUsername(req.user); - runtime.storage.projects.ssh.deleteSSHKey(username, req.params.id) - .then(function() { + var opts = { + user: req.user, + id: req.params.id + } + runtimeAPI.settings.generateUserKey(opts).then(function(name) { res.status(204).end(); - }) - .catch(function(err) { - if (err.code) { - res.status(400).json({error:err.code, message: err.message}); - } else { - res.status(400).json({error:"unexpected_error", message:err.toString()}); - } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); }); }); diff --git a/red/api/editor/theme.js b/red/api/editor/theme.js index 6699e7cd7..62e520579 100644 --- a/red/api/editor/theme.js +++ b/red/api/editor/theme.js @@ -40,7 +40,6 @@ var defaultContext = { var theme = null; var themeContext = clone(defaultContext); var themeSettings = null; -var runtime = null; var themeApp; @@ -78,11 +77,10 @@ function serveFilesFromTheme(themeValue, themeApp, directory) { } module.exports = { - init: function(runtime) { - var settings = runtime.settings; + init: function(settings,version) { themeContext = clone(defaultContext); - if (runtime.version) { - themeContext.version = runtime.version(); + if (version) { + themeContext.version = version; } themeSettings = null; theme = settings.editorTheme || {}; diff --git a/red/api/index.js b/red/api/index.js index dab88064c..7ee242ac9 100644 --- a/red/api/index.js +++ b/red/api/index.js @@ -26,13 +26,10 @@ var apiUtil = require("./util"); var adminApp; var server; -var runtime; var editor; -function init(_server,_runtime) { +function init(_server,settings,runtime,runtimeAPI) { server = _server; - runtime = _runtime; - var settings = runtime.settings; if (settings.httpAdminRoot !== false) { apiUtil.init(runtime); adminApp = express(); @@ -61,7 +58,7 @@ function init(_server,_runtime) { // Editor if (!settings.disableEditor) { editor = require("./editor"); - var editorApp = editor.init(server, runtime); + var editorApp = editor.init(server, settings, runtime, runtimeAPI); adminApp.use(editorApp); } @@ -70,7 +67,7 @@ function init(_server,_runtime) { adminApp.use(corsHandler); } - var adminApiApp = require("./admin").init(runtime); + var adminApiApp = require("./admin").init(runtimeAPI); adminApp.use(adminApiApp); } else { adminApp = null; diff --git a/red/api/util.js b/red/api/util.js index bb3e2be47..4a160a23d 100644 --- a/red/api/util.js +++ b/red/api/util.js @@ -41,5 +41,15 @@ module.exports = { lang = acceptedLanguages[0]; } return lang; + }, + rejectHandler: function(req,res,err) { + res.status(err.status||500); + if (err.code || err.message) { + res.json({ + code: err.code||"unexpected_error", + message: err.message + }) + } + res.end(); } } diff --git a/red/red.js b/red/red.js index 7aec5750a..15259330d 100644 --- a/red/red.js +++ b/red/red.js @@ -18,6 +18,8 @@ var fs = require("fs"); var path = require('path'); var runtime = require("./runtime"); +var runtimeAPI = require("./runtime-api"); + var api = require("./api"); process.env.NODE_RED_HOME = process.env.NODE_RED_HOME || path.resolve(__dirname+"/.."); @@ -67,7 +69,10 @@ module.exports = { if (userSettings.httpAdminRoot !== false) { runtime.init(userSettings,api); - api.init(httpServer,runtime); + + runtimeAPI.init(runtime); + api.init(httpServer,userSettings,runtime,runtimeAPI); + apiEnabled = true; server = runtime.adminApi.server; runtime.server = runtime.adminApi.server; diff --git a/red/runtime-api/auth.js b/red/runtime-api/auth.js new file mode 100644 index 000000000..420fef54b --- /dev/null +++ b/red/runtime-api/auth.js @@ -0,0 +1,19 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +/** + * @module red/auth + */ diff --git a/red/runtime-api/comms.js b/red/runtime-api/comms.js new file mode 100644 index 000000000..090a767bf --- /dev/null +++ b/red/runtime-api/comms.js @@ -0,0 +1,19 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +/** + * @module red/comms + */ diff --git a/red/runtime-api/flows.js b/red/runtime-api/flows.js new file mode 100644 index 000000000..f27ab4b34 --- /dev/null +++ b/red/runtime-api/flows.js @@ -0,0 +1,248 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + + /** + * @namespace RED.flows + */ + +/** + * @typedef Flows + * @alias Dave + * @type {object} + * @property {string} rev - the flow revision identifier + * @property {Array} flows - the flow configuration, an array of node configuration objects + */ + +/** + * @typedef Flow + * @type {object} + * @property {string} id - the flow identifier + * @property {string} label - a label for the flow + * @property {Array} nodes - an array of node configuration objects + */ + +var runtime; + +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, + /** + * Gets the current flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the active flow configuration + * @memberof RED.flows + */ + getFlows: function(opts) { + return new Promise(function(resolve,reject) { + runtime.log.audit({event: "flows.get"}/*,req*/); + var version = opts.version||"v1"; + return resolve(runtime.nodes.getFlows()); + }); + }, + /** + * Sets the current flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the active flow configuration + * @memberof RED.flows + */ + setFlows: function(opts) { + var err; + return new Promise(function(resolve,reject) { + + var flows = opts.flows; + var deploymentType = opts.deploymentType||"full"; + runtime.log.audit({event: "flows.set",type:deploymentType}/*,req*/); + + var apiPromise; + if (deploymentType === 'reload') { + apiPromise = runtime.nodes.loadFlows(); + } else { + if (flows.hasOwnProperty('rev')) { + var currentVersion = runtime.nodes.getFlows().rev; + if (currentVersion !== flows.rev) { + err = new Error(); + err.code = "version_mismatch"; + err.status = 409; + //TODO: log warning + return reject(err); + } + } + apiPromise = runtime.nodes.setFlows(flows.flows,deploymentType); + } + apiPromise.then(function(flowId) { + return resolve({rev:flowId}); + }).catch(function(err) { + log.warn(log._("api.flows.error-"+(deploymentType === 'reload'?'reload':'save'),{message:err.message})); + log.warn(err.stack); + return reject(err); + }); + }); + }, + + /** + * Adds a flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.flow - the flow to add + * @return {Promise} - the id of the added flow + * @memberof RED.flows + */ + addFlow: function(opts) { + return new Promise(function(resolve,reject) { + var flow = opts.flow; + runtime.nodes.addFlow(flow).then(function(id) { + runtime.log.audit({event: "flow.add",id:id}); + return resolve(id); + }).catch(function(err) { + runtime.log.audit({event: "flow.add",error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + }) + }) + + + }, + + /** + * Gets an individual flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.id - the id of the flow to retrieve + * @return {Promise} - the active flow configuration + * @memberof RED.flows + */ + getFlow: function(opts) { + return new Promise(function (resolve,reject) { + var flow = runtime.nodes.getFlow(opts.id); + if (flow) { + runtime.log.audit({event: "flow.get",id:opts.id}); + return resolve(flow); + } else { + runtime.log.audit({event: "flow.get",id:opts.id,error:"not_found"}); + var err = new Error(); + err.status = 404; + return reject(err); + } + }) + + }, + /** + * Updates an existing flow configuration + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.id - the id of the flow to update + * @param {Object} opts.flow - the flow configuration + * @return {Promise} - the id of the updated flow + * @memberof RED.flows + */ + updateFlow: function(opts) { + return new Promise(function (resolve,reject) { + var flow = opts.flow; + var id = opts.id; + try { + runtime.nodes.updateFlow(id,flow).then(function() { + runtime.log.audit({event: "flow.update",id:id}); + return resolve(id); + }).catch(function(err) { + runtime.log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + }) + } catch(err) { + if (err.code === 404) { + runtime.log.audit({event: "flow.update",id:id,error:"not_found"}); + // TODO: this swap around of .code and .status isn't ideal + err.status = 404; + err.code = "not_found"; + return reject(err); + } else { + runtime.log.audit({event: "flow.update",error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + } + }); + + }, + /** + * Deletes a flow + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.id - the id of the flow to delete + * @return {Promise} - resolves if successful + * @memberof RED.flows + */ + deleteFlow: function(opts) { + return new Promise(function (resolve,reject) { + var id = opts.id; + try { + runtime.nodes.removeFlow(id).then(function() { + log.audit({event: "flow.remove",id:id}); + return resolve(); + }) + } catch(err) { + if (err.code === 404) { + log.audit({event: "flow.remove",id:id,error:"not_found"}); + // TODO: this swap around of .code and .status isn't ideal + err.status = 404; + err.code = "not_found"; + return reject(err); + } else { + log.audit({event: "flow.remove",id:id,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + } + }); + }, + + /** + * Gets the safe credentials for a node + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.type - the node type to return the credential information for + * @param {String} opts.id - the node id + * @return {Promise} - the safe credentials + * @memberof RED.flows + */ + getNodeCredentials: function(opts) { + return new Promise(function(resolve,reject) { + log.audit({event: "credentials.get",type:opts.type,id:opts.id}); + var credentials = runtime.nodes.getCredentials(opts.id); + if (!credentials) { + return resolve({}); + } + var definition = runtime.nodes.getCredentialDefinition(opts.type); + + var sendCredentials = {}; + for (var cred in definition) { + if (definition.hasOwnProperty(cred)) { + if (definition[cred].type == "password") { + var key = 'has_' + cred; + sendCredentials[key] = credentials[cred] != null && credentials[cred] !== ''; + continue; + } + sendCredentials[cred] = credentials[cred] || ''; + } + } + resolve(sendCredentials); + }) + } + +} diff --git a/red/runtime-api/index.js b/red/runtime-api/index.js new file mode 100644 index 000000000..6a75d8583 --- /dev/null +++ b/red/runtime-api/index.js @@ -0,0 +1,64 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + + + /** + * A user accessing the API + * @typedef User + * @type {object} + */ + + +/** + * @namespace RED + */ +var api = module.exports = { + init: function(runtime) { + api.flows.init(runtime); + api.nodes.init(runtime); + api.settings.init(runtime); + }, + + /** + * Auth module + */ + auth: require("./auth"), + + /** + * Comms module + */ + comms: require("./comms"), + + /** + * Flows module + */ + flows: require("./flows"), + + /** + * Library module + */ + library: require("./library"), + + /** + * Nodes module + */ + nodes: require("./nodes"), + + /** + * Settings module + */ + settings: require("./settings") +} diff --git a/red/runtime-api/library.js b/red/runtime-api/library.js new file mode 100644 index 000000000..fbd265f88 --- /dev/null +++ b/red/runtime-api/library.js @@ -0,0 +1,35 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +/** + * @module red/library + */ + +module.exports = { + + /** + * Does something + */ + setEnty: function() {}, + /** + * Does something + */ + getEntry: function() {}, + /** + * Does something + */ + getEntries: function() {} +} diff --git a/red/runtime-api/nodes.js b/red/runtime-api/nodes.js new file mode 100644 index 000000000..5eb48b285 --- /dev/null +++ b/red/runtime-api/nodes.js @@ -0,0 +1,377 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ +"use strict" + /** + * @namespace RED.nodes + */ + +var runtime; + +function putNode(node, enabled) { + var promise; + if (!node.err && node.enabled === enabled) { + promise = Promise.resolve(node); + } else { + if (enabled) { + promise = runtime.nodes.enableNode(node.id); + } else { + promise = runtime.nodes.disableNode(node.id); + } + } + return promise; +} + + +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, + + + /** + * Gets the info of an individual node set + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.id - the id of the node set to return + * @return {Promise} - the node information + * @memberof RED.nodes + */ + getNodeInfo: function(opts) { + return new Promise(function(resolve,reject) { + var id = opts.id; + var result = redNodes.getNodeInfo(id); + if (result) { + runtime.log.audit({event: "nodes.info.get",id:id}); + delete result.loaded; + return resolve(result); + } else { + runtime.log.audit({event: "nodes.info.get",id:id,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + }) + }, + + /** + * Gets the list of node modules installed in the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the list of node modules + * @memberof RED.nodes + */ + getNodeList: function(opts) { + return new Promise(function(resolve,reject) { + runtime.log.audit({event: "nodes.list.get"}); + return resolve(runtime.nodes.getNodeList()); + }) + }, + + /** + * Gets an individual node's html content + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.id - the id of the node set to return + * @param {String} opts.lang - the locale language to return + * @return {Promise} - the node html content + * @memberof RED.nodes + */ + getNodeConfig: function(opts) { + return new Promise(function(resolve,reject) { + var id = opts.id; + var lang = opts.lang; + var result = runtime.nodes.getNodeConfig(id,lang); + if (result) { + runtime.log.audit({event: "nodes.config.get",id:id}); + return resolve(result); + } else { + runtime.log.audit({event: "nodes.config.get",id:id,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + }); + }, + /** + * Gets all node html content + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.lang - the locale language to return + * @return {Promise} - the node html content + * @memberof RED.nodes + */ + getNodeConfigs: function(opts) { + return new Promise(function(resolve,reject) { + runtime.log.audit({event: "nodes.configs.get"}); + return resolve(runtime.nodes.getNodeConfigs(opts.lang)); + }); + }, + + /** + * Gets the info of a node module + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.module - the id of the module to return + * @return {Promise} - the node module info + * @memberof RED.nodes + */ + getModuleInfo: function(opts) { + return new Promise(function(resolve,reject) { + var module = opts.module; + var result = redNodes.getModuleInfo(module); + if (result) { + runtime.log.audit({event: "nodes.module.get",id:id}); + return resolve(result); + } else { + runtime.log.audit({event: "nodes.module.get",id:id,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + }) + }, + + /** + * Install a new module into the runtime + * @param {Object} opts + * @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 + * @return {Promise} - the node module info + * @memberof RED.nodes + */ + addModule: function(opts) { + return new Promise(function(resolve,reject) { + if (!runtime.settings.available()) { + runtime.log.audit({event: "nodes.install",error:"settings_unavailable"}); + let err = new Error("Settings unavailable"); + err.code = "settings_unavailable"; + err.status = 400; + return reject(err); + } + if (opts.module) { + var existingModule = runtime.nodes.getModuleInfo(opts.module); + if (existingModule) { + if (!opts.version || existingModule.version === opts.version) { + runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_already_loaded"}); + let err = new Error("Module already loaded"); + err.code = "module_already_loaded"; + err.status = 400; + return reject(err); + } + if (!module.local) { + runtime.log.audit({event: "nodes.install",module:opts.module, version:opts.version, error:"module_not_local"}); + let err = new Error("Module not locally installed"); + err.code = "module_not_local"; + err.status = 400; + 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}); + 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"}); + // 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}); + } else { + err.status = 400; + runtime.log.audit({event: "nodes.install",module:opts.module,version:opts.version,error:err.code||"unexpected_error",message:err.toString()}); + } + return reject(err); + }) + } else { + runtime.log.audit({event: "nodes.install",module:opts.module,error:"invalid_request"}); + let err = new Error("Invalid request"); + err.code = "invalid_request"; + err.status = 400; + return reject(err); + } + + }); + }, + /** + * Removes a module from the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.module - the id of the module to remove + * @return {Promise} - resolves when complete + * @memberof RED.nodes + */ + removeModule: function(opts) { + return new Promise(function(resolve,reject) { + if (!runtime.settings.available()) { + runtime.log.audit({event: "nodes.install",error:"settings_unavailable"}); + let err = new Error("Settings unavailable"); + err.code = "settings_unavailable"; + err.status = 400; + return reject(err); + } + var module = runtime.nodes.getModuleInfo(opts.module); + if (!module) { + runtime.log.audit({event: "nodes.remove",module:opts.module,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + try { + runtime.nodes.uninstallModule(opts.module).then(function() { + runtime.log.audit({event: "nodes.remove",module:opts.module}); + resolve(); + }).catch(function(err) { + err.status = 400; + runtime.log.audit({event: "nodes.remove",module:opts.module,error:err.code||"unexpected_error",message:err.toString()}); + return reject(err); + }) + } catch(err) { + runtime.log.audit({event: "nodes.remove",module:opts.module,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + }); + }, + + /** + * Enables or disables a module in the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.module - the id of the module to enable or disable + * @param {String} opts.enabled - whether the module should be enabled or disabled + * @return {Promise} - the module info object + * @memberof RED.nodes + */ + setModuleState: function(opts) { + return new Promise(function(resolve,reject) { + if (!runtime.settings.available()) { + runtime.log.audit({event: "nodes.module.set",error:"settings_unavailable"}); + let err = new Error("Settings unavailable"); + err.code = "settings_unavailable"; + err.status = 400; + return reject(err); + } + try { + var mod = opts.module; + var module = runtime.nodes.getModuleInfo(mod); + if (!module) { + runtime.log.audit({event: "nodes.module.set",module:mod,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + + var nodes = module.nodes; + var promises = []; + for (var i = 0; i < nodes.length; ++i) { + promises.push(putNode(nodes[i],opts.enabled)); + } + Promise.all(promises).then(function() { + return resolve(runtime.nodes.getModuleInfo(mod)); + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + } catch(err) { + runtime.log.audit({event: "nodes.module.set",module:mod,enabled:opts.enabled,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + }); + }, + + /** + * Enables or disables a n individual node-set in the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.id - the id of the node-set to enable or disable + * @param {String} opts.enabled - whether the module should be enabled or disabled + * @return {Promise} - the module info object + * @memberof RED.nodes + */ + setNodeSetState: function(opts) { + return new Promise(function(resolve,reject) { + if (!runtime.settings.available()) { + runtime.log.audit({event: "nodes.info.set",error:"settings_unavailable"}); + let err = new Error("Settings unavailable"); + err.code = "settings_unavailable"; + err.status = 400; + return reject(err); + } + + var id = opts.id; + var enabled = opts.enabled; + try { + var node = runtime.nodes.getNodeInfo(id); + if (!node) { + runtime.log.audit({event: "nodes.info.set",id:id,error:"not_found"}); + var err = new Error(); + err.code = "not_found"; + err.status = 404; + return reject(err); + } else { + delete node.loaded; + putNode(node,enabled).then(function(result) { + runtime.log.audit({event: "nodes.info.set",id:id,enabled:enabled}); + return resolve(result); + }).catch(function(err) { + runtime.log.audit({event: "nodes.info.set",id:id,enabled:enabled,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + }); + } + } catch(err) { + runtime.log.audit({event: "nodes.info.set",id:id,enabled:enabled,error:err.code||"unexpected_error",message:err.toString()}); + res.status(400).json({error:err.code||"unexpected_error", message:err.toString()}); + } + }); + }, + + /** + * TODO: getModuleCatalogs + */ + getModuleCatalogs: function() {}, + /** + * TODO: getModuleCatalog + */ + getModuleCatalog: function() {}, + + /** + * Gets the list of all icons available in the modules installed within the runtime + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the list of all icons + * @memberof RED.nodes + */ + getIconList: function(opts) { + return new Promise(function(resolve,reject) { + runtime.log.audit({event: "nodes.icons.get"}); + return resolve(runtime.nodes.getNodeIcons()); + }); + + }, + /** + * TODO: getIcon + */ + getIcon: function() {} +} diff --git a/red/runtime-api/settings.js b/red/runtime-api/settings.js new file mode 100644 index 000000000..e10a6d9c2 --- /dev/null +++ b/red/runtime-api/settings.js @@ -0,0 +1,257 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +/** + * @namespace RED.settings + */ + +var util = require("util"); +var runtime; + +function extend(target, source) { + var keys = Object.keys(source); + var i = keys.length; + while(i--) { + var value = source[keys[i]] + var type = typeof value; + if (type === 'string' || type === 'number' || type === 'boolean' || Array.isArray(value)) { + target[keys[i]] = value; + } else if (value === null) { + if (target.hasOwnProperty(keys[i])) { + delete target[keys[i]]; + } + } else { + // Object + if (target.hasOwnProperty(keys[i])) { + target[keys[i]] = extend(target[keys[i]],value); + } else { + target[keys[i]] = value; + } + } + } + return target; +} + +function getUsername(userObj) { + var username = '__default'; + if ( userObj && userObj.name ) { + username = userObj.name; + } + return username; +} +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, + /** + * Gets the runtime settings object + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the runtime settings + * @memberof RED.settings + */ + getRuntimeSettings: function(opts) { + return new Promise(function(resolve,reject) { + try { + var safeSettings = { + httpNodeRoot: runtime.settings.httpNodeRoot||"/", + version: runtime.settings.version, + user: opts.user + } + + if (util.isArray(runtime.settings.paletteCategories)) { + safeSettings.paletteCategories = runtime.settings.paletteCategories; + } + + if (runtime.settings.flowFilePretty) { + safeSettings.flowFilePretty = runtime.settings.flowFilePretty; + } + + if (!runtime.nodes.paletteEditorEnabled()) { + safeSettings.editorTheme = safeSettings.editorTheme || {}; + safeSettings.editorTheme.palette = safeSettings.editorTheme.palette || {}; + safeSettings.editorTheme.palette.editable = false; + } + if (runtime.storage.projects) { + var activeProject = runtime.storage.projects.getActiveProject(); + if (activeProject) { + safeSettings.project = activeProject; + } else if (runtime.storage.projects.flowFileExists()) { + safeSettings.files = { + flow: runtime.storage.projects.getFlowFilename(), + credentials: runtime.storage.projects.getCredentialsFilename() + } + } + safeSettings.git = { + globalUser: runtime.storage.projects.getGlobalGitUser() + } + } + + safeSettings.flowEncryptionType = runtime.nodes.getCredentialKeyType(); + + runtime.settings.exportNodeSettings(safeSettings); + + resolve(safeSettings); + }catch(err) { + console.log(err); + } + }); + }, + + /** + * Gets an individual user's settings object + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the user settings + * @memberof RED.settings + */ + getUserSettings: function(opts) { + var username; + if (!opts.user || opts.user.anonymous) { + username = '_'; + } else { + username = opts.user.username; + } + return Promise.resolve(runtime.settings.getUserSettings(username)||{}); + }, + + /** + * Updates an individual user's settings object. + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {Object} opts.settings - the updates to the user settings + * @return {Promise} - the user settings + * @memberof RED.settings + */ + updateUserSettings: function(opts) { + var username; + if (!opts.user || opts.user.anonymous) { + username = '_'; + } else { + username = opts.user.username; + } + return new Promise(function(resolve,reject) { + var currentSettings = runtime.settings.getUserSettings(username)||{}; + currentSettings = extend(currentSettings, opts.settings); + try { + runtime.settings.setUserSettings(username, currentSettings).then(function() { + runtime.log.audit({event: "settings.update",username:username}); + return resolve(); + }).catch(function(err) { + runtime.log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + }); + } catch(err) { + log.warn(log._("settings.user-not-available",{message:log._("settings.not-available")})); + log.audit({event: "settings.update",username:username,error:err.code||"unexpected_error",message:err.toString()}); + err.status = 400; + return reject(err); + } + }); + }, + + /** + * Gets a list of a user's ssh keys + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @return {Promise} - the user's ssh keys + * @memberof RED.settings + */ + getUserKeys: function(opts) { + return new Promise(function(resolve,reject) { + var username = getUsername(opts.user); + runtime.storage.projects.ssh.listSSHKeys(username).then(function(list) { + return resolve(list); + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + }); + }, + + /** + * Gets a user's ssh public key + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.id - the id of the key to return + * @return {Promise} - the user's ssh public key + * @memberof RED.settings + */ + getUserKey: function(opts) { + return new Promise(function(resolve,reject) { + var username = getUsername(opts.user); + // console.log('username:', username); + runtime.storage.projects.ssh.getSSHKey(username, opts.id).then(function(data) { + if (data) { + return resolve(data); + } else { + var err = new Error("Key not found"); + err.code = "not_found"; + err.status = 404; + return reject(err); + } + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + }); + }, + + /** + * Generates a new ssh key pair + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.name - the id of the key to return + * @param {User} opts.password - (optional) the password for the key pair + * @param {User} opts.comment - (option) a comment to associate with the key pair + * @param {User} opts.size - (optional) the size of the key. Default: 2048 + * @return {Promise} - the id of the generated key + * @memberof RED.settings + */ + generateUserKey: function(opts) { + return new Promise(function(resolve,reject) { + var username = getUsername(opts.user); + runtime.storage.projects.ssh.generateSSHKey(username, opts).then(function(name) { + return resolve(name); + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + }); + }, + + /** + * Deletes a user's ssh key pair + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {User} opts.id - the id of the key to delete + * @return {Promise} - resolves when deleted + * @memberof RED.settings + */ + removeUserKey: function(opts) { + return new Promise(function(resolve,reject) { + var username = getUsername(req.user); + runtime.storage.projects.ssh.deleteSSHKey(username, opts.id).then(function() { + return resolve(); + }).catch(function(err) { + err.status = 400; + return reject(err); + }); + }); + + } +} diff --git a/red/runtime/storage/localfilesystem/projects/ssh/index.js b/red/runtime/storage/localfilesystem/projects/ssh/index.js index dd65379cb..cc9bd8754 100644 --- a/red/runtime/storage/localfilesystem/projects/ssh/index.js +++ b/red/runtime/storage/localfilesystem/projects/ssh/index.js @@ -107,6 +107,11 @@ function getSSHKey(username, name) { function generateSSHKey(username, options) { options = options || {}; var name = options.name || ""; + if (!/^[a-zA-Z0-9\-_]+$/.test(options.name)) { + var err = new Error("Invalid SSH Key name"); + e.code = "invalid_key_name"; + return Promise.reject(err); + } return checkExistSSHKeyFiles(username, name) .then(function(result) { if ( result ) { From 7409cb3abb59f49821359cf9373861b9302a9db8 Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 18 Apr 2018 17:09:31 +0100 Subject: [PATCH 02/51] Separate library api and runtime components --- red/api/editor/index.js | 8 +- red/api/editor/library.js | 178 ++++++++------------------- red/runtime-api/flows.js | 2 +- red/runtime-api/index.js | 1 + red/runtime-api/library.js | 102 +++++++++++++-- red/runtime-api/nodes.js | 21 +--- red/runtime/index.js | 3 + red/runtime/library/index.js | 100 +++++++++++++++ red/runtime/nodes/registry/loader.js | 8 +- 9 files changed, 263 insertions(+), 160 deletions(-) create mode 100644 red/runtime/library/index.js diff --git a/red/api/editor/index.js b/red/api/editor/index.js index 6412c2411..ee992bdf2 100644 --- a/red/api/editor/index.js +++ b/red/api/editor/index.js @@ -86,10 +86,12 @@ module.exports = { // Library var library = require("./library"); - library.init(editorApp,runtime); - editorApp.post(new RegExp("/library/flows\/(.*)"),needsPermission("library.write"),library.post,apiUtil.errorHandler); + library.init(editorApp,runtimeAPI); + editorApp.get("/library/flows",needsPermission("library.read"),library.getAll,apiUtil.errorHandler); - editorApp.get(new RegExp("/library/flows\/(.*)"),needsPermission("library.read"),library.get,apiUtil.errorHandler); + editorApp.get(/library\/([^\/]+)(?:$|\/(.*))/,needsPermission("library.read"),library.getEntry); + editorApp.post(/library\/([^\/]+)\/(.*)/,needsPermission("library.write"),library.saveEntry); + // Credentials var credentials = require("./credentials"); diff --git a/red/api/editor/library.js b/red/api/editor/library.js index d5cbb020f..be1b8ee5b 100644 --- a/red/api/editor/library.js +++ b/red/api/editor/library.js @@ -13,149 +13,71 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +var apiUtils = require("../util"); var fs = require('fs'); var fspath = require('path'); var when = require('when'); -var redApp = null; -var storage; -var log; -var redNodes; -var needsPermission = require("../auth").needsPermission; - -function createLibrary(type) { - if (redApp) { - redApp.get(new RegExp("/library/"+type+"($|\/(.*))"),needsPermission("library.read"),function(req,res) { - var path = req.params[1]||""; - storage.getLibraryEntry(type,path).then(function(result) { - log.audit({event: "library.get",type:type},req); - if (typeof result === "string") { - res.writeHead(200, {'Content-Type': 'text/plain'}); - res.write(result); - res.end(); - } else { - res.json(result); - } - }).catch(function(err) { - if (err) { - log.warn(log._("api.library.error-load-entry",{path:path,message:err.toString()})); - if (err.code === 'forbidden') { - log.audit({event: "library.get",type:type,error:"forbidden"},req); - res.status(403).end(); - return; - } - } - log.audit({event: "library.get",type:type,error:"not_found"},req); - res.status(404).end(); - }); - }); - - redApp.post(new RegExp("/library/"+type+"\/(.*)"),needsPermission("library.write"),function(req,res) { - var path = req.params[0]; - var meta = req.body; - var text = meta.text; - delete meta.text; - - storage.saveLibraryEntry(type,path,meta,text).then(function() { - log.audit({event: "library.set",type:type},req); - res.status(204).end(); - }).catch(function(err) { - log.warn(log._("api.library.error-save-entry",{path:path,message:err.toString()})); - if (err.code === 'forbidden') { - log.audit({event: "library.set",type:type,error:"forbidden"},req); - res.status(403).end(); - return; - } - log.audit({event: "library.set",type:type,error:"unexpected_error",message:err.toString()},req); - res.status(500).json({error:"unexpected_error", message:err.toString()}); - }); - }); - } -} +var runtimeAPI; module.exports = { - init: function(app,runtime) { - redApp = app; - log = runtime.log; - storage = runtime.storage; - redNodes = runtime.nodes; + init: function(app,_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, - register: createLibrary, getAll: function(req,res) { - storage.getAllFlows().then(function(flows) { - log.audit({event: "library.get.all",type:"flow"},req); - var examples = redNodes.getNodeExampleFlows(); - if (examples) { - flows.d = flows.d||{}; - flows.d._examples_ = redNodes.getNodeExampleFlows(); - } - res.json(flows); + var opts = { + user: req.user, + type: 'flows' + } + runtimeAPI.library.getEntries(opts).then(function(result) { + res.json(result); + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); }); }, - get: function(req,res) { - if (req.params[0].indexOf("_examples_/") === 0) { - var m = /^_examples_\/(@.*?\/[^\/]+|[^\/]+)\/(.*)$/.exec(req.params[0]); - if (m) { - var module = m[1]; - var path = m[2]; - var fullPath = redNodes.getNodeExampleFlowPath(module,path); - if (fullPath) { - try { - fs.statSync(fullPath); - log.audit({event: "library.get",type:"flow",path:req.params[0]},req); - return res.sendFile(fullPath,{ - headers:{ - 'Content-Type': 'application/json' - } - }) - } catch(err) { - console.log(err); - } - } - } - // IF we get here, we didn't find the file - log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req); - return res.status(404).end(); - } else { - storage.getFlow(req.params[0]).then(function(data) { - // data is already a JSON string - log.audit({event: "library.get",type:"flow",path:req.params[0]},req); - res.set('Content-Type', 'application/json'); - res.send(data); - }).catch(function(err) { - if (err) { - log.warn(log._("api.library.error-load-flow",{path:req.params[0],message:err.toString()})); - if (err.code === 'forbidden') { - log.audit({event: "library.get",type:"flow",path:req.params[0],error:"forbidden"},req); - res.status(403).end(); - return; - } - } - log.audit({event: "library.get",type:"flow",path:req.params[0],error:"not_found"},req); - res.status(404).end(); - }); + getEntry: function(req,res) { + var opts = { + user: req.user, + type: req.params[0], + path: req.params[1]||"" } + runtimeAPI.library.getEntry(opts).then(function(result) { + if (typeof result === "string") { + if (opts.type === 'flows') { + res.writeHead(200, {'Content-Type': 'application/json'}); + } else { + res.writeHead(200, {'Content-Type': 'text/plain'}); + } + res.write(result); + res.end(); + } else { + res.json(result); + } + }).catch(function(err) { + apiUtils.rejectHandler(req,res,err); + }); }, - post: function(req,res) { - // if (req.params[0].indexOf("_examples_/") === 0) { - // log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:"forbidden"})); - // log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req); - // return res.status(403).send({error:"unexpected_error", message:"forbidden"}); - // } - var flow = JSON.stringify(req.body); - storage.saveFlow(req.params[0],flow).then(function() { - log.audit({event: "library.set",type:"flow",path:req.params[0]},req); + saveEntry: function(req,res) { + var opts = { + user: req.user, + type: req.params[0], + path: req.params[1]||"" + } + // TODO: horrible inconsistencies between flows and all other types + if (opts.type === "flows") { + opts.meta = {}; + opts.body = JSON.stringify(req.body); + } else { + opts.meta = req.body; + opts.body = opts.meta.text; + delete opts.meta.text; + } + runtimeAPI.library.saveEntry(opts).then(function(result) { res.status(204).end(); }).catch(function(err) { - log.warn(log._("api.library.error-save-flow",{path:req.params[0],message:err.toString()})); - if (err.code === 'forbidden') { - log.audit({event: "library.set",type:"flow",path:req.params[0],error:"forbidden"},req); - res.status(403).end(); - return; - } - log.audit({event: "library.set",type:"flow",path:req.params[0],error:"unexpected_error",message:err.toString()},req); - res.status(500).send({error:"unexpected_error", message:err.toString()}); + apiUtils.rejectHandler(req,res,err); }); } } diff --git a/red/runtime-api/flows.js b/red/runtime-api/flows.js index f27ab4b34..bd858864f 100644 --- a/red/runtime-api/flows.js +++ b/red/runtime-api/flows.js @@ -223,7 +223,7 @@ var api = module.exports = { */ getNodeCredentials: function(opts) { return new Promise(function(resolve,reject) { - log.audit({event: "credentials.get",type:opts.type,id:opts.id}); + runtime.log.audit({event: "credentials.get",type:opts.type,id:opts.id}); var credentials = runtime.nodes.getCredentials(opts.id); if (!credentials) { return resolve({}); diff --git a/red/runtime-api/index.js b/red/runtime-api/index.js index 6a75d8583..75ee05602 100644 --- a/red/runtime-api/index.js +++ b/red/runtime-api/index.js @@ -30,6 +30,7 @@ var api = module.exports = { api.flows.init(runtime); api.nodes.init(runtime); api.settings.init(runtime); + api.library.init(runtime); }, /** diff --git a/red/runtime-api/library.js b/red/runtime-api/library.js index fbd265f88..fb0658554 100644 --- a/red/runtime-api/library.js +++ b/red/runtime-api/library.js @@ -15,21 +15,107 @@ **/ /** - * @module red/library + * @namespace RED.library */ -module.exports = { +var runtime; + +var api = module.exports = { + init: function(_runtime) { + runtime = _runtime; + }, /** - * Does something + * Gets an entry from the library. + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.type - the type of entry + * @param {String} opts.path - the path of the entry + * @return {Promise} - resolves when complete + * @memberof RED.library */ - setEnty: function() {}, + getEntry: function(opts) { + return new Promise(function(resolve,reject) { + runtime.library.getEntry(opts.type,opts.path).then(function(result) { + runtime.log.audit({event: "library.get",type:opts.type,path:opts.path}); + return resolve(result); + }).catch(function(err) { + if (err) { + runtime.log.warn(runtime.log._("api.library.error-load-entry",{path:opts.path,message:err.toString()})); + if (err.code === 'forbidden') { + err.status = 403; + return reject(err); + } else if (err.code === "not_found") { + err.status = 404; + } else { + err.status = 400; + } + runtime.log.audit({event: "library.get",type:opts.type,path:opts.path,error:err.code}); + return reject(err); + } + runtime.log.audit({event: "library.get",type:type,error:"not_found"}); + var error = new Error(); + error.code = "not_found"; + error.status = 404; + return reject(error); + }); + }) + }, + /** - * Does something + * Saves an entry to the library + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.type - the type of entry + * @param {String} opts.path - the path of the entry + * @param {Object} opts.meta - any meta data associated with the entry + * @param {String} opts.body - the body of the entry + * @return {Promise} - resolves when complete + * @memberof RED.library */ - getEntry: function() {}, + saveEntry: function(opts) { + return new Promise(function(resolve,reject) { + runtime.library.saveEntry(opts.type,opts.path,opts.meta,opts.body).then(function() { + runtime.log.audit({event: "library.set",type:opts.type,path:opts.path}); + return resolve(); + }).catch(function(err) { + runtime.log.warn(runtime.log._("api.library.error-save-entry",{path:opts.path,message:err.toString()})); + if (err.code === 'forbidden') { + runtime.log.audit({event: "library.set",type:opts.type,path:opts.path,error:"forbidden"}); + err.status = 403; + return reject(err); + } + runtime.log.audit({event: "library.set",type:opts.type,path:opts.path,error:"unexpected_error",message:err.toString()}); + var error = new Error(); + error.code = "not_found"; + error.status = 400; + return reject(error); + }); + }) + }, /** - * Does something + * Returns a complete listing of all entries of a given type in the library. + * @param {Object} opts + * @param {User} opts.user - the user calling the api + * @param {String} opts.type - the type of entry + * @return {Promise} - the entry listing + * @memberof RED.library */ - getEntries: function() {} + getEntries: function(opts) { + return new Promise(function(resolve,reject) { + if (opts.type !== 'flows') { + return reject(new Error("API only supports flows")); + + } + runtime.storage.getAllFlows().then(function(flows) { + runtime.log.audit({event: "library.get.all",type:"flow"}); + var examples = runtime.nodes.getNodeExampleFlows(); + if (examples) { + flows.d = flows.d||{}; + flows.d._examples_ = examples; + } + return resolve(flows); + }); + }) + } } diff --git a/red/runtime-api/nodes.js b/red/runtime-api/nodes.js index 5eb48b285..714372e65 100644 --- a/red/runtime-api/nodes.js +++ b/red/runtime-api/nodes.js @@ -14,27 +14,12 @@ * limitations under the License. **/ "use strict" - /** - * @namespace RED.nodes - */ +/** + * @namespace RED.nodes + */ var runtime; -function putNode(node, enabled) { - var promise; - if (!node.err && node.enabled === enabled) { - promise = Promise.resolve(node); - } else { - if (enabled) { - promise = runtime.nodes.enableNode(node.id); - } else { - promise = runtime.nodes.disableNode(node.id); - } - } - return promise; -} - - var api = module.exports = { init: function(_runtime) { runtime = _runtime; diff --git a/red/runtime/index.js b/red/runtime/index.js index 6577ee59c..e8fc0a44b 100644 --- a/red/runtime/index.js +++ b/red/runtime/index.js @@ -18,6 +18,7 @@ var when = require('when'); var redNodes = require("./nodes"); var storage = require("./storage"); +var library = require("./library"); var log = require("./log"); var i18n = require("./i18n"); var events = require("./events"); @@ -65,6 +66,7 @@ function init(userSettings,_adminApi) { adminApi = _adminApi; } redNodes.init(runtime); + library.init(runtime); } var version; @@ -245,6 +247,7 @@ var runtime = module.exports = { storage: storage, events: events, nodes: redNodes, + library: library, util: require("./util"), get adminApi() { return adminApi }, get nodeApp() { return nodeApp }, diff --git a/red/runtime/library/index.js b/red/runtime/library/index.js new file mode 100644 index 000000000..ce95d66ff --- /dev/null +++ b/red/runtime/library/index.js @@ -0,0 +1,100 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var fs = require('fs'); +var fspath = require('path'); + +var runtime; +var knownTypes = {}; + +var storage; + +function init(_runtime) { + runtime = _runtime; + storage = runtime.storage; + knownTypes = {}; +} + +function registerType(id,type) { + if (knownTypes.hasOwnProperty(type)) { + throw new Error(`Library type '${type}' already registerd by ${id}'`) + } + knownTypes[type] = id; +} + +function getAllEntries(type) { + if (!knownTypes.hasOwnProperty(type)) { + throw new Error(`Unknown library type '${type}'`); + } +} +function getEntry(type,path) { + if (type !== 'flows') { + if (!knownTypes.hasOwnProperty(type)) { + throw new Error(`Unknown library type '${type}'`); + } + return storage.getLibraryEntry(type,path); + } else { + return new Promise(function(resolve,reject) { + if (path.indexOf("_examples_/") === 0) { + var m = /^_examples_\/(@.*?\/[^\/]+|[^\/]+)\/(.*)$/.exec(path); + if (m) { + var module = m[1]; + var entryPath = m[2]; + var fullPath = runtime.nodes.getNodeExampleFlowPath(module,entryPath); + if (fullPath) { + try { + fs.readFile(fullPath,'utf8',function(err, data) { + runtime.log.audit({event: "library.get",type:"flow",path:path}); + if (err) { + return reject(err); + } + return resolve(data); + }) + } catch(err) { + return reject(err); + } + } + } else { + // IF we get here, we didn't find the file + var error = new Error("not_found"); + error.code = "not_found"; + return reject(error); + } + } else { + resolve(storage.getFlow(path)); + } + }); + } +} +function saveEntry(type,path,meta,body) { + if (type !== 'flows') { + if (!knownTypes.hasOwnProperty(type)) { + throw new Error(`Unknown library type '${type}'`); + } + return storage.saveLibraryEntry(type,path,meta,body); + } else { + return storage.saveFlow(path,body); + } +} + +module.exports = { + init: init, + registerType: registerType, + getAllEntries: getAllEntries, + getEntry: getEntry, + saveEntry: saveEntry + +} diff --git a/red/runtime/nodes/registry/loader.js b/red/runtime/nodes/registry/loader.js index f88463609..7f677f3da 100644 --- a/red/runtime/nodes/registry/loader.js +++ b/red/runtime/nodes/registry/loader.js @@ -80,7 +80,11 @@ function createNodeApi(node) { copyObjectProperties(runtime.settings,red.settings,null,["init","load","reset"]); if (runtime.adminApi) { red.comms = runtime.adminApi.comms; - red.library = runtime.adminApi.library; + red.library = { + register: function(type) { + return runtime.library.registerType(node.id,type); + } + }; red.auth = runtime.adminApi.auth; red.httpAdmin = runtime.adminApi.adminApp; red.httpNode = runtime.nodeApp; @@ -377,7 +381,7 @@ function addModule(module) { function loadNodeHelp(node,lang) { var base = path.basename(node.template); - var localePath = undefined; + var localePath; if (node.module === 'node-red') { var cat_dir = path.dirname(node.template); var cat = path.basename(cat_dir); From 1cdb039ea23bd61345fc346ac39ab309f957d1aa Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 19 Apr 2018 11:23:08 +0100 Subject: [PATCH 03/51] Move log and i18n to their own utils module --- red/api/auth/index.js | 10 +++--- red/api/auth/strategies.js | 5 +-- red/api/editor/index.js | 11 +++--- red/api/index.js | 3 +- red/api/util.js | 9 ++--- red/red.js | 13 +++---- red/runtime-api/index.js | 3 +- red/runtime/index.js | 22 ++++++------ red/runtime/nodes/Node.js | 2 +- red/runtime/nodes/credentials.js | 3 +- red/runtime/nodes/flows/Flow.js | 2 +- red/runtime/nodes/flows/index.js | 2 +- red/runtime/nodes/index.js | 3 +- red/runtime/nodes/registry/installer.js | 2 +- red/runtime/nodes/registry/localfilesystem.js | 6 ++-- red/runtime/settings.js | 2 +- red/runtime/storage/index.js | 2 +- red/runtime/storage/localfilesystem/index.js | 3 +- .../storage/localfilesystem/sessions.js | 3 +- .../storage/localfilesystem/settings.js | 2 +- red/runtime/storage/localfilesystem/util.js | 2 +- red/{runtime => util}/i18n.js | 35 +++++++++++-------- red/util/index.js | 28 +++++++++++++++ red/{runtime => util}/log.js | 0 24 files changed, 101 insertions(+), 72 deletions(-) rename red/{runtime => util}/i18n.js (83%) create mode 100644 red/util/index.js rename red/{runtime => util}/log.js (100%) diff --git a/red/api/auth/index.js b/red/api/auth/index.js index 211a712a7..355226a3f 100644 --- a/red/api/auth/index.js +++ b/red/api/auth/index.js @@ -25,7 +25,7 @@ var permissions = require("./permissions"); var theme = require("../editor/theme"); var settings = null; -var log = null +var log = require("../../util").log; // TODO: separate module passport.use(strategies.bearerStrategy.BearerStrategy); @@ -36,13 +36,11 @@ var server = oauth2orize.createServer(); server.exchange(oauth2orize.exchange.password(strategies.passwordTokenExchange)); -function init(runtime) { - settings = runtime.settings; - log = runtime.log; +function init(_settings,storage) { + settings = _settings; if (settings.adminAuth) { Users.init(settings.adminAuth); - Tokens.init(settings.adminAuth,runtime.storage); - strategies.init(runtime); + Tokens.init(settings.adminAuth,storage); } } diff --git a/red/api/auth/strategies.js b/red/api/auth/strategies.js index 0f5b554e4..e203437d1 100644 --- a/red/api/auth/strategies.js +++ b/red/api/auth/strategies.js @@ -26,7 +26,7 @@ var Users = require("./users"); var Clients = require("./clients"); var permissions = require("./permissions"); -var log; +var log = require("../../util").log; // TODO: separate module var bearerStrategy = function (accessToken, done) { // is this a valid token? @@ -124,9 +124,6 @@ AnonymousStrategy.prototype.authenticate = function(req) { } module.exports = { - init: function(runtime) { - log = runtime.log; - }, bearerStrategy: bearerStrategy, clientPasswordStrategy: clientPasswordStrategy, passwordTokenExchange: passwordTokenExchange, diff --git a/red/api/editor/index.js b/red/api/editor/index.js index ee992bdf2..d204d9411 100644 --- a/red/api/editor/index.js +++ b/red/api/editor/index.js @@ -26,7 +26,9 @@ var nodes = require("../admin/nodes"); // TODO: move /icons into here var needsPermission; var runtime; var runtimeAPI; -var log; +var log = require("../../util").log; // TODO: separate module +var i18n = require("../../util").i18n; // TODO: separate module + var apiUtil = require("../util"); var ensureRuntimeStarted = function(req,res,next) { @@ -42,15 +44,16 @@ module.exports = { init: function(server, settings, _runtime, _runtimeAPI) { runtime = _runtime; runtimeAPI = _runtimeAPI; - log = runtime.log; needsPermission = auth.needsPermission; if (!settings.disableEditor) { info.init(runtimeAPI); comms.init(server,runtime); var ui = require("./ui"); - // ui is passed runtime so it get access runtime.nodes.getNodeIconPath + + // TODO: ui is passed runtime so it get access runtime.nodes.getNodeIconPath ui.init(runtime); + var editorApp = express(); if (settings.requireHttps === true) { editorApp.enable('trust proxy'); @@ -112,7 +115,7 @@ module.exports = { }, start: function() { var catalogPath = path.resolve(path.join(__dirname,"locales")); - return runtime.i18n.registerMessageCatalogs([ + return i18n.registerMessageCatalogs([ {namespace: "editor", dir: catalogPath, file:"editor.json"}, {namespace: "jsonata", dir: catalogPath, file:"jsonata.json"}, {namespace: "infotips", dir: catalogPath, file:"infotips.json"} diff --git a/red/api/index.js b/red/api/index.js index 7ee242ac9..a46ba6968 100644 --- a/red/api/index.js +++ b/red/api/index.js @@ -31,9 +31,8 @@ var editor; function init(_server,settings,runtime,runtimeAPI) { server = _server; if (settings.httpAdminRoot !== false) { - apiUtil.init(runtime); adminApp = express(); - auth.init(runtime); + auth.init(settings,runtime.storage); var maxApiRequestSize = settings.apiMaxLength || '5mb'; adminApp.use(bodyParser.json({limit:maxApiRequestSize})); diff --git a/red/api/util.js b/red/api/util.js index 4a160a23d..6a6d97e1d 100644 --- a/red/api/util.js +++ b/red/api/util.js @@ -15,14 +15,11 @@ **/ -var i18n; -var log; +var log = require("../util").log; // TODO: separate module +var i18n = require("../util").i18n; // TODO: separate module + module.exports = { - init: function(_runtime) { - log = _runtime.log; - i18n = _runtime.i18n; - }, errorHandler: function(err,req,res,next) { console.error(err.stack); if (err.message === "request entity too large") { diff --git a/red/red.js b/red/red.js index 15259330d..405364a94 100644 --- a/red/red.js +++ b/red/red.js @@ -19,6 +19,7 @@ var path = require('path'); var runtime = require("./runtime"); var runtimeAPI = require("./runtime-api"); +var redUtil = require("./util"); var api = require("./api"); @@ -66,18 +67,18 @@ module.exports = { if (!userSettings.coreNodesDir) { userSettings.coreNodesDir = path.resolve(path.join(__dirname,"..","nodes")); } - + redUtil.init(userSettings); if (userSettings.httpAdminRoot !== false) { - runtime.init(userSettings,api); + runtime.init(userSettings,redUtil,api); - runtimeAPI.init(runtime); - api.init(httpServer,userSettings,runtime,runtimeAPI); + runtimeAPI.init(runtime,redUtil); + api.init(httpServer,userSettings,runtime,runtimeAPI,redUtil); apiEnabled = true; server = runtime.adminApi.server; runtime.server = runtime.adminApi.server; } else { - runtime.init(userSettings); + runtime.init(userSettings,redUtil); apiEnabled = false; if (httpServer){ server = httpServer; @@ -106,7 +107,7 @@ module.exports = { }) }, nodes: runtime.nodes, - log: runtime.log, + get log() { return redUtil.log }, settings:runtime.settings, util: runtime.util, version: runtime.version, diff --git a/red/runtime-api/index.js b/red/runtime-api/index.js index 75ee05602..ab1342953 100644 --- a/red/runtime-api/index.js +++ b/red/runtime-api/index.js @@ -21,12 +21,11 @@ * @type {object} */ - /** * @namespace RED */ var api = module.exports = { - init: function(runtime) { + init: function(runtime, redUtil) { api.flows.init(runtime); api.nodes.init(runtime); api.settings.init(runtime); diff --git a/red/runtime/index.js b/red/runtime/index.js index e8fc0a44b..05f7fcdba 100644 --- a/red/runtime/index.js +++ b/red/runtime/index.js @@ -19,8 +19,6 @@ var when = require('when'); var redNodes = require("./nodes"); var storage = require("./storage"); var library = require("./library"); -var log = require("./log"); -var i18n = require("./i18n"); var events = require("./events"); var settings = require("./settings"); @@ -29,6 +27,10 @@ var path = require('path'); var fs = require("fs"); var os = require("os"); +var redUtil; +var log; +var i18n; + var runtimeMetricInterval = null; var started = false; @@ -55,9 +57,12 @@ var adminApi = { var nodeApp; -function init(userSettings,_adminApi) { +function init(userSettings,_redUtil,_adminApi) { + redUtil = _redUtil; + log = redUtil.log; + i18n = redUtil.i18n; + userSettings.version = getVersion(); - log.init(userSettings); settings.init(userSettings); nodeApp = express(); @@ -86,10 +91,7 @@ function getVersion() { } function start() { - return i18n.init() - .then(function() { - return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"locales")),"runtime.json") - }) + return i18n.registerMessageCatalog("runtime",path.resolve(path.join(__dirname,"locales")),"runtime.json") .then(function() { return storage.init(runtime)}) .then(function() { return settings.load(storage)}) .then(function() { @@ -241,8 +243,8 @@ var runtime = module.exports = { version: getVersion, - log: log, - i18n: i18n, + get log() { return log }, + get i18n() { return i18n }, settings: settings, storage: storage, events: events, diff --git a/red/runtime/nodes/Node.js b/red/runtime/nodes/Node.js index b8cfa6532..989998665 100644 --- a/red/runtime/nodes/Node.js +++ b/red/runtime/nodes/Node.js @@ -19,7 +19,7 @@ var EventEmitter = require("events").EventEmitter; var when = require("when"); var redUtil = require("../util"); -var Log = require("../log"); +var Log = require("../../util").log; // TODO: separate module var context = require("./context"); var flows = require("./flows"); diff --git a/red/runtime/nodes/credentials.js b/red/runtime/nodes/credentials.js index 36b97b0b5..5e8b339f8 100644 --- a/red/runtime/nodes/credentials.js +++ b/red/runtime/nodes/credentials.js @@ -18,7 +18,8 @@ var when = require("when"); var crypto = require('crypto'); var runtime; var settings; -var log; +var log = require("../../util").log; // TODO: separate module + var encryptedCredentials = null; var credentialCache = {}; diff --git a/red/runtime/nodes/flows/Flow.js b/red/runtime/nodes/flows/Flow.js index 5d054a8a9..175bb6fe1 100644 --- a/red/runtime/nodes/flows/Flow.js +++ b/red/runtime/nodes/flows/Flow.js @@ -17,7 +17,7 @@ var when = require("when"); var clone = require("clone"); var typeRegistry = require("../registry"); -var Log = require("../../log"); +var Log = require("../../../util").log; // TODO: separate module var redUtil = require("../../util"); var flowUtil = require("./util"); diff --git a/red/runtime/nodes/flows/index.js b/red/runtime/nodes/flows/index.js index d1ba541f5..eef1c9774 100644 --- a/red/runtime/nodes/flows/index.js +++ b/red/runtime/nodes/flows/index.js @@ -24,7 +24,7 @@ var context = require("../context") var credentials = require("../credentials"); var flowUtil = require("./util"); -var log = require("../../log"); +var log = require("../../../util").log; // TODO: separate module var events = require("../../events"); var redUtil = require("../../util"); var deprecated = require("../registry/deprecated"); diff --git a/red/runtime/nodes/index.js b/red/runtime/nodes/index.js index b2f8ff3b5..ae1e3f253 100644 --- a/red/runtime/nodes/index.js +++ b/red/runtime/nodes/index.js @@ -25,7 +25,7 @@ var flows = require("./flows"); var flowUtil = require("./flows/util") var context = require("./context"); var Node = require("./Node"); -var log = null; +var log = require("../../util").log; // TODO: separate module var library = require("./library"); var events = require("../events"); @@ -94,7 +94,6 @@ function createNode(node,def) { function init(runtime) { settings = runtime.settings; - log = runtime.log; credentials.init(runtime); flows.init(runtime); registry.init(runtime); diff --git a/red/runtime/nodes/registry/installer.js b/red/runtime/nodes/registry/installer.js index 4e185c257..bceae2e6a 100644 --- a/red/runtime/nodes/registry/installer.js +++ b/red/runtime/nodes/registry/installer.js @@ -20,7 +20,7 @@ var path = require("path"); var fs = require("fs"); var registry = require("./registry"); -var log = require("../../log"); +var log = require("../../../util").log; // TODO: separate module var events = require("../../events"); diff --git a/red/runtime/nodes/registry/localfilesystem.js b/red/runtime/nodes/registry/localfilesystem.js index c2f0f9359..d9f170b78 100644 --- a/red/runtime/nodes/registry/localfilesystem.js +++ b/red/runtime/nodes/registry/localfilesystem.js @@ -19,8 +19,8 @@ var fs = require("fs"); var path = require("path"); var events; -var log; -var i18n; +var log = require("../../../util").log; // TODO: separate module +var i18n = require("../../../util").i18n; // TODO: separate module var settings; var disableNodePathScan = false; @@ -29,8 +29,6 @@ var iconFileExtensions = [".png", ".gif"]; function init(runtime) { settings = runtime.settings; events = runtime.events; - log = runtime.log; - i18n = runtime.i18n; } function isIncluded(name) { diff --git a/red/runtime/settings.js b/red/runtime/settings.js index 1addf7c47..780faf590 100644 --- a/red/runtime/settings.js +++ b/red/runtime/settings.js @@ -17,7 +17,7 @@ var when = require("when"); var clone = require("clone"); var assert = require("assert"); -var log = require("./log"); +var log = require("../util").log; // TODO: separate module var util = require("./util"); // localSettings are those provided in the runtime settings.js file diff --git a/red/runtime/storage/index.js b/red/runtime/storage/index.js index e0ef6d6b6..3f1833bc3 100644 --- a/red/runtime/storage/index.js +++ b/red/runtime/storage/index.js @@ -18,7 +18,7 @@ var when = require('when'); var Path = require('path'); var crypto = require('crypto'); -var log = require("../log"); +var log = require("../../util").log; // TODO: separate module var runtime; var storageModule; diff --git a/red/runtime/storage/localfilesystem/index.js b/red/runtime/storage/localfilesystem/index.js index 2a57c7323..af9d103fb 100644 --- a/red/runtime/storage/localfilesystem/index.js +++ b/red/runtime/storage/localfilesystem/index.js @@ -18,7 +18,8 @@ var fs = require('fs-extra'); var when = require('when'); var fspath = require("path"); -var log = require("../../log"); +var log = require("../../../util").log; // TODO: separate module + var util = require("./util"); var library = require("./library"); var sessions = require("./sessions"); diff --git a/red/runtime/storage/localfilesystem/sessions.js b/red/runtime/storage/localfilesystem/sessions.js index 29c6871a2..b4d024723 100644 --- a/red/runtime/storage/localfilesystem/sessions.js +++ b/red/runtime/storage/localfilesystem/sessions.js @@ -18,7 +18,8 @@ var when = require('when'); var fs = require('fs-extra'); var fspath = require("path"); -var log = require("../../log"); +var log = require("../../../util").log; // TODO: separate module + var util = require("./util"); var sessionsFile; diff --git a/red/runtime/storage/localfilesystem/settings.js b/red/runtime/storage/localfilesystem/settings.js index 17ece58d7..8837fcc2a 100644 --- a/red/runtime/storage/localfilesystem/settings.js +++ b/red/runtime/storage/localfilesystem/settings.js @@ -18,7 +18,7 @@ var when = require('when'); var fs = require('fs-extra'); var fspath = require("path"); -var log = require("../../log"); +var log = require("../../../util").log; // TODO: separate module var util = require("./util"); var globalSettingsFile; diff --git a/red/runtime/storage/localfilesystem/util.js b/red/runtime/storage/localfilesystem/util.js index f677bdd38..8164147ab 100644 --- a/red/runtime/storage/localfilesystem/util.js +++ b/red/runtime/storage/localfilesystem/util.js @@ -18,7 +18,7 @@ var fs = require('fs-extra'); var when = require('when'); var nodeFn = require('when/node/function'); -var log = require("../../log"); +var log = require("../../../util").log; // TODO: separate module function parseJSON(data) { if (data.charCodeAt(0) === 0xFEFF) { diff --git a/red/runtime/i18n.js b/red/util/i18n.js similarity index 83% rename from red/runtime/i18n.js rename to red/util/i18n.js index fc3599c87..b809f8081 100644 --- a/red/runtime/i18n.js +++ b/red/util/i18n.js @@ -23,6 +23,7 @@ var defaultLang = "en-US"; var resourceMap = {}; var resourceCache = {}; +var initPromise; function registerMessageCatalogs(catalogs) { var promises = catalogs.map(function(catalog) { @@ -32,10 +33,12 @@ function registerMessageCatalogs(catalogs) { } function registerMessageCatalog(namespace,dir,file) { - return when.promise(function(resolve,reject) { - resourceMap[namespace] = { basedir:dir, file:file}; - i18n.loadNamespace(namespace,function() { - resolve(); + return initPromise.then(function() { + when.promise(function(resolve,reject) { + resourceMap[namespace] = { basedir:dir, file:file}; + i18n.loadNamespace(namespace,function() { + resolve(); + }); }); }); } @@ -82,18 +85,20 @@ var MessageFileLoader = { } function init() { - return when.promise(function(resolve,reject) { - i18n.backend(MessageFileLoader); - i18n.init({ - ns: { - namespaces: [], - defaultNs: "runtime" - }, - fallbackLng: [defaultLang] - },function() { - resolve(); + if (!initPromise) { + initPromise = when.promise(function(resolve,reject) { + i18n.backend(MessageFileLoader); + i18n.init({ + ns: { + namespaces: [], + defaultNs: "runtime" + }, + fallbackLng: [defaultLang] + },function() { + resolve(); + }); }); - }); + } } function getCatalog(namespace,lang) { diff --git a/red/util/index.js b/red/util/index.js new file mode 100644 index 000000000..342d2dfff --- /dev/null +++ b/red/util/index.js @@ -0,0 +1,28 @@ +/** + * Copyright JS Foundation and other contributors, http://js.foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + + +var log = require("./log"); +var i18n = require("./i18n"); + +module.exports = { + init: function(settings) { + log.init(settings); + i18n.init(); + }, + log: log, + i18n: i18n +} diff --git a/red/runtime/log.js b/red/util/log.js similarity index 100% rename from red/runtime/log.js rename to red/util/log.js From 825b0fb22f75dbfd0b955037ad1f64e365fef6dd Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 19 Apr 2018 20:46:01 +0100 Subject: [PATCH 04/51] Update locales module to new structure --- red/api/editor/index.js | 5 ++--- red/api/editor/locales.js | 31 ++++++++++++++++++------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/red/api/editor/index.js b/red/api/editor/index.js index d204d9411..10f3c70b7 100644 --- a/red/api/editor/index.js +++ b/red/api/editor/index.js @@ -83,7 +83,7 @@ module.exports = { // Locales var locales = require("./locales"); - locales.init(runtime); + locales.init(runtimeAPI); editorApp.get('/locales/nodes',locales.getAllNodes,apiUtil.errorHandler); editorApp.get(/locales\/(.+)\/?$/,locales.get,apiUtil.errorHandler); @@ -124,6 +124,5 @@ module.exports = { }); }, stop: comms.stop, - publish: comms.publish, - registerLibrary: library.register + publish: comms.publish } diff --git a/red/api/editor/locales.js b/red/api/editor/locales.js index 32bfa5c80..464933d90 100644 --- a/red/api/editor/locales.js +++ b/red/api/editor/locales.js @@ -16,13 +16,14 @@ var fs = require('fs'); var path = require('path'); //var apiUtil = require('../util'); -var i18n; -var redNodes; + +var i18n = require("../../util").i18n; // TODO: separate module + +var runtimeAPI; module.exports = { - init: function(runtime) { - i18n = runtime.i18n; - redNodes = runtime.nodes; + init: function(_runtimeAPI) { + runtimeAPI = _runtimeAPI; }, get: function(req,res) { var namespace = req.params[0]; @@ -40,13 +41,17 @@ module.exports = { }, getAllNodes: function(req,res) { var lngs = req.query.lng; - var nodeList = redNodes.getNodeList(); - var result = {}; - nodeList.forEach(function(n) { - if (n.module !== "node-red") { - result[n.id] = i18n.catalog(n.id,lngs)||{}; - } - }); - res.json(result); + var opts = { + user: req.user + } + runtimeAPI.nodes.getNodeList(opts).then(function(nodeList) { + var result = {}; + nodeList.forEach(function(n) { + if (n.module !== "node-red") { + result[n.id] = i18n.catalog(n.id,lngs)||{}; + } + }); + res.json(result); + }) } } From 2dab1d3e6e314c407dfdbd4ca3b4628ba27ec0aa Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Thu, 19 Apr 2018 21:39:44 +0100 Subject: [PATCH 05/51] Fix up merge issue on api/nodes --- editor/js/ui/palette.js | 1 - red/runtime-api/flows.js | 4 +-- red/runtime-api/nodes.js | 67 ++++++++++++++++++++++++---------------- 3 files changed, 43 insertions(+), 29 deletions(-) diff --git a/editor/js/ui/palette.js b/editor/js/ui/palette.js index 38ebc1091..4bab0717a 100644 --- a/editor/js/ui/palette.js +++ b/editor/js/ui/palette.js @@ -431,7 +431,6 @@ RED.palette = (function() { } }); RED.events.on('registry:node-set-disabled', function(nodeSet) { - console.log(nodeSet); for (var j=0;j Date: Fri, 20 Apr 2018 20:50:20 +0100 Subject: [PATCH 06/51] Add projects to runtime-api --- editor/templates/index.mst | 2 +- red/api/editor/index.js | 9 +- red/api/editor/library.js | 2 +- red/api/editor/projects/index.js | 586 +++++++++++-------------- red/api/editor/theme.js | 5 +- red/api/editor/ui.js | 23 +- red/runtime-api/index.js | 34 +- red/runtime-api/nodes.js | 23 +- red/runtime-api/projects.js | 441 +++++++++++++++++++ red/runtime-api/settings.js | 82 ++-- red/runtime/nodes/registry/registry.js | 3 + 11 files changed, 808 insertions(+), 402 deletions(-) create mode 100644 red/runtime-api/projects.js diff --git a/editor/templates/index.mst b/editor/templates/index.mst index b8d0ac0c0..76036b93c 100644 --- a/editor/templates/index.mst +++ b/editor/templates/index.mst @@ -36,7 +36,7 @@