feat: Allow defining active plugins in config (#10767)

* Revert "Revert "feat: cross origin opener policy options (#10710)""

This reverts commit 46050ace1a.

* Revert "Revert "chore(i18n): fallback strings for new resources: nodebb.admin-settings-advanced""

This reverts commit 9f291c07d3.

* feat: closes #10719, don't trim children if category is marked section

* feat: fire hook to allow plugins to filter the pids returned in a user profile

/cc julianlam/nodebb-plugin-support-forum#14

* fix: use `user.hidePrivateData();` more consistently across user retrieval endpoints

* feat: Allow defining active plugins in config

resolves #10766

* fix: assign the db result to files properly

* test: add tests with plugins in config

* feat: better theme change handling

* feat: add visual indication that plugins can't be activated

* test: correct hooks

* test: fix test definitions

* test: remove instead of resetting nconf to avoid affecting other tests

* test: ... I forgot how nconf worked

* fix: remove negation

* docs: improve wording of error message

* feat: reduce code duplication

* style: remove a redundant space

* fix: remove unused imports

* fix: use nconf instead of requiring config.json

* fix: await...

* fix: second missed await

* fix: move back from getActiveIds to getActive

* fix: use paths again?

* fix: typo

* fix: move require into the function

* fix: forgot to change back to getActive

* test: getActive returns only id

* test: accedently commented out some stuff

* feat: added note to top of plugins page if \!canChangeState

Co-authored-by: Julian Lam <julian@nodebb.org>
Co-authored-by: Barış Soner Uşaklı <barisusakli@gmail.com>
isekai-main
Opliko 3 years ago committed by Julian Lam
parent a6af47da02
commit 23cb67a112

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

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

@ -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 &ndash; 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",

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

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

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

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

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

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

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

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

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

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

@ -1,3 +1,6 @@
{{{ if !canChangeState }}}
<div class="alert alert-warning">[[error:plugins-set-in-configuration]]</div>
{{{ end }}}
<ul class="nav nav-pills">
<li>
<a href="#trending" data-toggle="tab">

@ -5,7 +5,7 @@
<!-- IF ../isTheme -->
<a href="{config.relative_path}/admin/appearance/themes" class="btn btn-info">[[admin/extend/plugins:plugin-item.themes]]</a>
<!-- ELSE -->
<button data-action="toggleActive" class="btn <!-- IF ../active --> btn-warning<!-- ELSE --> btn-success<!-- ENDIF ../active -->">
<button data-action="toggleActive" class="btn <!-- IF ../active --> btn-warning<!-- ELSE --> btn-success<!-- ENDIF ../active --> <!-- IF !canChangeState -->disabled<!-- ENDIF -->">
<i class="fa fa-power-off"></i> <!-- IF ../active -->[[admin/extend/plugins:plugin-item.deactivate]]<!-- ELSE -->[[admin/extend/plugins:plugin-item.activate]]<!-- ENDIF ../active --></button>
<!-- ENDIF ../isTheme -->

@ -319,6 +319,85 @@ describe('Plugins', () => {
});
});
});
describe('plugin state set in configuration', () => {
const activePlugins = [
'nodebb-plugin-markdown',
'nodebb-plugin-mentions',
];
const inactivePlugin = 'nodebb-plugin-emoji';
beforeEach((done) => {
nconf.set('plugins:active', activePlugins);
done();
});
afterEach((done) => {
nconf.set('plugins:active', undefined);
done();
});
it('should return active plugin state from configuration', (done) => {
plugins.isActive(activePlugins[0], (err, isActive) => {
assert.ifError(err);
assert(isActive);
done();
});
});
it('should return inactive plugin state if not in configuration', (done) => {
plugins.isActive(inactivePlugin, (err, isActive) => {
assert.ifError(err);
assert(!isActive);
done();
});
});
it('should get a list of plugins from configuration', (done) => {
plugins.list((err, data) => {
assert.ifError(err);
const keys = ['id', 'name', 'url', 'description', 'latest', 'installed', 'active', 'latest'];
assert(Array.isArray(data));
keys.forEach((key) => {
assert(data[0].hasOwnProperty(key));
});
data.forEach((pluginData) => {
assert.equal(pluginData.active, activePlugins.includes(pluginData.id));
});
done();
});
});
it('should return a list of only active plugins from configuration', (done) => {
plugins.getActive((err, data) => {
assert.ifError(err);
assert(Array.isArray(data));
data.forEach((pluginData) => {
console.log(pluginData);
assert(activePlugins.includes(pluginData));
});
done();
});
});
it('should not deactivate a plugin if active plugins are set in configuration', (done) => {
assert.rejects(plugins.toggleActive(activePlugins[0]), Error).then(() => {
plugins.isActive(activePlugins[0], (err, isActive) => {
assert.ifError(err);
assert(isActive);
done();
});
});
});
it('should not activate a plugin if active plugins are set in configuration', (done) => {
assert.rejects(plugins.toggleActive(inactivePlugin), Error).then(() => {
plugins.isActive(inactivePlugin, (err, isActive) => {
assert.ifError(err);
assert(!isActive);
done();
});
});
});
});
});

Loading…
Cancel
Save