Add preliminary plugin support

-Plugins can now be enabled via settings.json after dropping the plugin files into the new plugins directory
-Enabling plugins will allow extending the normal functionality of the explorer with new database collections, menus, pages and apis + open up a new url for data to be sent from the plugin to the explorer
-A new plugins section was added to the settings with a definition for the generic-snapshots plugin
-Locale strings are now loaded and shared out via the settings so there is generally no more need to explicitly include the locale.js file
-The locale object has been updated to localization within the explorer
-A number of new locale strings have been added and their values replaced with the locale string within the explorer
-Added plugin support verbiage and a link to the generic-snapshots crowdfunding task to the README
This commit is contained in:
Joe Uhren
2024-06-16 18:58:12 -06:00
parent 7ebdb5e868
commit 788454051c
35 changed files with 1196 additions and 728 deletions
+204 -22
View File
@@ -9,10 +9,11 @@ var express = require('express'),
routes = require('./routes/index'),
lib = require('./lib/explorer'),
db = require('./lib/database'),
package_metadata = require('./package.json'),
locale = require('./lib/locale');
package_metadata = require('./package.json');
var app = express();
var apiAccessList = [];
var viewPaths = [path.join(__dirname, 'views')]
var pluginRoutes = [];
const { exec } = require('child_process');
// pass wallet rpc connection info to nodeapi
@@ -63,8 +64,88 @@ if (settings.webserver.cors.enabled == true) {
});
}
// loop through all plugins defined in the settings
settings.plugins.allowed_plugins.forEach(function (plugin) {
// check if this plugin is enabled
if (plugin.enabled) {
const pluginName = (plugin.plugin_name == null ? '' : plugin.plugin_name);
// check if the plugin exists in the plugins directory
if (db.fs.existsSync(`./plugins/${pluginName}`)) {
// check if the plugin's local_plugin_settings file exists
if (db.fs.existsSync(`./plugins/${pluginName}/lib/local_plugin_settings.js`)) {
// load the local_plugin_settings.js file from the plugin
let localPluginSettings = require(`./plugins/${pluginName}/lib/local_plugin_settings`);
// loop through all local plugin settings
Object.keys(localPluginSettings).forEach(function(key, index, map) {
// check if this is a known setting type that should be brought into the main settings
if (key.endsWith('_page') && typeof localPluginSettings[key] === 'object' && localPluginSettings[key]['enabled'] == true) {
// this is a page setting
// add the page_id to the page setting
localPluginSettings[key].page_id = key;
// add the menu item title to the page setting
localPluginSettings[key].menu_title = localPluginSettings['localization'][`${key}_menu_title`];
// check if there is already a page for this plugin
if (plugin.pages == null) {
// initialize the pages array
plugin.pages = [];
}
// add this page setting to the main plugin data
plugin['pages'].push(localPluginSettings[key]);
} else if (key == 'public_apis') {
// this is a collection of new apis
// check if there is an ext section
if (localPluginSettings[key]['ext'] != null) {
// loop through all ext apis for this plugin
Object.keys(localPluginSettings[key]['ext']).forEach(function(extKey, extIndex, extMap) {
// add the name of the api into the object
localPluginSettings[key]['ext'][extKey]['api_name'] = extKey;
// loop through all parameters for this api and replace them in the description string if applicable
for (let p = 0; p < localPluginSettings[key]['ext'][extKey]['api_parameters'].length; p++)
localPluginSettings['localization'][`${extKey}_description`] = localPluginSettings['localization'][`${extKey}_description`].replace(new RegExp(`\\{${(p + 1)}}`, 'g'), localPluginSettings[key]['ext'][extKey]['api_parameters'][p]['parameter_name']);
// add the localized api description into the object
localPluginSettings[key]['ext'][extKey]['api_desc'] = localPluginSettings['localization'][`${extKey}_description`];
});
}
// copy the entire public_apis section from the plugin into the main settings
plugin.public_apis = localPluginSettings[key];
}
});
}
// check if the plugin's routes/index.js file exists
if (db.fs.existsSync(`./plugins/${pluginName}/routes/index.js`)) {
// get the plugin routes and save them to an array
pluginRoutes.push(require(`./plugins/${pluginName}/routes/index`));
// check if the plugin has a views directory
if (db.fs.existsSync(`./plugins/${pluginName}/views`)) {
// get the list of files in the views directory
const files = db.fs.readdirSync(`./plugins/${pluginName}/views`);
// filter the list of files to check if any have the .pug extension
const pugFiles = files.filter(file => path.extname(file) === '.pug');
// check if the plugin has 1 or more views
if (pugFiles.length > 0) {
// add this plugins view path to the list of view paths
viewPaths.push(path.resolve(`./plugins/${pluginName}/views`));
}
}
}
}
}
});
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('views', viewPaths);
app.set('view engine', 'pug');
var default_favicon = '';
@@ -95,6 +176,11 @@ app.use(express.static(path.join(__dirname, 'public')));
app.use('/api', nodeapi.app);
app.use('/', routes);
// loop through all plugin routes and add them to the app
pluginRoutes.forEach(function (r) {
app.use('/', r);
});
// post method to claim an address using verifymessage functionality
app.post('/claim', function(req, res) {
// validate captcha if applicable
@@ -227,6 +313,101 @@ function validate_captcha(captcha_enabled, data, cb) {
}
}
// post method to receive data from a plugin
app.post('/plugin-request', function(req, res) {
const pluginLockName = 'plugin';
// check if another plugin request is already running
if (lib.is_locked([pluginLockName], true) == true)
res.json({'status': 'failed', 'error': true, 'message': `Another plugin request is already running..`});
else {
// create a new plugin lock before checking the rest of the locks to minimize problems with running scripts at the same time
lib.create_lock(pluginLockName);
// check the backup, restore and delete locks since those functions would be problematic when updating data
if (lib.is_locked(['backup', 'restore', 'delete'], true) == true) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Another script has locked the database..`});
} else {
// all lock tests passed. OK to run plugin request
let dataObject = {};
try {
// attempt to parse the POST data field into a JSON object
dataObject = JSON.parse(req.body.data);
} catch {
// do nothing. errors will be handled below
}
// check if the dataObject was populated
if (dataObject == null || JSON.stringify(dataObject) === '{}') {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': 'POST data is missing or not in the correct format'});
} else {
// check if the plugin secret code is correct and if the coin name was specified
if (dataObject.plugin_data == null || settings.plugins.plugin_secret_code != dataObject.plugin_data.secret_code) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': 'Secret code is missing or incorrect'});
} else if (dataObject.plugin_data.coin_name == null || dataObject.plugin_data.coin_name == '') {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': 'Coin name is missing'});
} else {
const tableData = dataObject.table_data;
// check if the table_data seems valid
if (tableData == null || !Array.isArray(tableData)) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `table_data from POST data is missing or empty`});
} else {
const pluginName = (dataObject.plugin_data.plugin_name == null ? '' : dataObject.plugin_data.plugin_name);
const pluginObj = settings.plugins.allowed_plugins.find(item => item.plugin_name === pluginName && pluginName != '');
// check if the requested plugin was found in the settings
if (pluginObj == null) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is not defined in settings`});
} else {
// check if the requested plugin is enabled
if (!pluginObj.enabled) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is not enabled`});
} else {
// check if the plugin exists in the plugins directory
if (!db.fs.existsSync(`./plugins/${pluginName}`)) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is not installed in the plugins directory`});
} else {
// check if the plugin's server_functions file exists
if (!db.fs.existsSync(`./plugins/${pluginName}/lib/server_functions.js`)) {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is missing the /lib/server_functions.js file`});
} else {
// load the server_functions.js file from the plugin
const serverFunctions = require(`./plugins/${pluginName}/lib/server_functions`);
// check if the process_plugin_request function exists
if (typeof serverFunctions.process_plugin_request !== 'function') {
lib.remove_lock(pluginLockName);
res.json({'status': 'failed', 'error': true, 'message': `Plugin '${pluginName}' is missing the process_plugin_request function`});
} else {
// call the process_plugin_request function to process the new table data
serverFunctions.process_plugin_request(dataObject.plugin_data.coin_name, tableData, settings.sync.update_timeout, function(response) {
lib.remove_lock(pluginLockName);
res.json(response);
});
}
}
}
}
}
}
}
}
}
}
});
// extended apis
app.use('/ext/getmoneysupply', function(req, res) {
// check if the getmoneysupply api is enabled
@@ -237,7 +418,7 @@ app.use('/ext/getmoneysupply', function(req, res) {
res.end((stats && stats.supply ? stats.supply.toString() : '0'));
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/getaddress/:hash', function(req, res) {
@@ -288,7 +469,7 @@ app.use('/ext/getaddress/:hash', function(req, res) {
});
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/gettx/:txid', function(req, res) {
@@ -343,7 +524,7 @@ app.use('/ext/gettx/:txid', function(req, res) {
}
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/getbalance/:hash', function(req, res) {
@@ -357,7 +538,7 @@ app.use('/ext/getbalance/:hash', function(req, res) {
res.send({ error: 'address not found.', hash: req.params.hash });
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/getdistribution', function(req, res) {
@@ -371,7 +552,7 @@ app.use('/ext/getdistribution', function(req, res) {
});
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/getcurrentprice', function(req, res) {
@@ -384,7 +565,7 @@ app.use('/ext/getcurrentprice', function(req, res) {
res.send(p_ext);
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/getbasicstats', function(req, res) {
@@ -408,7 +589,7 @@ app.use('/ext/getbasicstats', function(req, res) {
}
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/getlasttxs/:min', function(req, res) {
@@ -461,7 +642,7 @@ app.use('/ext/getlasttxs/:min', function(req, res) {
}
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/getaddresstxs/:address/:start/:length', function(req, res) {
@@ -533,7 +714,7 @@ app.use('/ext/getaddresstxs/:address/:start/:length', function(req, res) {
}
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
function get_connection_and_block_counts(get_data, cb) {
@@ -581,7 +762,7 @@ app.use('/ext/getsummary', function(req, res) {
difficulty = difficulty['proof-of-stake'];
}
if (hashrate == 'There was an error. Check your console.')
if (hashrate == `${settings.localization.ex_error}: ${settings.localization.check_console}`)
hashrate = 0;
let mn_total = 0;
@@ -618,7 +799,7 @@ app.use('/ext/getsummary', function(req, res) {
});
}
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/getnetworkpeers', function(req, res) {
@@ -649,7 +830,7 @@ app.use('/ext/getnetworkpeers', function(req, res) {
res.json(peers);
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
// get the list of masternodes from local collection
@@ -668,7 +849,7 @@ app.use('/ext/getmasternodelist', function(req, res) {
res.send(masternodes);
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
// returns a list of masternode reward txs for a single masternode address from a specific block height
@@ -694,7 +875,7 @@ app.use('/ext/getmasternoderewards/:hash/:since', function(req, res) {
res.send({error: "failed to retrieve masternode rewards", hash: req.params.hash, since: req.params.since});
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
// returns the total masternode rewards received for a single masternode address from a specific block height
@@ -709,7 +890,7 @@ app.use('/ext/getmasternoderewardstotal/:hash/:since', function(req, res) {
res.send({error: "failed to retrieve masternode rewards", hash: req.params.hash, since: req.params.since});
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
// get the list of orphans from local collection
@@ -742,7 +923,7 @@ app.use('/ext/getorphanlist/:start/:length', function(req, res) {
res.json({"data": data, "recordsTotal": count, "recordsFiltered": count});
});
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
// get the last updated date for a particular section
@@ -765,7 +946,7 @@ app.use('/ext/getlastupdated/:section', function(req, res) {
res.send({error: 'Cannot find last updated date'});
}
} else
res.end('This method is disabled');
res.end(settings.localization.method_disabled);
});
app.use('/ext/getnetworkchartdata', function(req, res) {
@@ -785,7 +966,7 @@ app.use('/system/restartexplorer', function(req, res, next) {
res.end();
} else {
// show the error page
var err = new Error('Not Found');
var err = new Error(settings.localization.error_not_found);
err.status = 404;
next(err);
}
@@ -934,7 +1115,7 @@ settings.api_page.public_apis.rpc.getmasternodelist = { "enabled": false };
// locals
app.set('explorer_version', package_metadata.version);
app.set('locale', locale);
app.set('localization', settings.localization);
app.set('coin', settings.coin);
app.set('network_history', settings.network_history);
app.set('shared_pages', settings.shared_pages);
@@ -956,6 +1137,7 @@ app.set('labels', settings.labels);
app.set('default_coingecko_ids', settings.default_coingecko_ids);
app.set('api_cmds', settings.api_cmds);
app.set('blockchain_specific', settings.blockchain_specific);
app.set('plugins', settings.plugins);
// determine panel offset based on which panels are enabled
var paneltotal = 5;