<commit_before>"use strict";

require('es6-collections');

var yaml = require('js-yaml');
// TODO: promisify & use async loading
var fs = require('fs');
var util = require('util');

var swaggerRouter = require('swagger-router');
var Node = swaggerRouter.Node;
var URI = swaggerRouter.URI;
var SwaggerRouter = swaggerRouter.Router;

function Router (options) {
    this._options = options;
    this._nodes = new Map();
    this._modules = new Map();
    this.router = new SwaggerRouter();
}

Router.prototype._readSpec = function(path) {
    // XXX: make the spec path configurable?
    var fsPath = 'interfaces/' + path + '.yaml';
    // Let errors bubble up for now. Fail loud & early.
    return yaml.safeLoad(fs.readFileSync(fsPath));
};

// Extend an existing route tree with a new path by walking the existing tree
// and inserting new subtrees at the desired location.
Router.prototype._buildPath = function route(node, path) {
    var params = {};
    for (var i = 0; i < path.length; i++) {
        var nextNode = node.getChild(path[i], params);
        if (!nextNode) {
            nextNode = new Node();
            node.setChild(path[i], nextNode);
            node = nextNode;
        } else {
            node = nextNode;
        }
    }
    return node;
};

// handle one spec path
Router.prototype._handleRESTBasePathSpec = function(node, subspec, symbols) {
    var self = this;
    var xrb = subspec['x-restbase'];
    if (xrb) {
        symbols = symbols || {};
        // modules
        if (Array.isArray(xrb.modules)) {
            // load each module
            xrb.modules.forEach(function(m) {
                // Share modules
                var mObj = self._modules.get(m);
                if (!mObj) {
                    //mObj = require(/* somepath + */ m.name)(m.options);
                    mObj = {};
                    self._modules.set(m, mObj);
                }

                for (var symbol in mObj) {
                    // check for duplicate symbols
                    if (symbols[symbol]) {
                        throw new Error("Duplicate symbol " + symbol
                                + " in module " + m.name);
                    } else {
                        symbols[symbol] = mObj[symbol];
                    }
                }
            });
        }

        // interfaces
        if (Array.isArray(xrb.interfaces)) {
            xrb.interfaces.forEach(function(subSpecOrPath) {
                var subSpec;
                if (subSpecOrPath instanceof Object) {
                    // Inline sub-spec: return directly
                    subSpec = subSpecOrPath;
                } else {
                    subSpec = self._readSpec(subSpecOrPath);
                }
                return self._handleRESTBaseSpec(node, subSpec, symbols);
            });
        }
    }

    for (var methodName in subspec) {
        if (methodName === 'x-restbase') {
            continue;
        }
        // Other methods
        var method = subspec[methodName];
        var mxrb = method['x-restbase'];
        if (mxrb) {
            // check for 'handler' in symbols
            if (mxrb.handler) {
                var handler = symbols[mxrb.handler];
                if (handler) {
                    node.value.methods[methodName] = handler;
                }
            } else if (mxrb.service) {
                // set up a handler
                // XXX: share?
                node.value.methods[methodName] = function (restbase, req) {
                    // XXX: expand the request with req information!
                    restbase.request(mxrb.service);
                };
            }
        }
    }
};

Router.prototype._handleRESTBaseSpec = function (rootNode, spec, modules) {
    var self = this;
    function handlePaths (paths) {
        // handle paths
        for (var pathPattern in paths) {
            var pathSpec = paths[pathPattern];
            var path = new URI(pathPattern).segments;
            // Expected to return
            // - rootNode for single-element path
            // - a subnode for longer paths
            var branchNode = self._buildPath(rootNode, path.slice(0, path.length - 1));
            // Check if we can share the path spec
            var subtree = self._nodes.get(pathSpec);
            if (!subtree) {
                console.log('not shared:', pathPattern);
                // Build a new tree
                subtree = new Node();
                // Set up our specific value object
                subtree.value = {
                    methods: {}
                };
                // Assign the node before building the tree, so that sharing
                // opportunities with the same spec are discovered while doing so
                self._nodes.set(pathSpec, subtree);
                self._handleRESTBasePathSpec(subtree, pathSpec, modules);
            } else {
                console.log('shared:', pathPattern);
                subtree = subtree.clone();
            }

            // XXX: Set ACLs and other value properties for path
            // subtree.value.acls = ...;

            // XXX: check for conflicts!
            branchNode.setChild(path[path.length - 1], subtree);
        }
    }

    // TODO: handle global spec settings

    // Handle internal paths
    if (spec['x-restbase-paths']) {
        // TODO:
        // - bail out if prefix is not '/{domain}/sys/'
        // - set up path-based ACLs to disallow direct access
        handlePaths(spec['x-restbase-paths']);
    }

    if (spec.paths) {
        handlePaths(spec.paths);
    }
};

Router.prototype.loadSpec = function(spec) {
    var rootNode = new Node();
    if (spec && spec.constructor === String) {
        spec = this._readSpec(spec);
    }
    this._handleRESTBaseSpec(rootNode, spec, {});
    this.tree = rootNode;
    this.router.setTree(rootNode);
};

Router.prototype.route = function (uri) {
    return this.router.lookup(uri);
}

module.exports = Router;
<commit_msg>Disable tree sharing debug prints<commit_after>"use strict";

require('es6-collections');

var yaml = require('js-yaml');
// TODO: promisify & use async loading
var fs = require('fs');
var util = require('util');

var swaggerRouter = require('swagger-router');
var Node = swaggerRouter.Node;
var URI = swaggerRouter.URI;
var SwaggerRouter = swaggerRouter.Router;

function Router (options) {
    this._options = options;
    this._nodes = new Map();
    this._modules = new Map();
    this.router = new SwaggerRouter();
}

Router.prototype._readSpec = function(path) {
    // XXX: make the spec path configurable?
    var fsPath = 'interfaces/' + path + '.yaml';
    // Let errors bubble up for now. Fail loud & early.
    return yaml.safeLoad(fs.readFileSync(fsPath));
};

// Extend an existing route tree with a new path by walking the existing tree
// and inserting new subtrees at the desired location.
Router.prototype._buildPath = function route(node, path) {
    var params = {};
    for (var i = 0; i < path.length; i++) {
        var nextNode = node.getChild(path[i], params);
        if (!nextNode) {
            nextNode = new Node();
            node.setChild(path[i], nextNode);
            node = nextNode;
        } else {
            node = nextNode;
        }
    }
    return node;
};

// handle one spec path
Router.prototype._handleRESTBasePathSpec = function(node, subspec, symbols) {
    var self = this;
    var xrb = subspec['x-restbase'];
    if (xrb) {
        symbols = symbols || {};
        // modules
        if (Array.isArray(xrb.modules)) {
            // load each module
            xrb.modules.forEach(function(m) {
                // Share modules
                var mObj = self._modules.get(m);
                if (!mObj) {
                    //mObj = require(/* somepath + */ m.name)(m.options);
                    mObj = {};
                    self._modules.set(m, mObj);
                }

                for (var symbol in mObj) {
                    // check for duplicate symbols
                    if (symbols[symbol]) {
                        throw new Error("Duplicate symbol " + symbol
                                + " in module " + m.name);
                    } else {
                        symbols[symbol] = mObj[symbol];
                    }
                }
            });
        }

        // interfaces
        if (Array.isArray(xrb.interfaces)) {
            xrb.interfaces.forEach(function(subSpecOrPath) {
                var subSpec;
                if (subSpecOrPath instanceof Object) {
                    // Inline sub-spec: return directly
                    subSpec = subSpecOrPath;
                } else {
                    subSpec = self._readSpec(subSpecOrPath);
                }
                return self._handleRESTBaseSpec(node, subSpec, symbols);
            });
        }
    }

    for (var methodName in subspec) {
        if (methodName === 'x-restbase') {
            continue;
        }
        // Other methods
        var method = subspec[methodName];
        var mxrb = method['x-restbase'];
        if (mxrb) {
            // check for 'handler' in symbols
            if (mxrb.handler) {
                var handler = symbols[mxrb.handler];
                if (handler) {
                    node.value.methods[methodName] = handler;
                }
            } else if (mxrb.service) {
                // set up a handler
                // XXX: share?
                node.value.methods[methodName] = function (restbase, req) {
                    // XXX: expand the request with req information!
                    restbase.request(mxrb.service);
                };
            }
        }
    }
};

Router.prototype._handleRESTBaseSpec = function (rootNode, spec, modules) {
    var self = this;
    function handlePaths (paths) {
        // handle paths
        for (var pathPattern in paths) {
            var pathSpec = paths[pathPattern];
            var path = new URI(pathPattern).segments;
            // Expected to return
            // - rootNode for single-element path
            // - a subnode for longer paths
            var branchNode = self._buildPath(rootNode, path.slice(0, path.length - 1));
            // Check if we can share the path spec
            var subtree = self._nodes.get(pathSpec);
            if (!subtree) {
                //console.log('not shared:', pathPattern);
                // Build a new tree
                subtree = new Node();
                // Set up our specific value object
                subtree.value = {
                    methods: {}
                };
                // Assign the node before building the tree, so that sharing
                // opportunities with the same spec are discovered while doing so
                self._nodes.set(pathSpec, subtree);
                self._handleRESTBasePathSpec(subtree, pathSpec, modules);
            } else {
                //console.log('shared:', pathPattern);
                subtree = subtree.clone();
            }

            // XXX: Set ACLs and other value properties for path
            // subtree.value.acls = ...;

            // XXX: check for conflicts!
            branchNode.setChild(path[path.length - 1], subtree);
        }
    }

    // TODO: handle global spec settings

    // Handle internal paths
    if (spec['x-restbase-paths']) {
        // TODO:
        // - bail out if prefix is not '/{domain}/sys/'
        // - set up path-based ACLs to disallow direct access
        handlePaths(spec['x-restbase-paths']);
    }

    if (spec.paths) {
        handlePaths(spec.paths);
    }
};

Router.prototype.loadSpec = function(spec) {
    var rootNode = new Node();
    if (spec && spec.constructor === String) {
        spec = this._readSpec(spec);
    }
    this._handleRESTBaseSpec(rootNode, spec, {});
    this.tree = rootNode;
    this.router.setTree(rootNode);
};

Router.prototype.route = function (uri) {
    return this.router.lookup(uri);
}

module.exports = Router;
<|endoftext|>


<commit_before>// router.js
// --------------------
//
// Determine a controller and action given a URL.
// Below, we start with the explicit routing table, then
// use a handful of different strategies to come up with
// sensible assumptions about where the URL should route by default.

/**
 * Functionality to apply Sails.js enhancements to an Express request
 */
var enhanceRequest = require('./request');

/**
 * Listen for network requests using bindRouteFn
 */
function listen(bindRouteFn) {
	startHandlingMappedRequests(bindRouteFn);
	startHandlingWildcardRequests(bindRouteFn);
}

/**
 * Listen for wildcard requests
 * @bindRouteFn - the function which will be run to bind the URL path to the appropriate function
 */
function startHandlingWildcardRequests(bindRouteFn) {

	// Bind the target function to a URL path expression and all HTTP verbs
	// NOTE: routing based on verb can't be precomputed and so it must occur at runtime when the request is processed
	bindRouteFn('/:entity/:action?/:id?', handleRequest(), null);

	// TODO: Use a more complex, but welcoming regex
	// bindRouteFn(/\/(\S+)(\/\S+)?(\/(\S+))?(\/(\S+))*/, targetFn, null);
}

/**
 * Listen for requests listed in the routing table
 * @bindRouteFn - the function which will be run to bind the URL path to the appropriate function
 */
function startHandlingMappedRequests(bindRouteFn) {

	// Map standard AJAX and REST routes
	_.each(sails.routes, function(target, urlExpr) {

		// If a specific verb was specified in the mapping, use it
		// (otherwise this handler will listen for *any* of the HTTP verbs)
		var verbExpr = /^(get|post|put|delete|trace|options|connect|patch|head)\s+/;
		verb = _.last(urlExpr.match(verbExpr) || []) || null;

		// If a verb was specified, eliminate the verb from the urlExpr
		if (verb) urlExpr = urlExpr.replace(verbExpr,"");

		// Get a urlExpr object from the url mapping and extend with the verb
		var actualTarget = fetchRoute(urlExpr,verb);
		actualTarget = _.extend(actualTarget,{verb: verb});

		// Bind the target function to a URL path expression and HTTP verb
		// This is where verb enforcement occurs
		bindRouteFn(urlExpr, handleRequest(actualTarget), actualTarget && actualTarget.verb);
	});
}

/**
 * Given a clean URL (@url) and a verb, combine them,
 * then scan the route mappings and return an entity/action object.
 * If no match exists, return null.
 */
function fetchRoute(url, verb) {

	// Build target set (start with url)
	var target = url;

	// A string target means this is a redirect
	// If no match can be found, leave target as a string
	// Then it will be resourcefully routed if possible at runtime with a wildcard handler
	var hopcount = 0;
	var targetExists = true;
	while(_.isString(target) && targetExists ) {

		// Try both the simple target and the fully qualified target (verb + URL)
		var fullyQualifiedTarget = verb && (verb.toLowerCase() + " " + target);
		var newTarget = sails.routes[fullyQualifiedTarget] || sails.routes[target];
		if (!newTarget) {
			// If no target found, keep it as a string and break from the redirect hunt
			break;
		}
		else target = newTarget;
		targetExists = !! target;

		// In case the user created a redirect loop, hopcount halts execution
		if(hopcount++ > 10) {
			throw new Error("Over 10 redirects detected!  You probably have a redirect loop.  Please check your url mappings configuration file(s).");
		}

	}

	// An object target means this maps directly to a controller and action
	if(_.isObject(target)) {

		// Return a complete route object for this target URL
		return _.extend(target, { verb: verb });
	}
	// Target is a string which maps to a controller/action which may not exist yet
	else if(_.isString(target)) {

		var routeObj;

		// If route has a '/' parse as path else parse as controller.route
		if(target.match('/')) {
			// Parse the entity/action for this url
			routeObj = sails.util.parsePath(target);

			// Add controller key to route object to map to routes.js conventions
			routeObj.controller = routeObj.entity;

			// Return a complete route object for this target URL
			return _.extend(routeObj, {verb: verb});
		} else {
			//Parse the target into controller/route when controller.route syntax is used
			routeObj = sails.util.parseStringRoute(target);

			//Return the route object
			return _.extend(routeObj, { verb: verb });
		}
	}
	// No match exists, return null
	else if (!target) {
		return null;
	} else {
		sails.log.error('Invalid target route specified: '+ target+ " from: "+ url);
		throw new Error('Invalid target route specified.');
	}
}

/**
 * Handle a request
 */
function handleRequest(target) {
	/**
	 * Handler fn
	 *
	 * @req - the Connect request object
	 * @res - the Connect response object
	 * @next - the Connect next() function
	 */
	return function(req, res, next) {

		// If defined, pull entity and action from target into request object
		if (target && target.controller && !req.param('entity')) {
			req.params.entity = target.controller;
		}
		else if (target && target.action && !req.param('action')) {
			req.params.action = target.action;
		}

		// If the action doesn't exist, use resourceful routing and Backbone semantics
		// to try and transparently route to the appropriate action.
		// (i.e. GET '/experiment' maps to ExperimentController.read())
		var myTarget = {
			controller: (target && target.controller) || req.param('entity'),
			action: (target && target.action) || guessAction(req)
		};

		// NOTE: We can safely run our guessed action here.
		// (If this route matches something in the routing table, it never would have made it this far)

		// Enhance Express request and response objects with Sails methods
		enhanceRequest(req, res, myTarget.controller, myTarget.action);

		// Run handler
		handler(req, res, next);

	};
}

/**
 * Given a request object with a potentially non-existent entity(controller), action, and HTTP verb,
 * return a best guess at the desired action (using resourceful routing + Backbone semantics)
 */
function guessAction(req) {

	var requestedEntity = req.param('entity'),
		requestedAction = req.param('action'),
		httpVerb = req.method;

	// Grab controller object
	var controller = sails.controllers[requestedEntity.toLowerCase()];

	// If requestedAction was not specified
	if(!requestedAction) {
		// For GET requests
		if(httpVerb == 'GET') {
			// default to index()
			return 'index';
		}
		// for POST requests
		else if(httpVerb == 'POST') {
			// use create()
			return 'create';
		}
		// for DELETE requests (Delete all!)
		else if(httpVerb == 'DELETE') {
			return 'destroy';
		}
	}

	// If requestedAction is unknown in controller
	// AND not a CRUD action (TODO: remove this part)
	// then treat requestedAction as id
	else if( !controller[requestedAction] && !isCrudAction(requestedAction)) {

		// Update request parameters accordingly
		req.params.id = requestedAction;

		// For GET requests
		if(httpVerb == 'GET') {
			return 'find';
		}
		// for PUT requests
		else if(httpVerb == 'PUT') {
			return 'update';
		}
		// for PUT requests
		else if(httpVerb == 'DELETE') {
			return 'destroy';
		}
	}
	// otherwise the action was specified
	// so just return it transparently
	else {
		return requestedAction;
	}
}

/**
 * The handler function, given an http request object, with the controller and action already decided
 */
function handler(req, res, next) {

	// Parse initial route data from request
	var requestedEntity = req.param('entity'),
		requestedAction = req.param('action'),
		httpVerb = req.method;

	sails.log.verbose("");
	sails.log.verbose("--- " + httpVerb + " "+req.url +" ---");
	sails.log.verbose("Entity: "+requestedEntity);
	sails.log.verbose("Action: "+requestedAction);
	sails.log.verbose("");

	// Defaults for controller, actionName, and handler function
	// (if actionName is not specified, use 'index')
	var controller = requestedEntity && sails.controllers[requestedEntity.toLowerCase()],
		actionName = requestedAction || 'index',
		fn = controller && controller[actionName];

	// Run policy
	// (For instance, might check if the requesting user has a valid session and appropriate permissions
	// to access this controller+actionName with the specified request parameters and session)
	require('./policies')(requestedEntity, actionName, req, res, function afterwards() {

		// After policy middleware is run, the main controller function can be run

		// If no index action exists for this controller, and no action is specified
		if (actionName === 'index' && !fn) {

			// Use find/findAll instead
			actionName = req.param('id') ? 'find' : 'findAll';
			fn = controller && controller[actionName];

			// (but dont override action param so it stays correct for view rendering)
			// req.params.action = actionName;

			// But still try to use 'index' view
			checkIfViewExists(requestedEntity, 'index', function (err, exists) {
				if (err) return res.send(err, 500);

				// If an index view exists, use it
				if (exists) {
					afterThat(err,exists, requestedEntity+'/index');
				}

				// otherwise try for a find/findAll view
				else checkIfViewExists(requestedEntity, actionName, afterThat);
			});
		}

		// Otherwise, just check once if the view exists
		else checkIfViewExists(requestedEntity, actionName, afterThat);

		// Determine if the view to render exists
		// viewPath is the path to the view (usually views/:entity/:action or views/:entity/index)
		function afterThat(err, exists, viewPath) {
			if (err) return res.send(err, 500);

			// then add res.viewExists flag to response object
			res.viewExists = !!exists;

			// Override next() to trigger the blueprint, but only if it exists
			if (qualifiedForBlueprint(requestedEntity, actionName)) {
				var nextFn = require('./blueprints/controller').definition(requestedEntity)[actionName];
				next = function (nextReq,nextRes, next ){
					if (! (nextReq && nextRes) ) nextFn(req,res,next);
					else nextFn(nextReq, nextRes, next);
				};
			}

			// if the handler fn is valid, run it!
			if(fn) return runHandler(req, res, next, fn);

			// If it's not valid
			else {

				// If a model exists, and the action is in the blueprint
				if(qualifiedForBlueprint(requestedEntity, actionName)) {

					// Use the JSON blueprint controller to return the appropriate CRUD action handler.
					var blueprintController = require('./blueprints/controller').definition(requestedEntity);
					var blueprintAction = blueprintController[actionName];
					return runHandler(req, res, next, blueprintAction);

				}

				// Otherwise if the proper view exists, go ahead and render it
				else if (res.viewExists) return runHandler(req, res, next, function (req,res,next) {

					// Render the default view
					res.view(viewPath);
				});

				// Respond with notFound/404 behavior
				else return runHandler(req, res, next);
			}
		}
	});
}

// Respond whether a view exists for the specified entity and action
function checkIfViewExists (entity, action, cb) {
	var pattern = sails.express.app.settings.views + "/" + entity + "/" + action + ".*";

	// If the proper view exists, go ahead and render it
	require("glob")(pattern, {}, function (err,matches) {
		if (err) return cb(err);

		// View exists
		else if (matches && matches.length) return cb(null, matches, entity+'/'+action);

		// View does not exist
		else return cb();
	});
}

/**
 * Run the route handler fn
 */
function runHandler(req, res, next, fn) {
	var notFoundBlueprint = require('./blueprints/notFound');

	// If no valid route handler can be found or blueprinted, redirect to the default 404 page
	if(!fn || !_.isFunction(fn)) {
		return notFoundBlueprint(req,res);
	}

	// Finally, run handler function
	fn(req, res, next);
}

// Map of various synonyms to the Sails standard CRUD method names
var crudSynonyms = {

	destroy: "destroy",

	update: "update",

	find: "find",

	findAll: "findAll",

	create: "create"

};

/**
 * Return whether the specified actionName is a CRUD operation, or falsy
 */
function isCrudAction(actionName) {
	return(_.contains(_.keys(crudSynonyms), actionName) || !actionName);
}

/**
 * Return whether the entity corresponds to an existing model
 */

function entityMatchesExistingModel(entity) {
	return typeof sails.models[entity] !== "undefined";
}

/**
 * Return whether this entity/action tuple qualifies to use blueprint methods
 */

function qualifiedForBlueprint(entity, action) {
	return isCrudAction(action) && entityMatchesExistingModel(entity);
}

// Export logic from module
module.exports = {
	listen: listen,
	enhance: enhanceRequest,
	fetchRoute: fetchRoute,
	handleRequest: handleRequest
};<commit_msg>Fix for index routing in most basic case.<commit_after>// router.js
// --------------------
//
// Determine a controller and action given a URL.
// Below, we start with the explicit routing table, then
// use a handful of different strategies to come up with
// sensible assumptions about where the URL should route by default.

/**
 * Functionality to apply Sails.js enhancements to an Express request
 */
var enhanceRequest = require('./request');

/**
 * Listen for network requests using bindRouteFn
 */
function listen(bindRouteFn) {
	startHandlingMappedRequests(bindRouteFn);
	startHandlingWildcardRequests(bindRouteFn);
}

/**
 * Listen for wildcard requests
 * @bindRouteFn - the function which will be run to bind the URL path to the appropriate function
 */
function startHandlingWildcardRequests(bindRouteFn) {

	// Bind the target function to a URL path expression and all HTTP verbs
	// NOTE: routing based on verb can't be precomputed and so it must occur at runtime when the request is processed
	bindRouteFn('/:entity/:action?/:id?', handleRequest(), null);

	// TODO: Use a more complex, but welcoming regex
	// bindRouteFn(/\/(\S+)(\/\S+)?(\/(\S+))?(\/(\S+))*/, targetFn, null);
}

/**
 * Listen for requests listed in the routing table
 * @bindRouteFn - the function which will be run to bind the URL path to the appropriate function
 */
function startHandlingMappedRequests(bindRouteFn) {

	// Map standard AJAX and REST routes
	_.each(sails.routes, function(target, urlExpr) {

		// If a specific verb was specified in the mapping, use it
		// (otherwise this handler will listen for *any* of the HTTP verbs)
		var verbExpr = /^(get|post|put|delete|trace|options|connect|patch|head)\s+/;
		verb = _.last(urlExpr.match(verbExpr) || []) || null;

		// If a verb was specified, eliminate the verb from the urlExpr
		if (verb) urlExpr = urlExpr.replace(verbExpr,"");

		// Get a urlExpr object from the url mapping and extend with the verb
		var actualTarget = fetchRoute(urlExpr,verb);
		actualTarget = _.extend(actualTarget,{verb: verb});

		// Bind the target function to a URL path expression and HTTP verb
		// This is where verb enforcement occurs
		bindRouteFn(urlExpr, handleRequest(actualTarget), actualTarget && actualTarget.verb);
	});
}

/**
 * Given a clean URL (@url) and a verb, combine them,
 * then scan the route mappings and return an entity/action object.
 * If no match exists, return null.
 */
function fetchRoute(url, verb) {

	// Build target set (start with url)
	var target = url;

	// A string target means this is a redirect
	// If no match can be found, leave target as a string
	// Then it will be resourcefully routed if possible at runtime with a wildcard handler
	var hopcount = 0;
	var targetExists = true;
	while(_.isString(target) && targetExists ) {

		// Try both the simple target and the fully qualified target (verb + URL)
		var fullyQualifiedTarget = verb && (verb.toLowerCase() + " " + target);
		var newTarget = sails.routes[fullyQualifiedTarget] || sails.routes[target];
		if (!newTarget) {
			// If no target found, keep it as a string and break from the redirect hunt
			break;
		}
		else target = newTarget;
		targetExists = !! target;

		// In case the user created a redirect loop, hopcount halts execution
		if(hopcount++ > 10) {
			throw new Error("Over 10 redirects detected!  You probably have a redirect loop.  Please check your url mappings configuration file(s).");
		}

	}

	// An object target means this maps directly to a controller and action
	if(_.isObject(target)) {

		// Return a complete route object for this target URL
		return _.extend(target, { verb: verb });
	}
	// Target is a string which maps to a controller/action which may not exist yet
	else if(_.isString(target)) {

		var routeObj;

		// If route has a '/' parse as path else parse as controller.route
		if(target.match('/')) {
			// Parse the entity/action for this url
			routeObj = sails.util.parsePath(target);

			// Add controller key to route object to map to routes.js conventions
			routeObj.controller = routeObj.entity;

			// Return a complete route object for this target URL
			return _.extend(routeObj, {verb: verb});
		} else {
			//Parse the target into controller/route when controller.route syntax is used
			routeObj = sails.util.parseStringRoute(target);

			//Return the route object
			return _.extend(routeObj, { verb: verb });
		}
	}
	// No match exists, return null
	else if (!target) {
		return null;
	} else {
		sails.log.error('Invalid target route specified: '+ target+ " from: "+ url);
		throw new Error('Invalid target route specified.');
	}
}

/**
 * Handle a request
 */
function handleRequest(target) {
	/**
	 * Handler fn
	 *
	 * @req - the Connect request object
	 * @res - the Connect response object
	 * @next - the Connect next() function
	 */
	return function(req, res, next) {

		// If defined, pull entity and action from target into request object
		if (target && target.controller && !req.param('entity')) {
			req.params.entity = target.controller;
		}
		else if (target && target.action && !req.param('action')) {
			req.params.action = target.action;
		}

		// If the action doesn't exist, use resourceful routing and Backbone semantics
		// to try and transparently route to the appropriate action.
		// (i.e. GET '/experiment' maps to ExperimentController.read())
		var myTarget = {
			controller: (target && target.controller) || req.param('entity'),
			action: (target && target.action) || guessAction(req)
		};

		// NOTE: We can safely run our guessed action here.
		// (If this route matches something in the routing table, it never would have made it this far)

		// Enhance Express request and response objects with Sails methods
		enhanceRequest(req, res, myTarget.controller, myTarget.action);

		// Run handler
		handler(req, res, next);

	};
}

/**
 * Given a request object with a potentially non-existent entity(controller), action, and HTTP verb,
 * return a best guess at the desired action (using resourceful routing + Backbone semantics)
 */
function guessAction(req) {

	var requestedEntity = req.param('entity'),
		requestedAction = req.param('action'),
		httpVerb = req.method;

	// Grab controller object
	var controller = sails.controllers[requestedEntity.toLowerCase()];

	// If requestedAction was not specified
	if(!requestedAction) {
		// For GET requests
		if(httpVerb == 'GET') {
			// default to index()
			return 'index';
		}
		// for POST requests
		else if(httpVerb == 'POST') {
			// use create()
			return 'create';
		}
		// for DELETE requests (Delete all!)
		else if(httpVerb == 'DELETE') {
			return 'destroy';
		}
	}

	// If requestedAction is unknown in controller
	// AND not a CRUD action (TODO: remove this part)
	// then treat requestedAction as id
	else if( !controller[requestedAction] && !isCrudAction(requestedAction) && requestedAction !== 'index') {

		// Update request parameters accordingly
		req.params.id = requestedAction;

		// For GET requests
		if(httpVerb == 'GET') {
			return 'find';
		}
		// for PUT requests
		else if(httpVerb == 'PUT') {
			return 'update';
		}
		// for PUT requests
		else if(httpVerb == 'DELETE') {
			return 'destroy';
		}
	}
	// otherwise the action was specified
	// so just return it transparently
	else {
		return requestedAction;
	}
}

/**
 * The handler function, given an http request object, with the controller and action already decided
 */
function handler(req, res, next) {

	// Parse initial route data from request
	var requestedEntity = req.param('entity'),
		requestedAction = req.param('action'),
		httpVerb = req.method;

	sails.log.verbose("");
	sails.log.verbose("--- " + httpVerb + " "+req.url +" ---");
	sails.log.verbose("Entity: "+requestedEntity);
	sails.log.verbose("Action: "+requestedAction);
	sails.log.verbose("");

	// Defaults for controller, actionName, and handler function
	// (if actionName is not specified, use 'index')
	var controller = requestedEntity && sails.controllers[requestedEntity.toLowerCase()],
		actionName = requestedAction || 'index',
		fn = controller && controller[actionName];

	// Run policy
	// (For instance, might check if the requesting user has a valid session and appropriate permissions
	// to access this controller+actionName with the specified request parameters and session)
	require('./policies')(requestedEntity, actionName, req, res, function afterwards() {

		// After policy middleware is run, the main controller function can be run

		// If no index action exists for this controller, and no action is specified
		if (actionName === 'index' && !fn) {

			// Use find/findAll instead
			actionName = req.param('id') ? 'find' : 'findAll';
			fn = controller && controller[actionName];

			// (but dont override action param so it stays correct for view rendering)
			// req.params.action = actionName;

			// But still try to use 'index' view
			checkIfViewExists(requestedEntity, 'index', function (err, exists) {
				if (err) return res.send(err, 500);

				// If an index view exists, use it
				if (exists) {
					afterThat(err,exists, requestedEntity+'/index');
				}

				// otherwise try for a find/findAll view
				else checkIfViewExists(requestedEntity, actionName, afterThat);
			});
		}

		// Otherwise, just check once if the view exists
		else checkIfViewExists(requestedEntity, actionName, afterThat);

		// Determine if the view to render exists
		// viewPath is the path to the view (usually views/:entity/:action or views/:entity/index)
		function afterThat(err, exists, viewPath) {
			if (err) return res.send(err, 500);

			// then add res.viewExists flag to response object
			res.viewExists = !!exists;

			// Override next() to trigger the blueprint, but only if it exists
			if (qualifiedForBlueprint(requestedEntity, actionName)) {
				var nextFn = require('./blueprints/controller').definition(requestedEntity)[actionName];
				next = function (nextReq,nextRes, next ){
					if (! (nextReq && nextRes) ) nextFn(req,res,next);
					else nextFn(nextReq, nextRes, next);
				};
			}

			// if the handler fn is valid, run it!
			if(fn) return runHandler(req, res, next, fn);

			// If it's not valid
			else {

				// If a model exists, and the action is in the blueprint
				if(qualifiedForBlueprint(requestedEntity, actionName)) {

					// Use the JSON blueprint controller to return the appropriate CRUD action handler.
					var blueprintController = require('./blueprints/controller').definition(requestedEntity);
					var blueprintAction = blueprintController[actionName];
					return runHandler(req, res, next, blueprintAction);

				}

				// Otherwise if the proper view exists, go ahead and render it
				else if (res.viewExists) return runHandler(req, res, next, function (req,res,next) {

					// Render the default view
					res.view(viewPath);
				});

				// Respond with notFound/404 behavior
				else return runHandler(req, res, next);
			}
		}
	});
}

// Respond whether a view exists for the specified entity and action
function checkIfViewExists (entity, action, cb) {
	var pattern = sails.express.app.settings.views + "/" + entity + "/" + action + ".*";

	// If the proper view exists, go ahead and render it
	require("glob")(pattern, {}, function (err,matches) {
		if (err) return cb(err);

		// View exists
		else if (matches && matches.length) return cb(null, matches, entity+'/'+action);

		// View does not exist
		else return cb();
	});
}

/**
 * Run the route handler fn
 */
function runHandler(req, res, next, fn) {
	var notFoundBlueprint = require('./blueprints/notFound');

	// If no valid route handler can be found or blueprinted, redirect to the default 404 page
	if(!fn || !_.isFunction(fn)) {
		return notFoundBlueprint(req,res);
	}

	// Finally, run handler function
	fn(req, res, next);
}

// Map of various synonyms to the Sails standard CRUD method names
var crudSynonyms = {

	destroy: "destroy",

	update: "update",

	find: "find",

	findAll: "findAll",

	create: "create"

};

/**
 * Return whether the specified actionName is a CRUD operation, or falsy
 */
function isCrudAction(actionName) {
	return(_.contains(_.keys(crudSynonyms), actionName) || !actionName);
}

/**
 * Return whether the entity corresponds to an existing model
 */

function entityMatchesExistingModel(entity) {
	return typeof sails.models[entity] !== "undefined";
}

/**
 * Return whether this entity/action tuple qualifies to use blueprint methods
 */

function qualifiedForBlueprint(entity, action) {
	return isCrudAction(action) && entityMatchesExistingModel(entity);
}

// Export logic from module
module.exports = {
	listen: listen,
	enhance: enhanceRequest,
	fetchRoute: fetchRoute,
	handleRequest: handleRequest
};<|endoftext|>


<commit_before>'use strict';

require('babel-polyfill')

const net = require('net'),
      Command = require('./command'),
      constants = require('./constants'),
      magic = require('./magic'),
      assert = require('assert'),
      Contact = require('./contact'),
      _ = require('lodash'),
      prettyjson = require('prettyjson'),
      utils = require('./utils'),
      Promise = require('bluebird')

Promise.config({ longStackTrace: true })

module.exports = node => {
  return new Promise((resolve, reject) => {
    /**** PRIVATE FIELDS ****/
    const pendingRequests = []
    const buckets = []
    const queryCache = []
    const logger = node.logger
    const messageDelimiter = ';'
    /**** END PRIVATE FIELDS ****/

    /**** METHODS ****/
    const sendCommand = command => {
      const p = new Promise((resolve, reject) => {
        logger.debug(`attempting to connect to ${command.destination.ip}:${command.destination.port}`)
        const socket = net.createConnection(command.destination.port, command.destination.ip)

        socket.on('error', err => {
          logger.error(`error connecting: ${err}`)
          reject(err)
        })

        socket.on('connect', () => {
          var data = Command.encryptAndSign(command, command.destination.publicKey, node.privateKey)
          data = Command.serialize(data) + messageDelimiter
          socket.write(data, () => {
            logger.debug(`destroyed socket connecting to ${command.destination.ip}:${command.destination.port}`)
            socket.destroy()
          })
          logger.info(`sent data from ${node.ip}:${node.port} to ${command.destination.ip}:${command.destination.port}\n${prettyjson.render(command, { noColor: true })}`)

          // wait for responses to some requests
          if (_.includes(['PING', 'MESSAGE', 'FIND', 'RETRIEVE_CONTACTS', 'KEY_REQUEST'], command.command)) {
            pendingRequests.push({ command, resolve, reject })
            //setTimeout(() => rejectRequest(command.id), constants.RESPONSE_TIMEOUT);
          }
        })
      })

      return p
    }

    const handleCommand = async (serializedData) => {
      let command = Command.deserialize(serializedData)
      command = Command.decrypt(command, node.privateKey)
      logger.info(`${node.id}((${node.ip}:${node.port}) received\n ${prettyjson.render(command, { noColor: true })}`)

      switch(command.command)
      {
        case 'PING':
          sendCommand(Command.createPingRes(command, node.asContact()))
          return
        case 'MESSAGE_RESPONSE':
        case 'PING_RESPONSE':
          resolveRequest(command.id, true)
          return
        case 'MESSAGE':
          logger.info(`received ${command.payload.messageString}`)
          sendCommand(Command.createMessageRes(command, node.asContact()))
          return
        case 'FIND':
          sendCommand(Command.createFindRes(command, node.asContact()))
          return
        case 'FIND_RESPONSE':
          resolveRequest(command.id, command.payload.publicKey)
          return
        case 'KEY_REQUEST':
          logger.warn(`in key request ${node.username}`)
          const requestedKey = routePublicKeyP(command.payload.state).then(res => {
            return sendCommand(Command.createPubKeyRes(command, node.asContact(), res))
          })
          return
        case 'KEY_RESPONSE':
          logger.warn(`in key response ${node.username}`)
          resolveRequest(command.id, command.payload.requestedKey)
          return
        case 'RETRIEVE_CONTACTS':
          const state = command.payload.state;

          // stop if we already responded to the same message
          if (_.includes(queryCache, state.queryId)){
            state.contacted.push(node.id);
            state.contacted = _.uniq(state.contacted);
            sendCommand(Command.createRetrieveContactRes(command, node.asContact()));
            return
          }
          queryCache.push(state.queryId);

          try {
            await updateContactP(command.payload.state.contact)
          } catch (e) {
            logger.error('RETRIEVE_CONTACTS: failed to update contact', e)
            return
          }

          // add current node to contacted list
          state.contacted = _.chain(state.contacted).concat([node.id]).uniq().value()

          // update closest node in state
          const distance = magic.getDistance(state.contact.id, node.id);
          if (magic.compareKeys(distance, magic.hexToBuffer(state.closestNodeDistance)) === -1) {
            state.previousClosestNode = state.closestNode;
            state.closestNode = node.id;
            state.closestNodeDistance = magic.bufferToHex(distance);
          }

          let closestNodeUnchanged = state.closestNode === state.previousClosestNode
          if(state.closestNode && state.previousClosestNode) {
            closestNodeUnchanged = state.closestNode.id === state.previousClosestNode.id;
          }

          // add nearest contacts of current node to contact list
          const nearestContacts = getNearestContacts(state.key, state.limit, node.id);
          state.contactlist = _.chain(state.contactlist).concat(nearestContacts).uniqBy('id').filter(contact => contact.id !== state.contact.id).value()

          // stop if conact list is full or the closest node didn't change
          const contactlistFull = state.contactlist.length >= constants.K;
          if (closestNodeUnchanged || contactlistFull) {
            sendCommand(Command.createRetrieveContactRes(command, node.asContact()));
            return
          }

          // recalculate remaining contacts
          state.remainingContacts = state.contactlist.filter(e => !_.includes(state.contacted, e.id))

          // stop if there are no more contacts to query
          if (state.remainingContacts.length === 0) {
            sendCommand(Command.createRetrieveContactRes(command, node.asContact()));
            return
          }

          try {
            command.payload.state = await iteritiveFindP(state)
            sendCommand(Command.createRetrieveContactRes(command, node.asContact()))
            logger.debug(`${node.username} received RETRIEVE_CONTACTS finished`)
          } catch (e) {
            logger.error('RETRIEVE_CONTACTS: iterative find failed', e)
          }

          return
        case 'RETRIEVE_CONTACTS_RESPONSE':
          resolveRequest(command.id, command.payload.state)
          return
      }
    }

    const updateContactP = async (contact) => {
      const bucketIndex = magic.getBucketIndex(node.id, contact.id)

      logger.debug(node.username, 'is updating buckets with contact', contact.username)
      assert(bucketIndex < constants.B)

      if (!buckets[bucketIndex]) buckets[bucketIndex] = []

      const bucket = buckets[bucketIndex]

      const foundAt = _.findIndex(bucket, e => e.id === contact.id)
      if (foundAt !== -1) {
        bucket[foundAt].lastSeen = Date.now()
      } else if (bucket.length < constants.K) {
        contact.lastSeen = Date.now()
        bucket.push(contact)
      } else {
        // TODO: untested branch
        try {
          await node.ping(bucket[0])
        } catch (e) {
          bucket.splice(0)
          bucket.push(contact)
        }
      }

      bucket.sort(e => e.lastSeen)

      return contact
    }

    const getNearestNodesP = async (contactID) => {
      var state = {
        key: contactID,
        queryId: utils.generateMessageId(),
        contact: node.asContact(),
        limit: constants.ALPHA,
        contacted: [],
        contactlist: [],
        remainingContacts: [],
        previousClosestNode: null
      }

      state.contacted.push(node.id)
      queryCache.push(state.queryId)

      // initialize state
      getNearestContacts(contactID, state.limit, node.id).forEach(contact => state.contactlist.push(contact))
      state.remainingContacts = state.contactlist
      state.closestNode = state.contactlist[0]
      state.previousClosestNode = state.closestNode
      assert(state.closestNode, 'Not connected to any peers')
      state.closestNodeDistance = magic.bufferToHex(magic.getDistance(state.key, state.closestNode.id))

      try {
        const finalState = await iteritiveFindP(state)
        return Promise.all(finalState.contactlist.map(e => updateContactP(e)))
      } catch (e) {
        logger.error('getNearestNodesP: failed iteritiveFindP', e)
        return []
      }
    }

    const iteritiveFindP = async (state) => {

      const remainingContacts = state.contactlist.filter(e => !_.includes(state.contacted, e.id))

      if (remainingContacts.length === 0) {
        return state
      }

      const routeResults = await route(state, undefined, () => Promise.resolve(_.take(remainingContacts, constants.ALPHA)), (c, state) => node.retrieveContacts(c, state))

      let contactlist = routeResults.map(e => e.contactlist).reduce((current, previous) => current.concat(previous))
      contactlist = _.uniqBy(contactlist, 'id')
      let contacted = routeResults.map(e => e.contacted).reduce((current, previous) => current.concat(previous))
      contacted = _.uniq(contacted)

      return await iteritiveFindP(_.merge({}, routeResults[0], { contactlist, contacted }))
    }

    const route = async (state, fnDestinationP, fnGatherContactsP, fnEachContactP) => {
      if (state.destinationId === node.id && fnDestinationP) return fnDestinationP()

      const contacts = await fnGatherContactsP()
      return await Promise.all(contacts.map(e => fnEachContactP(e, state)))
    }

    const findPublicKeyP = async username => {
      const id = utils.generateId(username)
      const bucketIndex = magic.getBucketIndex(node.id, id)
      const bucket = buckets[bucketIndex]

      if (bucket) {
        const contactIndex = _.findIndex(bucket, e => e.id === id)
        if (contactIndex !== -1) {
          return bucket[contactIndex].publicKey
        }
      }

      return await routePublicKeyP({ destinationId: id, visited: [] })
    }

    const routePublicKeyP = async state => {
      console.log('routePublicKeyP')
      state.visited.push(node.id)
      return await route(state, () => {
          console.log('found destination')
          return node.publicKey // return public key when it gets to destination node
        }, () => {
          const contactList = _.flatten(buckets).filter(e => e !== undefined).filter(e => !_.includes(state.visited, e))
          console.log(`found ${contactList.length} contacts`)
          return _.sampleSize(contactList, constants.ELI_THRESHOLD)
        }, contact => {
          console.log(`sending request from ${node.username} to ${contact.username}`)
          return sendCommand(Command.createPubKeyReq(node.asContact(), contact, state))
        });
    };

    const getNearestContacts = (contactID, limit, nodeID) => {
      const contacts = [];
      const index = magic.getBucketIndex(nodeID, contactID);
      let ascBucketIndex = index;
      let descBucketIndex = index;

      function addNearestFromBucket(bucket) {
        getNearestFromBucket(bucket, contactID, limit - contacts.length).forEach(function addToContacts(contact) {
            const isContact = contact.__classId === 'CONTACT'
            const poolNotFull = contacts.length < limit
            const notRequester = contact.nodeID !== nodeID

            if (isContact && poolNotFull && notRequester) { contacts.push(contact) }
          })
      }

      // add contacts from current bucket
      addNearestFromBucket(buckets[index])

      // add contacts while decrementing counter
      while (contacts.length < limit && ascBucketIndex < constants.B) { ascBucketIndex++; addNearestFromBucket(buckets[ascBucketIndex]) }

      // add contacts while incrementing counter
      while (contacts.length < limit && descBucketIndex >= 0) { descBucketIndex--; addNearestFromBucket(buckets[descBucketIndex]) }

      return contacts
    }

    const getNearestFromBucket = (bucket, key, limit) => {
      if (!bucket) { return [] }

      return bucket
        .map(contact => ({ contact: contact, distance: magic.getDistance(contact.id, key) })) // calculate distances
        .sort((a, b) => magic.compareKeys(a.distance, b.distance)) // sort keys by distance
        .splice(0, limit) // take top $limit
        .map(c => c.contact) // pluck contact
    }

    const refreshBucketsBeyondClosestP = async (shortlist) => {
      const bucketIndexes = Object.keys(buckets)
      const leastBucket = _.min(bucketIndexes)
      const bucketsToRefresh = bucketIndexes.filter(index => index > leastBucket);

      logger.debug('refreshing buckets farthest than closest known');

      for (let i = 0; i < bucketsToRefresh.length; i++) {
        const random = magic.getRandomInBucketRangeBuffer(bucketsToRefresh[i])
        await getNearestNodesP(random.toString('hex'))
      }

      logger.debug('refreshed all buckets');
    }

    /**** END METHODS ****/

    /**** HELPER METHODS ****/
    const completeRequest = (id, f) => {
      const i = _.findIndex(pendingRequests, e => e.command.id === id);
      if (i !== -1) {
        f(pendingRequests[i]);
        pendingRequests.splice(i, 1);
      }
    };
    const rejectRequest = id => completeRequest(id, request => request.reject(new Error('timeout')));
    const resolveRequest = (id, result) => completeRequest(id, request => request.resolve(result));
    /**** END HELPER METHODS ****/

    /**** CONSTRUCTOR ****/
    // create listening server
    const server = net.createServer(socket => {
      const addr = socket.remoteAddress;
      const port = socket.remotePort;

      logger.debug(`${node.id}((${node.ip}:${node.port}) connection from ${addr}:${port}`);

      let data = '';
      socket.on('data', chunk => {
        data += chunk;

        let dIndex = data.indexOf(messageDelimiter);
        while (dIndex > -1) {
          const command = data.substring(0, dIndex);
          handleCommand(command);

          data = data.substring(dIndex + 1);
          dIndex = data.indexOf(messageDelimiter);
        }
      });

      socket.on('close', () => logger.debug(`${node.id}(${node.ip}:${node.port}) closed connection with ${addr}:${port}`));
    });

    // reject promise if listening server fails to start
    server.on('error', err => reject(err));

    // resolve only after listening server has started
    server.on('listening', () => {
      resolve({
        close: () => new Promise((resolve, reject) => {
          server.close(err => {
            if (err) return reject(err)
            logger.debug(`stopped listening on ${node.ip}:${node.port}`);
            resolve();
          })
        }),
        sendCommand,
        updateContactP,
        getNearestNodesP,
        refreshBucketsBeyondClosestP,
        findPublicKeyP,
        _getNearestContacts: getNearestContacts,
        _buckets: buckets
      });
    });

    // attempt to start listening server
    logger.debug(`${node.id} is attempting to listen on ${node.ip}:${node.port}`);
    server.listen(node.port, node.ip);
    /**** END CONSTRUCTOR ****/
  });
};<commit_msg>purged router of the devil's keyword<commit_after>'use strict';

require('babel-polyfill')

const net = require('net'),
      Command = require('./command'),
      constants = require('./constants'),
      magic = require('./magic'),
      assert = require('assert'),
      Contact = require('./contact'),
      _ = require('lodash'),
      prettyjson = require('prettyjson'),
      utils = require('./utils'),
      Promise = require('bluebird')

Promise.config({ longStackTrace: true })

module.exports = node => {
  return new Promise((resolve, reject) => {
    /**** PRIVATE FIELDS ****/
    const pendingRequests = []
    const buckets = []
    const queryCache = []
    const logger = node.logger
    const messageDelimiter = ';'
    /**** END PRIVATE FIELDS ****/

    /**** METHODS ****/
    const sendCommand = command => {
      const p = new Promise((resolve, reject) => {
        logger.debug(`attempting to connect to ${command.destination.ip}:${command.destination.port}`)
        const socket = net.createConnection(command.destination.port, command.destination.ip)

        socket.on('error', err => {
          logger.error(`error connecting: ${err}`)
          reject(err)
        })

        socket.on('connect', () => {
          let data = Command.encryptAndSign(command, command.destination.publicKey, node.privateKey)
          data = Command.serialize(data) + messageDelimiter
          socket.write(data, () => {
            logger.debug(`destroyed socket connecting to ${command.destination.ip}:${command.destination.port}`)
            socket.destroy()
          })
          logger.info(`sent data from ${node.ip}:${node.port} to ${command.destination.ip}:${command.destination.port}\n${prettyjson.render(command, { noColor: true })}`)

          // wait for responses to some requests
          if (_.includes(['PING', 'MESSAGE', 'FIND', 'RETRIEVE_CONTACTS', 'KEY_REQUEST'], command.command)) {
            pendingRequests.push({ command, resolve, reject })
            //setTimeout(() => rejectRequest(command.id), constants.RESPONSE_TIMEOUT);
          }
        })
      })

      return p
    }

    const handleCommand = async (serializedData) => {
      let command = Command.deserialize(serializedData)
      command = Command.decrypt(command, node.privateKey)
      logger.info(`${node.id}((${node.ip}:${node.port}) received\n ${prettyjson.render(command, { noColor: true })}`)

      switch(command.command)
      {
        case 'PING':
          sendCommand(Command.createPingRes(command, node.asContact()))
          return
        case 'MESSAGE_RESPONSE':
        case 'PING_RESPONSE':
          resolveRequest(command.id, true)
          return
        case 'MESSAGE':
          logger.info(`received ${command.payload.messageString}`)
          sendCommand(Command.createMessageRes(command, node.asContact()))
          return
        case 'FIND':
          sendCommand(Command.createFindRes(command, node.asContact()))
          return
        case 'FIND_RESPONSE':
          resolveRequest(command.id, command.payload.publicKey)
          return
        case 'KEY_REQUEST':
          logger.warn(`in key request ${node.username}`)
          const requestedKey = routePublicKeyP(command.payload.state).then(res => {
            return sendCommand(Command.createPubKeyRes(command, node.asContact(), res))
          })
          return
        case 'KEY_RESPONSE':
          logger.warn(`in key response ${node.username}`)
          resolveRequest(command.id, command.payload.requestedKey)
          return
        case 'RETRIEVE_CONTACTS':
          const state = command.payload.state;

          // stop if we already responded to the same message
          if (_.includes(queryCache, state.queryId)){
            state.contacted.push(node.id);
            state.contacted = _.uniq(state.contacted);
            sendCommand(Command.createRetrieveContactRes(command, node.asContact()));
            return
          }
          queryCache.push(state.queryId);

          try {
            await updateContactP(command.payload.state.contact)
          } catch (e) {
            logger.error('RETRIEVE_CONTACTS: failed to update contact', e)
            return
          }

          // add current node to contacted list
          state.contacted = _.chain(state.contacted).concat([node.id]).uniq().value()

          // update closest node in state
          const distance = magic.getDistance(state.contact.id, node.id);
          if (magic.compareKeys(distance, magic.hexToBuffer(state.closestNodeDistance)) === -1) {
            state.previousClosestNode = state.closestNode;
            state.closestNode = node.id;
            state.closestNodeDistance = magic.bufferToHex(distance);
          }

          let closestNodeUnchanged = state.closestNode === state.previousClosestNode
          if(state.closestNode && state.previousClosestNode) {
            closestNodeUnchanged = state.closestNode.id === state.previousClosestNode.id;
          }

          // add nearest contacts of current node to contact list
          const nearestContacts = getNearestContacts(state.key, state.limit, node.id);
          state.contactlist = _.chain(state.contactlist).concat(nearestContacts).uniqBy('id').filter(contact => contact.id !== state.contact.id).value()

          // stop if conact list is full or the closest node didn't change
          const contactlistFull = state.contactlist.length >= constants.K;
          if (closestNodeUnchanged || contactlistFull) {
            sendCommand(Command.createRetrieveContactRes(command, node.asContact()));
            return
          }

          // recalculate remaining contacts
          state.remainingContacts = state.contactlist.filter(e => !_.includes(state.contacted, e.id))

          // stop if there are no more contacts to query
          if (state.remainingContacts.length === 0) {
            sendCommand(Command.createRetrieveContactRes(command, node.asContact()));
            return
          }

          try {
            command.payload.state = await iteritiveFindP(state)
            sendCommand(Command.createRetrieveContactRes(command, node.asContact()))
            logger.debug(`${node.username} received RETRIEVE_CONTACTS finished`)
          } catch (e) {
            logger.error('RETRIEVE_CONTACTS: iterative find failed', e)
          }

          return
        case 'RETRIEVE_CONTACTS_RESPONSE':
          resolveRequest(command.id, command.payload.state)
          return
      }
    }

    const updateContactP = async (contact) => {
      const bucketIndex = magic.getBucketIndex(node.id, contact.id)

      logger.debug(node.username, 'is updating buckets with contact', contact.username)
      assert(bucketIndex < constants.B)

      if (!buckets[bucketIndex]) buckets[bucketIndex] = []

      const bucket = buckets[bucketIndex]

      const foundAt = _.findIndex(bucket, e => e.id === contact.id)
      if (foundAt !== -1) {
        bucket[foundAt].lastSeen = Date.now()
      } else if (bucket.length < constants.K) {
        contact.lastSeen = Date.now()
        bucket.push(contact)
      } else {
        // TODO: untested branch
        try {
          await node.ping(bucket[0])
        } catch (e) {
          bucket.splice(0)
          bucket.push(contact)
        }
      }

      bucket.sort(e => e.lastSeen)

      return contact
    }

    const getNearestNodesP = async (contactID) => {
      const state = {
        key: contactID,
        queryId: utils.generateMessageId(),
        contact: node.asContact(),
        limit: constants.ALPHA,
        contacted: [],
        contactlist: [],
        remainingContacts: [],
        previousClosestNode: null
      }

      state.contacted.push(node.id)
      queryCache.push(state.queryId)

      // initialize state
      getNearestContacts(contactID, state.limit, node.id).forEach(contact => state.contactlist.push(contact))
      state.remainingContacts = state.contactlist
      state.closestNode = state.contactlist[0]
      state.previousClosestNode = state.closestNode
      assert(state.closestNode, 'Not connected to any peers')
      state.closestNodeDistance = magic.bufferToHex(magic.getDistance(state.key, state.closestNode.id))

      try {
        const finalState = await iteritiveFindP(state)
        return Promise.all(finalState.contactlist.map(e => updateContactP(e)))
      } catch (e) {
        logger.error('getNearestNodesP: failed iteritiveFindP', e)
        return []
      }
    }

    const iteritiveFindP = async (state) => {
      const remainingContacts = state.contactlist.filter(e => !_.includes(state.contacted, e.id))

      if (remainingContacts.length === 0) { return state }

      const routeResults = await route(state, undefined, () => Promise.resolve(_.take(remainingContacts, constants.ALPHA)), (c, state) => node.retrieveContacts(c, state))

      let contactlist = routeResults.map(e => e.contactlist).reduce((current, previous) => current.concat(previous))
      contactlist = _.uniqBy(contactlist, 'id')
      let contacted = routeResults.map(e => e.contacted).reduce((current, previous) => current.concat(previous))
      contacted = _.uniq(contacted)

      return await iteritiveFindP(_.merge({}, routeResults[0], { contactlist, contacted }))
    }

    const route = async (state, fnDestinationP, fnGatherContactsP, fnEachContactP) => {
      if (state.destinationId === node.id && fnDestinationP) return fnDestinationP()

      const contacts = await fnGatherContactsP()
      return await Promise.all(contacts.map(e => fnEachContactP(e, state)))
    }

    const findPublicKeyP = async username => {
      const id = utils.generateId(username)
      const bucketIndex = magic.getBucketIndex(node.id, id)
      const bucket = buckets[bucketIndex]

      if (bucket) {
        const contactIndex = _.findIndex(bucket, e => e.id === id)
        if (contactIndex !== -1) { return bucket[contactIndex].publicKey }
      }

      return await routePublicKeyP({ destinationId: id, visited: [] })
    }

    const routePublicKeyP = async state => {
      console.log('routePublicKeyP')
      state.visited.push(node.id)
      return await route(state, () => {
          console.log('found destination')
          return node.publicKey // return public key when it gets to destination node
        }, () => {
          const contactList = _.flatten(buckets).filter(e => e !== undefined).filter(e => !_.includes(state.visited, e))
          console.log(`found ${contactList.length} contacts`)
          return _.sampleSize(contactList, constants.ELI_THRESHOLD)
        }, contact => {
          console.log(`sending request from ${node.username} to ${contact.username}`)
          return sendCommand(Command.createPubKeyReq(node.asContact(), contact, state))
        });
    };

    const getNearestContacts = (contactID, limit, nodeID) => {
      const contacts = [];
      const index = magic.getBucketIndex(nodeID, contactID);
      let ascBucketIndex = index;
      let descBucketIndex = index;

      function addNearestFromBucket(bucket) {
        getNearestFromBucket(bucket, contactID, limit - contacts.length).forEach(function addToContacts(contact) {
            const isContact = contact.__classId === 'CONTACT'
            const poolNotFull = contacts.length < limit
            const notRequester = contact.nodeID !== nodeID

            if (isContact && poolNotFull && notRequester) { contacts.push(contact) }
          })
      }

      // add contacts from current bucket
      addNearestFromBucket(buckets[index])

      // add contacts while decrementing counter
      while (contacts.length < limit && ascBucketIndex < constants.B) { ascBucketIndex++; addNearestFromBucket(buckets[ascBucketIndex]) }

      // add contacts while incrementing counter
      while (contacts.length < limit && descBucketIndex >= 0) { descBucketIndex--; addNearestFromBucket(buckets[descBucketIndex]) }

      return contacts
    }

    const getNearestFromBucket = (bucket, key, limit) => {
      if (!bucket) { return [] }

      return bucket
        .map(contact => ({ contact: contact, distance: magic.getDistance(contact.id, key) })) // calculate distances
        .sort((a, b) => magic.compareKeys(a.distance, b.distance)) // sort keys by distance
        .splice(0, limit) // take top $limit
        .map(c => c.contact) // pluck contact
    }

    const refreshBucketsBeyondClosestP = async (shortlist) => {
      const bucketIndexes = Object.keys(buckets)
      const leastBucket = _.min(bucketIndexes)
      const bucketsToRefresh = bucketIndexes.filter(index => index > leastBucket);

      logger.debug('refreshing buckets farthest than closest known');

      for (let i = 0; i < bucketsToRefresh.length; i++) {
        const random = magic.getRandomInBucketRangeBuffer(bucketsToRefresh[i])
        await getNearestNodesP(random.toString('hex'))
      }

      logger.debug('refreshed all buckets');
    }

    /**** END METHODS ****/

    /**** HELPER METHODS ****/
    const completeRequest = (id, f) => {
      const i = _.findIndex(pendingRequests, e => e.command.id === id);
      if (i !== -1) {
        f(pendingRequests[i]);
        pendingRequests.splice(i, 1);
      }
    };
    const rejectRequest = id => completeRequest(id, request => request.reject(new Error('timeout')));
    const resolveRequest = (id, result) => completeRequest(id, request => request.resolve(result));
    /**** END HELPER METHODS ****/

    /**** CONSTRUCTOR ****/
    // create listening server
    const server = net.createServer(socket => {
      const addr = socket.remoteAddress;
      const port = socket.remotePort;

      logger.debug(`${node.id}((${node.ip}:${node.port}) connection from ${addr}:${port}`);

      let data = '';
      socket.on('data', chunk => {
        data += chunk;

        let dIndex = data.indexOf(messageDelimiter);
        while (dIndex > -1) {
          const command = data.substring(0, dIndex);
          handleCommand(command);

          data = data.substring(dIndex + 1);
          dIndex = data.indexOf(messageDelimiter);
        }
      });

      socket.on('close', () => logger.debug(`${node.id}(${node.ip}:${node.port}) closed connection with ${addr}:${port}`));
    });

    // reject promise if listening server fails to start
    server.on('error', err => reject(err));

    // resolve only after listening server has started
    server.on('listening', () => {
      resolve({
        close: () => new Promise((resolve, reject) => {
          server.close(err => {
            if (err) return reject(err)
            logger.debug(`stopped listening on ${node.ip}:${node.port}`);
            resolve();
          })
        }),
        sendCommand,
        updateContactP,
        getNearestNodesP,
        refreshBucketsBeyondClosestP,
        findPublicKeyP,
        _getNearestContacts: getNearestContacts,
        _buckets: buckets
      });
    });

    // attempt to start listening server
    logger.debug(`${node.id} is attempting to listen on ${node.ip}:${node.port}`);
    server.listen(node.port, node.ip);
    /**** END CONSTRUCTOR ****/
  });
};<|endoftext|>


<commit_before>var seoData = {
  admin: {
    title: "Admin",
    description: "Change administrative settings."
  },
  userEdit: {
    title: "Profile",
    description: "Edit profile information."
  },
  postSubmit: {
    title: "Submit Petition",
    description: "Submit a petition to PawPrints."
  },
  postsList: {
    title: "Petitions",
    description: "PawPrints is a place for sparking change at RIT. Share ideas with the RIT community and influence decision making."
  },
  postsResponses: {
    title: "Responses",
    description: "View petitions that have received responses from RIT Student Government and Administration."
  },
  postPage: {
    title: "View Petition",
    description: "View petition on PawPrints."
  },
  postEdit: {
    title: "Edit Petition",
    description: "Edit petition."
  },
  about: {
    title: "About PawPrints",
    description: "Learn more about the history of PawPrints, its goal and vision."
  },
  api: {
    title: "API Access",
    description: "Student Government provides a read-only JSON REST API for retreiving petition information."
  },
  moderation: {
    title: "Moderation Policy",
    description: "PawPrints follows the RIT Code of Conduct for Computer and Network Use."
  },
  petitionProcess: {
    title: "Petition Process",
    description: "Learn how to effectively create a successful petition."
  },
  index: {
    title: "PawPrints",
    description: "PawPrints is a place for sparking change at RIT. Share ideas with the RIT community and influence decision making."
  }
};

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  onBeforeAction: function () {
    var page = this.route.name;
    setSEO(seoData[page]);
  },
  onRun: function () {
    Deps.nonreactive(function() {
      GAnalytics.pageview();
    });
  },
  waitOn: function () {
    return [Meteor.subscribe('singleton')];
  }
});

SingletonController = RouteController.extend({
  data: function() {
    return {
      singleton: Singleton.findOne()
    };
  }
});

var infiniteScroll = function() {
  var didScroll = false,
      windowDom = $(window),
      documentDom = $(document);

  $(window).scroll(function() {
      didScroll = true;
  });

  setInterval(function() {
    if ( didScroll ) {
      didScroll = false;
      var atBottom = (windowDom.scrollTop() == documentDom.height() - windowDom.height());
      if (atBottom) {
        Session.set('postsLimit', Session.get('postsLimit') + 12);
      }
    }
  }, 500);
};

PostsListController = RouteController.extend({
  onBeforeAction: function() {
    Meteor.subscribe('posts', Session.get('postsLimit'));
    infiniteScroll();
  },
  data: function() {
    return {
      posts: Posts.find().fetch(),
      singleton: Singleton.findOne()
    };
  },
});

PostsWithResponsesController = RouteController.extend({
  onBeforeAction: function () {
    Meteor.subscribe('postsWithResponses', Session.get('postsLimit'));
    infiniteScroll();
  },
  data: function() {
    return {
      posts: Posts.find().fetch(),
      singleton: Singleton.findOne()
    };
  }
});

Router.map(function() {

  // Privileged Routes

  this.route('admin', {
    path: '/admin',
    template: 'admin',
    waitOn: function() {
      return [Meteor.subscribe('privilegedUsers')];
    },
    data: function() {
      return {
        admins: Meteor.users.find({roles: {$in: ['admin']}}),
        moderators: Meteor.users.find({roles: {$in: ['moderator']}}),
        singleton: Singleton.findOne()
      };
    }
  });

  // User Routes

  this.route('userEdit', {
    path: '/users/edit',
    waitOn: function() {
      return [Meteor.subscribe('apiKeys')];
    },
    data: function() {
      return {
        user: Meteor.user(),
        apiKeys: ApiKeys.findOne(),
        singleton: Singleton.findOne()
      };
    }
  });

  // Post Routes

  this.route('postSubmit', {
    path: '/petitions/create',
    controller: SingletonController
  });

  this.route('postsList', {
    path: '/petitions/list',
    template: 'postsList',
    controller: PostsListController
  });

  this.route('postsResponses', {
    path: '/petitions/responses',
    template: 'postsWithResponsesList',
    controller: PostsWithResponsesController
  });

  this.route('postPage', {
    path: '/petitions/:_id',
    template: 'postPage',
    waitOn: function() {
      return [Meteor.subscribe('singlePost', this.params._id),
              Meteor.subscribe('singleScore', this.params._id),
              Meteor.subscribe('signers', this.params._id)];
    },
    data: function() {
      return {
        post: Posts.findOne(this.params._id),
        singleton: Singleton.findOne(),
        scores: Scores.find().fetch(),
        url: window.location.href
      }
    },
    onAfterAction: function() {
      if (this.data().post)
        setSEO({title: this.data().post.title, description: this.data().post.description});
      return;
    }
  });

  this.route('postEdit', {
    path: '/petitions/:_id/edit',
    template: 'postEdit',
    waitOn: function () {
      return [Meteor.subscribe('singlePost', this.params._id)];
    },
    data: function() {
      return {
        post: Posts.findOne(this.params._id),
        singleton: Singleton.findOne()
      }
    }
  });

  // Static routes

  this.route('about', {
    path: '/about',
    template: 'aboutTemplate',
    data: function () {
      return {
        singleton: Singleton.findOne(),
        subtemplate: 'about'
      };
    }
  });

  this.route('api', {
    path: '/api-access',
    template: 'aboutTemplate',
    data: function () {
      return {
        singleton: Singleton.findOne(),
        subtemplate: 'api'
      };
    }
  });

  this.route('moderation', {
    path: '/moderation',
    template: 'aboutTemplate',
    data: function () {
      return {
        singleton: Singleton.findOne(),
        subtemplate: 'moderation'
      };
    }
  });

  this.route('petitionProcess', {
    path: '/petition-process',
    template: 'aboutTemplate',
    data: function () {
      return {
        singleton: Singleton.findOne(),
        subtemplate: 'petitionProcess'
      };
    }
  });

  // Must be last route (catch-all)

  this.route('index', {
    path: '/',
    template: 'index',
    controller: PostsListController
  });

});

var requireLogin = function(pause) {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render(this.loadingTemplate);
    else
      this.render('accessDenied');
    pause();
  }
};

var setSEO = function(data) {
  if (Meteor.isClient) {
    SEO.set({
      title: data.title,
      meta: {
        'description': data.description
      },
      og: {
        'title': data.title,
        'description': data.description,
        'image': window.location.origin + '/logo_200x200.png'
      }
    });
  }
};

var clearErrors = function() {
  Errors.remove({});
  Session.set("loginMsg", "");
}

Router.onBeforeAction('loading');
Router.onBeforeAction(clearErrors);
Router.onBeforeAction(requireLogin, {only: ['admin', 'postSubmit', 'userEdit']});
<commit_msg>fix og:image root url from pointing to localhost<commit_after>var seoData = {
  admin: {
    title: "Admin",
    description: "Change administrative settings."
  },
  userEdit: {
    title: "Profile",
    description: "Edit profile information."
  },
  postSubmit: {
    title: "Submit Petition",
    description: "Submit a petition to PawPrints."
  },
  postsList: {
    title: "Petitions",
    description: "PawPrints is a place for sparking change at RIT. Share ideas with the RIT community and influence decision making."
  },
  postsResponses: {
    title: "Responses",
    description: "View petitions that have received responses from RIT Student Government and Administration."
  },
  postPage: {
    title: "View Petition",
    description: "View petition on PawPrints."
  },
  postEdit: {
    title: "Edit Petition",
    description: "Edit petition."
  },
  about: {
    title: "About PawPrints",
    description: "Learn more about the history of PawPrints, its goal and vision."
  },
  api: {
    title: "API Access",
    description: "Student Government provides a read-only JSON REST API for retreiving petition information."
  },
  moderation: {
    title: "Moderation Policy",
    description: "PawPrints follows the RIT Code of Conduct for Computer and Network Use."
  },
  petitionProcess: {
    title: "Petition Process",
    description: "Learn how to effectively create a successful petition."
  },
  index: {
    title: "PawPrints",
    description: "PawPrints is a place for sparking change at RIT. Share ideas with the RIT community and influence decision making."
  }
};

Router.configure({
  layoutTemplate: 'layout',
  loadingTemplate: 'loading',
  onBeforeAction: function () {
    var page = this.route.name;
    setSEO(seoData[page]);
  },
  onRun: function () {
    Deps.nonreactive(function() {
      GAnalytics.pageview();
    });
  },
  waitOn: function () {
    return [Meteor.subscribe('singleton')];
  }
});

SingletonController = RouteController.extend({
  data: function() {
    return {
      singleton: Singleton.findOne()
    };
  }
});

var infiniteScroll = function() {
  var didScroll = false,
      windowDom = $(window),
      documentDom = $(document);

  $(window).scroll(function() {
      didScroll = true;
  });

  setInterval(function() {
    if ( didScroll ) {
      didScroll = false;
      var atBottom = (windowDom.scrollTop() == documentDom.height() - windowDom.height());
      if (atBottom) {
        Session.set('postsLimit', Session.get('postsLimit') + 12);
      }
    }
  }, 500);
};

PostsListController = RouteController.extend({
  onBeforeAction: function() {
    Meteor.subscribe('posts', Session.get('postsLimit'));
    infiniteScroll();
  },
  data: function() {
    return {
      posts: Posts.find().fetch(),
      singleton: Singleton.findOne()
    };
  },
});

PostsWithResponsesController = RouteController.extend({
  onBeforeAction: function () {
    Meteor.subscribe('postsWithResponses', Session.get('postsLimit'));
    infiniteScroll();
  },
  data: function() {
    return {
      posts: Posts.find().fetch(),
      singleton: Singleton.findOne()
    };
  }
});

Router.map(function() {

  // Privileged Routes

  this.route('admin', {
    path: '/admin',
    template: 'admin',
    waitOn: function() {
      return [Meteor.subscribe('privilegedUsers')];
    },
    data: function() {
      return {
        admins: Meteor.users.find({roles: {$in: ['admin']}}),
        moderators: Meteor.users.find({roles: {$in: ['moderator']}}),
        singleton: Singleton.findOne()
      };
    }
  });

  // User Routes

  this.route('userEdit', {
    path: '/users/edit',
    waitOn: function() {
      return [Meteor.subscribe('apiKeys')];
    },
    data: function() {
      return {
        user: Meteor.user(),
        apiKeys: ApiKeys.findOne(),
        singleton: Singleton.findOne()
      };
    }
  });

  // Post Routes

  this.route('postSubmit', {
    path: '/petitions/create',
    controller: SingletonController
  });

  this.route('postsList', {
    path: '/petitions/list',
    template: 'postsList',
    controller: PostsListController
  });

  this.route('postsResponses', {
    path: '/petitions/responses',
    template: 'postsWithResponsesList',
    controller: PostsWithResponsesController
  });

  this.route('postPage', {
    path: '/petitions/:_id',
    template: 'postPage',
    waitOn: function() {
      return [Meteor.subscribe('singlePost', this.params._id),
              Meteor.subscribe('singleScore', this.params._id),
              Meteor.subscribe('signers', this.params._id)];
    },
    data: function() {
      return {
        post: Posts.findOne(this.params._id),
        singleton: Singleton.findOne(),
        scores: Scores.find().fetch(),
        url: window.location.href
      }
    },
    onAfterAction: function() {
      if (this.data().post)
        setSEO({title: this.data().post.title, description: this.data().post.description});
      return;
    }
  });

  this.route('postEdit', {
    path: '/petitions/:_id/edit',
    template: 'postEdit',
    waitOn: function () {
      return [Meteor.subscribe('singlePost', this.params._id)];
    },
    data: function() {
      return {
        post: Posts.findOne(this.params._id),
        singleton: Singleton.findOne()
      }
    }
  });

  // Static routes

  this.route('about', {
    path: '/about',
    template: 'aboutTemplate',
    data: function () {
      return {
        singleton: Singleton.findOne(),
        subtemplate: 'about'
      };
    }
  });

  this.route('api', {
    path: '/api-access',
    template: 'aboutTemplate',
    data: function () {
      return {
        singleton: Singleton.findOne(),
        subtemplate: 'api'
      };
    }
  });

  this.route('moderation', {
    path: '/moderation',
    template: 'aboutTemplate',
    data: function () {
      return {
        singleton: Singleton.findOne(),
        subtemplate: 'moderation'
      };
    }
  });

  this.route('petitionProcess', {
    path: '/petition-process',
    template: 'aboutTemplate',
    data: function () {
      return {
        singleton: Singleton.findOne(),
        subtemplate: 'petitionProcess'
      };
    }
  });

  // Must be last route (catch-all)

  this.route('index', {
    path: '/',
    template: 'index',
    controller: PostsListController
  });

});

var requireLogin = function(pause) {
  if (! Meteor.user()) {
    if (Meteor.loggingIn())
      this.render(this.loadingTemplate);
    else
      this.render('accessDenied');
    pause();
  }
};

var setSEO = function(data) {
  if (Meteor.isClient) {
    var image_url;
    if( Meteor.settings && Meteor.settings.public !== undefined &&
        Meteor.settings.public.root_url !== undefined) {
      image_url = Meteor.settings.public.root_url + '/logo_200x200.png';
    }
    SEO.set({
      title: data.title,
      meta: {
        'description': data.description
      },
      og: {
        'title': data.title,
        'description': data.description,
        'image': image_url
      }
    });
  }
};

var clearErrors = function() {
  Errors.remove({});
  Session.set("loginMsg", "");
}

Router.onBeforeAction('loading');
Router.onBeforeAction(clearErrors);
Router.onBeforeAction(requireLogin, {only: ['admin', 'postSubmit', 'userEdit']});
<|endoftext|>


<commit_before>/**
 * Created by JackP on 10/8/2017.
 */
import React from 'react'
import {Field} from 'react-final-form'
import Toggle from 'react-toggle-button'
import styled from 'styled-components'
import Select from 'react-select';

const sortTypes = [
    {value: 'lastAdded', label: 'Last Added'},
    {value: 'lastUpdated', label: 'Last Updated'},
    {value: 'service_visibleName', label: 'By Name and Service'},
    {value: 'visibleName', label: 'By Name'},
]

const ToggleAdapter = ({input: {onChange, name, value}, toggle, label, ...rest}) => (
    <Toggle
        value={value}
        onToggle={(value) => {
            toggle(!value, name);
            onChange(!value)
        }}
        inactiveLabel={''}
        activeLabel={''}
        {...rest}
    />
)

const TextField = ({input, changeSetting, ...rest}) => (
    <InputField
        {...input}
        type="password"
        onBlur={(e) => {
            const value = e.target.value;
            const name = input.name
            input.onBlur(value);
            changeSetting(value, name)
        }}
    />
)

const ReactSelectAdapter = ({input, select, ...rest}) => (
    <Select {...input} {...rest} onChange={(selected) => {
        input.onChange(selected.value)
        select(selected.value, input.name)
    }} searchable/>
)

const SettingsForm = ({handleSubmit, pristine, reset, submitting, changeSetting, values}) => (
    <Form onSubmit={handleSubmit}>
        <FieldWrapper>
            LQ
            <InputWrapper>
                <Field
                    name="LQ"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper>
            Show Notifications
            <InputWrapper>
                <Field
                    name="showNotifications"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper>
            Minimize At Start
            <InputWrapper>
                <Field
                    name="minimizeAtStart"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper>
            Play On Balloon Click
            <InputWrapper>
                <Field
                    name="launchOnBalloonClick"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>

        <FieldWrapper>
            Night Mode
            <InputWrapper>
                <Field
                    name="nightMode"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper>
            Show tooltips
            <InputWrapper>
                <Field
                    name="showTooltip"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper full>
            Youtube Api Key
            <InputWrapper>
                <Field
                    name="youtubeApiKey"
                    component={TextField}
                    type="password"
                    changeSetting={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>

        <SelectWrapper>
            Sort Mode
            <SelectField
                name="sortType"
                component={ReactSelectAdapter}
                options={sortTypes}
                select={changeSetting}

            />
        </SelectWrapper>
        <FieldWrapper>
            Reversed Sort
            <InputWrapper>
                <Field
                    name="sortReverse"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
    </Form>
)

const Form = styled.form`
    display: flex;
    flex-direction: column;
    height: 100%;

`

const SelectWrapper = styled.div`
    margin: 2px 20px;
`

const SelectField = styled(Field)`
    margin-bottom: 20px;
`

const FieldWrapper = styled.div`
    display: flex;
    flex-direction: ${props => !!props.full ? 'column' : 'row' };
    justify-content: space-between;
    margin: 2px 20px;
`

const Label = styled.label`
    font-weight: bold;
    font-size: 15px;
    color: ${props => props.theme.client.color};
    padding-bottom:10px;
`

const InputWrapper = styled.div`

`

const InputField = styled.input`
    width: 100%;
`

export default SettingsForm
<commit_msg>Removed clearable for sortType<commit_after>/**
 * Created by JackP on 10/8/2017.
 */
import React from 'react'
import {Field} from 'react-final-form'
import Toggle from 'react-toggle-button'
import styled from 'styled-components'
import Select from 'react-select';

const sortTypes = [
    {value: 'lastAdded', label: 'Last Added'},
    {value: 'lastUpdated', label: 'Last Updated'},
    {value: 'service_visibleName', label: 'By Name and Service'},
    {value: 'visibleName', label: 'By Name'},
]

const ToggleAdapter = ({input: {onChange, name, value}, toggle, label, ...rest}) => (
    <Toggle
        value={value}
        onToggle={(value) => {
            toggle(!value, name);
            onChange(!value)
        }}
        inactiveLabel={''}
        activeLabel={''}
        {...rest}
    />
)

const TextField = ({input, changeSetting, ...rest}) => (
    <InputField
        {...input}
        type="password"
        onBlur={(e) => {
            const value = e.target.value;
            const name = input.name
            input.onBlur(value);
            changeSetting(value, name)
        }}
    />
)

const ReactSelectAdapter = ({input, select, ...rest}) => (
    <Select
        {...input}
        {...rest}
        onChange={(selected) => {
            input.onChange(selected.value)
            select(selected.value, input.name)
        }}
        clearable={false}
        searchable={false}
    />
)

const SettingsForm = ({handleSubmit, pristine, reset, submitting, changeSetting, values}) => (
    <Form onSubmit={handleSubmit}>
        <FieldWrapper>
            LQ
            <InputWrapper>
                <Field
                    name="LQ"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper>
            Show Notifications
            <InputWrapper>
                <Field
                    name="showNotifications"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper>
            Minimize At Start
            <InputWrapper>
                <Field
                    name="minimizeAtStart"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper>
            Play On Balloon Click
            <InputWrapper>
                <Field
                    name="launchOnBalloonClick"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>

        <FieldWrapper>
            Night Mode
            <InputWrapper>
                <Field
                    name="nightMode"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper>
            Show Tooltips
            <InputWrapper>
                <Field
                    name="showTooltips"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
        <FieldWrapper full>
            Youtube Api Key
            <InputWrapper>
                <Field
                    name="youtubeApiKey"
                    component={TextField}
                    type="password"
                    changeSetting={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>

        <SelectWrapper>
            Sort Mode
            <SelectField
                name="sortType"
                component={ReactSelectAdapter}
                options={sortTypes}
                select={changeSetting}

            />
        </SelectWrapper>
        <FieldWrapper>
            Reversed Sort
            <InputWrapper>
                <Field
                    name="sortReverse"
                    component={ToggleAdapter}
                    toggle={changeSetting}
                />
            </InputWrapper>
        </FieldWrapper>
    </Form>
)

const Form = styled.form`
    display: flex;
    flex-direction: column;
    height: 100%;

`

const SelectWrapper = styled.div`
    margin: 2px 20px;
`

const SelectField = styled(Field)`
    margin-bottom: 20px;
`

const FieldWrapper = styled.div`
    display: flex;
    flex-direction: ${props => !!props.full ? 'column' : 'row' };
    justify-content: space-between;
    margin: 2px 20px;
`

const Label = styled.label`
    font-weight: bold;
    font-size: 15px;
    color: ${props => props.theme.client.color};
    padding-bottom:10px;
`

const InputWrapper = styled.div`

`

const InputField = styled.input`
    width: 100%;
`

export default SettingsForm
<|endoftext|>