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');
|
2021-10-12 17:40:08 -04:00
|
|
|
const _ = require('lodash');
|
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');
|
2020-10-01 21:02:44 -06:00
|
|
|
const { paths } = require('../constants');
|
2019-07-21 22:40:00 -04:00
|
|
|
|
|
|
|
|
const Data = module.exports;
|
|
|
|
|
|
|
|
|
|
const basePath = path.join(__dirname, '../../');
|
|
|
|
|
|
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 () {
|
2022-07-25 20:04:55 +02:00
|
|
|
const plugins = await getActiveIds();
|
2021-08-23 21:27:03 -04:00
|
|
|
const pluginPaths = plugins.filter(plugin => plugin && typeof plugin === 'string')
|
2020-10-01 21:02:44 -06:00
|
|
|
.map(plugin => path.join(paths.nodeModules, plugin));
|
2021-08-23 21:27:03 -04:00
|
|
|
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
|
|
|
}
|
2022-02-25 14:43:10 -05:00
|
|
|
const dirPath = await resolveModulePath(pluginData.path, pluginData.staticDirs[route]);
|
2022-11-10 13:31:51 -05:00
|
|
|
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;
|
2017-05-18 01:32:20 -06:00
|
|
|
}
|
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)));
|
2017-05-18 01:32:20 -06:00
|
|
|
|
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) {
|
2017-05-23 21:55:23 -06:00
|
|
|
if (typeof pluginData.languages !== 'string') {
|
2019-07-21 22:40:00 -04:00
|
|
|
return;
|
2017-05-23 21:55:23 -06:00
|
|
|
}
|
|
|
|
|
|
2020-10-01 21:02:44 -06:00
|
|
|
const pathToFolder = path.join(paths.nodeModules, pluginData.id, pluginData.languages);
|
|
|
|
|
const filepaths = await file.walk(pathToFolder);
|
2017-05-23 21:55:23 -06:00
|
|
|
|
2019-07-21 22:40:00 -04:00
|
|
|
const namespaces = [];
|
|
|
|
|
const languages = [];
|
2017-05-23 21:55:23 -06:00
|
|
|
|
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$/, '');
|
2017-05-23 21:55:23 -06:00
|
|
|
|
2019-07-21 22:40:00 -04:00
|
|
|
if (!language || !namespace) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2017-05-23 21:55:23 -06:00
|
|
|
|
2019-07-21 22:40:00 -04:00
|
|
|
languages.push(language);
|
|
|
|
|
namespaces.push(namespace);
|
2017-05-23 21:55:23 -06:00
|
|
|
});
|
2019-07-21 22:40:00 -04:00
|
|
|
return {
|
2021-10-12 17:40:08 -04:00
|
|
|
languages: _.uniq(languages),
|
|
|
|
|
namespaces: _.uniq(namespaces),
|
2019-07-21 22:40:00 -04:00
|
|
|
};
|
|
|
|
|
};
|