Merge pull request #676 from ryanblenis/plugin-admin

Plugin GUI
This commit is contained in:
Ylian Saint-Hilaire 2019-11-25 09:53:55 -08:00 committed by GitHub
commit 73e3b669c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 998 additions and 32 deletions

44
db.js
View File

@ -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

View File

@ -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 + ')');
}

View File

@ -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;

View File

@ -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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

View File

@ -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%;
}

View File

@ -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">&nbsp;</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="">&nbsp;</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) {

View File

@ -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); } } }