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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user