Upgrade to Express 4.0

no related issue

- Updates package.json packages, adding express middleware packages
 that have been broken into their own modules

- Updates controllers/frontend.js to use the new Layer object that Express 4.0
 has.  Requires some monkey-patching as the Layer object isn't explicitly
 surfaced, however it should be safe to do.

- Moved the setup of routes into middleware/index.js because they need to
 be added as a middleware function before the 404 and 500 handlers. This is
 no longer possible with the old app.use(app.router) as that has been removed.

- Cleaned up middleware/index.js to make it compatible with Express 4.0.

- Simplified the way themes are activated and enabled when they are activated.
 The new handling is simpler, yet should still cover all the use cases that
 previously existed.

- The entire flow of activating a theme through middleware should be a little
 more centralized, letting it be easier to read and maintain.

- Moved every routes/*.js file to use an individual express.Router() instance.
This commit is contained in:
Harry Wolff 2014-04-11 23:46:15 -04:00
parent a55bfea5b0
commit 5d028b72fb
9 changed files with 168 additions and 139 deletions

View File

@ -1,4 +1,4 @@
var Store = require('express').session.Store,
var Store = require('express-session').Store,
models = require('./models'),
time12h = 12 * 60 * 60 * 1000,

View File

@ -9,7 +9,6 @@ var moment = require('moment'),
_ = require('lodash'),
url = require('url'),
when = require('when'),
Route = require('express').Route,
api = require('../api'),
config = require('../config'),
@ -18,8 +17,32 @@ var moment = require('moment'),
errors = require('../errors'),
frontendControllers,
// Cache static post permalink regex
staticPostPermalink = new Route(null, '/:slug/:edit?');
staticPostPermalink,
oldRoute,
dummyRouter = require('express').Router();
// Overload this dummyRouter as we only want the layer object.
// We don't want to keep in memory many items in an array so we
// clear the stack array after every invocation.
oldRoute = dummyRouter.route;
dummyRouter.route = function () {
var layer;
// Apply old route method
oldRoute.apply(dummyRouter, arguments);
// Grab layer object
layer = dummyRouter.stack[0];
// Reset stack array for memory purposes
dummyRouter.stack = [];
// Return layer
return layer;
};
// Cache static post permalink regex
staticPostPermalink = dummyRouter.route('/:slug/:edit?');
function getPostPage(options) {
return api.settings.read('postsPerPage').then(function (response) {
@ -155,7 +178,7 @@ frontendControllers = {
editFormat = permalink.value[permalink.value.length - 1] === '/' ? ':edit?' : '/:edit?';
// Convert saved permalink into an express Route object
permalink = new Route(null, permalink.value + editFormat);
permalink = dummyRouter.route(permalink.value + editFormat);
// Check if the path matches the permalink structure.
//

View File

@ -19,7 +19,6 @@ var crypto = require('crypto'),
models = require('./models'),
permissions = require('./permissions'),
apps = require('./apps'),
routes = require('./routes'),
packageInfo = require('../../package.json'),
// Variables
@ -265,20 +264,9 @@ function init(server) {
// Load helpers
helpers.loadCoreHelpers(adminHbs, assetHash);
// ## Middleware
// ## Middleware and Routing
middleware(server, dbHash);
// ## Routing
// Set up API routes
routes.api(server);
// Set up Admin routes
routes.admin(server);
// Set up Frontend routes
routes.frontend(server);
// Log all theme errors and warnings
_.each(config().paths.availableThemes._messages.errors, function (error) {
errors.logError(error.message, error.context, error.help);

View File

@ -4,14 +4,20 @@
var api = require('../api'),
BSStore = require('../bookshelf-session'),
bodyParser = require('body-parser'),
config = require('../config'),
cookieParser = require('cookie-parser'),
errors = require('../errors'),
express = require('express'),
favicon = require('static-favicon'),
fs = require('fs'),
hbs = require('express-hbs'),
logger = require('morgan'),
middleware = require('./middleware'),
packageInfo = require('../../../package.json'),
path = require('path'),
routes = require('../routes'),
session = require('express-session'),
slashes = require('connect-slashes'),
storage = require('../storage'),
url = require('url'),
@ -81,45 +87,14 @@ function initThemeData(secure) {
return themeConfig;
}
// ### InitViews Middleware
// Initialise Theme or Admin Views
function initViews(req, res, next) {
/*jslint unparam:true*/
if (!res.isAdmin) {
var themeData = initThemeData(req.secure);
hbs.updateTemplateOptions({ data: {blog: themeData} });
expressServer.engine('hbs', expressServer.get('theme view engine'));
expressServer.set('views', path.join(config().paths.themePath, expressServer.get('activeTheme')));
} else {
expressServer.engine('hbs', expressServer.get('admin view engine'));
expressServer.set('views', config().paths.adminViews);
}
// Pass 'secure' flag to the view engine
// so that templates can choose 'url' vs 'urlSSL'
res.locals.secure = req.secure;
next();
}
// ### Activate Theme
// Helper for manageAdminAndTheme
function activateTheme(activeTheme) {
var hbsOptions,
themePartials = path.join(config().paths.themePath, activeTheme, 'partials'),
stackLocation = _.indexOf(expressServer.stack, _.find(expressServer.stack, function (stackItem) {
return stackItem.route === config().paths.subdir && stackItem.handle.name === 'settingEnabled';
}));
themePartials = path.join(config().paths.themePath, activeTheme, 'partials');
// clear the view cache
expressServer.cache = {};
expressServer.disable(expressServer.get('activeTheme'));
expressServer.set('activeTheme', activeTheme);
expressServer.enable(expressServer.get('activeTheme'));
if (stackLocation) {
expressServer.stack[stackLocation].handle = middleware.whenEnabled(expressServer.get('activeTheme'), middleware.staticTheme());
}
// set view engine
hbsOptions = { partialsDir: [ config().paths.helperTemplates ] };
@ -135,24 +110,43 @@ function activateTheme(activeTheme) {
// Update user error template
errors.updateActiveTheme(activeTheme);
// Set active theme variable on the express server
expressServer.set('activeTheme', activeTheme);
}
// ### ManageAdminAndTheme Middleware
// ### decideContext 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) {
function decideContext(req, res, next) {
res.isAdmin = req.url.lastIndexOf(config().paths.subdir + '/ghost/', 0) === 0;
if (res.isAdmin) {
expressServer.enable('admin');
expressServer.disable(expressServer.get('activeTheme'));
expressServer.engine('hbs', expressServer.get('admin view engine'));
expressServer.set('views', config().paths.adminViews);
} else {
expressServer.enable(expressServer.get('activeTheme'));
expressServer.disable('admin');
var themeData = initThemeData(req.secure);
hbs.updateTemplateOptions({ data: {blog: themeData} });
expressServer.engine('hbs', expressServer.get('theme view engine'));
expressServer.set('views', path.join(config().paths.themePath, expressServer.get('activeTheme')));
}
// Pass 'secure' flag to the view engine
// so that templates can choose 'url' vs 'urlSSL'
res.locals.secure = req.secure;
next();
}
// ### updateActiveTheme
// Updates the expressServer's activeTheme variable and subsequently
// activates that theme's views with the hbs templating engine if it
// is not yet activated.
function updateActiveTheme(req, res, next) {
api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) {
var activeTheme = response.settings[0];
// Check if the theme changed
if (activeTheme.value !== expressServer.get('activeTheme')) {
// Change theme
@ -285,14 +279,14 @@ module.exports = function (server, dbHash) {
// Logging configuration
if (logging !== false) {
if (expressServer.get('env') !== 'development') {
expressServer.use(express.logger(logging || {}));
expressServer.use(logger(logging || {}));
} else {
expressServer.use(express.logger(logging || 'dev'));
expressServer.use(logger(logging || 'dev'));
}
}
// Favicon
expressServer.use(subdir, express.favicon(corePath + '/shared/favicon.ico'));
expressServer.use(subdir, favicon(corePath + '/shared/favicon.ico'));
// Static assets
expressServer.use(subdir + '/shared', express['static'](path.join(corePath, '/shared'), {maxAge: ONE_HOUR_MS}));
@ -301,7 +295,8 @@ module.exports = function (server, dbHash) {
expressServer.use(subdir + '/public', express['static'](path.join(corePath, '/built/public'), {maxAge: ONE_YEAR_MS}));
// First determine whether we're serving admin or theme content
expressServer.use(manageAdminAndTheme);
expressServer.use(updateActiveTheme);
expressServer.use(decideContext);
// Admin only config
expressServer.use(subdir + '/ghost', middleware.whenEnabled('admin', express['static'](path.join(corePath, '/clientold/assets'), {maxAge: ONE_YEAR_MS})));
@ -314,7 +309,7 @@ module.exports = function (server, dbHash) {
expressServer.use(checkSSL);
// Theme only config
expressServer.use(subdir, middleware.whenEnabled(expressServer.get('activeTheme'), middleware.staticTheme()));
expressServer.use(subdir, middleware.staticTheme());
// Serve robots.txt if not found in theme
expressServer.use(robots());
@ -323,8 +318,8 @@ module.exports = function (server, dbHash) {
expressServer.use(slashes(true, {headers: {'Cache-Control': 'public, max-age=' + ONE_YEAR_S}}));
// Body parsing
expressServer.use(express.json());
expressServer.use(express.urlencoded());
expressServer.use(bodyParser.json());
expressServer.use(bodyParser.urlencoded());
// ### Sessions
// we need the trailing slash in the cookie path. Session handling *must* be after the slash handling
@ -339,8 +334,8 @@ module.exports = function (server, dbHash) {
cookie.secure = true;
}
expressServer.use(express.cookieParser());
expressServer.use(express.session({
expressServer.use(cookieParser());
expressServer.use(session({
store: new BSStore(),
proxy: true,
secret: dbHash,
@ -366,11 +361,15 @@ module.exports = function (server, dbHash) {
// ToDo: Remove when ember handles passive notifications.
expressServer.use(middleware.cleanNotifications);
// Initialise the views
expressServer.use(initViews);
// ### Routing
expressServer.use(subdir, expressServer.router);
// Set up API routes
expressServer.use(subdir, routes.api(middleware));
// Set up Admin routes
expressServer.use(subdir, routes.admin(middleware));
// Set up Frontend routes
expressServer.use(subdir, routes.frontend());
// ### Error handling
// 404 Handler

View File

@ -3,6 +3,7 @@
// middleware_spec.js
var _ = require('lodash'),
csrf = require('csurf'),
express = require('express'),
busboy = require('./ghost-busboy'),
config = require('../config'),
@ -185,10 +186,9 @@ var middleware = {
},
conditionalCSRF: function (req, res, next) {
var csrf = express.csrf();
// CSRF is needed for admin only
if (res.isAdmin) {
csrf(req, res, next);
csrf()(req, res, next);
return;
}
next();

View File

@ -1,73 +1,77 @@
var admin = require('../controllers/admin'),
config = require('../config'),
middleware = require('../middleware').middleware,
express = require('express'),
ONE_HOUR_S = 60 * 60,
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
adminRoutes;
adminRoutes = function (server) {
adminRoutes = function (middleware) {
var router = express.Router(),
subdir = config().paths.subdir;
// Have ember route look for hits first
// to prevent conflicts with pre-existing routes
server.get('/ghost/ember/*', middleware.redirectToSignup, admin.index);
router.get('/ghost/ember/*', middleware.redirectToSignup, admin.index);
var subdir = config().paths.subdir;
// ### Admin routes
server.get('/logout/', function redirect(req, res) {
router.get('/logout/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signout/');
});
server.get('/signout/', function redirect(req, res) {
router.get('/signout/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signout/');
});
server.get('/signin/', function redirect(req, res) {
router.get('/signin/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signin/');
});
server.get('/signup/', function redirect(req, res) {
router.get('/signup/', function redirect(req, res) {
/*jslint unparam:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/ghost/signup/');
});
server.get('/ghost/signout/', admin.signout);
server.post('/ghost/signout/', admin.doSignout);
server.get('/ghost/signin/', middleware.redirectToSignup, middleware.redirectToDashboard, admin.signin);
server.post('/ghost/signin/', admin.doSignin);
server.get('/ghost/signup/', middleware.redirectToDashboard, admin.signup);
server.post('/ghost/signup/', admin.doSignup);
server.get('/ghost/forgotten/', middleware.redirectToDashboard, admin.forgotten);
server.post('/ghost/forgotten/', admin.doForgotten);
server.get('/ghost/reset/:token', admin.reset);
server.post('/ghost/reset/:token', admin.doReset);
server.post('/ghost/changepw/', admin.doChangePassword);
router.get('/ghost/signout/', admin.signout);
router.post('/ghost/signout/', admin.doSignout);
router.get('/ghost/signin/', middleware.redirectToSignup, middleware.redirectToDashboard, admin.signin);
router.post('/ghost/signin/', admin.doSignin);
router.get('/ghost/signup/', middleware.redirectToDashboard, admin.signup);
router.post('/ghost/signup/', admin.doSignup);
router.get('/ghost/forgotten/', middleware.redirectToDashboard, admin.forgotten);
router.post('/ghost/forgotten/', admin.doForgotten);
router.get('/ghost/reset/:token', admin.reset);
router.post('/ghost/reset/:token', admin.doReset);
router.post('/ghost/changepw/', admin.doChangePassword);
server.get('/ghost/editor/:id/:action', admin.editor);
server.get('/ghost/editor/:id/', admin.editor);
server.get('/ghost/editor/', admin.editor);
server.get('/ghost/content/', admin.content);
server.get('/ghost/settings*', admin.settings);
server.get('/ghost/debug/', admin.debug.index);
router.get('/ghost/editor/:id/:action', admin.editor);
router.get('/ghost/editor/:id/', admin.editor);
router.get('/ghost/editor/', admin.editor);
router.get('/ghost/content/', admin.content);
router.get('/ghost/settings*', admin.settings);
router.get('/ghost/debug/', admin.debug.index);
server.get('/ghost/export/', admin.debug.exportContent);
router.get('/ghost/export/', admin.debug.exportContent);
server.post('/ghost/upload/', middleware.busboy, admin.upload);
router.post('/ghost/upload/', middleware.busboy, admin.upload);
// redirect to /ghost and let that do the authentication to prevent redirects to /ghost//admin etc.
server.get(/\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)$/, function (req, res) {
router.get(/\/((ghost-admin|admin|wp-admin|dashboard|signin)\/?)$/, function (req, res) {
/*jslint unparam:true*/
res.redirect(subdir + '/ghost/');
});
server.get(/\/ghost$/, function (req, res) {
router.get(/\/ghost$/, function (req, res) {
/*jslint unparam:true*/
res.redirect(subdir + '/ghost/');
});
server.get('/ghost/', admin.indexold);
router.get('/ghost/', admin.indexold);
return router;
};
module.exports = adminRoutes;

View File

@ -1,42 +1,46 @@
// # API routes
var middleware = require('../middleware').middleware,
var express = require('express'),
api = require('../api'),
apiRoutes;
apiRoutes = function (server) {
apiRoutes = function (middleware) {
var router = express.Router();
// ## Posts
server.get('/ghost/api/v0.1/posts', api.http(api.posts.browse));
server.post('/ghost/api/v0.1/posts', api.http(api.posts.add));
server.get('/ghost/api/v0.1/posts/:id(\\d+)', api.http(api.posts.read));
server.get('/ghost/api/v0.1/posts/:slug([a-z-]+)', api.http(api.posts.read));
server.put('/ghost/api/v0.1/posts/:id', api.http(api.posts.edit));
server.del('/ghost/api/v0.1/posts/:id', api.http(api.posts.destroy));
router.get('/ghost/api/v0.1/posts', api.http(api.posts.browse));
router.post('/ghost/api/v0.1/posts', api.http(api.posts.add));
router.get('/ghost/api/v0.1/posts/:id(\\d+)', api.http(api.posts.read));
router.get('/ghost/api/v0.1/posts/:slug([a-z-]+)', api.http(api.posts.read));
router.put('/ghost/api/v0.1/posts/:id', api.http(api.posts.edit));
router['delete']('/ghost/api/v0.1/posts/:id', api.http(api.posts.destroy));
// ## Settings
server.get('/ghost/api/v0.1/settings/', api.http(api.settings.browse));
server.get('/ghost/api/v0.1/settings/:key/', api.http(api.settings.read));
server.put('/ghost/api/v0.1/settings/', api.http(api.settings.edit));
router.get('/ghost/api/v0.1/settings/', api.http(api.settings.browse));
router.get('/ghost/api/v0.1/settings/:key/', api.http(api.settings.read));
router.put('/ghost/api/v0.1/settings/', api.http(api.settings.edit));
// ## Users
server.get('/ghost/api/v0.1/users/', api.http(api.users.browse));
server.get('/ghost/api/v0.1/users/:id/', api.http(api.users.read));
server.put('/ghost/api/v0.1/users/:id/', api.http(api.users.edit));
router.get('/ghost/api/v0.1/users/', api.http(api.users.browse));
router.get('/ghost/api/v0.1/users/:id/', api.http(api.users.read));
router.put('/ghost/api/v0.1/users/:id/', api.http(api.users.edit));
// ## Tags
server.get('/ghost/api/v0.1/tags/', api.http(api.tags.browse));
router.get('/ghost/api/v0.1/tags/', api.http(api.tags.browse));
// ## Themes
server.get('/ghost/api/v0.1/themes/', api.http(api.themes.browse));
server.put('/ghost/api/v0.1/themes/:name', api.http(api.themes.edit));
router.get('/ghost/api/v0.1/themes/', api.http(api.themes.browse));
router.put('/ghost/api/v0.1/themes/:name', api.http(api.themes.edit));
// ## Notifications
server.get('/ghost/api/v0.1/notifications/', api.http(api.notifications.browse));
server.post('/ghost/api/v0.1/notifications/', api.http(api.notifications.add));
server.del('/ghost/api/v0.1/notifications/:id', api.http(api.notifications.destroy));
router.get('/ghost/api/v0.1/notifications/', api.http(api.notifications.browse));
router.post('/ghost/api/v0.1/notifications/', api.http(api.notifications.add));
router['delete']('/ghost/api/v0.1/notifications/:id', api.http(api.notifications.destroy));
// ## DB
server.get('/ghost/api/v0.1/db/', api.http(api.db.exportContent));
server.post('/ghost/api/v0.1/db/', middleware.busboy, api.http(api.db.importContent));
server.del('/ghost/api/v0.1/db/', api.http(api.db.deleteAllContent));
router.get('/ghost/api/v0.1/db/', api.http(api.db.exportContent));
router.post('/ghost/api/v0.1/db/', middleware.busboy, api.http(api.db.importContent));
router['delete']('/ghost/api/v0.1/db/', api.http(api.db.deleteAllContent));
// ## Mail
server.post('/ghost/api/v0.1/mail', api.http(api.mail.send));
server.post('/ghost/api/v0.1/mail/test', api.http(api.mail.sendTest));
router.post('/ghost/api/v0.1/mail', api.http(api.mail.send));
router.post('/ghost/api/v0.1/mail/test', api.http(api.mail.sendTest));
// #### Slugs
server.get('/ghost/api/v0.1/slugs/:type/:name', api.http(api.slugs.generate));
router.get('/ghost/api/v0.1/slugs/:type/:name', api.http(api.slugs.generate));
return router;
};
module.exports = apiRoutes;

View File

@ -1,31 +1,35 @@
var frontend = require('../controllers/frontend'),
config = require('../config'),
express = require('express'),
ONE_HOUR_S = 60 * 60,
ONE_YEAR_S = 365 * 24 * ONE_HOUR_S,
frontendRoutes;
frontendRoutes = function (server) {
var subdir = config().paths.subdir;
frontendRoutes = function () {
var router = express.Router(),
subdir = config().paths.subdir;
// ### Frontend routes
server.get('/rss/', frontend.rss);
server.get('/rss/:page/', frontend.rss);
server.get('/feed/', function redirect(req, res) {
router.get('/rss/', frontend.rss);
router.get('/rss/:page/', frontend.rss);
router.get('/feed/', function redirect(req, res) {
/*jshint unused:true*/
res.set({'Cache-Control': 'public, max-age=' + ONE_YEAR_S});
res.redirect(301, subdir + '/rss/');
});
server.get('/tag/:slug/rss/', frontend.rss);
server.get('/tag/:slug/rss/:page/', frontend.rss);
server.get('/tag/:slug/page/:page/', frontend.tag);
server.get('/tag/:slug/', frontend.tag);
server.get('/page/:page/', frontend.homepage);
server.get('/', frontend.homepage);
server.get('*', frontend.single);
router.get('/tag/:slug/rss/', frontend.rss);
router.get('/tag/:slug/rss/:page/', frontend.rss);
router.get('/tag/:slug/page/:page/', frontend.tag);
router.get('/tag/:slug/', frontend.tag);
router.get('/page/:page/', frontend.homepage);
router.get('/', frontend.homepage);
router.get('*', frontend.single);
return router;
};
module.exports = frontendRoutes;

View File

@ -32,18 +32,24 @@
"engineStrict": true,
"dependencies": {
"bcryptjs": "0.7.10",
"body-parser": "1.0.2",
"bookshelf": "0.6.8",
"busboy": "0.0.12",
"colors": "0.6.2",
"compression": "^1.0.2",
"cookie-parser": "1.0.1",
"connect": "3.0.0-rc.1",
"connect-slashes": "1.2.0",
"csurf": "1.1.0",
"downsize": "0.0.5",
"express": "3.4.6",
"express": "4.1.1",
"express-hbs": "0.7.10",
"express-session": "1.0.4",
"fs-extra": "0.8.1",
"knex": "0.5.8",
"lodash": "2.4.1",
"moment": "2.4.0",
"morgan": "1.0.0",
"node-polyglot": "0.3.0",
"node-uuid": "1.4.1",
"nodemailer": "0.5.13",
@ -51,6 +57,7 @@
"semver": "2.2.1",
"showdown": "https://github.com/ErisDS/showdown/archive/v0.3.2-ghost.tar.gz",
"sqlite3": "2.2.3",
"static-favicon": "1.0.2",
"unidecode": "0.1.3",
"validator": "3.4.0",
"when": "2.7.0",