Improved fix for open redirect allow list bypass
[express.git] / lib / view.js
blobc08ab4d8d521cc18cbb89c3a705d8f137d12d056
1 /*!
2  * express
3  * Copyright(c) 2009-2013 TJ Holowaychuk
4  * Copyright(c) 2013 Roman Shtylman
5  * Copyright(c) 2014-2015 Douglas Christopher Wilson
6  * MIT Licensed
7  */
9 'use strict';
11 /**
12  * Module dependencies.
13  * @private
14  */
16 var debug = require('debug')('express:view');
17 var path = require('path');
18 var fs = require('fs');
20 /**
21  * Module variables.
22  * @private
23  */
25 var dirname = path.dirname;
26 var basename = path.basename;
27 var extname = path.extname;
28 var join = path.join;
29 var resolve = path.resolve;
31 /**
32  * Module exports.
33  * @public
34  */
36 module.exports = View;
38 /**
39  * Initialize a new `View` with the given `name`.
40  *
41  * Options:
42  *
43  *   - `defaultEngine` the default template engine name
44  *   - `engines` template engine require() cache
45  *   - `root` root path for view lookup
46  *
47  * @param {string} name
48  * @param {object} options
49  * @public
50  */
52 function View(name, options) {
53   var opts = options || {};
55   this.defaultEngine = opts.defaultEngine;
56   this.ext = extname(name);
57   this.name = name;
58   this.root = opts.root;
60   if (!this.ext && !this.defaultEngine) {
61     throw new Error('No default engine was specified and no extension was provided.');
62   }
64   var fileName = name;
66   if (!this.ext) {
67     // get extension from default engine name
68     this.ext = this.defaultEngine[0] !== '.'
69       ? '.' + this.defaultEngine
70       : this.defaultEngine;
72     fileName += this.ext;
73   }
75   if (!opts.engines[this.ext]) {
76     // load engine
77     var mod = this.ext.slice(1)
78     debug('require "%s"', mod)
80     // default engine export
81     var fn = require(mod).__express
83     if (typeof fn !== 'function') {
84       throw new Error('Module "' + mod + '" does not provide a view engine.')
85     }
87     opts.engines[this.ext] = fn
88   }
90   // store loaded engine
91   this.engine = opts.engines[this.ext];
93   // lookup path
94   this.path = this.lookup(fileName);
97 /**
98  * Lookup view by the given `name`
99  *
100  * @param {string} name
101  * @private
102  */
104 View.prototype.lookup = function lookup(name) {
105   var path;
106   var roots = [].concat(this.root);
108   debug('lookup "%s"', name);
110   for (var i = 0; i < roots.length && !path; i++) {
111     var root = roots[i];
113     // resolve the path
114     var loc = resolve(root, name);
115     var dir = dirname(loc);
116     var file = basename(loc);
118     // resolve the file
119     path = this.resolve(dir, file);
120   }
122   return path;
126  * Render with the given options.
128  * @param {object} options
129  * @param {function} callback
130  * @private
131  */
133 View.prototype.render = function render(options, callback) {
134   debug('render "%s"', this.path);
135   this.engine(this.path, options, callback);
139  * Resolve the file within the given directory.
141  * @param {string} dir
142  * @param {string} file
143  * @private
144  */
146 View.prototype.resolve = function resolve(dir, file) {
147   var ext = this.ext;
149   // <path>.<ext>
150   var path = join(dir, file);
151   var stat = tryStat(path);
153   if (stat && stat.isFile()) {
154     return path;
155   }
157   // <path>/index.<ext>
158   path = join(dir, basename(file, ext), 'index' + ext);
159   stat = tryStat(path);
161   if (stat && stat.isFile()) {
162     return path;
163   }
167  * Return a stat, maybe.
169  * @param {string} path
170  * @return {fs.Stats}
171  * @private
172  */
174 function tryStat(path) {
175   debug('stat "%s"', path);
177   try {
178     return fs.statSync(path);
179   } catch (e) {
180     return undefined;
181   }