123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757 |
- /**
- * Module dependencies.
- */
- var http = require('http')
- , path = require('path')
- , connect = require('connect')
- , utils = connect.utils
- , sign = require('cookie-signature').sign
- , normalizeType = require('./utils').normalizeType
- , normalizeTypes = require('./utils').normalizeTypes
- , etag = require('./utils').etag
- , statusCodes = http.STATUS_CODES
- , cookie = require('cookie')
- , send = require('send')
- , mime = connect.mime
- , basename = path.basename
- , extname = path.extname
- , join = path.join;
- /**
- * Response prototype.
- */
- var res = module.exports = {
- __proto__: http.ServerResponse.prototype
- };
- /**
- * Set status `code`.
- *
- * @param {Number} code
- * @return {ServerResponse}
- * @api public
- */
- res.status = function(code){
- this.statusCode = code;
- return this;
- };
- /**
- * Set Link header field with the given `links`.
- *
- * Examples:
- *
- * res.links({
- * next: 'http://api.example.com/users?page=2',
- * last: 'http://api.example.com/users?page=5'
- * });
- *
- * @param {Object} links
- * @return {ServerResponse}
- * @api public
- */
- res.links = function(links){
- return this.set('Link', Object.keys(links).map(function(rel){
- return '<' + links[rel] + '>; rel="' + rel + '"';
- }).join(', '));
- };
- /**
- * Send a response.
- *
- * Examples:
- *
- * res.send(new Buffer('wahoo'));
- * res.send({ some: 'json' });
- * res.send('<p>some html</p>');
- * res.send(404, 'Sorry, cant find that');
- * res.send(404);
- *
- * @param {Mixed} body or status
- * @param {Mixed} body
- * @return {ServerResponse}
- * @api public
- */
- res.send = function(body){
- var req = this.req;
- var head = 'HEAD' == req.method;
- var len;
- // allow status / body
- if (2 == arguments.length) {
- // res.send(body, status) backwards compat
- if ('number' != typeof body && 'number' == typeof arguments[1]) {
- this.statusCode = arguments[1];
- } else {
- this.statusCode = body;
- body = arguments[1];
- }
- }
- switch (typeof body) {
- // response status
- case 'number':
- this.get('Content-Type') || this.type('txt');
- this.statusCode = body;
- body = http.STATUS_CODES[body];
- break;
- // string defaulting to html
- case 'string':
- if (!this.get('Content-Type')) {
- this.charset = this.charset || 'utf-8';
- this.type('html');
- }
- break;
- case 'boolean':
- case 'object':
- if (null == body) {
- body = '';
- } else if (Buffer.isBuffer(body)) {
- this.get('Content-Type') || this.type('bin');
- } else {
- return this.json(body);
- }
- break;
- }
- // populate Content-Length
- if (undefined !== body && !this.get('Content-Length')) {
- this.set('Content-Length', len = Buffer.isBuffer(body)
- ? body.length
- : Buffer.byteLength(body));
- }
- // ETag support
- // TODO: W/ support
- if (len > 1024 && 'GET' == req.method) {
- if (!this.get('ETag')) {
- this.set('ETag', etag(body));
- }
- }
- // freshness
- if (req.fresh) this.statusCode = 304;
- // strip irrelevant headers
- if (204 == this.statusCode || 304 == this.statusCode) {
- this.removeHeader('Content-Type');
- this.removeHeader('Content-Length');
- this.removeHeader('Transfer-Encoding');
- body = '';
- }
- // respond
- this.end(head ? null : body);
- return this;
- };
- /**
- * Send JSON response.
- *
- * Examples:
- *
- * res.json(null);
- * res.json({ user: 'tj' });
- * res.json(500, 'oh noes!');
- * res.json(404, 'I dont have that');
- *
- * @param {Mixed} obj or status
- * @param {Mixed} obj
- * @return {ServerResponse}
- * @api public
- */
- res.json = function(obj){
- // allow status / body
- if (2 == arguments.length) {
- // res.json(body, status) backwards compat
- if ('number' == typeof arguments[1]) {
- this.statusCode = arguments[1];
- } else {
- this.statusCode = obj;
- obj = arguments[1];
- }
- }
- // settings
- var app = this.app;
- var replacer = app.get('json replacer');
- var spaces = app.get('json spaces');
- var body = JSON.stringify(obj, replacer, spaces);
- // content-type
- this.get('Content-Type') || this.set('Content-Type', 'application/json');
- return this.send(body);
- };
- /**
- * Send JSON response with JSONP callback support.
- *
- * Examples:
- *
- * res.jsonp(null);
- * res.jsonp({ user: 'tj' });
- * res.jsonp(500, 'oh noes!');
- * res.jsonp(404, 'I dont have that');
- *
- * @param {Mixed} obj or status
- * @param {Mixed} obj
- * @return {ServerResponse}
- * @api public
- */
- res.jsonp = function(obj){
- // allow status / body
- if (2 == arguments.length) {
- // res.json(body, status) backwards compat
- if ('number' == typeof arguments[1]) {
- this.statusCode = arguments[1];
- } else {
- this.statusCode = obj;
- obj = arguments[1];
- }
- }
- // settings
- var app = this.app;
- var replacer = app.get('json replacer');
- var spaces = app.get('json spaces');
- var body = JSON.stringify(obj, replacer, spaces)
- .replace(/\u2028/g, '\\u2028')
- .replace(/\u2029/g, '\\u2029');
- var callback = this.req.query[app.get('jsonp callback name')];
- // content-type
- this.charset = this.charset || 'utf-8';
- this.set('Content-Type', 'application/json');
- // jsonp
- if (callback) {
- if (callback instanceof Array) callback = callback[0];
- this.set('Content-Type', 'text/javascript');
- var cb = callback.replace(/[^\[\]\w$.]/g, '');
- body = cb + ' && ' + cb + '(' + body + ');';
- }
- return this.send(body);
- };
- /**
- * Transfer the file at the given `path`.
- *
- * Automatically sets the _Content-Type_ response header field.
- * The callback `fn(err)` is invoked when the transfer is complete
- * or when an error occurs. Be sure to check `res.sentHeader`
- * if you wish to attempt responding, as the header and some data
- * may have already been transferred.
- *
- * Options:
- *
- * - `maxAge` defaulting to 0
- * - `root` root directory for relative filenames
- *
- * Examples:
- *
- * The following example illustrates how `res.sendfile()` may
- * be used as an alternative for the `static()` middleware for
- * dynamic situations. The code backing `res.sendfile()` is actually
- * the same code, so HTTP cache support etc is identical.
- *
- * app.get('/user/:uid/photos/:file', function(req, res){
- * var uid = req.params.uid
- * , file = req.params.file;
- *
- * req.user.mayViewFilesFrom(uid, function(yes){
- * if (yes) {
- * res.sendfile('/uploads/' + uid + '/' + file);
- * } else {
- * res.send(403, 'Sorry! you cant see that.');
- * }
- * });
- * });
- *
- * @param {String} path
- * @param {Object|Function} options or fn
- * @param {Function} fn
- * @api public
- */
- res.sendfile = function(path, options, fn){
- var self = this
- , req = self.req
- , next = this.req.next
- , options = options || {}
- , done;
- // support function as second arg
- if ('function' == typeof options) {
- fn = options;
- options = {};
- }
- // socket errors
- req.socket.on('error', error);
- // errors
- function error(err) {
- if (done) return;
- done = true;
- // clean up
- cleanup();
- if (!self.headerSent) self.removeHeader('Content-Disposition');
- // callback available
- if (fn) return fn(err);
- // list in limbo if there's no callback
- if (self.headerSent) return;
- // delegate
- next(err);
- }
- // streaming
- function stream() {
- if (done) return;
- cleanup();
- if (fn) self.on('finish', fn);
- }
- // cleanup
- function cleanup() {
- req.socket.removeListener('error', error);
- }
- // transfer
- var file = send(req, path);
- if (options.root) file.root(options.root);
- file.maxage(options.maxAge || 0);
- file.on('error', error);
- file.on('directory', next);
- file.on('stream', stream);
- file.pipe(this);
- this.on('finish', cleanup);
- };
- /**
- * Transfer the file at the given `path` as an attachment.
- *
- * Optionally providing an alternate attachment `filename`,
- * and optional callback `fn(err)`. The callback is invoked
- * when the data transfer is complete, or when an error has
- * ocurred. Be sure to check `res.headerSent` if you plan to respond.
- *
- * This method uses `res.sendfile()`.
- *
- * @param {String} path
- * @param {String|Function} filename or fn
- * @param {Function} fn
- * @api public
- */
- res.download = function(path, filename, fn){
- // support function as second arg
- if ('function' == typeof filename) {
- fn = filename;
- filename = null;
- }
- filename = filename || path;
- this.set('Content-Disposition', 'attachment; filename="' + basename(filename) + '"');
- return this.sendfile(path, fn);
- };
- /**
- * Set _Content-Type_ response header with `type` through `mime.lookup()`
- * when it does not contain "/", or set the Content-Type to `type` otherwise.
- *
- * Examples:
- *
- * res.type('.html');
- * res.type('html');
- * res.type('json');
- * res.type('application/json');
- * res.type('png');
- *
- * @param {String} type
- * @return {ServerResponse} for chaining
- * @api public
- */
- res.contentType =
- res.type = function(type){
- return this.set('Content-Type', ~type.indexOf('/')
- ? type
- : mime.lookup(type));
- };
- /**
- * Respond to the Acceptable formats using an `obj`
- * of mime-type callbacks.
- *
- * This method uses `req.accepted`, an array of
- * acceptable types ordered by their quality values.
- * When "Accept" is not present the _first_ callback
- * is invoked, otherwise the first match is used. When
- * no match is performed the server responds with
- * 406 "Not Acceptable".
- *
- * Content-Type is set for you, however if you choose
- * you may alter this within the callback using `res.type()`
- * or `res.set('Content-Type', ...)`.
- *
- * res.format({
- * 'text/plain': function(){
- * res.send('hey');
- * },
- *
- * 'text/html': function(){
- * res.send('<p>hey</p>');
- * },
- *
- * 'appliation/json': function(){
- * res.send({ message: 'hey' });
- * }
- * });
- *
- * In addition to canonicalized MIME types you may
- * also use extnames mapped to these types:
- *
- * res.format({
- * text: function(){
- * res.send('hey');
- * },
- *
- * html: function(){
- * res.send('<p>hey</p>');
- * },
- *
- * json: function(){
- * res.send({ message: 'hey' });
- * }
- * });
- *
- * By default Express passes an `Error`
- * with a `.status` of 406 to `next(err)`
- * if a match is not made. If you provide
- * a `.default` callback it will be invoked
- * instead.
- *
- * @param {Object} obj
- * @return {ServerResponse} for chaining
- * @api public
- */
- res.format = function(obj){
- var req = this.req
- , next = req.next;
- var fn = obj.default;
- if (fn) delete obj.default;
- var keys = Object.keys(obj);
- var key = req.accepts(keys);
- this.set('Vary', 'Accept');
- if (key) {
- this.set('Content-Type', normalizeType(key).value);
- obj[key](req, this, next);
- } else if (fn) {
- fn();
- } else {
- var err = new Error('Not Acceptable');
- err.status = 406;
- err.types = normalizeTypes(keys).map(function(o){ return o.value });
- next(err);
- }
- return this;
- };
- /**
- * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
- *
- * @param {String} filename
- * @return {ServerResponse}
- * @api public
- */
- res.attachment = function(filename){
- if (filename) this.type(extname(filename));
- this.set('Content-Disposition', filename
- ? 'attachment; filename="' + basename(filename) + '"'
- : 'attachment');
- return this;
- };
- /**
- * Set header `field` to `val`, or pass
- * an object of header fields.
- *
- * Examples:
- *
- * res.set('Foo', ['bar', 'baz']);
- * res.set('Accept', 'application/json');
- * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
- *
- * Aliased as `res.header()`.
- *
- * @param {String|Object|Array} field
- * @param {String} val
- * @return {ServerResponse} for chaining
- * @api public
- */
- res.set =
- res.header = function(field, val){
- if (2 == arguments.length) {
- if (Array.isArray(val)) val = val.map(String);
- else val = String(val);
- this.setHeader(field, val);
- } else {
- for (var key in field) {
- this.set(key, field[key]);
- }
- }
- return this;
- };
- /**
- * Get value for header `field`.
- *
- * @param {String} field
- * @return {String}
- * @api public
- */
- res.get = function(field){
- return this.getHeader(field);
- };
- /**
- * Clear cookie `name`.
- *
- * @param {String} name
- * @param {Object} options
- * @param {ServerResponse} for chaining
- * @api public
- */
- res.clearCookie = function(name, options){
- var opts = { expires: new Date(1), path: '/' };
- return this.cookie(name, '', options
- ? utils.merge(opts, options)
- : opts);
- };
- /**
- * Set cookie `name` to `val`, with the given `options`.
- *
- * Options:
- *
- * - `maxAge` max-age in milliseconds, converted to `expires`
- * - `signed` sign the cookie
- * - `path` defaults to "/"
- *
- * 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|Object} val
- * @param {Options} options
- * @api public
- */
- res.cookie = function(name, val, options){
- options = utils.merge({}, options);
- var secret = this.req.secret;
- var signed = options.signed;
- if (signed && !secret) throw new Error('connect.cookieParser("secret") required for signed cookies');
- if ('number' == typeof val) val = val.toString();
- if ('object' == typeof val) val = 'j:' + JSON.stringify(val);
- if (signed) val = 's:' + sign(val, secret);
- if ('maxAge' in options) {
- options.expires = new Date(Date.now() + options.maxAge);
- options.maxAge /= 1000;
- }
- if (null == options.path) options.path = '/';
- this.set('Set-Cookie', cookie.serialize(name, String(val), options));
- return this;
- };
- /**
- * Set the location header to `url`.
- *
- * 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 "/".
- *
- * Examples:
- *
- * res.location('/foo/bar').;
- * res.location('http://example.com');
- * res.location('../login'); // /blog/post/1 -> /blog/login
- *
- * Mounting:
- *
- * When an application is mounted and `res.location()`
- * is given a path that does _not_ lead with "/" it becomes
- * relative to the mount-point. For example if the application
- * is mounted at "/blog", the following would become "/blog/login".
- *
- * res.location('login');
- *
- * While the leading slash would result in a location of "/login":
- *
- * res.location('/login');
- *
- * @param {String} url
- * @api public
- */
- res.location = function(url){
- var app = this.app
- , req = this.req;
- // setup redirect map
- var map = { back: req.get('Referrer') || '/' };
- // perform redirect
- url = map[url] || url;
- // relative
- if (!~url.indexOf('://') && 0 != url.indexOf('//')) {
- var path
- // relative to path
- if ('.' == url[0]) {
- path = req.originalUrl.split('?')[0]
- url = path + ('/' == path[path.length - 1] ? '' : '/') + url;
- // relative to mount-point
- } else if ('/' != url[0]) {
- path = app.path();
- url = path + '/' + url;
- }
- }
- // Respond
- this.set('Location', url);
- return this;
- };
- /**
- * Redirect to the given `url` with optional response `status`
- * defaulting to 302.
- *
- * The resulting `url` is determined by `res.location()`, so
- * it will play nicely with mounted apps, relative paths,
- * `"back"` etc.
- *
- * Examples:
- *
- * res.redirect('/foo/bar');
- * res.redirect('http://example.com');
- * res.redirect(301, 'http://example.com');
- * res.redirect('http://example.com', 301);
- * res.redirect('../login'); // /blog/post/1 -> /blog/login
- *
- * @param {String} url
- * @param {Number} code
- * @api public
- */
- res.redirect = function(url){
- var app = this.app
- , head = 'HEAD' == this.req.method
- , status = 302
- , body;
- // allow status / url
- if (2 == arguments.length) {
- if ('number' == typeof url) {
- status = url;
- url = arguments[1];
- } else {
- status = arguments[1];
- }
- }
- // Set location header
- this.location(url);
- url = this.get('Location');
- // Support text/{plain,html} by default
- this.format({
- text: function(){
- body = statusCodes[status] + '. Redirecting to ' + encodeURI(url);
- },
- html: function(){
- var u = utils.escape(url);
- body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>';
- },
- default: function(){
- body = '';
- }
- });
- // Respond
- this.statusCode = status;
- this.set('Content-Length', Buffer.byteLength(body));
- this.end(head ? null : body);
- };
- /**
- * Render `view` with the given `options` and optional callback `fn`.
- * When a callback function is given a response will _not_ be made
- * automatically, otherwise a response of _200_ and _text/html_ is given.
- *
- * Options:
- *
- * - `cache` boolean hinting to the engine it should cache
- * - `filename` filename of the view being rendered
- *
- * @param {String} view
- * @param {Object|Function} options or callback function
- * @param {Function} fn
- * @api public
- */
- res.render = function(view, options, fn){
- var self = this
- , options = options || {}
- , req = this.req
- , app = req.app;
- // support callback function as second arg
- if ('function' == typeof options) {
- fn = options, options = {};
- }
- // merge res.locals
- options._locals = self.locals;
- // default callback to respond
- fn = fn || function(err, str){
- if (err) return req.next(err);
- self.send(str);
- };
- // render
- app.render(view, options, fn);
- };
|