UNPKG

access-controls

Version:

rule based access-controls engine for node.js (browser compatible)

1,631 lines (1,399 loc) 325 kB
!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`,