You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
460 lines
11 KiB
460 lines
11 KiB
|
|
/*! |
|
* Express - response |
|
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca> |
|
* MIT Licensed |
|
*/ |
|
|
|
/** |
|
* Module dependencies. |
|
*/ |
|
|
|
var fs = require('fs') |
|
, http = require('http') |
|
, path = require('path') |
|
, connect = require('connect') |
|
, utils = connect.utils |
|
, parseRange = require('./utils').parseRange |
|
, res = http.ServerResponse.prototype |
|
, send = connect.static.send |
|
, mime = require('mime') |
|
, basename = path.basename |
|
, join = path.join; |
|
|
|
/** |
|
* Send a response with the given `body` and optional `headers` and `status` code. |
|
* |
|
* Examples: |
|
* |
|
* res.send(); |
|
* res.send(new Buffer('wahoo')); |
|
* res.send({ some: 'json' }); |
|
* res.send('<p>some html</p>'); |
|
* res.send('Sorry, cant find that', 404); |
|
* res.send('text', { 'Content-Type': 'text/plain' }, 201); |
|
* res.send(404); |
|
* |
|
* @param {String|Object|Number|Buffer} body or status |
|
* @param {Object|Number} headers or status |
|
* @param {Number} status |
|
* @return {ServerResponse} |
|
* @api public |
|
*/ |
|
|
|
res.send = function(body, headers, status){ |
|
// allow status as second arg |
|
if ('number' == typeof headers) { |
|
status = headers, |
|
headers = null; |
|
} |
|
|
|
// default status |
|
status = status || this.statusCode; |
|
|
|
// allow 0 args as 204 |
|
if (!arguments.length || undefined === body) status = 204; |
|
|
|
// determine content type |
|
switch (typeof body) { |
|
case 'number': |
|
if (!this.header('Content-Type')) { |
|
this.contentType('.txt'); |
|
} |
|
body = http.STATUS_CODES[status = body]; |
|
break; |
|
case 'string': |
|
if (!this.header('Content-Type')) { |
|
this.charset = this.charset || 'utf-8'; |
|
this.contentType('.html'); |
|
} |
|
break; |
|
case 'boolean': |
|
case 'object': |
|
if (Buffer.isBuffer(body)) { |
|
if (!this.header('Content-Type')) { |
|
this.contentType('.bin'); |
|
} |
|
} else { |
|
return this.json(body, headers, status); |
|
} |
|
break; |
|
} |
|
|
|
// populate Content-Length |
|
if (undefined !== body && !this.header('Content-Length')) { |
|
this.header('Content-Length', Buffer.isBuffer(body) |
|
? body.length |
|
: Buffer.byteLength(body)); |
|
} |
|
|
|
// merge headers passed |
|
if (headers) { |
|
var fields = Object.keys(headers); |
|
for (var i = 0, len = fields.length; i < len; ++i) { |
|
var field = fields[i]; |
|
this.header(field, headers[field]); |
|
} |
|
} |
|
|
|
// strip irrelevant headers |
|
if (204 == status || 304 == status) { |
|
this.removeHeader('Content-Type'); |
|
this.removeHeader('Content-Length'); |
|
body = ''; |
|
} |
|
|
|
// respond |
|
this.statusCode = status; |
|
this.end('HEAD' == this.req.method ? null : body); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Send JSON response with `obj`, optional `headers`, and optional `status`. |
|
* |
|
* Examples: |
|
* |
|
* res.json(null); |
|
* res.json({ user: 'tj' }); |
|
* res.json('oh noes!', 500); |
|
* res.json('I dont have that', 404); |
|
* |
|
* @param {Mixed} obj |
|
* @param {Object|Number} headers or status |
|
* @param {Number} status |
|
* @return {ServerResponse} |
|
* @api public |
|
*/ |
|
|
|
res.json = function(obj, headers, status){ |
|
var body = JSON.stringify(obj) |
|
, callback = this.req.query.callback |
|
, jsonp = this.app.enabled('jsonp callback'); |
|
|
|
this.charset = this.charset || 'utf-8'; |
|
this.header('Content-Type', 'application/json'); |
|
|
|
if (callback && jsonp) { |
|
this.header('Content-Type', 'text/javascript'); |
|
body = callback.replace(/[^\w$.]/g, '') + '(' + body + ');'; |
|
} |
|
|
|
return this.send(body, headers, status); |
|
}; |
|
|
|
/** |
|
* Set status `code`. |
|
* |
|
* @param {Number} code |
|
* @return {ServerResponse} |
|
* @api public |
|
*/ |
|
|
|
res.status = function(code){ |
|
this.statusCode = code; |
|
return this; |
|
}; |
|
|
|
/** |
|
* Transfer the file at the given `path`. Automatically sets |
|
* the _Content-Type_ response header field. `next()` is called |
|
* when `path` is a directory, or when an error occurs. |
|
* |
|
* Options: |
|
* |
|
* - `maxAge` defaulting to 0 |
|
* - `root` root directory for relative filenames |
|
* |
|
* @param {String} path |
|
* @param {Object|Function} options or fn |
|
* @param {Function} fn |
|
* @api public |
|
*/ |
|
|
|
res.sendfile = function(path, options, fn){ |
|
var next = this.req.next; |
|
options = options || {}; |
|
|
|
// support function as second arg |
|
if ('function' == typeof options) { |
|
fn = options; |
|
options = {}; |
|
} |
|
|
|
options.path = encodeURIComponent(path); |
|
options.callback = fn; |
|
send(this.req, this, next, options); |
|
}; |
|
|
|
/** |
|
* Set _Content-Type_ response header passed through `mime.lookup()`. |
|
* |
|
* Examples: |
|
* |
|
* var filename = 'path/to/image.png'; |
|
* res.contentType(filename); |
|
* // res.headers['Content-Type'] is now "image/png" |
|
* |
|
* res.contentType('.html'); |
|
* res.contentType('html'); |
|
* res.contentType('json'); |
|
* res.contentType('png'); |
|
* |
|
* @param {String} type |
|
* @return {String} the resolved mime type |
|
* @api public |
|
*/ |
|
|
|
res.contentType = function(type){ |
|
return this.header('Content-Type', mime.lookup(type)); |
|
}; |
|
|
|
/** |
|
* Set _Content-Disposition_ header to _attachment_ with optional `filename`. |
|
* |
|
* @param {String} filename |
|
* @return {ServerResponse} |
|
* @api public |
|
*/ |
|
|
|
res.attachment = function(filename){ |
|
if (filename) this.contentType(filename); |
|
this.header('Content-Disposition', filename |
|
? 'attachment; filename="' + basename(filename) + '"' |
|
: 'attachment'); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Transfer the file at the given `path`, with optional |
|
* `filename` as an attachment and optional callback `fn(err)`, |
|
* and optional `fn2(err)` which is invoked when an error has |
|
* occurred after header has been sent. |
|
* |
|
* @param {String} path |
|
* @param {String|Function} filename or fn |
|
* @param {Function} fn |
|
* @param {Function} fn2 |
|
* @api public |
|
*/ |
|
|
|
res.download = function(path, filename, fn, fn2){ |
|
var self = this; |
|
|
|
// support callback as second arg |
|
if ('function' == typeof filename) { |
|
fn2 = fn; |
|
fn = filename; |
|
filename = null; |
|
} |
|
|
|
// transfer the file |
|
this.attachment(filename || path).sendfile(path, function(err){ |
|
var sentHeader = self._header; |
|
if (err) { |
|
if (!sentHeader) self.removeHeader('Content-Disposition'); |
|
if (sentHeader) { |
|
fn2 && fn2(err); |
|
} else if (fn) { |
|
fn(err); |
|
} else { |
|
self.req.next(err); |
|
} |
|
} else if (fn) { |
|
fn(); |
|
} |
|
}); |
|
}; |
|
|
|
/** |
|
* Set or get response header `name` with optional `val`. |
|
* |
|
* @param {String} name |
|
* @param {String} val |
|
* @return {ServerResponse} for chaining |
|
* @api public |
|
*/ |
|
|
|
res.header = function(name, val){ |
|
if (1 == arguments.length) return this.getHeader(name); |
|
this.setHeader(name, val); |
|
return this; |
|
}; |
|
|
|
/** |
|
* Clear cookie `name`. |
|
* |
|
* @param {String} name |
|
* @param {Object} options |
|
* @api public |
|
*/ |
|
|
|
res.clearCookie = function(name, options){ |
|
var opts = { expires: new Date(1) }; |
|
this.cookie(name, '', options |
|
? utils.merge(options, opts) |
|
: opts); |
|
}; |
|
|
|
/** |
|
* Set cookie `name` to `val`, with the given `options`. |
|
* |
|
* Options: |
|
* |
|
* - `maxAge` max-age in milliseconds, converted to `expires` |
|
* - `path` defaults to the "basepath" setting which is typically "/" |
|
* |
|
* Examples: |
|
* |
|
* // "Remember Me" for 15 minutes |
|
* res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); |
|
* |
|
* // save as above |
|
* res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) |
|
* |
|
* @param {String} name |
|
* @param {String} val |
|
* @param {Options} options |
|
* @api public |
|
*/ |
|
|
|
res.cookie = function(name, val, options){ |
|
options = options || {}; |
|
if ('maxAge' in options) options.expires = new Date(Date.now() + options.maxAge); |
|
if (undefined === options.path) options.path = this.app.set('basepath'); |
|
var cookie = utils.serializeCookie(name, val, options); |
|
this.header('Set-Cookie', cookie); |
|
}; |
|
|
|
/** |
|
* Redirect to the given `url` with optional response `status` |
|
* defauling to 302. |
|
* |
|
* The given `url` can also be the name of a mapped url, for |
|
* example by default express supports "back" which redirects |
|
* to the _Referrer_ or _Referer_ headers or the application's |
|
* "basepath" setting. Express also supports "basepath" out of the box, |
|
* which can be set via `app.set('basepath', '/blog');`, and defaults |
|
* to '/'. |
|
* |
|
* Redirect Mapping: |
|
* |
|
* To extend the redirect mapping capabilities that Express provides, |
|
* we may use the `app.redirect()` method: |
|
* |
|
* app.redirect('google', 'http://google.com'); |
|
* |
|
* Now in a route we may call: |
|
* |
|
* res.redirect('google'); |
|
* |
|
* We may also map dynamic redirects: |
|
* |
|
* app.redirect('comments', function(req, res){ |
|
* return '/post/' + req.params.id + '/comments'; |
|
* }); |
|
* |
|
* So now we may do the following, and the redirect will dynamically adjust to |
|
* the context of the request. If we called this route with _GET /post/12_ our |
|
* redirect _Location_ would be _/post/12/comments_. |
|
* |
|
* app.get('/post/:id', function(req, res){ |
|
* res.redirect('comments'); |
|
* }); |
|
* |
|
* Unless an absolute `url` is given, the app's mount-point |
|
* will be respected. For example if we redirect to `/posts`, |
|
* and our app is mounted at `/blog` we will redirect to `/blog/posts`. |
|
* |
|
* @param {String} url |
|
* @param {Number} code |
|
* @api public |
|
*/ |
|
|
|
res.redirect = function(url, status){ |
|
var app = this.app |
|
, req = this.req |
|
, base = app.set('basepath') || app.route |
|
, status = status || 302 |
|
, head = 'HEAD' == req.method |
|
, body; |
|
|
|
// Setup redirect map |
|
var map = { |
|
back: req.header('Referrer', base) |
|
, home: base |
|
}; |
|
|
|
// Support custom redirect map |
|
map.__proto__ = app.redirects; |
|
|
|
// Attempt mapped redirect |
|
var mapped = 'function' == typeof map[url] |
|
? map[url](req, this) |
|
: map[url]; |
|
|
|
// Perform redirect |
|
url = mapped || url; |
|
|
|
// Relative |
|
if (!~url.indexOf('://')) { |
|
// Respect mount-point |
|
if ('/' != base && 0 != url.indexOf(base)) url = base + url; |
|
|
|
// Absolute |
|
var host = req.headers.host |
|
, tls = req.connection.encrypted; |
|
url = 'http' + (tls ? 's' : '') + '://' + host + url; |
|
} |
|
|
|
// Support text/{plain,html} by default |
|
if (req.accepts('html')) { |
|
body = '<p>' + http.STATUS_CODES[status] + '. Redirecting to <a href="' + url + '">' + url + '</a></p>'; |
|
this.header('Content-Type', 'text/html'); |
|
} else { |
|
body = http.STATUS_CODES[status] + '. Redirecting to ' + url; |
|
this.header('Content-Type', 'text/plain'); |
|
} |
|
|
|
// Respond |
|
this.statusCode = status; |
|
this.header('Location', url); |
|
this.end(head ? null : body); |
|
}; |
|
|
|
/** |
|
* Assign the view local variable `name` to `val` or return the |
|
* local previously assigned to `name`. |
|
* |
|
* @param {String} name |
|
* @param {Mixed} val |
|
* @return {Mixed} val |
|
* @api public |
|
*/ |
|
|
|
res.local = function(name, val){ |
|
this._locals = this._locals || {}; |
|
return undefined === val |
|
? this._locals[name] |
|
: this._locals[name] = val; |
|
}; |
|
|
|
/** |
|
* Assign several locals with the given `obj`, |
|
* or return the locals. |
|
* |
|
* @param {Object} obj |
|
* @return {Object|Undefined} |
|
* @api public |
|
*/ |
|
|
|
res.locals = |
|
res.helpers = function(obj){ |
|
if (obj) { |
|
for (var key in obj) { |
|
this.local(key, obj[key]); |
|
} |
|
} else { |
|
return this._locals; |
|
} |
|
};
|
|
|