You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
nodebb/src/webserver.js

786 lines
22 KiB
JavaScript

var path = require('path'),
fs = require('fs'),
express = require('express'),
express_namespace = require('express-namespace'),
WebServer = express(),
server = require('http').createServer(WebServer),
RedisStore = require('connect-redis')(express),
nconf = require('nconf'),
winston = require('winston'),
validator = require('validator'),
async = require('async'),
pkg = require('../package.json'),
utils = require('../public/src/utils'),
RDB = require('./redis'),
user = require('./user'),
categories = require('./categories'),
posts = require('./posts'),
topics = require('./topics'),
notifications = require('./notifications'),
admin = require('./routes/admin'),
userRoute = require('./routes/user'),
apiRoute = require('./routes/api'),
auth = require('./routes/authentication'),
meta = require('./meta'),
feed = require('./feed'),
plugins = require('./plugins'),
logger = require('./logger');
11 years ago
(function (app) {
"use strict";
var templates = null,
clientScripts;
plugins.ready(function() {
// Minify client-side libraries
meta.js.get(function (err, scripts) {
clientScripts = scripts.map(function (script) {
script = {
script: script
};
return script;
});
});
});
server.app = app;
/**
* `options` object requires: req, res
* accepts: metaTags
*/
11 years ago
app.build_header = function (options, callback) {
var custom_header = {
'navigation': []
};
plugins.fireHook('filter:header.build', custom_header, function(err, custom_header) {
var defaultMetaTags = [{
name: 'viewport',
content: 'width=device-width, initial-scale=1.0, user-scalable=no'
}, {
name: 'content-type',
content: 'text/html; charset=UTF-8'
}, {
name: 'apple-mobile-web-app-capable',
content: 'yes'
}, {
property: 'og:site_name',
content: meta.config.title || 'NodeBB'
}, {
property: 'keywords',
content: meta.config.keywords || ''
}],
metaString = utils.buildMetaTags(defaultMetaTags.concat(options.metaTags || [])),
linkTags = utils.buildLinkTags(options.linkTags || []),
templateValues = {
cssSrc: meta.config['theme:src'] || nconf.get('relative_path') + '/vendor/bootstrap/css/bootstrap.min.css',
pluginCSS: plugins.cssFiles.map(function(file) { return { path: file }; }),
11 years ago
title: meta.config.title || '',
description: meta.config.description || '',
11 years ago
'brand:logo': meta.config['brand:logo'] || '',
'brand:logo:display': meta.config['brand:logo']?'':'hide',
browserTitle: meta.config.title || 'NodeBB',
csrf: options.res.locals.csrf_token,
relative_path: nconf.get('relative_path'),
meta_tags: metaString,
link_tags: linkTags,
clientScripts: clientScripts,
navigation: custom_header.navigation
};
var uid = '0';
if(options.req.user && options.req.user.uid) {
uid = options.req.user.uid;
}
user.isAdministrator(uid, function(isAdmin) {
templateValues.adminDisplay = isAdmin ? 'show' : 'hide';
translator.translate(templates.header.parse(templateValues), function(template) {
callback(null, template);
});
});
});
};
// Middlewares
app.configure(function() {
async.series([
function(next) {
// Pre-router middlewares
app.use(express.compress());
logger.init(app);
app.use(express.favicon(path.join(__dirname, '../', 'public', 'favicon.ico')));
app.use(require('less-middleware')({
src: path.join(__dirname, '../', 'public'),
prefix: nconf.get('relative_path'),
yuicompress: true
}));
app.use(express.bodyParser()); // Puts POST vars in request.body
app.use(express.cookieParser()); // If you want to parse cookies (res.cookies)
app.use(express.session({
store: new RedisStore({
client: RDB,
ttl: 60 * 60 * 24 * 30
}),
secret: nconf.get('secret'),
key: 'express.sid',
cookie: {
maxAge: 60 * 60 * 24 * 30 * 1000 // 30 days
}
}));
app.use(express.csrf());
// Local vars, other assorted setup
app.use(function (req, res, next) {
nconf.set('https', req.secure);
res.locals.csrf_token = req.session._csrf;
// Disable framing
res.setHeader('X-Frame-Options', 'DENY');
next();
});
// Authentication Routes
auth.initialize(app);
next();
},
function(next) {
async.parallel([
function(next) {
// Theme configuration
RDB.hmget('config', 'theme:type', 'theme:id', 'theme:staticDir', 'theme:templates', function(err, themeData) {
var themeId = (themeData[1] || 'nodebb-theme-vanilla');
// Detect if a theme has been selected, and handle appropriately
if (!themeData[0] || themeData[0] === 'local') {
// Local theme
if (process.env.NODE_ENV === 'development') {
winston.info('[themes] Using theme ' + themeId);
}
// Theme's static directory
if (themeData[2]) {
app.use('/css/assets', express.static(path.join(__dirname, '../node_modules', themeData[1], themeData[2]), {
maxAge: 5184000000
}));
if (process.env.NODE_ENV === 'development') {
winston.info('Static directory routed for theme: ' + themeData[1]);
}
}
if (themeData[3]) {
app.use('/templates', express.static(path.join(__dirname, '../node_modules', themeData[1], themeData[3]), {
maxAge: 5184000000
}));
if (process.env.NODE_ENV === 'development') {
winston.info('Custom templates directory routed for theme: ' + themeData[1]);
}
}
app.use(require('less-middleware')({
src: path.join(__dirname, '../node_modules/' + themeId),
dest: path.join(__dirname, '../public/css'),
prefix: nconf.get('relative_path') + '/css',
yuicompress: true
}));
next();
} else {
// If not using a local theme (bootswatch, etc), drop back to vanilla
if (process.env.NODE_ENV === 'development') {
winston.info('[themes] Using theme ' + themeId);
}
app.use(require('less-middleware')({
src: path.join(__dirname, '../node_modules/nodebb-theme-vanilla'),
dest: path.join(__dirname, '../public/css'),
prefix: nconf.get('relative_path') + '/css',
yuicompress: true
}));
next();
}
});
// Route paths to screenshots for installed themes
meta.themes.get(function(err, themes) {
var screenshotPath;
async.each(themes, function(themeObj, next) {
if (themeObj.screenshot) {
screenshotPath = path.join(__dirname, '../node_modules', themeObj.id, themeObj.screenshot);
11 years ago
(function(id, path) {
fs.exists(path, function(exists) {
if (exists) {
app.get('/css/previews/' + id, function(req, res) {
res.sendfile(path);
});
}
});
})(themeObj.id, screenshotPath);
} else {
next(false);
}
});
});
}
], next);
},
function(next) {
// Router & post-router middlewares
app.use(app.router);
// Static directory /public
app.use(nconf.get('relative_path'), express.static(path.join(__dirname, '../', 'public'), {
maxAge: 5184000000
}));
// 404 catch-all
app.use(function (req, res, next) {
var isLanguage = /^\/language\/[\w]{2,}\/.*\.json/,
isClientScript = /^\/src\/forum\/[\w]+\.js/;
res.status(404);
if (isClientScript.test(req.url)) {
// Handle missing client-side scripts
res.type('text/javascript').send(200, '');
} else if (isLanguage.test(req.url)) {
// Handle languages by sending an empty object
res.json(200, {});
} else if (req.accepts('html')) {
// respond with html page
if (process.env.NODE_ENV === 'development') {
winston.warn('Route requested but not found: ' + req.url);
}
res.redirect(nconf.get('relative_path') + '/404');
} else if (req.accepts('json')) {
// respond with json
if (process.env.NODE_ENV === 'development') {
winston.warn('Route requested but not found: ' + req.url);
}
res.json({
error: 'Not found'
});
} else {
// default to plain-text. send()
res.type('txt').send('Not found');
}
});
app.use(function (err, req, res, next) {
// we may use properties of the error object
// here and next(err) appropriately, or if
// we possibly recovered from the error, simply next().
console.error(err.stack);
res.status(err.status || 500);
res.json('500', {
error: err.message
});
});
next();
}
], function(err) {
if (err) {
winston.error('Errors were encountered while attempting to initialise NodeBB.');
process.exit();
} else {
if (process.env.NODE_ENV === 'development') {
winston.info('Middlewares loaded.');
}
}
});
});
11 years ago
module.exports.init = function () {
templates = global.templates;
// translate all static templates served by webserver here. ex. footer, logout
plugins.fireHook('filter:footer.build', '', function(err, appendHTML) {
var footer = templates.footer.parse({
footerHTML: appendHTML
});
translator.translate(footer, function(parsedTemplate) {
templates.footer = parsedTemplate;
});
});
plugins.fireHook('action:app.load');
11 years ago
translator.translate(templates.logout.toString(), function(parsedTemplate) {
templates.logout = parsedTemplate;
});
winston.info('NodeBB Ready');
server.listen(nconf.get('PORT') || nconf.get('port'), nconf.get('bind_address'));
};
11 years ago
app.create_route = function (url, tpl) { // to remove
return '<script>templates.ready(function(){ajaxify.go("' + url + '", null, "' + tpl + '", true);});</script>';
};
11 years ago
app.namespace(nconf.get('relative_path'), function () {
11 years ago
auth.createRoutes(app);
admin.createRoutes(app);
userRoute.createRoutes(app);
apiRoute.createRoutes(app);
// Basic Routes (entirely client-side parsed, goal is to move the rest of the crap in this file into this one section)
11 years ago
(function () {
var routes = ['login', 'register', 'account', 'recent', '403', '404'],
loginRequired = ['unread', 'search', 'notifications'];
async.each(routes.concat(loginRequired), function(route, next) {
app.get('/' + route, function (req, res) {
if ((route === 'login' || route === 'register') && (req.user && req.user.uid > 0)) {
user.getUserField(req.user.uid, 'userslug', function (err, userslug) {
res.redirect('/user/' + userslug);
});
return;
} else if (loginRequired.indexOf(route) !== -1 && !req.user) {
return res.redirect('/403');
}
app.build_header({
req: req,
res: res
}, function (err, header) {
res.send((isNaN(parseInt(route, 10)) ? 200 : parseInt(route, 10)), header + app.create_route(route) + templates.footer);
});
});
});
}());
11 years ago
app.get('/', function (req, res) {
async.parallel({
11 years ago
"header": function (next) {
12 years ago
app.build_header({
req: req,
res: res,
metaTags: [{
name: "title",
content: meta.config.title || 'NodeBB'
}, {
name: "description",
content: meta.config.description || ''
}, {
property: 'og:title',
content: 'Index | ' + (meta.config.title || 'NodeBB')
}, {
property: "og:type",
content: 'website'
}]
12 years ago
}, next);
},
11 years ago
"categories": function (next) {
categories.getAllCategories(function (returnData) {
returnData.categories = returnData.categories.filter(function (category) {
if (category.disabled !== '1') {
return true;
} else {
return false;
}
12 years ago
});
next(null, returnData);
}, 0);
}
11 years ago
}, function (err, data) {
res.send(
data.header +
'\n\t<noscript>\n' + templates['noscript/header'] + templates['noscript/home'].parse(data.categories) + '\n\t</noscript>' +
app.create_route('') +
templates.footer
);
});
});
11 years ago
app.get('/topic/:topic_id/:slug?', function (req, res) {
var tid = req.params.topic_id;
12 years ago
if (tid.match(/^\d+\.rss$/)) {
tid = tid.slice(0, -4);
var rssPath = path.join(__dirname, '../', 'feeds/topics', tid + '.rss'),
11 years ago
loadFeed = function () {
fs.readFile(rssPath, function (err, data) {
if (err) {
res.type('text').send(404, "Unable to locate an rss feed at this location.");
} else {
res.type('xml').set('Content-Length', data.length).send(data);
}
});
};
if (!fs.existsSync(rssPath)) {
11 years ago
feed.updateTopic(tid, function (err) {
if (err) {
res.redirect('/404');
} else {
loadFeed();
}
});
} else {
loadFeed();
}
return;
}
async.waterfall([
11 years ago
function (next) {
topics.getTopicWithPosts(tid, ((req.user) ? req.user.uid : 0), 0, -1, function (err, topicData) {
if (topicData) {
if (topicData.deleted === '1' && topicData.expose_tools === 0) {
return next(new Error('Topic deleted'), null);
}
}
next(err, topicData);
});
},
11 years ago
function (topicData, next) {
var lastMod = 0,
timestamp,
sanitize = validator.sanitize;
for (var x = 0, numPosts = topicData.posts.length; x < numPosts; x++) {
12 years ago
timestamp = parseInt(topicData.posts[x].timestamp, 10);
if (timestamp > lastMod) {
lastMod = timestamp;
}
12 years ago
}
app.build_header({
req: req,
res: res,
metaTags: [{
name: "title",
content: topicData.topic_name
}, {
name: "description",
content: sanitize(topicData.posts[0].content.substr(0, 255)).escape().replace('\n', '')
}, {
property: 'og:title',
content: topicData.topic_name + ' | ' + (meta.config.title || 'NodeBB')
}, {
property: "og:type",
content: 'article'
}, {
property: "og:url",
content: nconf.get('url') + 'topic/' + topicData.slug
}, {
property: 'og:image',
content: topicData.posts[0].picture
}, {
property: "article:published_time",
content: new Date(parseInt(topicData.posts[0].timestamp, 10)).toISOString()
}, {
property: 'article:modified_time',
content: new Date(lastMod).toISOString()
}, {
property: 'article:section',
content: topicData.category_name
}],
linkTags: [
{
rel: 'alternate',
type: 'application/rss+xml',
href: nconf.get('url') + 'topic/' + tid + '.rss'
},
{
rel: 'up',
href: nconf.get('url') + 'category/' + topicData.category_slug
}
]
11 years ago
}, function (err, header) {
next(err, {
header: header,
topics: topicData
});
});
},
11 years ago
], function (err, data) {
if (err) {
return res.redirect('404');
}
var topic_url = tid + (req.params.slug ? '/' + req.params.slug : '');
res.send(
data.header +
'\n\t<noscript>\n' + templates['noscript/header'] + templates['noscript/topic'].parse(data.topics) + '\n\t</noscript>' +
'\n\t<script>templates.ready(function(){ajaxify.go("topic/' + topic_url + '", undefined, undefined, true);});</script>' +
templates.footer
);
});
});
11 years ago
app.get('/category/:category_id/:slug?', function (req, res) {
var cid = req.params.category_id;
12 years ago
if (cid.match(/^\d+\.rss$/)) {
cid = cid.slice(0, -4);
var rssPath = path.join(__dirname, '../', 'feeds/categories', cid + '.rss'),
11 years ago
loadFeed = function () {
fs.readFile(rssPath, function (err, data) {
if (err) {
res.type('text').send(404, "Unable to locate an rss feed at this location.");
} else {
res.type('xml').set('Content-Length', data.length).send(data);
}
});
};
if (!fs.existsSync(rssPath)) {
11 years ago
feed.updateCategory(cid, function (err) {
if (err) {
res.redirect('/404');
} else {
loadFeed();
}
});
} else {
loadFeed();
}
return;
}
async.waterfall([
11 years ago
function (next) {
categories.getCategoryById(cid, 0, function (err, categoryData) {
12 years ago
if (categoryData) {
if (categoryData.disabled === '1') {
12 years ago
return next(new Error('Category disabled'), null);
}
12 years ago
}
next(err, categoryData);
});
},
11 years ago
function (categoryData, next) {
app.build_header({
req: req,
res: res,
metaTags: [{
name: 'title',
content: categoryData.category_name
}, {
name: 'description',
content: categoryData.category_description
}, {
property: "og:type",
content: 'website'
}],
linkTags: [
{
rel: 'alternate',
type: 'application/rss+xml',
href: nconf.get('url') + 'category/' + cid + '.rss'
},
{
rel: 'up',
href: nconf.get('url')
}
]
11 years ago
}, function (err, header) {
next(err, {
header: header,
categories: categoryData
});
});
}
11 years ago
], function (err, data) {
if (err) {
return res.redirect('404');
}
var category_url = cid + (req.params.slug ? '/' + req.params.slug : '');
res.send(
data.header +
'\n\t<noscript>\n' + templates['noscript/header'] + templates['noscript/category'].parse(data.categories) + '\n\t</noscript>' +
'\n\t<script>templates.ready(function(){ajaxify.go("category/' + category_url + '", undefined, undefined, true);});</script>' +
templates.footer
);
});
});
11 years ago
app.get('/confirm/:code', function (req, res) {
app.build_header({
req: req,
res: res
11 years ago
}, function (err, header) {
res.send(header + '<script>templates.ready(function(){ajaxify.go("confirm/' + req.params.code + '", undefined, undefined, true);});</script>' + templates.footer);
});
});
11 years ago
app.get('/sitemap.xml', function (req, res) {
var sitemap = require('./sitemap.js');
11 years ago
sitemap.render(function (xml) {
res.type('xml').set('Content-Length', xml.length).send(xml);
});
});
11 years ago
app.get('/robots.txt', function (req, res) {
res.set('Content-Type', 'text/plain');
res.send("User-agent: *\n" +
"Disallow: /admin/\n" +
"Sitemap: " + nconf.get('url') + "sitemap.xml");
});
11 years ago
app.get('/recent.rss', function(req, res) {
var rssPath = path.join(__dirname, '../', 'feeds/recent.rss'),
loadFeed = function () {
fs.readFile(rssPath, function (err, data) {
if (err) {
res.type('text').send(404, "Unable to locate an rss feed at this location.");
} else {
res.type('xml').set('Content-Length', data.length).send(data);
}
});
};
if (!fs.existsSync(rssPath)) {
feed.updateRecent(function (err) {
11 years ago
if (err) {
res.redirect('/404');
} else {
loadFeed();
}
});
} else {
loadFeed();
}
});
app.get('/recent/:term?', function (req, res) {
// TODO consolidate with /recent route as well -> that can be combined into this area. See "Basic Routes" near top.
app.build_header({
req: req,
res: res
}, function (err, header) {
res.send(header + app.create_route('recent/' + req.params.term, null, 'recent') + templates.footer);
});
});
11 years ago
app.get('/outgoing', function (req, res) {
if (!req.query.url) {
return res.redirect('/404');
}
app.build_header({
req: req,
res: res
11 years ago
}, function (err, header) {
res.send(
header +
12 years ago
'\n\t<script>templates.ready(function(){ajaxify.go("outgoing?url=' + encodeURIComponent(req.query.url) + '", null, null, true);});</script>' +
templates.footer
);
});
});
11 years ago
app.get('/search/:term', function (req, res) {
if (!req.user) {
11 years ago
return res.redirect('/403');
}
app.build_header({
req: req,
res: res
11 years ago
}, function (err, header) {
res.send(header + app.create_route('search/' + req.params.term, null, 'search') + templates.footer);
});
});
11 years ago
app.get('/reindex', function (req, res) {
topics.reIndexAll(function (err) {
if (err) {
return res.json(err);
12 years ago
}
11 years ago
user.reIndexAll(function (err) {
if (err) {
return res.json(err);
} else {
res.send('Topics and users reindexed');
}
});
12 years ago
});
});
// Other routes
require('./routes/plugins')(app);
// Debug routes
if (process.env.NODE_ENV === 'development') {
require('./routes/debug')(app);
}
var custom_routes = {
'routes': [],
'api_methods': []
};
plugins.ready(function() {
plugins.fireHook('filter:server.create_routes', custom_routes, function(err, custom_routes) {
var routes = custom_routes.routes;
for (var route in routes) {
if (routes.hasOwnProperty(route)) {
(function(route) {
app[routes[route].method || 'get'](routes[route].route, function(req, res) {
routes[route].options(req, res, function(options) {
app.build_header({
req: options.req,
res: options.res
}, function (err, header) {
res.send(header + options.content + templates.footer);
});
});
});
}(route));
}
}
});
});
});
}(WebServer));
global.server = server;