/** * 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 UglifyJS = require("uglify-js"); const path = require("path"); const fs = require("fs"); var library = require("./library"); const {events} = require("@node-red/util") var subflows = require("./subflow"); var externalModules = require("./externalModules") var settings; var loader; var nodeConfigCache = {}; var moduleConfigs = {}; var nodeList = []; var nodeConstructors = {}; var nodeOptions = {}; var subflowModules = {}; var nodeTypeToId = {}; var moduleNodes = {}; function init(_settings,_loader) { settings = _settings; loader = _loader; clear(); } function load() { if (settings.available()) { moduleConfigs = loadNodeConfigs(); } else { moduleConfigs = {}; } } function filterNodeInfo(n) { var r = { id: n.id||n.module+"/"+n.name, name: n.name, types: n.types, enabled: n.enabled, local: n.local||false, user: n.user || false }; if (n.hasOwnProperty("module")) { r.module = n.module; } if (n.hasOwnProperty("err")) { r.err = n.err; } if (n.hasOwnProperty("plugins")) { r.plugins = n.plugins; } if (n.type === "plugin") { r.editor = !!n.template; r.runtime = !!n.file; } return r; } function getModuleFromSetId(id) { var parts = id.split("/"); return parts.slice(0,parts.length-1).join("/"); } function getNodeFromSetId(id) { var parts = id.split("/"); return parts[parts.length-1]; } function saveNodeList() { var moduleList = {}; var hadPending = false; var hasPending = false; for (var module in moduleConfigs) { /* istanbul ignore else */ if (moduleConfigs.hasOwnProperty(module)) { if (Object.keys(moduleConfigs[module].nodes).length > 0) { if (!moduleList[module]) { moduleList[module] = { name: module, version: moduleConfigs[module].version, local: moduleConfigs[module].local||false, user: moduleConfigs[module].user||false, nodes: {} }; if (moduleConfigs[module].hasOwnProperty('pending_version')) { hadPending = true; if (moduleConfigs[module].pending_version !== moduleConfigs[module].version) { moduleList[module].pending_version = moduleConfigs[module].pending_version; hasPending = true; } else { delete moduleConfigs[module].pending_version; } } } var nodes = moduleConfigs[module].nodes; for(var node in nodes) { /* istanbul ignore else */ if (nodes.hasOwnProperty(node)) { var config = nodes[node]; var n = filterNodeInfo(config); delete n.err; delete n.file; delete n.id; n.file = config.file; moduleList[module].nodes[node] = n; } } } } } if (hadPending && !hasPending) { events.emit("runtime-event",{id:"restart-required",retain: true}); } if (settings.available()) { return settings.set("nodes",moduleList); } else { return Promise.reject("Settings unavailable"); } } function loadNodeConfigs() { var configs = settings.get("nodes"); if (!configs) { return {}; } else if (configs['node-red']) { return configs; } else { // Migrate from the 0.9.1 format of settings var newConfigs = {}; for (var id in configs) { /* istanbul ignore else */ if (configs.hasOwnProperty(id)) { var nodeConfig = configs[id]; var moduleName; var nodeSetName; if (nodeConfig.module) { moduleName = nodeConfig.module; nodeSetName = nodeConfig.name.split(":")[1]; } else { moduleName = "node-red"; nodeSetName = nodeConfig.name.replace(/^\d+-/,"").replace(/\.js$/,""); } if (!newConfigs[moduleName]) { newConfigs[moduleName] = { name: moduleName, nodes:{} }; } newConfigs[moduleName].nodes[nodeSetName] = { name: nodeSetName, types: nodeConfig.types, enabled: nodeConfig.enabled, module: moduleName }; } } settings.set("nodes",newConfigs); return newConfigs; } } function addModule(module) { moduleNodes[module.name] = []; moduleConfigs[module.name] = module; // console.log("registry.js.addModule",module.name,"user?",module.user,"usedBy",module.usedBy,"dependencies",module.dependencies) for (var setName in module.nodes) { if (module.nodes.hasOwnProperty(setName)) { var set = module.nodes[setName]; moduleNodes[module.name].push(set.name); nodeList.push(set.id); if (!set.err) { set.types.forEach(function(t) { if (nodeTypeToId.hasOwnProperty(t)) { set.err = new Error("Type already registered"); set.err.code = "type_already_registered"; set.err.details = { type: t, moduleA: getNodeInfo(t).module, moduleB: set.module } } }); if (!set.err) { set.types.forEach(function(t) { nodeTypeToId[t] = set.id; }); } } } } if (module.icons) { icon_paths[module.name] = icon_paths[module.name] || []; module.icons.forEach(icon=>icon_paths[module.name].push(path.resolve(icon.path)) ) } if (module.examples) { library.addExamplesDir(module.name,module.examples.path); } nodeConfigCache = {}; } function removeNode(id) { var config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; if (!config) { throw new Error("Unrecognised id: "+id); } delete moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; var i = nodeList.indexOf(id); if (i > -1) { nodeList.splice(i,1); } config.types.forEach(function(t) { var typeId = nodeTypeToId[t]; if (typeId === id) { delete subflowModules[t]; delete nodeConstructors[t]; delete nodeOptions[t]; delete nodeTypeToId[t]; } }); config.enabled = false; config.loaded = false; nodeConfigCache = {}; return filterNodeInfo(config); } function removeModule(name,skipSave) { if (!settings.available()) { throw new Error("Settings unavailable"); } var infoList = []; var module = moduleConfigs[name]; var nodes = moduleNodes[name]; if (!nodes) { throw new Error("Unrecognised module: "+name); } if (module.usedBy && module.usedBy > 0) { // We are removing a module that is used by other modules... so whilst // this module should be removed from the editor palette, it needs to // stay in the runtime... for now. module.user = false; for (var i=0;i m !== name); if (moduleConfigs[dep].usedBy.length === 0) { // Remove the dependency removeModule(dep,true); } } }); } for (var i=0;i 0)) { continue; } var nodes = moduleConfigs[module].nodes; for (var node in nodes) { /* istanbul ignore else */ if (nodes.hasOwnProperty(node)) { var nodeInfo = filterNodeInfo(nodes[node]); nodeInfo.version = moduleConfigs[module].version; if (moduleConfigs[module].pending_version) { nodeInfo.pending_version = moduleConfigs[module].pending_version; } if (!filter || filter(nodes[node])) { list.push(nodeInfo); } } } } } return list; } function getModuleList() { return moduleConfigs; } function getModule(id) { return moduleConfigs[id]; } function getModuleInfo(module) { if (moduleNodes[module]) { var nodes = moduleNodes[module]; var m = { name: module, version: moduleConfigs[module].version, local: moduleConfigs[module].local, user: moduleConfigs[module].user, path: moduleConfigs[module].path, nodes: [] }; if (moduleConfigs[module].dependencies) { m.dependencies = moduleConfigs[module].dependencies; } if (moduleConfigs[module] && moduleConfigs[module].pending_version) { m.pending_version = moduleConfigs[module].pending_version; } for (var i = 0; i < nodes.length; ++i) { var nodeInfo = filterNodeInfo(moduleConfigs[module].nodes[nodes[i]]); nodeInfo.version = m.version; m.nodes.push(nodeInfo); } return m; } else { return null; } } function getCaller(){ var orig = Error.prepareStackTrace; Error.prepareStackTrace = function(_, stack){ return stack; }; var err = new Error(); Error.captureStackTrace(err, arguments.callee); var stack = err.stack; Error.prepareStackTrace = orig; stack.shift(); stack.shift(); return stack[0].getFileName(); } function registerNodeConstructor(nodeSet,type,constructor,options) { if (nodeConstructors.hasOwnProperty(type)) { throw new Error(type+" already registered"); } //TODO: Ensure type is known - but doing so will break some tests // that don't have a way to register a node template ahead // of registering the constructor var nodeSetInfo = getFullNodeInfo(nodeSet); if (nodeSetInfo) { if (nodeSetInfo.types.indexOf(type) === -1) { // A type is being registered for a known set, but for some reason // we didn't spot it when parsing the HTML file. // Registered a type is the definitive action - not the presence // of an edit template. Ensure it is on the list of known types. nodeSetInfo.types.push(type); } } nodeConstructors[type] = constructor; nodeOptions[type] = options; if (options) { if (options.dynamicModuleList) { externalModules.register(type,options.dynamicModuleList); } } events.emit("type-registered",type); } function registerSubflow(nodeSet, subflow) { var nodeSetInfo = getFullNodeInfo(nodeSet); const result = subflows.register(nodeSet,subflow); if (subflowModules.hasOwnProperty(result.type)) { throw new Error(result.type+" already registered"); } if (nodeSetInfo) { if (nodeSetInfo.types.indexOf(result.type) === -1) { nodeSetInfo.types.push(result.type); nodeTypeToId[result.type] = nodeSetInfo.id; } nodeSetInfo.config = result.config; } subflowModules[result.type] = result; externalModules.registerSubflow(result.type,subflow); events.emit("type-registered",result.type); return result; } function getAllNodeConfigs(lang) { if (!nodeConfigCache[lang]) { var result = ""; var script = ""; for (var i=0;i 0)) { continue; } var config = module.nodes[getNodeFromSetId(id)]; if (config.enabled && !config.err) { result += "\n\n"; result += config.config; result += loader.getNodeHelp(config,lang||"en-US")||""; //script += config.script; } } //if (script.length > 0) { // result += ''; //} nodeConfigCache[lang] = result; } return nodeConfigCache[lang]; } function getNodeConfig(id,lang) { var config = moduleConfigs[getModuleFromSetId(id)]; if (!config) { return null; } config = config.nodes[getNodeFromSetId(id)]; if (config) { var result = "\n"+config.config; result += loader.getNodeHelp(config,lang||"en-US") //if (config.script) { // result += ''; //} return result; } else { return null; } } function getNodeConstructor(type) { var id = nodeTypeToId[type]; var config; if (typeof id === "undefined") { config = undefined; } else { config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; } if (!config || (config.enabled && !config.err)) { return nodeConstructors[type] || subflowModules[type]; } return null; } function clear() { nodeConfigCache = {}; moduleConfigs = {}; nodeList = []; nodeConstructors = {}; nodeOptions = {}; subflowModules = {}; nodeTypeToId = {}; } function getTypeId(type) { if (nodeTypeToId.hasOwnProperty(type)) { return nodeTypeToId[type]; } else { return null; } } function enableNodeSet(typeOrId) { if (!settings.available()) { throw new Error("Settings unavailable"); } var id = typeOrId; if (nodeTypeToId.hasOwnProperty(typeOrId)) { id = nodeTypeToId[typeOrId]; } var config; try { config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; delete config.err; config.enabled = true; nodeConfigCache = {}; settings.enableNodeSettings(config.types); return saveNodeList().then(function() { return filterNodeInfo(config); }); } catch (err) { throw new Error("Unrecognised id: "+typeOrId); } } function disableNodeSet(typeOrId) { if (!settings.available()) { throw new Error("Settings unavailable"); } var id = typeOrId; if (nodeTypeToId.hasOwnProperty(typeOrId)) { id = nodeTypeToId[typeOrId]; } var config; try { config = moduleConfigs[getModuleFromSetId(id)].nodes[getNodeFromSetId(id)]; // TODO: persist setting config.enabled = false; nodeConfigCache = {}; settings.disableNodeSettings(config.types); return saveNodeList().then(function() { return filterNodeInfo(config); }); } catch (err) { throw new Error("Unrecognised id: "+id); } } function cleanModuleList() { var removed = false; for (var mod in moduleConfigs) { /* istanbul ignore else */ if (moduleConfigs.hasOwnProperty(mod)) { var nodes = moduleConfigs[mod].nodes; var node; if (mod == "node-red") { // For core nodes, look for nodes that are enabled, !loaded and !errored for (node in nodes) { /* istanbul ignore else */ if (nodes.hasOwnProperty(node)) { var n = nodes[node]; if (n.enabled && !n.err && !n.loaded) { removeNode(mod+"/"+node); removed = true; } } } } else { if (moduleConfigs[mod] && !moduleNodes[mod]) { // For node modules, look for missing ones for (node in nodes) { /* istanbul ignore else */ if (nodes.hasOwnProperty(node)) { removeNode(mod+"/"+node); removed = true; } } delete moduleConfigs[mod]; } } } } if (removed) { saveNodeList(); } } function setModulePendingUpdated(module,version) { moduleConfigs[module].pending_version = version; return saveNodeList().then(function() { return getModuleInfo(module); }); } function setUserInstalled(module,userInstalled) { moduleConfigs[module].user = userInstalled; return saveNodeList().then(function() { return getModuleInfo(module); }); } function addModuleDependency(module,usedBy) { moduleConfigs[module].usedBy = moduleConfigs[module].usedBy || []; moduleConfigs[module].usedBy.push(usedBy); } var icon_paths = { }; var iconCache = {}; function getNodeIconPath(module,icon) { if (/\.\./.test(icon)) { throw new Error(); } var iconName = module+"/"+icon; if (iconCache[iconName]) { return iconCache[iconName]; } else { var paths = icon_paths[module]; if (paths) { for (var p=0;p{ iconList[module] = iconList[module].concat(icon.icons)}) } } } return iconList; } function getModuleResource(module, resourcePath) { let mod = moduleConfigs[module]; if (mod && mod.resources) { let basePath = mod.resources.path; let fullPath = path.join(basePath,resourcePath); if (/^\.\./.test(path.relative(basePath,fullPath))) { return null; } if (fs.existsSync(fullPath)) { return fullPath; } } return null; } var registry = module.exports = { init: init, load: load, clear: clear, registerNodeConstructor: registerNodeConstructor, getNodeConstructor: getNodeConstructor, registerSubflow: registerSubflow, addModule: addModule, enableNodeSet: enableNodeSet, disableNodeSet: disableNodeSet, setModulePendingUpdated: setModulePendingUpdated, setUserInstalled: setUserInstalled, addModuleDependency:addModuleDependency, removeModule: removeModule, getNodeInfo: getNodeInfo, getFullNodeInfo: getFullNodeInfo, getNodeList: getNodeList, getModuleList: getModuleList, getModule: getModule, getModuleInfo: getModuleInfo, getNodeIconPath: getNodeIconPath, getNodeIcons: getNodeIcons, getModuleResource: getModuleResource, /** * Gets all of the node template configs * @return all of the node templates in a single string */ getAllNodeConfigs: getAllNodeConfigs, getNodeConfig: getNodeConfig, getTypeId: getTypeId, saveNodeList: saveNodeList, cleanModuleList: cleanModuleList, getModuleFromSetId: getModuleFromSetId, getNodeFromSetId: getNodeFromSetId, filterNodeInfo: filterNodeInfo };