Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Module dependencies | |
3 | */ | |
4 | ||
5 | 1 | var toArray = require('./util').toArray; |
6 | 1 | var EventEmitter = require('events').EventEmitter; |
7 | 1 | var Klass = require('./klass').Klass; |
8 | ||
9 | /** | |
10 | * App constructor function | |
11 | * | |
12 | * @constructor | |
13 | */ | |
14 | ||
15 | function App() { | |
16 | 11 | EventEmitter.call(this); |
17 | 11 | var self = this; |
18 | 11 | self.publicMethods = {}; |
19 | ||
20 | 11 | for (var k in self) { |
21 | 228 | if (self.isPublicMethodName(k)) { |
22 | 21 | this.processPublicMethod(k); |
23 | } | |
24 | } | |
25 | } | |
26 | ||
27 | /** | |
28 | * App prototype object | |
29 | */ | |
30 | ||
31 | 1 | App.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) { | |
46 | 228 | 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 () { | |
56 | 6 | var emitter = this.delegate || this; |
57 | 6 | var args = toArray(arguments); |
58 | 6 | if (this.delegate && this.prefixDelegatedEvent) { |
59 | 3 | var eventName = 'string' === typeof this.prefixDelegatedEvent ? this.prefixDelegatedEvent : this.name; |
60 | 3 | eventName += ':' + args[0]; |
61 | 3 | args[0] = this; |
62 | 3 | args.unshift(eventName); |
63 | } | |
64 | 6 | 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) { | |
75 | 21 | var self = this; |
76 | 21 | var f = self[propName]; |
77 | 21 | if ('function' === typeof f) { |
78 | 21 | self[propName] = self.publicMethods[propName] = (function (name, f) { |
79 | 21 | return function () { |
80 | 10 | var args = toArray(arguments); |
81 | 10 | var callback = args[args.length - 1]; |
82 | 10 | var hasCallback = 'function' === typeof callback; |
83 | 10 | var resultIsNotUndefined = false; |
84 | ||
85 | 10 | var wrapper = function () { |
86 | 11 | if (resultIsNotUndefined) { |
87 | 2 | return; |
88 | } | |
89 | 9 | var _args = toArray(arguments); |
90 | ||
91 | 9 | if (hasCallback) { |
92 | 5 | if ('before' === self.emitInlineCallback) { |
93 | 1 | _args.unshift(name); |
94 | 1 | self.emit.apply(self, _args); |
95 | 1 | _args.shift(); |
96 | } | |
97 | 5 | callback.apply(self, _args); |
98 | } | |
99 | ||
100 | 9 | if (!hasCallback || 'after' === self.emitInlineCallback) { |
101 | 5 | _args.unshift(name); |
102 | 5 | self.emit.apply(self, _args); |
103 | } | |
104 | }; | |
105 | ||
106 | 10 | if (hasCallback) { |
107 | 5 | args[args.length - 1] = wrapper; |
108 | } else { | |
109 | 5 | args.push(wrapper); |
110 | } | |
111 | ||
112 | 10 | var result = f.apply(self, args); |
113 | 10 | resultIsNotUndefined = 'undefined' !== typeof result; |
114 | 10 | return result; |
115 | }; | |
116 | })(propName, f); | |
117 | } | |
118 | } | |
119 | }; | |
120 | ||
121 | /** | |
122 | * Add more reserved name | |
123 | * | |
124 | * @type {Array} | |
125 | */ | |
126 | ||
127 | 1 | App.prototype.reservedMethodNames = App.prototype.reservedMethodNames.concat(Object.keys(App.prototype), 'init', 'domain', 'name'); |
128 | ||
129 | /** | |
130 | * Module exports | |
131 | */ | |
132 | ||
133 | 1 | exports.App = Klass(EventEmitter, App); |
134 |
Line | Hits | Source |
---|---|---|
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 | ||
11 | 1 | var c = require('./crypto'), |
12 | sha1 = c.sha1, | |
13 | hmac_sha1 = c.hmac_sha1; | |
14 | ||
15 | function sign(username, expiration, data, serverKey) { | |
16 | 2 | var k = hmac_sha1([username, expiration].join("|"), serverKey); |
17 | 4 | if (typeof data !== "string") data = JSON.stringify(data); |
18 | 2 | var raw = [username, expiration, data].join("|"); |
19 | 2 | return raw + "|" + hmac_sha1(raw, k); |
20 | } | |
21 | ||
22 | function verify(input, serverKey) { | |
23 | 2 | if (typeof input !== "string") return false; |
24 | 2 | var values = input.split("|"); |
25 | 2 | if (values.length !== 4) return false; |
26 | 2 | if (new Date() >= new Date(values[1])) return false; |
27 | 2 | var k = hmac_sha1([values[0], values[1]].join("|"), serverKey); |
28 | 2 | return values.pop().replace(/[\x00-\x20]/g, "") === hmac_sha1(values.join("|"), k) ? values : false; |
29 | } | |
30 | ||
31 | function checkPassword(password, raw) { | |
32 | 3 | var pass = password.split("$"); |
33 | // "algo$salt$hash" | |
34 | 3 | if (pass.length === 3) { |
35 | 2 | if (pass[0] === "sha1") { |
36 | 2 | return pass[2] === sha1(pass[1] + raw); |
37 | } | |
38 | } | |
39 | 1 | return false; |
40 | } | |
41 | ||
42 | function makePassword(raw) { | |
43 | 1 | var salt = sha1("" + Math.random() + Math.random()).slice(0, 9); |
44 | 1 | return 'sha1$' + salt + "$" + sha1(salt + raw); |
45 | } | |
46 | ||
47 | 1 | exports.sign = sign; |
48 | 1 | exports.verify = verify; |
49 | 1 | exports.checkPassword = checkPassword; |
50 | 1 | exports.makePassword = makePassword; |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Request/response context for Router and Core | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies | |
7 | */ | |
8 | ||
9 | 1 | var Klass = require('./klass').Klass; |
10 | 1 | var EventEmitter = require('events').EventEmitter; |
11 | 1 | var 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 | ||
22 | function Context(request, response) { | |
23 | 24 | EventEmitter.call(this); |
24 | 24 | this.request = request; |
25 | 24 | this.response = response; |
26 | 24 | this.statusCode = 200; |
27 | 24 | this._headers = {}; |
28 | 24 | this._headerSent = false; |
29 | } | |
30 | ||
31 | /** | |
32 | * Context prototype object | |
33 | * | |
34 | * @type {Object} | |
35 | */ | |
36 | ||
37 | 1 | Context.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) { | |
48 | 8 | this._headerSent = true; |
49 | 8 | this.response.writeHead(statusCode || this.statusCode, extend(this._headers, headers)); |
50 | 8 | 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) { | |
62 | 0 | this.response.write(chunk, encoding); |
63 | 0 | 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) { | |
75 | 8 | if (!this._headerSent) { |
76 | 3 | this.writeHead(this.statusCode, this._headers); |
77 | } | |
78 | 8 | this.response.end(data, encoding); |
79 | 8 | 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) { | |
92 | 53 | this._headers[key] = value; |
93 | 53 | 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) { | |
105 | 48 | 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) { | |
117 | 42 | 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) { | |
130 | 42 | if (!this.hasHeader(key)) { |
131 | 29 | this.setHeader(key, value); |
132 | } | |
133 | 42 | 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) { | |
145 | 1 | delete this._headers[key]; |
146 | 1 | 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) { | |
158 | 3 | this.statusCode = code; |
159 | 3 | 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) { | |
171 | 2 | this.setStatusCode(permanent ? 301 : 302); |
172 | 2 | this.setHeader('Location', url); |
173 | 2 | 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) { | |
187 | 18 | if (typeof body !== 'string') { |
188 | 5 | body = JSON.stringify(body) || ""; |
189 | } | |
190 | 18 | this.addHeader("Content-Length", Buffer.byteLength(body, encoding || 'utf8')); |
191 | 18 | this.addHeader("Content-Type", 'text/plain'); |
192 | 18 | this.writeHead(code, headers); |
193 | 18 | 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) { | |
204 | 5 | this.setHeader('Content-Type', 'application/json; charset=utf-8'); |
205 | 5 | 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) { | |
216 | 7 | this.setHeader('Content-Type', 'text/html; charset=utf-8'); |
217 | 7 | 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) { | |
228 | 3 | var msg = err.message || err.stack; |
229 | 3 | this.send(msg, err.statusCode || 500); |
230 | } | |
231 | }; | |
232 | ||
233 | /** | |
234 | * Expose Context class | |
235 | * | |
236 | * @type {Function} | |
237 | */ | |
238 | ||
239 | 1 | exports.Context = Klass(EventEmitter, Context); |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Utilities for controlling javascript execution flow | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies | |
7 | */ | |
8 | ||
9 | 1 | var 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 | ||
17 | 1 | var Chain = function () { |
18 | 16 | this._chain = {}; |
19 | }; | |
20 | ||
21 | /** | |
22 | * Chain prototype object | |
23 | */ | |
24 | ||
25 | 1 | Chain.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) { | |
36 | 44 | if (!this._chain[name]) { |
37 | 40 | this._chain[name] = []; |
38 | } | |
39 | 44 | 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) { | |
51 | 41 | if (this._chain[name]) { |
52 | 40 | var fnChain = chain(this._chain[name], function (fn, idx, fnChain, next, args) { |
53 | 35 | args = toArray(args); |
54 | 35 | args.push(next); |
55 | 35 | if (fn.apply(this, args)) { |
56 | 2 | next(); |
57 | } | |
58 | }); | |
59 | 40 | return function () { |
60 | 31 | 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 | ||
76 | function chain(array, callback, doneCallback) { | |
77 | 67 | var length; |
78 | 67 | if (!Array.isArray(array)) { |
79 | 2 | 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 | |
82 | 1 | length = parseInt(array, 10); |
83 | } else { | |
84 | 1 | throw new Error('First argument should be either `Array` or `Number`, `' + typeof array + '` got.'); |
85 | } | |
86 | } else { | |
87 | 65 | length = array.length; |
88 | } | |
89 | 66 | return function next(step, args, ctx) { |
90 | 113 | step = step || 0; |
91 | 113 | if (step >= length) { |
92 | 11 | if (doneCallback) { |
93 | 1 | doneCallback.call(ctx); |
94 | } | |
95 | 11 | return; |
96 | } | |
97 | 102 | callback.call(ctx, array[step], step++, array, function () { |
98 | 63 | 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 | ||
114 | function parallel(array, callback, doneCallback, context) { | |
115 | 4 | var count = 0, len = array.length, result = []; |
116 | 4 | var failCallback; |
117 | 4 | var timer, timeoutSeconds, timeoutCallback; |
118 | 4 | process.nextTick(function () { |
119 | 4 | array.forEach(function (item, idx, arr) { |
120 | 13 | callback.call(context, item, idx, arr, function (err, res) { |
121 | 12 | ++count; |
122 | 12 | if (timer && count === len) { |
123 | 0 | clearTimeout(timer); |
124 | } | |
125 | 12 | if (err) { |
126 | 1 | if (failCallback) { |
127 | 1 | failCallback.call(context, err, item, idx); |
128 | } else { | |
129 | 0 | console.trace('Please set error handling function with `.fail`'); |
130 | 0 | throw err; |
131 | } | |
132 | } else { | |
133 | 11 | result[idx] = res; |
134 | } | |
135 | 12 | if (count === len) { |
136 | 3 | if (doneCallback) { |
137 | 3 | doneCallback.call(context, result); |
138 | } | |
139 | } | |
140 | }); | |
141 | }); | |
142 | 4 | if (timeoutSeconds) { |
143 | 1 | timer = setTimeout(function () { |
144 | 1 | timeoutCallback.call(context, result); |
145 | }, timeoutSeconds * 1000); | |
146 | } | |
147 | }); | |
148 | 4 | var ret = { |
149 | done: function (callback) { | |
150 | 3 | doneCallback = callback; |
151 | 3 | return ret; |
152 | }, | |
153 | fail: function (callback) { | |
154 | 1 | failCallback = callback; |
155 | 1 | return ret; |
156 | }, | |
157 | timeout: function (callback, timeoutInSeconds) { | |
158 | 1 | timeoutSeconds = timeoutInSeconds; |
159 | 1 | timeoutCallback = callback; |
160 | } | |
161 | }; | |
162 | 4 | 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 | ||
175 | function promise(asyncFn, context) { | |
176 | 1 | return function () { |
177 | // the callback registered by calling `when(callback)` | |
178 | 1 | 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 | |
181 | 1 | var args = toArray(arguments); |
182 | 1 | args.push(function () { |
183 | 1 | if (callback) { |
184 | 1 | 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 | |
189 | 1 | process.nextTick(function () { |
190 | 1 | asyncFn.apply(context, args); |
191 | }); | |
192 | 1 | return { |
193 | when: function (handleFunc) { | |
194 | // register the callback function | |
195 | 1 | 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 | ||
211 | function defer(asyncFn, context, emitter) { | |
212 | 4 | return function () { |
213 | // the callback handler(s) registered by calling `then(callback)` | |
214 | 6 | 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 | |
217 | 6 | var args = toArray(arguments); |
218 | 6 | args.push(function (err) { |
219 | 6 | if (nextDeferred || callback) { |
220 | // `callback` and `nextDeferred` should accept `err` as argument | |
221 | 2 | andCallbacks.push(function (defer) { |
222 | 2 | var _args = toArray(arguments); |
223 | 2 | _args[0] = err; |
224 | // call the `callback` first if any | |
225 | 2 | if (callback) { |
226 | 1 | callback.apply(this, _args); |
227 | } | |
228 | 2 | if (nextDeferred) { |
229 | 1 | if (err) { |
230 | 0 | nextDeferred.error(err); |
231 | } else { | |
232 | 1 | 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` | |
238 | 6 | if (!err) { |
239 | 5 | 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 | |
243 | 5 | theArgs.shift(); |
244 | 5 | if (thenCallbacks.length > 0) { |
245 | // call the `then` stack | |
246 | 2 | thenCallbacks.forEach(function (fn) { |
247 | 3 | fn.apply(context, theArgs); |
248 | }); | |
249 | } | |
250 | 5 | if (andCallbacks.length > 0) { |
251 | // call the `and` stack | |
252 | 5 | chain(andCallbacks, function (fn, idx, fnChains, next, chainArgs) { |
253 | 16 | chainArgs.unshift({next: next, error: errback_}); |
254 | 16 | if (fn.apply(this, chainArgs)) { |
255 | 4 | chainArgs.shift(); |
256 | 4 | next.apply(null, chainArgs); |
257 | } | |
258 | }, doneCallback || (emitter && function () { | |
259 | 0 | emitter.emit('done'); |
260 | }))(0, theArgs, context); | |
261 | } | |
262 | } else { | |
263 | 1 | errback_(err); |
264 | } | |
265 | // define an error callback so that we can call it in the `and` chain | |
266 | function errback_(err) { | |
267 | 1 | if (failCallbacks.length > 0 || emitter) { |
268 | 1 | if (emitter) { |
269 | 1 | emitter.emit('fail', err); |
270 | } | |
271 | // if we have error handler then use it | |
272 | 1 | failCallbacks.forEach(function (fn) { |
273 | 1 | fn.call(context, err); |
274 | }); | |
275 | } else { | |
276 | // otherwise throw the exception | |
277 | 0 | console.trace('Please set error handling function with `.fail` or supply an `emitter` and listen to the `fail` event.'); |
278 | 0 | 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 | |
284 | 6 | process.nextTick(function () { |
285 | 6 | asyncFn.apply(context, args); |
286 | }); | |
287 | 6 | var ret = { |
288 | then: function () { | |
289 | 6 | var fns = toArray(arguments); |
290 | 6 | if (andCallbacks.length > 0) { |
291 | // put at the tail of `and` callbacks | |
292 | 3 | var fn = function () { |
293 | 3 | var args = toArray(arguments); |
294 | 3 | args.shift(); // shift the `defer` object |
295 | 3 | fns.forEach(function (f) { |
296 | 4 | f.apply(this, args); |
297 | }, this); | |
298 | 3 | return true; |
299 | }; | |
300 | 3 | andCallbacks.push(fn); |
301 | } else { | |
302 | // register the callback function in parallel | |
303 | 3 | thenCallbacks = thenCallbacks.concat(fns); |
304 | } | |
305 | 6 | return ret; |
306 | }, | |
307 | and: function () { | |
308 | 10 | andCallbacks = andCallbacks.concat(toArray(arguments)); |
309 | 10 | return ret; |
310 | }, | |
311 | callback: function (cb) { | |
312 | 1 | callback = cb; |
313 | 1 | return ret; |
314 | }, | |
315 | defer: function (deferred) { | |
316 | 1 | nextDeferred = deferred; |
317 | 1 | return ret; |
318 | }, | |
319 | done: function (fn) { | |
320 | 1 | doneCallback = emitter ? function () { |
321 | 1 | emitter.emit('done'); |
322 | 1 | fn.call(this); |
323 | } : fn; | |
324 | 1 | return ret; |
325 | }, | |
326 | fail: function (errorFunc, replace) { | |
327 | 1 | if (replace) { |
328 | 0 | failCallbacks = [errorFunc]; |
329 | } else { | |
330 | 1 | failCallbacks.push(errorFunc); |
331 | } | |
332 | 1 | return ret; |
333 | } | |
334 | }; | |
335 | 6 | if (emitter) { |
336 | 2 | ret.emitter = emitter; |
337 | } | |
338 | 6 | return ret; |
339 | }; | |
340 | } | |
341 | ||
342 | /** | |
343 | * Module exports | |
344 | */ | |
345 | ||
346 | 1 | module.exports = { |
347 | chain: chain, | |
348 | parallel: parallel, | |
349 | Chain: Chain, | |
350 | promise: promise, | |
351 | defer: defer | |
352 | }; |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Cookie parsing/encoding utilities | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies | |
7 | */ | |
8 | ||
9 | 1 | var qs = require('querystring'); |
10 | 1 | var escape = qs.escape; |
11 | 1 | var unescape = qs.unescape; |
12 | ||
13 | /** | |
14 | * Module exports | |
15 | * | |
16 | * @type {{parse: Function, stringify: Function, checkLength: Function}} | |
17 | */ | |
18 | ||
19 | 1 | module.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) { | |
31 | 5 | var cookies = {}; |
32 | 5 | if ('string' === typeof cookie) { |
33 | // Copied from: [cookie-sessions](http://github.com/caolan/cookie-sessions/blob/master/lib/cookie-sessions.js) | |
34 | 5 | cookies = cookie.split(/\s*;\s*/g).map( |
35 | function (x) { | |
36 | 15 | return x.split('='); |
37 | }).reduce(function (a, x) { | |
38 | 15 | if (x[0] && x[1]) { |
39 | 13 | a[unescape(x[0])] = unescape(x[1]); |
40 | } | |
41 | 15 | return a; |
42 | }, {}); | |
43 | } | |
44 | 5 | 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) { | |
58 | 4 | var cookie = name + "=" + escape(value); |
59 | 4 | if (options) { |
60 | 4 | if (options.expires) { |
61 | 4 | cookie += "; expires=" + options.expires.toUTCString(); |
62 | } | |
63 | 4 | if (options.path) { |
64 | 3 | cookie += "; path=" + options.path; |
65 | } | |
66 | 4 | if (options.domain) { |
67 | 2 | cookie += "; domain=" + options.domain; |
68 | } | |
69 | 4 | if (options.secure) { |
70 | 1 | cookie += "; secure=" + options.secure; |
71 | } | |
72 | 4 | if (options.httponly) { |
73 | 1 | cookie += "; httponly=" + options.httponly; |
74 | } | |
75 | 4 | 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) { | |
87 | 1 | return cookieStr.length <= 4096; |
88 | } | |
89 | }; | |
90 |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Middleware system inspired by Connect | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies | |
7 | */ | |
8 | ||
9 | 1 | var Klass = require('./klass').Klass; |
10 | 1 | var Chain = require('./control').Chain; |
11 | 1 | var EventEmitter = require('events').EventEmitter; |
12 | 1 | var Path = require('path'); |
13 | 1 | var toArray = require('./util').toArray; |
14 | 1 | var App = require('./app').App; |
15 | 1 | var Context = require('./context').Context; |
16 | 1 | var 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 | ||
26 | 1 | var 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) { | |
37 | 14 | this.site = site; |
38 | 14 | this.plugins = []; |
39 | 14 | this.stackNames = []; |
40 | 14 | this.stacks = {}; |
41 | 14 | this.apps = {}; |
42 | 14 | this.routes = {}; |
43 | }, | |
44 | ||
45 | /** | |
46 | * Get the "writeHead" functions chain | |
47 | * | |
48 | * @returns {Function} | |
49 | * @private | |
50 | */ | |
51 | ||
52 | writeHead: function () { | |
53 | 13 | this.add('writeHead', function (statusCode, headers) { |
54 | 15 | this.response.writeHead(statusCode, headers); |
55 | }); | |
56 | // return the stacked callbacks | |
57 | 13 | return this.get('writeHead'); |
58 | }, | |
59 | ||
60 | /** | |
61 | * Get the "write" functions chain | |
62 | * | |
63 | * @returns {Function} | |
64 | * @private | |
65 | */ | |
66 | ||
67 | write: function () { | |
68 | 13 | this.add('write', function (chunk, encoding) { |
69 | 0 | if (!this._headerSent) { |
70 | 0 | this.writeHead(); |
71 | } | |
72 | 0 | this.response.write(chunk, encoding); |
73 | }); | |
74 | 13 | return this.get('write'); |
75 | }, | |
76 | ||
77 | /** | |
78 | * Get the "end" functions chain | |
79 | * | |
80 | * @returns {Function} | |
81 | * @private | |
82 | */ | |
83 | ||
84 | end: function () { | |
85 | 13 | this.add('end', function (chunk, encoding) { |
86 | 15 | if (!this._headerSent) { |
87 | 0 | this.writeHead(); |
88 | } | |
89 | 15 | this.response.end(chunk, encoding); |
90 | }); | |
91 | 13 | 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) { | |
103 | 26 | if ('string' === typeof plugin) { |
104 | 24 | plugin = require(Path.join(__dirname, '/plugin', plugin.toLowerCase())); |
105 | } | |
106 | ||
107 | 26 | var pluginName = plugin.name; |
108 | ||
109 | 26 | if (this.plugins.indexOf(pluginName) > -1) { |
110 | 0 | throw new Error('Duplicated plugin name: ' + pluginName); |
111 | } | |
112 | ||
113 | 26 | this.plugins.push(pluginName); |
114 | ||
115 | 26 | if (plugin.attach) { |
116 | 25 | var fn = plugin.attach(this, options); |
117 | 25 | if ('function' === typeof fn) { |
118 | 23 | this.stackNames.push(pluginName); |
119 | 23 | this.stacks[pluginName] = fn; |
120 | } | |
121 | } | |
122 | ||
123 | 26 | if (plugin.module) { |
124 | 3 | Context = Context(plugin.module); |
125 | } | |
126 | 26 | return this; |
127 | }, | |
128 | ||
129 | /** | |
130 | * | |
131 | * @param app | |
132 | * @public | |
133 | */ | |
134 | load: function (app) { | |
135 | 3 | this.apps[app.name.toLowerCase()] = app; |
136 | 3 | return this; |
137 | }, | |
138 | ||
139 | /** | |
140 | * | |
141 | * @param routes | |
142 | * @returns {*} | |
143 | * @public | |
144 | */ | |
145 | map: function (routes) { | |
146 | 13 | extend(this.routes, routes); |
147 | 13 | 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 () { | |
157 | 13 | if (!this.router) { |
158 | 4 | this.use('router'); |
159 | } | |
160 | ||
161 | 13 | var self = this; |
162 | 13 | var router = this.router; |
163 | 13 | var routes = this.routes; |
164 | 13 | var apps = this.apps; |
165 | ||
166 | 13 | Object.keys(apps).forEach(function (appName) { |
167 | 3 | var _app = apps[appName]; |
168 | 3 | var publicMethods = _app.publicMethods; |
169 | 3 | Object.keys(publicMethods).forEach(function (name) { |
170 | 3 | var routeName = appName + name[0].toUpperCase() + name.slice(1); |
171 | 3 | var route = self.normalizeRoute(routeName, routes, apps); |
172 | 3 | if (route) { |
173 | 3 | router.add(route.method, route.url, route.handler, route.hooks); |
174 | 3 | delete routes[route.name]; |
175 | } | |
176 | }); | |
177 | }); | |
178 | ||
179 | 13 | Object.keys(routes).forEach(function (routeName) { |
180 | 12 | var route = self.normalizeRoute(routeName, routes, apps); |
181 | 12 | if (route) { |
182 | 11 | router.add(route.method, route.url, route.handler, route.hooks); |
183 | 11 | 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) { | |
199 | 15 | var router = this.router; |
200 | 15 | var self = this; |
201 | 15 | var defaultViewPath = router.slashCamelCase(routeName); |
202 | 15 | var appName = defaultViewPath.split('/')[0]; |
203 | 15 | if (routeName === appName) { |
204 | 1 | return false; |
205 | } | |
206 | ||
207 | 14 | var app; |
208 | 14 | var appFn; |
209 | ||
210 | 14 | if (apps) { |
211 | 14 | app = apps[appName]; |
212 | 14 | if (app) { |
213 | 3 | var methodName = routeName.replace(appName, ''); |
214 | 3 | methodName = methodName[0].toLowerCase() + methodName.slice(1); |
215 | 3 | appFn = app.publicMethods[methodName]; |
216 | } | |
217 | } | |
218 | ||
219 | 14 | var route = routes[routeName] || {}; |
220 | 14 | route.name = routeName; |
221 | ||
222 | 14 | defaultViewPath = defaultViewPath.split('/'); |
223 | 14 | defaultViewPath.shift(); |
224 | 14 | defaultViewPath = appName + '/' + defaultViewPath.join('_'); |
225 | 14 | defaultViewPath += '.html'; |
226 | ||
227 | 14 | var appRouteDefaults = routes[appName] || {}; |
228 | ||
229 | 14 | if (!route.method) { |
230 | 4 | route.method = appRouteDefaults.method || 'GET'; |
231 | } | |
232 | ||
233 | 14 | if (!route.url) { |
234 | 4 | route.url = router.slashCamelCase(routeName); |
235 | } | |
236 | ||
237 | 14 | if (appRouteDefaults.urlRoot && route.url[0] !== '^') { |
238 | 0 | route.url = route.url.split('/'); |
239 | 0 | route.url.shift(); |
240 | 0 | route.url = router.prefixUrl(appRouteDefaults.urlRoot, route.url.join('/')); |
241 | } | |
242 | ||
243 | 14 | if (!route.view) { |
244 | 9 | if (self.view) { |
245 | 0 | route.view = defaultViewPath; |
246 | } else { | |
247 | 9 | route.view = 'html'; |
248 | } | |
249 | } | |
250 | ||
251 | 14 | switch (route.view) { |
252 | case 'html': | |
253 | 9 | route.view = function (err, result) { |
254 | 2 | if (err) { |
255 | 0 | this.error(err); |
256 | 0 | return; |
257 | } | |
258 | 2 | this.sendHTML(result || ''); |
259 | }; | |
260 | 9 | break; |
261 | case 'json': | |
262 | 1 | route.view = function (err, result) { |
263 | 1 | if (err) { |
264 | 0 | this.error(err); |
265 | 0 | return; |
266 | } | |
267 | 1 | this.sendJSON(result || '{}'); |
268 | }; | |
269 | 1 | break; |
270 | } | |
271 | ||
272 | 14 | if (self.view && 'string' === typeof route.view) { |
273 | 0 | defaultViewPath = route.view; |
274 | 0 | route.view = function (err, result) { |
275 | 0 | if (err) { |
276 | 0 | this.error(err); |
277 | 0 | return; |
278 | } | |
279 | 0 | this.render(result); |
280 | }; | |
281 | } | |
282 | ||
283 | 14 | if (!route.handler) { |
284 | 7 | if (app && appFn) { |
285 | 3 | route.handler = function (context) { |
286 | 3 | var next; |
287 | 3 | var args = toArray(arguments); |
288 | 3 | context.app = app; |
289 | ||
290 | function callback() { | |
291 | 3 | if (self.view) { |
292 | 0 | context.view = self.view; |
293 | 0 | context.viewPath = defaultViewPath; |
294 | } | |
295 | 3 | route.view.apply(context, arguments); |
296 | 3 | if (next) { |
297 | 1 | next(); |
298 | } | |
299 | } | |
300 | ||
301 | 3 | if (context.session) { |
302 | 1 | args[0] = context.session; |
303 | } else { | |
304 | 2 | args.shift(); |
305 | } | |
306 | ||
307 | 3 | if ('function' === typeof args[args.length - 1]) { |
308 | 1 | next = args.pop(); |
309 | } | |
310 | ||
311 | 3 | args.push(callback); |
312 | 3 | appFn.apply(app, args); |
313 | }; | |
314 | } else { | |
315 | 4 | route.handler = function (context) { |
316 | 4 | if (app) { |
317 | 0 | context.app = app; |
318 | } | |
319 | 4 | if (self.view) { |
320 | 4 | context.view = self.view; |
321 | 4 | context.viewPath = defaultViewPath; |
322 | } | |
323 | 4 | var args = toArray(arguments); |
324 | 4 | args[0] = null; |
325 | 4 | route.view.apply(context, args); |
326 | 4 | return true; |
327 | }; | |
328 | } | |
329 | 7 | } else if (app) { |
330 | 0 | var handler = route.handler; |
331 | 0 | route.handler = function (context) { |
332 | 0 | context.app = app; |
333 | 0 | handler.apply(null, arguments); |
334 | }; | |
335 | } | |
336 | ||
337 | 14 | if (appRouteDefaults && appRouteDefaults.hooks) { |
338 | 1 | if (route.hooks) { |
339 | 0 | route.hooks = router.stackHook(appRouteDefaults.hooks, route.hooks); |
340 | } else { | |
341 | 1 | route.hooks = appRouteDefaults.hooks.slice(0); |
342 | } | |
343 | } | |
344 | ||
345 | 14 | 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 () { | |
356 | 13 | this.buildRoutes(); |
357 | 13 | var stacks = this.stacks; |
358 | 13 | var stackNames = this.stackNames; |
359 | ||
360 | function go(idx, ctx, request, response, where, jumping) { | |
361 | 36 | var curr; |
362 | 36 | if ('undefined' === typeof where) { |
363 | 36 | curr = stacks[stackNames[idx++]]; |
364 | 36 | if (!curr) { |
365 | 13 | return; |
366 | } | |
367 | } else { | |
368 | 0 | var whereIdx = stackNames.indexOf(where); |
369 | 0 | if (whereIdx > -1) { |
370 | 0 | curr = stacks[where]; |
371 | 0 | if (jumping) { |
372 | 0 | idx = whereIdx + 1; |
373 | } | |
374 | } else { | |
375 | 0 | throw new Error('Undefined plugin: ' + where); |
376 | } | |
377 | } | |
378 | ||
379 | 23 | curr.call(ctx, request, response, function (where, jumping) { |
380 | 20 | go(idx, ctx, request, response, where, jumping); |
381 | }); | |
382 | } | |
383 | ||
384 | 13 | var writeHead = this.writeHead(); |
385 | 13 | var write = this.write(); |
386 | 13 | var end = this.end(); |
387 | ||
388 | 13 | Context = Context({ |
389 | writeHead: function (statusCode, headers) { | |
390 | 15 | this._headerSent = true; |
391 | 15 | writeHead.call(this, statusCode || this.statusCode, extend(this._headers, headers)); |
392 | 15 | return this; |
393 | }, | |
394 | write: function (chunk, encoding) { | |
395 | 0 | write.call(this, chunk, encoding); |
396 | 0 | return this; |
397 | }, | |
398 | end: function (chunk, encoding) { | |
399 | 15 | end.call(this, chunk, encoding); |
400 | 15 | return this; |
401 | } | |
402 | }); | |
403 | ||
404 | 13 | if (this.site) { |
405 | 4 | var site = this.site; |
406 | 4 | Context = Context({ |
407 | set: function () { | |
408 | 0 | return site.set.apply(site, arguments); |
409 | }, | |
410 | get: function () { | |
411 | 0 | return site.get.apply(site, arguments); |
412 | } | |
413 | }); | |
414 | } | |
415 | ||
416 | 13 | return function (request, response) { |
417 | 16 | var ctx = new Context(request, response); |
418 | 16 | go(0, ctx, request, response); |
419 | }; | |
420 | } | |
421 | }); | |
422 | ||
423 | /** | |
424 | * Expose Core | |
425 | * | |
426 | * @type {Function} | |
427 | */ | |
428 | ||
429 | 1 | exports.Core = Core; |
Line | Hits | Source |
---|---|---|
1 | /*! | |
2 | * Some shortcuts for crypto | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies | |
7 | */ | |
8 | 1 | var crypto = require('crypto'); |
9 | 1 | var fs = require('fs'); |
10 | ||
11 | function _hashFile(alg, path, encoding, callback) { | |
12 | 4 | if (typeof encoding === 'function') { |
13 | 2 | callback = encoding; |
14 | 2 | encoding = undefined; |
15 | } | |
16 | 4 | var stream = fs.createReadStream(path); |
17 | 4 | var hash = crypto.createHash(alg); |
18 | 4 | stream.on('data', |
19 | function(chunk) { | |
20 | 3 | hash.update(chunk); |
21 | }).on('end', | |
22 | function() { | |
23 | 3 | callback(null, hash.digest(encoding || 'hex')); |
24 | }).on('error', function(err) { | |
25 | 1 | callback(err); |
26 | }); | |
27 | } | |
28 | ||
29 | 1 | module.exports = { |
30 | md5: function (data, encoding) { | |
31 | 2 | return crypto.createHash('md5').update(data).digest(encoding || 'hex'); |
32 | }, | |
33 | ||
34 | md5file: function(path, encoding, callback) { | |
35 | 2 | _hashFile('md5', path, encoding, callback); |
36 | }, | |
37 | ||
38 | sha1: function(data, encoding) { | |
39 | 5 | return crypto.createHash('sha1').update(data).digest(encoding || 'hex'); |
40 | }, | |
41 | ||
42 | sha1file: function(path, encoding, callback) { | |
43 | 2 | _hashFile('sha1', path, encoding, callback); |
44 | }, | |
45 | ||
46 | hmac: function(algo, data, key, encoding) { | |
47 | 9 | return crypto.createHmac(algo, key).update(data).digest(encoding || 'hex'); |
48 | }, | |
49 | ||
50 | hmac_sha1: function(data, key, encoding) { | |
51 | 9 | return module.exports.hmac('sha1', data, key, encoding); |
52 | }, | |
53 | ||
54 | cipher: function(plain, key, options) { | |
55 | 2 | options = options || {}; |
56 | 2 | var oe = options.outputEncoding || 'hex'; |
57 | 2 | var c = crypto.createCipher(options.algorithm || 'aes256', key); |
58 | 2 | return c.update(plain, options.inputEncoding || 'utf8', oe) + c.final(oe); |
59 | }, | |
60 | ||
61 | decipher: function(cipher, key, options) { | |
62 | 2 | options = options || {}; |
63 | 2 | var oe = options.outputEncoding || 'utf8'; |
64 | 2 | var d = crypto.createDecipher(options.algorithm || 'aes256', key); |
65 | 2 | var result = ''; |
66 | 2 | try { |
67 | 2 | result = d.update(cipher, options.inputEncoding || 'hex', oe) + d.final(oe); |
68 | } catch (e) {} | |
69 | 2 | return result; |
70 | }, | |
71 | ||
72 | base64Encode: function(input, inputEncoding) { | |
73 | 1 | var buf = new Buffer(input, inputEncoding || 'utf8'); |
74 | 1 | return buf.toString('base64'); |
75 | }, | |
76 | ||
77 | base64Decode: function(input, outputEncoding) { | |
78 | 1 | var base64 = new Buffer(input, 'base64'); |
79 | 1 | return base64.toString(outputEncoding || 'utf8'); |
80 | } | |
81 | }; |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * The global Genji module | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Exposes version of Genji | |
7 | * | |
8 | * @type {string} | |
9 | */ | |
10 | ||
11 | 1 | exports.version = exports.VERSION = '0.7.0'; |
12 | ||
13 | /** | |
14 | * Exposes public apis and modules | |
15 | */ | |
16 | ||
17 | 1 | var util = require('./util'); |
18 | 1 | var extend = util.extend; |
19 | ||
20 | /** | |
21 | * Extends frequently used sub-modules to the global Genji module | |
22 | */ | |
23 | ||
24 | 1 | extend(exports, util); |
25 | 1 | extend(exports, require('./klass')); |
26 | 1 | extend(exports, require('./control')); |
27 | 1 | extend(exports, require('./core')); |
28 | 1 | extend(exports, require('./context')); |
29 | 1 | extend(exports, require('./site')); |
30 | 1 | extend(exports, require('./router')); |
31 | 1 | extend(exports, require('./model')); |
32 | 1 | extend(exports, require('./app')); |
33 | 1 | extend(exports, {View: require('./view').View}); |
34 | ||
35 | /** | |
36 | * Extends other sub-modules with module name as namespace | |
37 | */ | |
38 | ||
39 | 1 | extend(exports, {auth: require('./auth')}); |
40 | 1 | extend(exports, {cookie: require('./cookie')}); |
41 | 1 | extend(exports, {crypto: require('./crypto')}); |
42 | 1 | extend(exports, {view: require('./view')}); |
43 | 1 | extend(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 | ||
55 | 1 | exports.route = function route(routes, options) { |
56 | 9 | options = options || {}; |
57 | 9 | return new exports.Router(routes, options); |
58 | }; | |
59 | ||
60 | /** | |
61 | * Creates a Site instance | |
62 | * | |
63 | * @returns {exports.Site} | |
64 | */ | |
65 | ||
66 | 1 | exports.site = function site() { |
67 | 4 | return new exports.Site(); |
68 | }; |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Javascript OO helpers | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module exports. | |
7 | */ | |
8 | ||
9 | 1 | exports.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 | ||
21 | function Klass(superClass, subModule, inherit) { | |
22 | 37 | var global = this; |
23 | 37 | var superCtor = superClass; |
24 | ||
25 | 37 | if ('function' === typeof subModule) { |
26 | 4 | superCtor = subModule; |
27 | 4 | subModule = subModule.prototype; |
28 | } | |
29 | ||
30 | 37 | var _inherit = function (prototype, subModule) { |
31 | 34 | Object.keys(subModule).forEach(function (key) { |
32 | 114 | prototype[key] = subModule[key]; |
33 | }); | |
34 | }; | |
35 | ||
36 | 37 | if (inherit) { |
37 | 32 | _inherit = inherit; |
38 | } | |
39 | ||
40 | function constructor(subModule, inherit) { | |
41 | 265 | if (global !== this) { |
42 | 234 | superCtor.apply(this, arguments); |
43 | 234 | if ('function' === typeof this.init) { |
44 | 37 | this.init.apply(this, arguments); |
45 | } | |
46 | } else { | |
47 | 31 | return Klass(constructor, subModule, inherit || _inherit); |
48 | } | |
49 | } | |
50 | ||
51 | 37 | var prototype = constructor.prototype; |
52 | 37 | prototype.__proto__ = superClass.prototype; |
53 | ||
54 | 37 | if (subModule) { |
55 | 36 | _inherit(prototype, subModule); |
56 | } | |
57 | ||
58 | 37 | return constructor; |
59 | } | |
60 |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Database agnostic data model class | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies | |
7 | */ | |
8 | 1 | var Klass = require('./klass').Klass; |
9 | 1 | var extend = require('./util').extend; |
10 | 1 | var 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 | ||
19 | function Model(data) { | |
20 | // mark that we are initializing, changed fields should not be recorded. | |
21 | 4 | this.initialized = false; |
22 | // tell if the input data has `idAttribute` or not. (default `idAttribute` field is `_id`) | |
23 | 4 | this.noIdAttribute = !data[this.idAttribute]; |
24 | // object to hold the document data | |
25 | 4 | this.data = {}; |
26 | // object contains the invaild fields and values and reasons | |
27 | 4 | this.invalidFields = {count: 0, fields: {}}; |
28 | 4 | this.setters = this.setters || {}; |
29 | 4 | this.getters = this.getters || {}; |
30 | 4 | this.aliases = this.aliases || {}; |
31 | // object contains the changed field/value pairs | |
32 | 4 | this.changedFields = false; |
33 | ||
34 | // check for required fields | |
35 | 4 | var self = this; |
36 | 4 | if (Array.isArray(this.requires) && this.requires.length > 0) { |
37 | 3 | this.requires.forEach(function (fieldName) { |
38 | 6 | if (!data.hasOwnProperty(fieldName)) { |
39 | // missing field found | |
40 | 1 | self.invalidFields.count++; |
41 | 1 | 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. | |
46 | 4 | Object.keys(data).forEach(function (key) { |
47 | 19 | self.set(key, data[key]); |
48 | }); | |
49 | // mark that the initialization is finished | |
50 | 4 | this.initialized = true; |
51 | } | |
52 | ||
53 | /** | |
54 | * Model prototype object | |
55 | */ | |
56 | ||
57 | 1 | Model.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) { | |
75 | 21 | var isInvalid = this.validateField(key, value); |
76 | 21 | if (isInvalid) { |
77 | 3 | this.invalidFields.count++; |
78 | 3 | this.invalidFields.fields[key] = { |
79 | error: isInvalid === true ? this.ERROR_FIELD_INVALID : isInvalid, | |
80 | value: value | |
81 | }; | |
82 | } else { | |
83 | 18 | var data = this.data; |
84 | 18 | var aliasKey = this.aliases[key] || key; |
85 | 18 | if (this.getInvalidFields().hasOwnProperty(key)) { |
86 | // if this field is invalid previously, then delete the field from invalidFields hash. | |
87 | 1 | this.invalidFields.count--; |
88 | 1 | delete this.invalidFields.fields[key]; |
89 | } | |
90 | // get setter function from setters hash | |
91 | 18 | var setter = this.setters[key]; |
92 | // apply attribute's setter function if any, when original data doc has no `_idAttribute` | |
93 | 18 | var newValue = this.noIdAttribute && setter ? setter.call(this, value) : value; |
94 | // save the changed value if we're not initializing. | |
95 | 18 | if (this.initialized && data[aliasKey] !== newValue) { |
96 | 2 | this.changedFields = this.changedFields || {}; |
97 | 2 | this.changedFields[key] = newValue; |
98 | } | |
99 | 18 | data[aliasKey] = newValue; |
100 | } | |
101 | 21 | 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) { | |
113 | 18 | if (Array.isArray(key)) { |
114 | 1 | var obj = {}; |
115 | 1 | key.forEach(function (keyName) { |
116 | 2 | obj[keyName] = this.get(keyName); |
117 | }, this); | |
118 | 1 | return obj; |
119 | } | |
120 | 17 | var data = this.data; |
121 | // use original key name | |
122 | 17 | var aliasKey = this.aliases[key] || key; |
123 | 17 | var getter = this.getters[key]; |
124 | 17 | 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) { | |
136 | 21 | if (!this.fields) { |
137 | 0 | return false; |
138 | } | |
139 | ||
140 | 21 | var field = this.maps[fieldName] || fieldName; |
141 | 21 | var validator = this.fields[field]; |
142 | ||
143 | 21 | if (!validator) { |
144 | 0 | return false; |
145 | } | |
146 | ||
147 | 21 | var validatorType = typeof validator; |
148 | 21 | if ('string' === validatorType) { |
149 | 12 | switch (validator) { |
150 | case 'number': | |
151 | case 'string': | |
152 | 7 | return validator === typeof value ? false : this.ERROR_FIELD_TYPE; |
153 | case 'array': | |
154 | 1 | return Array.isArray(value) ? false : this.ERROR_FIELD_TYPE; |
155 | case 'regexp': | |
156 | 1 | return util.isRegExp(value) ? false : this.ERROR_FIELD_TYPE; |
157 | case 'date': | |
158 | 1 | return util.isDate(value) ? false : this.ERROR_FIELD_TYPE; |
159 | case 'bool': | |
160 | 2 | return value === true || value === false ? false : this.ERROR_FIELD_TYPE; |
161 | } | |
162 | 9 | } else if (validatorType === 'function') { |
163 | 8 | return validator(value); |
164 | } | |
165 | 1 | 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 () { | |
176 | 8 | 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 () { | |
187 | 19 | 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 () { | |
198 | 1 | 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) { | |
210 | 2 | if (!this.isValid()) { |
211 | // return false if we have invalid field | |
212 | 1 | return false; |
213 | } | |
214 | 1 | var _data = this.data; |
215 | 1 | var data = {}; |
216 | 1 | keys = keys || Object.keys(_data); |
217 | 1 | var self = this; |
218 | 1 | var _maps = this.maps; |
219 | 1 | keys.forEach(function (key) { |
220 | 3 | data[_maps[key] || key] = self.get(_maps[key] || key); |
221 | }); | |
222 | 1 | 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) { | |
234 | 3 | if (!this.isValid()) { |
235 | // return false if we have invalid field | |
236 | 1 | return false; |
237 | } | |
238 | 2 | var _data = this.data; |
239 | 2 | var doc = {}; |
240 | 2 | keys = keys || Object.keys(_data); |
241 | 2 | var self = this; |
242 | 2 | var _aliases = this.aliases; |
243 | 2 | var _maps = this.maps; |
244 | 2 | keys.forEach(function (key) { |
245 | 7 | doc[_aliases[key] || key] = self.get(_maps[key] || key); |
246 | }); | |
247 | 2 | 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 | ||
268 | function modelInherits(prototype, subModule) { | |
269 | 2 | Object.keys(subModule).forEach(function (propKey) { |
270 | 11 | var notReserved = false; |
271 | 11 | switch (propKey) { |
272 | case 'name': | |
273 | case 'requires': | |
274 | 3 | prototype[propKey] = subModule[propKey]; |
275 | 3 | break; |
276 | case 'fields': | |
277 | 2 | prototype.fields = extend({}, prototype.fields, subModule.fields); |
278 | 2 | break; |
279 | case 'aliases': | |
280 | 2 | var aliases = extend({}, prototype.aliases, subModule.aliases); |
281 | 2 | var maps = {}; |
282 | 2 | Object.keys(aliases).forEach(function (alias) { |
283 | 3 | maps[aliases[alias]] = alias; |
284 | }); | |
285 | 2 | prototype.aliases = aliases; |
286 | 2 | prototype.maps = maps; |
287 | 2 | break; |
288 | case 'id': | |
289 | 0 | prototype.idAttribute = subModule.id; |
290 | 0 | break; |
291 | default: | |
292 | 4 | notReserved = true; |
293 | } | |
294 | 11 | if (notReserved) { |
295 | 4 | if (/^(set|get)[A-Z]+/.test(propKey)) { |
296 | 2 | var attributeName = propKey.slice(3); |
297 | 2 | attributeName = attributeName[0].toLowerCase() + attributeName.slice(1); |
298 | 2 | if (prototype.fields && prototype.fields.hasOwnProperty(attributeName)) { |
299 | // field is defined, this should be a setter/getter for attribute | |
300 | 2 | var fn = subModule[propKey]; |
301 | 2 | if (typeof fn === 'function') { |
302 | 2 | switch (propKey.slice(0, 3)) { |
303 | case 'set': | |
304 | 1 | prototype.setters = prototype.setters || {}; |
305 | 1 | prototype.setters[attributeName] = fn; |
306 | 1 | break; |
307 | case 'get': | |
308 | 1 | prototype.getters = prototype.getters || {}; |
309 | 1 | prototype.getters[attributeName] = fn; |
310 | 1 | break; |
311 | } | |
312 | } | |
313 | 2 | return; |
314 | } | |
315 | } | |
316 | 2 | prototype[propKey] = subModule[propKey]; |
317 | } | |
318 | }); | |
319 | } | |
320 | ||
321 | /** | |
322 | * Module exports | |
323 | */ | |
324 | ||
325 | 1 | exports.Model = Klass(Model, null, modelInherits); |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Cookie module plugin for context | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies. | |
7 | */ | |
8 | ||
9 | 1 | var cookie = require('../cookie'); |
10 | ||
11 | /** | |
12 | * Module exports | |
13 | * | |
14 | * @type {{name: String, module: Object}} | |
15 | * @public | |
16 | */ | |
17 | ||
18 | 1 | module.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) { | |
44 | 3 | var cookieString = cookie.stringify(name, value, options); |
45 | 3 | var cookies = this.getHeader('Set-Cookie'); |
46 | 3 | if (!Array.isArray(cookies)) { |
47 | 2 | cookies = []; |
48 | } | |
49 | 3 | cookies.push(cookieString); |
50 | 3 | 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) { | |
62 | 1 | if (!this.request.headers.cookie) { |
63 | 0 | return null; |
64 | } | |
65 | 1 | if (!this.cookies) { // request cookie will be parsed only one time |
66 | 1 | this.cookies = cookie.parse(this.request.headers.cookie); |
67 | } | |
68 | 1 | 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) { | |
79 | 1 | options = options || {}; |
80 | 1 | options.expires = new Date(0); |
81 | 1 | this.setCookie(name, "", options); |
82 | } | |
83 | } | |
84 | }; |
Line | Hits | Source |
---|---|---|
1 | 1 | require('../util').expose(module); |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Simple parser for url query and post/put parameters. | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies. | |
7 | */ | |
8 | ||
9 | 1 | var parseQuerystring = require('querystring').parse; |
10 | 1 | var parseUrl = require('url').parse; |
11 | ||
12 | /** | |
13 | * Module exports. | |
14 | * | |
15 | * @type {{name: string, attach: Function}} | |
16 | * @public | |
17 | */ | |
18 | ||
19 | 1 | module.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) { | |
37 | 6 | options = options || {}; |
38 | 6 | options.encoding = options.encoding || 'utf8'; |
39 | 6 | options.maxIncomingSize = options.maxIncomingSize || 256 * 1024; |
40 | ||
41 | 6 | return function (request, response, go) { |
42 | 6 | var context = this; |
43 | 6 | var urlObj = parseUrl(request.url); |
44 | 6 | if (urlObj.query) { |
45 | 2 | this.query = parseQuerystring(urlObj.query); |
46 | } | |
47 | ||
48 | 6 | if (request.method === 'POST' || request.method === 'PUT' || request.method === 'DELETE') { |
49 | 4 | request.setEncoding(options.encoding); |
50 | 4 | var contentLength = parseInt(request.headers['content-length'], 10); |
51 | ||
52 | 4 | if (isNaN(contentLength) || contentLength > options.maxIncomingSize) { |
53 | // not a vaild request | |
54 | 1 | request.removeAllListeners(); |
55 | 1 | response.writeHead(413, {'Connection': 'close'}); |
56 | 1 | response.end(); |
57 | 1 | return; |
58 | } | |
59 | ||
60 | 3 | var buff = ''; |
61 | 3 | request.on("data", function (chunk) { |
62 | 3 | buff += chunk; |
63 | }); | |
64 | ||
65 | 3 | var contentType = request.headers['content-type']; |
66 | 3 | request.on("end", function () { |
67 | 3 | context.data = buff; |
68 | 3 | if (contentType === 'application/x-www-form-urlencoded') { |
69 | 1 | context.param = parseQuerystring(buff); |
70 | 2 | } else if (/json|javascript/.test(contentType)) { |
71 | 2 | try { |
72 | 2 | context.json = JSON.parse(buff); |
73 | } catch (e) { | |
74 | } | |
75 | } | |
76 | 3 | go(); |
77 | }); | |
78 | } else { | |
79 | 2 | go(); |
80 | } | |
81 | }; | |
82 | } | |
83 | }; |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Router plugin for working with middleware | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies. | |
7 | */ | |
8 | ||
9 | 1 | var Router = require('../router').Router; |
10 | ||
11 | /** | |
12 | * Module exports | |
13 | * | |
14 | * @type {{name: string, attach: Function}} | |
15 | * @public | |
16 | */ | |
17 | ||
18 | 1 | module.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) { | |
35 | 13 | var router; |
36 | 13 | if (options instanceof Router) { |
37 | 1 | router = options; |
38 | } else { | |
39 | 12 | options = options || {}; |
40 | 12 | router = new Router(options); |
41 | } | |
42 | 13 | core.router = router; |
43 | ||
44 | 13 | return function (request, response, go) { |
45 | 13 | var matched = router.route(request.method, request.url, this, function (request, response) { |
46 | 1 | var notFound = options.notFound || function (request, response) { |
47 | 1 | var error = { |
48 | statusCode: 404, | |
49 | message: 'Content not found: ' + request.url | |
50 | }; | |
51 | 1 | this.error(error); |
52 | }; | |
53 | 1 | notFound.call(this, request, response); |
54 | 1 | go(); |
55 | }); | |
56 | 13 | if (matched) { |
57 | 12 | go(); |
58 | } | |
59 | }; | |
60 | } | |
61 | }; |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Plugin for decrypting and verifying secure cookie | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies | |
7 | */ | |
8 | ||
9 | 1 | var c = require('../crypto'); |
10 | 1 | var cipher = c.cipher; |
11 | 1 | var decipher = c.decipher; |
12 | 1 | var auth = require('../auth'); |
13 | 1 | var cookieHelper = require('../cookie'); |
14 | 1 | var parse = cookieHelper.parse; |
15 | 1 | var stringify = cookieHelper.stringify; |
16 | 1 | var extend = require('../util').extend; |
17 | ||
18 | /** | |
19 | * Module exports | |
20 | * | |
21 | * @type {{name: string, attach: Function}} | |
22 | */ | |
23 | ||
24 | 1 | module.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) { | |
45 | 2 | var secureKey = options.secureKey; |
46 | 2 | var cookieName = options.cookieName; |
47 | 2 | var cookiePath = options.cookiePath; |
48 | 2 | var cookieDomain = options.cookieDomain; |
49 | 2 | var serverKey = options.serverKey; |
50 | 2 | var cookieOption = {}; |
51 | ||
52 | 2 | if (cookiePath) { |
53 | 2 | cookieOption.path = cookiePath; |
54 | } | |
55 | ||
56 | 2 | if (cookieDomain) { |
57 | 2 | cookieOption.domain = cookieDomain; |
58 | } | |
59 | ||
60 | // encrypt cookie if found in response header | |
61 | 2 | core.add('writeHead', function (statusCode, headers, next) { |
62 | 2 | if (headers.hasOwnProperty(cookieName)) { |
63 | // build the cookie string from specified property of `headers` | |
64 | 1 | var originalCookie = headers[cookieName]; |
65 | // sign the cookie by hmac | |
66 | 1 | var signed = auth.sign(originalCookie[0], originalCookie[1], originalCookie[2], serverKey); |
67 | // encrypt signed cookie into base64 string | |
68 | 1 | var encryptedBase64 = cipher(signed, secureKey, {outputEncoding: 'base64'}); |
69 | // set other cookie option | |
70 | 1 | var option = extend({}, cookieOption, originalCookie[3]); |
71 | 1 | option.expires = originalCookie[1]; |
72 | ||
73 | // make the cookie | |
74 | 1 | var cookieStr = stringify(cookieName, encryptedBase64, option); |
75 | // delete the original raw cookie | |
76 | 1 | delete headers[cookieName]; |
77 | ||
78 | // try to put the cookie string into `headers` | |
79 | 1 | var setCookie = this.getHeader('Set-Cookie'); |
80 | 1 | if (!Array.isArray(setCookie)) { |
81 | 0 | setCookie = []; |
82 | } | |
83 | 1 | setCookie.push(cookieStr); |
84 | 1 | this.setHeader('Set-Cookie', setCookie); |
85 | ||
86 | 1 | next(statusCode, headers); |
87 | } else { | |
88 | // do nothing and go to next step | |
89 | 1 | return true; |
90 | } | |
91 | }); | |
92 | ||
93 | 2 | return function (req, res, go) { |
94 | 2 | var cookies = parse(req.headers.cookie); |
95 | 2 | var cookie = cookies[cookieName]; |
96 | ||
97 | 2 | if (cookie) { |
98 | 1 | var decrypted = auth.verify(decipher(cookie, secureKey, {inputEncoding: 'base64'}), serverKey); |
99 | 1 | if (!decrypted) { |
100 | 0 | return go(); |
101 | } | |
102 | 1 | var data = decrypted[2]; |
103 | 1 | try { |
104 | 1 | data = JSON.parse(decrypted[2]); |
105 | } catch (e) { | |
106 | 0 | console.error('JSON parsing error for object: %s', data); |
107 | } | |
108 | 1 | this.session = extend({}, this.session, { |
109 | cookieId: decrypted[0], | |
110 | cookieExpires: new Date(decrypted[1]), | |
111 | cookieData: data | |
112 | }); | |
113 | // for later usage | |
114 | 1 | this.cookies = cookies; |
115 | } | |
116 | 2 | go(); |
117 | }; | |
118 | } | |
119 | }; |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * View module plugin extends context with template rendering functions | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies. | |
7 | */ | |
8 | ||
9 | 1 | var View = require('../view').View; |
10 | ||
11 | /** | |
12 | * Module exports | |
13 | * | |
14 | * @type {{name: String, module: Object}} | |
15 | * @public | |
16 | */ | |
17 | ||
18 | 1 | module.exports = { |
19 | ||
20 | /** | |
21 | * Name of the plugin | |
22 | */ | |
23 | ||
24 | name: "View", | |
25 | ||
26 | attach: function (core, options) { | |
27 | 2 | if (options instanceof View) { |
28 | 0 | core.view = options; |
29 | } else { | |
30 | 2 | 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) { | |
51 | 4 | if ('string' !== typeof path) { |
52 | 1 | callback = ctx; |
53 | 1 | ctx = path; |
54 | 1 | path = this.viewPath; |
55 | } | |
56 | 4 | var self = this; |
57 | 4 | if ('function' !== typeof callback) { |
58 | 3 | callback = function (err, html) { |
59 | 3 | if (err) { |
60 | 1 | self.error({error: err, statusCode: 503, message: 'Failed to render file', context: self}); |
61 | 1 | return; |
62 | } | |
63 | 2 | self.sendHTML(html); |
64 | }; | |
65 | } | |
66 | 4 | this.view.renderFile(path, ctx || {}, callback); |
67 | } | |
68 | } | |
69 | }; |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Module dependencies | |
3 | */ | |
4 | ||
5 | 1 | var chain = require('./control').chain; |
6 | 1 | var util = require('./util'); |
7 | 1 | var extend = util.extend; |
8 | 1 | var toArray = util.toArray; |
9 | 1 | var Context = require('./core').Context; |
10 | ||
11 | /** | |
12 | * Module exports | |
13 | */ | |
14 | ||
15 | 1 | exports.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 | ||
28 | function Router(routes, options) { | |
29 | 30 | this.rules = []; |
30 | 30 | if (!Array.isArray(routes)) { |
31 | 22 | options = routes; |
32 | 22 | routes = null; |
33 | } | |
34 | 30 | options = options || {}; |
35 | 30 | this.urlRoot = options.urlRoot || '^/'; |
36 | 30 | if (this.urlRoot[0] !== '^') { |
37 | 0 | this.urlRoot = '^' + this.urlRoot; |
38 | } | |
39 | 30 | if (this.urlRoot[this.urlRoot.length - 1] !== '/') { |
40 | 4 | this.urlRoot += '/'; |
41 | } | |
42 | 30 | this.contextClass = options.contextClass || Context; |
43 | 30 | if (Array.isArray(routes)) { |
44 | 8 | this.mount(routes); |
45 | } | |
46 | } | |
47 | ||
48 | /** | |
49 | * Prototype object of Router class | |
50 | * | |
51 | * @type {Object} | |
52 | */ | |
53 | ||
54 | 1 | Router.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) { | |
75 | 46 | var _routes = routes[input.type]; |
76 | 46 | if (_routes) { |
77 | 45 | for (var i in _routes) { |
78 | 70 | if (_routes.hasOwnProperty(i)) { |
79 | 70 | var _route = _routes[i]; |
80 | 70 | var condition = input.matched ? input.matched : input.condition; |
81 | 70 | var matched = _route.match(condition); |
82 | // matched or replaced | |
83 | 70 | if (matched !== null) { |
84 | 48 | if (Array.isArray(matched)) { |
85 | // destination arrived, do dispatching | |
86 | // and keep the context of `route` | |
87 | 31 | _route.dispatch(matched, context); |
88 | 31 | return true; |
89 | 17 | } else if (matched !== condition) { |
90 | // part of the `condition` is replaced, `matched` will become new condition | |
91 | 10 | input.matched = matched; |
92 | // route to sub-module | |
93 | 10 | if (route(context, input, _route.routes)) { |
94 | 10 | return true; |
95 | } | |
96 | } | |
97 | } | |
98 | } | |
99 | } | |
100 | } | |
101 | ||
102 | 5 | if ('NOTFOUND' !== input.type) { |
103 | // if nothing matched, route to `NOTFOUND` and do appropriate operations. | |
104 | 3 | if (route(context, {condition: input.condition, type: 'NOTFOUND', matched: input.matched}, routes)) { |
105 | 1 | return true; |
106 | } | |
107 | } | |
108 | // if rules of `NOTFOUND` is not matched, call the `notFound` function if supplied. | |
109 | 4 | return !!(notFound && notFound.call(context, context.request, context.response)); |
110 | } | |
111 | ||
112 | 33 | var input = {type: type, condition: url}; |
113 | 33 | 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) { | |
124 | 17 | var self = this; |
125 | 17 | routes.forEach(function (route) { |
126 | 27 | var def = self.normalizeRouteDefinition(route); |
127 | 27 | 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) { | |
145 | 41 | var _method = method.toUpperCase(); |
146 | ||
147 | 41 | if (this.supportedMethods.indexOf(_method) === -1) { |
148 | 0 | throw new Error('Routing method `' + _method + '` not supported.'); |
149 | } | |
150 | ||
151 | 41 | var rule = {type: _method, pattern: pattern}; |
152 | ||
153 | // sanity check for all possible formats of handler | |
154 | 41 | if (Array.isArray(handler) && Array.isArray(handler[0])) { |
155 | // handler is an array of sub-url routing definitions | |
156 | 6 | var routes = handler; |
157 | 6 | routes.forEach(function (def) { |
158 | 10 | if (!Array.isArray(def)) { |
159 | 0 | throw new Error('Element of sub-url definition should be type of array.'); |
160 | } | |
161 | }); | |
162 | ||
163 | 6 | var self = this; |
164 | 6 | routes.forEach(function (url, idx) { |
165 | 10 | var def = self.normalizeRouteDefinition(url, {method: _method}); |
166 | 10 | if (hooks) { |
167 | 4 | def.hooks = self.stackHook(hooks, def.hooks); |
168 | } | |
169 | 10 | routes[idx] = [def.pattern, def.handler, def.method, def.hooks]; |
170 | }); | |
171 | ||
172 | 6 | var subRouter = new Router(routes); |
173 | 6 | rule.rules = subRouter.rules; |
174 | 35 | } else if ('function' === typeof handler) { |
175 | // the simplest case, `handler` is a function | |
176 | 35 | rule._handleFunction = handler; |
177 | 35 | if (hooks) { |
178 | 7 | rule.hooks = hooks; |
179 | 7 | handler = this.stackHook(hooks, [handler]); |
180 | // build the function chain | |
181 | 7 | handler = this.chainFn(handler); |
182 | } | |
183 | 35 | rule.handler = handler; |
184 | } else { | |
185 | 0 | 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 | |
189 | 41 | if ('NOTFOUND' === _method) { |
190 | 1 | this.rules.unshift(rule); |
191 | } else { | |
192 | 40 | this.rules.push(rule); |
193 | } | |
194 | 41 | 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) { | |
210 | 8 | this.mount([ |
211 | [url, handler, 'get', hooks] | |
212 | ]); | |
213 | 8 | 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) { | |
229 | 1 | this.mount([ |
230 | [url, handler, 'post', hooks] | |
231 | ]); | |
232 | 1 | 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) { | |
248 | 0 | this.mount([ |
249 | [url, handler, 'put', hooks] | |
250 | ]); | |
251 | 0 | 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) { | |
267 | 0 | this.mount([ |
268 | [url, handler, 'delete', hooks] | |
269 | ]); | |
270 | 0 | 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) { | |
286 | 0 | this.mount([ |
287 | [url, handler, 'head', hooks] | |
288 | ]); | |
289 | 0 | 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) { | |
305 | 0 | this.mount([ |
306 | [url, handler, 'notFound', hooks] | |
307 | ]); | |
308 | 0 | 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 () { | |
319 | 34 | if (!this.routes) { |
320 | 20 | this.build(); |
321 | } | |
322 | 33 | 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) { | |
333 | 1 | hooks = this.normalizeHooks(hooks); |
334 | 1 | if (Array.isArray(hooks) && hooks.length > 0) { |
335 | 1 | var self = this; |
336 | 1 | var _hook = function (rules) { |
337 | 7 | rules.forEach(function (rule) { |
338 | 18 | if (rule.rules) { |
339 | 6 | _hook(rule.rules); |
340 | } else { | |
341 | 12 | rule.hooks = self.stackHook(hooks, rule.hooks); |
342 | 12 | rule.handler = self.stackHook(rule.hooks, [rule._handleFunction]); |
343 | 12 | rule.handler = self.chainFn(rule.handler); |
344 | } | |
345 | }); | |
346 | }; | |
347 | 1 | _hook(this.rules); |
348 | 1 | 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) { | |
361 | 20 | name = name.replace(/([A-Z]+)/g, function (word) { |
362 | 34 | return '/' + word.toLowerCase(); |
363 | }); | |
364 | 20 | 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) { | |
377 | 59 | switch (url[0]) { |
378 | case '^': | |
379 | 52 | return url; |
380 | case '/': | |
381 | 3 | url = url.slice(1); |
382 | 3 | break; |
383 | } | |
384 | ||
385 | 7 | if (prefix[prefix.length - 1] !== '/') { |
386 | 0 | prefix += '/'; |
387 | } | |
388 | 7 | url = prefix + url; |
389 | 7 | 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) { | |
402 | 8 | var self = this; |
403 | 8 | contextClass = contextClass || this.contextClass; |
404 | 8 | if ('function' !== typeof notFound) { |
405 | 8 | notFound = function (request, response) { |
406 | 0 | response.statusCode = 404; |
407 | 0 | response.end('404 not found!', 'utf8'); |
408 | 0 | console.error('Cannot match a route for url "%s" method "%s"', request.url, request.method); |
409 | }; | |
410 | } | |
411 | 8 | server.on('request', function (request, response) { |
412 | 8 | var context = new contextClass(request, response); |
413 | 8 | 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) { | |
427 | 58 | if (pattern instanceof RegExp) { |
428 | 0 | return pattern; |
429 | } | |
430 | 58 | if (typeof pattern === "string") { |
431 | 57 | pattern = this.prefixUrl(this.urlRoot, pattern); |
432 | 57 | return new RegExp(pattern); |
433 | } | |
434 | 1 | 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) { | |
447 | 37 | var method = route[2]; |
448 | 37 | var hooks = route[3]; |
449 | 37 | defaults = defaults || {}; |
450 | ||
451 | 37 | if ('string' !== typeof method) { |
452 | 8 | hooks = method; |
453 | 8 | method = defaults.method || 'get'; |
454 | } | |
455 | ||
456 | 37 | hooks = this.normalizeHooks(hooks); |
457 | ||
458 | 37 | 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) { | |
470 | 73 | if (hooks) { |
471 | // do sanity check for pre hooks | |
472 | 44 | if ('function' === typeof hooks) { |
473 | 1 | hooks = [hooks]; |
474 | 43 | } else if (!Array.isArray(hooks)) { |
475 | 0 | throw new Error('Hooks must be a function or array of functions'); |
476 | } | |
477 | 44 | hooks.forEach(function (hook) { |
478 | 142 | if ('function' !== typeof hook && null !== hook) { |
479 | 0 | throw new Error('Hooks must be array of functions with optional null placeholder for function being hooked.'); |
480 | } | |
481 | }); | |
482 | ||
483 | 44 | if (hooks.indexOf(null) === -1) { |
484 | // all pre hooks | |
485 | 3 | hooks.push(null); |
486 | } | |
487 | } | |
488 | 73 | 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) { | |
501 | 35 | targetHooks = targetHooks || [null]; |
502 | 35 | srcHooks = this.normalizeHooks(srcHooks); |
503 | 35 | var idx = srcHooks.indexOf(null); |
504 | 35 | var pre = srcHooks.slice(0, idx++); |
505 | 35 | targetHooks = pre.concat(targetHooks); |
506 | 35 | if (idx !== srcHooks.length) { |
507 | 34 | var post = srcHooks.slice(idx); |
508 | 34 | targetHooks = targetHooks.concat(post); |
509 | } | |
510 | 35 | 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) { | |
522 | 19 | var fn = chain(fns, function (h, idx, arr, next, args) { |
523 | 42 | args = toArray(args); |
524 | 42 | args.push(next); |
525 | 42 | if (h.apply(this, args)) { |
526 | 22 | next(); |
527 | } | |
528 | }); | |
529 | 19 | return function () { |
530 | 12 | 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) { | |
543 | 33 | var routes = {}; |
544 | 33 | var _rules = rules; |
545 | 33 | rules = rules || this.rules; |
546 | 33 | var self = this; |
547 | 33 | rules.forEach(function (rule) { |
548 | 58 | var hasSub = Array.isArray(rule.rules) && rule.rules.length > 0; |
549 | 58 | var type = rule.type.toUpperCase(); |
550 | 58 | var handler = rule.handler; |
551 | 58 | var pattern = self.normalizePattern(rule.pattern); |
552 | ||
553 | 57 | var current = {id: type + '_' + pattern.toString()}; |
554 | ||
555 | 57 | if (hasSub) { |
556 | // sub-urls | |
557 | 12 | current.match = function (condition) { |
558 | 17 | return condition.replace(pattern, ''); |
559 | }; | |
560 | 12 | current.routes = self.build(rule.rules); |
561 | 12 | type = 'PREFIX'; |
562 | } else { | |
563 | 45 | current.dispatch = function (matched, context) { |
564 | 31 | matched[0] = context; |
565 | 31 | handler.apply(null, matched); |
566 | }; | |
567 | 45 | current.match = function (condition) { |
568 | 53 | return pattern.exec(condition); |
569 | }; | |
570 | } | |
571 | ||
572 | 57 | if (!routes.hasOwnProperty(type)) { |
573 | 41 | routes[type] = {}; |
574 | } | |
575 | ||
576 | 57 | routes[type][current.id] = current; |
577 | }); | |
578 | ||
579 | 32 | if (routes.PREFIX) { |
580 | 8 | this.supportedMethods.forEach(function (type) { |
581 | 48 | if (type !== 'NOTFOUND') { |
582 | // method of sub-url may various, so put `prefix` to all types | |
583 | 40 | routes[type] = routes[type] || {}; |
584 | 40 | extend(routes[type], routes.PREFIX); |
585 | } | |
586 | }); | |
587 | } | |
588 | ||
589 | 32 | if (!_rules) { |
590 | 20 | this.routes = routes; |
591 | } | |
592 | 32 | return routes; |
593 | } | |
594 | }; | |
595 |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Module dependencies | |
3 | */ | |
4 | ||
5 | 1 | var http = require('http'); |
6 | 1 | var EventEmitter = require('events').EventEmitter; |
7 | 1 | var Klass = require('./klass').Klass; |
8 | 1 | var extend = require('./util').extend; |
9 | 1 | var Core = require('./core').Core; |
10 | ||
11 | /** | |
12 | * Site constructor function | |
13 | * | |
14 | * @constructor | |
15 | * @private | |
16 | */ | |
17 | ||
18 | function Site() { | |
19 | 4 | EventEmitter.call(this); |
20 | 4 | if (!process.env.NODE_ENV) { |
21 | 1 | process.env.NODE_ENV = 'default'; |
22 | } | |
23 | 4 | this.data = {}; |
24 | 4 | this.plugins = {}; |
25 | 4 | this.apps = {}; |
26 | 4 | this.core = new Core(this); |
27 | 4 | this.env(); |
28 | 4 | 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 | ||
40 | 1 | Site.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) { | |
51 | 7 | name = name || 'default'; |
52 | 7 | if (!this.data[name]) { |
53 | 6 | this.data[name] = {}; |
54 | 6 | this.plugins[name] = {}; |
55 | 6 | this.apps[name] = {}; |
56 | } | |
57 | 7 | this._env = name; |
58 | 7 | 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) { | |
71 | 10 | 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) { | |
83 | 6 | 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) { | |
96 | 7 | var name = 'string' === typeof plugin ? plugin : plugin.name; |
97 | 7 | var _plugin = { |
98 | plugin: plugin, | |
99 | options: options | |
100 | }; | |
101 | ||
102 | 7 | 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) { | |
116 | 1 | if (Array.isArray(app)) { |
117 | 0 | var self = this; |
118 | 0 | app.forEach(function (_app) { |
119 | 0 | self.load(_app, options, routes); |
120 | }); | |
121 | } | |
122 | ||
123 | 1 | var name; |
124 | ||
125 | 1 | if ('function' === typeof app) { |
126 | 1 | name = app.prototype.name; |
127 | 1 | if (!routes) { |
128 | 1 | routes = options; |
129 | } | |
130 | } else { | |
131 | 0 | name = app.name; |
132 | 0 | routes = options; |
133 | 0 | options = null; |
134 | } | |
135 | ||
136 | 1 | var _app = { |
137 | app: app, | |
138 | options: options | |
139 | }; | |
140 | ||
141 | 1 | this.map(routes); |
142 | ||
143 | 1 | return _set(this, this.apps, name, _app); |
144 | }, | |
145 | ||
146 | map: function (routes) { | |
147 | 2 | if (routes) { |
148 | 1 | var _routes = this.get('routes') || {}; |
149 | 1 | _routes = extend(_routes, routes); |
150 | 1 | this.set('routes', _routes); |
151 | } | |
152 | 2 | return this; |
153 | }, | |
154 | ||
155 | /** | |
156 | * Process all the plugins with core | |
157 | * | |
158 | * @private | |
159 | */ | |
160 | ||
161 | usePlugins: function () { | |
162 | 4 | var plugins = extend({}, this.plugins['default']); |
163 | 4 | if (process.env.NODE_ENV && 'default' !== process.env.NODE_ENV) { |
164 | 2 | extend(plugins, this.plugins[process.env.NODE_ENV]); |
165 | } | |
166 | ||
167 | 4 | var core = this.core; |
168 | 4 | Object.keys(plugins).forEach(function (name) { |
169 | 7 | var plugin = plugins[name]; |
170 | 7 | core.use(plugin.plugin, plugin.options); |
171 | }); | |
172 | 4 | return this; |
173 | }, | |
174 | ||
175 | /** | |
176 | * Process all apps in current environment. | |
177 | * | |
178 | * @private | |
179 | */ | |
180 | ||
181 | loadApps: function () { | |
182 | 4 | var apps = extend({}, this.apps['default']); |
183 | 4 | if (process.env.NODE_ENV && 'default' !== process.env.NODE_ENV) { |
184 | 2 | apps = extend(apps, this.apps[process.env.NODE_ENV]); |
185 | } | |
186 | ||
187 | 4 | var defaultAppOptions = this.get('appOptions'); |
188 | 4 | var self = this; |
189 | 4 | var core = self.core; |
190 | ||
191 | 4 | Object.keys(apps).forEach(function (k) { |
192 | 1 | var appObj = apps[k]; |
193 | 1 | var appOptions = extend({}, defaultAppOptions, appObj.options); |
194 | 1 | var app = appObj.app; |
195 | 1 | if ('function' === typeof app) { |
196 | 1 | app = new app(appOptions); |
197 | } | |
198 | 1 | app.delegate = self; |
199 | 1 | core.load(app); |
200 | }); | |
201 | 4 | return this; |
202 | }, | |
203 | ||
204 | mapRoutes: function () { | |
205 | 4 | var routes = extend({}, this.data['default'].routes); |
206 | 4 | if (process.env.NODE_ENV && 'default' !== process.env.NODE_ENV) { |
207 | 2 | var dataEnv = this.data[process.env.NODE_ENV]; |
208 | 2 | routes = dataEnv ? extend(routes, dataEnv.routes) : routes; |
209 | } | |
210 | 4 | this.core.map(routes); |
211 | 4 | 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) { | |
223 | 4 | this.usePlugins(); |
224 | 4 | this.loadApps(); |
225 | 4 | this.mapRoutes(); |
226 | 4 | var listener = this.core.getListener(); |
227 | 4 | if (server) { |
228 | 4 | server.on('request', listener); |
229 | } else { | |
230 | 0 | server = http.createServer(listener); |
231 | 0 | server.listen(this.get('port'), this.get('host')); |
232 | } | |
233 | 4 | this.server = server; |
234 | 4 | return server; |
235 | } | |
236 | }; | |
237 | ||
238 | /** | |
239 | * Exposes Site | |
240 | */ | |
241 | ||
242 | 1 | exports.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 | ||
255 | function _set(self, data, key, value) { | |
256 | 18 | var _data = data[self._env]; |
257 | 18 | if ('object' === typeof key) { |
258 | 4 | extend(_data, key); |
259 | } else { | |
260 | 14 | _data[key] = value; |
261 | } | |
262 | 18 | 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 | ||
276 | function _get(self, data, key) { | |
277 | 8 | if (Array.isArray(key)) { |
278 | 1 | var values = {}; |
279 | 1 | key.forEach(function (k) { |
280 | 2 | values[k] = _get(self, data, k); |
281 | }); | |
282 | 1 | return values; |
283 | } | |
284 | 7 | var _data = data[process.env.NODE_ENV]; |
285 | 7 | if (!_data || !_data.hasOwnProperty(key)) { |
286 | 5 | _data = data['default']; |
287 | } | |
288 | 7 | return _data[key]; |
289 | } | |
290 |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * Module dependencies | |
3 | */ | |
4 | ||
5 | 1 | var path = require('path'); |
6 | 1 | var fs = require('fs'); |
7 | ||
8 | /** | |
9 | * Module exports | |
10 | */ | |
11 | ||
12 | 1 | exports.extend = extend; |
13 | 1 | exports.toArray = toArray; |
14 | 1 | exports.byteLength = byteLength; |
15 | 1 | exports.expose = expose; |
16 | ||
17 | /** | |
18 | * Convert an array-like object to array (arguments etc.) | |
19 | * | |
20 | * @param obj | |
21 | * @return {Array} | |
22 | */ | |
23 | ||
24 | function toArray(obj) { | |
25 | 332 | var len = obj.length, |
26 | arr = new Array(len); | |
27 | 332 | for (var i = 0; i < len; ++i) { |
28 | 601 | arr[i] = obj[i]; |
29 | } | |
30 | 332 | return arr; |
31 | } | |
32 | ||
33 | /** | |
34 | * Extend an object with given properties | |
35 | * | |
36 | * @param obj | |
37 | * @param props | |
38 | * @return {*} | |
39 | */ | |
40 | ||
41 | function extend(obj, props) { | |
42 | 142 | var propsObj = toArray(arguments); |
43 | 142 | propsObj.shift(); |
44 | 142 | propsObj.forEach(function(props) { |
45 | 166 | if (props) { |
46 | 126 | Object.keys(props).forEach(function(key) { |
47 | 188 | obj[key] = props[key]; |
48 | }); | |
49 | } | |
50 | }); | |
51 | 142 | return obj; |
52 | } | |
53 | ||
54 | /** | |
55 | * Calculate byte length of string | |
56 | * | |
57 | * @param str | |
58 | * @return {Number} | |
59 | */ | |
60 | ||
61 | function byteLength(str) { | |
62 | 1 | if (!str) { |
63 | 0 | return 0; |
64 | } | |
65 | 1 | var matched = str.match(/[^\x00-\xff]/g); |
66 | 1 | 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 | ||
77 | function expose(module, subModules) { | |
78 | 6 | var modulePath = path.dirname(module.filename); |
79 | 6 | var exports_ = module.exports; |
80 | 6 | if (subModules) { |
81 | 5 | if (!Array.isArray(subModules)) { |
82 | 5 | subModules = [subModules]; |
83 | } | |
84 | 5 | subModules.forEach(function (sub) { |
85 | 5 | var subName; |
86 | 5 | var subPath; |
87 | 5 | if (sub[0] === '/') { |
88 | 5 | subPath = sub; |
89 | 5 | sub = sub.split('/'); |
90 | 5 | sub = sub[sub.length - 1]; |
91 | } else { | |
92 | 0 | subName = sub; |
93 | 0 | subPath = path.join(modulePath, subName); |
94 | } | |
95 | 5 | subName = sub.split('.')[0]; |
96 | 5 | var _module = {}; |
97 | 5 | _module[subName] = require(subPath); |
98 | 5 | extend(exports_, _module); |
99 | }); | |
100 | } else { | |
101 | 1 | subModules = fs.readdirSync(modulePath); |
102 | 1 | if (subModules.length > 0) { |
103 | 1 | subModules.forEach(function (sub) { |
104 | 6 | var subPath = path.join(modulePath, sub); |
105 | 6 | var stats = fs.statSync(subPath); |
106 | 6 | if ((stats.isDirectory() && fs.existsSync(path.join(subPath, 'index.js'))) || (stats.isFile() && path.extname(subPath) === '.js')) { |
107 | 6 | if (subPath !== module.filename) { |
108 | 5 | expose(module, subPath); |
109 | } | |
110 | } | |
111 | }); | |
112 | } | |
113 | } | |
114 | } | |
115 |
Line | Hits | Source |
---|---|---|
1 | /** | |
2 | * View template manager | |
3 | */ | |
4 | ||
5 | /** | |
6 | * Module dependencies | |
7 | */ | |
8 | ||
9 | 1 | var Path = require('path'); |
10 | 1 | var fs = require('fs'); |
11 | 1 | var Base = require('./klass').Base; |
12 | 1 | var extend = require('./util').extend; |
13 | 1 | var 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 | ||
31 | function View(engine, options) { | |
32 | 10 | if ('function' !== typeof engine.render && 'function' !== typeof engine.compile) { |
33 | 0 | throw new Error('Your template engine must implements at least one of "render" or "compile" functions.'); |
34 | } | |
35 | ||
36 | 10 | this.engine = engine; |
37 | 10 | options = options || {}; |
38 | 10 | this.exts = options.exts || ['.mu', '.js']; |
39 | 10 | this.contexts = {}; |
40 | 10 | this.viewPaths = {}; |
41 | 10 | this.rootViewPaths = {}; |
42 | 10 | this.partials = {}; |
43 | 10 | this.partialsEncoded = {}; |
44 | ||
45 | 10 | if (options.rootViewPath) { |
46 | 7 | this.rootViewPath = options.rootViewPath; |
47 | 7 | this.registerPartialDir(); |
48 | } | |
49 | ||
50 | 10 | this.hasCompiler = 'function' === typeof this.engine.compile; |
51 | ||
52 | 10 | if (options.cache) { |
53 | 2 | this.cache = {}; |
54 | 2 | if (this.hasCompiler) { |
55 | 2 | this.cacheCompiled = {}; |
56 | } | |
57 | } | |
58 | 10 | if (options.context) { |
59 | 2 | this.context = options.context; |
60 | } | |
61 | 10 | if (options.layout) { |
62 | 1 | this.setLayout(options.layout); |
63 | } | |
64 | 10 | if (options.minify) { |
65 | 1 | this.minify = options.minify; |
66 | } | |
67 | } | |
68 | ||
69 | /** | |
70 | * View prototype object | |
71 | */ | |
72 | ||
73 | 1 | View.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) { | |
85 | 1 | this.contexts[path] = context; |
86 | 1 | 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) { | |
100 | 13 | if (this.hasCompiler) { |
101 | 10 | var compiled; |
102 | 10 | if (this.cacheCompiled) { |
103 | 4 | compiled = this.cacheCompiled[templateString]; |
104 | } | |
105 | 10 | if (!compiled) { |
106 | 9 | compiled = this.engine.compile(templateString); |
107 | 9 | if (this.cacheCompiled) { |
108 | 3 | this.cacheCompiled[templateString] = compiled; |
109 | } | |
110 | } | |
111 | 10 | return compiled.render(extend({}, this.context, context), partials); |
112 | } else { | |
113 | 3 | 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) { | |
129 | 10 | if ('function' === typeof context) { |
130 | 1 | callback = context; |
131 | 1 | context = null; |
132 | } | |
133 | ||
134 | 10 | var self = this; |
135 | 10 | var partials = this.getPartials(); |
136 | 10 | var path_ = this.getViewPath(path); |
137 | ||
138 | 10 | if (this.contexts[path]) { |
139 | 1 | context = extend({}, this.contexts[path], context); |
140 | } | |
141 | ||
142 | 10 | if (this.cache && this.cache[path_]) { |
143 | 1 | callback(null, this.render(this.cache[path_], context, partials)); |
144 | 1 | return; |
145 | } | |
146 | ||
147 | 9 | fs.readFile(path_, 'utf8', function (err, templateString) { |
148 | 9 | if (err) { |
149 | 1 | callback(err, ''); |
150 | } else { | |
151 | 8 | if (self.cache) { |
152 | 2 | self.cache[path_] = templateString; |
153 | } | |
154 | 8 | 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) { | |
169 | 2 | this.layout = this.layout || {}; |
170 | function layoutToStr(arr) { | |
171 | 2 | var str = ''; |
172 | 2 | arr.forEach(function (l) { |
173 | 7 | str += '{{> ' + l + '}}\n'; |
174 | }); | |
175 | 2 | return str; |
176 | } | |
177 | ||
178 | 2 | if (typeof name === 'string') { |
179 | 1 | this.layout[name] = layoutToStr(layout); |
180 | } else { | |
181 | 1 | Object.keys(name).forEach(function (n) { |
182 | 1 | this.layout[n] = layoutToStr(name[n]); |
183 | }, this); | |
184 | } | |
185 | 2 | 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) { | |
198 | 2 | var layoutStr = this.layout[layoutName]; |
199 | 2 | if (!layoutStr) { |
200 | 0 | throw new Error('Layout not exists'); |
201 | } | |
202 | 2 | if (this.contexts) { |
203 | 2 | context = extend({}, this.contexts[layoutName], context); |
204 | } | |
205 | 2 | 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) { | |
218 | 121 | var name_ = name; |
219 | ||
220 | 121 | if (namespace) { |
221 | 7 | name_ = namespace + ':' + name; |
222 | } | |
223 | ||
224 | 121 | var self = this; |
225 | 121 | var minify; |
226 | // try to get minify function for current partial type when caching is enabled | |
227 | 121 | if (this.cache && this.minify) { |
228 | 4 | var ext = Path.extname(name); |
229 | 4 | if (this.minify.hasOwnProperty(ext)) { |
230 | 1 | minify = this.minify[ext]; |
231 | } | |
232 | } | |
233 | ||
234 | 121 | if (!templateString) { |
235 | 112 | var path_ = this.getViewPath(name_); |
236 | 112 | templateString = fs.readFileSync(path_, 'utf8'); |
237 | } | |
238 | ||
239 | 121 | if (minify) { |
240 | 1 | templateString = minify(templateString); |
241 | } | |
242 | 121 | templateString = self.wrapPartial(name, templateString); |
243 | 121 | self.partials[name_] = templateString; |
244 | 121 | self.partialsEncoded[name_] = encodeURIComponent(templateString); |
245 | 121 | 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 () { | |
256 | 12 | if (!this.cache) { |
257 | 8 | this.registerPartialDir(); |
258 | 8 | Object.keys(this.rootViewPaths).forEach(function (namespace) { |
259 | 1 | this.registerPartialDir('', namespace); |
260 | }, this); | |
261 | } | |
262 | 12 | 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) { | |
275 | 1 | this.rootViewPaths[pathNamespace] = viewPath; |
276 | 1 | this.registerPartialDir('', pathNamespace); |
277 | 1 | 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) { | |
289 | 122 | if ((Path.sep || '/') === path[0]) { |
290 | 3 | return path; |
291 | } | |
292 | 119 | var path_ = this.viewPaths[path]; |
293 | 119 | if (!path_) { |
294 | 6 | path_ = path.split(':'); |
295 | 6 | if (1 === path_.length) { |
296 | 5 | path_ = Path.join(this.rootViewPath, path); |
297 | } else { | |
298 | 1 | path_ = Path.join(this.rootViewPaths[path_[0]], path_[1]); |
299 | } | |
300 | } | |
301 | 119 | 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) { | |
312 | 1 | this.scriptLoaderUrl = url; |
313 | 1 | 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) { | |
327 | 2 | var scriptStr = ['<script src="', this.scriptLoaderUrl, '" type="text/javascript"></script>\n'].join(''); |
328 | 2 | scriptStr += '<script type="text/javascript">\n'; |
329 | 2 | var self = this; |
330 | 2 | scriptStr += 'head.js('; |
331 | 2 | var lastIdx = scripts.length - 1; |
332 | 2 | scripts.forEach(function (scriptName, idx) { |
333 | 3 | var url = self.scripts[scriptName]; |
334 | 3 | scriptStr += "'" + url + "'"; |
335 | 3 | if (idx < lastIdx) { |
336 | 1 | scriptStr += ', '; |
337 | } | |
338 | }); | |
339 | ||
340 | 2 | if (typeof onLoad === 'function') { |
341 | 1 | scriptStr += ', ' + onLoad.toString(); |
342 | } | |
343 | 2 | scriptStr += ');\n</script>'; |
344 | 2 | var pName = partialName.split(':'); |
345 | 2 | if (pName.length === 2) { |
346 | // partial has namespace | |
347 | 1 | this.registerPartial(pName[1], scriptStr, pName[0]); |
348 | } else { | |
349 | 1 | this.registerPartial(partialName, scriptStr); |
350 | } | |
351 | 2 | 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) { | |
364 | 3 | var self = this; |
365 | ||
366 | function _set(name, url) { | |
367 | 3 | self.scripts = self.scripts || {}; |
368 | 3 | self.scripts[name] = url; |
369 | } | |
370 | ||
371 | 3 | if (typeof urls === 'string') { |
372 | 2 | _set(urls, url); |
373 | } else { | |
374 | 1 | Object.keys(urls).forEach(function (name) { |
375 | 1 | _set(name, urls[name]); |
376 | }); | |
377 | } | |
378 | 3 | 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) { | |
390 | 38 | relativeDir = relativeDir || ''; |
391 | 38 | var self = this; |
392 | 38 | var rootViewPath = (namespace ? this.rootViewPaths[namespace] : this.rootViewPath) || this.rootViewPath; |
393 | 38 | var exts = this.exts; |
394 | 38 | var dir = Path.join(rootViewPath, relativeDir); |
395 | 38 | var files = fs.readdirSync(dir); |
396 | ||
397 | 38 | files.forEach(function (file) { |
398 | 171 | var absolutePath = Path.join(rootViewPath, relativeDir, file); |
399 | 171 | var relativeFilePath = Path.join(relativeDir, file); |
400 | 171 | var stats = fs.statSync(absolutePath); |
401 | 171 | if (stats.isFile() && exts.indexOf(Path.extname(absolutePath)) > -1) { |
402 | 112 | var viewName = (namespace ? namespace + ':' : '') + relativeFilePath; |
403 | 112 | self.viewPaths[viewName] = absolutePath; |
404 | 112 | self.registerPartial(relativeFilePath, null, namespace); |
405 | 59 | } else if (stats.isDirectory()) { |
406 | 21 | 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) { | |
421 | 121 | var type = Path.extname(filename); |
422 | 121 | if ('.js' === type) { |
423 | 79 | str = '<script type="text/javascript">' + str + '</script>'; |
424 | } | |
425 | 121 | return str; |
426 | } | |
427 | }; | |
428 | ||
429 | /** | |
430 | * Expose View. | |
431 | * @type {Function} | |
432 | */ | |
433 | ||
434 | 1 | exports.View = View; |
435 |