Files
NodeBB/src/plugins/data.js

266 lines
7.2 KiB
JavaScript
Raw Normal View History

2017-05-17 17:25:41 -06:00
'use strict';
2019-07-21 22:40:00 -04:00
const fs = require('fs');
const path = require('path');
const winston = require('winston');
const _ = require('lodash');
feat: Allow defining active plugins in config (#10767) * Revert "Revert "feat: cross origin opener policy options (#10710)"" This reverts commit 46050ace1a65430fe1b567086727da22afab4f73. * Revert "Revert "chore(i18n): fallback strings for new resources: nodebb.admin-settings-advanced"" This reverts commit 9f291c07d3423a41550b38770620a998b45e5c55. * 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>
2022-07-25 20:04:55 +02:00
const nconf = require('nconf');
2019-07-21 22:40:00 -04:00
const db = require('../database');
const file = require('../file');
const { paths } = require('../constants');
2019-07-21 22:40:00 -04:00
const Data = module.exports;
const basePath = path.join(__dirname, '../../');
feat: Allow defining active plugins in config (#10767) * Revert "Revert "feat: cross origin opener policy options (#10710)"" This reverts commit 46050ace1a65430fe1b567086727da22afab4f73. * Revert "Revert "chore(i18n): fallback strings for new resources: nodebb.admin-settings-advanced"" This reverts commit 9f291c07d3423a41550b38770620a998b45e5c55. * 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>
2022-07-25 20:04:55 +02:00
// 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);
}
2019-07-21 22:40:00 -04:00
Data.getPluginPaths = async function () {
feat: Allow defining active plugins in config (#10767) * Revert "Revert "feat: cross origin opener policy options (#10710)"" This reverts commit 46050ace1a65430fe1b567086727da22afab4f73. * Revert "Revert "chore(i18n): fallback strings for new resources: nodebb.admin-settings-advanced"" This reverts commit 9f291c07d3423a41550b38770620a998b45e5c55. * 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>
2022-07-25 20:04:55 +02:00
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) {
winston.warn(`[plugins] "${plugins[i]}" is active but not installed.`);
}
});
2021-08-23 21:41:08 -04:00
return pluginPaths.filter((p, i) => exists[i]);
2019-07-21 22:40:00 -04:00
};
Data.loadPluginInfo = async function (pluginPath) {
const [packageJson, pluginJson] = await Promise.all([
2020-08-14 00:05:03 -04:00
fs.promises.readFile(path.join(pluginPath, 'package.json'), 'utf8'),
fs.promises.readFile(path.join(pluginPath, 'plugin.json'), 'utf8'),
2019-07-21 22:40:00 -04:00
]);
let pluginData;
let packageData;
try {
pluginData = JSON.parse(pluginJson);
packageData = JSON.parse(packageJson);
pluginData.license = parseLicense(packageData);
pluginData.id = packageData.name;
pluginData.name = packageData.name;
pluginData.description = packageData.description;
pluginData.version = packageData.version;
pluginData.repository = packageData.repository;
pluginData.nbbpm = packageData.nbbpm;
pluginData.path = pluginPath;
} catch (err) {
2021-02-04 00:06:15 -07:00
const pluginDir = path.basename(pluginPath);
2019-07-21 22:40:00 -04:00
2021-02-03 23:59:08 -07:00
winston.error(`[plugins/${pluginDir}] Error in plugin.json or package.json!${err.stack}`);
2019-07-21 22:40:00 -04:00
throw new Error('[[error:parse-error]]');
}
return pluginData;
};
function parseLicense(packageData) {
try {
2021-02-03 23:59:08 -07:00
const licenseData = require(`spdx-license-list/licenses/${packageData.license}`);
2019-07-21 22:40:00 -04:00
return {
name: licenseData.name,
text: licenseData.licenseText,
};
} catch (e) {
// No license matched
return null;
}
2017-05-17 17:25:41 -06:00
}
2019-07-21 22:40:00 -04:00
Data.getActive = async function () {
const pluginPaths = await Data.getPluginPaths();
return await Promise.all(pluginPaths.map(p => Data.loadPluginInfo(p)));
};
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
Data.getStaticDirectories = async function (pluginData) {
2021-02-04 00:06:15 -07:00
const validMappedPath = /^[\w\-_]+$/;
2017-05-17 17:25:41 -06:00
if (!pluginData.staticDirs) {
2019-07-21 22:40:00 -04:00
return;
2017-05-17 17:25:41 -06:00
}
2019-07-21 22:40:00 -04:00
const dirs = Object.keys(pluginData.staticDirs);
2017-05-17 17:25:41 -06:00
if (!dirs.length) {
2019-07-21 22:40:00 -04:00
return;
2017-05-17 17:25:41 -06:00
}
2019-07-21 22:40:00 -04:00
const staticDirs = {};
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
async function processDir(route) {
2017-05-17 17:25:41 -06:00
if (!validMappedPath.test(route)) {
2021-02-03 23:59:08 -07:00
winston.warn(`[plugins/${pluginData.id}] Invalid mapped path specified: ${
route}. Path must adhere to: ${validMappedPath.toString()}`);
2019-07-21 22:40:00 -04:00
return;
2017-05-17 17:25:41 -06:00
}
const dirPath = await resolveModulePath(pluginData.path, pluginData.staticDirs[route]);
if (!dirPath) {
winston.warn(`[plugins/${pluginData.id}] Invalid mapped path specified: ${
route} => ${pluginData.staticDirs[route]}`);
return;
}
2019-07-21 22:40:00 -04:00
try {
2020-08-14 00:05:03 -04:00
const stats = await fs.promises.stat(dirPath);
2017-05-17 17:25:41 -06:00
if (!stats.isDirectory()) {
2021-02-03 23:59:08 -07:00
winston.warn(`[plugins/${pluginData.id}] Mapped path '${
route} => ${dirPath}' is not a directory.`);
2019-07-21 22:40:00 -04:00
return;
2017-05-17 17:25:41 -06:00
}
2021-02-03 23:59:08 -07:00
staticDirs[`${pluginData.id}/${route}`] = dirPath;
2019-07-21 22:40:00 -04:00
} catch (err) {
if (err.code === 'ENOENT') {
2021-02-03 23:59:08 -07:00
winston.warn(`[plugins/${pluginData.id}] Mapped path '${
route} => ${dirPath}' not found.`);
2019-07-21 22:40:00 -04:00
return;
}
throw err;
}
2019-07-21 22:40:00 -04:00
}
await Promise.all(dirs.map(route => processDir(route)));
Webpack5 (#10311) * feat: webpack 5 part 1 * fix: gruntfile fixes * fix: fix taskbar warning add app.importScript copy public/src/modules to build folder * refactor: remove commented old code * feat: reenable admin * fix: acp settings pages, fix sortable on manage categories embedded require in html not allowed * fix: bundle serialize/deserizeli so plugins dont break * test: fixe util tests * test: fix require path * test: more test fixes * test: require correct utils module * test: require correct utils * test: log stack * test: fix db require blowing up tests * test: move and disable bundle test * refactor: add aliases * test: disable testing route * fix: move webpack modules necessary for build, into `dependencies` * test: fix one more test remove 500-embed.tpl * fix: restore use of assets/nodebb.min.js, at least for now * fix: remove unnecessary line break * fix: point to proper ACP bundle * test: maybe fix build test * test: composer * refactor: dont need dist * refactor: more cleanup use everything from build/public folder * get rid of conditional import in app.js * fix: ace * refactor: cropper alias * test: lint and test fixes * lint: fix * refactor: rename function to app.require * refactor: go back to using app.require * chore: use github branch * chore: use webpack branch * feat: webpack webinstaller * feat: add chunkFile name with contenthash * refactor: move hooks to top * refactor: get rid of template500Function * fix(deps): use webpack5 branch of 2factor plugin * chore: tagging v2.0.0-beta.0 pre-release version :boom: :shipit: :tada: :rocket: * refactor: disable cache on templates loadTemplate is called once by benchpress and the result is cache internally * refactor: add server side helpers.js * feat: deprecate /plugins shorthand route, closes #10343 * refactor: use build/public for webpack * test: fix filename * fix: more specific selector * lint: ignore * refactor: fix comments * test: add debug for random failing test * refactor: cleanup remove test page, remove dupe functions in utils.common * lint: use relative path for now * chore: bump prerelease version * feat: add translateKeys * fix: optional params * fix: get rid of extra timeago files * refactor: cleanup, require timeago locale earlier remove translator.prepareDOM, it is in header.tpl html tag * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378) * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels - Existing hooks are preserved (to be deprecated at a later date, possibly) - New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks * docs: fix typo in comment * test: spec changes * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels (#10378) * refactor: privileges system to use a Map in the backend instead of separate objects for keys and labels - Existing hooks are preserved (to be deprecated at a later date, possibly) - New init hooks are called on NodeBB start, and provide a one-stop shop to add new privileges, instead of having to add to four different hooks * docs: fix typo in comment * test: spec changes * feat: allow app.require('bootbox'/'benchpressjs') * refactor: require server side utils * test: jquery ready * change istaller to use build/public * test: use document.addEventListener * refactor: closes #10301 * refactor: generateTopicClass * fix: column counts for other privileges * fix: #10443, regression where sorted-list items did not render into the DOM in the predicted order [breaking] * fix: typo in hook name * refactor: introduce a generic autocomplete.init() method that can be called to add nodebb-style autocompletion but using different data sources (e.g. not user/groups/tags) * fix: crash if `delay` not passed in (as it cannot be destructured) * refactor: replace substr * feat: set --panel-offset style in html element based on stored value in localStorage * refactor: addDropupHandler() logic to be less naive - Take into account height of the menu - Don't apply dropUp logic if there's nothing in the dropdown - Remove 'hidden' class (added by default in Persona for post tools) when menu items are added closes #10423 * refactor: simplify utils.params [breaking] Retrospective analysis of the usage of this method suggests that the options passed in are superfluous, and that only `url` is required. Using a browser built-in makes more sense to accomplish what this method sets out to do. * feat: add support for returning full URLSearchParams for utils.params * fix: utils.params() fallback handling * fix: default empty obj for params() * fix: remove \'loggedin\' and \'register\' qs parameters once they have been used, delay invocation of messages until ajaxify.end * fix: utils.params() not allowing relative paths to be passed in * refactor(DRY): new assertPasswordValidity utils method * fix: incorrect error message returned on insufficient privilege on flag edit * fix: read/update/delete access to flags API should be limited for moderators to only post flags in categories they moderate - added failing tests and patched up middleware.assert.flags to fix * refactor: flag api v3 tests to create new post and flags on every round * fix: missing error:no-flag language key * refactor: flags.canView to check flag existence, simplify middleware.assert.flag * feat: flag deletion API endpoint, #10426 * feat: UI for flag deletion, closes #10426 * chore: update plugin versions * chore: up emoji * chore: update markdown * chore: up emoji-android * fix: regression caused by utils.params() refactor, supports arrays and pipes all values through utils.toType, adjusts tests to type check Co-authored-by: Julian Lam <julian@nodebb.org>
2022-04-29 21:39:33 -04:00
winston.verbose(`[plugins] found ${Object.keys(staticDirs).length} static directories for ${pluginData.id}`);
2019-07-21 22:40:00 -04:00
return staticDirs;
};
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
Data.getFiles = async function (pluginData, type) {
2017-05-17 17:25:41 -06:00
if (!Array.isArray(pluginData[type]) || !pluginData[type].length) {
2019-07-21 22:40:00 -04:00
return;
2017-05-17 17:25:41 -06:00
}
2021-02-03 23:59:08 -07:00
winston.verbose(`[plugins] Found ${pluginData[type].length} ${type} file(s) for plugin ${pluginData.id}`);
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
return pluginData[type].map(file => path.join(pluginData.id, file));
};
2017-05-17 17:25:41 -06:00
/**
* With npm@3, dependencies can become flattened, and appear at the root level.
* This method resolves these differences if it can.
*/
2019-07-21 22:40:00 -04:00
async function resolveModulePath(basePath, modulePath) {
const isNodeModule = /node_modules/;
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
const currentPath = path.join(basePath, modulePath);
const exists = await file.exists(currentPath);
if (exists) {
return currentPath;
}
if (!isNodeModule.test(modulePath)) {
2021-02-03 23:59:08 -07:00
winston.warn(`[plugins] File not found: ${currentPath} (Ignoring)`);
2019-07-21 22:40:00 -04:00
return;
}
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
const dirPath = path.dirname(basePath);
if (dirPath === basePath) {
2021-02-03 23:59:08 -07:00
winston.warn(`[plugins] File not found: ${currentPath} (Ignoring)`);
2019-07-21 22:40:00 -04:00
return;
}
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
return await resolveModulePath(dirPath, modulePath);
2017-05-17 17:25:41 -06:00
}
2019-07-21 22:40:00 -04:00
Data.getScripts = async function getScripts(pluginData, target) {
2017-05-17 17:25:41 -06:00
target = (target === 'client') ? 'scripts' : 'acpScripts';
2019-07-21 22:40:00 -04:00
const input = pluginData[target];
2017-05-17 17:25:41 -06:00
if (!Array.isArray(input) || !input.length) {
2019-07-21 22:40:00 -04:00
return;
2017-05-17 17:25:41 -06:00
}
2019-07-21 22:40:00 -04:00
const scripts = [];
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
for (const filePath of input) {
/* eslint-disable no-await-in-loop */
const modulePath = await resolveModulePath(pluginData.path, filePath);
if (modulePath) {
scripts.push(modulePath);
2017-05-17 17:25:41 -06:00
}
2019-07-21 22:40:00 -04:00
}
if (scripts.length) {
2021-02-03 23:59:08 -07:00
winston.verbose(`[plugins] Found ${scripts.length} js file(s) for plugin ${pluginData.id}`);
2019-07-21 22:40:00 -04:00
}
return scripts;
};
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
Data.getModules = async function getModules(pluginData) {
2017-05-17 17:25:41 -06:00
if (!pluginData.modules || !pluginData.hasOwnProperty('modules')) {
2019-07-21 22:40:00 -04:00
return;
2017-05-17 17:25:41 -06:00
}
2019-07-21 22:40:00 -04:00
let pluginModules = pluginData.modules;
2017-05-17 17:25:41 -06:00
if (Array.isArray(pluginModules)) {
2021-02-04 00:06:15 -07:00
const strip = parseInt(pluginData.modulesStrip, 10) || 0;
2017-05-17 17:25:41 -06:00
2021-02-04 00:01:39 -07:00
pluginModules = pluginModules.reduce((prev, modulePath) => {
2021-02-04 00:06:15 -07:00
let key;
2017-05-17 17:25:41 -06:00
if (strip) {
2021-02-03 23:59:08 -07:00
key = modulePath.replace(new RegExp(`.?(/[^/]+){${strip}}/`), '');
2017-05-17 17:25:41 -06:00
} else {
key = path.basename(modulePath);
}
prev[key] = modulePath;
return prev;
}, {});
}
2019-07-21 22:40:00 -04:00
const modules = {};
async function processModule(key) {
const modulePath = await resolveModulePath(pluginData.path, pluginModules[key]);
if (modulePath) {
modules[key] = path.relative(basePath, modulePath);
2017-05-17 17:25:41 -06:00
}
2019-07-21 22:40:00 -04:00
}
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
await Promise.all(Object.keys(pluginModules).map(key => processModule(key)));
2019-07-21 22:40:00 -04:00
const len = Object.keys(modules).length;
2021-02-03 23:59:08 -07:00
winston.verbose(`[plugins] Found ${len} AMD-style module(s) for plugin ${pluginData.id}`);
2019-07-21 22:40:00 -04:00
return modules;
};
2017-05-17 17:25:41 -06:00
2019-07-21 22:40:00 -04:00
Data.getLanguageData = async function getLanguageData(pluginData) {
if (typeof pluginData.languages !== 'string') {
2019-07-21 22:40:00 -04:00
return;
}
const pathToFolder = path.join(paths.nodeModules, pluginData.id, pluginData.languages);
const filepaths = await file.walk(pathToFolder);
2019-07-21 22:40:00 -04:00
const namespaces = [];
const languages = [];
2021-02-04 00:01:39 -07:00
filepaths.forEach((p) => {
2019-07-21 22:40:00 -04:00
const rel = path.relative(pathToFolder, p).split(/[/\\]/);
const language = rel.shift().replace('_', '-').replace('@', '-x-');
const namespace = rel.join('/').replace(/\.json$/, '');
2019-07-21 22:40:00 -04:00
if (!language || !namespace) {
return;
}
2019-07-21 22:40:00 -04:00
languages.push(language);
namespaces.push(namespace);
});
2019-07-21 22:40:00 -04:00
return {
languages: _.uniq(languages),
namespaces: _.uniq(namespaces),
2019-07-21 22:40:00 -04:00
};
};