Unwrap meta modules

This commit is contained in:
Peter Jaszkowiak
2017-05-29 12:14:55 -06:00
parent aefdc4b24b
commit b60dbe7d1e
11 changed files with 1161 additions and 1173 deletions

View File

@@ -12,16 +12,16 @@ var Meta = module.exports;
Meta.reloadRequired = false; Meta.reloadRequired = false;
require('./meta/configs')(Meta); Meta.configs = require('./meta/configs');
require('./meta/themes')(Meta); Meta.themes = require('./meta/themes');
require('./meta/js')(Meta); Meta.js = require('./meta/js');
require('./meta/css')(Meta); Meta.css = require('./meta/css');
require('./meta/sounds')(Meta); Meta.sounds = require('./meta/sounds');
require('./meta/settings')(Meta); Meta.settings = require('./meta/settings');
require('./meta/logs')(Meta); Meta.logs = require('./meta/logs');
require('./meta/errors')(Meta); Meta.errors = require('./meta/errors');
require('./meta/tags')(Meta); Meta.tags = require('./meta/tags');
require('./meta/dependencies')(Meta); Meta.dependencies = require('./meta/dependencies');
Meta.templates = require('./meta/templates'); Meta.templates = require('./meta/templates');
Meta.blacklist = require('./meta/blacklist'); Meta.blacklist = require('./meta/blacklist');
Meta.languages = require('./meta/languages'); Meta.languages = require('./meta/languages');

View File

@@ -6,142 +6,142 @@ var nconf = require('nconf');
var db = require('../database'); var db = require('../database');
var pubsub = require('../pubsub'); var pubsub = require('../pubsub');
var Meta = require('../meta');
var cacheBuster = require('./cacheBuster'); var cacheBuster = require('./cacheBuster');
module.exports = function (Meta) { var Configs = module.exports;
Meta.config = {};
Meta.configs = {};
Meta.configs.init = function (callback) { Meta.config = {};
delete Meta.config;
async.waterfall([ Configs.init = function (callback) {
function (next) { Meta.config = null;
Meta.configs.list(next);
},
function (config, next) {
cacheBuster.read(function (err, buster) {
if (err) {
return next(err);
}
config['cache-buster'] = 'v=' + (buster || Date.now()); async.waterfall([
function (next) {
Meta.config = config; Configs.list(next);
next(); },
}); function (config, next) {
}, cacheBuster.read(function (err, buster) {
], callback); if (err) {
}; return next(err);
Meta.configs.list = function (callback) {
db.getObject('config', function (err, config) {
config = config || {};
config.version = nconf.get('version');
config.registry = nconf.get('registry');
callback(err, config);
});
};
Meta.configs.get = function (field, callback) {
db.getObjectField('config', field, callback);
};
Meta.configs.getFields = function (fields, callback) {
db.getObjectFields('config', fields, callback);
};
Meta.configs.set = function (field, value, callback) {
callback = callback || function () {};
if (!field) {
return callback(new Error('[[error:invalid-data]]'));
}
var data = {};
data[field] = value;
Meta.configs.setMultiple(data, callback);
};
Meta.configs.setMultiple = function (data, callback) {
async.waterfall([
function (next) {
processConfig(data, next);
},
function (next) {
db.setObject('config', data, next);
},
function (next) {
updateConfig(data);
setImmediate(next);
},
], callback);
};
function processConfig(data, callback) {
if (data.customCSS) {
return saveRenderedCss(data, callback);
}
setImmediate(callback);
}
function saveRenderedCss(data, callback) {
var less = require('less');
async.waterfall([
function (next) {
less.render(data.customCSS, {
compress: true,
}, next);
},
function (lessObject, next) {
data.renderedCustomCSS = lessObject.css;
setImmediate(next);
},
], callback);
}
function updateConfig(config) {
updateLocalConfig(config);
pubsub.publish('config:update', config);
}
function updateLocalConfig(config) {
for (var field in config) {
if (config.hasOwnProperty(field)) {
Meta.config[field] = config[field];
}
}
}
pubsub.on('config:update', function onConfigReceived(config) {
if (typeof config === 'object' && Meta.config) {
updateLocalConfig(config);
}
});
Meta.configs.setOnEmpty = function (values, callback) {
async.waterfall([
function (next) {
db.getObject('config', next);
},
function (data, next) {
data = data || {};
var empty = {};
Object.keys(values).forEach(function (key) {
if (!data.hasOwnProperty(key)) {
empty[key] = values[key];
}
});
if (Object.keys(empty).length) {
db.setObject('config', empty, next);
} else {
setImmediate(next);
} }
},
], callback);
};
Meta.configs.remove = function (field, callback) { config['cache-buster'] = 'v=' + (buster || Date.now());
db.deleteObjectField('config', field, callback);
}; Meta.config = config;
next();
});
},
], callback);
};
Configs.list = function (callback) {
db.getObject('config', function (err, config) {
config = config || {};
config.version = nconf.get('version');
config.registry = nconf.get('registry');
callback(err, config);
});
};
Configs.get = function (field, callback) {
db.getObjectField('config', field, callback);
};
Configs.getFields = function (fields, callback) {
db.getObjectFields('config', fields, callback);
};
Configs.set = function (field, value, callback) {
callback = callback || function () {};
if (!field) {
return callback(new Error('[[error:invalid-data]]'));
}
var data = {};
data[field] = value;
Configs.setMultiple(data, callback);
};
Configs.setMultiple = function (data, callback) {
async.waterfall([
function (next) {
processConfig(data, next);
},
function (next) {
db.setObject('config', data, next);
},
function (next) {
updateConfig(data);
setImmediate(next);
},
], callback);
};
function processConfig(data, callback) {
if (data.customCSS) {
return saveRenderedCss(data, callback);
}
setImmediate(callback);
}
function saveRenderedCss(data, callback) {
var less = require('less');
async.waterfall([
function (next) {
less.render(data.customCSS, {
compress: true,
}, next);
},
function (lessObject, next) {
data.renderedCustomCSS = lessObject.css;
setImmediate(next);
},
], callback);
}
function updateConfig(config) {
updateLocalConfig(config);
pubsub.publish('config:update', config);
}
function updateLocalConfig(config) {
for (var field in config) {
if (config.hasOwnProperty(field)) {
Meta.config[field] = config[field];
}
}
}
pubsub.on('config:update', function onConfigReceived(config) {
if (typeof config === 'object' && Meta.config) {
updateLocalConfig(config);
}
});
Configs.setOnEmpty = function (values, callback) {
async.waterfall([
function (next) {
db.getObject('config', next);
},
function (data, next) {
data = data || {};
var empty = {};
Object.keys(values).forEach(function (key) {
if (!data.hasOwnProperty(key)) {
empty[key] = values[key];
}
});
if (Object.keys(empty).length) {
db.setObject('config', empty, next);
} else {
setImmediate(next);
}
},
], callback);
};
Configs.remove = function (field, callback) {
db.deleteObjectField('config', field, callback);
}; };

View File

@@ -11,158 +11,156 @@ var db = require('../database');
var file = require('../file'); var file = require('../file');
var minifier = require('./minifier'); var minifier = require('./minifier');
module.exports = function (Meta) { var CSS = module.exports;
Meta.css = {};
var buildImports = { var buildImports = {
client: function (source) { client: function (source) {
return '@import "./theme";\n' + source + '\n' + [ return '@import "./theme";\n' + source + '\n' + [
'@import "font-awesome";', '@import "font-awesome";',
'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";', '@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";', '@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
'@import (inline) "../public/vendor/colorpicker/colorpicker.css";', '@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
'@import (inline) "../node_modules/cropperjs/dist/cropper.css";', '@import (inline) "../node_modules/cropperjs/dist/cropper.css";',
'@import "../../public/less/flags.less";', '@import "../../public/less/flags.less";',
'@import "../../public/less/blacklist.less";', '@import "../../public/less/blacklist.less";',
'@import "../../public/less/generics.less";', '@import "../../public/less/generics.less";',
'@import "../../public/less/mixins.less";', '@import "../../public/less/mixins.less";',
'@import "../../public/less/global.less";', '@import "../../public/less/global.less";',
].map(function (str) { ].map(function (str) {
return str.replace(/\//g, path.sep); return str.replace(/\//g, path.sep);
}).join('\n'); }).join('\n');
}, },
admin: function (source) { admin: function (source) {
return source + '\n' + [ return source + '\n' + [
'@import "font-awesome";', '@import "font-awesome";',
'@import "../public/less/admin/admin";', '@import "../public/less/admin/admin";',
'@import "../public/less/generics.less";', '@import "../public/less/generics.less";',
'@import (inline) "../public/vendor/colorpicker/colorpicker.css";', '@import (inline) "../public/vendor/colorpicker/colorpicker.css";',
'@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";', '@import (inline) "../public/vendor/jquery/css/smoothness/jquery-ui.css";',
'@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";', '@import (inline) "../public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.css";',
'@import (inline) "../public/vendor/mdl/material.css";', '@import (inline) "../public/vendor/mdl/material.css";',
].map(function (str) { ].map(function (str) {
return str.replace(/\//g, path.sep); return str.replace(/\//g, path.sep);
}).join('\n'); }).join('\n');
}, },
}; };
function filterMissingFiles(filepaths, callback) { function filterMissingFiles(filepaths, callback) {
async.filter(filepaths, function (filepath, next) { async.filter(filepaths, function (filepath, next) {
file.exists(path.join(__dirname, '../../node_modules', filepath), function (err, exists) { file.exists(path.join(__dirname, '../../node_modules', filepath), function (err, exists) {
if (!exists) { if (!exists) {
winston.warn('[meta/css] File not found! ' + filepath); winston.warn('[meta/css] File not found! ' + filepath);
} }
next(err, exists); next(err, exists);
}); });
}, callback); }, callback);
} }
function getImports(files, prefix, extension, callback) { function getImports(files, prefix, extension, callback) {
var pluginDirectories = []; var pluginDirectories = [];
var source = ''; var source = '';
files.forEach(function (styleFile) { files.forEach(function (styleFile) {
if (styleFile.endsWith(extension)) { if (styleFile.endsWith(extension)) {
source += prefix + path.sep + styleFile + '";'; source += prefix + path.sep + styleFile + '";';
} else { } else {
pluginDirectories.push(styleFile); pluginDirectories.push(styleFile);
} }
}); });
async.each(pluginDirectories, function (directory, next) { async.each(pluginDirectories, function (directory, next) {
file.walk(directory, function (err, styleFiles) { file.walk(directory, function (err, styleFiles) {
if (err) { if (err) {
return next(err); return next(err);
} }
styleFiles.forEach(function (styleFile) { styleFiles.forEach(function (styleFile) {
source += prefix + path.sep + styleFile + '";'; source += prefix + path.sep + styleFile + '";';
}); });
next(); next();
}); });
}, function (err) { }, function (err) {
callback(err, source); callback(err, source);
}); });
} }
function getBundleMetadata(target, callback) { function getBundleMetadata(target, callback) {
var paths = [ var paths = [
path.join(__dirname, '../../node_modules'), path.join(__dirname, '../../node_modules'),
path.join(__dirname, '../../public/vendor/fontawesome/less'), path.join(__dirname, '../../public/vendor/fontawesome/less'),
]; ];
async.waterfall([ async.waterfall([
function (next) { function (next) {
if (target !== 'client') { if (target !== 'client') {
return next(null, null); return next(null, null);
} }
db.getObjectFields('config', ['theme:type', 'theme:id'], next); db.getObjectFields('config', ['theme:type', 'theme:id'], next);
}, },
function (themeData, next) { function (themeData, next) {
if (target === 'client') { if (target === 'client') {
var themeId = (themeData['theme:id'] || 'nodebb-theme-persona'); var themeId = (themeData['theme:id'] || 'nodebb-theme-persona');
var baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')); var baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla'));
paths.unshift(baseThemePath); paths.unshift(baseThemePath);
} }
async.parallel({ async.parallel({
less: function (cb) { less: function (cb) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
filterMissingFiles(plugins.lessFiles, next); filterMissingFiles(plugins.lessFiles, next);
}, },
function (lessFiles, next) { function (lessFiles, next) {
getImports(lessFiles, '\n@import ".', '.less', next); getImports(lessFiles, '\n@import ".', '.less', next);
}, },
], cb); ], cb);
}, },
css: function (cb) { css: function (cb) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
filterMissingFiles(plugins.cssFiles, next); filterMissingFiles(plugins.cssFiles, next);
}, },
function (cssFiles, next) { function (cssFiles, next) {
getImports(cssFiles, '\n@import (inline) ".', '.css', next); getImports(cssFiles, '\n@import (inline) ".', '.css', next);
}, },
], cb); ], cb);
}, },
}, next); }, next);
}, },
function (result, next) { function (result, next) {
var cssImports = result.css; var cssImports = result.css;
var lessImports = result.less; var lessImports = result.less;
var imports = cssImports + '\n' + lessImports; var imports = cssImports + '\n' + lessImports;
imports = buildImports[target](imports); imports = buildImports[target](imports);
next(null, imports); next(null, imports);
}, },
], function (err, imports) { ], function (err, imports) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
callback(null, { paths: paths, imports: imports }); callback(null, { paths: paths, imports: imports });
}); });
} }
Meta.css.buildBundle = function (target, fork, callback) { CSS.buildBundle = function (target, fork, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
getBundleMetadata(target, next); getBundleMetadata(target, next);
}, },
function (data, next) { function (data, next) {
var minify = global.env !== 'development'; var minify = global.env !== 'development';
minifier.css.bundle(data.imports, data.paths, minify, fork, next); minifier.css.bundle(data.imports, data.paths, minify, fork, next);
}, },
function (bundle, next) { function (bundle, next) {
var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css'; var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css';
fs.writeFile(path.join(__dirname, '../../build/public', filename), bundle.code, next); fs.writeFile(path.join(__dirname, '../../build/public', filename), bundle.code, next);
}, },
], callback); ], callback);
};
}; };

View File

@@ -9,73 +9,72 @@ require('colors');
var pkg = require('../../package.json'); var pkg = require('../../package.json');
module.exports = function (Meta) { var Dependencies = module.exports;
Meta.dependencies = {};
var depsMissing = false;
var depsOutdated = false;
Meta.dependencies.check = function (callback) { var depsMissing = false;
var modules = Object.keys(pkg.dependencies); var depsOutdated = false;
winston.verbose('Checking dependencies for outdated modules'); Dependencies.check = function (callback) {
var modules = Object.keys(pkg.dependencies);
async.each(modules, Meta.dependencies.checkModule, function (err) { winston.verbose('Checking dependencies for outdated modules');
if (err) {
return callback(err);
}
if (depsMissing) { async.each(modules, Dependencies.checkModule, function (err) {
callback(new Error('dependencies-missing')); if (err) {
} else if (depsOutdated) { return callback(err);
callback(global.env !== 'development' ? new Error('dependencies-out-of-date') : null);
} else {
callback(null);
}
});
};
Meta.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'))) {
winston.warn('[meta/dependencies] Bundled plugin ' + moduleName + ' not found, skipping dependency check.');
return callback(null, true);
}
return callback(err);
}
pkgData = Meta.dependencies.parseModuleData(moduleName, pkgData);
var satisfies = Meta.dependencies.doesSatisfy(pkgData, pkg.dependencies[moduleName]);
callback(null, satisfies);
});
};
Meta.dependencies.parseModuleData = function (moduleName, pkgData) {
try {
pkgData = JSON.parse(pkgData);
} catch (e) {
winston.warn('[' + 'missing'.red + '] ' + moduleName.bold + ' is a required dependency but could not be found\n');
depsMissing = true;
return null;
} }
return pkgData;
};
Meta.dependencies.doesSatisfy = function (moduleData, packageJSONVersion) { if (depsMissing) {
if (!moduleData) { callback(new Error('dependencies-missing'));
return false; } else if (depsOutdated) {
callback(global.env !== 'development' ? new Error('dependencies-out-of-date') : null);
} else {
callback(null);
} }
var versionOk = !semver.validRange(packageJSONVersion) || semver.satisfies(moduleData.version, packageJSONVersion); });
var githubRepo = moduleData._resolved && moduleData._resolved.indexOf('//github.com') !== -1; };
var satisfies = versionOk || githubRepo;
if (!satisfies) { Dependencies.checkModule = function (moduleName, callback) {
winston.warn('[' + 'outdated'.yellow + '] ' + moduleData.name.bold + ' installed v' + moduleData.version + ', package.json requires ' + packageJSONVersion + '\n'); fs.readFile(path.join(__dirname, '../../node_modules/', moduleName, 'package.json'), {
depsOutdated = true; encoding: 'utf-8',
} }, function (err, pkgData) {
return satisfies; 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'))) {
winston.warn('[meta/dependencies] Bundled plugin ' + moduleName + ' not found, skipping dependency check.');
return callback(null, true);
}
return callback(err);
}
pkgData = Dependencies.parseModuleData(moduleName, pkgData);
var satisfies = Dependencies.doesSatisfy(pkgData, pkg.dependencies[moduleName]);
callback(null, satisfies);
});
};
Dependencies.parseModuleData = function (moduleName, pkgData) {
try {
pkgData = JSON.parse(pkgData);
} catch (e) {
winston.warn('[' + 'missing'.red + '] ' + moduleName.bold + ' is a required dependency but could not be found\n');
depsMissing = true;
return null;
}
return pkgData;
};
Dependencies.doesSatisfy = function (moduleData, packageJSONVersion) {
if (!moduleData) {
return false;
}
var versionOk = !semver.validRange(packageJSONVersion) || semver.satisfies(moduleData.version, packageJSONVersion);
var githubRepo = moduleData._resolved && moduleData._resolved.indexOf('//github.com') !== -1;
var satisfies = versionOk || githubRepo;
if (!satisfies) {
winston.warn('[' + 'outdated'.yellow + '] ' + moduleData.name.bold + ' installed v' + moduleData.version + ', package.json requires ' + packageJSONVersion + '\n');
depsOutdated = true;
}
return satisfies;
}; };

View File

@@ -8,61 +8,59 @@ var cronJob = require('cron').CronJob;
var db = require('../database'); var db = require('../database');
var analytics = require('../analytics'); var analytics = require('../analytics');
module.exports = function (Meta) { var Errors = module.exports;
Meta.errors = {};
var counters = {}; var counters = {};
new cronJob('0 * * * * *', function () { new cronJob('0 * * * * *', function () {
Meta.errors.writeData(); Errors.writeData();
}, null, true); }, null, true);
Meta.errors.writeData = function () { Errors.writeData = function () {
var dbQueue = []; var dbQueue = [];
if (Object.keys(counters).length > 0) { if (Object.keys(counters).length > 0) {
for (var key in counters) { for (var key in counters) {
if (counters.hasOwnProperty(key)) { if (counters.hasOwnProperty(key)) {
dbQueue.push(async.apply(db.sortedSetIncrBy, 'errors:404', counters[key], key)); dbQueue.push(async.apply(db.sortedSetIncrBy, 'errors:404', counters[key], key));
}
} }
counters = {};
async.series(dbQueue, function (err) {
if (err) {
winston.error(err);
}
});
} }
}; counters = {};
async.series(dbQueue, function (err) {
Meta.errors.log404 = function (route, callback) { if (err) {
callback = callback || function () {}; winston.error(err);
if (!route) { }
return setImmediate(callback); });
} }
route = route.replace(/\/$/, ''); // remove trailing slashes };
analytics.increment('errors:404');
counters[route] = counters[route] || 0; Errors.log404 = function (route, callback) {
counters[route] += 1; callback = callback || function () {};
setImmediate(callback); if (!route) {
}; return setImmediate(callback);
}
Meta.errors.get = function (escape, callback) { route = route.replace(/\/$/, ''); // remove trailing slashes
async.waterfall([ analytics.increment('errors:404');
function (next) { counters[route] = counters[route] || 0;
db.getSortedSetRevRangeWithScores('errors:404', 0, 199, next); counters[route] += 1;
}, setImmediate(callback);
function (data, next) { };
data = data.map(function (nfObject) {
nfObject.value = escape ? validator.escape(String(nfObject.value || '')) : nfObject.value; Errors.get = function (escape, callback) {
return nfObject; async.waterfall([
}); function (next) {
db.getSortedSetRevRangeWithScores('errors:404', 0, 199, next);
next(null, data); },
}, function (data, next) {
], callback); data = data.map(function (nfObject) {
}; nfObject.value = escape ? validator.escape(String(nfObject.value || '')) : nfObject.value;
return nfObject;
Meta.errors.clear = function (callback) { });
db.delete('errors:404', callback);
}; next(null, data);
},
], callback);
};
Errors.clear = function (callback) {
db.delete('errors:404', callback);
}; };

View File

@@ -10,341 +10,339 @@ var file = require('../file');
var plugins = require('../plugins'); var plugins = require('../plugins');
var minifier = require('./minifier'); var minifier = require('./minifier');
module.exports = function (Meta) { var JS = module.exports;
Meta.js = {};
Meta.js.scripts = { JS.scripts = {
base: [ base: [
'node_modules/jquery/dist/jquery.js', 'node_modules/jquery/dist/jquery.js',
'node_modules/socket.io-client/dist/socket.io.js', 'node_modules/socket.io-client/dist/socket.io.js',
'public/vendor/jquery/timeago/jquery.timeago.js', 'public/vendor/jquery/timeago/jquery.timeago.js',
'public/vendor/jquery/js/jquery.form.min.js', 'public/vendor/jquery/js/jquery.form.min.js',
'public/vendor/visibility/visibility.min.js', 'public/vendor/visibility/visibility.min.js',
'node_modules/bootstrap/dist/js/bootstrap.js', 'node_modules/bootstrap/dist/js/bootstrap.js',
'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js', 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js',
'public/vendor/jquery/textcomplete/jquery.textcomplete.js', 'public/vendor/jquery/textcomplete/jquery.textcomplete.js',
'public/vendor/requirejs/require.js', 'public/vendor/requirejs/require.js',
'public/src/require-config.js', 'public/src/require-config.js',
'public/vendor/bootbox/bootbox.js', 'public/vendor/bootbox/bootbox.js',
'public/vendor/bootbox/wrapper.js', 'public/vendor/bootbox/wrapper.js',
'public/vendor/tinycon/tinycon.js', 'public/vendor/tinycon/tinycon.js',
'public/vendor/xregexp/xregexp.js', 'public/vendor/xregexp/xregexp.js',
'public/vendor/xregexp/unicode/unicode-base.js', 'public/vendor/xregexp/unicode/unicode-base.js',
'node_modules/templates.js/lib/templates.js', 'node_modules/templates.js/lib/templates.js',
'public/src/utils.js', 'public/src/utils.js',
'public/src/sockets.js', 'public/src/sockets.js',
'public/src/app.js', 'public/src/app.js',
'public/src/ajaxify.js', 'public/src/ajaxify.js',
'public/src/overrides.js', 'public/src/overrides.js',
'public/src/widgets.js', 'public/src/widgets.js',
'node_modules/promise-polyfill/promise.js', 'node_modules/promise-polyfill/promise.js',
], ],
// files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load // files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load
rjs: [ rjs: [
'public/src/client/footer.js', 'public/src/client/footer.js',
'public/src/client/chats.js', 'public/src/client/chats.js',
'public/src/client/infinitescroll.js', 'public/src/client/infinitescroll.js',
'public/src/client/pagination.js', 'public/src/client/pagination.js',
'public/src/client/recent.js', 'public/src/client/recent.js',
'public/src/client/unread.js', 'public/src/client/unread.js',
'public/src/client/topic.js', 'public/src/client/topic.js',
'public/src/client/topic/events.js', 'public/src/client/topic/events.js',
'public/src/client/topic/fork.js', 'public/src/client/topic/fork.js',
'public/src/client/topic/move.js', 'public/src/client/topic/move.js',
'public/src/client/topic/posts.js', 'public/src/client/topic/posts.js',
'public/src/client/topic/images.js', 'public/src/client/topic/images.js',
'public/src/client/topic/postTools.js', 'public/src/client/topic/postTools.js',
'public/src/client/topic/threadTools.js', 'public/src/client/topic/threadTools.js',
'public/src/client/categories.js', 'public/src/client/categories.js',
'public/src/client/category.js', 'public/src/client/category.js',
'public/src/client/category/tools.js', 'public/src/client/category/tools.js',
'public/src/modules/translator.js', 'public/src/modules/translator.js',
'public/src/modules/notifications.js', 'public/src/modules/notifications.js',
'public/src/modules/chat.js', 'public/src/modules/chat.js',
'public/src/modules/components.js', 'public/src/modules/components.js',
'public/src/modules/sort.js', 'public/src/modules/sort.js',
'public/src/modules/navigator.js', 'public/src/modules/navigator.js',
'public/src/modules/topicSelect.js', 'public/src/modules/topicSelect.js',
'public/src/modules/categorySelector.js', 'public/src/modules/categorySelector.js',
'public/src/modules/share.js', 'public/src/modules/share.js',
'public/src/modules/search.js', 'public/src/modules/search.js',
'public/src/modules/alerts.js', 'public/src/modules/alerts.js',
'public/src/modules/taskbar.js', 'public/src/modules/taskbar.js',
'public/src/modules/helpers.js', 'public/src/modules/helpers.js',
'public/src/modules/string.js', 'public/src/modules/string.js',
'public/src/modules/flags.js', 'public/src/modules/flags.js',
'public/src/modules/storage.js', 'public/src/modules/storage.js',
], ],
// modules listed below are built (/src/modules) so they can be defined anonymously // modules listed below are built (/src/modules) so they can be defined anonymously
modules: { modules: {
'Chart.js': 'node_modules/chart.js/dist/Chart.min.js', 'Chart.js': 'node_modules/chart.js/dist/Chart.min.js',
'mousetrap.js': 'node_modules/mousetrap/mousetrap.min.js', 'mousetrap.js': 'node_modules/mousetrap/mousetrap.min.js',
'cropper.js': 'node_modules/cropperjs/dist/cropper.min.js', 'cropper.js': 'node_modules/cropperjs/dist/cropper.min.js',
'jqueryui.js': 'public/vendor/jquery/js/jquery-ui.js', 'jqueryui.js': 'public/vendor/jquery/js/jquery-ui.js',
'zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js', 'zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js',
ace: 'node_modules/ace-builds/src-min', ace: 'node_modules/ace-builds/src-min',
}, },
}; };
var basePath = path.resolve(__dirname, '../..'); var basePath = path.resolve(__dirname, '../..');
function minifyModules(modules, fork, callback) { function minifyModules(modules, fork, callback) {
var moduleDirs = modules.reduce(function (prev, mod) { var moduleDirs = modules.reduce(function (prev, mod) {
var dir = path.resolve(path.dirname(mod.destPath)); var dir = path.resolve(path.dirname(mod.destPath));
if (prev.indexOf(dir) === -1) { if (prev.indexOf(dir) === -1) {
prev.push(dir); prev.push(dir);
}
return prev;
}, []);
async.eachLimit(moduleDirs, 1000, mkdirp, function (err) {
if (err) {
return callback(err);
}
var filtered = modules.reduce(function (prev, mod) {
if (mod.srcPath.endsWith('.min.js') || path.dirname(mod.srcPath).endsWith('min')) {
prev.skip.push(mod);
} else {
prev.minify.push(mod);
} }
return prev; return prev;
}, []); }, { minify: [], skip: [] });
async.eachLimit(moduleDirs, 1000, mkdirp, function (err) { async.parallel([
if (err) { function (cb) {
return callback(err); minifier.js.minifyBatch(filtered.minify, fork, cb);
}
var filtered = modules.reduce(function (prev, mod) {
if (mod.srcPath.endsWith('.min.js') || path.dirname(mod.srcPath).endsWith('min')) {
prev.skip.push(mod);
} else {
prev.minify.push(mod);
}
return prev;
}, { minify: [], skip: [] });
async.parallel([
function (cb) {
minifier.js.minifyBatch(filtered.minify, fork, cb);
},
function (cb) {
async.eachLimit(filtered.skip, 500, function (mod, next) {
file.link(mod.srcPath, mod.destPath, next);
}, cb);
},
], callback);
});
}
function linkModules(callback) {
var modules = Meta.js.scripts.modules;
async.eachLimit(Object.keys(modules), 1000, function (relPath, next) {
var srcPath = path.join(__dirname, '../../', modules[relPath]);
var destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
async.parallel({
dir: function (cb) {
mkdirp(path.dirname(destPath), function (err) {
cb(err);
});
},
stats: function (cb) {
fs.stat(srcPath, cb);
},
}, function (err, res) {
if (err) {
return next(err);
}
if (res.stats.isDirectory()) {
return file.linkDirs(srcPath, destPath, next);
}
if (process.platform === 'win32') {
fs.readFile(srcPath, function (err, file) {
if (err) {
return next(err);
}
fs.writeFile(destPath, file, next);
});
} else {
file.link(srcPath, destPath, next);
}
});
}, callback);
}
var moduleDirs = ['modules', 'admin', 'client'];
function getModuleList(callback) {
var modules = Object.keys(Meta.js.scripts.modules).map(function (relPath) {
return {
srcPath: path.join(__dirname, '../../', Meta.js.scripts.modules[relPath]),
destPath: path.join(__dirname, '../../build/public/src/modules', relPath),
};
});
var coreDirs = moduleDirs.map(function (dir) {
return {
srcPath: path.join(__dirname, '../../public/src', dir),
destPath: path.join(__dirname, '../../build/public/src', dir),
};
});
modules = modules.concat(coreDirs);
var moduleFiles = [];
async.eachLimit(modules, 1000, function (module, next) {
var srcPath = module.srcPath;
var destPath = module.destPath;
fs.stat(srcPath, function (err, stats) {
if (err) {
return next(err);
}
if (!stats.isDirectory()) {
moduleFiles.push(module);
return next();
}
file.walk(srcPath, function (err, files) {
if (err) {
return next(err);
}
var mods = files.filter(function (filePath) {
return path.extname(filePath) === '.js';
}).map(function (filePath) {
return {
srcPath: path.normalize(filePath),
destPath: path.join(destPath, path.relative(srcPath, filePath)),
};
});
moduleFiles = moduleFiles.concat(mods).map(function (mod) {
mod.filename = path.relative(basePath, mod.srcPath).replace(/\\/g, '/');
return mod;
});
next();
});
});
}, function (err) {
callback(err, moduleFiles);
});
}
function clearModules(callback) {
var builtPaths = moduleDirs.map(function (p) {
return path.join(__dirname, '../../build/public/src', p);
});
async.each(builtPaths, function (builtPath, next) {
rimraf(builtPath, next);
}, function (err) {
callback(err);
});
}
Meta.js.buildModules = function (fork, callback) {
async.waterfall([
clearModules,
function (next) {
if (global.env === 'development') {
return linkModules(callback);
}
getModuleList(next);
}, },
function (modules, next) { function (cb) {
minifyModules(modules, fork, next); async.eachLimit(filtered.skip, 500, function (mod, next) {
file.link(mod.srcPath, mod.destPath, next);
}, cb);
}, },
], callback); ], callback);
}; });
}
Meta.js.linkStatics = function (callback) { function linkModules(callback) {
rimraf(path.join(__dirname, '../../build/public/plugins'), function (err) { var modules = JS.scripts.modules;
async.eachLimit(Object.keys(modules), 1000, function (relPath, next) {
var srcPath = path.join(__dirname, '../../', modules[relPath]);
var destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
async.parallel({
dir: function (cb) {
mkdirp(path.dirname(destPath), function (err) {
cb(err);
});
},
stats: function (cb) {
fs.stat(srcPath, cb);
},
}, function (err, res) {
if (err) { if (err) {
return callback(err); return next(err);
}
if (res.stats.isDirectory()) {
return file.linkDirs(srcPath, destPath, next);
} }
async.eachLimit(Object.keys(plugins.staticDirs), 1000, function (mappedPath, next) {
var sourceDir = plugins.staticDirs[mappedPath];
var destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);
mkdirp(path.dirname(destDir), function (err) { if (process.platform === 'win32') {
fs.readFile(srcPath, function (err, file) {
if (err) { if (err) {
return next(err); return next(err);
} }
file.linkDirs(sourceDir, destDir, next); fs.writeFile(destPath, file, next);
}); });
}, callback); } else {
file.link(srcPath, destPath, next);
}
}); });
}; }, callback);
}
function getBundleScriptList(target, callback) { var moduleDirs = ['modules', 'admin', 'client'];
var pluginDirectories = [];
if (target === 'admin') { function getModuleList(callback) {
target = 'acp'; var modules = Object.keys(JS.scripts.modules).map(function (relPath) {
} return {
var pluginScripts = plugins[target + 'Scripts'].filter(function (path) { srcPath: path.join(__dirname, '../../', JS.scripts.modules[relPath]),
if (path.endsWith('.js')) { destPath: path.join(__dirname, '../../build/public/src/modules', relPath),
return true; };
});
var coreDirs = moduleDirs.map(function (dir) {
return {
srcPath: path.join(__dirname, '../../public/src', dir),
destPath: path.join(__dirname, '../../build/public/src', dir),
};
});
modules = modules.concat(coreDirs);
var moduleFiles = [];
async.eachLimit(modules, 1000, function (module, next) {
var srcPath = module.srcPath;
var destPath = module.destPath;
fs.stat(srcPath, function (err, stats) {
if (err) {
return next(err);
}
if (!stats.isDirectory()) {
moduleFiles.push(module);
return next();
} }
pluginDirectories.push(path); file.walk(srcPath, function (err, files) {
return false;
});
async.each(pluginDirectories, function (directory, next) {
file.walk(directory, function (err, scripts) {
if (err) { if (err) {
return next(err); return next(err);
} }
pluginScripts = pluginScripts.concat(scripts); var mods = files.filter(function (filePath) {
return path.extname(filePath) === '.js';
}).map(function (filePath) {
return {
srcPath: path.normalize(filePath),
destPath: path.join(destPath, path.relative(srcPath, filePath)),
};
});
moduleFiles = moduleFiles.concat(mods).map(function (mod) {
mod.filename = path.relative(basePath, mod.srcPath).replace(/\\/g, '/');
return mod;
});
next(); next();
}); });
}, function (err) {
if (err) {
return callback(err);
}
var scripts = Meta.js.scripts.base.concat(pluginScripts);
if (target === 'client' && global.env !== 'development') {
scripts = scripts.concat(Meta.js.scripts.rjs);
}
scripts = scripts.map(function (script) {
var srcPath = path.resolve(basePath, script).replace(/\\/g, '/');
return {
srcPath: srcPath,
filename: path.relative(basePath, srcPath).replace(/\\/g, '/'),
};
});
callback(null, scripts);
}); });
} }, function (err) {
callback(err, moduleFiles);
});
}
Meta.js.buildBundle = function (target, fork, callback) { function clearModules(callback) {
var fileNames = { var builtPaths = moduleDirs.map(function (p) {
client: 'nodebb.min.js', return path.join(__dirname, '../../build/public/src', p);
admin: 'acp.min.js', });
}; async.each(builtPaths, function (builtPath, next) {
rimraf(builtPath, next);
}, function (err) {
callback(err);
});
}
async.waterfall([ JS.buildModules = function (fork, callback) {
function (next) { async.waterfall([
getBundleScriptList(target, next); clearModules,
}, function (next) {
function (files, next) { if (global.env === 'development') {
var minify = global.env !== 'development'; return linkModules(callback);
var filePath = path.join(__dirname, '../../build/public', fileNames[target]); }
minifier.js.bundle({ getModuleList(next);
files: files, },
filename: fileNames[target], function (modules, next) {
destPath: filePath, minifyModules(modules, fork, next);
}, minify, fork, next); },
}, ], callback);
], callback); };
};
JS.linkStatics = function (callback) {
Meta.js.killMinifier = function () { rimraf(path.join(__dirname, '../../build/public/plugins'), function (err) {
minifier.killAll(); if (err) {
}; return callback(err);
}
async.eachLimit(Object.keys(plugins.staticDirs), 1000, function (mappedPath, next) {
var sourceDir = plugins.staticDirs[mappedPath];
var destDir = path.join(__dirname, '../../build/public/plugins', mappedPath);
mkdirp(path.dirname(destDir), function (err) {
if (err) {
return next(err);
}
file.linkDirs(sourceDir, destDir, next);
});
}, callback);
});
};
function getBundleScriptList(target, callback) {
var pluginDirectories = [];
if (target === 'admin') {
target = 'acp';
}
var pluginScripts = plugins[target + 'Scripts'].filter(function (path) {
if (path.endsWith('.js')) {
return true;
}
pluginDirectories.push(path);
return false;
});
async.each(pluginDirectories, function (directory, next) {
file.walk(directory, function (err, scripts) {
if (err) {
return next(err);
}
pluginScripts = pluginScripts.concat(scripts);
next();
});
}, function (err) {
if (err) {
return callback(err);
}
var scripts = JS.scripts.base.concat(pluginScripts);
if (target === 'client' && global.env !== 'development') {
scripts = scripts.concat(JS.scripts.rjs);
}
scripts = scripts.map(function (script) {
var srcPath = path.resolve(basePath, script).replace(/\\/g, '/');
return {
srcPath: srcPath,
filename: path.relative(basePath, srcPath).replace(/\\/g, '/'),
};
});
callback(null, scripts);
});
}
JS.buildBundle = function (target, fork, callback) {
var fileNames = {
client: 'nodebb.min.js',
admin: 'acp.min.js',
};
async.waterfall([
function (next) {
getBundleScriptList(target, next);
},
function (files, next) {
var minify = global.env !== 'development';
var filePath = path.join(__dirname, '../../build/public', fileNames[target]);
minifier.js.bundle({
files: files,
filename: fileNames[target],
destPath: filePath,
}, minify, fork, next);
},
], callback);
};
JS.killMinifier = function () {
minifier.killAll();
}; };

View File

@@ -3,18 +3,16 @@
var path = require('path'); var path = require('path');
var fs = require('fs'); var fs = require('fs');
module.exports = function (Meta) { var Logs = module.exports;
Meta.logs = {
path: path.join(__dirname, '..', '..', 'logs', 'output.log'),
};
Meta.logs.get = function (callback) { Logs.path = path.join(__dirname, '..', '..', 'logs', 'output.log');
fs.readFile(Meta.logs.path, {
encoding: 'utf-8',
}, callback);
};
Meta.logs.clear = function (callback) { Logs.get = function (callback) {
fs.truncate(Meta.logs.path, 0, callback); fs.readFile(Logs.path, {
}; encoding: 'utf-8',
}, callback);
};
Logs.clear = function (callback) {
fs.truncate(Logs.path, 0, callback);
}; };

View File

@@ -4,61 +4,60 @@ var async = require('async');
var db = require('../database'); var db = require('../database');
var plugins = require('../plugins'); var plugins = require('../plugins');
var Meta = require('../meta');
module.exports = function (Meta) { var Settings = module.exports;
Meta.settings = {};
Meta.settings.get = function (hash, callback) { Settings.get = function (hash, callback) {
db.getObject('settings:' + hash, function (err, settings) { db.getObject('settings:' + hash, function (err, settings) {
callback(err, settings || {}); callback(err, settings || {});
}); });
}; };
Meta.settings.getOne = function (hash, field, callback) { Settings.getOne = function (hash, field, callback) {
db.getObjectField('settings:' + hash, field, callback); db.getObjectField('settings:' + hash, field, callback);
}; };
Meta.settings.set = function (hash, values, callback) { Settings.set = function (hash, values, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
db.setObject('settings:' + hash, values, next); db.setObject('settings:' + hash, values, next);
}, },
function (next) { function (next) {
plugins.fireHook('action:settings.set', { plugins.fireHook('action:settings.set', {
plugin: hash, plugin: hash,
settings: values, settings: values,
}); });
Meta.reloadRequired = true; Meta.reloadRequired = true;
next(); next();
}, },
], callback); ], callback);
}; };
Meta.settings.setOne = function (hash, field, value, callback) { Settings.setOne = function (hash, field, value, callback) {
db.setObjectField('settings:' + hash, field, value, callback); db.setObjectField('settings:' + hash, field, value, callback);
}; };
Meta.settings.setOnEmpty = function (hash, values, callback) { Settings.setOnEmpty = function (hash, values, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
db.getObject('settings:' + hash, next); db.getObject('settings:' + hash, next);
}, },
function (settings, next) { function (settings, next) {
settings = settings || {}; settings = settings || {};
var empty = {}; var empty = {};
Object.keys(values).forEach(function (key) { Object.keys(values).forEach(function (key) {
if (!settings.hasOwnProperty(key)) { if (!settings.hasOwnProperty(key)) {
empty[key] = values[key]; empty[key] = values[key];
} }
}); });
if (Object.keys(empty).length) { if (Object.keys(empty).length) {
db.setObject('settings:' + hash, empty, next); db.setObject('settings:' + hash, empty, next);
} else { } else {
next(); next();
} }
}, },
], callback); ], callback);
};
}; };

View File

@@ -9,125 +9,124 @@ var async = require('async');
var file = require('../file'); var file = require('../file');
var plugins = require('../plugins'); var plugins = require('../plugins');
var user = require('../user'); var user = require('../user');
var Meta = require('../meta');
var soundsPath = path.join(__dirname, '../../build/public/sounds'); var soundsPath = path.join(__dirname, '../../build/public/sounds');
var uploadsPath = path.join(__dirname, '../../public/uploads/sounds'); var uploadsPath = path.join(__dirname, '../../public/uploads/sounds');
module.exports = function (Meta) { var Sounds = module.exports;
Meta.sounds = {};
Meta.sounds.addUploads = function addUploads(callback) { Sounds.addUploads = function addUploads(callback) {
fs.readdir(uploadsPath, function (err, files) { fs.readdir(uploadsPath, function (err, files) {
if (err) { if (err) {
if (err.code !== 'ENOENT') { if (err.code !== 'ENOENT') {
return callback(err); return callback(err);
}
files = [];
} }
var uploadSounds = files.reduce(function (prev, fileName) { files = [];
var name = fileName.split('.'); }
if (!name.length || !name[0].length) {
return prev;
}
name = name[0];
name = name[0].toUpperCase() + name.slice(1);
prev[name] = fileName; var uploadSounds = files.reduce(function (prev, fileName) {
var name = fileName.split('.');
if (!name.length || !name[0].length) {
return prev;
}
name = name[0];
name = name[0].toUpperCase() + name.slice(1);
prev[name] = fileName;
return prev;
}, {});
plugins.soundpacks = plugins.soundpacks.filter(function (pack) {
return pack.name !== 'Uploads';
});
if (Object.keys(uploadSounds).length) {
plugins.soundpacks.push({
name: 'Uploads',
id: 'uploads',
dir: uploadsPath,
sounds: uploadSounds,
});
}
callback();
});
};
Sounds.build = function build(callback) {
Sounds.addUploads(function (err) {
if (err) {
return callback(err);
}
var map = plugins.soundpacks.map(function (pack) {
return Object.keys(pack.sounds).reduce(function (prev, soundName) {
var soundPath = pack.sounds[soundName];
prev[pack.name + ' | ' + soundName] = pack.id + '/' + soundPath;
return prev; return prev;
}, {}); }, {});
plugins.soundpacks = plugins.soundpacks.filter(function (pack) {
return pack.name !== 'Uploads';
});
if (Object.keys(uploadSounds).length) {
plugins.soundpacks.push({
name: 'Uploads',
id: 'uploads',
dir: uploadsPath,
sounds: uploadSounds,
});
}
callback();
}); });
}; map.unshift({});
map = Object.assign.apply(null, map);
Meta.sounds.build = function build(callback) { async.series([
Meta.sounds.addUploads(function (err) { function (next) {
if (err) { rimraf(soundsPath, next);
return callback(err);
}
var map = plugins.soundpacks.map(function (pack) {
return Object.keys(pack.sounds).reduce(function (prev, soundName) {
var soundPath = pack.sounds[soundName];
prev[pack.name + ' | ' + soundName] = pack.id + '/' + soundPath;
return prev;
}, {});
});
map.unshift({});
map = Object.assign.apply(null, map);
async.series([
function (next) {
rimraf(soundsPath, next);
},
function (next) {
mkdirp(soundsPath, next);
},
function (cb) {
async.parallel([
function (next) {
fs.writeFile(path.join(soundsPath, 'fileMap.json'), JSON.stringify(map), next);
},
function (next) {
async.each(plugins.soundpacks, function (pack, next) {
file.linkDirs(pack.dir, path.join(soundsPath, pack.id), next);
}, next);
},
], cb);
},
], function (err) {
callback(err);
});
});
};
var keys = ['chat-incoming', 'chat-outgoing', 'notification'];
Meta.sounds.getUserSoundMap = function getUserSoundMap(uid, callback) {
async.parallel({
defaultMapping: function (next) {
Meta.configs.getFields(keys, next);
}, },
userSettings: function (next) { function (next) {
user.getSettings(uid, next); mkdirp(soundsPath, next);
}, },
}, function (err, results) { function (cb) {
if (err) { async.parallel([
return callback(err); function (next) {
} fs.writeFile(path.join(soundsPath, 'fileMap.json'), JSON.stringify(map), next);
},
var userSettings = results.userSettings; function (next) {
userSettings = { async.each(plugins.soundpacks, function (pack, next) {
notification: userSettings.notificationSound, file.linkDirs(pack.dir, path.join(soundsPath, pack.id), next);
'chat-incoming': userSettings.incomingChatSound, }, next);
'chat-outgoing': userSettings.outgoingChatSound, },
}; ], cb);
var defaultMapping = results.defaultMapping || {}; },
var soundMapping = {}; ], function (err) {
callback(err);
keys.forEach(function (key) {
if (userSettings[key] || userSettings[key] === '') {
soundMapping[key] = userSettings[key] || '';
} else {
soundMapping[key] = defaultMapping[key] || '';
}
});
callback(null, soundMapping);
}); });
}; });
};
var keys = ['chat-incoming', 'chat-outgoing', 'notification'];
Sounds.getUserSoundMap = function getUserSoundMap(uid, callback) {
async.parallel({
defaultMapping: function (next) {
Meta.configs.getFields(keys, next);
},
userSettings: function (next) {
user.getSettings(uid, next);
},
}, function (err, results) {
if (err) {
return callback(err);
}
var userSettings = results.userSettings;
userSettings = {
notification: userSettings.notificationSound,
'chat-incoming': userSettings.incomingChatSound,
'chat-outgoing': userSettings.outgoingChatSound,
};
var defaultMapping = results.defaultMapping || {};
var soundMapping = {};
keys.forEach(function (key) {
if (userSettings[key] || userSettings[key] === '') {
soundMapping[key] = userSettings[key] || '';
} else {
soundMapping[key] = defaultMapping[key] || '';
}
});
callback(null, soundMapping);
});
}; };

View File

@@ -4,163 +4,163 @@ var nconf = require('nconf');
var validator = require('validator'); var validator = require('validator');
var async = require('async'); var async = require('async');
var winston = require('winston'); var winston = require('winston');
var plugins = require('../plugins'); var plugins = require('../plugins');
var Meta = require('../meta');
module.exports = function (Meta) { var Tags = module.exports;
Meta.tags = {};
Meta.tags.parse = function (req, meta, link, callback) { Tags.parse = function (req, meta, link, callback) {
async.parallel({ async.parallel({
tags: function (next) { tags: function (next) {
var defaultTags = [{ var defaultTags = [{
name: 'viewport', name: 'viewport',
content: 'width=device-width, initial-scale=1.0', content: 'width=device-width, initial-scale=1.0',
}, { }, {
name: 'content-type', name: 'content-type',
content: 'text/html; charset=UTF-8', content: 'text/html; charset=UTF-8',
noEscape: true,
}, {
name: 'apple-mobile-web-app-capable',
content: 'yes',
}, {
name: 'mobile-web-app-capable',
content: 'yes',
}, {
property: 'og:site_name',
content: Meta.config.title || 'NodeBB',
}, {
name: 'msapplication-badge',
content: 'frequency=30; polling-uri=' + nconf.get('url') + '/sitemap.xml',
noEscape: true,
}];
if (Meta.config.keywords) {
defaultTags.push({
name: 'keywords',
content: Meta.config.keywords,
});
}
if (Meta.config['brand:logo']) {
defaultTags.push({
name: 'msapplication-square150x150logo',
content: Meta.config['brand:logo'],
noEscape: true, noEscape: true,
}, { });
name: 'apple-mobile-web-app-capable', }
content: 'yes',
}, {
name: 'mobile-web-app-capable',
content: 'yes',
}, {
property: 'og:site_name',
content: Meta.config.title || 'NodeBB',
}, {
name: 'msapplication-badge',
content: 'frequency=30; polling-uri=' + nconf.get('url') + '/sitemap.xml',
noEscape: true,
}];
if (Meta.config.keywords) { plugins.fireHook('filter:meta.getMetaTags', defaultTags, next);
defaultTags.push({ },
name: 'keywords', links: function (next) {
content: Meta.config.keywords, var defaultLinks = [{
}); rel: 'icon',
} type: 'image/x-icon',
href: nconf.get('relative_path') + '/favicon.ico' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : ''),
}, {
rel: 'manifest',
href: nconf.get('relative_path') + '/manifest.json',
}];
if (Meta.config['brand:logo']) { if (plugins.hasListeners('filter:search.query')) {
defaultTags.push({ defaultLinks.push({
name: 'msapplication-square150x150logo', rel: 'search',
content: Meta.config['brand:logo'], type: 'application/opensearchdescription+xml',
noEscape: true, href: nconf.get('relative_path') + '/osd.xml',
}); });
} }
plugins.fireHook('filter:meta.getMetaTags', defaultTags, next); // Touch icons for mobile-devices
}, if (Meta.config['brand:touchIcon']) {
links: function (next) { defaultLinks.push({
var defaultLinks = [{ rel: 'apple-touch-icon',
href: nconf.get('relative_path') + '/apple-touch-icon',
}, {
rel: 'icon', rel: 'icon',
type: 'image/x-icon', sizes: '36x36',
href: nconf.get('relative_path') + '/favicon.ico' + (Meta.config['cache-buster'] ? '?' + Meta.config['cache-buster'] : ''), href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-36.png',
}, { }, {
rel: 'manifest', rel: 'icon',
href: nconf.get('relative_path') + '/manifest.json', sizes: '48x48',
}]; href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-48.png',
}, {
if (plugins.hasListeners('filter:search.query')) { rel: 'icon',
defaultLinks.push({ sizes: '72x72',
rel: 'search', href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-72.png',
type: 'application/opensearchdescription+xml', }, {
href: nconf.get('relative_path') + '/osd.xml', rel: 'icon',
}); sizes: '96x96',
} href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-96.png',
}, {
// Touch icons for mobile-devices rel: 'icon',
if (Meta.config['brand:touchIcon']) { sizes: '144x144',
defaultLinks.push({ href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-144.png',
rel: 'apple-touch-icon', }, {
href: nconf.get('relative_path') + '/apple-touch-icon', rel: 'icon',
}, { sizes: '192x192',
rel: 'icon', href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-192.png',
sizes: '36x36', });
href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-36.png',
}, {
rel: 'icon',
sizes: '48x48',
href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-48.png',
}, {
rel: 'icon',
sizes: '72x72',
href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-72.png',
}, {
rel: 'icon',
sizes: '96x96',
href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-96.png',
}, {
rel: 'icon',
sizes: '144x144',
href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-144.png',
}, {
rel: 'icon',
sizes: '192x192',
href: nconf.get('relative_path') + '/assets/uploads/system/touchicon-192.png',
});
}
plugins.fireHook('filter:meta.getLinkTags', defaultLinks, next);
},
}, function (err, results) {
if (err) {
return callback(err);
} }
plugins.fireHook('filter:meta.getLinkTags', defaultLinks, next);
meta = results.tags.concat(meta || []).map(function (tag) { },
if (!tag || typeof tag.content !== 'string') { }, function (err, results) {
winston.warn('Invalid meta tag. ', tag); if (err) {
return tag; return callback(err);
}
if (!tag.noEscape) {
tag.content = validator.escape(String(tag.content));
}
return tag;
});
addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB');
var ogUrl = nconf.get('url') + req.path;
addIfNotExists(meta, 'property', 'og:url', ogUrl);
addIfNotExists(meta, 'name', 'description', Meta.config.description);
addIfNotExists(meta, 'property', 'og:description', Meta.config.description);
var ogImage = Meta.config['og:image'] || Meta.config['brand:logo'] || '';
if (ogImage && !ogImage.startsWith('http')) {
ogImage = nconf.get('url') + ogImage;
}
addIfNotExists(meta, 'property', 'og:image', ogImage);
if (ogImage) {
addIfNotExists(meta, 'property', 'og:image:width', 200);
addIfNotExists(meta, 'property', 'og:image:height', 200);
}
link = results.links.concat(link || []);
callback(null, {
meta: meta,
link: link,
});
});
};
function addIfNotExists(meta, keyName, tagName, value) {
var exists = false;
meta.forEach(function (tag) {
if (tag[keyName] === tagName) {
exists = true;
}
});
if (!exists && value) {
var data = {
content: validator.escape(String(value)),
};
data[keyName] = tagName;
meta.push(data);
} }
}
meta = results.tags.concat(meta || []).map(function (tag) {
if (!tag || typeof tag.content !== 'string') {
winston.warn('Invalid meta tag. ', tag);
return tag;
}
if (!tag.noEscape) {
tag.content = validator.escape(String(tag.content));
}
return tag;
});
addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB');
var ogUrl = nconf.get('url') + req.path;
addIfNotExists(meta, 'property', 'og:url', ogUrl);
addIfNotExists(meta, 'name', 'description', Meta.config.description);
addIfNotExists(meta, 'property', 'og:description', Meta.config.description);
var ogImage = Meta.config['og:image'] || Meta.config['brand:logo'] || '';
if (ogImage && !ogImage.startsWith('http')) {
ogImage = nconf.get('url') + ogImage;
}
addIfNotExists(meta, 'property', 'og:image', ogImage);
if (ogImage) {
addIfNotExists(meta, 'property', 'og:image:width', 200);
addIfNotExists(meta, 'property', 'og:image:height', 200);
}
link = results.links.concat(link || []);
callback(null, {
meta: meta,
link: link,
});
});
}; };
function addIfNotExists(meta, keyName, tagName, value) {
var exists = false;
meta.forEach(function (tag) {
if (tag[keyName] === tagName) {
exists = true;
}
});
if (!exists && value) {
var data = {
content: validator.escape(String(value)),
};
data[keyName] = tagName;
meta.push(data);
}
}

View File

@@ -9,168 +9,167 @@ var async = require('async');
var file = require('../file'); var file = require('../file');
var db = require('../database'); var db = require('../database');
var Meta = require('../meta');
module.exports = function (Meta) { var Themes = module.exports;
Meta.themes = {};
Meta.themes.get = function (callback) { Themes.get = function (callback) {
var themePath = nconf.get('themes_path'); var themePath = nconf.get('themes_path');
if (typeof themePath !== 'string') { if (typeof themePath !== 'string') {
return callback(null, []); return callback(null, []);
} }
async.waterfall([ async.waterfall([
function (next) { function (next) {
fs.readdir(themePath, next); fs.readdir(themePath, next);
}, },
function (files, next) { function (files, next) {
async.filter(files, function (file, next) { async.filter(files, function (file, next) {
fs.stat(path.join(themePath, file), function (err, fileStat) { fs.stat(path.join(themePath, file), function (err, fileStat) {
if (err) { if (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
return next(null, false); return next(null, false);
}
return next(err);
} }
return next(err);
}
next(null, (fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-')); next(null, (fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
}); });
}, next); }, next);
}, },
function (themes, next) { function (themes, next) {
async.map(themes, function (theme, next) { async.map(themes, function (theme, next) {
var config = path.join(themePath, theme, 'theme.json'); var config = path.join(themePath, theme, 'theme.json');
fs.readFile(config, function (err, file) { fs.readFile(config, function (err, file) {
if (err) { if (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
return next(null, null); return next(null, null);
}
return next(err);
} }
try { return next(err);
var configObj = JSON.parse(file.toString()); }
try {
var configObj = JSON.parse(file.toString());
// Minor adjustments for API output // Minor adjustments for API output
configObj.type = 'local'; configObj.type = 'local';
if (configObj.screenshot) { if (configObj.screenshot) {
configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + configObj.id; configObj.screenshot_url = nconf.get('relative_path') + '/css/previews/' + configObj.id;
} else {
configObj.screenshot_url = nconf.get('relative_path') + '/assets/images/themes/default.png';
}
next(null, configObj);
} catch (err) {
winston.error('[themes] Unable to parse theme.json ' + theme);
next(null, null);
}
});
}, next);
},
function (themes, next) {
themes = themes.filter(Boolean);
next(null, themes);
},
], callback);
};
Meta.themes.set = function (data, callback) {
var themeData = {
'theme:type': data.type,
'theme:id': data.id,
'theme:staticDir': '',
'theme:templates': '',
'theme:src': '',
};
switch (data.type) {
case 'local':
async.waterfall([
async.apply(Meta.configs.get, 'theme:id'),
function (current, next) {
async.series([
async.apply(db.sortedSetRemove, 'plugins:active', current),
async.apply(db.sortedSetAdd, 'plugins:active', 0, data.id),
], function (err) {
next(err);
});
},
function (next) {
fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function (err, config) {
if (!err) {
config = JSON.parse(config.toString());
next(null, config);
} else { } else {
next(err); configObj.screenshot_url = nconf.get('relative_path') + '/assets/images/themes/default.png';
} }
}); next(null, configObj);
}, } catch (err) {
function (config, next) { winston.error('[themes] Unable to parse theme.json ' + theme);
themeData['theme:staticDir'] = config.staticDir ? config.staticDir : ''; next(null, null);
themeData['theme:templates'] = config.templates ? config.templates : ''; }
themeData['theme:src'] = ''; });
}, next);
},
function (themes, next) {
themes = themes.filter(Boolean);
next(null, themes);
},
], callback);
};
Meta.configs.setMultiple(themeData, next); Themes.set = function (data, callback) {
var themeData = {
// Re-set the themes path (for when NodeBB is reloaded) 'theme:type': data.type,
Meta.themes.setPath(config); 'theme:id': data.id,
}, 'theme:staticDir': '',
], callback); 'theme:templates': '',
'theme:src': '',
Meta.reloadRequired = true;
break;
case 'bootswatch':
Meta.configs.setMultiple({
'theme:src': data.src,
bootswatchSkin: data.id.toLowerCase(),
}, callback);
break;
}
}; };
Meta.themes.setupPaths = function (callback) { switch (data.type) {
case 'local':
async.waterfall([ async.waterfall([
function (next) { async.apply(Meta.configs.get, 'theme:id'),
async.parallel({ function (current, next) {
themesData: Meta.themes.get, async.series([
currentThemeId: function (next) { async.apply(db.sortedSetRemove, 'plugins:active', current),
db.getObjectField('config', 'theme:id', next); async.apply(db.sortedSetAdd, 'plugins:active', 0, data.id),
}, ], function (err) {
}, next); next(err);
});
}, },
function (data, next) { function (next) {
var themeId = data.currentThemeId || 'nodebb-theme-persona'; fs.readFile(path.join(nconf.get('themes_path'), data.id, 'theme.json'), function (err, config) {
if (!err) {
config = JSON.parse(config.toString());
next(null, config);
} else {
next(err);
}
});
},
function (config, next) {
themeData['theme:staticDir'] = config.staticDir ? config.staticDir : '';
themeData['theme:templates'] = config.templates ? config.templates : '';
themeData['theme:src'] = '';
var themeObj = data.themesData.filter(function (themeObj) { Meta.configs.setMultiple(themeData, next);
return themeObj.id === themeId;
})[0];
if (process.env.NODE_ENV === 'development') { // Re-set the themes path (for when NodeBB is reloaded)
winston.info('[themes] Using theme ' + themeId); Themes.setPath(config);
}
if (!themeObj) {
return callback(new Error('[[error:theme-not-found]]'));
}
Meta.themes.setPath(themeObj);
next();
}, },
], callback); ], callback);
};
Meta.themes.setPath = function (themeObj) { Meta.reloadRequired = true;
// Theme's templates path break;
var themePath = nconf.get('base_templates_path');
var fallback = path.join(nconf.get('themes_path'), themeObj.id, 'templates');
if (themeObj.templates) { case 'bootswatch':
themePath = path.join(nconf.get('themes_path'), themeObj.id, themeObj.templates); Meta.configs.setMultiple({
} else if (file.existsSync(fallback)) { 'theme:src': data.src,
themePath = fallback; bootswatchSkin: data.id.toLowerCase(),
} }, callback);
break;
nconf.set('theme_templates_path', themePath); }
nconf.set('theme_config', path.join(nconf.get('themes_path'), themeObj.id, 'theme.json')); };
};
Themes.setupPaths = function (callback) {
async.waterfall([
function (next) {
async.parallel({
themesData: Themes.get,
currentThemeId: function (next) {
db.getObjectField('config', 'theme:id', next);
},
}, next);
},
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);
}
if (!themeObj) {
return callback(new Error('[[error:theme-not-found]]'));
}
Themes.setPath(themeObj);
next();
},
], callback);
};
Themes.setPath = function (themeObj) {
// Theme's templates path
var themePath = nconf.get('base_templates_path');
var fallback = path.join(nconf.get('themes_path'), themeObj.id, 'templates');
if (themeObj.templates) {
themePath = path.join(nconf.get('themes_path'), themeObj.id, themeObj.templates);
} else if (file.existsSync(fallback)) {
themePath = fallback;
}
nconf.set('theme_templates_path', themePath);
nconf.set('theme_config', path.join(nconf.get('themes_path'), themeObj.id, 'theme.json'));
}; };