diff --git a/.eslintignore b/.eslintignore index 11b456699f..ca092ce2ea 100644 --- a/.eslintignore +++ b/.eslintignore @@ -18,3 +18,4 @@ logs/ /build .eslintrc test/files +*.min.js diff --git a/src/cli/manage.js b/src/cli/manage.js index 37d2d9bc26..9d360494ec 100644 --- a/src/cli/manage.js +++ b/src/cli/manage.js @@ -34,8 +34,11 @@ function buildTargets() { ); } +var themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/; +var pluginNamePattern = /^(@.*?\/)?nodebb-(theme|plugin|widget|rewards)-.*$/; + function activate(plugin) { - if (plugin.startsWith('nodebb-theme-')) { + if (themeNamePattern.test(plugin)) { reset.reset({ theme: plugin, }, function (err) { @@ -50,7 +53,7 @@ function activate(plugin) { db.init(next); }, function (next) { - if (!plugin.startsWith('nodebb-')) { + if (!pluginNamePattern.test(plugin)) { // Allow omission of `nodebb-plugin-` plugin = 'nodebb-plugin-' + plugin; } diff --git a/src/cli/reset.js b/src/cli/reset.js index bb0d110478..44d78df961 100644 --- a/src/cli/reset.js +++ b/src/cli/reset.js @@ -14,6 +14,9 @@ var widgets = require('../widgets'); var dirname = require('./paths').baseDir; +var themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/; +var pluginNamePattern = /^(@.*?\/)?nodebb-(theme|plugin|widget|rewards)-.*$/; + exports.reset = function (options, callback) { var map = { theme: function (next) { @@ -21,7 +24,7 @@ exports.reset = function (options, callback) { if (themeId === true) { resetThemes(next); } else { - if (!themeId.startsWith('nodebb-theme-')) { + if (!themeNamePattern.test(themeId)) { // Allow omission of `nodebb-theme-` themeId = 'nodebb-theme-' + themeId; } @@ -34,7 +37,7 @@ exports.reset = function (options, callback) { if (pluginId === true) { resetPlugins(next); } else { - if (!pluginId.startsWith('nodebb-plugin-')) { + if (!pluginNamePattern.test(pluginId)) { // Allow omission of `nodebb-plugin-` pluginId = 'nodebb-plugin-' + pluginId; } diff --git a/src/controllers/admin/themes.js b/src/controllers/admin/themes.js index 717d4bf0dc..719cb8ecff 100644 --- a/src/controllers/admin/themes.js +++ b/src/controllers/admin/themes.js @@ -16,22 +16,26 @@ themesController.get = function (req, res, next) { var screenshotPath; async.waterfall([ function (next) { - file.exists(themeConfigPath, next); - }, - function (exists, next) { - if (!exists) { - return next(Error('invalid-data')); - } + fs.readFile(themeConfigPath, 'utf8', function (err, config) { + if (err) { + if (err.code === 'ENOENT') { + return next(Error('invalid-data')); + } - fs.readFile(themeConfigPath, 'utf8', next); + return next(err); + } + + return next(null, config); + }); }, function (themeConfig, next) { try { themeConfig = JSON.parse(themeConfig); - next(null, themeConfig.screenshot ? path.join(themeDir, themeConfig.screenshot) : defaultScreenshotPath); } catch (e) { - next(e); + return next(e); } + + next(null, themeConfig.screenshot ? path.join(themeDir, themeConfig.screenshot) : defaultScreenshotPath); }, function (_screenshotPath, next) { screenshotPath = _screenshotPath; diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js index db403732ec..095b48e19d 100644 --- a/src/meta/dependencies.js +++ b/src/meta/dependencies.js @@ -34,13 +34,15 @@ Dependencies.check = function (callback) { }); }; +var pluginNamePattern = /^(@.*?\/)?nodebb-(theme|plugin|widget|rewards)-.*$/; + Dependencies.checkModule = function (moduleName, callback) { fs.readFile(path.join(__dirname, '../../node_modules/', moduleName, 'package.json'), { encoding: 'utf-8', }, function (err, pkgData) { if (err) { // If a bundled plugin/theme is not present, skip the dep check (#3384) - if (err.code === 'ENOENT' && (moduleName === 'nodebb-rewards-essentials' || moduleName.startsWith('nodebb-plugin') || moduleName.startsWith('nodebb-theme'))) { + if (err.code === 'ENOENT' && pluginNamePattern.test(moduleName)) { winston.warn('[meta/dependencies] Bundled plugin ' + moduleName + ' not found, skipping dependency check.'); return callback(null, true); } diff --git a/src/meta/templates.js b/src/meta/templates.js index b2d14801ae..1ccf14c8e3 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -45,9 +45,11 @@ function processImports(paths, templatePath, source, callback) { } Templates.processImports = processImports; +var themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/; + function getTemplateDirs(activePlugins, callback) { var pluginTemplates = activePlugins.map(function (id) { - if (id.startsWith('nodebb-theme-')) { + if (themeNamePattern.test(id)) { return nconf.get('theme_templates_path'); } if (!plugins.pluginsData[id]) { diff --git a/src/meta/themes.js b/src/meta/themes.js index e6eeb7011d..0f22993c3f 100644 --- a/src/meta/themes.js +++ b/src/meta/themes.js @@ -14,6 +14,8 @@ var events = require('../events'); var Themes = module.exports; +var themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/; + Themes.get = function (callback) { var themePath = nconf.get('themes_path'); if (typeof themePath !== 'string') { @@ -24,9 +26,13 @@ Themes.get = function (callback) { function (next) { fs.readdir(themePath, next); }, - function (files, next) { - async.filter(files, function (file, next) { - fs.stat(path.join(themePath, file), function (err, fileStat) { + function (dirs, next) { + async.map(dirs.filter(function (dir) { + return themeNamePattern.test(dir) || dir.startsWith('@'); + }), function (dir, next) { + var dirpath = path.join(themePath, dir); + + fs.stat(dirpath, function (err, stat) { if (err) { if (err.code === 'ENOENT') { return next(null, false); @@ -34,11 +40,54 @@ Themes.get = function (callback) { return next(err); } - next(null, (fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-')); + if (!stat.isDirectory()) { + return next(null, null); + } + + if (!dir.startsWith('@')) { + return next(null, dir); + } + + fs.readdir(dirpath, function (err, themes) { + if (err) { + return next(err); + } + + async.filter(themes.filter(function (theme) { + return themeNamePattern.test(theme); + }), function (theme, next) { + fs.stat(path.join(dirpath, theme), function (err, stat) { + if (err) { + if (err.code === 'ENOENT') { + return next(null, false); + } + return next(err); + } + + next(null, stat.isDirectory()); + }); + }, function (err, themes) { + if (err) { + return next(err); + } + + next(null, themes.map(function (theme) { + return dir + '/' + theme; + })); + }); + }); }); }, next); }, function (themes, next) { + themes = themes.reduce(function (prev, theme) { + if (!theme) { + return prev; + } + + return prev.concat(theme); + }, []); + async.map(themes, function (theme, next) { var config = path.join(themePath, theme, 'theme.json'); @@ -55,9 +104,9 @@ Themes.get = function (callback) { // Minor adjustments for API output configObj.type = 'local'; if (configObj.screenshot) { - configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + configObj.id; + configObj.screenshot_url = 'css/previews/' + encodeURIComponent(configObj.id); } else { - configObj.screenshot_url = nconf.get('relative_path') + '/assets/images/themes/default.png'; + configObj.screenshot_url = 'assets/images/themes/default.png'; } next(null, configObj); } catch (err) { @@ -150,14 +199,14 @@ Themes.setupPaths = function (callback) { function (data, next) { var themeId = data.currentThemeId || 'nodebb-theme-persona'; - var themeObj = data.themesData.filter(function (themeObj) { - return themeObj.id === themeId; - })[0]; - if (process.env.NODE_ENV === 'development') { winston.info('[themes] Using theme ' + themeId); } + var themeObj = data.themesData.find(function (themeObj) { + return themeObj.id === themeId; + }); + if (!themeObj) { return callback(new Error('[[error:theme-not-found]]')); } diff --git a/src/plugins.js b/src/plugins.js index a1193125e8..a4d28944fe 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -138,6 +138,8 @@ Plugins.reloadRoutes = function (callback) { }); }; +var themeNamePattern = /^(@.*?\/)?nodebb-theme-.*$/; + // DEPRECATED: remove in v1.8.0 Plugins.getTemplates = function (callback) { var templates = {}; @@ -151,7 +153,7 @@ Plugins.getTemplates = function (callback) { } async.eachSeries(plugins, function (plugin, next) { - if (plugin.templates || plugin.id.startsWith('nodebb-theme-')) { + if (plugin.templates || themeNamePattern.test(plugin.id)) { winston.verbose('[plugins] Loading templates (' + plugin.id + ')'); var templatesPath = path.join(__dirname, '../node_modules', plugin.id, plugin.templates || 'templates'); file.walk(templatesPath, function (err, pluginTemplates) { @@ -261,7 +263,7 @@ Plugins.normalise = function (apiReturn, callback) { pluginMap[plugin.id].description = plugin.description; pluginMap[plugin.id].url = pluginMap[plugin.id].url || plugin.url; pluginMap[plugin.id].installed = true; - pluginMap[plugin.id].isTheme = !!plugin.id.match('nodebb-theme-'); + pluginMap[plugin.id].isTheme = themeNamePattern.test(plugin.id); pluginMap[plugin.id].error = plugin.error || false; pluginMap[plugin.id].active = plugin.active; pluginMap[plugin.id].version = plugin.version; diff --git a/src/plugins/load.js b/src/plugins/load.js index 8745a833a0..1f36b4eb55 100644 --- a/src/plugins/load.js +++ b/src/plugins/load.js @@ -122,13 +122,16 @@ module.exports = function (Plugins) { ], callback); }; + var themeNamePattern = /(@.*?\/)?nodebb-theme-.*$/; + Plugins.loadPlugin = function (pluginPath, callback) { Plugins.data.loadPluginInfo(pluginPath, function (err, pluginData) { if (err) { if (err.message === '[[error:parse-error]]') { return callback(); } - return callback(pluginPath.match('nodebb-theme') ? null : err); + + return callback(themeNamePattern.test(pluginPath) ? null : err); } checkVersion(pluginData); diff --git a/src/views/admin/partials/theme_list.tpl b/src/views/admin/partials/theme_list.tpl index 26a226b803..aede3d71d0 100644 --- a/src/views/admin/partials/theme_list.tpl +++ b/src/views/admin/partials/theme_list.tpl @@ -1,7 +1,7 @@
data-css="{themes.css}">
-
+

{themes.name}