UNPKG

27.3 kBJavaScriptView Raw
1/**
2 * @license
3 * MOST Web Framework 2.0 Codename Blueshift
4 * Copyright (c) 2017, THEMOST LP All rights reserved
5 *
6 * Use of this source code is governed by an BSD-3-Clause license that can be
7 * found in the LICENSE file at https://themost.io/license
8 */
9
10var url = require('url');
11var sprintf = require('sprintf').sprintf;
12var async = require('async');
13var fs = require('fs');
14var route = require('../http-route');
15var LangUtils = require('@themost/common/utils').LangUtils;
16var TraceUtils = require('@themost/common/utils').TraceUtils;
17var HttpError = require('@themost/common/errors').HttpError;
18var HttpNotFoundError = require('@themost/common/errors').HttpNotFoundError;
19var path = require('path');
20var _ = require('lodash');
21var HttpConsumer = require('../consumers').HttpConsumer;
22var HttpResult = require('../mvc').HttpResult;
23var HttpNextResult = require('../mvc').HttpNextResult;
24var Q = require('q');
25var accepts = require('accepts');
26
27var STR_CONTROLLER_FILE = './%s-controller.js';
28var STR_CONTROLLER_RELPATH = '/controllers/%s-controller.js';
29
30
31function interopRequireDefault(path) {
32 var obj = require(path);
33 return obj && obj.__esModule ? obj['default'] : obj;
34}
35
36if (process.execArgv.indexOf('ts-node/register')>=0) {
37 //change controller resolution to typescript
38 STR_CONTROLLER_FILE = './%s-controller.ts';
39 STR_CONTROLLER_RELPATH = '/controllers/%s-controller.ts';
40}
41
42/**
43 *
44 * @param s
45 * @returns {*}
46 * @private
47 */
48function _dasherize(s) {
49 if (_.isString(s))
50 return _.trim(s).replace(/[_\s]+/g, '-').replace(/([A-Z])/g, '-$1').replace(/-+/g, '-').replace(/^-/,'').toLowerCase();
51 return s;
52}
53
54
55/**
56 * @method dasherize
57 * @memberOf _
58 */
59
60if (typeof _.dasherize !== 'function') {
61 _.mixin({'dasherize' : _dasherize});
62}
63
64function _isPromise(f) {
65 if (typeof f !== 'object') {
66 return false;
67 }
68 return (typeof f.then === 'function') && (typeof f.catch === 'function');
69}
70
71/**
72 * @method isPromise
73 * @memberOf _
74 */
75if (typeof _.isPromise !== 'function') {
76 _.mixin({'isPromise' : _isPromise});
77}
78
79
80/**
81 * @class
82 * @constructor
83 * @implements AuthorizeRequestHandler
84 * @implements MapRequestHandler
85 * @implements PostMapRequestHandler
86 * @implements ProcessRequestHandler
87 */
88function ViewHandler() {
89 //
90}
91/**
92 *
93 * @param ctor
94 * @param superCtor
95 */
96Object.inherits = function (ctor, superCtor) {
97 if (!ctor.super_) {
98 ctor.super_ = superCtor;
99 while (superCtor) {
100 var superProto = superCtor.prototype;
101 var keys = Object.keys(superProto);
102 for (var i = 0; i < keys.length; i++) {
103 var key = keys[i];
104 if (typeof ctor.prototype[key] === 'undefined')
105 ctor.prototype[key] = superProto[key];
106 }
107 superCtor = superCtor.super_;
108 }
109 }
110};
111
112/**
113 *
114 * @param {string} controllerName
115 * @param {HttpContext} context
116 * @param {Function} callback
117 */
118ViewHandler.queryControllerClass = function(controllerName, context, callback) {
119
120 if (typeof controllerName === 'undefined' || controllerName===null) {
121 callback();
122 }
123 else {
124 //get controller class path and model (if any)
125 var controllerPath = context.getApplication().mapPath(sprintf(STR_CONTROLLER_RELPATH, _.dasherize(controllerName))),
126 controllerModel = context.model(controllerName);
127 //if controller does not exists
128 fs.exists(controllerPath, function(exists){
129 try {
130 //if controller class file does not exist in /controllers/ folder
131 if (!exists) {
132 //try to find if current controller has a model defined
133 if (controllerModel) {
134 var controllerType = controllerModel.type || 'data';
135 if (controllerModel.hidden || controllerModel.abstract) {
136 controllerType = 'hidden';
137 }
138 //try to find controller based on the model's type in controllers folder (e.g. /library-controller.js)
139 controllerPath = context.getApplication().mapPath(sprintf(STR_CONTROLLER_RELPATH, controllerType));
140 fs.exists(controllerPath, function(exists) {
141 if (!exists) {
142 //get controller path according to related model's type (e.g ./data-controller)
143 controllerPath = sprintf(STR_CONTROLLER_FILE, controllerType);
144 //if controller does not exist
145 controllerPath = path.join(__dirname, controllerPath);
146 fs.exists(controllerPath, function(exists) {
147 if (!exists)
148 callback(null, interopRequireDefault('../controllers/base'));
149 else
150 callback(null, interopRequireDefault(controllerPath));
151 });
152 }
153 else {
154 callback(null, interopRequireDefault(controllerPath));
155 }
156 });
157 }
158 else {
159 var ControllerCtor = context.getApplication().getConfiguration().controllers[controllerName] || interopRequireDefault('../controllers/base');
160 callback(null, ControllerCtor);
161 }
162 }
163 else {
164 //return controller class
165 callback(null, interopRequireDefault(controllerPath));
166 }
167 }
168 catch (err) {
169 callback(err);
170 }
171 });
172 }
173};
174
175ViewHandler.RestrictedLocations = [
176 { "path":"^/controllers/", "description":"Most web framework server controllers" },
177 { "path":"^/models/", "description":"Most web framework server models" },
178 { "path":"^/extensions/", "description":"Most web framework server extensions" },
179 { "path":"^/handlers/", "description":"Most web framework server handlers" },
180 { "path":"^/views/", "description":"Most web framework server views" }
181];
182
183ViewHandler.prototype.authorizeRequest = function (context, callback) {
184 try {
185 var uri = url.parse(context.request.url);
186 for (var i = 0; i < ViewHandler.RestrictedLocations.length; i++) {
187 /**
188 * @type {*|LocationSetting}
189 */
190 var location = ViewHandler.RestrictedLocations[i],
191 /**
192 * @type {RegExp}
193 */
194 re = new RegExp(location.path,'ig');
195 if (re.test(uri.pathname)) {
196 callback(new HttpError(403, 'Forbidden'));
197 return;
198 }
199 }
200 callback();
201 }
202 catch(e) {
203 callback(e);
204 }
205};
206/**
207 * @param {HttpContext} context
208 * @param {Function} callback
209 */
210ViewHandler.validateMediaType = function(context, callback) {
211 if (typeof context === 'undefined' || context === null) {
212 return callback();
213 }
214 //validate mime type and route format
215 var accept = accepts(context.request);
216 if (context.request.route && context.request.route.format) {
217 if (accept.type(context.request.route.format)) {
218 return callback();
219 }
220 return callback(new HttpError(415));
221 }
222 return callback();
223};
224
225/**
226 * @param {HttpContext} context
227 * @param {Function} callback
228 */
229ViewHandler.prototype.mapRequest = function (context, callback) {
230 callback = callback || function () { };
231 //try to map request
232 try {
233 //first of all check if a request handler is already defined
234 if (typeof context.request.currentHandler !== 'undefined') {
235 //do nothing (exit mapping)
236 return callback();
237 }
238 var requestUri = url.parse(context.request.url);
239 /**
240 * find route by querying application routes
241 * @type {HttpRoute}
242 */
243 var currentRoute;
244 // check if view handler has been already attached to this request in order to continue route processing by using last route index
245 if (context.request.route && context.request.routeIndex) {
246 // destroy request route
247 delete context.request.route;
248 // destroy request route data
249 delete context.request.routeData;
250 // continue querying routes from the last index
251 currentRoute = queryRoute(requestUri, context, context.request.routeIndex);
252 }
253 else {
254 currentRoute = queryRoute(requestUri, context);
255 }
256 if (typeof currentRoute === 'undefined' || currentRoute === null) {
257 return callback();
258 }
259 //query controller
260 var controllerName = currentRoute["controller"] || currentRoute.routeData["controller"] || queryController(requestUri);
261 if (typeof controllerName === 'undefined' || controllerName === null) {
262 return callback();
263 }
264 //try to find controller class
265 ViewHandler.queryControllerClass(controllerName, context, function(err, ControllerClass) {
266 if (err) {
267 return callback(err);
268 }
269 try {
270 //initialize controller
271 var controller = new ControllerClass();
272 //set controller's name
273 controller.name = controllerName.toLowerCase();
274 //set controller's context
275 controller.context = context;
276 //set request handler
277 var handler = new ViewHandler();
278 handler.controller = controller;
279 context.request.currentHandler = handler;
280 //set route data
281 context.request.route = _.assign({ },currentRoute.route);
282 context.request.routeIndex = currentRoute.routeIndex;
283 context.request.routeData = currentRoute.routeData;
284 //set route data as params
285 for(var prop in currentRoute.routeData) {
286 if (currentRoute.routeData.hasOwnProperty(prop)) {
287 context.params[prop] = currentRoute.routeData[prop];
288 }
289 }
290 return ViewHandler.validateMediaType(context, function(err) {
291 return callback(err);
292 });
293 }
294 catch(err) {
295 return callback(err);
296 }
297 });
298
299 }
300 catch (e) {
301 callback(e);
302 }
303
304};
305/**
306 * @param {HttpContext} context
307 * @param {Function} callback
308 */
309ViewHandler.prototype.postMapRequest = function (context, callback) {
310 try {
311 ViewHandler.prototype.preflightRequest.call(this, context, function(err) {
312 if (err) { return callback(err); }
313 var obj;
314 if (context.is('POST')) {
315 if (context.format==='json') {
316 if (typeof context.request.body === 'string') {
317 //parse json data
318 try {
319 obj = JSON.parse(context.request.body);
320 //set context data
321 context.params.data = obj;
322 }
323 catch(err) {
324 TraceUtils.log(err);
325 return callback(new Error('Invalid JSON data.'));
326 }
327 }
328 }
329 }
330 return callback();
331 });
332 }
333 catch(e) {
334 callback(e);
335 }
336};
337ViewHandler.prototype.preflightRequest = function (context, callback) {
338 try {
339 if (context && (context.request.currentHandler instanceof ViewHandler)) {
340 //set the default origin (with wildcard)
341 var allowCredentials = true,
342 allowOrigin="*",
343 allowHeaders = "Origin, X-Requested-With, Content-Type, Content-Language, Accept, Accept-Language, Authorization",
344 allowMethods = "GET, OPTIONS, PUT, POST, PATCH, DELETE";
345 /**
346 * @private
347 * @type {{allowOrigin:string,allowHeaders:string,allowCredentials:Boolean,allowMethods:string,allow:string}|*}
348 */
349 var route = context.request.route;
350 if (route) {
351 if (typeof route.allowOrigin !== 'undefined')
352 allowOrigin = route.allowOrigin;
353 if (typeof route.allowHeaders !== 'undefined')
354 allowHeaders = route.allowHeaders;
355 if (typeof route.allowCredentials !== 'undefined')
356 allowCredentials = route.allowCredentials;
357 if ((typeof route.allowMethods !== 'undefined') || (typeof route.allow !== 'undefined'))
358 allowMethods = route.allow || route.allowMethods;
359 }
360 //ensure header names
361 var headerNames = context.response["_headerNames"] || { };
362 //1. Access-Control-Allow-Origin
363 if (typeof headerNames["access-control-allow-origin"] === 'undefined') {
364 //if request contains origin header
365 if (context.request.headers.origin) {
366 if (allowOrigin === "*") {
367 //set access-control-allow-origin header equal to request origin header
368 context.response.setHeader("Access-Control-Allow-Origin", context.request.headers.origin);
369 }
370 else if (allowOrigin.indexOf(context.request.headers.origin)>-1) {
371 context.response.setHeader("Access-Control-Allow-Origin", context.request.headers.origin);
372 }
373 }
374 else {
375 //set access-control-allow-origin header equal to the predefined origin header
376 context.response.setHeader("Access-Control-Allow-Origin", "*");
377 }
378 }
379 //2. Access-Control-Allow-Origin
380 if (typeof headerNames["access-control-allow-credentials"] === 'undefined') {
381 context.response.setHeader("Access-Control-Allow-Credentials", allowCredentials);
382 }
383
384 //3. Access-Control-Allow-Headers
385 if (typeof headerNames["access-control-allow-headers"] === 'undefined') {
386 context.response.setHeader("Access-Control-Allow-Headers", allowHeaders);
387 }
388
389 //4. Access-Control-Allow-Methods
390 if (typeof headerNames["access-control-allow-methods"] === 'undefined') {
391 context.response.setHeader("Access-Control-Allow-Methods", allowMethods);
392 }
393 }
394 if (typeof callback === 'undefined') { return; }
395 return callback();
396 }
397 catch(e) {
398 if (typeof callback === 'undefined') { throw e; }
399 callback(e);
400 }
401
402};
403/**
404 * @param {HttpContext} context
405 * @param {Function} callback
406 */
407ViewHandler.prototype.processRequest = function (context, callback) {
408 var self = this;
409 callback = callback || function () { };
410 try {
411 if (context.is('OPTIONS')) {
412 //do nothing
413 return callback();
414 }
415 //validate request controller
416 var controller = self.controller;
417 if (controller) {
418 /**
419 * try to find action
420 * @type {String}
421 */
422 var action = context.request.routeData["action"];
423 if (action) {
424 //execute action
425 var fn, useHttpMethodNamingConvention = false;
426 if (controller.constructor['httpController']) {
427 fn = queryControllerAction(controller, action);
428 if (typeof fn === 'function') {
429 useHttpMethodNamingConvention = true;
430 }
431 }
432 else {
433 fn = controller[action];
434 if (typeof fn !== 'function') {
435 fn = controller[_.camelCase(action)];
436 }
437 }
438 if (typeof fn !== 'function') {
439 fn = controller.action;
440 }
441 //enumerate params
442 var functionParams = LangUtils.getFunctionParams(fn), params =[];
443 if (functionParams.length>0) {
444 if (!useHttpMethodNamingConvention) {
445 //remove last parameter (the traditional callback function)
446 functionParams.pop();
447 }
448 }
449 //execute action handler decorators
450 var actionConsumers = _.filter(_.keys(fn), function(x) {
451 return (fn[x] instanceof HttpConsumer);
452 });
453 return async.eachSeries(actionConsumers, function(actionConsumer, cb) {
454 try {
455 var source = fn[actionConsumer].run(context);
456 if (!_.isPromise(source)) {
457 return cb(new Error("Invalid type. Action consumer result must be a promise."));
458 }
459 return source.then(function() {
460 return cb();
461 }).catch(function(err) {
462 return cb(err);
463 });
464 }
465 catch(err) {
466 return cb(err);
467 }
468 }, function(err) {
469 if (err) {
470 return callback(err);
471 }
472 try {
473 if (functionParams.length>0) {
474 var k = 0;
475 while (k < functionParams.length) {
476 if (typeof context.getParam === 'function') {
477 params.push(context.getParam(functionParams[k]));
478 }
479 else {
480 params.push(context.params[functionParams[k]]);
481 }
482 k+=1;
483 }
484 }
485 if (useHttpMethodNamingConvention) {
486 var source = fn.apply(controller, params);
487 // continue processing
488 if (source instanceof HttpNextResult) {
489 // destroy handler
490 delete context.request.currentHandler;
491 // execute ViewHandler.mapRequest() again
492 return ViewHandler.prototype.mapRequest.bind(self)(context, function(err) {
493 if (err) {
494 return callback(err);
495 }
496 // if current handler is ViewHandler
497 if (context.request.currentHandler instanceof ViewHandler) {
498 // execute ViewHandler.processRequest()
499 return ViewHandler.prototype.processRequest.bind(self)(context, function(err) {
500 if (err) {
501 return callback(err);
502 }
503 return callback();
504 });
505 }
506 return callback(new HttpNotFoundError());
507 });
508 }
509 //if action result is an instance of HttpResult
510 else if (source instanceof HttpResult) {
511 //execute http result
512 return source.execute(context, callback);
513 }
514 var finalSource = _.isPromise(source) ? source : Q.resolve(source);
515 //if action result is a promise
516 return finalSource.then(function(result) {
517 if (result instanceof HttpResult) {
518 //execute http result
519 return result.execute(context, callback);
520 }
521 else {
522 //convert result (any result) to an instance HttpResult
523 if (typeof controller.result === 'function') {
524 var httpResult = controller.result(result);
525 //and finally execute result
526 return httpResult.execute(context, callback);
527 }
528 else {
529 return callback(new TypeError('Invalid controller prototype.'));
530 }
531 }
532 }).catch(function(err) {
533 return callback.bind(context)(err);
534 });
535
536 }
537 else {
538 params.push(function (err, result) {
539 if (err) {
540 //throw error
541 callback.call(context, err);
542 }
543 else {
544 //execute http result
545 return result.execute(context, callback);
546 }
547 });
548 //invoke controller method
549 return fn.apply(controller, params);
550 }
551 }
552 catch(err) {
553 return callback(err);
554 }
555 });
556 }
557 }
558 else {
559 return callback();
560 }
561
562 }
563 catch (error) {
564 callback(error);
565 }
566};
567
568/**
569 *
570 * @param {string|*} requestUri
571 * @param {HttpContext} context
572 * @param {number=} startIndex
573 * @returns {HttpRoute}
574 * @private
575 */
576function queryRoute(requestUri, context, startIndex) {
577 /**
578 * @type Array
579 * */
580 var routes = context.getApplication().getConfiguration().routes;
581 // create http route instance
582 var httpRoute = route.createInstance();
583 // validate start index
584 var index = typeof startIndex === 'number' && isFinite(startIndex) && startIndex>0 ? startIndex : -1;
585 // enumerate routes
586 var re = new RegExp('\\b' + context.request.method + '\\b|^\\*$', 'ig');
587 var allow = true;
588 for (var i = index + 1; i < routes.length; i++) {
589 httpRoute.route = routes[i];
590 // if uri path is matched
591 if (httpRoute.isMatch(requestUri.pathname)) {
592 // validate allow attribute
593 allow = routes[i].allow ? re.test(routes[i].allow) : true;
594 if (allow) {
595 // set route index
596 httpRoute.routeIndex = i;
597 // and finally return current route
598 return httpRoute;
599 }
600 }
601 }
602}
603/**
604 * @function
605 * @private
606 * @param {HttpController|*} controller
607 * @param {string} action
608 * @returns {boolean}
609 */
610function isValidControllerAction(controller, action) {
611 var httpMethodDecorator = _.camelCase('http-' + controller.context.request.method);
612 if (typeof controller[action] === 'function') {
613 //get httpAction decorator
614 if ((typeof controller[action].httpAction === 'undefined') ||
615 (controller[action].httpAction===action)) {
616 //and supports current request method (see http decorators)
617 if (controller[action][httpMethodDecorator]) {
618 //return this action
619 return true;
620 }
621 }
622 }
623 return false;
624}
625
626function getControllerPropertyNames_(obj) {
627 if (typeof obj === 'undefined' || obj === null) {
628 return [];
629 }
630 var ownPropertyNames = [];
631 //get object methods
632 var proto = obj;
633 while(proto) {
634 ownPropertyNames = ownPropertyNames.concat(Object.getOwnPropertyNames(proto).filter( function(x) {
635 return ownPropertyNames.indexOf(x)<0;
636 }));
637 proto = Object.getPrototypeOf(proto);
638 }
639 return ownPropertyNames;
640}
641
642/**
643 * @function
644 * @private
645 * @param {HttpController|*} controller
646 * @param {string} action
647 * @returns {Function}
648 */
649function queryControllerAction(controller, action) {
650 var httpMethodDecorator = _.camelCase('http-' + controller.context.request.method),
651 method = _.camelCase(action);
652 var controllerPrototype = Object.getPrototypeOf(controller);
653 var controllerPropertyNames = getControllerPropertyNames_(controllerPrototype);
654 if (controllerPrototype) {
655 //query controller methods that support current http request
656 var protoActionMethods = _.filter(controllerPropertyNames, function(x) {
657 return (typeof controller[x] === 'function')
658 && (controller[x].httpAction === action)
659 && controller[x][httpMethodDecorator];
660 });
661 //if an action was found for the given criteria
662 if (protoActionMethods.length===1) {
663 return controller[protoActionMethods[0]];
664 }
665 }
666 //if an action with the given name is a method of current controller
667 if (isValidControllerAction(controller, action)) {
668 return controller[action];
669 }
670 //if a camel cased action with the given name is a method of current controller
671 if (isValidControllerAction(controller, method)) {
672 return controller[method];
673 }
674}
675
676/**
677 * Gets the controller of the given url
678 * @param {string|*} requestUri - A string that represents the url we want to parse.
679 * @private
680 * */
681function queryController(requestUri) {
682 try {
683 if (requestUri === undefined)
684 return null;
685 //split path
686 var segments = requestUri.pathname.split('/');
687 //put an exception for root controller
688 //maybe this is unnecessary exception but we need to search for root controller e.g. /index.html, /about.html
689 if (segments.length === 2)
690 return 'root';
691 else
692 //e.g /pages/about where segments are ['','pages','about']
693 //and the controller of course is always the second segment.
694 return segments[1];
695 }
696 catch (err) {
697 throw err;
698 }
699}
700
701if (typeof exports !== 'undefined') {
702 module.exports.ViewHandler = ViewHandler;
703 module.exports.createInstance = function() {
704 return new ViewHandler();
705 };
706}
707