mirror of
https://github.com/Ylianst/MeshCentral.git
synced 2025-01-23 12:43:14 -05:00
commit
73e3b669c8
44
db.js
44
db.js
@ -442,6 +442,9 @@ module.exports.CreateDB = function (parent, func) {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Setup plugin info collection
|
||||
obj.pluginsfile = db.collection('plugins');
|
||||
|
||||
setupFunctions(func); // Completed setup of MongoDB
|
||||
});
|
||||
@ -543,6 +546,9 @@ module.exports.CreateDB = function (parent, func) {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Setup plugin info collection
|
||||
obj.pluginsfile = db.collection('plugins');
|
||||
|
||||
setupFunctions(func); // Completed setup of MongoJS
|
||||
} else {
|
||||
@ -604,6 +610,10 @@ module.exports.CreateDB = function (parent, func) {
|
||||
obj.serverstatsfile.ensureIndex({ fieldName: 'time', expireAfterSeconds: 60 * 60 * 24 * 30 }); // Limit the server stats log to 30 days (Seconds * Minutes * Hours * Days)
|
||||
obj.serverstatsfile.ensureIndex({ fieldName: 'expire', expireAfterSeconds: 0 }); // Auto-expire events
|
||||
|
||||
// Setup plugin info collection
|
||||
obj.pluginsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-plugins.db'), autoload: true });
|
||||
obj.pluginsfile.persistence.setAutocompactionInterval(36000);
|
||||
|
||||
setupFunctions(func); // Completed setup of NeDB
|
||||
}
|
||||
|
||||
@ -754,6 +764,23 @@ module.exports.CreateDB = function (parent, func) {
|
||||
func(r);
|
||||
});
|
||||
}
|
||||
|
||||
// Add a plugin
|
||||
obj.addPlugin = function (plugin, func) { plugin.type = "plugin"; obj.pluginsfile.insertOne(plugin, func); };
|
||||
|
||||
// Get all plugins
|
||||
obj.getPlugins = function (func) { obj.pluginsfile.find({"type": "plugin"}).project({"type": 0}).sort({ name: 1 }).toArray(func); };
|
||||
|
||||
// Get plugin
|
||||
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, 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); };
|
||||
|
||||
obj.updatePlugin = function(id, args, func) { delete args._id; id = require('mongodb').ObjectID(id); obj.pluginsfile.updateOne({ _id: id }, { $set: args }, func); };
|
||||
|
||||
} else {
|
||||
// Database actions on the main collection (NeDB and MongoJS)
|
||||
obj.Set = function (data, func) {
|
||||
@ -884,6 +911,23 @@ module.exports.CreateDB = function (parent, func) {
|
||||
func(r);
|
||||
});
|
||||
}
|
||||
|
||||
// Add a plugin
|
||||
obj.addPlugin = function (plugin, func) { plugin.type = "plugin"; obj.pluginsfile.insert(plugin, func); };
|
||||
|
||||
// Get all plugins
|
||||
obj.getPlugins = function (func) { obj.pluginsfile.find({"type": "plugin"}, {"type": 0}).sort({ name: 1 }).exec(func); };
|
||||
|
||||
// Get plugin
|
||||
obj.getPlugin = function (id, func) { obj.pluginsfile.find({ _id: id }).sort({ name: 1 }).exec(func); };
|
||||
|
||||
// Delete plugin
|
||||
obj.deletePlugin = function (id, func) { obj.pluginsfile.remove({ _id: id }, func); };
|
||||
|
||||
obj.setPluginStatus = function(id, status, func) { obj.pluginsfile.update({ _id: id }, { $set: {status: status } }, func); };
|
||||
|
||||
obj.updatePlugin = function(id, args, func) { delete args._id; obj.pluginsfile.update({ _id: id }, { $set: args }, func); };
|
||||
|
||||
}
|
||||
|
||||
func(obj); // Completed function setup
|
||||
|
@ -1295,8 +1295,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 + ')');
|
||||
}
|
||||
|
85
meshuser.js
85
meshuser.js
@ -3137,6 +3137,88 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'distributeCore': {
|
||||
for (var i in command.nodes) {
|
||||
parent.sendMeshAgentCore(user, domain, command.nodes[i]._id, 'default');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'plugins': {
|
||||
// Since plugin actions generally require a server restart, use the Full admin permission
|
||||
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()
|
||||
.then(function(latest) {
|
||||
try { ws.send(JSON.stringify({ action: 'pluginVersionsAvailable', list: latest })); } catch (ex) { }
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'addplugin': {
|
||||
if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled
|
||||
try {
|
||||
parent.parent.pluginHandler.getPluginConfig(command.url)
|
||||
.then(parent.parent.pluginHandler.addPlugin)
|
||||
.then(function(docs){
|
||||
var targets = ['*', 'server-users'];
|
||||
parent.parent.DispatchEvent(targets, obj, { action: 'updatePluginList', list: docs });
|
||||
})
|
||||
.catch(function(err) {
|
||||
if (typeof err == 'object') err = err.message;
|
||||
try { ws.send(JSON.stringify({ action: 'pluginError', msg: err })); } catch (er) { }
|
||||
});
|
||||
|
||||
} catch(e) { console.log('Cannot add plugin: ' + e); }
|
||||
break;
|
||||
}
|
||||
case 'installplugin': {
|
||||
if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled
|
||||
parent.parent.pluginHandler.installPlugin(command.id, command.version_only, null, function(){
|
||||
parent.db.getPlugins(function(err, docs) {
|
||||
try { ws.send(JSON.stringify({ action: 'updatePluginList', list: docs, result: err })); } catch (ex) { }
|
||||
});
|
||||
var targets = ['*', 'server-users'];
|
||||
parent.parent.DispatchEvent(targets, obj, { action: 'pluginStateChange' });
|
||||
});
|
||||
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) { }
|
||||
var targets = ['*', 'server-users'];
|
||||
parent.parent.DispatchEvent(targets, obj, { action: 'pluginStateChange' });
|
||||
});
|
||||
});
|
||||
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 'getpluginversions': {
|
||||
if ((user.siteadmin & 0xFFFFFFFF) == 0 || parent.parent.pluginHandler == null) break; // must be full admin, plugins enabled
|
||||
parent.parent.pluginHandler.getPluginVersions(command.id)
|
||||
.then(function (versionInfo) {
|
||||
try { ws.send(JSON.stringify({ action: 'downgradePluginVersions', info: versionInfo, error: null })); } catch (ex) { }
|
||||
})
|
||||
.catch(function (e) {
|
||||
try { ws.send(JSON.stringify({ action: 'pluginError', msg: e })); } catch (ex) { }
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case 'plugin': {
|
||||
if (parent.parent.pluginHandler == null) break; // If the plugin's are not supported, reject this command.
|
||||
command.userid = user._id;
|
||||
@ -3144,8 +3226,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;
|
||||
|
392
pluginHandler.js
392
pluginHandler.js
@ -13,6 +13,7 @@
|
||||
/*jshint strict: false */
|
||||
/*jshint esversion: 6 */
|
||||
"use strict";
|
||||
require('promise');
|
||||
|
||||
module.exports.pluginHandler = function (parent) {
|
||||
var obj = {};
|
||||
@ -23,43 +24,64 @@ 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);
|
||||
}
|
||||
try { // try loading local info about plugin to database (if it changed locally)
|
||||
var plugin_config = obj.fs.readFileSync(obj.pluginPath + '/' + plugin.shortName + '/config.json');
|
||||
plugin_config = JSON.parse(plugin_config);
|
||||
parent.db.updatePlugin(plugin._id, plugin_config);
|
||||
} catch (e) { console.log('Plugin config file for '+ plugin.name +' could not be parsed.'); }
|
||||
}
|
||||
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.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';
|
||||
if (Array.isArray(obj.exports[p])) {
|
||||
for (const l of Object.values(obj.exports[p])) {
|
||||
str += ' obj.' + p + '.' + l + ' = ' + obj.plugins[p][l].toString() + '\r\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
str += `obj.onDeviceRefeshEnd = function(nodeid, panel, refresh, event) {
|
||||
for (const p of Object.keys(obj)) {
|
||||
if (typeof obj[p].onDeviceRefreshEnd == 'function') {
|
||||
obj[p].onDeviceRefreshEnd(nodeid, panel, refresh, event);
|
||||
str += `
|
||||
obj.callHook = function(hookName, ...args) {
|
||||
for (const p of Object.keys(obj)) {
|
||||
if (typeof obj[p][hookName] == 'function') {
|
||||
obj[p][hookName].apply(this, args);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
obj.registerPluginTab = function(pluginRegInfo) {
|
||||
var d = pluginRegInfo();
|
||||
if (!Q(d.tabId)) {
|
||||
@ -71,10 +93,28 @@ module.exports.pluginHandler = function (parent) {
|
||||
for (const i of pages) { i.style.display = 'none'; }
|
||||
QV(id, true);
|
||||
};
|
||||
obj.addPluginEx = function() {
|
||||
meshserver.send({ action: 'addplugin', url: Q('pluginurlinput').value});
|
||||
};
|
||||
obj.addPluginDlg = function() {
|
||||
setDialogMode(2, "Plugin Config URL", 3, obj.addPluginEx, '<input type=text id=pluginurlinput style=width:100% />');
|
||||
focusTextBox('pluginurlinput');
|
||||
};
|
||||
obj.refreshPluginHandler = function() {
|
||||
let st = document.createElement('script');
|
||||
st.src = '/pluginHandler.js';
|
||||
document.body.appendChild(st);
|
||||
};
|
||||
return obj; };`;
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
obj.refreshJS = function(req, res) {
|
||||
// to minimize server reboots when installing new plugins, we call the new data and overwrite the old pluginHandler on the front end
|
||||
res.set('Content-Type', 'text/javascript');
|
||||
res.send('pluginHandlerBuilder = '+obj.prepExports() + ' pluginHandler = new pluginHandlerBuilder();');
|
||||
}
|
||||
|
||||
obj.callHook = function (hookName, ...args) {
|
||||
for (var p in obj.plugins) {
|
||||
if (typeof obj.plugins[p][hookName] == 'function') {
|
||||
@ -152,7 +192,311 @@ module.exports.pluginHandler = function (parent) {
|
||||
}
|
||||
}
|
||||
return panel;
|
||||
};
|
||||
|
||||
obj.isValidConfig = function(conf, url) { // check for the required attributes
|
||||
var isValid = true;
|
||||
if (!(
|
||||
typeof conf.name == 'string'
|
||||
&& typeof conf.shortName == 'string'
|
||||
&& typeof conf.version == 'string'
|
||||
// && typeof conf.author == 'string'
|
||||
&& typeof conf.description == 'string'
|
||||
&& typeof conf.hasAdminPanel == 'boolean'
|
||||
&& typeof conf.homepage == 'string'
|
||||
&& typeof conf.changelogUrl == 'string'
|
||||
&& typeof conf.configUrl == 'string'
|
||||
&& typeof conf.repository == 'object'
|
||||
&& typeof conf.repository.type == 'string'
|
||||
&& typeof conf.repository.url == 'string'
|
||||
&& typeof conf.meshCentralCompat == 'string'
|
||||
// && 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.getPluginConfig = function(configUrl) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (configUrl.indexOf('https://') >= 0) {
|
||||
var http = require('https');
|
||||
} else {
|
||||
var http = require('http');
|
||||
}
|
||||
if (configUrl.indexOf('://') === -1) reject('Unable to fetch the config: Bad URL (' + configUrl + ')');
|
||||
http.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)) {
|
||||
resolve(pluginConfig);
|
||||
} else {
|
||||
reject("This does not appear to be a valid plugin configuration.");
|
||||
}
|
||||
|
||||
} catch (e) { reject('Error getting plugin config. Check that you have valid JSON.'); }
|
||||
} else {
|
||||
reject('Error getting plugin config. Check that you have valid JSON.');
|
||||
}
|
||||
});
|
||||
|
||||
}).on('error', function(e) {
|
||||
reject("Error getting plugin config: " + e.message);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
obj.getPluginLatest = function() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
parent.db.getPlugins(function(err, plugins) {
|
||||
var proms = [];
|
||||
plugins.forEach(function(curconf) {
|
||||
proms.push(obj.getPluginConfig(curconf.configUrl).catch(e => { return null; } ));
|
||||
});
|
||||
var latestRet = [];
|
||||
Promise.all(proms).then(function(newconfs) {
|
||||
var nconfs = [];
|
||||
// filter out config download issues
|
||||
newconfs.forEach(function(nc) {
|
||||
if (nc !== null) nconfs.push(nc);
|
||||
});
|
||||
nconfs.forEach(function(newconf) {
|
||||
var curconf = null;
|
||||
plugins.forEach(function(conf) {
|
||||
if (conf.configUrl == newconf.configUrl) curconf = conf;
|
||||
});
|
||||
if (curconf == null) reject('Some plugin configs could not be parsed');
|
||||
var s = require('semver');
|
||||
// MeshCentral doesn't adhere to semantic versioning (due to the -<alpha_char> at the end of the version)
|
||||
// Convert the letter to ASCII for a "true" version number comparison
|
||||
var mcCurVer = parent.currentVer.replace(/-(.)$/, (m, p1) => { return p1.charCodeAt(0); });
|
||||
var piCompatVer = newconf.meshCentralCompat.replace(/-(.)\b/g, (m, p1) => { return p1.charCodeAt(0); });
|
||||
latestRet.push({
|
||||
"id": curconf._id,
|
||||
"installedVersion": curconf.version,
|
||||
"version": newconf.version,
|
||||
"hasUpdate": s.gt(newconf.version, curconf.version),
|
||||
"meshCentralCompat": s.satisfies(mcCurVer, piCompatVer),
|
||||
"changelogUrl": curconf.changelogUrl,
|
||||
"status": curconf.status
|
||||
});
|
||||
resolve(latestRet);
|
||||
});
|
||||
}).catch((e) => { console.log('Error reaching plugins, update call aborted. ', e)});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
obj.addPlugin = function(pluginConfig) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
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,
|
||||
"versionHistoryUrl": pluginConfig.versionHistoryUrl,
|
||||
"status": 0 // 0: disabled, 1: enabled
|
||||
}, function() {
|
||||
parent.db.getPlugins(function(err, docs){
|
||||
if (err) reject(err);
|
||||
else resolve(docs);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
obj.installPlugin = function(id, version_only, force_url, func) {
|
||||
parent.db.getPlugin(id, function(err, docs){
|
||||
// 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 dl_url = plugin.downloadUrl;
|
||||
if (version_only != null && version_only != false) dl_url = version_only.url;
|
||||
if (force_url != null) dl_url = force_url;
|
||||
var url = require('url');
|
||||
var q = url.parse(dl_url, true);
|
||||
var http = (q.protocol == "http") ? require('http') : require('https');
|
||||
var opts = {
|
||||
path: q.pathname,
|
||||
host: q.hostname,
|
||||
port: q.port,
|
||||
headers: {
|
||||
'User-Agent': 'MeshCentral'
|
||||
},
|
||||
followRedirects: true,
|
||||
method: 'GET'
|
||||
};
|
||||
var request = http.get(opts, function(response) {
|
||||
// handle redirections with grace
|
||||
if (response.headers.location) return obj.installPlugin(id, version_only, response.headers.location, func);
|
||||
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);
|
||||
if (version_only == null || version_only === false) {
|
||||
parent.db.setPluginStatus(id, 1, func);
|
||||
} else {
|
||||
parent.db.updatePlugin(id, { status: 1, version: version_only.name }, 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;
|
||||
if (typeof obj.plugins[plugin.shortName].server_startup == 'function') obj.plugins[plugin.shortName].server_startup();
|
||||
var plugin_config = obj.fs.readFileSync(obj.pluginPath + '/' + plugin.shortName + '/config.json');
|
||||
plugin_config = JSON.parse(plugin_config);
|
||||
parent.db.updatePlugin(plugin._id, plugin_config);
|
||||
parent.updateMeshCore();
|
||||
}); });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if (plugin.repository.type == 'npm') {
|
||||
// @TODO npm support? (need a test plugin)
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
};
|
||||
|
||||
obj.getPluginVersions = function(id) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
parent.db.getPlugin(id, function(err, docs) {
|
||||
var plugin = docs[0];
|
||||
if (plugin.versionHistoryUrl == null) reject('No version history available for this plugin.');
|
||||
var url = require('url');
|
||||
var q = url.parse(plugin.versionHistoryUrl, true);
|
||||
var http = (q.protocol == "http") ? require('http') : require('https');
|
||||
var opts = {
|
||||
path: q.pathname,
|
||||
host: q.hostname,
|
||||
port: q.port,
|
||||
headers: {
|
||||
'User-Agent': 'MeshCentral',
|
||||
'Accept': 'application/vnd.github.v3+json'
|
||||
}
|
||||
};
|
||||
http.get(opts, function(res) {
|
||||
var versStr = '';
|
||||
res.on('data', function(chunk){
|
||||
versStr += chunk;
|
||||
});
|
||||
res.on('end', function(){
|
||||
if (versStr[0] == '{' || versStr[0] == '[') { // let's be sure we're JSON
|
||||
try {
|
||||
var vers = JSON.parse(versStr);
|
||||
var vList = [];
|
||||
var s = require('semver');
|
||||
vers.forEach((v) => {
|
||||
if (s.lt(v.name, plugin.version)) vList.push(v);
|
||||
});
|
||||
if (vers.length == 0) reject('No previous versions available.');
|
||||
resolve({ 'id': plugin._id, 'name': plugin.name, versionList: vList });
|
||||
} catch (e) { reject('Version history problem.'); }
|
||||
} else {
|
||||
reject('Version history appears to be malformed.'+versStr);
|
||||
}
|
||||
});
|
||||
}).on('error', function(e) {
|
||||
reject("Error getting plugin versions: " + e.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
obj.disablePlugin = function(id, func) {
|
||||
parent.db.getPlugin(id, function(err, docs){
|
||||
var plugin = docs[0];
|
||||
parent.db.setPluginStatus(id, 0, func);
|
||||
delete obj.plugins[plugin.shortName];
|
||||
delete obj.exports[plugin.shortName];
|
||||
});
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
};
|
||||
|
||||
obj.handleAdminReq = function (req, res, user, serv) {
|
||||
var path = obj.path.join(obj.pluginPath, req.query.pin, 'views');
|
||||
serv.app.set('views', path);
|
||||
if (obj.plugins[req.query.pin] != null && typeof obj.plugins[req.query.pin].handleAdminReq == 'function') {
|
||||
obj.plugins[req.query.pin].handleAdminReq(req, res, user);
|
||||
}
|
||||
else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
}
|
||||
|
||||
obj.handleAdminPostReq = function(req, res, user, serv) {
|
||||
var path = obj.path.join(obj.pluginPath, req.query.pin, 'views');
|
||||
serv.app.set('views', path);
|
||||
if (obj.plugins[req.query.pin] != null && typeof obj.plugins[req.query.pin].handleAdminPostReq == 'function') {
|
||||
obj.plugins[req.query.pin].handleAdminPostReq(req, res, user);
|
||||
}
|
||||
else {
|
||||
res.sendStatus(401);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
};
|
88
plugin_development.md
Normal file
88
plugin_development.md
Normal file
@ -0,0 +1,88 @@
|
||||
# Plugin Development
|
||||
|
||||
## Overview
|
||||
|
||||
## Anatomy of a plugin:
|
||||
|
||||
- plugin_name/
|
||||
-- config.json
|
||||
-- plugin_name.js
|
||||
-- modules_meshcore/ // optional
|
||||
--- plugin_name.js // optional
|
||||
|
||||
## Plugin Configuration File
|
||||
A valid JSON object within a file named `config.json` in the root folder of your project. An example:
|
||||
|
||||
{
|
||||
"name": "Plugin Name",
|
||||
"shortName": "plugin_name",
|
||||
"version": "0.0.0",
|
||||
"author": "Author Name",
|
||||
"description": "Short Description of the plugin",
|
||||
"hasAdminPanel": false,
|
||||
"homepage": "https://www.example.com",
|
||||
"changelogUrl": "https://raw.githubusercontent.com/User/Project/master/changelog.md",
|
||||
"configUrl": "https://raw.githubusercontent.com/User/Project/master/config.json",
|
||||
"downloadUrl": "https://github.com/User/Project/archive/master.zip",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/User/Project.git"
|
||||
},
|
||||
"versionHistoryUrl": "https://api.github.com/repos/User/Project/tags",
|
||||
"meshCentralCompat": ">0.4.3"
|
||||
}
|
||||
|
||||
## Configuration File Properties
|
||||
| Field | Required | Type | Description
|
||||
|--|--|--|--|
|
||||
| name | Yes | string | a human-readable name for the plugin
|
||||
| shortName | Yes | string | an alphanumeric, unique short identifier for the plugin (will be used to access your functions throughout the project
|
||||
| version | Yes | string | the current version of the plugin
|
||||
| author | No | string | the author's name
|
||||
| description | Yes | string | a short, human-readable description of what the plugin does
|
||||
| hasAdminPanel | Yes | boolean | `true` or `false`, indicates whether or not the plugin will offer its own administrative interface
|
||||
| homepage | Yes | string | the URL of the projects homepage
|
||||
| changelogUrl | Yes | string | the URL to the changelog of the project
|
||||
| configUrl | Yes | string | the URL to the config.json of the project
|
||||
| downloadUrl | Yes | string | the URL to a ZIP of the project (used for installation/upgrades)
|
||||
| repository | Yes | JSON object | contains the following attributes
|
||||
| repository.type | Yes | string | valid values are `git` and in the future, `npm` will also be supported in the future
|
||||
| repository.url | Yes | string | the URL to the project's repository
|
||||
| versionHistoryUrl | No | string | the URL to the project's versions/tags
|
||||
| meshCentralCompat | Yes | string | the semantic version string of required compatibility with the MeshCentral server
|
||||
|
||||
## Plugin Hooks
|
||||
These are separated into the following categories depending on the type of functionality the plugin should offer.
|
||||
|
||||
- Web UI, to modify the MeshCentral admin interface
|
||||
- Back End, to modify core functionality of the server and communicate with the Web UI layer as well as the Mesh Agent (Node) layer to send commands and data
|
||||
- Mesh Agent (Node), to introduce functionality to each agent
|
||||
|
||||
### Web UI Hooks
|
||||
`onDeviceRefeshEnd`: called when a device is selected in the MeshCentral web interface
|
||||
`registerPluginTab`: called when a device is selected in the MeshCentral web interface to register a new tab for plugin data, if required
|
||||
`onDesktopDisconnect`: called when a remote desktop session is disconnected
|
||||
|
||||
#### Exports
|
||||
Any function can be exported to the Web UI layer by adding the name of the function to an `exports` array in the plugin object.
|
||||
|
||||
### Back End Hooks
|
||||
`server_startup`: called once when the server starts (or when the plugin is first installed)
|
||||
`hook_agentCoreIsStable`: called once when an agent initially checks in
|
||||
`hook_processAgentData`: called each time an agent transmits data back to the server
|
||||
|
||||
### Mesh Agent
|
||||
Use of the optional file `plugin_name.js` in the optional folder `modules_meshcore` will include the file in the default meshcore file sent to each endpoint. This is useful to add functionality on each of the endpoints.
|
||||
|
||||
## Structure
|
||||
Much of MeshCentral revolves around returning objects for your structures, and plugins are no different. Within your plugin you can traverse all the way up to the web server and MeshCentral Server classes to access all the functionality those layers provide. This is done by passing the current object to newly created objects, and assigning that reference to a `parent` variable within that object.
|
||||
|
||||
|
||||
## Versioning
|
||||
Versioning your plugin correctly and consistently is essential to ensure users of your plugin are prompted to upgrade when it is available. Semantic versioning is recommended.
|
||||
|
||||
## Changelog
|
||||
A changelog is highly recommended so that your users know what's changed since their last version.
|
||||
|
||||
## Sample Plugin
|
||||
[MeshCentral-Sample](https://github.com/ryanblenis/MeshCentral-Sample) is a simple plugin that, upon disconnecting from remote desktop, prompts the user to enter a manual event (note), pre-filled in with the date and timestamp.
|
Binary file not shown.
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 41 KiB |
BIN
public/images/plus32.png
Normal file
BIN
public/images/plus32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 656 B |
@ -233,7 +233,7 @@ body {
|
||||
}
|
||||
|
||||
/* #UserDummyMenuSpan, */
|
||||
#MainSubMenuSpan, #MeshSubMenuSpan, #UserSubMenuSpan, #ServerSubMenuSpan, #MainMenuSpan, #MainSubMenu, #MeshSubMenu, #UserSubMenu, #ServerSubMenu, #UserDummyMenu {
|
||||
#MainSubMenuSpan, #MeshSubMenuSpan, #UserSubMenuSpan, #ServerSubMenuSpan, #MainMenuSpan, #MainSubMenu, #MeshSubMenu, #UserSubMenu, #ServerSubMenu, #UserDummyMenu, #PluginSubMenu {
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
color: white;
|
||||
@ -244,6 +244,10 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu_stack #PluginSubMenu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#MainMenuSpan {
|
||||
display: table;
|
||||
}
|
||||
@ -1291,6 +1295,17 @@ a {
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.lb7 {
|
||||
background: url(../images/leftbar-64.png) -382px -2px;
|
||||
height: 62px;
|
||||
width: 62px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.m0 {
|
||||
background: url(../images/images16.png) -32px 0px;
|
||||
height: 16px;
|
||||
@ -2563,4 +2578,73 @@ a {
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
background-color: #DDD;
|
||||
}
|
||||
}
|
||||
|
||||
#p7tbl {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
#p7tbl th, #p7tbl td {
|
||||
text-align: left;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
#p7tbl tr:nth-child(n+2):nth-child(odd) {
|
||||
background-color: #cfeeff;
|
||||
}
|
||||
|
||||
#p7tbl .chName {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
#p7tbl .chDescription {
|
||||
width: 38%;
|
||||
}
|
||||
|
||||
#p7tbl .chSite {
|
||||
width: 7%;
|
||||
}
|
||||
|
||||
#p7tbl .chVersion {
|
||||
width: 5%;
|
||||
}
|
||||
|
||||
#p7tbl .chUpgradeAvail {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
#p7tbl .chStatus {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
#p7tbl .chAction {
|
||||
width: 10%;
|
||||
}
|
||||
|
||||
.pActDisable, .pActDelete, .pActInstall, .pActUpgrade {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#addPlugin {
|
||||
background-image: url(../images/plus32.png);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
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;
|
||||
}
|
||||
|
||||
.pluginContent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -57,6 +57,9 @@
|
||||
<div id="cxtermnorm" class="cmtext" onclick="cmtermaction(1,event)">Normal Connect</div>
|
||||
<div id="cxtermps" class="cmtext" onclick="cmtermaction(2,event)">PowerShell Connect</div>
|
||||
</div>
|
||||
<div id="pluginTabContextMenu" class="contextMenu noselect" style="display:none;min-width:0px">
|
||||
<div id="cxclose" class="cmtext" onclick="pluginTabClose(event)">Close Tab</div>
|
||||
</div>
|
||||
<!-- main page -->
|
||||
<div id=container>
|
||||
<div id="notifiyBox" class="notifiyBox" style="display:none"></div>
|
||||
@ -88,6 +91,9 @@
|
||||
<div id=LeftMenuMyServer tabindex=0 class="lbbutton" style="display:none" title="My Server" onclick=go(6,event) onkeypress="if (event.key=='Enter') { go(6); }">
|
||||
<div class="lb6"></div>
|
||||
</div>
|
||||
<div id=LeftMenuMyPlugins tabindex=0 class="lbbutton" style="display:none" title="My Plugins" onclick=go(7,event) onkeypress="if (event.key=='Enter') { go(7); }">
|
||||
<div class="lb7"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id=topbar class=noselect>
|
||||
<div>
|
||||
@ -109,6 +115,7 @@
|
||||
<td tabindex=0 id=MainMenuMyFiles class="topbar_td style3x" onclick=go(5,event) onkeypress="if (event.key == 'Enter') go(5)">My Files</td>
|
||||
<td tabindex=0 id=MainMenuMyUsers class="topbar_td style3x" onclick=go(4,event) onkeypress="if (event.key == 'Enter') go(4)">My Users</td>
|
||||
<td tabindex=0 id=MainMenuMyServer class="topbar_td style3x" onclick=go(6,event) onkeypress="if (event.key == 'Enter') go(6)">My Server</td>
|
||||
<td tabindex=0 id=MainMenuMyPlugins class="topbar_td style3x" onclick=go(7,event) onkeypress="if (event.key == 'Enter') go(7)">My Plugins</td>
|
||||
<td class="topbar_td_end style3"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
@ -156,6 +163,11 @@
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div id=PluginSubMenuSpan>
|
||||
<table id=PluginSubMenu cellpadding=0 cellspacing=0 class=style1>
|
||||
<tr><td onclick="goPlugin(-1)" onkeypress="if (event.key == 'Enter') goPlugin(-1)" class="topbar_td style3x">Home</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div id=UserDummyMenuSpan>
|
||||
<table id=UserDummyMenu cellpadding=0 cellspacing=0 class=style1>
|
||||
<tr><td class=style3 style=""> </td></tr>
|
||||
@ -405,6 +417,16 @@
|
||||
<div id="serverStatsTable"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id=p7 style="display:none">
|
||||
<h1>My Plugins</h1>
|
||||
<div id="addPlugin" title="Add New Plugin" onclick="return pluginHandler.addPluginDlg();"></div>
|
||||
<table id="p7tbl">
|
||||
<tr><th class="chName">Name</th><th class="chDescription">Description</th><th class="chSite">Link</th><th class="chVersion">Version</th><th class="chUpgradeAvail">Latest Available</th><th class="chStatus">Status</th><th class="chAction">Action</th></tr>
|
||||
</table>
|
||||
<div id="pluginRestartNotice" style="display:none;"><div>Notice:</div> MeshCentral plugins have been altered. Agent cores require may require an update before full features are available.<br />
|
||||
Click <a href="#" onclick="distributeCore(); return false;">here</a> to update all Mesh Agent cores.
|
||||
</div>
|
||||
</div>
|
||||
<div id=p10 style="display:none">
|
||||
<table style="width:100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
@ -1042,6 +1064,7 @@
|
||||
var pluginHandlerBuilder = {{{pluginHandler}}};
|
||||
var pluginHandler = null;
|
||||
if (pluginHandlerBuilder != null) { pluginHandler = new pluginHandlerBuilder(); }
|
||||
var installedPluginList = null;
|
||||
|
||||
// Console Message Display Timers
|
||||
var p11DeskConsoleMsgTimer = null;
|
||||
@ -1295,6 +1318,7 @@
|
||||
// Fetch list of meshes, nodes, files
|
||||
meshserver.send({ action: 'meshes' });
|
||||
meshserver.send({ action: 'nodes', id: '{{currentNode}}' });
|
||||
if (pluginHandler != null) { meshserver.send({ action: 'plugins' }); }
|
||||
if ('{{currentNode}}' == '') { meshserver.send({ action: 'files' }); }
|
||||
if ('{{viewmode}}' == '') { go(1); }
|
||||
authCookieRenewTimer = setInterval(function () { meshserver.send({ action: 'authcookie' }); }, 1800000); // Request a cookie refresh every 30 minutes.
|
||||
@ -1337,6 +1361,8 @@
|
||||
QV('p2ServerActionsVersion', siteRights & 16);
|
||||
QV('MainMenuMyFiles', siteRights & 8);
|
||||
QV('LeftMenuMyFiles', siteRights & 8);
|
||||
QV('MainMenuMyPlugins', ((pluginHandler != null) && (siteRights & 0xFFFFFFFF)));
|
||||
QV('LeftMenuMyPlugins', ((pluginHandler != null) && (siteRights & 0xFFFFFFFF)));
|
||||
if (((siteRights & 8) == 0) && (xxcurrentView == 5)) { setDialogMode(0); go(1); }
|
||||
if (currentNode != null) { gotoDevice(currentNode._id, xxcurrentView, true); }
|
||||
|
||||
@ -2284,6 +2310,16 @@
|
||||
//console.log(message.msg);
|
||||
break;
|
||||
}
|
||||
case 'updatePluginList': {
|
||||
installedPluginList = message.event.list;
|
||||
updatePluginList();
|
||||
break;
|
||||
}
|
||||
case 'pluginStateChange': {
|
||||
if (pluginHandler == null) break;
|
||||
pluginHandler.refreshPluginHandler();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
//console.log('Unknown message.event.action', message.event.action);
|
||||
break;
|
||||
@ -2337,6 +2373,29 @@
|
||||
QH('p0span', message.msg);
|
||||
break;
|
||||
}
|
||||
case 'updatePluginList': {
|
||||
installedPluginList = message.list;
|
||||
updatePluginList();
|
||||
break;
|
||||
}
|
||||
case 'pluginVersionsAvailable': {
|
||||
if (pluginHandler == null) break;
|
||||
updatePluginList(message.list);
|
||||
break;
|
||||
}
|
||||
case 'downgradePluginVersions': {
|
||||
var vSelect = '<select id="lastPluginVersion">';
|
||||
message.info.versionList.forEach(function(v){
|
||||
vSelect += '<option value="' + v.zipball_url + '">' + v.name + '</option>';
|
||||
});
|
||||
vSelect += '</select>';
|
||||
setDialogMode(2, 'Plugin Action', 3, pluginActionEx, 'Select the version to downgrade the plugin: ' + message.info.name + '<hr />' + vSelect + '<hr />Please be aware that downgrading is not recommended. Please only do so in the event that a recent upgrade has broken something.<input id="lastPluginAct" type="hidden" value="downgrade" /><input id="lastPluginId" type="hidden" value="' + message.info.id + '" />');
|
||||
break;
|
||||
}
|
||||
case 'pluginError': {
|
||||
setDialogMode(2, 'Oops!', 1, null, message.msg);
|
||||
break;
|
||||
}
|
||||
case 'plugin': {
|
||||
if ((pluginHandler == null) || (typeof message.plugin != 'string')) break;
|
||||
try { pluginHandler[message.plugin][message.method](server, message); } catch (e) { console.log('Error loading plugin handler ('+ e + ')'); }
|
||||
@ -3484,6 +3543,12 @@
|
||||
contextmenudiv.style.left = event.pageX + 'px';
|
||||
contextmenudiv.style.top = event.pageY + 'px';
|
||||
contextmenudiv.style.display = 'block';
|
||||
} else if (elem && elem != null && elem.classList.contains('pluginTab')) {
|
||||
contextelement = elem;
|
||||
var contextmenudiv = document.getElementById('pluginTabContextMenu');
|
||||
contextmenudiv.style.left = event.pageX + 'px';
|
||||
contextmenudiv.style.top = event.pageY + 'px';
|
||||
contextmenudiv.style.display = 'block';
|
||||
} else {
|
||||
while (elem && elem != null && elem.id != 'devs') { elem = elem.parentElement; }
|
||||
if (!elem || elem == null) return true;
|
||||
@ -3555,10 +3620,21 @@
|
||||
connectTerminal(null, 1, { powershell: (action == 2) });
|
||||
}
|
||||
|
||||
function pluginTabClose() {
|
||||
var pluginTab = contextelement;
|
||||
var pname = pluginTab.getAttribute('x-data-plugin-sname');
|
||||
var pdiv = Q('plugin-'+pname);
|
||||
pdiv.parentNode.removeChild(pdiv);
|
||||
pluginTab.parentNode.removeChild(pluginTab);
|
||||
QV('p7', true);
|
||||
goPlugin(-1);
|
||||
}
|
||||
|
||||
function hideContextMenu() {
|
||||
QV('contextMenu', false);
|
||||
QV('meshContextMenu', false);
|
||||
QV('termShellContextMenu', false);
|
||||
QV('pluginTabContextMenu', false);
|
||||
contextelement = null;
|
||||
}
|
||||
|
||||
@ -4480,7 +4556,7 @@
|
||||
p13clearConsoleMsg();
|
||||
|
||||
// Device refresh plugin handler
|
||||
if (pluginHandler != null) { pluginHandler.onDeviceRefeshEnd(nodeid, panel, refresh, event); }
|
||||
if (pluginHandler != null) { pluginHandler.callHook('onDeviceRefreshEnd', nodeid, panel, refresh, event); }
|
||||
}
|
||||
setupDesktop(); // Always refresh the desktop, even if we are on the same device, we need to do some canvas switching.
|
||||
if (!panel) panel = 10;
|
||||
@ -5141,6 +5217,7 @@
|
||||
desktop.Stop();
|
||||
webRtcDesktopReset();
|
||||
desktopNode = desktop = null;
|
||||
if (pluginHandler != null) { pluginHandler.callHook('onDesktopDisconnect'); }
|
||||
}
|
||||
}
|
||||
|
||||
@ -9292,6 +9369,9 @@
|
||||
|
||||
// Remove top bar selection
|
||||
var mainBarItems = ['MainMenuMyDevices', 'MainMenuMyAccount', 'MainMenuMyEvents', 'MainMenuMyFiles', 'MainMenuMyUsers', 'MainMenuMyServer'];
|
||||
if (pluginHandler != null) {
|
||||
mainBarItems.push('MainMenuMyPlugins');
|
||||
}
|
||||
for (var i in mainBarItems) {
|
||||
QC(mainBarItems[i]).remove('fullselect');
|
||||
QC(mainBarItems[i]).remove('semiselect');
|
||||
@ -9299,6 +9379,9 @@
|
||||
|
||||
// Remove left bar selection
|
||||
var leftBarItems = ['LeftMenuMyDevices', 'LeftMenuMyAccount', 'LeftMenuMyEvents', 'LeftMenuMyFiles', 'LeftMenuMyUsers', 'LeftMenuMyServer'];
|
||||
if (pluginHandler != null) {
|
||||
leftBarItems.push('LeftMenuMyPlugins');
|
||||
}
|
||||
for (var i in leftBarItems) {
|
||||
QC(leftBarItems[i]).remove('lbbuttonsel');
|
||||
QC(leftBarItems[i]).remove('lbbuttonsel2');
|
||||
@ -9331,7 +9414,11 @@
|
||||
// My Server
|
||||
if ((x == 6) || (x == 115)) QC('MainMenuMyServer').add(mainMenuActiveClass);
|
||||
if ((x == 6) || (x == 115) || (x == 40)) QC('LeftMenuMyServer').add(leftMenuActiveClass);
|
||||
|
||||
|
||||
// My Plugins
|
||||
if (x == 7) QC('MainMenuMyPlugins').add(mainMenuActiveClass);
|
||||
if (x == 7) QC('LeftMenuMyPlugins').add(leftMenuActiveClass);
|
||||
|
||||
// column_l max-height
|
||||
if (webPageStackMenu && (x >= 10)) { QC('column_l').add('room4submenu'); } else { QC('column_l').remove('room4submenu'); }
|
||||
|
||||
@ -9368,6 +9455,8 @@
|
||||
// Fetch the server timeline stats if needed
|
||||
if ((x == 40) && (serverTimelineStats == null)) { refreshServerTimelineStats(); }
|
||||
|
||||
if (x == 7) { refreshPluginLatest(); QV('PluginSubMenuSpan', true); goPlugin(-1); } else { noGoPlugin(); }
|
||||
|
||||
// Update the web page title
|
||||
if ((currentNode) && (x >= 10) && (x < 20)) {
|
||||
document.title = decodeURIComponent('{{{extitle}}}') + ' - ' + currentNode.name + ' - ' + meshes[currentNode.meshid].name;
|
||||
@ -9375,7 +9464,209 @@
|
||||
document.title = decodeURIComponent('{{{extitle}}}');
|
||||
}
|
||||
}
|
||||
|
||||
function updatePluginList(versInfo) {
|
||||
if (Array.isArray(versInfo)) {
|
||||
versInfo.forEach(function(v) { updatePluginList(v); });
|
||||
}
|
||||
if (installedPluginList.length) {
|
||||
if (versInfo != null) {
|
||||
if (installedPluginList['version_info'] == null) installedPluginList['version_info'] = [];
|
||||
installedPluginList['version_info'][versInfo.id] = versInfo;
|
||||
}
|
||||
var tr = Q('p7tbl').querySelectorAll(".p7tblRow");
|
||||
if (tr.length) {
|
||||
for (const i in Object.values(tr)) {
|
||||
tr[i].parentNode.removeChild(tr[i]);
|
||||
}
|
||||
}
|
||||
var statusMap = {
|
||||
0: {
|
||||
"text": 'Disabled',
|
||||
"color": '858483'
|
||||
},
|
||||
1: {
|
||||
"text": 'Installed',
|
||||
"color": '00ff00'
|
||||
}
|
||||
};
|
||||
var statusAvailability = {
|
||||
0: {
|
||||
'install': 'Install',
|
||||
'delete': 'Delete'
|
||||
},
|
||||
1: {
|
||||
'disable': 'Disable',
|
||||
'upgrade': 'Upgrade',
|
||||
// 'downgrade': 'Downgrade' // disabling until plugins have prior versions available for better testing
|
||||
}
|
||||
};
|
||||
var vers_not_compat = ` [ <span onclick="return setDialogMode(2, 'Compatibility Issue', 1, null, 'This plugin version is not compatible with your MeshCentral installation, please upgrade MeshCentral first.');" title="Version incompatible, please upgrade your MeshCentral installation first" style="cursor: pointer; color:red;"> ! </span> ]`;
|
||||
|
||||
var tbl = Q('p7tbl');
|
||||
installedPluginList.forEach(function(p){
|
||||
var cant_action = [];
|
||||
if (p.hasAdminPanel == true && p.status) {
|
||||
p.nameHtml = `<a onclick="return goPlugin('${p.shortName}', '${p.name}');">${p.name}</a>`;
|
||||
} else {
|
||||
p.nameHtml = p.name;
|
||||
}
|
||||
p.statusText = statusMap[p.status].text;
|
||||
p.statusColor = statusMap[p.status].color;
|
||||
|
||||
if (p.versionHistoryUrl == null) {
|
||||
cant_action.push('downgrade');
|
||||
}
|
||||
|
||||
if (!p.status) { // It isn't technically installed, so no version number
|
||||
p.version = ' - ';
|
||||
}
|
||||
p.upgradeAvail = 'Checking...';
|
||||
if (installedPluginList['version_info'] != null && installedPluginList['version_info'][p._id] != null) {
|
||||
var vin = installedPluginList['version_info'][p._id];
|
||||
if (vin.hasUpdate) {
|
||||
p.upgradeAvail = '<a title="View Changelog" target="_blank" href="' + vin.changelogUrl + '">' + vin.version + '</a>';
|
||||
} else {
|
||||
cant_action.push('upgrade');
|
||||
if (p.status) p.upgradeAvail = 'Up to date';
|
||||
else p.upgradeAvail = '<a title="View Changelog" target="_blank" href="' + vin.changelogUrl + '">' + vin.version + '</a>';
|
||||
}
|
||||
if (!vin.meshCentralCompat) {
|
||||
p.upgradeAvail += vers_not_compat;
|
||||
cant_action.push('install');
|
||||
cant_action.push('upgrade');
|
||||
}
|
||||
}
|
||||
|
||||
p.actions = '<select onchange="return pluginAction(this, \'' + p._id + '\');"><option value=""> --</option>';
|
||||
for (const [k, v] of Object.entries(statusAvailability[p.status])) {
|
||||
if (cant_action.indexOf(k) === -1) {
|
||||
p.actions += '<option value="' + k + '">' + v + '</option>';
|
||||
}
|
||||
}
|
||||
p.actions += '</select>';
|
||||
|
||||
let tpl = `<td>${p.nameHtml}</td><td>${p.description}</td><td><a href="${p.homepage}" target="_blank">Homepage</a></td><td>${p.version}</td><td class="pluginUpgradeAvailable">${p.upgradeAvail}</td><td style="color: #${p.statusColor}">${p.statusText}</td><td class="pluginAction">${p.actions}</td>`;
|
||||
let tr = tbl.insertRow(-1);
|
||||
tr.innerHTML = tpl;
|
||||
tr.classList.add('p7tblRow');
|
||||
tr.setAttribute('data-id', p._id);
|
||||
tr.setAttribute('id', 'pluginRow-'+p._id);
|
||||
});
|
||||
} else {
|
||||
var tr = Q('p7tbl').querySelectorAll(".p7tblRow");
|
||||
for (const i in Object.values(tr)) {
|
||||
tr[i].parentNode.removeChild(tr[i]);
|
||||
}
|
||||
}
|
||||
if (versInfo == null) refreshPluginLatest();
|
||||
}
|
||||
|
||||
function refreshPluginLatest() {
|
||||
meshserver.send({ action: 'pluginLatestCheck' });
|
||||
}
|
||||
|
||||
function distributeCore() {
|
||||
meshserver.send({ action: 'distributeCore', nodes: nodes }); // all nodes the user has access to
|
||||
QV('pluginRestartNotice', false);
|
||||
}
|
||||
|
||||
function pluginActionEx() {
|
||||
var act = Q('lastPluginAct').value, id = Q('lastPluginId').value, pVersUrl = Q('lastPluginVersion').value;
|
||||
|
||||
switch(act) {
|
||||
case 'upgrade':
|
||||
case 'install':
|
||||
meshserver.send({ "action": "installplugin", "id": id, "version_only": false });
|
||||
break;
|
||||
case 'downgrade':
|
||||
Q('lastPluginVersion').querySelectorAll('option').forEach(function(opt) {
|
||||
if (opt.value == pVersUrl) pVers = opt.text;
|
||||
});
|
||||
meshserver.send({ "action": "installplugin", "id": id, "version_only": { "name": pVers, "url": pVersUrl }});
|
||||
break;
|
||||
case 'delete':
|
||||
meshserver.send({ "action": "removeplugin", "id": id });
|
||||
break;
|
||||
case 'disable':
|
||||
meshserver.send({ "action": "disableplugin", "id": id });
|
||||
break;
|
||||
}
|
||||
QS('pluginRestartNotice').display = '';
|
||||
}
|
||||
|
||||
function pluginAction(elem, id) {
|
||||
if (elem.value == 'downgrade') {
|
||||
meshserver.send({ "action": "getpluginversions", "id": id });
|
||||
} else {
|
||||
setDialogMode(2, 'Plugin Action', 3, pluginActionEx, 'Are you sure you want to ' + elem.value + ' the plugin: ' + elem.parentNode.parentNode.firstChild.innerText+'<input id="lastPluginAct" type="hidden" value="' + elem.value + '" /><input id="lastPluginId" type="hidden" value="' + elem.parentNode.parentNode.getAttribute('data-id') + '" /><input id="lastPluginVersion" type="hidden" value="" />');
|
||||
}
|
||||
elem.value = '';
|
||||
}
|
||||
|
||||
function goPlugin(pname, title) {
|
||||
let holder = Q('PluginSubMenu').querySelectorAll('tr')[0];
|
||||
let loadedPluginsTDs = holder.querySelectorAll('td');
|
||||
var found = false;
|
||||
loadedPluginsTDs.forEach((p) => {
|
||||
p.classList.remove('style3sel');
|
||||
p.classList.add('style3x');
|
||||
var tname = p.getAttribute('x-data-plugin-sname');
|
||||
if (tname != null) { Q('plugin-'+tname).style.display = 'none'; }
|
||||
if (tname == pname) {
|
||||
// show existing tab / content
|
||||
p.classList.remove('style3x');
|
||||
p.classList.add('style3sel');
|
||||
QS('p7').display = 'none';
|
||||
Q('plugin-'+tname).style.display = '';
|
||||
found = true;
|
||||
}
|
||||
});
|
||||
if (pname == -1) { // go gome
|
||||
QV('p7', true);
|
||||
let homeTab = loadedPluginsTDs[0];
|
||||
homeTab.classList.add('style3sel');
|
||||
homeTab.classList.remove('style3x');
|
||||
found = true;
|
||||
}
|
||||
if (found) return;
|
||||
|
||||
Q('PluginSubMenu').style.display = 'block';
|
||||
let sif = document.createElement('td');
|
||||
sif.setAttribute('x-data-plugin-sname', pname);
|
||||
sif.classList.add('topbar_td');
|
||||
sif.classList.add('style3sel');
|
||||
sif.classList.add('pluginTab');
|
||||
sif.setAttribute('onclick', 'goPlugin("' + pname + '", "' + title + '")');
|
||||
sif.setAttribute('onkeypress', 'if (event.key == "Enter") goPlugin("' + pname + '", "' + title + '")');
|
||||
sif.innerHTML = title;
|
||||
holder.append(sif);
|
||||
|
||||
let dif = document.createElement('div');
|
||||
dif.setAttribute('id', 'plugin-'+pname);
|
||||
dif.classList.add('pluginContent');
|
||||
let pif = document.createElement('iframe');
|
||||
pif.src = '/pluginadmin.ashx?pin='+pname;
|
||||
pif.setAttribute('frameBorder', '0');
|
||||
pif.style.width = '100%';
|
||||
pif.style.height = '100%';
|
||||
pif.setAttribute('frameBorder', '0');
|
||||
dif.append(pif);
|
||||
let x = Q('column_l_bottomgap');
|
||||
x.parentNode.insertBefore(dif, x.previousSibling);
|
||||
QS('p7').display = 'none';
|
||||
|
||||
}
|
||||
|
||||
function noGoPlugin(el) {
|
||||
QV('PluginSubMenuSpan', false);
|
||||
let loadedPluginsTDs = Q('PluginSubMenu').querySelectorAll('td');
|
||||
loadedPluginsTDs.forEach((p) => {
|
||||
var tname = p.getAttribute('x-data-plugin-sname');
|
||||
if (tname != null) Q('plugin-'+tname).style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
// Generic methods
|
||||
function joinPaths() { var x = []; for (var i in arguments) { var w = arguments[i]; if ((w != null) && (w != '')) { while (w.endsWith('/') || w.endsWith('\\')) { w = w.substring(0, w.length - 1); } while (w.startsWith('/') || w.startsWith('\\')) { w = w.substring(1); } x.push(w); } } return x.join('/'); }
|
||||
function putstore(name, val) {
|
||||
|
35
webserver.js
35
webserver.js
@ -3214,6 +3214,36 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
obj.handlePluginAdminReq = function(req, res) {
|
||||
const domain = checkUserIpAddress(req, res);
|
||||
if (domain == null) { res.sendStatus(404); return; }
|
||||
if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
|
||||
var user = obj.users[req.session.userid];
|
||||
if (user == null) { res.sendStatus(401); return; }
|
||||
|
||||
parent.pluginHandler.handleAdminReq(req, res, user, obj);
|
||||
}
|
||||
|
||||
obj.handlePluginAdminPostReq = function(req, res) {
|
||||
const domain = checkUserIpAddress(req, res);
|
||||
if (domain == null) { res.sendStatus(404); return; }
|
||||
if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
|
||||
var user = obj.users[req.session.userid];
|
||||
if (user == null) { res.sendStatus(401); return; }
|
||||
|
||||
parent.pluginHandler.handleAdminPostReq(req, res, user, obj);
|
||||
}
|
||||
|
||||
obj.handlePluginJS = function(req, res) {
|
||||
const domain = checkUserIpAddress(req, res);
|
||||
if (domain == null) { res.sendStatus(404); return; }
|
||||
if ((!req.session) || (req.session == null) || (!req.session.userid)) { res.sendStatus(401); return; }
|
||||
var user = obj.users[req.session.userid];
|
||||
if (user == null) { res.sendStatus(401); return; }
|
||||
|
||||
parent.pluginHandler.refreshJS(req, res);
|
||||
}
|
||||
|
||||
// Starts the HTTPS server, this should be called after the user/mesh tables are loaded
|
||||
function serverStart() {
|
||||
@ -3337,6 +3367,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) {
|
||||
obj.app.get(url + 'logo.png', handleLogoRequest);
|
||||
obj.app.get(url + 'welcome.jpg', handleWelcomeImageRequest);
|
||||
obj.app.ws(url + 'amtactivate', handleAmtActivateWebSocket);
|
||||
if (parent.pluginHandler != null) {
|
||||
obj.app.get(url + 'pluginadmin.ashx', obj.handlePluginAdminReq);
|
||||
obj.app.post(url + 'pluginadmin.ashx', obj.handlePluginAdminPostReq);
|
||||
obj.app.get(url + 'pluginHandler.js', obj.handlePluginJS);
|
||||
}
|
||||
|
||||
// Server redirects
|
||||
if (parent.config.domains[i].redirects) { for (var j in parent.config.domains[i].redirects) { if (j[0] != '_') { obj.app.get(url + j, obj.handleDomainRedirect); } } }
|
||||
|
Loading…
x
Reference in New Issue
Block a user