diff options
Diffstat (limited to 'node_modules/express/lib/response.js')
| -rw-r--r-- | node_modules/express/lib/response.js | 756 | 
1 files changed, 756 insertions, 0 deletions
| diff --git a/node_modules/express/lib/response.js b/node_modules/express/lib/response.js new file mode 100644 index 0000000..50fcd05 --- /dev/null +++ b/node_modules/express/lib/response.js @@ -0,0 +1,756 @@ +/** + * 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 +    , head = 'HEAD' == req.method +    , 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) { +    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.charset = this.charset || 'utf-8'; +  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) { +    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 ('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); +}; | 
