From d9344fcb1a108478d147d556b26fd1400c6b24da Mon Sep 17 00:00:00 2001 From: Ryan Blenis Date: Fri, 1 Nov 2019 16:49:18 -0400 Subject: [PATCH] gui plugin admin updates part 2 --- db.js | 8 +- meshagent.js | 3 +- meshuser.js | 42 +++++- pluginHandler.js | 283 ++++++++++++++++++++++++++++----------- public/styles/style.css | 21 ++- views/default.handlebars | 100 ++++++++++++-- 6 files changed, 357 insertions(+), 100 deletions(-) diff --git a/db.js b/db.js index e4861996..116f6802 100644 --- a/db.js +++ b/db.js @@ -753,16 +753,18 @@ module.exports.CreateDB = function (parent, func) { } // Add a plugin - obj.addPlugin = function (plugin) { obj.pluginsfile.insertOne(plugin); }; + obj.addPlugin = function (plugin, func) { obj.pluginsfile.insertOne(plugin, func); }; // Get all plugins obj.getPlugins = function (func) { obj.pluginsfile.find().sort({ name: 1 }).toArray(func); }; // Get plugin - obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); }; + obj.getPlugin = function (id, func) { id = require('mongodb').ObjectID(id); obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).toArray(func); }; // Delete plugin - obj.deletePlugin = function (id) { obj.pluginsfile.deleteOne({ _id: id }); }; + obj.deletePlugin = function (id, func) { id = require('mongodb').ObjectID(id); obj.pluginsfile.deleteOne({ _id: id }, func); }; + + obj.setPluginStatus = function(id, status, func) { id = require('mongodb').ObjectID(id); obj.pluginsfile.updateOne({ _id: id }, { $set: {status: status } }, func); }; } else { // Database actions on the main collection (NeDB and MongoJS) diff --git a/meshagent.js b/meshagent.js index f9dfdedb..9835ef44 100644 --- a/meshagent.js +++ b/meshagent.js @@ -1287,8 +1287,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) { case 'plugin': { if ((parent.parent.pluginHandler == null) || (typeof command.plugin != 'string')) break; try { - var pluginHandler = require('./pluginHandler.js').pluginHandler(parent.parent); - pluginHandler.plugins[command.plugin].serveraction(command, obj, parent); + parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent); } catch (e) { console.log('Error loading plugin handler (' + e + ')'); } diff --git a/meshuser.js b/meshuser.js index 9f59d764..527258f5 100644 --- a/meshuser.js +++ b/meshuser.js @@ -3103,22 +3103,51 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use break; } case 'plugins': { - if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled + // @Ylianst - Do we need a new permission set here? + if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin with plugins enabled parent.db.getPlugins(function(err, docs) { try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { } }); break; } + case 'pluginLatestCheck': { + if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin with plugins enabled + parent.parent.pluginHandler.getPluginLatest(function(latest) { + try { ws.send(JSON.stringify({ action: 'pluginVersionsAvailable', list: latest })); } catch (ex) { } + }); + break; + } case 'addplugin': { - // @Ylianst - Do we need a new permission here? if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled parent.parent.pluginHandler.addPlugin(command.url); break; } - case 'removeplugin': { - // @Ylianst - Do we need a new permission here? + case 'installplugin': { if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled - parent.parent.pluginHandler.removePlugin(command.id); + parent.parent.pluginHandler.installPlugin(command.id, function(){ + parent.parent.updateMeshCore(); + parent.db.getPlugins(function(err, docs) { + try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { } + }); + }); + break; + } + case 'disableplugin': { + if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled + parent.parent.pluginHandler.disablePlugin(command.id, function(){ + parent.db.getPlugins(function(err, docs) { + try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { } + }); + }); + break; + } + case 'removeplugin': { + if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled + parent.parent.pluginHandler.removePlugin(command.id, function(){ + parent.db.getPlugins(function(err, docs) { + try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { } + }); + }); break; } case 'plugin': { @@ -3128,8 +3157,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use routeCommandToNode(command); } else { try { - var pluginHandler = require('./pluginHandler.js').pluginHandler(parent.parent); - pluginHandler.plugins[command.plugin].serveraction(command, obj, parent); + parent.parent.pluginHandler.plugins[command.plugin].serveraction(command, obj, parent); } catch (e) { console.log('Error loading plugin handler (' + e + ')'); } } break; diff --git a/pluginHandler.js b/pluginHandler.js index 5b328ee6..12fe18b8 100644 --- a/pluginHandler.js +++ b/pluginHandler.js @@ -23,34 +23,52 @@ module.exports.pluginHandler = function (parent) { obj.pluginPath = obj.parent.path.join(obj.parent.datapath, 'plugins'); obj.plugins = {}; obj.exports = {}; - obj.loadList = obj.parent.config.settings.plugins.list; - + obj.loadList = obj.parent.config.settings.plugins.list; // For local development / manual install, not from DB + if (typeof obj.loadList != 'object') { obj.loadList = {}; - console.log('Plugin list not specified, please fix configuration file.'); - return null; - } - - obj.loadList.forEach(function (plugin, index) { - if (obj.fs.existsSync(obj.pluginPath + '/' + plugin)) { - try { - obj.plugins[plugin] = require(obj.pluginPath + '/' + plugin + '/' + plugin + '.js')[plugin](obj); - obj.exports[plugin] = obj.plugins[plugin].exports; - } catch (e) { - console.log("Error loading plugin: " + plugin + " (" + e + "). It has been disabled.", e.stack); + parent.db.getPlugins(function(err, plugins){ + plugins.forEach(function(plugin){ + if (plugin.status != 1) return; + if (obj.fs.existsSync(obj.pluginPath + '/' + plugin.shortName)) { + try { + obj.plugins[plugin.shortName] = require(obj.pluginPath + '/' + plugin.shortName + '/' + plugin.shortName + '.js')[plugin.shortName](obj); + obj.exports[plugin.shortName] = obj.plugins[plugin.shortName].exports; + } catch (e) { + console.log("Error loading plugin: " + plugin.shortName + " (" + e + "). It has been disabled.", e.stack); + } + } + obj.parent.updateMeshCore(); // db calls are delayed, lets inject here once we're ready + }); + }); + } else { + obj.loadList.forEach(function (plugin, index) { + if (obj.fs.existsSync(obj.pluginPath + '/' + plugin)) { + try { + obj.plugins[plugin] = require(obj.pluginPath + '/' + plugin + '/' + plugin + '.js')[plugin](obj); + obj.exports[plugin] = obj.plugins[plugin].exports; + } catch (e) { + console.log("Error loading plugin: " + plugin + " (" + e + "). It has been disabled.", e.stack); + } } + }); + } + + obj.prepExportsForPlugin = function(plugin) { + var str = ''; + str += ' obj.' + plugin + ' = {};\r\n'; + for (const l of Object.values(obj.exports[plugin])) { + str += ' obj.' + plugin + '.' + l + ' = ' + obj.plugins[plugin][l].toString() + '\r\n'; } - }); - + return str; + }; + obj.prepExports = function () { var str = 'function() {\r\n'; str += ' var obj = {};\r\n'; for (const p of Object.keys(obj.plugins)) { - str += ' obj.' + p + ' = {};\r\n'; - for (const l of Object.values(obj.exports[p])) { - str += ' obj.' + p + '.' + l + ' = ' + obj.plugins[p][l].toString() + '\r\n'; - } + str += obj.prepExportsForPlugin(p); } str += `obj.onDeviceRefeshEnd = function(nodeid, panel, refresh, event) { @@ -75,7 +93,7 @@ module.exports.pluginHandler = function (parent) { meshserver.send({ action: 'addplugin', url: Q('pluginurlinput').value}); }; obj.addPluginDlg = function() { - setDialogMode(2, "Plugin URL", 3, obj.addPluginEx, ''); + setDialogMode(2, "Plugin Config URL", 3, obj.addPluginEx, ''); focusTextBox('pluginurlinput'); }; return obj; };`; @@ -165,6 +183,7 @@ module.exports.pluginHandler = function (parent) { var isValid = true; if (!( typeof conf.name == 'string' + && typeof conf.shortName == 'string' && typeof conf.version == 'string' && typeof conf.author == 'string' && typeof conf.description == 'string' @@ -179,68 +198,178 @@ module.exports.pluginHandler = function (parent) { // && conf.configUrl == url // make sure we're loading a plugin from its desired config )) isValid = false; // more checks here? + if (conf.repository.type == 'git') { + if (typeof conf.downloadUrl != 'string') isValid = false; + } return isValid; }; - obj.addPlugin = function(url) { - var https = require('https'); - //var pit = obj.path.join(obj.pluginPath, ) - - https.get(url, function(res) { - var configStr = ''; - res.on('data', function(chunk){ - configStr += chunk; - }); - res.on('end', function(){ - if (configStr[0] == '{') { - try { - var pluginConfig = JSON.parse(configStr); - if (obj.isValidConfig(pluginConfig, url)) { - // add to database - // we met the requirements of a valid config, but in case there's extra, let's rebuild for what we need - parent.db.addPlugin({ - "name": pluginConfig.name, - "version": pluginConfig.version, - "description": pluginConfig.description, - "hasAdminPanel": pluginConfig.hasAdminPanel, - "homepage": pluginConfig.homepage, - "changelogUrl": pluginConfig.changelogUrl, - "configUrl": pluginConfig.configUrl, - "repository": { - "type": pluginConfig.repository.type, - "url": pluginConfig.repository.url - }, - "meshCentralCompat": pluginConfig.meshCentralCompat, - "status": 0 // 0: disabled, 1: enabled - }); - parent.db.getPlugins(function(err, docs){ - var targets = ['*', 'server-users']; - parent.DispatchEvent(targets, obj, { action: 'updatePluginList', list: docs }); - - }) - } else { - // @TODO return error to user - } - - } catch (e) { console.log('Error processing addPlugin request. Check that you have valid JSON.'); } - } - }); - - }).on('error', function(e) { - console.log("Got error: " + e.message); - }); - /* const file = fs.createWriteStream("file.jpg"); - const request = http.get("http://i3.ytimg.com/vi/J---aiyznGQ/mqdefault.jpg", function(response) { - response.pipe(file); - }); */ - }; - - obj.getPlugins = function() { - var p = parent.db.getPlugins(); - if (typeof p == 'undefined' || p.length == 0) { + obj.getPlugins = function(func) { + var plugins = parent.db.getPlugins(); + if (typeof plugins == 'undefined' || plugins.length == 0) { return null; } - return p; + + plugins.forEach(function(p, x){ + // check semantic version + console.log('FOREACH PLUGIN', p, x); + // callbacks to new versions + + }); + + return plugins; } + + obj.getPluginConfig = function(configUrl, func) { + var https = require('https'); + if (configUrl.indexOf('://') === -1) return; // @TODO error here + https.get(configUrl, function(res) { + var configStr = ''; + res.on('data', function(chunk){ + configStr += chunk; + }); + res.on('end', function(){ + if (configStr[0] == '{') { // let's be sure we're JSON + try { + var pluginConfig = JSON.parse(configStr); + if (Array.isArray(pluginConfig) && pluginConfig.length == 1) pluginConfig = pluginConfig[0]; + if (obj.isValidConfig(pluginConfig, configUrl)) { + func(pluginConfig); + } + + } catch (e) { console.log('Error getting plugin config. Check that you have valid JSON.', e.stack); } + } + }); + + }).on('error', function(e) { + console.log("Error getting plugin config. Check that the URL is correct.: " + e.message); + }); + }; + + obj.getPluginLatest = function(func) { + parent.db.getPlugins(function(err, plugins){ + plugins.forEach(function(curconf){ + obj.getPluginConfig(curconf.configUrl, function(newconf){ + var s = require('semver'); + func({ + "id": curconf._id, + "installedVersion": curconf.version, + "version": newconf.version, + "hasUpdate": s.gt(newconf.version, curconf.version), + "meshCentralCompat": s.satisfies(s.coerce(parent.currentVer), newconf.meshCentralCompat), + "changelogUrl": curconf.changelogUrl, + "status": curconf.status + }); + }); + }); + }); + + }; + + obj.addPlugin = function(url) { + obj.getPluginConfig(url, function(pluginConfig){ + parent.db.addPlugin({ + "name": pluginConfig.name, + "shortName": pluginConfig.shortName, + "version": pluginConfig.version, + "description": pluginConfig.description, + "hasAdminPanel": pluginConfig.hasAdminPanel, + "homepage": pluginConfig.homepage, + "changelogUrl": pluginConfig.changelogUrl, + "configUrl": pluginConfig.configUrl, + "downloadUrl": pluginConfig.downloadUrl, + "repository": { + "type": pluginConfig.repository.type, + "url": pluginConfig.repository.url + }, + "meshCentralCompat": pluginConfig.meshCentralCompat, + "status": 0 // 0: disabled, 1: enabled + }, function() { + parent.db.getPlugins(function(err, docs){ + var targets = ['*', 'server-users']; + parent.DispatchEvent(targets, obj, { action: 'updatePluginList', list: docs }); + + }); + }); + }); + }; + + obj.installPlugin = function(id, func) { + parent.db.getPlugin(id, function(err, docs){ + var http = require('https'); + // the "id" would probably suffice, but is probably an sanitary issue, generate a random instead + var randId = Math.random().toString(32).replace('0.', ''); + var fileName = obj.parent.path.join(require('os').tmpdir(), 'Plugin_'+randId+'.zip'); + var plugin = docs[0]; + if (plugin.repository.type == 'git') { + const file = obj.fs.createWriteStream(fileName); + var request = http.get(plugin.downloadUrl, function(response) { + response.pipe(file); + file.on('finish', function() { + file.close(function(){ + var yauzl = require("yauzl"); + if (!obj.fs.existsSync(obj.pluginPath)) { + obj.fs.mkdirSync(obj.pluginPath); + } + if (!obj.fs.existsSync(obj.parent.path.join(obj.pluginPath, plugin.shortName))) { + obj.fs.mkdirSync(obj.parent.path.join(obj.pluginPath, plugin.shortName)); + } + yauzl.open(fileName, { lazyEntries: true }, function (err, zipfile) { + if (err) throw err; + zipfile.readEntry(); + zipfile.on("entry", function (entry) { + let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName); + let pathReg = new RegExp(/(.*?\/)/); + if (process.platform == 'win32') pathReg = new RegExp(/(.*?\\/); + let filePath = obj.parent.path.join(pluginPath, entry.fileName.replace(pathReg, '')); // remove top level dir + + if (/\/$/.test(entry.fileName)) { // dir + if (!obj.fs.existsSync(filePath)) + obj.fs.mkdirSync(filePath); + zipfile.readEntry(); + } else { // file + zipfile.openReadStream(entry, function (err, readStream) { + if (err) throw err; + readStream.on("end", function () { zipfile.readEntry(); }); + readStream.pipe(obj.fs.createWriteStream(filePath)); + }); + } + }); + zipfile.on("end", function () { setTimeout(function () { + obj.fs.unlinkSync(fileName); + parent.db.setPluginStatus(id, 1, func); + obj.plugins[plugin.shortName] = require(obj.pluginPath + '/' + plugin.shortName + '/' + plugin.shortName + '.js')[plugin.shortName](obj); + obj.exports[plugin.shortName] = obj.plugins[plugin.shortName].exports; + }); }); + }); + }); + }); + }); + } else if (plugin.repository.type == 'npm') { + // @TODO npm install and symlink dirs (need a test plugin) + } + + + }); + + + }; + + obj.disablePlugin = function(id, func) { + parent.db.setPluginStatus(id, 0, func); + }; + + obj.removePlugin = function(id, func) { + parent.db.getPlugin(id, function(err, docs){ + var plugin = docs[0]; + var rimraf = require("rimraf"); + let pluginPath = obj.parent.path.join(obj.pluginPath, plugin.shortName); + rimraf.sync(pluginPath); + parent.db.deletePlugin(id, func); + delete obj.plugins[plugin.shortName]; + obj.parent.updateMeshCore(); + }); + }; + return obj; }; \ No newline at end of file diff --git a/public/styles/style.css b/public/styles/style.css index ebd1751c..b588133b 100644 --- a/public/styles/style.css +++ b/public/styles/style.css @@ -2584,14 +2584,18 @@ a { } #p7tbl .chDescription { - width: 40%; + width: 38%; } #p7tbl .chSite { - width: 10%; + width: 7%; } #p7tbl .chVersion { + width: 5%; +} + +#p7tbl .chUpgradeAvail { width: 10%; } @@ -2603,6 +2607,10 @@ a { width: 10%; } +.pActDisable, .pActDelete, .pActInstall, .pActUpgrade { + cursor: pointer; +} + #addPlugin { background-image: url(../images/plus32.png); width: 32px; @@ -2610,4 +2618,13 @@ a { float: right; cursor: pointer; margin-right: 12px; +} + +#pluginRestartNotice { + width: 40em; + font-weight: bold; + border: 1px solid red; + text-align: center; + padding: 14px; + margin: 50px auto; } \ No newline at end of file diff --git a/views/default.handlebars b/views/default.handlebars index 9eb3687b..6601da5e 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -411,10 +411,11 @@