Spaces:
Sleeping
Sleeping
/*! | |
* express | |
* Copyright(c) 2009-2013 TJ Holowaychuk | |
* Copyright(c) 2013 Roman Shtylman | |
* Copyright(c) 2014-2015 Douglas Christopher Wilson | |
* MIT Licensed | |
*/ | |
; | |
/** | |
* 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 a 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.slice(1)) + ', fn) instead') | |
name = name.slice(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 sync = 0 | |
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.slice(1) | |
slashAdded = false; | |
} | |
// restore altered req.url | |
if (removed.length !== 0) { | |
req.baseUrl = parentUrl; | |
req.url = protohost + removed + req.url.slice(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; | |
} | |
// max sync stack | |
if (++sync > 100) { | |
return setImmediate(next, err) | |
} | |
// 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; | |
} | |
} | |
// 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) { | |
next(layerError || err) | |
} else if (route) { | |
layer.handle_request(req, res, next) | |
} else { | |
trim_prefix(layer, layerError, layerPath, path) | |
} | |
sync = 0 | |
}); | |
} | |
function trim_prefix(layer, layerError, layerPath, path) { | |
if (layerPath.length !== 0) { | |
// Validate path is a prefix match | |
if (layerPath !== path.slice(0, layerPath.length)) { | |
next(layerError) | |
return | |
} | |
// 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.slice(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.slice(0, pathLength).indexOf('://') | |
return fqdnIndex !== -1 | |
? url.substring(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); | |
}; | |
} | |