• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NodeBB / NodeBB / 23305017021

19 Mar 2026 04:20PM UTC coverage: 85.425% (-0.1%) from 85.545%
23305017021

push

github

nodebb-misty
chore: update changelog for v4.10.0

13445 of 18442 branches covered (72.9%)

28384 of 33227 relevant lines covered (85.42%)

3333.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.99
/src/plugins/data.js
1
'use strict';
2

3
const fs = require('fs');
32✔
4
const path = require('path');
32✔
5
const winston = require('winston');
32✔
6
const _ = require('lodash');
32✔
7
const nconf = require('nconf');
32✔
8

9
const db = require('../database');
32✔
10
const file = require('../file');
32✔
11
const { paths } = require('../constants');
32✔
12

13
const Data = module.exports;
32✔
14

15
const basePath = path.join(__dirname, '../../');
32✔
16

17
// to get this functionality use `plugins.getActive()` from `src/plugins/install.js` instead
18
// this method duplicates that one, because requiring that file here would have side effects
19
async function getActiveIds() {
20
        if (nconf.get('plugins:active')) {
36!
21
                return nconf.get('plugins:active');
×
22
        }
23
        return await db.getSortedSetRange('plugins:active', 0, -1);
36✔
24
}
25

26
Data.getPluginPaths = async function () {
32✔
27
        const plugins = await getActiveIds();
36✔
28
        const pluginPaths = plugins.filter(plugin => plugin && typeof plugin === 'string')
176✔
29
                .map(plugin => path.join(paths.nodeModules, plugin));
176✔
30
        const exists = await Promise.all(pluginPaths.map(file.exists));
36✔
31
        exists.forEach((exists, i) => {
36✔
32
                if (!exists) {
176!
33
                        winston.warn(`[plugins] "${plugins[i]}" is active but not installed.`);
×
34
                }
35
        });
36
        return pluginPaths.filter((p, i) => exists[i]);
176✔
37
};
38

39
Data.loadPluginInfo = async function (pluginPath) {
32✔
40
        const [packageJson, pluginJson] = await Promise.all([
968✔
41
                fs.promises.readFile(path.join(pluginPath, 'package.json'), 'utf8'),
42
                fs.promises.readFile(path.join(pluginPath, 'plugin.json'), 'utf8'),
43
        ]);
44

45
        let pluginData;
46
        let packageData;
47
        try {
968✔
48
                pluginData = JSON.parse(pluginJson);
968✔
49
                packageData = JSON.parse(packageJson);
968✔
50

51
                pluginData.license = parseLicense(packageData);
968✔
52

53
                pluginData.id = packageData.name;
968✔
54
                pluginData.name = packageData.name;
968✔
55
                pluginData.description = packageData.description;
968✔
56
                pluginData.version = packageData.version;
968✔
57
                pluginData.repository = packageData.repository;
968✔
58
                pluginData.url = pluginData.url || pluginData?.repository?.url || '';
968✔
59
                pluginData.nbbpm = packageData.nbbpm;
968✔
60
                pluginData.path = pluginPath;
968✔
61
        } catch (err) {
62
                const pluginDir = path.basename(pluginPath);
×
63

64
                winston.error(`[plugins/${pluginDir}] Error in plugin.json or package.json!${err.stack}`);
×
65
                throw new Error('[[error:parse-error]]');
×
66
        }
67
        return pluginData;
968✔
68
};
69

70
function parseLicense(packageData) {
71
        try {
968✔
72
                const licenseData = require(`spdx-license-list/licenses/${packageData.license}`);
968✔
73
                return {
960✔
74
                        name: licenseData.name,
75
                        text: licenseData.licenseText,
76
                };
77
        } catch (e) {
78
                // No license matched
79
                return null;
8✔
80
        }
81
}
82

83
Data.getActive = async function () {
32✔
84
        const pluginPaths = await Data.getPluginPaths();
32✔
85
        return await Promise.all(pluginPaths.map(p => Data.loadPluginInfo(p)));
160✔
86
};
87

88

89
Data.getStaticDirectories = async function (pluginData) {
32✔
90
        const validMappedPath = /^[\w\-_]+$/;
40✔
91

92
        if (!pluginData.staticDirs) {
40✔
93
                return;
32✔
94
        }
95

96
        const dirs = Object.keys(pluginData.staticDirs);
8✔
97
        if (!dirs.length) {
8!
98
                return;
×
99
        }
100

101
        const staticDirs = {};
8✔
102

103
        async function processDir(route) {
104
                if (!validMappedPath.test(route)) {
8!
105
                        winston.warn(`[plugins/${pluginData.id}] Invalid mapped path specified: ${
×
106
                                route}. Path must adhere to: ${validMappedPath.toString()}`);
107
                        return;
×
108
                }
109
                const dirPath = await resolveModulePath(pluginData.path, pluginData.staticDirs[route]);
8✔
110
                if (!dirPath) {
8!
111
                        winston.warn(`[plugins/${pluginData.id}] Invalid mapped path specified: ${
×
112
                                route} => ${pluginData.staticDirs[route]}`);
113
                        return;
×
114
                }
115
                try {
8✔
116
                        const stats = await fs.promises.stat(dirPath);
8✔
117
                        if (!stats.isDirectory()) {
8!
118
                                winston.warn(`[plugins/${pluginData.id}] Mapped path '${
×
119
                                        route} => ${dirPath}' is not a directory.`);
120
                                return;
×
121
                        }
122

123
                        staticDirs[`${pluginData.id}/${route}`] = dirPath;
8✔
124
                } catch (err) {
125
                        if (err.code === 'ENOENT') {
×
126
                                winston.warn(`[plugins/${pluginData.id}] Mapped path '${
×
127
                                        route} => ${dirPath}' not found.`);
128
                                return;
×
129
                        }
130
                        throw err;
×
131
                }
132
        }
133

134
        await Promise.all(dirs.map(route => processDir(route)));
8✔
135
        winston.verbose(`[plugins] found ${Object.keys(staticDirs).length} static directories for ${pluginData.id}`);
8✔
136
        return staticDirs;
8✔
137
};
138

139

140
Data.getFiles = async function (pluginData, type) {
32✔
141
        if (!Array.isArray(pluginData[type]) || !pluginData[type].length) {
160✔
142
                return;
124✔
143
        }
144

145
        winston.verbose(`[plugins] Found ${pluginData[type].length} ${type} file(s) for plugin ${pluginData.id}`);
36✔
146

147
        return pluginData[type].map(file => path.join(pluginData.id, file));
36✔
148
};
149

150
/**
151
 * With npm@3, dependencies can become flattened, and appear at the root level.
152
 * This method resolves these differences if it can.
153
 */
154
async function resolveModulePath(basePath, modulePath) {
155
        const isNodeModule = /node_modules/;
240✔
156

157
        const currentPath = path.join(basePath, modulePath);
240✔
158
        const exists = await file.exists(currentPath);
240✔
159
        if (exists) {
240✔
160
                return currentPath;
208✔
161
        }
162
        if (!isNodeModule.test(modulePath)) {
32!
163
                winston.warn(`[plugins] File not found: ${currentPath} (Ignoring)`);
×
164
                return;
×
165
        }
166

167
        const dirPath = path.dirname(basePath);
32✔
168
        if (dirPath === basePath) {
32!
169
                winston.warn(`[plugins] File not found: ${currentPath} (Ignoring)`);
×
170
                return;
×
171
        }
172

173
        return await resolveModulePath(dirPath, modulePath);
32✔
174
}
175

176

177
Data.getScripts = async function getScripts(pluginData, target) {
32✔
178
        target = (target === 'client') ? 'scripts' : 'acpScripts';
80✔
179

180
        const input = pluginData[target];
80✔
181
        if (!Array.isArray(input) || !input.length) {
80✔
182
                return;
56✔
183
        }
184

185
        const scripts = [];
24✔
186

187
        for (const filePath of input) {
24✔
188
                /* eslint-disable no-await-in-loop */
189
                const modulePath = await resolveModulePath(pluginData.path, filePath);
40✔
190
                if (modulePath) {
40!
191
                        scripts.push(modulePath);
40✔
192
                }
193
        }
194
        if (scripts.length) {
24!
195
                winston.verbose(`[plugins] Found ${scripts.length} js file(s) for plugin ${pluginData.id}`);
24✔
196
        }
197
        return scripts;
24✔
198
};
199

200

201
Data.getModules = async function getModules(pluginData) {
32✔
202
        if (!pluginData.modules || !pluginData.hasOwnProperty('modules')) {
40✔
203
                return;
8✔
204
        }
205

206
        let pluginModules = pluginData.modules;
32✔
207

208
        if (Array.isArray(pluginModules)) {
32!
209
                const strip = parseInt(pluginData.modulesStrip, 10) || 0;
×
210

211
                pluginModules = pluginModules.reduce((prev, modulePath) => {
×
212
                        let key;
213
                        if (strip) {
×
214
                                key = modulePath.replace(new RegExp(`.?(/[^/]+){${strip}}/`), '');
×
215
                        } else {
216
                                key = path.basename(modulePath);
×
217
                        }
218

219
                        prev[key] = modulePath;
×
220
                        return prev;
×
221
                }, {});
222
        }
223

224
        const modules = {};
32✔
225
        async function processModule(key) {
226
                const modulePath = await resolveModulePath(pluginData.path, pluginModules[key]);
160✔
227
                if (modulePath) {
160!
228
                        modules[key] = path.relative(basePath, modulePath);
160✔
229
                }
230
        }
231

232
        await Promise.all(Object.keys(pluginModules).map(key => processModule(key)));
160✔
233

234
        const len = Object.keys(modules).length;
32✔
235
        winston.verbose(`[plugins] Found ${len} AMD-style module(s) for plugin ${pluginData.id}`);
32✔
236
        return modules;
32✔
237
};
238

239
Data.getLanguageData = async function getLanguageData(pluginData) {
32✔
240
        if (typeof pluginData.languages !== 'string') {
40✔
241
                return;
24✔
242
        }
243

244
        const pathToFolder = path.join(paths.nodeModules, pluginData.id, pluginData.languages);
16✔
245
        const filepaths = await file.walk(pathToFolder);
16✔
246

247
        const namespaces = [];
16✔
248
        const languages = [];
16✔
249

250
        filepaths.forEach((p) => {
16✔
251
                const rel = path.relative(pathToFolder, p).split(/[/\\]/);
80✔
252
                const language = rel.shift().replace('_', '-').replace('@', '-x-');
80✔
253
                const namespace = rel.join('/').replace(/\.json$/, '');
80✔
254

255
                if (!language || !namespace) {
80✔
256
                        return;
8✔
257
                }
258

259
                languages.push(language);
72✔
260
                namespaces.push(namespace);
72✔
261
        });
262
        return {
16✔
263
                languages: _.uniq(languages),
264
                namespaces: _.uniq(namespaces),
265
        };
266
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc