Coverage

93%
1303
1215
88

lib/app.js

100%
49
49
0
LineHitsSource
1/**
2 * Module dependencies
3 */
4
51var toArray = require('./util').toArray;
61var EventEmitter = require('events').EventEmitter;
71var Klass = require('./klass').Klass;
8
9/**
10 * App constructor function
11 *
12 * @constructor
13 */
14
15function App() {
1611 EventEmitter.call(this);
1711 var self = this;
1811 self.publicMethods = {};
19
2011 for (var k in self) {
21228 if (self.isPublicMethodName(k)) {
2221 this.processPublicMethod(k);
23 }
24 }
25}
26
27/**
28 * App prototype object
29 */
30
311App.prototype = {
32 emitInlineCallback: false,
33 prefixDelegatedEvent: true,
34 publicMethods: null,
35 reservedMethodNames: Object.keys(EventEmitter.prototype),
36
37 /**
38 * Check if given name can be used as public method name
39 *
40 * @param name {String} Name to check
41 * @returns {boolean}
42 * @public
43 */
44
45 isPublicMethodName: function (name) {
46228 return name && 'string' === typeof name && this.reservedMethodNames.indexOf(name) === -1 && name[0] !== '_';
47 },
48
49 /**
50 * The modified emit function which prefixes event with app name if necessary
51 *
52 * @public
53 */
54
55 emit: function () {
566 var emitter = this.delegate || this;
576 var args = toArray(arguments);
586 if (this.delegate && this.prefixDelegatedEvent) {
593 var eventName = 'string' === typeof this.prefixDelegatedEvent ? this.prefixDelegatedEvent : this.name;
603 eventName += ':' + args[0];
613 args[0] = this;
623 args.unshift(eventName);
63 }
646 EventEmitter.prototype.emit.apply(emitter, args);
65 },
66
67 /**
68 * Generate default callback for each public method
69 *
70 * @param propName {String} Name of the property
71 * @private
72 */
73
74 processPublicMethod: function (propName) {
7521 var self = this;
7621 var f = self[propName];
7721 if ('function' === typeof f) {
7821 self[propName] = self.publicMethods[propName] = (function (name, f) {
7921 return function () {
8010 var args = toArray(arguments);
8110 var callback = args[args.length - 1];
8210 var hasCallback = 'function' === typeof callback;
8310 var resultIsNotUndefined = false;
84
8510 var wrapper = function () {
8611 if (resultIsNotUndefined) {
872 return;
88 }
899 var _args = toArray(arguments);
90
919 if (hasCallback) {
925 if ('before' === self.emitInlineCallback) {
931 _args.unshift(name);
941 self.emit.apply(self, _args);
951 _args.shift();
96 }
975 callback.apply(self, _args);
98 }
99
1009 if (!hasCallback || 'after' === self.emitInlineCallback) {
1015 _args.unshift(name);
1025 self.emit.apply(self, _args);
103 }
104 };
105
10610 if (hasCallback) {
1075 args[args.length - 1] = wrapper;
108 } else {
1095 args.push(wrapper);
110 }
111
11210 var result = f.apply(self, args);
11310 resultIsNotUndefined = 'undefined' !== typeof result;
11410 return result;
115 };
116 })(propName, f);
117 }
118 }
119};
120
121/**
122 * Add more reserved name
123 *
124 * @type {Array}
125 */
126
1271App.prototype.reservedMethodNames = App.prototype.reservedMethodNames.concat(Object.keys(App.prototype), 'init', 'domain', 'name');
128
129/**
130 * Module exports
131 */
132
1331exports.App = Klass(EventEmitter, App);
134

lib/auth.js

100%
22
22
0
LineHitsSource
1/**
2 * Secured cookie implementation based on this pubilcation:
3 * A Secure Cookie Protocol (http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.92.7649&rep=rep1&type=pdf)
4 *
5 * More about salted hash, goto:
6 * http://phpsec.org/articles/2005/password-hashing.html or http://www.aspheute.com/english/20040105.asp
7 *
8 * @todo more secured way (e.g. http://gitorious.com/django-password-model)
9 */
10
111var c = require('./crypto'),
12 sha1 = c.sha1,
13 hmac_sha1 = c.hmac_sha1;
14
15function sign(username, expiration, data, serverKey) {
162 var k = hmac_sha1([username, expiration].join("|"), serverKey);
174 if (typeof data !== "string") data = JSON.stringify(data);
182 var raw = [username, expiration, data].join("|");
192 return raw + "|" + hmac_sha1(raw, k);
20}
21
22function verify(input, serverKey) {
232 if (typeof input !== "string") return false;
242 var values = input.split("|");
252 if (values.length !== 4) return false;
262 if (new Date() >= new Date(values[1])) return false;
272 var k = hmac_sha1([values[0], values[1]].join("|"), serverKey);
282 return values.pop().replace(/[\x00-\x20]/g, "") === hmac_sha1(values.join("|"), k) ? values : false;
29}
30
31function checkPassword(password, raw) {
323 var pass = password.split("$");
33 // "algo$salt$hash"
343 if (pass.length === 3) {
352 if (pass[0] === "sha1") {
362 return pass[2] === sha1(pass[1] + raw);
37 }
38 }
391 return false;
40}
41
42function makePassword(raw) {
431 var salt = sha1("" + Math.random() + Math.random()).slice(0, 9);
441 return 'sha1$' + salt + "$" + sha1(salt + raw);
45}
46
471exports.sign = sign;
481exports.verify = verify;
491exports.checkPassword = checkPassword;
501exports.makePassword = makePassword;

lib/context.js

95%
46
44
2
LineHitsSource
1/**
2 * Request/response context for Router and Core
3 */
4
5/**
6 * Module dependencies
7 */
8
91var Klass = require('./klass').Klass;
101var EventEmitter = require('events').EventEmitter;
111var extend = require('./util').extend;
12
13/**
14 * The constructor of context class
15 *
16 * @param request {http.IncomingMessage}
17 * @param response {http.ServerResponse}
18 * @constructor
19 * @public
20 */
21
22function Context(request, response) {
2324 EventEmitter.call(this);
2424 this.request = request;
2524 this.response = response;
2624 this.statusCode = 200;
2724 this._headers = {};
2824 this._headerSent = false;
29}
30
31/**
32 * Context prototype object
33 *
34 * @type {Object}
35 */
36
371Context.prototype = {
38
39 /**
40 * Send a response headers to client
41 *
42 * @param [statusCode] {Number|String} 3-digit HTTP status code, default is 200
43 * @param [headers] {Object} Optional headers object
44 * @public
45 */
46
47 writeHead: function (statusCode, headers) {
488 this._headerSent = true;
498 this.response.writeHead(statusCode || this.statusCode, extend(this._headers, headers));
508 return this;
51 },
52
53 /**
54 * Send a chunk of response body to client
55 *
56 * @param chunk {String|Buffer} Data of chunk
57 * @param [encoding] {String} Encoding string, defaults to 'utf8'
58 * @public
59 */
60
61 write: function (chunk, encoding) {
620 this.response.write(chunk, encoding);
630 return this;
64 },
65
66 /**
67 * Finish the response process with optional data
68 *
69 * @param [data] {String|Buffer} Data to send
70 * @param [encoding] {String} Encoding string, defaults to 'utf8'
71 * @public
72 */
73
74 end: function (data, encoding) {
758 if (!this._headerSent) {
763 this.writeHead(this.statusCode, this._headers);
77 }
788 this.response.end(data, encoding);
798 return this;
80 },
81
82 /**
83 * ServerResponse.setHeader
84 *
85 * @param key {String} Key of the header
86 * @param value {*} Value of the header
87 * @returns {Object} "this"
88 * @public
89 */
90
91 setHeader: function (key, value) {
9253 this._headers[key] = value;
9353 return this;
94 },
95
96 /**
97 * ServerResponse.getHeader
98 *
99 * @param key {String} Key of the header
100 * @returns {*} Value of the header if any
101 * @public
102 */
103
104 getHeader: function (key) {
10548 return this._headers[key];
106 },
107
108 /**
109 * Check if the given key is setted in header
110 *
111 * @param key {String} Key of the header
112 * @returns {boolean}
113 * @public
114 */
115
116 hasHeader: function (key) {
11742 return !!this.getHeader(key);
118 },
119
120 /**
121 * Add the header if it's not setted
122 *
123 * @param key {String} Key of the header
124 * @param value {*} Value of the header
125 * @returns {Object} "this"
126 * @public
127 */
128
129 addHeader: function (key, value) {
13042 if (!this.hasHeader(key)) {
13129 this.setHeader(key, value);
132 }
13342 return this;
134 },
135
136 /**
137 * ServerResponse.removeHeader
138 *
139 * @param key {String} Key of the header
140 * @returns {Object} "this"
141 * @public
142 */
143
144 removeHeader: function (key) {
1451 delete this._headers[key];
1461 return this;
147 },
148
149 /**
150 * Set the HTTP status code for response
151 *
152 * @param code {Number|String} 3-digit HTTP status code
153 * @returns {Object} "this"
154 * @public
155 */
156
157 setStatusCode: function (code) {
1583 this.statusCode = code;
1593 return this;
160 },
161
162 /**
163 * Redirect to given url permanently or not
164 *
165 * @param url {String} Url of redirection
166 * @param [permanent] {Boolean} Indicate the permanence of the redirection
167 * @public
168 */
169
170 redirect: function (url, permanent) {
1712 this.setStatusCode(permanent ? 301 : 302);
1722 this.setHeader('Location', url);
1732 this.end();
174 },
175
176 /**
177 * Send response to client in one operation
178 *
179 * @param {String|Object} body Body of the http response
180 * @param {Number} [code] Http response status code, default `200`
181 * @param {Object} [headers] Http response headers, default `Content-Type, text/plain;`
182 * @param {String} [encoding] default 'utf8'
183 * @public
184 */
185
186 send: function (body, code, headers, encoding) {
18718 if (typeof body !== 'string') {
1885 body = JSON.stringify(body) || "";
189 }
19018 this.addHeader("Content-Length", Buffer.byteLength(body, encoding || 'utf8'));
19118 this.addHeader("Content-Type", 'text/plain');
19218 this.writeHead(code, headers);
19318 this.end(body, encoding);
194 },
195
196 /**
197 * Send data as JSON
198 *
199 * @param data {String|Object} Data as a JSON string or object
200 * @public
201 */
202
203 sendJSON: function (data) {
2045 this.setHeader('Content-Type', 'application/json; charset=utf-8');
2055 this.send(data);
206 },
207
208 /**
209 * Send data as HTML text
210 *
211 * @param data {String} String of data
212 * @public
213 */
214
215 sendHTML: function (data) {
2167 this.setHeader('Content-Type', 'text/html; charset=utf-8');
2177 this.send(data);
218 },
219
220 /**
221 * Default error handling function, please override
222 *
223 * @param err {Object|String} Error object
224 * @public
225 */
226
227 error: function (err) {
2283 var msg = err.message || err.stack;
2293 this.send(msg, err.statusCode || 500);
230 }
231};
232
233/**
234 * Expose Context class
235 *
236 * @type {Function}
237 */
238
2391exports.Context = Klass(EventEmitter, Context);

lib/control.js

94%
135
127
8
LineHitsSource
1/**
2 * Utilities for controlling javascript execution flow
3 */
4
5/**
6 * Module dependencies
7 */
8
91var toArray = require('./util').toArray;
10
11/**
12 * Add functions in named chain and generate a function to call them in the adding order serially
13 *
14 * @constructor
15 */
16
171var Chain = function () {
1816 this._chain = {};
19};
20
21/**
22 * Chain prototype object
23 */
24
251Chain.prototype = {
26
27 /**
28 * Add a function to a named chain
29 *
30 * @param name {String} Name of the chain
31 * @param fn {Function} Function to be added
32 * @public
33 */
34
35 add: function (name, fn) {
3644 if (!this._chain[name]) {
3740 this._chain[name] = [];
38 }
3944 this._chain[name].push(fn);
40 },
41
42 /**
43 * Get the chained function
44 *
45 * @param name {String} Name of the chain
46 * @returns {Function}
47 * @public
48 */
49
50 get: function (name) {
5141 if (this._chain[name]) {
5240 var fnChain = chain(this._chain[name], function (fn, idx, fnChain, next, args) {
5335 args = toArray(args);
5435 args.push(next);
5535 if (fn.apply(this, args)) {
562 next();
57 }
58 });
5940 return function () {
6031 fnChain(0, toArray(arguments), this);
61 };
62 }
63 }
64};
65
66/**
67 * Generate a function which calls each function in array in serial order
68 *
69 * @param array {Array} Array of items need to be iterated and called against "callback"
70 * @param callback {Function} Callback function for each iteration
71 * @param [doneCallback] Optional finishing callback which will be called at the end of iteration
72 * @returns {Function}
73 * @public
74 */
75
76function chain(array, callback, doneCallback) {
7767 var length;
7867 if (!Array.isArray(array)) {
792 if (isFinite(array)) {
80 // can be parsed as a number,
81 // this is useful if you want to call one function many times in serial order
821 length = parseInt(array, 10);
83 } else {
841 throw new Error('First argument should be either `Array` or `Number`, `' + typeof array + '` got.');
85 }
86 } else {
8765 length = array.length;
88 }
8966 return function next(step, args, ctx) {
90113 step = step || 0;
91113 if (step >= length) {
9211 if (doneCallback) {
931 doneCallback.call(ctx);
94 }
9511 return;
96 }
97102 callback.call(ctx, array[step], step++, array, function () {
9863 next(step, arguments.length > 0 ? toArray(arguments) : args, ctx);
99 }, args);
100 };
101}
102
103/**
104 * Executes async function `callback` for data sets `array` in parallel
105 *
106 * @param array {Array}
107 * @param callback {Function}
108 * @param doneCallback {Function}
109 * @param context {Object}
110 * @returns {Object}
111 * @public
112 */
113
114function parallel(array, callback, doneCallback, context) {
1154 var count = 0, len = array.length, result = [];
1164 var failCallback;
1174 var timer, timeoutSeconds, timeoutCallback;
1184 process.nextTick(function () {
1194 array.forEach(function (item, idx, arr) {
12013 callback.call(context, item, idx, arr, function (err, res) {
12112 ++count;
12212 if (timer && count === len) {
1230 clearTimeout(timer);
124 }
12512 if (err) {
1261 if (failCallback) {
1271 failCallback.call(context, err, item, idx);
128 } else {
1290 console.trace('Please set error handling function with `.fail`');
1300 throw err;
131 }
132 } else {
13311 result[idx] = res;
134 }
13512 if (count === len) {
1363 if (doneCallback) {
1373 doneCallback.call(context, result);
138 }
139 }
140 });
141 });
1424 if (timeoutSeconds) {
1431 timer = setTimeout(function () {
1441 timeoutCallback.call(context, result);
145 }, timeoutSeconds * 1000);
146 }
147 });
1484 var ret = {
149 done: function (callback) {
1503 doneCallback = callback;
1513 return ret;
152 },
153 fail: function (callback) {
1541 failCallback = callback;
1551 return ret;
156 },
157 timeout: function (callback, timeoutInSeconds) {
1581 timeoutSeconds = timeoutInSeconds;
1591 timeoutCallback = callback;
160 }
161 };
1624 return ret;
163}
164
165/**
166 * Simple function which can convert your async function to promise style and call as many times as you wish
167 *
168 * @param {Function} asyncFn Your original async function like `fs.readFile`
169 * @param {Object} context Context you want to keep for the asyncFn (`this` inside your function scope)
170 * @returns {Function} Promise version of your function,
171 * which you can register the callback functions by calling `then(callback)` of the returned object
172 * @public
173 */
174
175function promise(asyncFn, context) {
1761 return function () {
177 // the callback registered by calling `when(callback)`
1781 var callback;
179 // we assume last argument of the original function is its callback
180 // and you should omit this argument for the promised function generated
1811 var args = toArray(arguments);
1821 args.push(function () {
1831 if (callback) {
1841 callback.apply(context, arguments);
185 }
186 });
187 // call the original async function with given context in next tick
188 // to make sure the `when` function set the callback first
1891 process.nextTick(function () {
1901 asyncFn.apply(context, args);
191 });
1921 return {
193 when: function (handleFunc) {
194 // register the callback function
1951 callback = handleFunc;
196 }
197 };
198 };
199}
200
201/**
202 * This an extended version of previous `promise` function with error handling and flow control facilities
203 * @param {Function} asyncFn Your original async function like `fs.readFile`
204 * @param {Object} context Context you want to keep for the asyncFn (`this` inside your function scope)
205 * @param {Object} emitter Event emitter (delegate) use to emit and listen events
206 * @returns {Function} Deferred version of your function,
207 * which you can register the callback functions by calling `then(callback)` of the returned object
208 * and set the error handling function by calling `fail(errback)` respectively
209 */
210
211function defer(asyncFn, context, emitter) {
2124 return function () {
213 // the callback handler(s) registered by calling `then(callback)`
2146 var thenCallbacks = [], failCallbacks = [], andCallbacks = [], doneCallback, callback, nextDeferred;
215 // we assume last argument of the original function is its callback
216 // and you should omit this argument for the deferred function generated
2176 var args = toArray(arguments);
2186 args.push(function (err) {
2196 if (nextDeferred || callback) {
220 // `callback` and `nextDeferred` should accept `err` as argument
2212 andCallbacks.push(function (defer) {
2222 var _args = toArray(arguments);
2232 _args[0] = err;
224 // call the `callback` first if any
2252 if (callback) {
2261 callback.apply(this, _args);
227 }
2282 if (nextDeferred) {
2291 if (err) {
2300 nextDeferred.error(err);
231 } else {
2321 nextDeferred.next.apply(this, _args.slice(1));
233 }
234 }
235 });
236 }
237 // this assume that, the first argument of the callback is an error object if error occurs, otherwise should be `null`
2386 if (!err) {
2395 var theArgs = toArray(arguments);
240 // since there is no `err`, remove it from the arguments
241 // this can help you separate your error handling logic from business logic
242 // and make the error handling part reusable
2435 theArgs.shift();
2445 if (thenCallbacks.length > 0) {
245 // call the `then` stack
2462 thenCallbacks.forEach(function (fn) {
2473 fn.apply(context, theArgs);
248 });
249 }
2505 if (andCallbacks.length > 0) {
251 // call the `and` stack
2525 chain(andCallbacks, function (fn, idx, fnChains, next, chainArgs) {
25316 chainArgs.unshift({next: next, error: errback_});
25416 if (fn.apply(this, chainArgs)) {
2554 chainArgs.shift();
2564 next.apply(null, chainArgs);
257 }
258 }, doneCallback || (emitter && function () {
2590 emitter.emit('done');
260 }))(0, theArgs, context);
261 }
262 } else {
2631 errback_(err);
264 }
265 // define an error callback so that we can call it in the `and` chain
266 function errback_(err) {
2671 if (failCallbacks.length > 0 || emitter) {
2681 if (emitter) {
2691 emitter.emit('fail', err);
270 }
271 // if we have error handler then use it
2721 failCallbacks.forEach(function (fn) {
2731 fn.call(context, err);
274 });
275 } else {
276 // otherwise throw the exception
2770 console.trace('Please set error handling function with `.fail` or supply an `emitter` and listen to the `fail` event.');
2780 throw err;
279 }
280 }
281 });
282 // call the original async function with given context in next tick
283 // to make sure the `then`,`and`,`done`,`fail` function set the callback first
2846 process.nextTick(function () {
2856 asyncFn.apply(context, args);
286 });
2876 var ret = {
288 then: function () {
2896 var fns = toArray(arguments);
2906 if (andCallbacks.length > 0) {
291 // put at the tail of `and` callbacks
2923 var fn = function () {
2933 var args = toArray(arguments);
2943 args.shift(); // shift the `defer` object
2953 fns.forEach(function (f) {
2964 f.apply(this, args);
297 }, this);
2983 return true;
299 };
3003 andCallbacks.push(fn);
301 } else {
302 // register the callback function in parallel
3033 thenCallbacks = thenCallbacks.concat(fns);
304 }
3056 return ret;
306 },
307 and: function () {
30810 andCallbacks = andCallbacks.concat(toArray(arguments));
30910 return ret;
310 },
311 callback: function (cb) {
3121 callback = cb;
3131 return ret;
314 },
315 defer: function (deferred) {
3161 nextDeferred = deferred;
3171 return ret;
318 },
319 done: function (fn) {
3201 doneCallback = emitter ? function () {
3211 emitter.emit('done');
3221 fn.call(this);
323 } : fn;
3241 return ret;
325 },
326 fail: function (errorFunc, replace) {
3271 if (replace) {
3280 failCallbacks = [errorFunc];
329 } else {
3301 failCallbacks.push(errorFunc);
331 }
3321 return ret;
333 }
334 };
3356 if (emitter) {
3362 ret.emitter = emitter;
337 }
3386 return ret;
339 };
340}
341
342/**
343 * Module exports
344 */
345
3461module.exports = {
347 chain: chain,
348 parallel: parallel,
349 Chain: Chain,
350 promise: promise,
351 defer: defer
352};

lib/cookie.js

100%
26
26
0
LineHitsSource
1/**
2 * Cookie parsing/encoding utilities
3 */
4
5/**
6 * Module dependencies
7 */
8
91var qs = require('querystring');
101var escape = qs.escape;
111var unescape = qs.unescape;
12
13/**
14 * Module exports
15 *
16 * @type {{parse: Function, stringify: Function, checkLength: Function}}
17 */
18
191module.exports = {
20
21 /**
22 * Parse a cookie string
23 *
24 * @param cookie {String} The cookie string being parsed
25 * @param [name] {String} If the name is given, it returns cookie value for this name
26 * @returns {Object|String}
27 * @public
28 */
29
30 parse: function (cookie, name) {
315 var cookies = {};
325 if ('string' === typeof cookie) {
33 // Copied from: [cookie-sessions](http://github.com/caolan/cookie-sessions/blob/master/lib/cookie-sessions.js)
345 cookies = cookie.split(/\s*;\s*/g).map(
35 function (x) {
3615 return x.split('=');
37 }).reduce(function (a, x) {
3815 if (x[0] && x[1]) {
3913 a[unescape(x[0])] = unescape(x[1]);
40 }
4115 return a;
42 }, {});
43 }
445 return name ? cookies[name] : cookies;
45 },
46
47 /**
48 * Encode cookie into string
49 *
50 * @param name {String} Cookie name
51 * @param value {String} Cookie value
52 * @param options {Object} Cookie options
53 * @returns {string}
54 * @public
55 */
56
57 stringify: function (name, value, options) {
584 var cookie = name + "=" + escape(value);
594 if (options) {
604 if (options.expires) {
614 cookie += "; expires=" + options.expires.toUTCString();
62 }
634 if (options.path) {
643 cookie += "; path=" + options.path;
65 }
664 if (options.domain) {
672 cookie += "; domain=" + options.domain;
68 }
694 if (options.secure) {
701 cookie += "; secure=" + options.secure;
71 }
724 if (options.httponly) {
731 cookie += "; httponly=" + options.httponly;
74 }
754 return cookie;
76 }
77 },
78
79 /**
80 * Check if the length of cookie string is conform to standard
81 * @param cookieStr {String} Cookie string
82 * @returns {boolean}
83 * @private
84 */
85
86 checkLength: function (cookieStr) {
871 return cookieStr.length <= 4096;
88 }
89};
90

lib/core.js

80%
194
157
37
LineHitsSource
1/**
2 * Middleware system inspired by Connect
3 */
4
5/**
6 * Module dependencies
7 */
8
91var Klass = require('./klass').Klass;
101var Chain = require('./control').Chain;
111var EventEmitter = require('events').EventEmitter;
121var Path = require('path');
131var toArray = require('./util').toArray;
141var App = require('./app').App;
151var Context = require('./context').Context;
161var extend = require('./util').extend;
17
18/**
19 * The middleware management class
20 *
21 * @param [site] {Object} Optional Site instance object
22 * @constructor
23 * @public
24 */
25
261var Core = Klass(Chain, {
27
28 /**
29 * Constructor class
30 *
31 * @param [site] {Object} Optional Site instance object
32 * @constructor
33 * @public
34 */
35
36 init: function (site) {
3714 this.site = site;
3814 this.plugins = [];
3914 this.stackNames = [];
4014 this.stacks = {};
4114 this.apps = {};
4214 this.routes = {};
43 },
44
45 /**
46 * Get the "writeHead" functions chain
47 *
48 * @returns {Function}
49 * @private
50 */
51
52 writeHead: function () {
5313 this.add('writeHead', function (statusCode, headers) {
5415 this.response.writeHead(statusCode, headers);
55 });
56 // return the stacked callbacks
5713 return this.get('writeHead');
58 },
59
60 /**
61 * Get the "write" functions chain
62 *
63 * @returns {Function}
64 * @private
65 */
66
67 write: function () {
6813 this.add('write', function (chunk, encoding) {
690 if (!this._headerSent) {
700 this.writeHead();
71 }
720 this.response.write(chunk, encoding);
73 });
7413 return this.get('write');
75 },
76
77 /**
78 * Get the "end" functions chain
79 *
80 * @returns {Function}
81 * @private
82 */
83
84 end: function () {
8513 this.add('end', function (chunk, encoding) {
8615 if (!this._headerSent) {
870 this.writeHead();
88 }
8915 this.response.end(chunk, encoding);
90 });
9113 return this.get('end');
92 },
93
94 /**
95 * Load a plugin and push to stack or extend as module
96 *
97 * @param plugin {String|Object} The name string of the built-in plugin or the plugin object
98 * @param [options] {Object} Optional plugin options
99 * @public
100 */
101
102 use: function (plugin, options) {
10326 if ('string' === typeof plugin) {
10424 plugin = require(Path.join(__dirname, '/plugin', plugin.toLowerCase()));
105 }
106
10726 var pluginName = plugin.name;
108
10926 if (this.plugins.indexOf(pluginName) > -1) {
1100 throw new Error('Duplicated plugin name: ' + pluginName);
111 }
112
11326 this.plugins.push(pluginName);
114
11526 if (plugin.attach) {
11625 var fn = plugin.attach(this, options);
11725 if ('function' === typeof fn) {
11823 this.stackNames.push(pluginName);
11923 this.stacks[pluginName] = fn;
120 }
121 }
122
12326 if (plugin.module) {
1243 Context = Context(plugin.module);
125 }
12626 return this;
127 },
128
129 /**
130 *
131 * @param app
132 * @public
133 */
134 load: function (app) {
1353 this.apps[app.name.toLowerCase()] = app;
1363 return this;
137 },
138
139 /**
140 *
141 * @param routes
142 * @returns {*}
143 * @public
144 */
145 map: function (routes) {
14613 extend(this.routes, routes);
14713 return this;
148 },
149
150 /**
151 * Map methods of app to routes and add normalized routes to router
152 *
153 * @private
154 */
155
156 buildRoutes: function () {
15713 if (!this.router) {
1584 this.use('router');
159 }
160
16113 var self = this;
16213 var router = this.router;
16313 var routes = this.routes;
16413 var apps = this.apps;
165
16613 Object.keys(apps).forEach(function (appName) {
1673 var _app = apps[appName];
1683 var publicMethods = _app.publicMethods;
1693 Object.keys(publicMethods).forEach(function (name) {
1703 var routeName = appName + name[0].toUpperCase() + name.slice(1);
1713 var route = self.normalizeRoute(routeName, routes, apps);
1723 if (route) {
1733 router.add(route.method, route.url, route.handler, route.hooks);
1743 delete routes[route.name];
175 }
176 });
177 });
178
17913 Object.keys(routes).forEach(function (routeName) {
18012 var route = self.normalizeRoute(routeName, routes, apps);
18112 if (route) {
18211 router.add(route.method, route.url, route.handler, route.hooks);
18311 delete routes[route.name];
184 }
185 });
186 },
187
188 /**
189 * Normalize route and generate default settings based on route name, app or provided routes
190 *
191 * @param routeName {String} Name of the route, if app supplied this should be name of the app function
192 * @param routes {Object} The routes definition object
193 * @param [apps] {Object} Optional object of app instances
194 * @returns {Object|Boolean}
195 * @private
196 */
197
198 normalizeRoute: function (routeName, routes, apps) {
19915 var router = this.router;
20015 var self = this;
20115 var defaultViewPath = router.slashCamelCase(routeName);
20215 var appName = defaultViewPath.split('/')[0];
20315 if (routeName === appName) {
2041 return false;
205 }
206
20714 var app;
20814 var appFn;
209
21014 if (apps) {
21114 app = apps[appName];
21214 if (app) {
2133 var methodName = routeName.replace(appName, '');
2143 methodName = methodName[0].toLowerCase() + methodName.slice(1);
2153 appFn = app.publicMethods[methodName];
216 }
217 }
218
21914 var route = routes[routeName] || {};
22014 route.name = routeName;
221
22214 defaultViewPath = defaultViewPath.split('/');
22314 defaultViewPath.shift();
22414 defaultViewPath = appName + '/' + defaultViewPath.join('_');
22514 defaultViewPath += '.html';
226
22714 var appRouteDefaults = routes[appName] || {};
228
22914 if (!route.method) {
2304 route.method = appRouteDefaults.method || 'GET';
231 }
232
23314 if (!route.url) {
2344 route.url = router.slashCamelCase(routeName);
235 }
236
23714 if (appRouteDefaults.urlRoot && route.url[0] !== '^') {
2380 route.url = route.url.split('/');
2390 route.url.shift();
2400 route.url = router.prefixUrl(appRouteDefaults.urlRoot, route.url.join('/'));
241 }
242
24314 if (!route.view) {
2449 if (self.view) {
2450 route.view = defaultViewPath;
246 } else {
2479 route.view = 'html';
248 }
249 }
250
25114 switch (route.view) {
252 case 'html':
2539 route.view = function (err, result) {
2542 if (err) {
2550 this.error(err);
2560 return;
257 }
2582 this.sendHTML(result || '');
259 };
2609 break;
261 case 'json':
2621 route.view = function (err, result) {
2631 if (err) {
2640 this.error(err);
2650 return;
266 }
2671 this.sendJSON(result || '{}');
268 };
2691 break;
270 }
271
27214 if (self.view && 'string' === typeof route.view) {
2730 defaultViewPath = route.view;
2740 route.view = function (err, result) {
2750 if (err) {
2760 this.error(err);
2770 return;
278 }
2790 this.render(result);
280 };
281 }
282
28314 if (!route.handler) {
2847 if (app && appFn) {
2853 route.handler = function (context) {
2863 var next;
2873 var args = toArray(arguments);
2883 context.app = app;
289
290 function callback() {
2913 if (self.view) {
2920 context.view = self.view;
2930 context.viewPath = defaultViewPath;
294 }
2953 route.view.apply(context, arguments);
2963 if (next) {
2971 next();
298 }
299 }
300
3013 if (context.session) {
3021 args[0] = context.session;
303 } else {
3042 args.shift();
305 }
306
3073 if ('function' === typeof args[args.length - 1]) {
3081 next = args.pop();
309 }
310
3113 args.push(callback);
3123 appFn.apply(app, args);
313 };
314 } else {
3154 route.handler = function (context) {
3164 if (app) {
3170 context.app = app;
318 }
3194 if (self.view) {
3204 context.view = self.view;
3214 context.viewPath = defaultViewPath;
322 }
3234 var args = toArray(arguments);
3244 args[0] = null;
3254 route.view.apply(context, args);
3264 return true;
327 };
328 }
3297 } else if (app) {
3300 var handler = route.handler;
3310 route.handler = function (context) {
3320 context.app = app;
3330 handler.apply(null, arguments);
334 };
335 }
336
33714 if (appRouteDefaults && appRouteDefaults.hooks) {
3381 if (route.hooks) {
3390 route.hooks = router.stackHook(appRouteDefaults.hooks, route.hooks);
340 } else {
3411 route.hooks = appRouteDefaults.hooks.slice(0);
342 }
343 }
344
34514 return route;
346 },
347
348 /**
349 * Get the listener which can handle "request" event
350 *
351 * @returns {Function}
352 * @public
353 */
354
355 getListener: function () {
35613 this.buildRoutes();
35713 var stacks = this.stacks;
35813 var stackNames = this.stackNames;
359
360 function go(idx, ctx, request, response, where, jumping) {
36136 var curr;
36236 if ('undefined' === typeof where) {
36336 curr = stacks[stackNames[idx++]];
36436 if (!curr) {
36513 return;
366 }
367 } else {
3680 var whereIdx = stackNames.indexOf(where);
3690 if (whereIdx > -1) {
3700 curr = stacks[where];
3710 if (jumping) {
3720 idx = whereIdx + 1;
373 }
374 } else {
3750 throw new Error('Undefined plugin: ' + where);
376 }
377 }
378
37923 curr.call(ctx, request, response, function (where, jumping) {
38020 go(idx, ctx, request, response, where, jumping);
381 });
382 }
383
38413 var writeHead = this.writeHead();
38513 var write = this.write();
38613 var end = this.end();
387
38813 Context = Context({
389 writeHead: function (statusCode, headers) {
39015 this._headerSent = true;
39115 writeHead.call(this, statusCode || this.statusCode, extend(this._headers, headers));
39215 return this;
393 },
394 write: function (chunk, encoding) {
3950 write.call(this, chunk, encoding);
3960 return this;
397 },
398 end: function (chunk, encoding) {
39915 end.call(this, chunk, encoding);
40015 return this;
401 }
402 });
403
40413 if (this.site) {
4054 var site = this.site;
4064 Context = Context({
407 set: function () {
4080 return site.set.apply(site, arguments);
409 },
410 get: function () {
4110 return site.get.apply(site, arguments);
412 }
413 });
414 }
415
41613 return function (request, response) {
41716 var ctx = new Context(request, response);
41816 go(0, ctx, request, response);
419 };
420 }
421});
422
423/**
424 * Expose Core
425 *
426 * @type {Function}
427 */
428
4291exports.Core = Core;

lib/crypto.js

100%
33
33
0
LineHitsSource
1/*!
2 * Some shortcuts for crypto
3 */
4
5/**
6 * Module dependencies
7 */
81var crypto = require('crypto');
91var fs = require('fs');
10
11function _hashFile(alg, path, encoding, callback) {
124 if (typeof encoding === 'function') {
132 callback = encoding;
142 encoding = undefined;
15 }
164 var stream = fs.createReadStream(path);
174 var hash = crypto.createHash(alg);
184 stream.on('data',
19 function(chunk) {
203 hash.update(chunk);
21 }).on('end',
22 function() {
233 callback(null, hash.digest(encoding || 'hex'));
24 }).on('error', function(err) {
251 callback(err);
26 });
27}
28
291module.exports = {
30 md5: function (data, encoding) {
312 return crypto.createHash('md5').update(data).digest(encoding || 'hex');
32 },
33
34 md5file: function(path, encoding, callback) {
352 _hashFile('md5', path, encoding, callback);
36 },
37
38 sha1: function(data, encoding) {
395 return crypto.createHash('sha1').update(data).digest(encoding || 'hex');
40 },
41
42 sha1file: function(path, encoding, callback) {
432 _hashFile('sha1', path, encoding, callback);
44 },
45
46 hmac: function(algo, data, key, encoding) {
479 return crypto.createHmac(algo, key).update(data).digest(encoding || 'hex');
48 },
49
50 hmac_sha1: function(data, key, encoding) {
519 return module.exports.hmac('sha1', data, key, encoding);
52 },
53
54 cipher: function(plain, key, options) {
552 options = options || {};
562 var oe = options.outputEncoding || 'hex';
572 var c = crypto.createCipher(options.algorithm || 'aes256', key);
582 return c.update(plain, options.inputEncoding || 'utf8', oe) + c.final(oe);
59 },
60
61 decipher: function(cipher, key, options) {
622 options = options || {};
632 var oe = options.outputEncoding || 'utf8';
642 var d = crypto.createDecipher(options.algorithm || 'aes256', key);
652 var result = '';
662 try {
672 result = d.update(cipher, options.inputEncoding || 'hex', oe) + d.final(oe);
68 } catch (e) {}
692 return result;
70 },
71
72 base64Encode: function(input, inputEncoding) {
731 var buf = new Buffer(input, inputEncoding || 'utf8');
741 return buf.toString('base64');
75 },
76
77 base64Decode: function(input, outputEncoding) {
781 var base64 = new Buffer(input, 'base64');
791 return base64.toString(outputEncoding || 'utf8');
80 }
81};

lib/genji.js

100%
23
23
0
LineHitsSource
1/**
2 * The global Genji module
3 */
4
5/**
6 * Exposes version of Genji
7 *
8 * @type {string}
9 */
10
111exports.version = exports.VERSION = '0.7.0';
12
13/**
14 * Exposes public apis and modules
15 */
16
171var util = require('./util');
181var extend = util.extend;
19
20/**
21 * Extends frequently used sub-modules to the global Genji module
22 */
23
241extend(exports, util);
251extend(exports, require('./klass'));
261extend(exports, require('./control'));
271extend(exports, require('./core'));
281extend(exports, require('./context'));
291extend(exports, require('./site'));
301extend(exports, require('./router'));
311extend(exports, require('./model'));
321extend(exports, require('./app'));
331extend(exports, {View: require('./view').View});
34
35/**
36 * Extends other sub-modules with module name as namespace
37 */
38
391extend(exports, {auth: require('./auth')});
401extend(exports, {cookie: require('./cookie')});
411extend(exports, {crypto: require('./crypto')});
421extend(exports, {view: require('./view')});
431extend(exports, {plugin: require('./plugin')});
44
45/**
46 * Creates a Router instance
47 *
48 * @param [routes] {Array} Optional predefined routing definitions
49 * @param [options] {Object} Optional router options which may contains:
50 * - contextClass Default context constructor class
51 * - urlRoot The default url prefix
52 * @returns {exports.Router} The router instance
53 */
54
551exports.route = function route(routes, options) {
569 options = options || {};
579 return new exports.Router(routes, options);
58};
59
60/**
61 * Creates a Site instance
62 *
63 * @returns {exports.Site}
64 */
65
661exports.site = function site() {
674 return new exports.Site();
68};

lib/klass.js

100%
21
21
0
LineHitsSource
1/**
2 * Javascript OO helpers
3 */
4
5/**
6 * Module exports.
7 */
8
91exports.Klass = Klass;
10
11/**
12 * Super lightweight javascript OO implementation
13 *
14 * @param superClass {Function} The parent class which you want to inherit from
15 * @param subModule {Object} Instance properties of subclass
16 * @param [inherit] {Function} Optional customized inheriting function for changing the default inheriting behaviour
17 * @return {Function} The inherited subclass
18 * @constructor
19 */
20
21function Klass(superClass, subModule, inherit) {
2237 var global = this;
2337 var superCtor = superClass;
24
2537 if ('function' === typeof subModule) {
264 superCtor = subModule;
274 subModule = subModule.prototype;
28 }
29
3037 var _inherit = function (prototype, subModule) {
3134 Object.keys(subModule).forEach(function (key) {
32114 prototype[key] = subModule[key];
33 });
34 };
35
3637 if (inherit) {
3732 _inherit = inherit;
38 }
39
40 function constructor(subModule, inherit) {
41265 if (global !== this) {
42234 superCtor.apply(this, arguments);
43234 if ('function' === typeof this.init) {
4437 this.init.apply(this, arguments);
45 }
46 } else {
4731 return Klass(constructor, subModule, inherit || _inherit);
48 }
49 }
50
5137 var prototype = constructor.prototype;
5237 prototype.__proto__ = superClass.prototype;
53
5437 if (subModule) {
5536 _inherit(prototype, subModule);
56 }
57
5837 return constructor;
59}
60

lib/model.js

96%
121
117
4
LineHitsSource
1/**
2 * Database agnostic data model class
3 */
4
5/**
6 * Module dependencies
7 */
81var Klass = require('./klass').Klass;
91var extend = require('./util').extend;
101var util = require('util');
11
12/**
13 * Constructor function of Model
14 * @param data {Object} Data that need to be handled by Model
15 * @constructor
16 * @public
17 */
18
19function Model(data) {
20 // mark that we are initializing, changed fields should not be recorded.
214 this.initialized = false;
22 // tell if the input data has `idAttribute` or not. (default `idAttribute` field is `_id`)
234 this.noIdAttribute = !data[this.idAttribute];
24 // object to hold the document data
254 this.data = {};
26 // object contains the invaild fields and values and reasons
274 this.invalidFields = {count: 0, fields: {}};
284 this.setters = this.setters || {};
294 this.getters = this.getters || {};
304 this.aliases = this.aliases || {};
31 // object contains the changed field/value pairs
324 this.changedFields = false;
33
34 // check for required fields
354 var self = this;
364 if (Array.isArray(this.requires) && this.requires.length > 0) {
373 this.requires.forEach(function (fieldName) {
386 if (!data.hasOwnProperty(fieldName)) {
39 // missing field found
401 self.invalidFields.count++;
411 self.invalidFields.fields[fieldName] = {error: this.ERROR_FIELD_MISSING};
42 }
43 });
44 }
45 // set input data to internal `this.data` object, apply validators and setters.
464 Object.keys(data).forEach(function (key) {
4719 self.set(key, data[key]);
48 });
49 // mark that the initialization is finished
504 this.initialized = true;
51}
52
53/**
54 * Model prototype object
55 */
56
571Model.prototype = {
58
59 /**
60 * Name of the Model
61 */
62 name: 'Model',
63 idAttribute: '_id',
64
65 /**
66 * Data field setter function.
67 *
68 * @param key {String} Name of the field
69 * @param value {*} Value of the field
70 * @returns {*} "this"
71 * @public
72 */
73
74 set: function (key, value) {
7521 var isInvalid = this.validateField(key, value);
7621 if (isInvalid) {
773 this.invalidFields.count++;
783 this.invalidFields.fields[key] = {
79 error: isInvalid === true ? this.ERROR_FIELD_INVALID : isInvalid,
80 value: value
81 };
82 } else {
8318 var data = this.data;
8418 var aliasKey = this.aliases[key] || key;
8518 if (this.getInvalidFields().hasOwnProperty(key)) {
86 // if this field is invalid previously, then delete the field from invalidFields hash.
871 this.invalidFields.count--;
881 delete this.invalidFields.fields[key];
89 }
90 // get setter function from setters hash
9118 var setter = this.setters[key];
92 // apply attribute's setter function if any, when original data doc has no `_idAttribute`
9318 var newValue = this.noIdAttribute && setter ? setter.call(this, value) : value;
94 // save the changed value if we're not initializing.
9518 if (this.initialized && data[aliasKey] !== newValue) {
962 this.changedFields = this.changedFields || {};
972 this.changedFields[key] = newValue;
98 }
9918 data[aliasKey] = newValue;
100 }
10121 return this;
102 },
103
104 /**
105 * Get field value by name.
106 *
107 * @param key {String|Array} Name of the field or array of field names
108 * @returns {*} Field value or field/value hash if key is Array
109 * @public
110 */
111
112 get: function (key) {
11318 if (Array.isArray(key)) {
1141 var obj = {};
1151 key.forEach(function (keyName) {
1162 obj[keyName] = this.get(keyName);
117 }, this);
1181 return obj;
119 }
12017 var data = this.data;
121 // use original key name
12217 var aliasKey = this.aliases[key] || key;
12317 var getter = this.getters[key];
12417 return getter ? getter.call(this, data[aliasKey]) : data[aliasKey];
125 },
126
127 /**
128 * Validate field base on the "this.fields" object.
129 *
130 * @param fieldName {String} Name of the field
131 * @param value {*} Value that need to be validated
132 * @returns {Boolean|*} Returns "false" if validated successfully otherwise returns error code
133 */
134
135 validateField: function (fieldName, value) {
13621 if (!this.fields) {
1370 return false;
138 }
139
14021 var field = this.maps[fieldName] || fieldName;
14121 var validator = this.fields[field];
142
14321 if (!validator) {
1440 return false;
145 }
146
14721 var validatorType = typeof validator;
14821 if ('string' === validatorType) {
14912 switch (validator) {
150 case 'number':
151 case 'string':
1527 return validator === typeof value ? false : this.ERROR_FIELD_TYPE;
153 case 'array':
1541 return Array.isArray(value) ? false : this.ERROR_FIELD_TYPE;
155 case 'regexp':
1561 return util.isRegExp(value) ? false : this.ERROR_FIELD_TYPE;
157 case 'date':
1581 return util.isDate(value) ? false : this.ERROR_FIELD_TYPE;
159 case 'bool':
1602 return value === true || value === false ? false : this.ERROR_FIELD_TYPE;
161 }
1629 } else if (validatorType === 'function') {
1638 return validator(value);
164 }
1651 return this.ERROR_VALIDATOR;
166 },
167
168 /**
169 * Check if current data in model is valid or not.
170 *
171 * @returns {Boolean}
172 * @public
173 */
174
175 isValid: function () {
1768 return this.invalidFields.count === 0;
177 },
178
179 /**
180 * Get invalid data fields
181 *
182 * @returns {Boolean|Object} False if all fields are valid otherwise object.
183 * @public
184 */
185
186 getInvalidFields: function () {
18719 return this.invalidFields.count === 0 ? false : this.invalidFields.fields;
188 },
189
190 /**
191 * Get the changed fields after model initialized.
192 *
193 * @returns {*}
194 * @public
195 */
196
197 changed: function () {
1981 return this.changedFields;
199 },
200
201 /**
202 * Return data with fields' name that you want to present to end-user.
203 *
204 * @param [keys] {Array} Array of fields you want to get, if not supplied return all fields.
205 * @returns {Object|Boolean} False if model has invalid field
206 * @public
207 */
208
209 toData: function (keys) {
2102 if (!this.isValid()) {
211 // return false if we have invalid field
2121 return false;
213 }
2141 var _data = this.data;
2151 var data = {};
2161 keys = keys || Object.keys(_data);
2171 var self = this;
2181 var _maps = this.maps;
2191 keys.forEach(function (key) {
2203 data[_maps[key] || key] = self.get(_maps[key] || key);
221 });
2221 return data;
223 },
224
225 /**
226 * Return data with fields' name that you want to save to database.
227 *
228 * @param [keys] {Array} Array of fields you want to get, if not supplied return all fields.
229 * @returns {Object|Boolean} False if model has invalid field
230 * @public
231 */
232
233 toDoc: function (keys) {
2343 if (!this.isValid()) {
235 // return false if we have invalid field
2361 return false;
237 }
2382 var _data = this.data;
2392 var doc = {};
2402 keys = keys || Object.keys(_data);
2412 var self = this;
2422 var _aliases = this.aliases;
2432 var _maps = this.maps;
2442 keys.forEach(function (key) {
2457 doc[_aliases[key] || key] = self.get(_maps[key] || key);
246 });
2472 return doc;
248 },
249
250 /**
251 * Error codes.
252 */
253
254 ERROR_FIELD_MISSING: 1001,
255 ERROR_FIELD_TYPE: 1002,
256 ERROR_FIELD_INVALID: 1003,
257 ERROR_VALIDATOR: 1004
258};
259
260/**
261 * Model inherits function
262 *
263 * @param prototype {Object} Prototype object of parent class
264 * @param subModule {Object}
265 * @private
266 */
267
268function modelInherits(prototype, subModule) {
2692 Object.keys(subModule).forEach(function (propKey) {
27011 var notReserved = false;
27111 switch (propKey) {
272 case 'name':
273 case 'requires':
2743 prototype[propKey] = subModule[propKey];
2753 break;
276 case 'fields':
2772 prototype.fields = extend({}, prototype.fields, subModule.fields);
2782 break;
279 case 'aliases':
2802 var aliases = extend({}, prototype.aliases, subModule.aliases);
2812 var maps = {};
2822 Object.keys(aliases).forEach(function (alias) {
2833 maps[aliases[alias]] = alias;
284 });
2852 prototype.aliases = aliases;
2862 prototype.maps = maps;
2872 break;
288 case 'id':
2890 prototype.idAttribute = subModule.id;
2900 break;
291 default:
2924 notReserved = true;
293 }
29411 if (notReserved) {
2954 if (/^(set|get)[A-Z]+/.test(propKey)) {
2962 var attributeName = propKey.slice(3);
2972 attributeName = attributeName[0].toLowerCase() + attributeName.slice(1);
2982 if (prototype.fields && prototype.fields.hasOwnProperty(attributeName)) {
299 // field is defined, this should be a setter/getter for attribute
3002 var fn = subModule[propKey];
3012 if (typeof fn === 'function') {
3022 switch (propKey.slice(0, 3)) {
303 case 'set':
3041 prototype.setters = prototype.setters || {};
3051 prototype.setters[attributeName] = fn;
3061 break;
307 case 'get':
3081 prototype.getters = prototype.getters || {};
3091 prototype.getters[attributeName] = fn;
3101 break;
311 }
312 }
3132 return;
314 }
315 }
3162 prototype[propKey] = subModule[propKey];
317 }
318 });
319}
320
321/**
322 * Module exports
323 */
324
3251exports.Model = Klass(Model, null, modelInherits);

lib/plugin/cookie.js

93%
16
15
1
LineHitsSource
1/**
2 * Cookie module plugin for context
3 */
4
5/**
6 * Module dependencies.
7 */
8
91var cookie = require('../cookie');
10
11/**
12 * Module exports
13 *
14 * @type {{name: String, module: Object}}
15 * @public
16 */
17
181module.exports = {
19
20 /**
21 * Name of the plugin
22 */
23
24 name: "Cookie",
25
26 /**
27 * Cookie module object
28 *
29 * @public
30 */
31
32 module: {
33
34 /**
35 * Set cookie to response header
36 *
37 * @param name {String} Name of the cookie
38 * @param value {*} Value of the cookie
39 * @param options {Object} Options of the cookie
40 * @public
41 */
42
43 setCookie: function (name, value, options) {
443 var cookieString = cookie.stringify(name, value, options);
453 var cookies = this.getHeader('Set-Cookie');
463 if (!Array.isArray(cookies)) {
472 cookies = [];
48 }
493 cookies.push(cookieString);
503 this.setHeader('Set-Cookie', cookies);
51 },
52
53 /**
54 * Get request cookie value
55 *
56 * @param name {String} Name of the cookie
57 * @returns {*} Value of the cookie if found
58 * @public
59 */
60
61 getCookie: function (name) {
621 if (!this.request.headers.cookie) {
630 return null;
64 }
651 if (!this.cookies) { // request cookie will be parsed only one time
661 this.cookies = cookie.parse(this.request.headers.cookie);
67 }
681 return this.cookies[name];
69 },
70
71 /**
72 * Set a clear cookie to response header
73 *
74 * @param name {String} Name of the cookie need to be cleared
75 * @param options {Object} Options of the cookie
76 */
77
78 clearCookie: function (name, options) {
791 options = options || {};
801 options.expires = new Date(0);
811 this.setCookie(name, "", options);
82 }
83 }
84};

lib/plugin/index.js

100%
1
1
0
LineHitsSource
11require('../util').expose(module);

lib/plugin/parser.js

100%
32
32
0
LineHitsSource
1/**
2 * Simple parser for url query and post/put parameters.
3 */
4
5/**
6 * Module dependencies.
7 */
8
91var parseQuerystring = require('querystring').parse;
101var parseUrl = require('url').parse;
11
12/**
13 * Module exports.
14 *
15 * @type {{name: string, attach: Function}}
16 * @public
17 */
18
191module.exports = {
20
21 /**
22 * Name of the plugin.
23 */
24
25 name: "Parser",
26
27 /**
28 * Setup function, call once during the middleware setup process
29 *
30 * @param core {Object} Instance of Core class
31 * @param [options] {Object} middleware specific settings
32 * @return {Function} Function will be called for each new request
33 * @public
34 */
35
36 attach: function (core, options) {
376 options = options || {};
386 options.encoding = options.encoding || 'utf8';
396 options.maxIncomingSize = options.maxIncomingSize || 256 * 1024;
40
416 return function (request, response, go) {
426 var context = this;
436 var urlObj = parseUrl(request.url);
446 if (urlObj.query) {
452 this.query = parseQuerystring(urlObj.query);
46 }
47
486 if (request.method === 'POST' || request.method === 'PUT' || request.method === 'DELETE') {
494 request.setEncoding(options.encoding);
504 var contentLength = parseInt(request.headers['content-length'], 10);
51
524 if (isNaN(contentLength) || contentLength > options.maxIncomingSize) {
53 // not a vaild request
541 request.removeAllListeners();
551 response.writeHead(413, {'Connection': 'close'});
561 response.end();
571 return;
58 }
59
603 var buff = '';
613 request.on("data", function (chunk) {
623 buff += chunk;
63 });
64
653 var contentType = request.headers['content-type'];
663 request.on("end", function () {
673 context.data = buff;
683 if (contentType === 'application/x-www-form-urlencoded') {
691 context.param = parseQuerystring(buff);
702 } else if (/json|javascript/.test(contentType)) {
712 try {
722 context.json = JSON.parse(buff);
73 } catch (e) {
74 }
75 }
763 go();
77 });
78 } else {
792 go();
80 }
81 };
82 }
83};

lib/plugin/router.js

100%
17
17
0
LineHitsSource
1/**
2 * Router plugin for working with middleware
3 */
4
5/**
6 * Module dependencies.
7 */
8
91var Router = require('../router').Router;
10
11/**
12 * Module exports
13 *
14 * @type {{name: string, attach: Function}}
15 * @public
16 */
17
181module.exports = {
19
20 /**
21 * Name of the plugin
22 */
23
24 name: 'Router',
25
26 /**
27 * Plugin setup function
28 *
29 * @param core {Object} Instance of Core class
30 * @param [options] {Object} Optional options for router plugin or instance of Router
31 * @returns {Function}
32 */
33
34 attach: function (core, options) {
3513 var router;
3613 if (options instanceof Router) {
371 router = options;
38 } else {
3912 options = options || {};
4012 router = new Router(options);
41 }
4213 core.router = router;
43
4413 return function (request, response, go) {
4513 var matched = router.route(request.method, request.url, this, function (request, response) {
461 var notFound = options.notFound || function (request, response) {
471 var error = {
48 statusCode: 404,
49 message: 'Content not found: ' + request.url
50 };
511 this.error(error);
52 };
531 notFound.call(this, request, response);
541 go();
55 });
5613 if (matched) {
5712 go();
58 }
59 };
60 }
61};

lib/plugin/securecookie.js

93%
49
46
3
LineHitsSource
1/**
2 * Plugin for decrypting and verifying secure cookie
3 */
4
5/**
6 * Module dependencies
7 */
8
91var c = require('../crypto');
101var cipher = c.cipher;
111var decipher = c.decipher;
121var auth = require('../auth');
131var cookieHelper = require('../cookie');
141var parse = cookieHelper.parse;
151var stringify = cookieHelper.stringify;
161var extend = require('../util').extend;
17
18/**
19 * Module exports
20 *
21 * @type {{name: string, attach: Function}}
22 */
23
241module.exports = {
25
26 /**
27 * Name of the plugin
28 */
29
30 name: 'SecureCookie',
31
32 /**
33 * Plugin setup function
34 * @param core {Object} Instance object of Core
35 * @param options {Object} Plugin options:
36 * - secureKey {String} Key use to encrypt the signed cookie
37 * - serverKey {String} Hmac key use to sign the cookie
38 * - cookieName {String} Name of the cookie
39 * - [cookiePath] {String} Path of the cookie
40 * - [cookieDomain] {String} Domain of the cookie
41 * @returns {Function}
42 */
43
44 attach: function (core, options) {
452 var secureKey = options.secureKey;
462 var cookieName = options.cookieName;
472 var cookiePath = options.cookiePath;
482 var cookieDomain = options.cookieDomain;
492 var serverKey = options.serverKey;
502 var cookieOption = {};
51
522 if (cookiePath) {
532 cookieOption.path = cookiePath;
54 }
55
562 if (cookieDomain) {
572 cookieOption.domain = cookieDomain;
58 }
59
60 // encrypt cookie if found in response header
612 core.add('writeHead', function (statusCode, headers, next) {
622 if (headers.hasOwnProperty(cookieName)) {
63 // build the cookie string from specified property of `headers`
641 var originalCookie = headers[cookieName];
65 // sign the cookie by hmac
661 var signed = auth.sign(originalCookie[0], originalCookie[1], originalCookie[2], serverKey);
67 // encrypt signed cookie into base64 string
681 var encryptedBase64 = cipher(signed, secureKey, {outputEncoding: 'base64'});
69 // set other cookie option
701 var option = extend({}, cookieOption, originalCookie[3]);
711 option.expires = originalCookie[1];
72
73 // make the cookie
741 var cookieStr = stringify(cookieName, encryptedBase64, option);
75 // delete the original raw cookie
761 delete headers[cookieName];
77
78 // try to put the cookie string into `headers`
791 var setCookie = this.getHeader('Set-Cookie');
801 if (!Array.isArray(setCookie)) {
810 setCookie = [];
82 }
831 setCookie.push(cookieStr);
841 this.setHeader('Set-Cookie', setCookie);
85
861 next(statusCode, headers);
87 } else {
88 // do nothing and go to next step
891 return true;
90 }
91 });
92
932 return function (req, res, go) {
942 var cookies = parse(req.headers.cookie);
952 var cookie = cookies[cookieName];
96
972 if (cookie) {
981 var decrypted = auth.verify(decipher(cookie, secureKey, {inputEncoding: 'base64'}), serverKey);
991 if (!decrypted) {
1000 return go();
101 }
1021 var data = decrypted[2];
1031 try {
1041 data = JSON.parse(decrypted[2]);
105 } catch (e) {
1060 console.error('JSON parsing error for object: %s', data);
107 }
1081 this.session = extend({}, this.session, {
109 cookieId: decrypted[0],
110 cookieExpires: new Date(decrypted[1]),
111 cookieData: data
112 });
113 // for later usage
1141 this.cookies = cookies;
115 }
1162 go();
117 };
118 }
119};

lib/plugin/view.js

94%
17
16
1
LineHitsSource
1/**
2 * View module plugin extends context with template rendering functions
3 */
4
5/**
6 * Module dependencies.
7 */
8
91var View = require('../view').View;
10
11/**
12 * Module exports
13 *
14 * @type {{name: String, module: Object}}
15 * @public
16 */
17
181module.exports = {
19
20 /**
21 * Name of the plugin
22 */
23
24 name: "View",
25
26 attach: function (core, options) {
272 if (options instanceof View) {
280 core.view = options;
29 } else {
302 core.view = new View(options.engine, options);
31 }
32 },
33
34 /**
35 * View module object
36 *
37 * @public
38 */
39
40 module: {
41
42 /**
43 * Render a template/layout with given view context data
44 *
45 * @param [path] {String} Path or name of the template file
46 * @param [ctx] {Object} Data for rendering template
47 * @param [callback] {function} Callback function for handling result, default is to send out result as html text
48 */
49
50 render: function (path, ctx, callback) {
514 if ('string' !== typeof path) {
521 callback = ctx;
531 ctx = path;
541 path = this.viewPath;
55 }
564 var self = this;
574 if ('function' !== typeof callback) {
583 callback = function (err, html) {
593 if (err) {
601 self.error({error: err, statusCode: 503, message: 'Failed to render file', context: self});
611 return;
62 }
632 self.sendHTML(html);
64 };
65 }
664 this.view.renderFile(path, ctx || {}, callback);
67 }
68 }
69};

lib/router.js

90%
194
175
19
LineHitsSource
1/**
2 * Module dependencies
3 */
4
51var chain = require('./control').chain;
61var util = require('./util');
71var extend = util.extend;
81var toArray = util.toArray;
91var Context = require('./core').Context;
10
11/**
12 * Module exports
13 */
14
151exports.Router = Router;
16
17/**
18 * Constructor function of Router class
19 *
20 * @param [routes] {Array} Optional predefined routing definitions
21 * @param [options] {Object} Optional router options which may contains:
22 * - "contextClass" The constructor function of the context class
23 * - "urlRoot" The default url prefix
24 * @constructor
25 * @private
26 */
27
28function Router(routes, options) {
2930 this.rules = [];
3030 if (!Array.isArray(routes)) {
3122 options = routes;
3222 routes = null;
33 }
3430 options = options || {};
3530 this.urlRoot = options.urlRoot || '^/';
3630 if (this.urlRoot[0] !== '^') {
370 this.urlRoot = '^' + this.urlRoot;
38 }
3930 if (this.urlRoot[this.urlRoot.length - 1] !== '/') {
404 this.urlRoot += '/';
41 }
4230 this.contextClass = options.contextClass || Context;
4330 if (Array.isArray(routes)) {
448 this.mount(routes);
45 }
46}
47
48/**
49 * Prototype object of Router class
50 *
51 * @type {Object}
52 */
53
541Router.prototype = {
55
56 /**
57 * Supported routing types
58 */
59
60 supportedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'NOTFOUND'],
61
62 /**
63 * The routing method that routes and dispatches incoming request
64 *
65 * @param type {String} Type of the routing request
66 * @param url {String} Url of the request
67 * @param context {Object} Dispatching context
68 * @param [notFound] {Function} Optional function to handle miss matched request
69 * @return {Boolean} True if matched otherwise false
70 * @public
71 */
72
73 route: function (type, url, context, notFound) {
74 function route(context, input, routes, notFound) {
7546 var _routes = routes[input.type];
7646 if (_routes) {
7745 for (var i in _routes) {
7870 if (_routes.hasOwnProperty(i)) {
7970 var _route = _routes[i];
8070 var condition = input.matched ? input.matched : input.condition;
8170 var matched = _route.match(condition);
82 // matched or replaced
8370 if (matched !== null) {
8448 if (Array.isArray(matched)) {
85 // destination arrived, do dispatching
86 // and keep the context of `route`
8731 _route.dispatch(matched, context);
8831 return true;
8917 } else if (matched !== condition) {
90 // part of the `condition` is replaced, `matched` will become new condition
9110 input.matched = matched;
92 // route to sub-module
9310 if (route(context, input, _route.routes)) {
9410 return true;
95 }
96 }
97 }
98 }
99 }
100 }
101
1025 if ('NOTFOUND' !== input.type) {
103 // if nothing matched, route to `NOTFOUND` and do appropriate operations.
1043 if (route(context, {condition: input.condition, type: 'NOTFOUND', matched: input.matched}, routes)) {
1051 return true;
106 }
107 }
108 // if rules of `NOTFOUND` is not matched, call the `notFound` function if supplied.
1094 return !!(notFound && notFound.call(context, context.request, context.response));
110 }
111
11233 var input = {type: type, condition: url};
11333 return route(context, input, this.getRoutes(), notFound);
114 },
115
116 /**
117 * Add batch of routing rules in declarative format
118 *
119 * @param routes {Array} Array of routes definition
120 * @public
121 */
122
123 mount: function (routes) {
12417 var self = this;
12517 routes.forEach(function (route) {
12627 var def = self.normalizeRouteDefinition(route);
12727 self.add(def.method, def.pattern, def.handler, def.hooks);
128 });
129 },
130
131 /**
132 * Add a signle route definition to this router
133 *
134 * @param method {String} Http method string
135 * @param pattern {String|RegExp} A RegExp instance or string to repersent matching pattern
136 * @param handler {Function} Function that will be called during dispatch
137 * @param [hooks] {Array|Function} An optional hook object:
138 * - Array: an array of hook functions
139 * - Function: a single pre hook function
140 * @return {Object}
141 * @public
142 */
143
144 add: function (method, pattern, handler, hooks) {
14541 var _method = method.toUpperCase();
146
14741 if (this.supportedMethods.indexOf(_method) === -1) {
1480 throw new Error('Routing method `' + _method + '` not supported.');
149 }
150
15141 var rule = {type: _method, pattern: pattern};
152
153 // sanity check for all possible formats of handler
15441 if (Array.isArray(handler) && Array.isArray(handler[0])) {
155 // handler is an array of sub-url routing definitions
1566 var routes = handler;
1576 routes.forEach(function (def) {
15810 if (!Array.isArray(def)) {
1590 throw new Error('Element of sub-url definition should be type of array.');
160 }
161 });
162
1636 var self = this;
1646 routes.forEach(function (url, idx) {
16510 var def = self.normalizeRouteDefinition(url, {method: _method});
16610 if (hooks) {
1674 def.hooks = self.stackHook(hooks, def.hooks);
168 }
16910 routes[idx] = [def.pattern, def.handler, def.method, def.hooks];
170 });
171
1726 var subRouter = new Router(routes);
1736 rule.rules = subRouter.rules;
17435 } else if ('function' === typeof handler) {
175 // the simplest case, `handler` is a function
17635 rule._handleFunction = handler;
17735 if (hooks) {
1787 rule.hooks = hooks;
1797 handler = this.stackHook(hooks, [handler]);
180 // build the function chain
1817 handler = this.chainFn(handler);
182 }
18335 rule.handler = handler;
184 } else {
1850 throw new Error('Format of url definition not correct' +
186 ', second item of definition array should be a function or definition of sub-urls. Got: ' + typeof handler);
187 }
188 // reversed for `NOTFOUND` make it easy to override the default one
18941 if ('NOTFOUND' === _method) {
1901 this.rules.unshift(rule);
191 } else {
19240 this.rules.push(rule);
193 }
19441 return this;
195 },
196
197 /**
198 * Define a route with type 'GET'
199 *
200 * @param url {String|RegExp} A RegExp instance or string to repersent matching pattern
201 * @param handler {Function} Function to handle the request
202 * @param [hooks] {Array|Function} An optional hook object:
203 * - Array: an array of hook functions
204 * - Function: a single pre hook function
205 * @return {Object}
206 * @public
207 */
208
209 get: function (url, handler, hooks) {
2108 this.mount([
211 [url, handler, 'get', hooks]
212 ]);
2138 return this;
214 },
215
216 /**
217 * Define a route with type 'POST'
218 *
219 * @param url {String|RegExp} A RegExp instance or string to repersent matching pattern
220 * @param handler {Function} Function to handle the request
221 * @param [hooks] {Array|Function} An optional hook object:
222 * - Array: an array of hook functions
223 * - Function: a single pre hook function
224 * @return {Object}
225 * @public
226 */
227
228 post: function (url, handler, hooks) {
2291 this.mount([
230 [url, handler, 'post', hooks]
231 ]);
2321 return this;
233 },
234
235 /**
236 * Define a route with type 'PUT'
237 *
238 * @param url {String|RegExp} A RegExp instance or string to repersent matching pattern
239 * @param handler {Function} Function to handle the request
240 * @param [hooks] {Array|Function} An optional hook object:
241 * - Array: an array of hook functions
242 * - Function: a single pre hook function
243 * @return {Object}
244 * @public
245 */
246
247 put: function (url, handler, hooks) {
2480 this.mount([
249 [url, handler, 'put', hooks]
250 ]);
2510 return this;
252 },
253
254 /**
255 * Define a route with type 'DELETE'
256 *
257 * @param url {String|RegExp} A RegExp instance or string to repersent matching pattern
258 * @param handler {Function} Function to handle the request
259 * @param [hooks] {Array|Function} An optional hook object:
260 * - Array: an array of hook functions
261 * - Function: a single pre hook function
262 * @return {Object}
263 * @public
264 */
265
266 'delete': function (url, handler, hooks) {
2670 this.mount([
268 [url, handler, 'delete', hooks]
269 ]);
2700 return this;
271 },
272
273 /**
274 * Define a route with type 'HEAD'
275 *
276 * @param url {String|RegExp} A RegExp instance or string to repersent matching pattern
277 * @param handler {Function} Function to handle the request
278 * @param [hooks] {Array|Function} An optional hook object:
279 * - Array: an array of hook functions
280 * - Function: a single pre hook function
281 * @return {Object}
282 * @public
283 */
284
285 head: function (url, handler, hooks) {
2860 this.mount([
287 [url, handler, 'head', hooks]
288 ]);
2890 return this;
290 },
291
292 /**
293 * Define a route with type 'NOTFOUND'
294 *
295 * @param url {String|RegExp} A RegExp instance or string to repersent matching pattern
296 * @param handler {Function} Function to handle the request
297 * @param [hooks] {Array|Function} An optional hook object:
298 * - Array: an array of hook functions
299 * - Function: a single pre hook function
300 * @return {Object}
301 * @public
302 */
303
304 notFound: function (url, handler, hooks) {
3050 this.mount([
306 [url, handler, 'notFound', hooks]
307 ]);
3080 return this;
309 },
310
311 /**
312 * Get the built routes of the router.
313 *
314 * @return {Object} Routes object which can be used to do routing with `route` method
315 * @public
316 */
317
318 getRoutes: function () {
31934 if (!this.routes) {
32020 this.build();
321 }
32233 return this.routes;
323 },
324
325 /**
326 * Add global hooks to all existent routes
327 *
328 * @param hooks {Function|Array} Hook functions
329 * @public
330 */
331
332 hook: function (hooks) {
3331 hooks = this.normalizeHooks(hooks);
3341 if (Array.isArray(hooks) && hooks.length > 0) {
3351 var self = this;
3361 var _hook = function (rules) {
3377 rules.forEach(function (rule) {
33818 if (rule.rules) {
3396 _hook(rule.rules);
340 } else {
34112 rule.hooks = self.stackHook(hooks, rule.hooks);
34212 rule.handler = self.stackHook(rule.hooks, [rule._handleFunction]);
34312 rule.handler = self.chainFn(rule.handler);
344 }
345 });
346 };
3471 _hook(this.rules);
3481 this.routes = this.build();
349 }
350 },
351
352 /**
353 * Generate a url based on the camel cased name string
354 *
355 * @param name {String} Camel cased name
356 * @returns {string}
357 * @public
358 */
359
360 slashCamelCase: function (name) {
36120 name = name.replace(/([A-Z]+)/g, function (word) {
36234 return '/' + word.toLowerCase();
363 });
36420 return name;
365 },
366
367 /**
368 * Combine url with perfix
369 *
370 * @param prefix {String} Prefix string
371 * @param url {String} Url string
372 * @returns {String}
373 * @public
374 */
375
376 prefixUrl: function (prefix, url) {
37759 switch (url[0]) {
378 case '^':
37952 return url;
380 case '/':
3813 url = url.slice(1);
3823 break;
383 }
384
3857 if (prefix[prefix.length - 1] !== '/') {
3860 prefix += '/';
387 }
3887 url = prefix + url;
3897 return url;
390 },
391
392 /**
393 * Listen to the request event of HttpServer instance
394 *
395 * @param server {Object} Instance object of "http.HttpServer"
396 * @param contextClass {Function} The context constructor function
397 * @param [notFound] {Function} Optional function to handle miss matched url
398 * @public
399 */
400
401 listen: function (server, contextClass, notFound) {
4028 var self = this;
4038 contextClass = contextClass || this.contextClass;
4048 if ('function' !== typeof notFound) {
4058 notFound = function (request, response) {
4060 response.statusCode = 404;
4070 response.end('404 not found!', 'utf8');
4080 console.error('Cannot match a route for url "%s" method "%s"', request.url, request.method);
409 };
410 }
4118 server.on('request', function (request, response) {
4128 var context = new contextClass(request, response);
4138 self.route(request.method, request.url, context, notFound);
414 });
415 },
416
417 /**
418 * Check the input url pattern and format it to regexp if need
419 *
420 * @param {String|RegExp} pattern Url pattern in format of string or regular expression
421 * @returns {RegExp}
422 * @throws {Error}
423 * @private
424 */
425
426 normalizePattern: function (pattern) {
42758 if (pattern instanceof RegExp) {
4280 return pattern;
429 }
43058 if (typeof pattern === "string") {
43157 pattern = this.prefixUrl(this.urlRoot, pattern);
43257 return new RegExp(pattern);
433 }
4341 throw new Error("Invalid url pattern");
435 },
436
437 /**
438 * Normalize the route definition array to object
439 *
440 * @param route {Array} Single route definition array
441 * @param [defaults] {Object} Optional default value for missed value, only support `method` (e.g. {method: 'get'})
442 * @return {Object}
443 * @private
444 */
445
446 normalizeRouteDefinition: function (route, defaults) {
44737 var method = route[2];
44837 var hooks = route[3];
44937 defaults = defaults || {};
450
45137 if ('string' !== typeof method) {
4528 hooks = method;
4538 method = defaults.method || 'get';
454 }
455
45637 hooks = this.normalizeHooks(hooks);
457
45837 return {pattern: route[0], handler: route[1], method: method, hooks: hooks};
459 },
460
461 /**
462 * Convert function to array and add `null` to end of array if necessary
463 *
464 * @param hooks
465 * @returns {Array}
466 * @private
467 */
468
469 normalizeHooks: function (hooks) {
47073 if (hooks) {
471 // do sanity check for pre hooks
47244 if ('function' === typeof hooks) {
4731 hooks = [hooks];
47443 } else if (!Array.isArray(hooks)) {
4750 throw new Error('Hooks must be a function or array of functions');
476 }
47744 hooks.forEach(function (hook) {
478142 if ('function' !== typeof hook && null !== hook) {
4790 throw new Error('Hooks must be array of functions with optional null placeholder for function being hooked.');
480 }
481 });
482
48344 if (hooks.indexOf(null) === -1) {
484 // all pre hooks
4853 hooks.push(null);
486 }
487 }
48873 return hooks;
489 },
490
491 /**
492 * Wrap targetHooks with srcHooks
493 *
494 * @param srcHooks {Array} Array of src hooks
495 * @param [targetHooks] Optional target hooks
496 * @returns {Array}
497 * @public
498 */
499
500 stackHook: function (srcHooks, targetHooks) {
50135 targetHooks = targetHooks || [null];
50235 srcHooks = this.normalizeHooks(srcHooks);
50335 var idx = srcHooks.indexOf(null);
50435 var pre = srcHooks.slice(0, idx++);
50535 targetHooks = pre.concat(targetHooks);
50635 if (idx !== srcHooks.length) {
50734 var post = srcHooks.slice(idx);
50834 targetHooks = targetHooks.concat(post);
509 }
51035 return targetHooks;
511 },
512
513 /**
514 * Convert array of functions to a function chain which can be called in serial order
515 *
516 * @param fns {Array} Array of functions
517 * @returns {Function}
518 * @private
519 */
520
521 chainFn: function (fns) {
52219 var fn = chain(fns, function (h, idx, arr, next, args) {
52342 args = toArray(args);
52442 args.push(next);
52542 if (h.apply(this, args)) {
52622 next();
527 }
528 });
52919 return function () {
53012 fn(0, arguments, this);
531 };
532 },
533
534 /**
535 * Build and return routes definition object which can be used by `route` method
536 *
537 * @param [rules] {Array}
538 * @return {Object}
539 * @public
540 */
541
542 build: function (rules) {
54333 var routes = {};
54433 var _rules = rules;
54533 rules = rules || this.rules;
54633 var self = this;
54733 rules.forEach(function (rule) {
54858 var hasSub = Array.isArray(rule.rules) && rule.rules.length > 0;
54958 var type = rule.type.toUpperCase();
55058 var handler = rule.handler;
55158 var pattern = self.normalizePattern(rule.pattern);
552
55357 var current = {id: type + '_' + pattern.toString()};
554
55557 if (hasSub) {
556 // sub-urls
55712 current.match = function (condition) {
55817 return condition.replace(pattern, '');
559 };
56012 current.routes = self.build(rule.rules);
56112 type = 'PREFIX';
562 } else {
56345 current.dispatch = function (matched, context) {
56431 matched[0] = context;
56531 handler.apply(null, matched);
566 };
56745 current.match = function (condition) {
56853 return pattern.exec(condition);
569 };
570 }
571
57257 if (!routes.hasOwnProperty(type)) {
57341 routes[type] = {};
574 }
575
57657 routes[type][current.id] = current;
577 });
578
57932 if (routes.PREFIX) {
5808 this.supportedMethods.forEach(function (type) {
58148 if (type !== 'NOTFOUND') {
582 // method of sub-url may various, so put `prefix` to all types
58340 routes[type] = routes[type] || {};
58440 extend(routes[type], routes.PREFIX);
585 }
586 });
587 }
588
58932 if (!_rules) {
59020 this.routes = routes;
591 }
59232 return routes;
593 }
594};
595

lib/site.js

92%
101
93
8
LineHitsSource
1/**
2 * Module dependencies
3 */
4
51var http = require('http');
61var EventEmitter = require('events').EventEmitter;
71var Klass = require('./klass').Klass;
81var extend = require('./util').extend;
91var Core = require('./core').Core;
10
11/**
12 * Site constructor function
13 *
14 * @constructor
15 * @private
16 */
17
18function Site() {
194 EventEmitter.call(this);
204 if (!process.env.NODE_ENV) {
211 process.env.NODE_ENV = 'default';
22 }
234 this.data = {};
244 this.plugins = {};
254 this.apps = {};
264 this.core = new Core(this);
274 this.env();
284 this.set({
29 host: '127.0.0.1',
30 port: 8888
31 });
32}
33
34/**
35 * Site prototype object
36 *
37 * @type {Object}
38 */
39
401Site.prototype = {
41
42 /**
43 * Switch between different environments
44 *
45 * @param [name] {String} Optional env name, default is 'default'
46 * @returns {Site}
47 * @public
48 */
49
50 env: function (name) {
517 name = name || 'default';
527 if (!this.data[name]) {
536 this.data[name] = {};
546 this.plugins[name] = {};
556 this.apps[name] = {};
56 }
577 this._env = name;
587 return this;
59 },
60
61 /**
62 * Store a value for a key in current env
63 *
64 * @param key {String|Object} Key of the value or object of key/value pairs that need to be stored
65 * @param value {*} Value that need to be stored
66 * @returns {Object} The "this" object
67 * @public
68 */
69
70 set: function (key, value) {
7110 return _set(this, this.data, key, value);
72 },
73
74 /**
75 * Get value for key
76 *
77 * @param key {String|Array} Key of the value or array of keys
78 * @returns {*} Value stored
79 * @public
80 */
81
82 get: function (key) {
836 return _get(this, this.data, key);
84 },
85
86 /**
87 * Use a plugin in current env
88 *
89 * @param plugin {String|Object} The name string of the built-in plugin or the plugin object
90 * @param [options] {Object} Optional plugin options
91 * @returns {Object} The "this" object
92 * @public
93 */
94
95 use: function (plugin, options) {
967 var name = 'string' === typeof plugin ? plugin : plugin.name;
977 var _plugin = {
98 plugin: plugin,
99 options: options
100 };
101
1027 return _set(this, this.plugins, name, _plugin);
103 },
104
105 /**
106 * Load an app for current env
107 *
108 * @param app {Function|Object|Array} App constructor or instance or array of apps
109 * @param [options] {Object} Optional options for app constructor or routes if app is an instance object
110 * @param [routes] {Object} Optional routes object
111 * @returns {Object}
112 * @public
113 */
114
115 load: function (app, options, routes) {
1161 if (Array.isArray(app)) {
1170 var self = this;
1180 app.forEach(function (_app) {
1190 self.load(_app, options, routes);
120 });
121 }
122
1231 var name;
124
1251 if ('function' === typeof app) {
1261 name = app.prototype.name;
1271 if (!routes) {
1281 routes = options;
129 }
130 } else {
1310 name = app.name;
1320 routes = options;
1330 options = null;
134 }
135
1361 var _app = {
137 app: app,
138 options: options
139 };
140
1411 this.map(routes);
142
1431 return _set(this, this.apps, name, _app);
144 },
145
146 map: function (routes) {
1472 if (routes) {
1481 var _routes = this.get('routes') || {};
1491 _routes = extend(_routes, routes);
1501 this.set('routes', _routes);
151 }
1522 return this;
153 },
154
155 /**
156 * Process all the plugins with core
157 *
158 * @private
159 */
160
161 usePlugins: function () {
1624 var plugins = extend({}, this.plugins['default']);
1634 if (process.env.NODE_ENV && 'default' !== process.env.NODE_ENV) {
1642 extend(plugins, this.plugins[process.env.NODE_ENV]);
165 }
166
1674 var core = this.core;
1684 Object.keys(plugins).forEach(function (name) {
1697 var plugin = plugins[name];
1707 core.use(plugin.plugin, plugin.options);
171 });
1724 return this;
173 },
174
175 /**
176 * Process all apps in current environment.
177 *
178 * @private
179 */
180
181 loadApps: function () {
1824 var apps = extend({}, this.apps['default']);
1834 if (process.env.NODE_ENV && 'default' !== process.env.NODE_ENV) {
1842 apps = extend(apps, this.apps[process.env.NODE_ENV]);
185 }
186
1874 var defaultAppOptions = this.get('appOptions');
1884 var self = this;
1894 var core = self.core;
190
1914 Object.keys(apps).forEach(function (k) {
1921 var appObj = apps[k];
1931 var appOptions = extend({}, defaultAppOptions, appObj.options);
1941 var app = appObj.app;
1951 if ('function' === typeof app) {
1961 app = new app(appOptions);
197 }
1981 app.delegate = self;
1991 core.load(app);
200 });
2014 return this;
202 },
203
204 mapRoutes: function () {
2054 var routes = extend({}, this.data['default'].routes);
2064 if (process.env.NODE_ENV && 'default' !== process.env.NODE_ENV) {
2072 var dataEnv = this.data[process.env.NODE_ENV];
2082 routes = dataEnv ? extend(routes, dataEnv.routes) : routes;
209 }
2104 this.core.map(routes);
2114 return this;
212 },
213
214 /**
215 * Create and start the http server using settings of current env
216 *
217 * @param [server] {Object} Optional HttpServer instance
218 * @returns {Object} HttpServer instance
219 * @public
220 */
221
222 start: function (server) {
2234 this.usePlugins();
2244 this.loadApps();
2254 this.mapRoutes();
2264 var listener = this.core.getListener();
2274 if (server) {
2284 server.on('request', listener);
229 } else {
2300 server = http.createServer(listener);
2310 server.listen(this.get('port'), this.get('host'));
232 }
2334 this.server = server;
2344 return server;
235 }
236};
237
238/**
239 * Exposes Site
240 */
241
2421exports.Site = Klass(EventEmitter, Site);
243
244/**
245 * Private setter function
246 *
247 * @param self {Object} The `this` object
248 * @param data {Object} Data hash to store values
249 * @param key {String|Object} Key of the value or object of key/value pairs that need to be stored
250 * @param value {*} Value that need to be stored
251 * @returns {Object} Self object
252 * @private
253 */
254
255function _set(self, data, key, value) {
25618 var _data = data[self._env];
25718 if ('object' === typeof key) {
2584 extend(_data, key);
259 } else {
26014 _data[key] = value;
261 }
26218 return self;
263}
264
265
266/**
267 * Private getter function
268 *
269 * @param self {Object} The `this` object
270 * @param data {Object} Data hash to store values
271 * @param key {String|Array} Key of the value or array of keys
272 * @returns {*} Value stored
273 * @private
274 */
275
276function _get(self, data, key) {
2778 if (Array.isArray(key)) {
2781 var values = {};
2791 key.forEach(function (k) {
2802 values[k] = _get(self, data, k);
281 });
2821 return values;
283 }
2847 var _data = data[process.env.NODE_ENV];
2857 if (!_data || !_data.hasOwnProperty(key)) {
2865 _data = data['default'];
287 }
2887 return _data[key];
289}
290

lib/util.js

93%
47
44
3
LineHitsSource
1/**
2 * Module dependencies
3 */
4
51var path = require('path');
61var fs = require('fs');
7
8/**
9 * Module exports
10 */
11
121exports.extend = extend;
131exports.toArray = toArray;
141exports.byteLength = byteLength;
151exports.expose = expose;
16
17/**
18 * Convert an array-like object to array (arguments etc.)
19 *
20 * @param obj
21 * @return {Array}
22 */
23
24function toArray(obj) {
25332 var len = obj.length,
26 arr = new Array(len);
27332 for (var i = 0; i < len; ++i) {
28601 arr[i] = obj[i];
29 }
30332 return arr;
31}
32
33/**
34 * Extend an object with given properties
35 *
36 * @param obj
37 * @param props
38 * @return {*}
39 */
40
41function extend(obj, props) {
42142 var propsObj = toArray(arguments);
43142 propsObj.shift();
44142 propsObj.forEach(function(props) {
45166 if (props) {
46126 Object.keys(props).forEach(function(key) {
47188 obj[key] = props[key];
48 });
49 }
50 });
51142 return obj;
52}
53
54/**
55 * Calculate byte length of string
56 *
57 * @param str
58 * @return {Number}
59 */
60
61function byteLength(str) {
621 if (!str) {
630 return 0;
64 }
651 var matched = str.match(/[^\x00-\xff]/g);
661 return (str.length + (!matched ? 0 : matched.length));
67}
68
69/**
70 * Export sub modules under the module's directory
71 *
72 * @param module {Object} The global "module" inside each node.js source file context
73 * @param subModules {String|Array} Name(s) of the sub-module you want to expose
74 * @public
75 */
76
77function expose(module, subModules) {
786 var modulePath = path.dirname(module.filename);
796 var exports_ = module.exports;
806 if (subModules) {
815 if (!Array.isArray(subModules)) {
825 subModules = [subModules];
83 }
845 subModules.forEach(function (sub) {
855 var subName;
865 var subPath;
875 if (sub[0] === '/') {
885 subPath = sub;
895 sub = sub.split('/');
905 sub = sub[sub.length - 1];
91 } else {
920 subName = sub;
930 subPath = path.join(modulePath, subName);
94 }
955 subName = sub.split('.')[0];
965 var _module = {};
975 _module[subName] = require(subPath);
985 extend(exports_, _module);
99 });
100 } else {
1011 subModules = fs.readdirSync(modulePath);
1021 if (subModules.length > 0) {
1031 subModules.forEach(function (sub) {
1046 var subPath = path.join(modulePath, sub);
1056 var stats = fs.statSync(subPath);
1066 if ((stats.isDirectory() && fs.existsSync(path.join(subPath, 'index.js'))) || (stats.isFile() && path.extname(subPath) === '.js')) {
1076 if (subPath !== module.filename) {
1085 expose(module, subPath);
109 }
110 }
111 });
112 }
113 }
114}
115

lib/view.js

98%
159
157
2
LineHitsSource
1/**
2 * View template manager
3 */
4
5/**
6 * Module dependencies
7 */
8
91var Path = require('path');
101var fs = require('fs');
111var Base = require('./klass').Base;
121var extend = require('./util').extend;
131var parallel = require('./control').parallel;
14
15/**
16 * View constructor function.
17 *
18 * @param engine {Object} Template engine instance object (e.g. hogan.js)
19 * @param [options] {Object} View options:
20 * {
21 * rootViewPath: '/path/to/your/view/templates', // The default absolute directory path of your template files
22 * cache: true, // cache or not
23 * context: {x: 1}, // the default context of the view instance,
24 * exts: ['.mu', '.js'] // template file extentions,
25 * layout: {'name': ['partial.mu']} // initial layout
26 * }
27 * @constructor
28 * @public
29 */
30
31function View(engine, options) {
3210 if ('function' !== typeof engine.render && 'function' !== typeof engine.compile) {
330 throw new Error('Your template engine must implements at least one of "render" or "compile" functions.');
34 }
35
3610 this.engine = engine;
3710 options = options || {};
3810 this.exts = options.exts || ['.mu', '.js'];
3910 this.contexts = {};
4010 this.viewPaths = {};
4110 this.rootViewPaths = {};
4210 this.partials = {};
4310 this.partialsEncoded = {};
44
4510 if (options.rootViewPath) {
467 this.rootViewPath = options.rootViewPath;
477 this.registerPartialDir();
48 }
49
5010 this.hasCompiler = 'function' === typeof this.engine.compile;
51
5210 if (options.cache) {
532 this.cache = {};
542 if (this.hasCompiler) {
552 this.cacheCompiled = {};
56 }
57 }
5810 if (options.context) {
592 this.context = options.context;
60 }
6110 if (options.layout) {
621 this.setLayout(options.layout);
63 }
6410 if (options.minify) {
651 this.minify = options.minify;
66 }
67}
68
69/**
70 * View prototype object
71 */
72
731View.prototype = {
74
75 /**
76 * Set default rendering context for template file or layout
77 *
78 * @param path {String} Path/name of the template/layout
79 * @param context {Object} Context object
80 * @returns {*}
81 * @public
82 */
83
84 setContext: function (path, context) {
851 this.contexts[path] = context;
861 return this;
87 },
88
89 /**
90 * Render template with given context and partials
91 *
92 * @param templateString {String} Template text string
93 * @param [context] {Object} Template context object
94 * @param [partials] {Object} Optional template partial object
95 * @returns {String}
96 * @public
97 */
98
99 render: function (templateString, context, partials) {
10013 if (this.hasCompiler) {
10110 var compiled;
10210 if (this.cacheCompiled) {
1034 compiled = this.cacheCompiled[templateString];
104 }
10510 if (!compiled) {
1069 compiled = this.engine.compile(templateString);
1079 if (this.cacheCompiled) {
1083 this.cacheCompiled[templateString] = compiled;
109 }
110 }
11110 return compiled.render(extend({}, this.context, context), partials);
112 } else {
1133 return this.engine.render(templateString, extend({}, this.context, context), partials);
114 }
115 },
116
117 /**
118 * Read the template from file and render it
119 *
120 * @param path {String} Path to the template file
121 * @param [context] {Object} Template context object
122 * @param callback {Function} Callback function which accepts following arguments:
123 * {Object} err Error object,
124 * {String} rendered Rendered text
125 * @public
126 */
127
128 renderFile: function (path, context, callback) {
12910 if ('function' === typeof context) {
1301 callback = context;
1311 context = null;
132 }
133
13410 var self = this;
13510 var partials = this.getPartials();
13610 var path_ = this.getViewPath(path);
137
13810 if (this.contexts[path]) {
1391 context = extend({}, this.contexts[path], context);
140 }
141
14210 if (this.cache && this.cache[path_]) {
1431 callback(null, this.render(this.cache[path_], context, partials));
1441 return;
145 }
146
1479 fs.readFile(path_, 'utf8', function (err, templateString) {
1489 if (err) {
1491 callback(err, '');
150 } else {
1518 if (self.cache) {
1522 self.cache[path_] = templateString;
153 }
1548 callback(null, self.render(templateString, context, partials));
155 }
156 });
157 },
158
159 /**
160 * Set a layout view.
161 *
162 * @param name {String|Object} Name of the layout of name/layout pair hash object
163 * @param [layout] {Array} Array of partials' name for the layout
164 * @returns {Object}
165 * @public
166 */
167
168 setLayout: function (name, layout) {
1692 this.layout = this.layout || {};
170 function layoutToStr(arr) {
1712 var str = '';
1722 arr.forEach(function (l) {
1737 str += '{{> ' + l + '}}\n';
174 });
1752 return str;
176 }
177
1782 if (typeof name === 'string') {
1791 this.layout[name] = layoutToStr(layout);
180 } else {
1811 Object.keys(name).forEach(function (n) {
1821 this.layout[n] = layoutToStr(name[n]);
183 }, this);
184 }
1852 return this;
186 },
187
188 /**
189 * Render a layout with optional context.
190 *
191 * @param layoutName {String} Name of the layout
192 * @param [context] {Object} Optional context
193 * @returns {String} Rendered result
194 * @public
195 */
196
197 renderLayout: function (layoutName, context) {
1982 var layoutStr = this.layout[layoutName];
1992 if (!layoutStr) {
2000 throw new Error('Layout not exists');
201 }
2022 if (this.contexts) {
2032 context = extend({}, this.contexts[layoutName], context);
204 }
2052 return this.render(layoutStr, context, this.getPartials());
206 },
207
208 /**
209 * Register a template partial
210 *
211 * @param name {String} Name of your partial or path to the partial file if `templateString` is not provided
212 * @param [templateString] {String} Template text
213 * @param [namespace] {String} Optional namespace of the partial
214 * @public
215 */
216
217 registerPartial: function (name, templateString, namespace) {
218121 var name_ = name;
219
220121 if (namespace) {
2217 name_ = namespace + ':' + name;
222 }
223
224121 var self = this;
225121 var minify;
226 // try to get minify function for current partial type when caching is enabled
227121 if (this.cache && this.minify) {
2284 var ext = Path.extname(name);
2294 if (this.minify.hasOwnProperty(ext)) {
2301 minify = this.minify[ext];
231 }
232 }
233
234121 if (!templateString) {
235112 var path_ = this.getViewPath(name_);
236112 templateString = fs.readFileSync(path_, 'utf8');
237 }
238
239121 if (minify) {
2401 templateString = minify(templateString);
241 }
242121 templateString = self.wrapPartial(name, templateString);
243121 self.partials[name_] = templateString;
244121 self.partialsEncoded[name_] = encodeURIComponent(templateString);
245121 self.partialsString = JSON.stringify(self.partialsEncoded);
246 },
247
248 /**
249 * Get partial object. Reload partials if caching is not enabled.
250 *
251 * @returns {Object}
252 * @public
253 */
254
255 getPartials: function () {
25612 if (!this.cache) {
2578 this.registerPartialDir();
2588 Object.keys(this.rootViewPaths).forEach(function (namespace) {
2591 this.registerPartialDir('', namespace);
260 }, this);
261 }
26212 return this.partials;
263 },
264
265 /**
266 * Set namespaced view path, register partials under the path with namespace.
267 *
268 * @param pathNamespace {String} Namespace of the path
269 * @param viewPath {String} Directory path
270 * @returns {Object}
271 * @public
272 */
273
274 setViewPath: function (pathNamespace, viewPath) {
2751 this.rootViewPaths[pathNamespace] = viewPath;
2761 this.registerPartialDir('', pathNamespace);
2771 return this;
278 },
279
280 /**
281 * Get absolute path of the view.
282 *
283 * @param path {String} Relative path
284 * @returns {String}
285 * @public
286 */
287
288 getViewPath: function (path) {
289122 if ((Path.sep || '/') === path[0]) {
2903 return path;
291 }
292119 var path_ = this.viewPaths[path];
293119 if (!path_) {
2946 path_ = path.split(':');
2956 if (1 === path_.length) {
2965 path_ = Path.join(this.rootViewPath, path);
297 } else {
2981 path_ = Path.join(this.rootViewPaths[path_[0]], path_[1]);
299 }
300 }
301119 return path_;
302 },
303
304 /**
305 * Set the url of script loader. Now only support head.js.
306 * @param url {String} Url to loader script
307 * @returns {Object}
308 * @public
309 */
310
311 setScriptLoaderUrl: function (url) {
3121 this.scriptLoaderUrl = url;
3131 return this;
314 },
315
316 /**
317 * Registry script partial with given script names
318 *
319 * @param partialName {String} Name of the script partial
320 * @param scripts {Array} Names of script add to this partial
321 * @param onLoad {Function} Script on load function
322 * @returns {Object}
323 * @public
324 */
325
326 registerScriptPartial: function (partialName, scripts, onLoad) {
3272 var scriptStr = ['<script src="', this.scriptLoaderUrl, '" type="text/javascript"></script>\n'].join('');
3282 scriptStr += '<script type="text/javascript">\n';
3292 var self = this;
3302 scriptStr += 'head.js(';
3312 var lastIdx = scripts.length - 1;
3322 scripts.forEach(function (scriptName, idx) {
3333 var url = self.scripts[scriptName];
3343 scriptStr += "'" + url + "'";
3353 if (idx < lastIdx) {
3361 scriptStr += ', ';
337 }
338 });
339
3402 if (typeof onLoad === 'function') {
3411 scriptStr += ', ' + onLoad.toString();
342 }
3432 scriptStr += ');\n</script>';
3442 var pName = partialName.split(':');
3452 if (pName.length === 2) {
346 // partial has namespace
3471 this.registerPartial(pName[1], scriptStr, pName[0]);
348 } else {
3491 this.registerPartial(partialName, scriptStr);
350 }
3512 return this;
352 },
353
354 /**
355 * Store script name/url pair to view.
356 *
357 * @param urls {Object|String} Name and url hash or name of the url
358 * @param [url] {String} Optional url string if "urls" is string
359 * @returns {Object}
360 * @public
361 */
362
363 setScriptUrl: function (urls, url) {
3643 var self = this;
365
366 function _set(name, url) {
3673 self.scripts = self.scripts || {};
3683 self.scripts[name] = url;
369 }
370
3713 if (typeof urls === 'string') {
3722 _set(urls, url);
373 } else {
3741 Object.keys(urls).forEach(function (name) {
3751 _set(name, urls[name]);
376 });
377 }
3783 return this;
379 },
380
381 /**
382 * Recursively register all template partial files under the directory.
383 *
384 * @param [relativeDir] {String} Relative path of the directory
385 * @param [namespace] {String} Namespace of the path
386 * @private
387 */
388
389 registerPartialDir: function (relativeDir, namespace) {
39038 relativeDir = relativeDir || '';
39138 var self = this;
39238 var rootViewPath = (namespace ? this.rootViewPaths[namespace] : this.rootViewPath) || this.rootViewPath;
39338 var exts = this.exts;
39438 var dir = Path.join(rootViewPath, relativeDir);
39538 var files = fs.readdirSync(dir);
396
39738 files.forEach(function (file) {
398171 var absolutePath = Path.join(rootViewPath, relativeDir, file);
399171 var relativeFilePath = Path.join(relativeDir, file);
400171 var stats = fs.statSync(absolutePath);
401171 if (stats.isFile() && exts.indexOf(Path.extname(absolutePath)) > -1) {
402112 var viewName = (namespace ? namespace + ':' : '') + relativeFilePath;
403112 self.viewPaths[viewName] = absolutePath;
404112 self.registerPartial(relativeFilePath, null, namespace);
40559 } else if (stats.isDirectory()) {
40621 self.registerPartialDir(relativeFilePath, namespace);
407 }
408 });
409 },
410
411 /**
412 * Wrap partial with script tag if it's a javascript partial
413 *
414 * @param filename {String} Name of the partial
415 * @param str {String} Content of the partial
416 * @returns {String}
417 * @private
418 */
419
420 wrapPartial: function (filename, str) {
421121 var type = Path.extname(filename);
422121 if ('.js' === type) {
42379 str = '<script type="text/javascript">' + str + '</script>';
424 }
425121 return str;
426 }
427};
428
429/**
430 * Expose View.
431 * @type {Function}
432 */
433
4341exports.View = View;
435