forked from cliftonc/calipso
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Router.js
285 lines (238 loc) · 9.5 KB
/
Router.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
/*!
* Calipso Core Library
* Copyright(c) 2011 Clifton Cunningham
* MIT Licensed
*
* The Calipso Router provides a router object to each module that enables each module to register
* its own functions to respond to URL patterns (as per the typical Express approach). Note
* that Calipso itself does not respond to any URL outside of those exposed by a module, if all are disabled
* the application will do nothing.
*
* Borrowed liberally from Connect / ExpressJS itself for this, thanks for the great work!
*/
/**
* Includes
*/
var rootpath = process.cwd() + '/',
path = require('path'),
calipso = require(path.join(rootpath, 'lib/calipso')),
url = require('url'),
fs = require('fs'),
blocks = require(path.join(rootpath, 'lib/Blocks'));
/**
* Normalize the given path string,
* returning a regular expression.
*
* An empty array should be passed,
* which will contain the placeholder
* key names. For example "/user/:id" will
* then contain ["id"].
*
* BORROWED FROM Connect
*
* @param {String} path
* @param {Array} keys
* @return {RegExp}
* @api private
*/
function normalizePath(path, keys) {
path = path
.concat('/?')
.replace(/\/\(/g, '(?:/')
.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function (_, slash, format, key, capture, optional) {
keys.push(key);
slash = slash || '';
return ''
+ (optional ? '' : slash)
+ '(?:'
+ (optional ? slash : '')
+ (format || '') + (capture || '([^/]+?)') + ')'
+ (optional || '');
})
.replace(/([\/.])/g, '\\$1')
.replace(/\*/g, '(.+)');
return new RegExp('^' + path + '$', 'i');
}
/**
* Core router object, use the return model to ensure
* that we always return a new instance when called.
*
* A Router is attached to each module, and allows each module to effectively
* act as its own controller in a mini MVC model.
*
* This class exposes:
*
* addRoutes: function, to add Routes to a module.
* route: iterate through the routes, match, and then call the matched function in the module.
*
*/
var Router = function (moduleName, modulePath) {
return {
moduleName: moduleName,
modulePath: modulePath,
routes: [],
/**
* A route is defined by three parameters:
*
* path: a string in the form 'GET /url' where the first piece is the HTTP method to respond to.
* OR
* a regex function (it matches only on GET requests).
* fn: the function in the module to call if the route matches.
* options: additional configuration options, specifically:
* end - deprecated. TODO CLEANUP
* admin - is the route an administrative route (user must have isAdmin = true).
*/
addRoute: function (path, fn, options, next) {
var router = this;
// Default options
options = options || {end: true, admin: false};
// Can't do any real checking here as everything is initialised in parallel.
router.routes.push({path: path, fn: fn, options: options});
next();
},
/**
* Module routing function, iterates through the configured routes and trys to match.
* This has been borrowed from the Express core routing module and refactored slightly
* to deal with the fact this is much more specific.
*/
route: function (req, res, next) {
var router = this,
requestUrl = url.parse(req.url, true),
routes = this.routes;
// Use step to enable parallel execution
calipso.lib.step(
function matchRoutes() {
// Emit event to indicate starting
var i, l, group = this.group(), // Enable parallel execution
counter = 0;
for (i = 0, l = routes.length; i < l; i = i + 1) {
var keys = [],
route = routes[i],
template = null,
block = "",
cache = true,
routeMethod = "",
routeRegEx,
j,
paramLen,
param,
allPages = false;
if (typeof route.path === "string") {
routeMethod = route.path.split(" ")[0];
routeRegEx = normalizePath(route.path.split(" ")[1], keys);
} else {
routeRegEx = route.path;
allPages = true; // Is a regex
}
var captures = requestUrl.pathname.match(routeRegEx);
if (captures && (!routeMethod || req.method === routeMethod)) {
// Check to see if we matched a non /.*/ route to flag a 404 later
res.routeMatched = !allPages || res.routeMatched;
// If admin, then set the route
if (route.options.admin) {
res.layout = "admin";
}
// Check to see if it requires admin access
var isAdmin = req.session.user && req.session.user.isAdmin;
if (route.options.admin && !isAdmin) {
req.flash('error', req.t('You need to be an administrative user to view that page.'));
res.statusCode = 401;
res.redirect("/");
group()();
return;
}
// Check to see if it requires logged in user access
var isUser = req.session.user && req.session.user.username;
if (route.options.user && !isUser) {
req.flash('error', req.t('You need to be logged in to view that page.'));
res.statusCode = 401;
res.redirect("/");
group()();
return;
}
// Debugging
calipso.silly("Module " + router.moduleName + " matched route: " + requestUrl.pathname + " / " + routeRegEx.toString() + " [" + res.routeMatched + "]");
// Lookup the template for this route
if (route.options.template) {
template = calipso.modules[router.moduleName].templates[route.options.template];
if (!template && route.options.template) {
var err = new Error("The specified template: " + route.options.template + " does not exist in the module: " + router.modulePath);
return group()(err);
} else {
calipso.silly("Using template: " + route.options.template + " for module: " + router.modulePath);
}
}
// Set the block & cache settings
block = route.options.block;
// Set the object to hold the rendered blocks if it hasn't been created already
if (!res.renderedBlocks) {
res.renderedBlocks = new blocks.RenderedBlocks(calipso.cache);
}
// Copy over any params that make sense from the url
req.moduleParams = {};
for (j = 1, paramLen = captures.length; j < paramLen; j = j + 1) {
var key = keys[j - 1],
val = typeof captures[j] === 'string'
? decodeURIComponent(captures[j])
: captures[j];
if (key) {
req.moduleParams[key] = val;
} else {
// Comes from custom regex, no key
req.moduleParams["regex"] = val;
}
}
// Convert any url parameters if we are not a .* match
if (requestUrl.query && !allPages) {
for (param in requestUrl.query) {
if (requestUrl.query.hasOwnProperty(param)) {
req.moduleParams[param] = requestUrl.query[param];
}
}
}
// Store the params for use outside the router
res.params = res.params || {};
calipso.lib._.extend(res.params,req.moduleParams);
// Set if we should cache this block - do not cache by default, do not cache admins
var cacheBlock = res.renderedBlocks.contentCache[block] = route.options.cache && !isAdmin;
var cacheEnabled = calipso.config.cache;
if(block && cacheBlock && cacheEnabled) {
//console.dir(req.moduleParams);
var cacheKey = calipso.helpers.getCacheKey('block',block,true);
calipso.cache.check(cacheKey,function(err,isCached) {
if(isCached) {
// Set the block from the cache, set layout if needed
res.renderedBlocks.getCache(cacheKey,block,function(err,layout) {
if(layout)
res.layout = layout;
group()(err);
});
} else {
// Execute the module route function and cache the result
route.fn(req, res, template, block, group());
}
});
} else {
route.fn(req, res, template, block, group());
}
}
}
},
function allMatched(err) {
// Once all functions have been called, log the error and pass it back up the tree.
if (err) {
// Enrich the error message with info on the module
// calipso.error("Error in module " + this.moduleName + ", of " + err.message);
err.message = err.message + " Calipso Module: " + router.moduleName;
}
// Emit routed event
next(err, router.moduleName);
}
);
}
};
};
/**
* Exports
*/
module.exports.Router = Router;