|
|
/*! * express * Copyright(c) 2009-2013 TJ Holowaychuk * Copyright(c) 2013 Roman Shtylman * Copyright(c) 2014-2015 Douglas Christopher Wilson * MIT Licensed */
'use strict';
/** * Module dependencies. * @private */
var Route = require('./route'); var Layer = require('./layer'); var methods = require('methods'); var mixin = require('utils-merge'); var debug = require('debug')('express:router'); var deprecate = require('depd')('express'); var flatten = require('array-flatten'); var parseUrl = require('parseurl'); var setPrototypeOf = require('setprototypeof')
/** * Module variables. * @private */
var objectRegExp = /^\[object (\S+)\]$/; var slice = Array.prototype.slice; var toString = Object.prototype.toString;
/** * Initialize a new `Router` with the given `options`. * * @param {Object} [options] * @return {Router} which is an callable function * @public */
var proto = module.exports = function(options) { var opts = options || {};
function router(req, res, next) { router.handle(req, res, next); }
// mixin Router class functions
setPrototypeOf(router, proto)
router.params = {}; router._params = []; router.caseSensitive = opts.caseSensitive; router.mergeParams = opts.mergeParams; router.strict = opts.strict; router.stack = [];
return router; };
/** * Map the given param placeholder `name`(s) to the given callback. * * Parameter mapping is used to provide pre-conditions to routes * which use normalized placeholders. For example a _:user_id_ parameter * could automatically load a user's information from the database without * any additional code, * * The callback uses the same signature as middleware, the only difference * being that the value of the placeholder is passed, in this case the _id_ * of the user. Once the `next()` function is invoked, just like middleware * it will continue on to execute the route, or subsequent parameter functions. * * Just like in middleware, you must either respond to the request or call next * to avoid stalling the request. * * app.param('user_id', function(req, res, next, id){ * User.find(id, function(err, user){ * if (err) { * return next(err); * } else if (!user) { * return next(new Error('failed to load user')); * } * req.user = user; * next(); * }); * }); * * @param {String} name * @param {Function} fn * @return {app} for chaining * @public */
proto.param = function param(name, fn) { // param logic
if (typeof name === 'function') { deprecate('router.param(fn): Refactor to use path params'); this._params.push(name); return; }
// apply param functions
var params = this._params; var len = params.length; var ret;
if (name[0] === ':') { deprecate('router.param(' + JSON.stringify(name) + ', fn): Use router.param(' + JSON.stringify(name.substr(1)) + ', fn) instead'); name = name.substr(1); }
for (var i = 0; i < len; ++i) { if (ret = params[i](name, fn)) { fn = ret; } }
// ensure we end up with a
// middleware function
if ('function' !== typeof fn) { throw new Error('invalid param() call for ' + name + ', got ' + fn); }
(this.params[name] = this.params[name] || []).push(fn); return this; };
/** * Dispatch a req, res into the router. * @private */
proto.handle = function handle(req, res, out) { var self = this;
debug('dispatching %s %s', req.method, req.url);
var idx = 0; var protohost = getProtohost(req.url) || '' var removed = ''; var slashAdded = false; var paramcalled = {};
// store options for OPTIONS request
// only used if OPTIONS request
var options = [];
// middleware and routes
var stack = self.stack;
// manage inter-router variables
var parentParams = req.params; var parentUrl = req.baseUrl || ''; var done = restore(out, req, 'baseUrl', 'next', 'params');
// setup next layer
req.next = next;
// for options requests, respond with a default if nothing else responds
if (req.method === 'OPTIONS') { done = wrap(done, function(old, err) { if (err || options.length === 0) return old(err); sendOptionsResponse(res, options, old); }); }
// setup basic req values
req.baseUrl = parentUrl; req.originalUrl = req.originalUrl || req.url;
next();
function next(err) { var layerError = err === 'route' ? null : err;
// remove added slash
if (slashAdded) { req.url = req.url.substr(1); slashAdded = false; }
// restore altered req.url
if (removed.length !== 0) { req.baseUrl = parentUrl; req.url = protohost + removed + req.url.substr(protohost.length); removed = ''; }
// signal to exit router
if (layerError === 'router') { setImmediate(done, null) return }
// no more matching layers
if (idx >= stack.length) { setImmediate(done, layerError); return; }
// get pathname of request
var path = getPathname(req);
if (path == null) { return done(layerError); }
// find next matching layer
var layer; var match; var route;
while (match !== true && idx < stack.length) { layer = stack[idx++]; match = matchLayer(layer, path); route = layer.route;
if (typeof match !== 'boolean') { // hold on to layerError
layerError = layerError || match; }
if (match !== true) { continue; }
if (!route) { // process non-route handlers normally
continue; }
if (layerError) { // routes do not match with a pending error
match = false; continue; }
var method = req.method; var has_method = route._handles_method(method);
// build up automatic options response
if (!has_method && method === 'OPTIONS') { appendMethods(options, route._options()); }
// don't even bother matching route
if (!has_method && method !== 'HEAD') { match = false; continue; } }
// no match
if (match !== true) { return done(layerError); }
// store route for dispatch on change
if (route) { req.route = route; }
// Capture one-time layer values
req.params = self.mergeParams ? mergeParams(layer.params, parentParams) : layer.params; var layerPath = layer.path;
// this should be done for the layer
self.process_params(layer, paramcalled, req, res, function (err) { if (err) { return next(layerError || err); }
if (route) { return layer.handle_request(req, res, next); }
trim_prefix(layer, layerError, layerPath, path); }); }
function trim_prefix(layer, layerError, layerPath, path) { if (layerPath.length !== 0) { // Validate path breaks on a path separator
var c = path[layerPath.length] if (c && c !== '/' && c !== '.') return next(layerError)
// Trim off the part of the url that matches the route
// middleware (.use stuff) needs to have the path stripped
debug('trim prefix (%s) from url %s', layerPath, req.url); removed = layerPath; req.url = protohost + req.url.substr(protohost.length + removed.length);
// Ensure leading slash
if (!protohost && req.url[0] !== '/') { req.url = '/' + req.url; slashAdded = true; }
// Setup base URL (no trailing slash)
req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' ? removed.substring(0, removed.length - 1) : removed); }
debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
if (layerError) { layer.handle_error(layerError, req, res, next); } else { layer.handle_request(req, res, next); } } };
/** * Process any parameters for the layer. * @private */
proto.process_params = function process_params(layer, called, req, res, done) { var params = this.params;
// captured parameters from the layer, keys and values
var keys = layer.keys;
// fast track
if (!keys || keys.length === 0) { return done(); }
var i = 0; var name; var paramIndex = 0; var key; var paramVal; var paramCallbacks; var paramCalled;
// process params in order
// param callbacks can be async
function param(err) { if (err) { return done(err); }
if (i >= keys.length ) { return done(); }
paramIndex = 0; key = keys[i++]; name = key.name; paramVal = req.params[name]; paramCallbacks = params[name]; paramCalled = called[name];
if (paramVal === undefined || !paramCallbacks) { return param(); }
// param previously called with same value or error occurred
if (paramCalled && (paramCalled.match === paramVal || (paramCalled.error && paramCalled.error !== 'route'))) { // restore value
req.params[name] = paramCalled.value;
// next param
return param(paramCalled.error); }
called[name] = paramCalled = { error: null, match: paramVal, value: paramVal };
paramCallback(); }
// single param callbacks
function paramCallback(err) { var fn = paramCallbacks[paramIndex++];
// store updated value
paramCalled.value = req.params[key.name];
if (err) { // store error
paramCalled.error = err; param(err); return; }
if (!fn) return param();
try { fn(req, res, paramCallback, paramVal, key.name); } catch (e) { paramCallback(e); } }
param(); };
/** * Use the given middleware function, with optional path, defaulting to "/". * * Use (like `.all`) will run for any http METHOD, but it will not add * handlers for those methods so OPTIONS requests will not consider `.use` * functions even if they could respond. * * The other difference is that _route_ path is stripped and not visible * to the handler function. The main effect of this feature is that mounted * handlers can operate without any code changes regardless of the "prefix" * pathname. * * @public */
proto.use = function use(fn) { var offset = 0; var path = '/';
// default path to '/'
// disambiguate router.use([fn])
if (typeof fn !== 'function') { var arg = fn;
while (Array.isArray(arg) && arg.length !== 0) { arg = arg[0]; }
// first arg is the path
if (typeof arg !== 'function') { offset = 1; path = fn; } }
var callbacks = flatten(slice.call(arguments, offset));
if (callbacks.length === 0) { throw new TypeError('Router.use() requires a middleware function') }
for (var i = 0; i < callbacks.length; i++) { var fn = callbacks[i];
if (typeof fn !== 'function') { throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn)) }
// add the middleware
debug('use %o %s', path, fn.name || '<anonymous>')
var layer = new Layer(path, { sensitive: this.caseSensitive, strict: false, end: false }, fn);
layer.route = undefined;
this.stack.push(layer); }
return this; };
/** * Create a new Route for the given path. * * Each route contains a separate middleware stack and VERB handlers. * * See the Route api documentation for details on adding handlers * and middleware to routes. * * @param {String} path * @return {Route} * @public */
proto.route = function route(path) { var route = new Route(path);
var layer = new Layer(path, { sensitive: this.caseSensitive, strict: this.strict, end: true }, route.dispatch.bind(route));
layer.route = route;
this.stack.push(layer); return route; };
// create Router#VERB functions
methods.concat('all').forEach(function(method){ proto[method] = function(path){ var route = this.route(path) route[method].apply(route, slice.call(arguments, 1)); return this; }; });
// append methods to a list of methods
function appendMethods(list, addition) { for (var i = 0; i < addition.length; i++) { var method = addition[i]; if (list.indexOf(method) === -1) { list.push(method); } } }
// get pathname of request
function getPathname(req) { try { return parseUrl(req).pathname; } catch (err) { return undefined; } }
// Get get protocol + host for a URL
function getProtohost(url) { if (typeof url !== 'string' || url.length === 0 || url[0] === '/') { return undefined }
var searchIndex = url.indexOf('?') var pathLength = searchIndex !== -1 ? searchIndex : url.length var fqdnIndex = url.substr(0, pathLength).indexOf('://')
return fqdnIndex !== -1 ? url.substr(0, url.indexOf('/', 3 + fqdnIndex)) : undefined }
// get type for error message
function gettype(obj) { var type = typeof obj;
if (type !== 'object') { return type; }
// inspect [[Class]] for objects
return toString.call(obj) .replace(objectRegExp, '$1'); }
/** * Match path to a layer. * * @param {Layer} layer * @param {string} path * @private */
function matchLayer(layer, path) { try { return layer.match(path); } catch (err) { return err; } }
// merge params with parent params
function mergeParams(params, parent) { if (typeof parent !== 'object' || !parent) { return params; }
// make copy of parent for base
var obj = mixin({}, parent);
// simple non-numeric merging
if (!(0 in params) || !(0 in parent)) { return mixin(obj, params); }
var i = 0; var o = 0;
// determine numeric gaps
while (i in params) { i++; }
while (o in parent) { o++; }
// offset numeric indices in params before merge
for (i--; i >= 0; i--) { params[i + o] = params[i];
// create holes for the merge when necessary
if (i < o) { delete params[i]; } }
return mixin(obj, params); }
// restore obj props after function
function restore(fn, obj) { var props = new Array(arguments.length - 2); var vals = new Array(arguments.length - 2);
for (var i = 0; i < props.length; i++) { props[i] = arguments[i + 2]; vals[i] = obj[props[i]]; }
return function () { // restore vals
for (var i = 0; i < props.length; i++) { obj[props[i]] = vals[i]; }
return fn.apply(this, arguments); }; }
// send an OPTIONS response
function sendOptionsResponse(res, options, next) { try { var body = options.join(','); res.set('Allow', body); res.send(body); } catch (err) { next(err); } }
// wrap a function
function wrap(old, fn) { return function proxy() { var args = new Array(arguments.length + 1);
args[0] = old; for (var i = 0, len = arguments.length; i < len; i++) { args[i + 1] = arguments[i]; }
fn.apply(this, args); }; }
|