/** * 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 path = require("path"); var fs = require("fs-extra"); var os = require("os"); var util = require("@node-red/registry/lib/util"); var api; var runtime; var settings; var exec; var log; var npmCommand = (process.platform === "win32") ? "npm.cmd" : "npm"; var metadataFileName = "npm-modules.json"; var moduleProp = {}; var moduleBase = null; var allowInstall = true; var allowList = ["*"]; var denyList = []; var inProgress = {}; /** * Initialise npm install module. * @param {Object} _runtime - runtime object */ function init(_runtime) { runtime = _runtime; settings = _runtime.settings; exec = _runtime.exec; log = _runtime.log; moduleProp = {}; inProgress = {}; moduleBase = settings.userDir || process.env.NODE_RED_HOME || "."; if (settings.hasOwnProperty("externalModules")) { var em = settings.externalModules; if (em && em.hasOwnProperty("modules")) { var mod = em.modules; if (mod.hasOwnProperty("allowInstall")) { allowInstall = mod.allowInstall; } if (mod.hasOwnProperty("allowList")) { var alist = mod.allowList; if (Array.isArray(alist)) { allowList = alist; } else { log.warn("unexpected value of externalModule.allowList in settings.js"); } } if (mod.hasOwnProperty("denyList")) { var dlist = mod.denyList; if (Array.isArray(dlist)) { denyList = dlist; } else { log.warn("unexpected value of externalModule.denyList in settings.js"); } } } } } /** * Register dynamic module installation property. * @param {string} type - node type * @param {string} prop - property name */ function register(type, prop) { if (prop) { moduleProp[type] = prop; } else { delete moduleProp[prop] } } /** * Get path to install modules */ function modulePath() { // takes variable length arguments in `arguments` var result = moduleBase; for(var i = 0; i < arguments.length; i++) { result = path.join(result, arguments[i]); } return result; } /** * Decompose NPM module specification string * @param {string} module - module specification * @return {Object} array [name, version], where name is name part and version is version part of the module */ function moduleName(module) { var match = /^([^@]+)@(.+)/.exec(module); if (match) { return [match[1], match[2]]; } return [module, undefined]; } /** * Get NPM module info * @param {string} name - module name * @return {Object} package.json for specified NPM module */ function infoNPM(name) { var path = modulePath("node_modules", name, "package.json"); try { var pkg = fs.readFileSync(path); return JSON.parse(pkg); } catch (e) { } return null; } /** * Load NPM module metadata * @return {object} module metadata object */ function loadMetadata() { var path = modulePath(metadataFileName); try { var pkg = fs.readFileSync(path); return JSON.parse(pkg); } catch (e) { } return { modules: [] }; } /** * Save NPM module metadata * @param {string} data - module metadata object */ function saveMetadata(data) { var path = modulePath(metadataFileName); var str = JSON.stringify(data, null, 4); fs.writeFileSync(path, str); } /** * Find item in metadata * @param {Object} meta - metadata * @param {string} name - module name * @return {object} metadata item */ function findModule(meta, name) { var modules = meta.modules; var item = modules.find(item => (item.name === name)); return item; } function setInProgress(name) { inProgress[name] = true; } function clearInProgress(name) { inProgress[name] = false; } /** * Install NPM module * @param {string} module - module specification */ function installNPM(module) { var [name, ver] = moduleName(module); setInProgress(name); return new Promise((resolve, reject) => { var pkg = infoNPM(name); if (!pkg) { var args = ["install", module]; var dir = modulePath(); return exec.run(npmCommand, args, { cwd: dir }, true).then(result => { if (result && (result.code === 0)) { pkg = infoNPM(name); var spec = name +(pkg ? "@"+pkg.version : ""); log.info("successfully installed: "+spec); var meta = loadMetadata(); var item = { name: name, spec: module, status: "installed", }; meta.modules.push(item); saveMetadata(meta); clearInProgress(name); resolve(true); } else { clearInProgress(name); var msg = "failed to install: "+name; log.warn(msg); reject(msg); } }).catch(e => { clearInProgress(name); var msg = "failed to install: "+name log.warn(msg); reject(msg); }); } else { var meta = loadMetadata(); if (!findModule(meta, name)) { var item = { name: name, spec: module, status: "preinstalled", }; meta.modules.push(item); saveMetadata(meta); } clearInProgress(name); var spec = name +(pkg ? ("@"+pkg.version) : ""); log.info("already installed: "+spec); } resolve(true); }); } /** * Check allowance of NPM module installation * @param {string} name - module specification */ function isAllowed(name) { if (!allowInstall) { return false; } var [module, ver] = moduleName(name); var aList = util.parseModuleList(allowList); var dList = util.parseModuleList(denyList); return util.checkModuleAllowed(module, ver, aList, dList); } /** * Check and install NPM module according to dynamic module specification * @param {Object} node - node object */ function checkInstall(node) { var name = null; if(moduleProp.hasOwnProperty(node.type)) { name = moduleProp[node.type]; } var promises = []; if (name && node.hasOwnProperty(name)) { var modules = node[name]; modules.forEach(module => { var name = module; if ((typeof module === "object") && module && module.hasOwnProperty("name")) { name = module.name; } if (isAllowed(name)) { var [n, v] = moduleName(name); setInProgress(name); promises.push(installNPM(name)); } else { log.info("installation not allowed: "+name); } }); } return Promise.all(promises); } /** * Load NPM module * @param {string} module - module to load */ function load(module) { try { var [name, ver] = moduleName(module); var path = modulePath("node_modules", name); var npm = require(path); return npm; } catch (e) { return null; } } /** * Get list of installed modules */ function listModules() { return new Promise((resolve, reject) => { var meta = loadMetadata(); var modules = meta.modules; modules.forEach(item => { var name = item.name; var info = infoNPM(name); if (info) { item.version = info.version; } item.inProgress = ((name in inProgress) && inProgress[name]); }); Object.keys(inProgress).forEach(name => { if (inProgress[name] && !modules.find(item => (item.name === name))) { modules.push({ name: name, spec: name, state: "inprogress", inProgress: true }); } }); resolve(meta.modules); }); } /** * Uninstall NPM modules */ function uninstall(module) { var [name, ver] = moduleName(module); setInProgress(name); return new Promise((resolve, reject) => { var pkg = infoNPM(name); var meta = loadMetadata(); var item = findModule(meta, name); if (pkg && item) { if (item.status === "preinstalled") { clearInProgress(name); var msg = "can't uninstall preinstalled: "+name; log.warn(msg); reject(msg); } else { var args = ["uninstall", module]; var dir = modulePath(); return exec.run(npmCommand, args, { cwd: dir }, true).then(result => { if (result && (result.code === 0)) { log.info("successfully uninstalled: "+name); var meta = loadMetadata(); var items = meta.modules.filter(item => (item.name !== name)); meta.modules = items; saveMetadata(meta); clearInProgress(name); resolve(true); } else { clearInProgress(name); var msg = "failed to uninstall: "+name; log.warn(msg); reject(msg); } }).catch(e => { clearInProgress(name); var msg = "failed to uninstall: "+name; log.warn(msg); reject(msg); }); } } else { clearInProgress(name); var msg = "module not installed: "+name; log.info(msg); reject(msg); } }); } /** * Update NPM modules */ function update(module, isUpdate) { var act = (isUpdate ? "updated": "install") var acted = (isUpdate ? "updated": "installed") var [name, ver] = moduleName(module); setInProgress(name); return new Promise((resolve, reject) => { var pkg = infoNPM(name); if (!pkg || isUpdate) { var args = ["install", module]; var dir = modulePath(); return exec.run(npmCommand, args, { cwd: dir }, true).then(result => { if (result && (result.code === 0)) { pkg = infoNPM(name); var spec = name +(pkg ? "@"+pkg.version : ""); log.info("successfully "+acted+": "+spec); var meta = loadMetadata(); var items = meta.modules.filter(item => (item.name !== name)); var item = { name: name, spec: module, status: "installed", }; items.push(item); meta.modules = items; saveMetadata(meta); clearInProgress(name); resolve(true); } else { clearInProgress(name); var msg = "failed to "+act+": "+name; log.warn(msg); reject(msg); } }).catch(e => { clearInProgress(name); var msg = "failed to "+act+": "+name; log.warn(msg); reject(msg); }); } else { clearInProgress(name); var msg = "not "+acted+": "+name; log.info(msg); reject(msg); } }); } api = { init: init, register: register, checkInstall: checkInstall, load: load, list: listModules, uninstall: uninstall, update: update }; module.exports = api;