access-controls
Version:
rule based access-controls engine for node.js (browser compatible)
1,631 lines (1,399 loc) • 325 kB
JavaScript
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var o;"undefined"!=typeof window?o=window:"undefined"!=typeof global?o=global:"undefined"!=typeof self&&(o=self),o.AccessControls=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
module.exports = _dereq_('./lib/AccessControlProcedure.js')
},{"./lib/AccessControlProcedure.js":3}],2:[function(_dereq_,module,exports){
var setImmediateShim = setTimeout
// browser shim
if(typeof setImmediate !== 'undefined') {
setImmediateShim = setImmediate
}
// var perm = {
// acl: {
// roles: ['emea'],
// control: 'required',
// actions: 'rw',
// conditions: [{
// attributes: {
// 'region': 'emea'
// }
// }
// ]
// }
// }
var _ = _dereq_('lodash')
var OTParser = _dereq_('object-tree')
function AccessControlList(conf) {
if(!conf) { throw new Error ('missing configuration') }
if(!conf.roles) { throw new Error('roles is required') }
if(!_.isArray(conf.roles)) { throw new Error('roles should be a string array') }
this._roles = conf.roles
this._name = conf.name || JSON.stringify(conf.roles)
this._hard = conf.hard || false
if(!conf.control) { throw new Error('control is required') }
this._control = conf.control
// create a map for O(1) speed
this._actions = {}
if(conf.actions && _.isArray(conf.actions)) {
for(var i = 0 ; i < conf.actions.length ; i++) {
this._actions[conf.actions[i]] = true
}
}
if(conf.filters) {
this._filters = conf.filters
}
this._conditions = conf.conditions || []
if (conf.inherit) {
this._inherit = conf.inherit
}
this._objectParser = new OTParser()
}
AccessControlList.prototype.shouldApplyInherited = function(obj, action, roles, context) {
var inheritApply = {
ok: true
}
var self = this
if (self._inherit && context.inherit$) {
var skip = _.any(self._inherit.allow, function(allow) {
var rolesMatch = self._rolesMatch(allow.roles, roles)
if (rolesMatch.ok) {
var entityMatch = _.findWhere(allow.entities, context.inherit$.entityDef)
if (entityMatch) {
return _.all(allow.conditions, function(condition) {
var match = self._conditionMatch(condition, context.inherit$.entity, action, context)
return (match.ok === true)
})
}
else {
return false
}
}
else {
return false
}
})
if (skip) {
inheritApply.ok = false
inheritApply.reason = 'inherit pass through'
}
}
return inheritApply
}
AccessControlList.prototype.shouldApply = function(obj, action) {
var shouldApply = {
ok: true
}
if(!this._actionMatch(action)) {
shouldApply.ok = false
shouldApply.reason = 'action does not match'
}
return shouldApply
}
AccessControlList.prototype._actionMatch = function(intendedAction) {
return this._actions[intendedAction] === true
}
/**
* returns:
* - obj.ok=true if the rule should apply and the conditions match the context
* - obj.ok=false if the rule should apply and the conditions don't match the context (access denied)
* - obj.ok=undefined if the conditions don't match the object (rule should not apply)
*/
AccessControlList.prototype._conditionsMatch = function(obj, action, context) {
var totalMatch = {
ok: true
}
for(var i = 0 ; i < this._conditions.length ; i++) {
var condition = this._conditions[i]
var match = this._conditionMatch(condition, obj, action, context)
if(match.ok === undefined) {
totalMatch.ok = undefined
} else if(match.ok === false && totalMatch.ok !== undefined) {
totalMatch.ok = false
if(match.reason) {
totalMatch.reason = match.reason
} else {
totalMatch.reason = 'Condition #'+i+' does not match ==> '+ JSON.stringify(condition)
}
}
if(match.inherit) {
totalMatch.inherit = totalMatch.inherit || []
totalMatch.inherit = totalMatch.inherit.concat(match.inherit)
}
}
return totalMatch
}
AccessControlList.prototype._filter = function(obj) {
if(this._filters) {
var filters = []
for(var attr in this._filters) {
var filter = this._applyFilter(this._filters[attr], obj, attr)
filters.push(filter)
}
return filters
}
}
AccessControlList.prototype._applyFilter = function(filter, obj, attribute) {
var filterResult = {}
filterResult.attribute = attribute
// foo: false => foo denied
if(!filter) {
filterResult.access = 'denied'
filterResult.originalValue = obj[attribute]
} else
// foo: [...] => foo denied if (new) value is in specified array of values
// used with write operations to disallow certain values to be set on a field
// doesn't make much sense for read operations, but if used with reads it will
// stop certain values from being returned
if(_.isArray(filter)) {
filterResult.originalValue = obj[attribute]
if (~filter.indexOf(obj[attribute])) {
filterResult.access = 'denied'
}
else {
// if value is not in specified array of values, allow it through
}
} else
// custom filter function
// only works for read operations, replaces original field value with
// the value returned from the custom filtration function
// when used with write operations the returned value has no effect
// and access to field will be always denied
if(_.isFunction(filter)) {
filterResult.access = 'partial'
filterResult.originalValue = obj[attribute]
filterResult.filteredValue = filter(obj[attribute])
} else
// "mask" filter for read operations
// if positive will replace first N characters with *
// if negative will replace last N characters with *
if(_.isNumber(filter)) {
// only works with strings, for non-string values simply denies access
if(_.isString(obj[attribute])) {
filterResult.access = 'partial'
filterResult.originalValue = obj[attribute]
var fullMask = filterResult.originalValue.replace(/./g, '*')
var maskedValue
if(filter > 0) {
// N = filter
// mask all but the 'N' first characters
maskedValue = filterResult.originalValue.substr(0, filter)
if(fullMask.length > filter) {
maskedValue += fullMask.substr(filter)
}
} else if(filter < 0) {
// N = filter
// mask all but the 'N' last characters
maskedValue = filterResult.originalValue.substr(filter)
if(fullMask.length > (-filter)) {
maskedValue = fullMask.substr(0, fullMask.length + filter) + maskedValue
}
}
filterResult.filteredValue = maskedValue
} else {
filterResult.access = 'denied'
if(!filter) {
filterResult.reason = 'trying to apply a replace filter on a falsy value'
} else {
filterResult.reason = 'trying to apply a replace filter on a value that is not a string'
}
filterResult.originalValue = obj[attribute]
if (typeof console !== 'undefined') {
console.warn('Denying access to field ['+attribute+'].', filterResult.reason)
}
}
} else {
throw new Error('unsupported filter', filter)
}
return filterResult
}
AccessControlList.prototype._conditionMatch = function(condition, obj, action, context) {
var match = {ok: true}
// at least one common element between expected and actual
var oneMatch = function(expected, actual) {
if (_.isUndefined(expected) || _.isUndefined(actual)) {
return false
}
expected = _.isArray(expected) ? expected : [expected]
actual = _.isArray(actual) ? actual : [actual]
for (var i=0; i<expected.length; i++) {
for (var j=0; j<actual.length; j++) {
if (expected[i] === actual[j]) {
return true
}
}
}
return false
}
if(condition.attributes) {
for(var attr in condition.attributes) {
if(condition.attributes.hasOwnProperty(attr)) {
var areEqual
var invertedCondition = false
var expectedValue = condition.attributes[attr]
if(attr.indexOf('!') === 0) {
attr = attr.slice(1)
invertedCondition = true
}
var actualValue
if (obj.original$) {
actualValue = this._objectParser.lookup(attr, obj.original$)
}
else {
actualValue = this._objectParser.lookup(attr, obj)
}
if(this._objectParser.isTemplate(expectedValue)) {
// Check to see if this is a NOT template. This will happen if it starts with {!
// In order for the template to work replace the {! with { and set a flag that
// we are inverting the following logic
if(expectedValue.indexOf('{!') === 0) {
invertedCondition = true
expectedValue = '{' + expectedValue.substr(2)
}
expectedValue = this._objectParser.lookupTemplate(expectedValue, context)
areEqual = oneMatch(expectedValue, actualValue)
if(invertedCondition) {
if (areEqual) {
match.ok = false
match.reason = 'Attr [' + attr + '] should not be [' + actualValue + '] but is in [' + expectedValue + ']'
}
} else {
if (!areEqual) {
match.ok = false
match.reason = 'Attr [' + attr + '] should be [' + actualValue + '] but is not in [' + expectedValue + ']'
}
}
} else {
if (expectedValue === null) { // special handling when expecting value null (literal)
var bothNull = (actualValue === undefined || actualValue === null)
if (invertedCondition) { // inverted
if (bothNull) { // left null, right null
// TODO: review
// irregular behaviour here when using the bang on the attr name and literal null as the expectected value
// returns false here if actual value is null when normally it should return undefined
match.ok = false
match.reason = 'Condition do not apply. Truthy value expected for attr ['+attr+'] but got ['+actualValue+']'
return match
}
else {
// if the condition matches then match.ok should preserve its current value
}
}
else { // normal
if (!bothNull) { // left null, right !null
// TODO: review
// figure out why we're returning false on the inverted path above
// might need to return false here as well
match.ok = undefined // this ACL should not apply to this object
match.reason = 'Condition do not apply. Attr ['+attr+'] should be ['+expectedValue+'] but is ['+actualValue+']'
return match
}
else { // left null, right null
// nothing to do here. condition matches, match.ok preserves its current value
// left overs from v0.5.2:
//match.reason = 'falsy value expected'
}
}
}
else {
areEqual = oneMatch(expectedValue, actualValue)
if (invertedCondition) { // inverted
if (areEqual) {
// !!! not handled in v0.5.2
match.ok = undefined
match.reason = 'Condition do not apply. Attr ['+attr+'] should *NOT* be ['+expectedValue+'] but is ['+actualValue+']'
return match
}
else {
// if the condition matches then match.ok should preserve its current value
// !!! removed:
//match.ok = true
// as match.ok should is initialized with true and should only be set to false or undefined while evaluating attrs
// more left overs from v0.5.2:
//match.reason = 'Condition match. Attr ['+attr+'] should *NOT* be ['+expectedValue+'] and is ['+actualValue+']'
}
}
else { // normal
if (!areEqual) {
match.ok = undefined // this ACL should not apply to this object
match.reason = 'Condition do not apply. Attr ['+attr+'] should be ['+expectedValue+'] but is ['+actualValue+']'
return match
}
else {
// nothing to do here. condition matches, match.ok preserves its current value
}
}
}
}
}
}
} else if (condition.fn) {
var result = condition.fn(obj, context)
if (result.ok !== true) {
match.ok = result.ok
match.reason = result.reason
}
} else if(/^\{(.+\/){0,2}.*::.*\}$/.test(condition)) {
// match {-/-/foobar::path.to.attr} or {-/foobar::path.to.attr} etc.
var data = condition.slice(1, condition.length-1).split('::')
var referencedId = this._objectParser.lookup(data[1], obj)
var typeData = data[0].split('/')
if(!referencedId) {
// shortcut to denial if the reference does not exist, we cannot inherit its permissions
match.ok = false
match.reason = 'Authorization should be inherited from field ['+data[1]+'] but the field is falsy'
return match
} else {
match.inherit = match.inherit || []
var inheritance = {
entity: {
},
id: referencedId
}
if(typeData[2]) {
inheritance.entity.zone = typeData[0]
inheritance.entity.base = typeData[1]
inheritance.entity.name = typeData[2]
} else if(typeData[1] && typeData[0] !== '-') {
inheritance.entity.base = typeData[0]
inheritance.entity.name = typeData[1]
} else {
inheritance.entity.name = typeData[0]
}
match.inherit.push(inheritance)
}
}
return match
}
AccessControlList.prototype.authorize = function(obj, action, roles, context, callback) {
var authorize = false
var reason = ''
var inherit = []
var shouldApply = this.shouldApply(obj, action)
var filters = null
var hard = this._hard
var missing = null
if(shouldApply.ok) {
var conditionsMatch = this._conditionsMatch(obj, action, context)
if(conditionsMatch.inherit) {
inherit = inherit.concat(conditionsMatch.inherit)
}
if(conditionsMatch.ok === false) {
reason = conditionsMatch.reason
if(this.control() === 'filter') {
reason = 'skipping filter because the conditions do not match'
authorize = true
} else {
authorize = false
}
} else if(conditionsMatch.ok === true) {
var rolesMatch = this._rolesMatch(this._roles, roles)
reason = rolesMatch.reason
missing = rolesMatch.missing
if(!rolesMatch.ok && this.control() === 'filter') {
reason = 'applying filter because the roles do not match'
filters = this._filter(obj)
authorize = true
} else {
authorize = rolesMatch.ok
}
} else {
// conditions say this ACL does not apply
if(this.control() === 'sufficient') {
authorize = false
} else {
authorize = true
}
reason = conditionsMatch.reason || 'ACL conditions do not apply'
}
} else {
reason = shouldApply.reason
authorize = true
}
setImmediateShim(function() {
callback(undefined, {
authorize: authorize,
reason: reason,
inherit: inherit,
filters: filters,
hard: hard,
missingRoles: missing
})
})
}
AccessControlList.prototype._rolesMatch = function(expectedRoles, actualRoles) {
var rolesMatch = {ok: true}
var missingRoles = []
if(expectedRoles && expectedRoles.length > 0) {
// TODO: optimize this O(N square) into at least a O(N)
for(var i = 0 ; i < expectedRoles.length ; i++) {
var match = false
if(actualRoles) {
for(var j = 0 ; j < actualRoles.length ; j++) {
if(actualRoles[j] === expectedRoles[i]) {
match = true
break
}
}
}
if(!match) {
missingRoles.push(expectedRoles[i])
}
}
}
if(missingRoles.length > 0) {
rolesMatch.ok = false
rolesMatch.missing = missingRoles
rolesMatch.reason = 'expected roles ' + JSON.stringify(expectedRoles) +
' but got roles ' + JSON.stringify(actualRoles) +
'. missing roles ' + JSON.stringify(missingRoles)
} else if(rolesMatch.ok) {
rolesMatch.reason = 'roles match as expected: ' + expectedRoles.join(',')
}
return rolesMatch
}
AccessControlList.prototype.roles = function() {
return this._roles
}
AccessControlList.prototype.control = function() {
return this._control
}
AccessControlList.prototype.name = function() {
return this._name
}
AccessControlList.prototype.hard = function() {
return this._hard
}
AccessControlList.prototype.toString = function() {
return 'ACL::' + this._name
}
module.exports = AccessControlList
},{"lodash":5,"object-tree":6}],3:[function(_dereq_,module,exports){
var _ = _dereq_('lodash')
var ACL = _dereq_('../lib/AccessControlList.js')
var patrun = _dereq_('patrun')
/** An access control procedure runs a set of ACLs against a given pair of <entity> and <action>
*/
function AccessControlProcedure(acls) {
this._accessControls = []
if(acls) {
this.addAccessControls(acls)
}
}
AccessControlProcedure.ACL = ACL
AccessControlProcedure.generateActionsMapping = function(accessControls) {
var mapping = patrun()
for(var i = 0 ; i < accessControls.length ; i++) {
var aclDefinition = accessControls[i]
for(var j = 0 ; j < aclDefinition.entities.length ; j++) {
var actions = aclDefinition.actions
for(var k = 0 ; k < actions.length ; k++) {
var argsMatching = _.clone(aclDefinition.entities[j])
argsMatching.role = 'entity'
// TODO: differentiate create from update
switch(actions[k]) {
case 'save':
case 'save_new':
case 'save_existing':
argsMatching.cmd = 'save'
break
case 'load':
argsMatching.cmd = 'load'
break
case 'list':
argsMatching.cmd = 'list'
break
case 'remove':
argsMatching.cmd = 'remove'
break
default:
throw new Error('unsupported action ['+actions[k]+'] in ' + JSON.stringify(aclDefinition))
}
var aclProcedure = mapping.find(argsMatching)
if(!aclProcedure) {
aclProcedure = new AccessControlProcedure()
mapping.add(argsMatching, aclProcedure)
}
aclProcedure.addAccessControls(aclDefinition)
}
}
}
return mapping
}
/**
* mapping: patrun mapping returned by AccessControlProcedure.generateActionsMapping()
* entityDef: { zone: ..., base: ..., name: ... }
* action: 'load' | 'list' | 'save' | 'remove'
*/
AccessControlProcedure.getProcedureForEntity = function(mapping, entityDef, action) {
return mapping.find({role: 'entity', zone: entityDef.zone, base: entityDef.base, name: entityDef.name, cmd: action})
}
AccessControlProcedure.prototype.addAccessControls = function(acl) {
if(_.isArray(acl)) {
for(var i = 0 ; i < acl.length ; i++) {
this.addAccessControls(acl[i])
}
} else if(_.isObject(acl)) {
this._accessControls.push(new ACL(acl))
} else {
throw new Error('unsuported ACL object type: ' + typeof acl)
}
}
AccessControlProcedure.prototype.authorize = function(obj, action, roles, context, callback) {
this._nextACL(obj, action, roles, this._accessControls.slice(0), context, undefined, function(err, details) {
callback(err, details)
})
}
AccessControlProcedure.prototype._nextACL = function(obj, action, roles, accessControls, context, details, callback) {
if(!details) {
details = {authorize: true}
}
if(!details.history) {
details.history = []
}
if(!details.inherit) {
details.inherit = []
}
if(!details.summary) {
details.summary = []
}
details.context = context
details.roles = roles
details.action = action
details.hard = details.hard || false
var self = this
if(accessControls && accessControls.length > 0) {
var accessControl = accessControls.shift()
var shouldApply = accessControl.shouldApply(obj, action)
// inherited acls can be bypassed via inherit.allow on the acl definition
if (shouldApply.ok && context.inherit$) {
shouldApply = accessControl.shouldApplyInherited(obj, action, roles, context)
}
if(shouldApply.ok) {
//console.log('running authorization service', accessControl.name())
accessControl.authorize(obj, action, roles, context, function(err, result) {
details.history.push({
service: accessControl.name(),
authorize: result ? result.authorize : null,
control: accessControl.control(),
err: err || null,
reason: result ? result.reason : null,
hard: result ? result.hard : null
})
//console.log(obj, action, roles, JSON.stringify(result))
if(err || !result) {
details.authorize = false
callback(err, details)
}
if(result.inherit) {
details.inherit = details.inherit.concat(result.inherit)
}
var stop = false
switch(accessControl.control()) {
case 'filter':
if(!details.filters) {
details.filters = []
}
if(result.filters) {
details.filters = details.filters.concat(result.filters)
}
break
case 'requisite':
if(!result.authorize) {
details.hard = details.hard || result.hard
details.authorize = false
stop = true
details.summary.push({
service: accessControl.name(),
reason: result.reason,
missingRoles: result.missingRoles
})
}
break
case 'required':
if(!result.authorize) {
details.hard = details.hard || result.hard
details.authorize = false
details.summary.push({
service: accessControl.name(),
reason: result.reason,
missingRoles: result.missingRoles
})
}
break
case 'sufficient':
if(result.authorize) {
details.authorize = true
stop = true
details.summary = [{
service: accessControl.name(),
reason: result.reason
}]
}
break
}
if(stop) {
callback(undefined, details)
} else {
self._nextACL(obj, action, roles, accessControls, context, details, callback)
}
})
} else {
//console.log('ignoring authorization service', accessControl.name(), '. reason:', shouldApply.reason)
self._nextACL(obj, action, roles, accessControls, context, details, callback)
}
} else {
callback(undefined, details)
}
}
AccessControlProcedure.prototype.applyFilters = function(filters, obj, action) {
var filterType = 'read'
switch(action) {
case 'save':
case 'save_new':
case 'save_existing':
filterType = 'write'
break
//case 'load':
//case 'list':
default:
filterType = 'read'
break
}
if(filters && filters.length > 0) {
for(var i = 0 ; i < filters.length ; i++) {
var filter = filters[i]
switch(filter.access) {
case 'denied':
delete obj[filter.attribute]
break
case 'partial':
if(filterType === 'read') {
obj[filter.attribute] = filter.filteredValue
} else {
delete obj[filter.attribute]
}
break
}
}
}
}
module.exports = AccessControlProcedure
},{"../lib/AccessControlList.js":2,"lodash":5,"patrun":9}],4:[function(_dereq_,module,exports){
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
function EventEmitter() {
this._events = this._events || {};
this._maxListeners = this._maxListeners || undefined;
}
module.exports = EventEmitter;
// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.prototype._events = undefined;
EventEmitter.prototype._maxListeners = undefined;
// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
EventEmitter.defaultMaxListeners = 10;
// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function(n) {
if (!isNumber(n) || n < 0 || isNaN(n))
throw TypeError('n must be a positive number');
this._maxListeners = n;
return this;
};
EventEmitter.prototype.emit = function(type) {
var er, handler, len, args, i, listeners;
if (!this._events)
this._events = {};
// If there is no 'error' event listener then throw.
if (type === 'error') {
if (!this._events.error ||
(isObject(this._events.error) && !this._events.error.length)) {
er = arguments[1];
if (er instanceof Error) {
throw er; // Unhandled 'error' event
}
throw TypeError('Uncaught, unspecified "error" event.');
}
}
handler = this._events[type];
if (isUndefined(handler))
return false;
if (isFunction(handler)) {
switch (arguments.length) {
// fast cases
case 1:
handler.call(this);
break;
case 2:
handler.call(this, arguments[1]);
break;
case 3:
handler.call(this, arguments[1], arguments[2]);
break;
// slower
default:
len = arguments.length;
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
handler.apply(this, args);
}
} else if (isObject(handler)) {
len = arguments.length;
args = new Array(len - 1);
for (i = 1; i < len; i++)
args[i - 1] = arguments[i];
listeners = handler.slice();
len = listeners.length;
for (i = 0; i < len; i++)
listeners[i].apply(this, args);
}
return true;
};
EventEmitter.prototype.addListener = function(type, listener) {
var m;
if (!isFunction(listener))
throw TypeError('listener must be a function');
if (!this._events)
this._events = {};
// To avoid recursion in the case that type === "newListener"! Before
// adding it to the listeners, first emit "newListener".
if (this._events.newListener)
this.emit('newListener', type,
isFunction(listener.listener) ?
listener.listener : listener);
if (!this._events[type])
// Optimize the case of one listener. Don't need the extra array object.
this._events[type] = listener;
else if (isObject(this._events[type]))
// If we've already got an array, just append.
this._events[type].push(listener);
else
// Adding the second element, need to change to array.
this._events[type] = [this._events[type], listener];
// Check for listener leak
if (isObject(this._events[type]) && !this._events[type].warned) {
var m;
if (!isUndefined(this._maxListeners)) {
m = this._maxListeners;
} else {
m = EventEmitter.defaultMaxListeners;
}
if (m && m > 0 && this._events[type].length > m) {
this._events[type].warned = true;
console.error('(node) warning: possible EventEmitter memory ' +
'leak detected. %d listeners added. ' +
'Use emitter.setMaxListeners() to increase limit.',
this._events[type].length);
if (typeof console.trace === 'function') {
// not supported in IE 10
console.trace();
}
}
}
return this;
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.once = function(type, listener) {
if (!isFunction(listener))
throw TypeError('listener must be a function');
var fired = false;
function g() {
this.removeListener(type, g);
if (!fired) {
fired = true;
listener.apply(this, arguments);
}
}
g.listener = listener;
this.on(type, g);
return this;
};
// emits a 'removeListener' event iff the listener was removed
EventEmitter.prototype.removeListener = function(type, listener) {
var list, position, length, i;
if (!isFunction(listener))
throw TypeError('listener must be a function');
if (!this._events || !this._events[type])
return this;
list = this._events[type];
length = list.length;
position = -1;
if (list === listener ||
(isFunction(list.listener) && list.listener === listener)) {
delete this._events[type];
if (this._events.removeListener)
this.emit('removeListener', type, listener);
} else if (isObject(list)) {
for (i = length; i-- > 0;) {
if (list[i] === listener ||
(list[i].listener && list[i].listener === listener)) {
position = i;
break;
}
}
if (position < 0)
return this;
if (list.length === 1) {
list.length = 0;
delete this._events[type];
} else {
list.splice(position, 1);
}
if (this._events.removeListener)
this.emit('removeListener', type, listener);
}
return this;
};
EventEmitter.prototype.removeAllListeners = function(type) {
var key, listeners;
if (!this._events)
return this;
// not listening for removeListener, no need to emit
if (!this._events.removeListener) {
if (arguments.length === 0)
this._events = {};
else if (this._events[type])
delete this._events[type];
return this;
}
// emit removeListener for all listeners on all events
if (arguments.length === 0) {
for (key in this._events) {
if (key === 'removeListener') continue;
this.removeAllListeners(key);
}
this.removeAllListeners('removeListener');
this._events = {};
return this;
}
listeners = this._events[type];
if (isFunction(listeners)) {
this.removeListener(type, listeners);
} else {
// LIFO order
while (listeners.length)
this.removeListener(type, listeners[listeners.length - 1]);
}
delete this._events[type];
return this;
};
EventEmitter.prototype.listeners = function(type) {
var ret;
if (!this._events || !this._events[type])
ret = [];
else if (isFunction(this._events[type]))
ret = [this._events[type]];
else
ret = this._events[type].slice();
return ret;
};
EventEmitter.listenerCount = function(emitter, type) {
var ret;
if (!emitter._events || !emitter._events[type])
ret = 0;
else if (isFunction(emitter._events[type]))
ret = 1;
else
ret = emitter._events[type].length;
return ret;
};
function isFunction(arg) {
return typeof arg === 'function';
}
function isNumber(arg) {
return typeof arg === 'number';
}
function isObject(arg) {
return typeof arg === 'object' && arg !== null;
}
function isUndefined(arg) {
return arg === void 0;
}
},{}],5:[function(_dereq_,module,exports){
(function (global){
/**
* @license
* Lo-Dash 2.4.1 (Custom Build) <http://lodash.com/>
* Build: `lodash modern -o ./dist/lodash.js`
* Copyright 2012-2013 The Dojo Foundation <http://dojofoundation.org/>
* Based on Underscore.js 1.5.2 <http://underscorejs.org/LICENSE>
* Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
* Available under MIT license <http://lodash.com/license>
*/
;(function() {
/** Used as a safe reference for `undefined` in pre ES5 environments */
var undefined;
/** Used to pool arrays and objects used internally */
var arrayPool = [],
objectPool = [];
/** Used to generate unique IDs */
var idCounter = 0;
/** Used to prefix keys to avoid issues with `__proto__` and properties on `Object.prototype` */
var keyPrefix = +new Date + '';
/** Used as the size when optimizations are enabled for large arrays */
var largeArraySize = 75;
/** Used as the max size of the `arrayPool` and `objectPool` */
var maxPoolSize = 40;
/** Used to detect and test whitespace */
var whitespace = (
// whitespace
' \t\x0B\f\xA0\ufeff' +
// line terminators
'\n\r\u2028\u2029' +
// unicode category "Zs" space separators
'\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000'
);
/** Used to match empty string literals in compiled template source */
var reEmptyStringLeading = /\b__p \+= '';/g,
reEmptyStringMiddle = /\b(__p \+=) '' \+/g,
reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g;
/**
* Used to match ES6 template delimiters
* http://people.mozilla.org/~jorendorff/es6-draft.html#sec-literals-string-literals
*/
var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g;
/** Used to match regexp flags from their coerced string values */
var reFlags = /\w*$/;
/** Used to detected named functions */
var reFuncName = /^\s*function[ \n\r\t]+\w/;
/** Used to match "interpolate" template delimiters */
var reInterpolate = /<%=([\s\S]+?)%>/g;
/** Used to match leading whitespace and zeros to be removed */
var reLeadingSpacesAndZeros = RegExp('^[' + whitespace + ']*0+(?=.$)');
/** Used to ensure capturing order of template delimiters */
var reNoMatch = /($^)/;
/** Used to detect functions containing a `this` reference */
var reThis = /\bthis\b/;
/** Used to match unescaped characters in compiled string literals */
var reUnescapedString = /['\n\r\t\u2028\u2029\\]/g;
/** Used to assign default `context` object properties */
var contextProps = [
'Array', 'Boolean', 'Date', 'Function', 'Math', 'Number', 'Object',
'RegExp', 'String', '_', 'attachEvent', 'clearTimeout', 'isFinite', 'isNaN',
'parseInt', 'setTimeout'
];
/** Used to make template sourceURLs easier to identify */
var templateCounter = 0;
/** `Object#toString` result shortcuts */
var argsClass = '[object Arguments]',
arrayClass = '[object Array]',
boolClass = '[object Boolean]',
dateClass = '[object Date]',
funcClass = '[object Function]',
numberClass = '[object Number]',
objectClass = '[object Object]',
regexpClass = '[object RegExp]',
stringClass = '[object String]';
/** Used to identify object classifications that `_.clone` supports */
var cloneableClasses = {};
cloneableClasses[funcClass] = false;
cloneableClasses[argsClass] = cloneableClasses[arrayClass] =
cloneableClasses[boolClass] = cloneableClasses[dateClass] =
cloneableClasses[numberClass] = cloneableClasses[objectClass] =
cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true;
/** Used as an internal `_.debounce` options object */
var debounceOptions = {
'leading': false,
'maxWait': 0,
'trailing': false
};
/** Used as the property descriptor for `__bindData__` */
var descriptor = {
'configurable': false,
'enumerable': false,
'value': null,
'writable': false
};
/** Used to determine if values are of the language type Object */
var objectTypes = {
'boolean': false,
'function': true,
'object': true,
'number': false,
'string': false,
'undefined': false
};
/** Used to escape characters for inclusion in compiled string literals */
var stringEscapes = {
'\\': '\\',
"'": "'",
'\n': 'n',
'\r': 'r',
'\t': 't',
'\u2028': 'u2028',
'\u2029': 'u2029'
};
/** Used as a reference to the global object */
var root = (objectTypes[typeof window] && window) || this;
/** Detect free variable `exports` */
var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports;
/** Detect free variable `module` */
var freeModule = objectTypes[typeof module] && module && !module.nodeType && module;
/** Detect the popular CommonJS extension `module.exports` */
var moduleExports = freeModule && freeModule.exports === freeExports && freeExports;
/** Detect free variable `global` from Node.js or Browserified code and use it as `root` */
var freeGlobal = objectTypes[typeof global] && global;
if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) {
root = freeGlobal;
}
/*--------------------------------------------------------------------------*/
/**
* The base implementation of `_.indexOf` without support for binary searches
* or `fromIndex` constraints.
*
* @private
* @param {Array} array The array to search.
* @param {*} value The value to search for.
* @param {number} [fromIndex=0] The index to search from.
* @returns {number} Returns the index of the matched value or `-1`.
*/
function baseIndexOf(array, value, fromIndex) {
var index = (fromIndex || 0) - 1,
length = array ? array.length : 0;
while (++index < length) {
if (array[index] === value) {
return index;
}
}
return -1;
}
/**
* An implementation of `_.contains` for cache objects that mimics the return
* signature of `_.indexOf` by returning `0` if the value is found, else `-1`.
*
* @private
* @param {Object} cache The cache object to inspect.
* @param {*} value The value to search for.
* @returns {number} Returns `0` if `value` is found, else `-1`.
*/
function cacheIndexOf(cache, value) {
var type = typeof value;
cache = cache.cache;
if (type == 'boolean' || value == null) {
return cache[value] ? 0 : -1;
}
if (type != 'number' && type != 'string') {
type = 'object';
}
var key = type == 'number' ? value : keyPrefix + value;
cache = (cache = cache[type]) && cache[key];
return type == 'object'
? (cache && baseIndexOf(cache, value) > -1 ? 0 : -1)
: (cache ? 0 : -1);
}
/**
* Adds a given value to the corresponding cache object.
*
* @private
* @param {*} value The value to add to the cache.
*/
function cachePush(value) {
var cache = this.cache,
type = typeof value;
if (type == 'boolean' || value == null) {
cache[value] = true;
} else {
if (type != 'number' && type != 'string') {
type = 'object';
}
var key = type == 'number' ? value : keyPrefix + value,
typeCache = cache[type] || (cache[type] = {});
if (type == 'object') {
(typeCache[key] || (typeCache[key] = [])).push(value);
} else {
typeCache[key] = true;
}
}
}
/**
* Used by `_.max` and `_.min` as the default callback when a given
* collection is a string value.
*
* @private
* @param {string} value The character to inspect.
* @returns {number} Returns the code unit of given character.
*/
function charAtCallback(value) {
return value.charCodeAt(0);
}
/**
* Used by `sortBy` to compare transformed `collection` elements, stable sorting
* them in ascending order.
*
* @private
* @param {Object} a The object to compare to `b`.
* @param {Object} b The object to compare to `a`.
* @returns {number} Returns the sort order indicator of `1` or `-1`.
*/
function compareAscending(a, b) {
var ac = a.criteria,
bc = b.criteria,
index = -1,
length = ac.length;
while (++index < length) {
var value = ac[index],
other = bc[index];
if (value !== other) {
if (value > other || typeof value == 'undefined') {
return 1;
}
if (value < other || typeof other == 'undefined') {
return -1;
}
}
}
// Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications
// that causes it, under certain circumstances, to return the same value for
// `a` and `b`. See https://github.com/jashkenas/underscore/pull/1247
//
// This also ensures a stable sort in V8 and other engines.
// See http://code.google.com/p/v8/issues/detail?id=90
return a.index - b.index;
}
/**
* Creates a cache object to optimize linear searches of large arrays.
*
* @private
* @param {Array} [array=[]] The array to search.
* @returns {null|Object} Returns the cache object or `null` if caching should not be used.
*/
function createCache(array) {
var index = -1,
length = array.length,
first = array[0],
mid = array[(length / 2) | 0],
last = array[length - 1];
if (first && typeof first == 'object' &&
mid && typeof mid == 'object' && last && typeof last == 'object') {
return false;
}
var cache = getObject();
cache['false'] = cache['null'] = cache['true'] = cache['undefined'] = false;
var result = getObject();
result.array = array;
result.cache = cache;
result.push = cachePush;
while (++index < length) {
result.push(array[index]);
}
return result;
}
/**
* Used by `template` to escape characters for inclusion in compiled
* string literals.
*
* @private
* @param {string} match The matched character to escape.
* @returns {string} Returns the escaped character.
*/
function escapeStringChar(match) {
return '\\' + stringEscapes[match];
}
/**
* Gets an array from the array pool or creates a new one if the pool is empty.
*
* @private
* @returns {Array} The array from the pool.
*/
function getArray() {
return arrayPool.pop() || [];
}
/**
* Gets an object from the object pool or creates a new one if the pool is empty.
*
* @private
* @returns {Object} The object from the pool.
*/
function getObject() {
return objectPool.pop() || {
'array': null,
'cache': null,
'criteria': null,
'false': false,
'index': 0,
'null': false,
'number': null,
'object': null,
'push': null,
'string': null,
'true': false,
'undefined': false,
'value': null
};
}
/**
* Releases the given array back to the array pool.
*
* @private
* @param {Array} [array] The array to release.
*/
function releaseArray(array) {
array.length = 0;
if (arrayPool.length < maxPoolSize) {
arrayPool.push(array);
}
}
/**
* Releases the given object back to the object pool.
*
* @private
* @param {Object} [object] The object to release.
*/
function releaseObject(object) {
var cache = object.cache;
if (cache) {
releaseObject(cache);
}
object.array = object.cache = object.criteria = object.object = object.number = object.string = object.value = null;
if (objectPool.length < maxPoolSize) {
objectPool.push(object);
}
}
/**
* Slices the `collection` from the `start` index up to, but not including,
* the `end` index.
*
* Note: This function is used instead of `Array#slice` to support node lists
* in IE < 9 and to ensure dense arrays are returned.
*
* @private
* @param {Array|Object|string} collection The collection to slice.
* @param {number} start The start index.
* @param {number} end The end index.
* @returns {Array} Returns the new array.
*/
function slice(array, start, end) {
start || (start = 0);
if (typeof end == 'undefined') {
end = array ? array.length : 0;
}
var index = -1,
length = end - start || 0,
result = Array(length < 0 ? 0 : length);
while (++index < length) {
result[index] = array[start + index];
}
return result;
}
/*--------------------------------------------------------------------------*/
/**
* Create a new `lodash` function using the given context object.
*
* @static
* @memberOf _
* @category Utilities
* @param {Object} [context=root] The context object.
* @returns {Function} Returns the `lodash` function.
*/
function runInContext(context) {
// Avoid issues with some ES3 environments that attempt to use values, named
// after built-in constructors like `Object`, for the creation of literals.
// ES5 clears this up by stating that literals must use built-in constructors.
// See http://es5.github.io/#x11.1.5.
context = context ? _.defaults(root.Object(), context, _.pick(root, contextProps)) : root;
/** Native constructor references */
var Array = context.Array,
Boolean = context.Boolean,
Date = context.Date,
Function = context.Function,
Math = context.Math,
Number = context.Number,
Object = context.Object,
RegExp = context.RegExp,
String = context.String,
TypeError = context.TypeError;
/**
* Used for `Array` method references.
*
* Normally `Array.prototype` would suffice, however, using an array literal
* avoids issues in Narwhal.
*/
var arrayRef = [];
/** Used for native method references */
var objectProto = Object.prototype;
/** Used to restore the original `_` reference in `noConflict` */
var oldDash = context._;
/** Used to resolve the internal [[Class]] of values */
var toString = objectProto.toString;
/** Used to detect if a method is native */
var reNative = RegExp('^' +
String(toString)
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/toString| for [^\]]+/g, '.*?') + '$'
);
/** Native method shortcuts */
var ceil = Math.ceil,
clearTimeout = context.clearTimeout,
floor = Math.floor,
fnToString = Function.prototype.toString,
getPrototypeOf = isNative(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf,
hasOwnProperty = objectProto.hasOwnProperty,
push = arrayRef.push,
setTimeout = context.setTimeout,
splice = arrayRef.splice,
unshift = arrayRef.unshift;
/** Used to set meta data on functions */
var defineProperty = (function() {
// IE 8 only accepts DOM elements
try {
var o = {},
func = isNative(func = Object.defineProperty) && func,
result = func(o, o, o) && func;
} catch(e) { }
return result;
}());
/* Native method shortcuts for methods with the same name as other `lodash` methods */
var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate,
nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray,
nativeIsFinite = context.isFinite,
nativeIsNaN = context.isNaN,
nativeKeys = isNative(nativeKeys = Object.keys) && nativeKeys,
nativeMax = Math.max,
nativeMin = Math.min,
nativeParseInt = context.parseInt,
nativeRandom = Math.random;
/** Used to lookup a built-in constructor by [[Class]] */
var ctorByClass = {};
ctorByClass[arrayClass] = Array;
ctorByClass[boolClass] = Boolean;
ctorByClass[dateClass] = Date;
ctorByClass[funcClass] = Function;
ctorByClass[objectClass] = Object;
ctorByClass[numberClass] = Number;
ctorByClass[regexpClass] = RegExp;
ctorByClass[stringClass] = String;
/*--------------------------------------------------------------------------*/
/**
* Creates a `lodash` object which wraps the given value to enable intuitive
* method chaining.
*
* In addition to Lo-Dash methods, wrappers also have the following `Array` methods:
* `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, `splice`,
* and `unshift`
*
* Chaining is supported in custom builds as long as the `value` method is
* implicitly or explicitly included in the build.
*
* The chainable wrapper functions are:
* `after`, `assign`, `bind`, `bindAll`, `bindKey`, `chain`, `compact`,
* `compose`, `concat`, `countBy`, `create`, `createCallback`, `curry`,
* `debounce`, `defaults`, `defer`, `delay`, `difference`, `filter`, `flatten`,
* `forEach`, `forEachRight`,