diff --git a/Gruntfile.js b/Gruntfile.js index 7c866dbe91..ea8c518ec7 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -20,6 +20,7 @@ const prestart = require('./src/prestart'); prestart.loadConfig(configFile); const db = require('./src/database'); +const plugins = require('./src/plugins'); module.exports = function (grunt) { const args = []; @@ -40,35 +41,35 @@ module.exports = function (grunt) { grunt.registerTask('init', async function () { const done = this.async(); - let plugins = []; + let pluginList = []; if (!process.argv.includes('--core')) { await db.init(); - plugins = await db.getSortedSetRange('plugins:active', 0, -1); - addBaseThemes(plugins); - if (!plugins.includes('nodebb-plugin-composer-default')) { - plugins.push('nodebb-plugin-composer-default'); + pluginList = await plugins.getActive(); + addBaseThemes(pluginList); + if (!pluginList.includes('nodebb-plugin-composer-default')) { + pluginList.push('nodebb-plugin-composer-default'); } - if (!plugins.includes('nodebb-theme-persona')) { - plugins.push('nodebb-theme-persona'); + if (!pluginList.includes('nodebb-theme-persona')) { + pluginList.push('nodebb-theme-persona'); } } - const styleUpdated_Client = plugins.map(p => `node_modules/${p}/*.less`) - .concat(plugins.map(p => `node_modules/${p}/*.css`)) - .concat(plugins.map(p => `node_modules/${p}/+(public|static|less)/**/*.less`)) - .concat(plugins.map(p => `node_modules/${p}/+(public|static)/**/*.css`)); + const styleUpdated_Client = pluginList.map(p => `node_modules/${p}/*.less`) + .concat(pluginList.map(p => `node_modules/${p}/*.css`)) + .concat(pluginList.map(p => `node_modules/${p}/+(public|static|less)/**/*.less`)) + .concat(pluginList.map(p => `node_modules/${p}/+(public|static)/**/*.css`)); - const styleUpdated_Admin = plugins.map(p => `node_modules/${p}/*.less`) - .concat(plugins.map(p => `node_modules/${p}/*.css`)) - .concat(plugins.map(p => `node_modules/${p}/+(public|static|less)/**/*.less`)) - .concat(plugins.map(p => `node_modules/${p}/+(public|static)/**/*.css`)); + const styleUpdated_Admin = pluginList.map(p => `node_modules/${p}/*.less`) + .concat(pluginList.map(p => `node_modules/${p}/*.css`)) + .concat(pluginList.map(p => `node_modules/${p}/+(public|static|less)/**/*.less`)) + .concat(pluginList.map(p => `node_modules/${p}/+(public|static)/**/*.css`)); - const clientUpdated = plugins.map(p => `node_modules/${p}/+(public|static)/**/*.js`); - const serverUpdated = plugins.map(p => `node_modules/${p}/*.js`) - .concat(plugins.map(p => `node_modules/${p}/+(lib|src)/**/*.js`)); + const clientUpdated = pluginList.map(p => `node_modules/${p}/+(public|static)/**/*.js`); + const serverUpdated = pluginList.map(p => `node_modules/${p}/*.js`) + .concat(pluginList.map(p => `node_modules/${p}/+(lib|src)/**/*.js`)); - const templatesUpdated = plugins.map(p => `node_modules/${p}/+(public|static|templates)/**/*.tpl`); - const langUpdated = plugins.map(p => `node_modules/${p}/+(public|static|languages)/**/*.json`); + const templatesUpdated = pluginList.map(p => `node_modules/${p}/+(public|static|templates)/**/*.tpl`); + const langUpdated = pluginList.map(p => `node_modules/${p}/+(public|static|languages)/**/*.json`); grunt.config(['watch'], { styleUpdated_Client: { @@ -195,10 +196,10 @@ module.exports = function (grunt) { }); }; -function addBaseThemes(plugins) { - let themeId = plugins.find(p => p.includes('nodebb-theme-')); +function addBaseThemes(pluginList) { + let themeId = pluginList.find(p => p.includes('nodebb-theme-')); if (!themeId) { - return plugins; + return pluginList; } let baseTheme; do { @@ -209,9 +210,9 @@ function addBaseThemes(plugins) { } if (baseTheme) { - plugins.push(baseTheme); + pluginList.push(baseTheme); themeId = baseTheme; } } while (baseTheme); - return plugins; + return pluginList; } diff --git a/app.js b/app.js index f985362fbe..b6067d726b 100644 --- a/app.js +++ b/app.js @@ -71,7 +71,7 @@ if (nconf.get('setup') || nconf.get('install')) { }); } else if (nconf.get('activate')) { require('./src/cli/manage').activate(nconf.get('activate')); -} else if (nconf.get('plugins')) { +} else if (nconf.get('plugins') && typeof nconf.get('plugins') !== 'object') { require('./src/cli/manage').listPlugins(); } else if (nconf.get('build')) { require('./src/cli/manage').build(nconf.get('build')); diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 67a40dcb20..56d7f71f3b 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -241,6 +241,8 @@ "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP", + "plugins-set-in-configuration": "You are not allowed to change plugin state as they are defined at runtime (config.json, environmental variables or terminal arguments), please modify the configuration instead.", + "theme-not-set-in-configuration": "When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP", "topic-event-unrecognized": "Topic event '%1' unrecognized", diff --git a/src/cli/manage.js b/src/cli/manage.js index e8c565c87d..24052e21f6 100644 --- a/src/cli/manage.js +++ b/src/cli/manage.js @@ -4,6 +4,7 @@ const winston = require('winston'); const childProcess = require('child_process'); const CliGraph = require('cli-graph'); const chalk = require('chalk'); +const nconf = require('nconf'); const build = require('../meta/build'); const db = require('../database'); @@ -38,6 +39,10 @@ async function activate(plugin) { winston.info('Plugin `%s` already active', plugin); process.exit(0); } + if (nconf.get('plugins:active')) { + winston.error('Cannot activate plugins while plugin state configuration is set, please change your active configuration (config.json, environmental variables or terminal arguments) instead'); + process.exit(1); + } const numPlugins = await db.sortedSetCard('plugins:active'); winston.info('Activating plugin `%s`', plugin); await db.sortedSetAdd('plugins:active', numPlugins, plugin); @@ -57,8 +62,7 @@ async function listPlugins() { await db.init(); const installed = await plugins.showInstalled(); const installedList = installed.map(plugin => plugin.name); - const active = await db.getSortedSetRange('plugins:active', 0, -1); - + const active = await plugins.getActive(); // Merge the two sets, defer to plugins in `installed` if already present const combined = installed.concat(active.reduce((memo, cur) => { if (!installedList.includes(cur)) { @@ -108,13 +112,12 @@ async function info() { const hash = childProcess.execSync('git rev-parse HEAD'); console.log(` git hash: ${hash}`); - const config = require('../../config.json'); - console.log(` database: ${config.database}`); + console.log(` database: ${nconf.get('database')}`); await db.init(); const info = await db.info(db.client); - switch (config.database) { + switch (nconf.get('database')) { case 'redis': console.log(` version: ${info.redis_version}`); console.log(` disk sync: ${info.rdb_last_bgsave_status}`); @@ -124,6 +127,10 @@ async function info() { console.log(` version: ${info.version}`); console.log(` engine: ${info.storageEngine}`); break; + case 'postgres': + console.log(` version: ${info.version}`); + console.log(` uptime: ${info.uptime}`); + break; } const analyticsData = await analytics.getHourlyStatsForSet('analytics:pageviews', Date.now(), 24); diff --git a/src/cli/reset.js b/src/cli/reset.js index b026a22816..2de33244bc 100644 --- a/src/cli/reset.js +++ b/src/cli/reset.js @@ -4,6 +4,7 @@ const path = require('path'); const winston = require('winston'); const fs = require('fs'); const chalk = require('chalk'); +const nconf = require('nconf'); const db = require('../database'); const events = require('../events'); @@ -118,6 +119,10 @@ async function resetThemeTo(themeId) { async function resetPlugin(pluginId) { try { + if (nconf.get('plugins:active')) { + winston.error('Cannot reset plugins while plugin state is set in the configuration (config.json, environmental variables or terminal arguments), please modify the configuration instead'); + process.exit(1); + } const isActive = await db.isSortedSetMember('plugins:active', pluginId); if (isActive) { await db.sortedSetRemove('plugins:active', pluginId); @@ -137,6 +142,10 @@ async function resetPlugin(pluginId) { } async function resetPlugins() { + if (nconf.get('plugins:active')) { + winston.error('Cannot reset plugins while plugin state is set in the configuration (config.json, environmental variables or terminal arguments), please modify the configuration instead'); + process.exit(1); + } await db.delete('plugins:active'); winston.info('[reset] All Plugins De-activated'); } diff --git a/src/controllers/admin/plugins.js b/src/controllers/admin/plugins.js index eca548af72..a0b7457f01 100644 --- a/src/controllers/admin/plugins.js +++ b/src/controllers/admin/plugins.js @@ -35,6 +35,7 @@ pluginsController.get = async function (req, res) { installedCount: installedPlugins.length, activeCount: activePlugins.length, inactiveCount: Math.max(0, installedPlugins.length - activePlugins.length), + canChangeState: !nconf.get('plugins:active'), upgradeCount: compatible.reduce((count, current) => { if (current.installed && current.outdated) { count += 1; diff --git a/src/meta/build.js b/src/meta/build.js index eae68b4d12..0647cc7e0d 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -199,9 +199,9 @@ exports.webpack = async function (options) { const webpack = require('webpack'); const fs = require('fs'); const util = require('util'); - const db = require('../database'); + const plugins = require('../plugins/data'); - const activePlugins = await db.getSortedSetRange('plugins:active', 0, -1); + const activePlugins = await plugins.getActive(); if (!activePlugins.includes('nodebb-plugin-composer-default')) { activePlugins.push('nodebb-plugin-composer-default'); } diff --git a/src/meta/templates.js b/src/meta/templates.js index e801b79e44..89a68080fa 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -15,7 +15,6 @@ const Benchpress = require('benchpressjs'); const plugins = require('../plugins'); const file = require('../file'); -const db = require('../database'); const { themeNamePattern, paths } = require('../constants'); const viewsPath = nconf.get('views_dir'); @@ -119,7 +118,7 @@ async function compile() { await _rimraf(viewsPath); await mkdirp(viewsPath); - let files = await db.getSortedSetRange('plugins:active', 0, -1); + let files = await plugins.getActive(); files = await getTemplateDirs(files); files = await getTemplateFiles(files); diff --git a/src/meta/themes.js b/src/meta/themes.js index b769cf9e38..d4d3f58f45 100644 --- a/src/meta/themes.js +++ b/src/meta/themes.js @@ -101,6 +101,16 @@ Themes.set = async (data) => { let config = await fs.promises.readFile(pathToThemeJson, 'utf8'); config = JSON.parse(config); + const activePluginsConfig = nconf.get('plugins:active'); + if (!activePluginsConfig) { + await db.sortedSetRemove('plugins:active', current); + const numPlugins = await db.sortedSetCard('plugins:active'); + await db.sortedSetAdd('plugins:active', numPlugins, data.id); + } else if (!activePluginsConfig.includes(data.id)) { + // This prevents changing theme when configuration doesn't include it, but allows it otherwise + winston.error('When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP'); + throw new Error('[[error:theme-not-set-in-configuration]]'); + } // Re-set the themes path (for when NodeBB is reloaded) Themes.setPath(config); diff --git a/src/plugins/data.js b/src/plugins/data.js index ad125fda72..9f95703217 100644 --- a/src/plugins/data.js +++ b/src/plugins/data.js @@ -4,6 +4,7 @@ const fs = require('fs'); const path = require('path'); const winston = require('winston'); const _ = require('lodash'); +const nconf = require('nconf'); const db = require('../database'); const file = require('../file'); @@ -13,11 +14,19 @@ const Data = module.exports; const basePath = path.join(__dirname, '../../'); +// to get this functionality use `plugins.getActive()` from `src/plugins/install.js` instead +// this method duplicates that one, because requiring that file here would have side effects +async function getActiveIds() { + if (nconf.get('plugins:active')) { + return nconf.get('plugins:active'); + } + return await db.getSortedSetRange('plugins:active', 0, -1); +} + Data.getPluginPaths = async function () { - const plugins = await db.getSortedSetRange('plugins:active', 0, -1); + const plugins = await getActiveIds(); const pluginPaths = plugins.filter(plugin => plugin && typeof plugin === 'string') .map(plugin => path.join(paths.nodeModules, plugin)); - const exists = await Promise.all(pluginPaths.map(file.exists)); exists.forEach((exists, i) => { if (!exists) { @@ -96,7 +105,6 @@ Data.getStaticDirectories = async function (pluginData) { route}. Path must adhere to: ${validMappedPath.toString()}`); return; } - const dirPath = await resolveModulePath(pluginData.path, pluginData.staticDirs[route]); try { const stats = await fs.promises.stat(dirPath); diff --git a/src/plugins/install.js b/src/plugins/install.js index 05aca08e89..3784df09a0 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -56,6 +56,10 @@ module.exports = function (Plugins) { } Plugins.toggleActive = async function (id) { + if (nconf.get('plugins:active')) { + winston.error('Cannot activate plugins while plugin state is set in the configuration (config.json, environmental variables or terminal arguments), please modify the configuration instead'); + throw new Error('[[error:plugins-set-in-configuration]]'); + } const isActive = await Plugins.isActive(id); if (isActive) { await db.sortedSetRemove('plugins:active', id); @@ -144,10 +148,16 @@ module.exports = function (Plugins) { }; Plugins.isActive = async function (id) { + if (nconf.get('plugins:active')) { + return nconf.get('plugins:active').includes(id); + } return await db.isSortedSetMember('plugins:active', id); }; Plugins.getActive = async function () { + if (nconf.get('plugins:active')) { + return nconf.get('plugins:active'); + } return await db.getSortedSetRange('plugins:active', 0, -1); }; diff --git a/src/socket.io/admin/plugins.js b/src/socket.io/admin/plugins.js index c2d3f2ff13..46d77c9c1a 100644 --- a/src/socket.io/admin/plugins.js +++ b/src/socket.io/admin/plugins.js @@ -1,5 +1,7 @@ 'use strict'; +const nconf = require('nconf'); + const plugins = require('../../plugins'); const events = require('../../events'); const db = require('../../database'); @@ -35,6 +37,9 @@ Plugins.getActive = async function () { }; Plugins.orderActivePlugins = async function (socket, data) { + if (nconf.get('plugins:active')) { + throw new Error('[[error:plugins-set-in-configuration]]'); + } data = data.filter(plugin => plugin && plugin.name); await Promise.all(data.map(plugin => db.sortedSetAdd('plugins:active', plugin.order || 0, plugin.name))); }; diff --git a/src/upgrade.js b/src/upgrade.js index 829a338513..edea5a51ff 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -8,10 +8,10 @@ const readline = require('readline'); const winston = require('winston'); const chalk = require('chalk'); +const plugins = require('./plugins'); const db = require('./database'); const file = require('./file'); const { paths } = require('./constants'); - /* * Need to write an upgrade script for NodeBB? Cool. * @@ -61,8 +61,8 @@ Upgrade.getAll = async function () { Upgrade.appendPluginScripts = async function (files) { // Find all active plugins - const plugins = await db.getSortedSetRange('plugins:active', 0, -1); - plugins.forEach((plugin) => { + const activePlugins = await plugins.getActive(); + activePlugins.forEach((plugin) => { const configPath = path.join(paths.nodeModules, plugin, 'plugin.json'); try { const pluginConfig = require(configPath); diff --git a/src/views/admin/extend/plugins.tpl b/src/views/admin/extend/plugins.tpl index 12eb193396..e6d041181f 100644 --- a/src/views/admin/extend/plugins.tpl +++ b/src/views/admin/extend/plugins.tpl @@ -1,3 +1,6 @@ +{{{ if !canChangeState }}} +