Ghost/core/server.js
Christopher Giffard 48b75fa396 Initial 404 Error Handling Support
Fixes #356

- Adds new generic methods for handling errors to errorHandling.js
- Initialises generic methods as middleware
- Created error.hbs view in admin
- Error handler searches for error.hbs view file in user theme folder
  and renders it if available, otherwise lets the error fall through
  to express.

- We *could* change the final behaviour to render a default ghost
  template should the user template be missing
- Because it currently isn't possible to require(ghost) in errorHandling.js,
  it was necessary to duplicate some aspects of the ghost path init code
  inside errorhandling.js. This should be cleaned up and moved back
  into ghost.js when possible.
2013-09-17 20:56:05 +10:00

430 lines
15 KiB
JavaScript

// Module dependencies
var express = require('express'),
when = require('when'),
_ = require('underscore'),
colors = require("colors"),
semver = require("semver"),
slashes = require("connect-slashes"),
errors = require('./server/errorHandling'),
admin = require('./server/controllers/admin'),
frontend = require('./server/controllers/frontend'),
api = require('./server/api'),
path = require('path'),
hbs = require('express-hbs'),
Ghost = require('./ghost'),
helpers = require('./server/helpers'),
packageInfo = require('../package.json'),
// Variables
loading = when.defer(),
server = express(),
ghost = new Ghost();
// ##Custom Middleware
// ### Auth Middleware
// Authenticate a request by redirecting to login if not logged in.
// We strip /ghost/ out of the redirect parameter for neatness
function auth(req, res, next) {
if (!req.session.user) {
var path = req.path.replace(/^\/ghost\/?/gi, ''),
redirect = '',
msg;
if (path !== '') {
msg = {
type: 'error',
message: 'Please Sign In',
status: 'passive',
id: 'failedauth'
};
// let's only add the notification once
if (!_.contains(_.pluck(ghost.notifications, 'id'), 'failedauth')) {
ghost.notifications.push(msg);
}
redirect = '?r=' + encodeURIComponent(path);
}
return res.redirect('/ghost/signin/' + redirect);
}
next();
}
// Check if we're logged in, and if so, redirect people back to dashboard
// Login and signup forms in particular
function redirectToDashboard(req, res, next) {
if (req.session.user) {
return res.redirect('/ghost/');
}
next();
}
function redirectToSignup(req, res, next) {
api.users.browse().then(function (users) {
if (users.length === 0) {
return res.redirect('/ghost/signup/');
}
});
next();
}
// While we're here, let's clean up on aisle 5
// That being ghost.notifications, and let's remove the passives from there
// plus the local messages, as they have already been added at this point
// otherwise they'd appear one too many times
function cleanNotifications(req, res, next) {
ghost.notifications = _.reject(ghost.notifications, function (notification) {
return notification.status === 'passive';
});
next();
}
// ## AuthApi Middleware
// Authenticate a request to the API by responding with a 401 and json error details
function authAPI(req, res, next) {
if (!req.session.user) {
// TODO: standardize error format/codes/messages
res.json(401, { error: 'Please sign in' });
return;
}
next();
}
// ### GhostLocals Middleware
// Expose the standard locals that every external page should have available,
// separating between the theme and the admin
function ghostLocals(req, res, next) {
// Make sure we have a locals value.
res.locals = res.locals || {};
res.locals.version = packageInfo.version;
if (res.isAdmin) {
_.extend(res.locals, {
messages: ghost.notifications
});
api.users.read({id: req.session.user}).then(function (currentUser) {
_.extend(res.locals, {
currentUser: {
name: currentUser.attributes.name,
profile: currentUser.attributes.image
}
});
next();
}).otherwise(function () {
next();
});
} else {
next();
}
}
// ### DisableCachedResult Middleware
// Disable any caching until it can be done properly
function disableCachedResult(req, res, next) {
res.set({
"Cache-Control": "no-cache, must-revalidate",
"Expires": "Sat, 26 Jul 1997 05:00:00 GMT"
});
next();
}
// ### whenEnabled Middleware
// Selectively use middleware
// From https://github.com/senchalabs/connect/issues/676#issuecomment-9569658
function whenEnabled(setting, fn) {
return function settingEnabled(req, res, next) {
if (server.enabled(setting)) {
fn(req, res, next);
} else {
next();
}
};
}
// ### InitViews Middleware
// Initialise Theme or Admin Views
function initViews(req, res, next) {
var hbsOptions;
if (!res.isAdmin) {
// self.globals is a hack til we have a better way of getting combined settings & config
hbsOptions = {templateOptions: {data: {blog: ghost.blogGlobals()}}};
if (ghost.themeDirectories[ghost.settings('activeTheme')].hasOwnProperty('partials')) {
// Check that the theme has a partials directory before trying to use it
hbsOptions.partialsDir = path.join(ghost.paths().activeTheme, 'partials');
}
server.engine('hbs', hbs.express3(hbsOptions));
server.set('views', ghost.paths().activeTheme);
} else {
server.engine('hbs', hbs.express3({partialsDir: ghost.paths().adminViews + 'partials'}));
server.set('views', ghost.paths().adminViews);
}
next();
}
// ### Activate Theme
// Helper for manageAdminAndTheme
function activateTheme() {
var stackLocation = _.indexOf(server.stack, _.find(server.stack, function (stackItem, key) {
return stackItem.route === '' && stackItem.handle.name === 'settingEnabled';
}));
// clear the view cache
server.cache = {};
server.disable(server.get('activeTheme'));
server.set('activeTheme', ghost.settings('activeTheme'));
server.enable(server.get('activeTheme'));
if (stackLocation) {
server.stack[stackLocation].handle = whenEnabled(server.get('activeTheme'), express['static'](ghost.paths().activeTheme));
}
}
// ### ManageAdminAndTheme Middleware
// Uses the URL to detect whether this response should be an admin response
// This is used to ensure the right content is served, and is not for security purposes
function manageAdminAndTheme(req, res, next) {
// TODO improve this regex
res.isAdmin = /(^\/ghost\/)/.test(req.url);
if (res.isAdmin) {
server.enable('admin');
server.disable(server.get('activeTheme'));
} else {
server.enable(server.get('activeTheme'));
server.disable('admin');
}
// Check if the theme changed
if (ghost.settings('activeTheme') !== server.get('activeTheme')) {
// Change theme
if (!ghost.themeDirectories.hasOwnProperty(ghost.settings('activeTheme'))) {
if (!res.isAdmin) {
// Throw an error if the theme is not available, but not on the admin UI
errors.logAndThrowError('The currently active theme ' + ghost.settings('activeTheme') + ' is missing.');
}
} else {
activateTheme();
}
}
next();
}
// Expose the promise we will resolve after our pre-loading
ghost.loaded = loading.promise;
when.all([ghost.init(), helpers.loadCoreHelpers(ghost)]).then(function () {
// ##Configuration
var oneYear = 31536000000;
// Logging configuration
if (server.get('env') !== "development") {
server.use(express.logger());
} else {
server.use(express.logger('dev'));
}
// return the correct mime type for woff filess
express['static'].mime.define({'application/font-woff': ['woff']});
// Shared static config
server.use('/shared', express['static'](path.join(__dirname, '/shared')));
server.use('/content/images', express['static'](path.join(__dirname, '/../content/images')));
// Serve our built scripts; can't use /scripts here because themes already are
server.use("/built/scripts", express['static'](path.join(__dirname, '/built/scripts'), {
// Put a maxAge of one year on built scripts
maxAge: oneYear
}));
// First determine whether we're serving admin or theme content
server.use(manageAdminAndTheme);
// Admin only config
server.use('/ghost', whenEnabled('admin', express['static'](path.join(__dirname, '/client/assets'))));
// Theme only config
server.use(whenEnabled(server.get('activeTheme'), express['static'](ghost.paths().activeTheme)));
// Add in all trailing slashes
server.use(slashes());
server.use(express.favicon(__dirname + '/shared/favicon.ico'));
server.use(express.bodyParser({}));
server.use(express.bodyParser({uploadDir: __dirname + '/content/images'}));
server.use(express.cookieParser(ghost.dbHash));
server.use(express.cookieSession({ cookie: { maxAge: 60000000 }}));
// local data
server.use(ghostLocals);
// So on every request we actually clean out reduntant passive notifications from the server side
server.use(cleanNotifications);
// set the view engine
server.set('view engine', 'hbs');
// Initialise the views
server.use(initViews);
// process the application routes
server.use(server.router);
// ### Error handling
// 404 Handler
server.use(errors.render404Page);
// TODO: Handle all application errors (~500)
// Just stubbed at this stage!
server.use(function error500Handler(err, req, res, next) {
if (!err || !(err instanceof Error)) {
next();
}
// For the time being, just log and continue.
errors.logError(err, "Middleware", "Ghost caught a processing error in the middleware layer.");
next(err);
});
// All other errors
if (server.get('env') === "production") {
server.use(express.errorHandler({ dumpExceptions: false, showStack: false }));
} else {
server.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
}
// ## Routing
// ### API routes
/* TODO: auth should be public auth not user auth */
// #### Posts
server.get('/api/v0.1/posts', authAPI, disableCachedResult, api.requestHandler(api.posts.browse));
server.post('/api/v0.1/posts', authAPI, disableCachedResult, api.requestHandler(api.posts.add));
server.get('/api/v0.1/posts/:id', authAPI, disableCachedResult, api.requestHandler(api.posts.read));
server.put('/api/v0.1/posts/:id', authAPI, disableCachedResult, api.requestHandler(api.posts.edit));
server.del('/api/v0.1/posts/:id', authAPI, disableCachedResult, api.requestHandler(api.posts.destroy));
// #### Settings
server.get('/api/v0.1/settings/', authAPI, disableCachedResult, api.requestHandler(api.settings.browse));
server.get('/api/v0.1/settings/:key/', authAPI, disableCachedResult, api.requestHandler(api.settings.read));
server.put('/api/v0.1/settings/', authAPI, disableCachedResult, api.requestHandler(api.settings.edit));
// #### Users
server.get('/api/v0.1/users/', authAPI, disableCachedResult, api.requestHandler(api.users.browse));
server.get('/api/v0.1/users/:id/', authAPI, disableCachedResult, api.requestHandler(api.users.read));
server.put('/api/v0.1/users/:id/', authAPI, disableCachedResult, api.requestHandler(api.users.edit));
// #### Tags
server.get('/api/v0.1/tags/', authAPI, disableCachedResult, api.requestHandler(api.tags.all));
// #### Notifications
server.del('/api/v0.1/notifications/:id', authAPI, disableCachedResult, api.requestHandler(api.notifications.destroy));
server.post('/api/v0.1/notifications/', authAPI, disableCachedResult, api.requestHandler(api.notifications.add));
// ### Admin routes
/* TODO: put these somewhere in admin */
server.get(/^\/logout\/?$/, function redirect(req, res) {
res.redirect(301, '/signout/');
});
server.get(/^\/signout\/?$/, admin.logout);
server.get('/ghost/login/', function redirect(req, res) {
res.redirect(301, '/ghost/signin/');
});
server.get('/ghost/signin/', redirectToSignup, redirectToDashboard, admin.login);
server.get('/ghost/signup/', redirectToDashboard, admin.signup);
server.get('/ghost/forgotten/', redirectToDashboard, admin.forgotten);
server.post('/ghost/forgotten/', admin.resetPassword);
server.post('/ghost/signin/', admin.auth);
server.post('/ghost/signup/', admin.doRegister);
server.post('/ghost/changepw/', auth, admin.changepw);
server.get('/ghost/editor(/:id)/', auth, admin.editor);
server.get('/ghost/editor/', auth, admin.editor);
server.get('/ghost/content/', auth, admin.content);
server.get('/ghost/settings*', auth, admin.settings);
server.get('/ghost/debug/', auth, admin.debug.index);
server.get('/ghost/debug/db/export/', auth, admin.debug['export']);
server.post('/ghost/debug/db/import/', auth, admin.debug['import']);
server.get('/ghost/debug/db/reset/', auth, admin.debug.reset);
server.post('/ghost/upload/', admin.uploader);
server.get(/^\/(ghost$|(ghost-admin|admin|wp-admin|dashboard|signin)\/?)/, auth, function (req, res) {
res.redirect('/ghost/');
});
server.get('/ghost/', redirectToSignup, auth, admin.index);
// ### Frontend routes
/* TODO: dynamic routing, homepage generator, filters ETC ETC */
server.get('/rss/', frontend.rss);
server.get('/rss/:page/', frontend.rss);
server.get('/page/:page/', frontend.homepage);
server.get('/:slug/', frontend.single);
server.get('/', frontend.homepage);
// ## Start Ghost App
server.listen(
ghost.config().server.port,
ghost.config().server.host,
function () {
// Tell users if their node version is not supported, and exit
if (!semver.satisfies(process.versions.node, packageInfo.engines.node)) {
console.log(
"\nERROR: Unsupported version of Node".red,
"\nGhost needs Node version".red,
packageInfo.engines.node.yellow,
"you are using version".red,
process.versions.node.yellow,
"\nPlease go to http://nodejs.org to get the latest version".green
);
process.exit(0);
}
// Startup & Shutdown messages
if (process.env.NODE_ENV === 'production') {
console.log(
"Ghost is running...".green,
"\nYour blog is now available on",
ghost.config().url,
"\nCtrl+C to shut down".grey
);
// ensure that Ghost exits correctly on Ctrl+C
process.on('SIGINT', function () {
console.log(
"\nGhost has shut down".red,
"\nYour blog is now offline"
);
process.exit(0);
});
} else {
console.log(
"Ghost is running...".green,
"\nListening on",
ghost.config().server.host + ':' + ghost.config().server.port,
"\nUrl configured as:",
ghost.config().url,
"\nCtrl+C to shut down".grey
);
// ensure that Ghost exits correctly on Ctrl+C
process.on('SIGINT', function () {
console.log(
"\nGhost has shutdown".red,
"\nGhost was running for",
Math.round(process.uptime()),
"seconds"
);
process.exit(0);
});
}
// Let everyone know we have finished loading
loading.resolve();
}
);
}, errors.logAndThrowError);