Fix license issues
[sdnc/oam.git] / dgbuilder / dgeflows / node_modules / express / lib / response.js
1 /**
2  * Module dependencies.
3  */
4
5 var contentDisposition = require('content-disposition');
6 var deprecate = require('depd')('express');
7 var escapeHtml = require('escape-html');
8 var http = require('http');
9 var isAbsolute = require('./utils').isAbsolute;
10 var onFinished = require('on-finished');
11 var path = require('path');
12 var merge = require('utils-merge');
13 var sign = require('cookie-signature').sign;
14 var normalizeType = require('./utils').normalizeType;
15 var normalizeTypes = require('./utils').normalizeTypes;
16 var setCharset = require('./utils').setCharset;
17 var statusCodes = http.STATUS_CODES;
18 var cookie = require('cookie');
19 var send = require('send');
20 var extname = path.extname;
21 var mime = send.mime;
22 var resolve = path.resolve;
23 var vary = require('vary');
24
25 /**
26  * Response prototype.
27  */
28
29 var res = module.exports = {
30   __proto__: http.ServerResponse.prototype
31 };
32
33 /**
34  * Set status `code`.
35  *
36  * @param {Number} code
37  * @return {ServerResponse}
38  * @api public
39  */
40
41 res.status = function(code){
42   this.statusCode = code;
43   return this;
44 };
45
46 /**
47  * Set Link header field with the given `links`.
48  *
49  * Examples:
50  *
51  *    res.links({
52  *      next: 'http://api.example.com/users?page=2',
53  *      last: 'http://api.example.com/users?page=5'
54  *    });
55  *
56  * @param {Object} links
57  * @return {ServerResponse}
58  * @api public
59  */
60
61 res.links = function(links){
62   var link = this.get('Link') || '';
63   if (link) link += ', ';
64   return this.set('Link', link + Object.keys(links).map(function(rel){
65     return '<' + links[rel] + '>; rel="' + rel + '"';
66   }).join(', '));
67 };
68
69 /**
70  * Send a response.
71  *
72  * Examples:
73  *
74  *     res.send(new Buffer('wahoo'));
75  *     res.send({ some: 'json' });
76  *     res.send('<p>some html</p>');
77  *
78  * @param {string|number|boolean|object|Buffer} body
79  * @api public
80  */
81
82 res.send = function send(body) {
83   var chunk = body;
84   var encoding;
85   var len;
86   var req = this.req;
87   var type;
88
89   // settings
90   var app = this.app;
91
92   // allow status / body
93   if (arguments.length === 2) {
94     // res.send(body, status) backwards compat
95     if (typeof arguments[0] !== 'number' && typeof arguments[1] === 'number') {
96       deprecate('res.send(body, status): Use res.status(status).send(body) instead');
97       this.statusCode = arguments[1];
98     } else {
99       deprecate('res.send(status, body): Use res.status(status).send(body) instead');
100       this.statusCode = arguments[0];
101       chunk = arguments[1];
102     }
103   }
104
105   // disambiguate res.send(status) and res.send(status, num)
106   if (typeof chunk === 'number' && arguments.length === 1) {
107     // res.send(status) will set status message as text string
108     if (!this.get('Content-Type')) {
109       this.type('txt');
110     }
111
112     deprecate('res.send(status): Use res.sendStatus(status) instead');
113     this.statusCode = chunk;
114     chunk = http.STATUS_CODES[chunk];
115   }
116
117   switch (typeof chunk) {
118     // string defaulting to html
119     case 'string':
120       if (!this.get('Content-Type')) {
121         this.type('html');
122       }
123       break;
124     case 'boolean':
125     case 'number':
126     case 'object':
127       if (chunk === null) {
128         chunk = '';
129       } else if (Buffer.isBuffer(chunk)) {
130         if (!this.get('Content-Type')) {
131           this.type('bin');
132         }
133       } else {
134         return this.json(chunk);
135       }
136       break;
137   }
138
139   // write strings in utf-8
140   if (typeof chunk === 'string') {
141     encoding = 'utf8';
142     type = this.get('Content-Type');
143
144     // reflect this in content-type
145     if (typeof type === 'string') {
146       this.set('Content-Type', setCharset(type, 'utf-8'));
147     }
148   }
149
150   // populate Content-Length
151   if (chunk !== undefined) {
152     if (!Buffer.isBuffer(chunk)) {
153       // convert chunk to Buffer; saves later double conversions
154       chunk = new Buffer(chunk, encoding);
155       encoding = undefined;
156     }
157
158     len = chunk.length;
159     this.set('Content-Length', len);
160   }
161
162   // method check
163   var isHead = req.method === 'HEAD';
164
165   // ETag support
166   if (len !== undefined && (isHead || req.method === 'GET')) {
167     var etag = app.get('etag fn');
168     if (etag && !this.get('ETag')) {
169       etag = etag(chunk, encoding);
170       etag && this.set('ETag', etag);
171     }
172   }
173
174   // freshness
175   if (req.fresh) this.statusCode = 304;
176
177   // strip irrelevant headers
178   if (204 == this.statusCode || 304 == this.statusCode) {
179     this.removeHeader('Content-Type');
180     this.removeHeader('Content-Length');
181     this.removeHeader('Transfer-Encoding');
182     chunk = '';
183   }
184
185   if (isHead) {
186     // skip body for HEAD
187     this.end();
188   } else {
189     // respond
190     this.end(chunk, encoding);
191   }
192
193   return this;
194 };
195
196 /**
197  * Send JSON response.
198  *
199  * Examples:
200  *
201  *     res.json(null);
202  *     res.json({ user: 'tj' });
203  *
204  * @param {string|number|boolean|object} obj
205  * @api public
206  */
207
208 res.json = function json(obj) {
209   var val = obj;
210
211   // allow status / body
212   if (arguments.length === 2) {
213     // res.json(body, status) backwards compat
214     if (typeof arguments[1] === 'number') {
215       deprecate('res.json(obj, status): Use res.status(status).json(obj) instead');
216       this.statusCode = arguments[1];
217     } else {
218       deprecate('res.json(status, obj): Use res.status(status).json(obj) instead');
219       this.statusCode = arguments[0];
220       val = arguments[1];
221     }
222   }
223
224   // settings
225   var app = this.app;
226   var replacer = app.get('json replacer');
227   var spaces = app.get('json spaces');
228   var body = JSON.stringify(val, replacer, spaces);
229
230   // content-type
231   if (!this.get('Content-Type')) {
232     this.set('Content-Type', 'application/json');
233   }
234
235   return this.send(body);
236 };
237
238 /**
239  * Send JSON response with JSONP callback support.
240  *
241  * Examples:
242  *
243  *     res.jsonp(null);
244  *     res.jsonp({ user: 'tj' });
245  *
246  * @param {string|number|boolean|object} obj
247  * @api public
248  */
249
250 res.jsonp = function jsonp(obj) {
251   var val = obj;
252
253   // allow status / body
254   if (arguments.length === 2) {
255     // res.json(body, status) backwards compat
256     if (typeof arguments[1] === 'number') {
257       deprecate('res.jsonp(obj, status): Use res.status(status).json(obj) instead');
258       this.statusCode = arguments[1];
259     } else {
260       deprecate('res.jsonp(status, obj): Use res.status(status).jsonp(obj) instead');
261       this.statusCode = arguments[0];
262       val = arguments[1];
263     }
264   }
265
266   // settings
267   var app = this.app;
268   var replacer = app.get('json replacer');
269   var spaces = app.get('json spaces');
270   var body = JSON.stringify(val, replacer, spaces);
271   var callback = this.req.query[app.get('jsonp callback name')];
272
273   // content-type
274   if (!this.get('Content-Type')) {
275     this.set('X-Content-Type-Options', 'nosniff');
276     this.set('Content-Type', 'application/json');
277   }
278
279   // fixup callback
280   if (Array.isArray(callback)) {
281     callback = callback[0];
282   }
283
284   // jsonp
285   if (typeof callback === 'string' && callback.length !== 0) {
286     this.charset = 'utf-8';
287     this.set('X-Content-Type-Options', 'nosniff');
288     this.set('Content-Type', 'text/javascript');
289
290     // restrict callback charset
291     callback = callback.replace(/[^\[\]\w$.]/g, '');
292
293     // replace chars not allowed in JavaScript that are in JSON
294     body = body
295       .replace(/\u2028/g, '\\u2028')
296       .replace(/\u2029/g, '\\u2029');
297
298     // the /**/ is a specific security mitigation for "Rosetta Flash JSONP abuse"
299     // the typeof check is just to reduce client error noise
300     body = '/**/ typeof ' + callback + ' === \'function\' && ' + callback + '(' + body + ');';
301   }
302
303   return this.send(body);
304 };
305
306 /**
307  * Send given HTTP status code.
308  *
309  * Sets the response status to `statusCode` and the body of the
310  * response to the standard description from node's http.STATUS_CODES
311  * or the statusCode number if no description.
312  *
313  * Examples:
314  *
315  *     res.sendStatus(200);
316  *
317  * @param {number} statusCode
318  * @api public
319  */
320
321 res.sendStatus = function sendStatus(statusCode) {
322   var body = http.STATUS_CODES[statusCode] || String(statusCode);
323
324   this.statusCode = statusCode;
325   this.type('txt');
326
327   return this.send(body);
328 };
329
330 /**
331  * Transfer the file at the given `path`.
332  *
333  * Automatically sets the _Content-Type_ response header field.
334  * The callback `fn(err)` is invoked when the transfer is complete
335  * or when an error occurs. Be sure to check `res.sentHeader`
336  * if you wish to attempt responding, as the header and some data
337  * may have already been transferred.
338  *
339  * Options:
340  *
341  *   - `maxAge`   defaulting to 0 (can be string converted by `ms`)
342  *   - `root`     root directory for relative filenames
343  *   - `headers`  object of headers to serve with file
344  *   - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
345  *
346  * Other options are passed along to `send`.
347  *
348  * Examples:
349  *
350  *  The following example illustrates how `res.sendFile()` may
351  *  be used as an alternative for the `static()` middleware for
352  *  dynamic situations. The code backing `res.sendFile()` is actually
353  *  the same code, so HTTP cache support etc is identical.
354  *
355  *     app.get('/user/:uid/photos/:file', function(req, res){
356  *       var uid = req.params.uid
357  *         , file = req.params.file;
358  *
359  *       req.user.mayViewFilesFrom(uid, function(yes){
360  *         if (yes) {
361  *           res.sendFile('/uploads/' + uid + '/' + file);
362  *         } else {
363  *           res.send(403, 'Sorry! you cant see that.');
364  *         }
365  *       });
366  *     });
367  *
368  * @api public
369  */
370
371 res.sendFile = function sendFile(path, options, fn) {
372   var req = this.req;
373   var res = this;
374   var next = req.next;
375
376   if (!path) {
377     throw new TypeError('path argument is required to res.sendFile');
378   }
379
380   // support function as second arg
381   if (typeof options === 'function') {
382     fn = options;
383     options = {};
384   }
385
386   options = options || {};
387
388   if (!options.root && !isAbsolute(path)) {
389     throw new TypeError('path must be absolute or specify root to res.sendFile');
390   }
391
392   // create file stream
393   var pathname = encodeURI(path);
394   var file = send(req, pathname, options);
395
396   // transfer
397   sendfile(res, file, options, function (err) {
398     if (fn) return fn(err);
399     if (err && err.code === 'EISDIR') return next();
400
401     // next() all but write errors
402     if (err && err.code !== 'ECONNABORT' && err.syscall !== 'write') {
403       next(err);
404     }
405   });
406 };
407
408 /**
409  * Transfer the file at the given `path`.
410  *
411  * Automatically sets the _Content-Type_ response header field.
412  * The callback `fn(err)` is invoked when the transfer is complete
413  * or when an error occurs. Be sure to check `res.sentHeader`
414  * if you wish to attempt responding, as the header and some data
415  * may have already been transferred.
416  *
417  * Options:
418  *
419  *   - `maxAge`   defaulting to 0 (can be string converted by `ms`)
420  *   - `root`     root directory for relative filenames
421  *   - `headers`  object of headers to serve with file
422  *   - `dotfiles` serve dotfiles, defaulting to false; can be `"allow"` to send them
423  *
424  * Other options are passed along to `send`.
425  *
426  * Examples:
427  *
428  *  The following example illustrates how `res.sendfile()` may
429  *  be used as an alternative for the `static()` middleware for
430  *  dynamic situations. The code backing `res.sendfile()` is actually
431  *  the same code, so HTTP cache support etc is identical.
432  *
433  *     app.get('/user/:uid/photos/:file', function(req, res){
434  *       var uid = req.params.uid
435  *         , file = req.params.file;
436  *
437  *       req.user.mayViewFilesFrom(uid, function(yes){
438  *         if (yes) {
439  *           res.sendfile('/uploads/' + uid + '/' + file);
440  *         } else {
441  *           res.send(403, 'Sorry! you cant see that.');
442  *         }
443  *       });
444  *     });
445  *
446  * @api public
447  */
448
449 res.sendfile = function(path, options, fn){
450   var req = this.req;
451   var res = this;
452   var next = req.next;
453
454   // support function as second arg
455   if (typeof options === 'function') {
456     fn = options;
457     options = {};
458   }
459
460   options = options || {};
461
462   // create file stream
463   var file = send(req, path, options);
464
465   // transfer
466   sendfile(res, file, options, function (err) {
467     if (fn) return fn(err);
468     if (err && err.code === 'EISDIR') return next();
469
470     // next() all but write errors
471     if (err && err.code !== 'ECONNABORT' && err.syscall !== 'write') {
472       next(err);
473     }
474   });
475 };
476
477 res.sendfile = deprecate.function(res.sendfile,
478   'res.sendfile: Use res.sendFile instead');
479
480 /**
481  * Transfer the file at the given `path` as an attachment.
482  *
483  * Optionally providing an alternate attachment `filename`,
484  * and optional callback `fn(err)`. The callback is invoked
485  * when the data transfer is complete, or when an error has
486  * ocurred. Be sure to check `res.headersSent` if you plan to respond.
487  *
488  * This method uses `res.sendfile()`.
489  *
490  * @api public
491  */
492
493 res.download = function download(path, filename, fn) {
494   // support function as second arg
495   if (typeof filename === 'function') {
496     fn = filename;
497     filename = null;
498   }
499
500   filename = filename || path;
501
502   // set Content-Disposition when file is sent
503   var headers = {
504     'Content-Disposition': contentDisposition(filename)
505   };
506
507   // Resolve the full path for sendFile
508   var fullPath = resolve(path);
509
510   return this.sendFile(fullPath, { headers: headers }, fn);
511 };
512
513 /**
514  * Set _Content-Type_ response header with `type` through `mime.lookup()`
515  * when it does not contain "/", or set the Content-Type to `type` otherwise.
516  *
517  * Examples:
518  *
519  *     res.type('.html');
520  *     res.type('html');
521  *     res.type('json');
522  *     res.type('application/json');
523  *     res.type('png');
524  *
525  * @param {String} type
526  * @return {ServerResponse} for chaining
527  * @api public
528  */
529
530 res.contentType =
531 res.type = function(type){
532   return this.set('Content-Type', ~type.indexOf('/')
533     ? type
534     : mime.lookup(type));
535 };
536
537 /**
538  * Respond to the Acceptable formats using an `obj`
539  * of mime-type callbacks.
540  *
541  * This method uses `req.accepted`, an array of
542  * acceptable types ordered by their quality values.
543  * When "Accept" is not present the _first_ callback
544  * is invoked, otherwise the first match is used. When
545  * no match is performed the server responds with
546  * 406 "Not Acceptable".
547  *
548  * Content-Type is set for you, however if you choose
549  * you may alter this within the callback using `res.type()`
550  * or `res.set('Content-Type', ...)`.
551  *
552  *    res.format({
553  *      'text/plain': function(){
554  *        res.send('hey');
555  *      },
556  *
557  *      'text/html': function(){
558  *        res.send('<p>hey</p>');
559  *      },
560  *
561  *      'appliation/json': function(){
562  *        res.send({ message: 'hey' });
563  *      }
564  *    });
565  *
566  * In addition to canonicalized MIME types you may
567  * also use extnames mapped to these types:
568  *
569  *    res.format({
570  *      text: function(){
571  *        res.send('hey');
572  *      },
573  *
574  *      html: function(){
575  *        res.send('<p>hey</p>');
576  *      },
577  *
578  *      json: function(){
579  *        res.send({ message: 'hey' });
580  *      }
581  *    });
582  *
583  * By default Express passes an `Error`
584  * with a `.status` of 406 to `next(err)`
585  * if a match is not made. If you provide
586  * a `.default` callback it will be invoked
587  * instead.
588  *
589  * @param {Object} obj
590  * @return {ServerResponse} for chaining
591  * @api public
592  */
593
594 res.format = function(obj){
595   var req = this.req;
596   var next = req.next;
597
598   var fn = obj.default;
599   if (fn) delete obj.default;
600   var keys = Object.keys(obj);
601
602   var key = req.accepts(keys);
603
604   this.vary("Accept");
605
606   if (key) {
607     this.set('Content-Type', normalizeType(key).value);
608     obj[key](req, this, next);
609   } else if (fn) {
610     fn();
611   } else {
612     var err = new Error('Not Acceptable');
613     err.status = 406;
614     err.types = normalizeTypes(keys).map(function(o){ return o.value });
615     next(err);
616   }
617
618   return this;
619 };
620
621 /**
622  * Set _Content-Disposition_ header to _attachment_ with optional `filename`.
623  *
624  * @param {String} filename
625  * @return {ServerResponse}
626  * @api public
627  */
628
629 res.attachment = function attachment(filename) {
630   if (filename) {
631     this.type(extname(filename));
632   }
633
634   this.set('Content-Disposition', contentDisposition(filename));
635
636   return this;
637 };
638
639 /**
640  * Append additional header `field` with value `val`.
641  *
642  * Example:
643  *
644  *    res.append('Link', ['<http://localhost/>', '<http://localhost:3000/>']);
645  *    res.append('Set-Cookie', 'foo=bar; Path=/; HttpOnly');
646  *    res.append('Warning', '199 Miscellaneous warning');
647  *
648  * @param {String} field
649  * @param {String|Array} val
650  * @return {ServerResponse} for chaining
651  * @api public
652  */
653
654 res.append = function append(field, val) {
655   var prev = this.get(field);
656   var value = val;
657
658   if (prev) {
659     // concat the new and prev vals
660     value = Array.isArray(prev) ? prev.concat(val)
661       : Array.isArray(val) ? [prev].concat(val)
662       : [prev, val];
663   }
664
665   return this.set(field, value);
666 };
667
668 /**
669  * Set header `field` to `val`, or pass
670  * an object of header fields.
671  *
672  * Examples:
673  *
674  *    res.set('Foo', ['bar', 'baz']);
675  *    res.set('Accept', 'application/json');
676  *    res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' });
677  *
678  * Aliased as `res.header()`.
679  *
680  * @param {String|Object|Array} field
681  * @param {String} val
682  * @return {ServerResponse} for chaining
683  * @api public
684  */
685
686 res.set =
687 res.header = function header(field, val) {
688   if (arguments.length === 2) {
689     if (Array.isArray(val)) val = val.map(String);
690     else val = String(val);
691     if ('content-type' == field.toLowerCase() && !/;\s*charset\s*=/.test(val)) {
692       var charset = mime.charsets.lookup(val.split(';')[0]);
693       if (charset) val += '; charset=' + charset.toLowerCase();
694     }
695     this.setHeader(field, val);
696   } else {
697     for (var key in field) {
698       this.set(key, field[key]);
699     }
700   }
701   return this;
702 };
703
704 /**
705  * Get value for header `field`.
706  *
707  * @param {String} field
708  * @return {String}
709  * @api public
710  */
711
712 res.get = function(field){
713   return this.getHeader(field);
714 };
715
716 /**
717  * Clear cookie `name`.
718  *
719  * @param {String} name
720  * @param {Object} options
721  * @return {ServerResponse} for chaining
722  * @api public
723  */
724
725 res.clearCookie = function(name, options){
726   var opts = { expires: new Date(1), path: '/' };
727   return this.cookie(name, '', options
728     ? merge(opts, options)
729     : opts);
730 };
731
732 /**
733  * Set cookie `name` to `val`, with the given `options`.
734  *
735  * Options:
736  *
737  *    - `maxAge`   max-age in milliseconds, converted to `expires`
738  *    - `signed`   sign the cookie
739  *    - `path`     defaults to "/"
740  *
741  * Examples:
742  *
743  *    // "Remember Me" for 15 minutes
744  *    res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true });
745  *
746  *    // save as above
747  *    res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true })
748  *
749  * @param {String} name
750  * @param {String|Object} val
751  * @param {Options} options
752  * @return {ServerResponse} for chaining
753  * @api public
754  */
755
756 res.cookie = function(name, val, options){
757   options = merge({}, options);
758   var secret = this.req.secret;
759   var signed = options.signed;
760   if (signed && !secret) throw new Error('cookieParser("secret") required for signed cookies');
761   if ('number' == typeof val) val = val.toString();
762   if ('object' == typeof val) val = 'j:' + JSON.stringify(val);
763   if (signed) val = 's:' + sign(val, secret);
764   if ('maxAge' in options) {
765     options.expires = new Date(Date.now() + options.maxAge);
766     options.maxAge /= 1000;
767   }
768   if (null == options.path) options.path = '/';
769   var headerVal = cookie.serialize(name, String(val), options);
770
771   // supports multiple 'res.cookie' calls by getting previous value
772   var prev = this.get('Set-Cookie');
773   if (prev) {
774     if (Array.isArray(prev)) {
775       headerVal = prev.concat(headerVal);
776     } else {
777       headerVal = [prev, headerVal];
778     }
779   }
780   this.set('Set-Cookie', headerVal);
781   return this;
782 };
783
784
785 /**
786  * Set the location header to `url`.
787  *
788  * The given `url` can also be "back", which redirects
789  * to the _Referrer_ or _Referer_ headers or "/".
790  *
791  * Examples:
792  *
793  *    res.location('/foo/bar').;
794  *    res.location('http://example.com');
795  *    res.location('../login');
796  *
797  * @param {String} url
798  * @return {ServerResponse} for chaining
799  * @api public
800  */
801
802 res.location = function(url){
803   var req = this.req;
804
805   // "back" is an alias for the referrer
806   if ('back' == url) url = req.get('Referrer') || '/';
807
808   // Respond
809   this.set('Location', url);
810   return this;
811 };
812
813 /**
814  * Redirect to the given `url` with optional response `status`
815  * defaulting to 302.
816  *
817  * The resulting `url` is determined by `res.location()`, so
818  * it will play nicely with mounted apps, relative paths,
819  * `"back"` etc.
820  *
821  * Examples:
822  *
823  *    res.redirect('/foo/bar');
824  *    res.redirect('http://example.com');
825  *    res.redirect(301, 'http://example.com');
826  *    res.redirect('../login'); // /blog/post/1 -> /blog/login
827  *
828  * @api public
829  */
830
831 res.redirect = function redirect(url) {
832   var address = url;
833   var body;
834   var status = 302;
835
836   // allow status / url
837   if (arguments.length === 2) {
838     if (typeof arguments[0] === 'number') {
839       status = arguments[0];
840       address = arguments[1];
841     } else {
842       deprecate('res.redirect(url, status): Use res.redirect(status, url) instead');
843       status = arguments[1];
844     }
845   }
846
847   // Set location header
848   this.location(address);
849   address = this.get('Location');
850
851   // Support text/{plain,html} by default
852   this.format({
853     text: function(){
854       body = statusCodes[status] + '. Redirecting to ' + encodeURI(address);
855     },
856
857     html: function(){
858       var u = escapeHtml(address);
859       body = '<p>' + statusCodes[status] + '. Redirecting to <a href="' + u + '">' + u + '</a></p>';
860     },
861
862     default: function(){
863       body = '';
864     }
865   });
866
867   // Respond
868   this.statusCode = status;
869   this.set('Content-Length', Buffer.byteLength(body));
870
871   if (this.req.method === 'HEAD') {
872     this.end();
873   } else {
874     this.end(body);
875   }
876 };
877
878 /**
879  * Add `field` to Vary. If already present in the Vary set, then
880  * this call is simply ignored.
881  *
882  * @param {Array|String} field
883  * @return {ServerResponse} for chaining
884  * @api public
885  */
886
887 res.vary = function(field){
888   // checks for back-compat
889   if (!field || (Array.isArray(field) && !field.length)) {
890     deprecate('res.vary(): Provide a field name');
891     return this;
892   }
893
894   vary(this, field);
895
896   return this;
897 };
898
899 /**
900  * Render `view` with the given `options` and optional callback `fn`.
901  * When a callback function is given a response will _not_ be made
902  * automatically, otherwise a response of _200_ and _text/html_ is given.
903  *
904  * Options:
905  *
906  *  - `cache`     boolean hinting to the engine it should cache
907  *  - `filename`  filename of the view being rendered
908  *
909  * @api public
910  */
911
912 res.render = function(view, options, fn){
913   options = options || {};
914   var self = this;
915   var req = this.req;
916   var app = req.app;
917
918   // support callback function as second arg
919   if ('function' == typeof options) {
920     fn = options, options = {};
921   }
922
923   // merge res.locals
924   options._locals = self.locals;
925
926   // default callback to respond
927   fn = fn || function(err, str){
928     if (err) return req.next(err);
929     self.send(str);
930   };
931
932   // render
933   app.render(view, options, fn);
934 };
935
936 // pipe the send file stream
937 function sendfile(res, file, options, callback) {
938   var done = false;
939   var streaming;
940
941   // request aborted
942   function onaborted() {
943     if (done) return;
944     done = true;
945
946     var err = new Error('Request aborted');
947     err.code = 'ECONNABORT';
948     callback(err);
949   }
950
951   // directory
952   function ondirectory() {
953     if (done) return;
954     done = true;
955
956     var err = new Error('EISDIR, read');
957     err.code = 'EISDIR';
958     callback(err);
959   }
960
961   // errors
962   function onerror(err) {
963     if (done) return;
964     done = true;
965     callback(err);
966   }
967
968   // ended
969   function onend() {
970     if (done) return;
971     done = true;
972     callback();
973   }
974
975   // file
976   function onfile() {
977     streaming = false;
978   }
979
980   // finished
981   function onfinish(err) {
982     if (err) return onerror(err);
983     if (done) return;
984
985     setImmediate(function () {
986       if (streaming !== false && !done) {
987         onaborted();
988         return;
989       }
990
991       if (done) return;
992       done = true;
993       callback();
994     });
995   }
996
997   // streaming
998   function onstream() {
999     streaming = true;
1000   }
1001
1002   file.on('directory', ondirectory);
1003   file.on('end', onend);
1004   file.on('error', onerror);
1005   file.on('file', onfile);
1006   file.on('stream', onstream);
1007   onFinished(res, onfinish);
1008
1009   if (options.headers) {
1010     // set headers on successful transfer
1011     file.on('headers', function headers(res) {
1012       var obj = options.headers;
1013       var keys = Object.keys(obj);
1014
1015       for (var i = 0; i < keys.length; i++) {
1016         var k = keys[i];
1017         res.setHeader(k, obj[k]);
1018       }
1019     });
1020   }
1021
1022   // pipe
1023   file.pipe(res);
1024 }