Created
September 5, 2011 22:01
-
-
Save BonsaiDen/1196020 to your computer and use it in GitHub Desktop.
Guard.js | Access control in JavaScript by using ES5 features | Sorry Safari, you're out for not support function.caller.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) 2011 Ivo Wetzel. | |
* | |
* 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(exports) { | |
var classId = 1, | |
classes = {}, | |
regex = /function\s([a-zA-Z_$]+[a-zA-Z$_0-9]*)\(([a-zA-Z,\s]+|)\)\s?\{/g; | |
var parent = null; | |
function shield(key, obj, id, clas) { | |
var level = key.match(/^(_{1,2}|)/)[0].length, | |
value = obj[key], | |
isMethod = typeof value === 'function'; | |
if (level === 0) { | |
return; | |
} | |
Object.defineProperty(obj, key, { | |
configurable: false, | |
enumerable: level < 2, | |
get: function gf() { | |
var cid = getClassId(gf, id, this); | |
if ((level === 1 && cid === -1) || (level === 2 && cid !== id)) { | |
throw new AccessError(key, clas, 'get', level, gf, value); | |
} else { | |
return value; | |
} | |
}, | |
set: function sf(to) { | |
if (isMethod || getClassId(sf, id, this) !== id) { | |
throw new AccessError(key, clas, 'set', level, sf, to); | |
} else { | |
value = to; | |
} | |
} | |
}); | |
} | |
function getClassId(f, id, proto) { | |
var callerId = f.caller && f.caller.___classId || -1, | |
all = classes[callerId], | |
other = classes[id]; | |
// Invalid on outer calls... or something like that | |
if (!all) { | |
parent = null; | |
} | |
// Watch calls into sub classes, in case those sub classes called the base class ctor | |
var base = (all && all.___bases[id] === true) || (other && parent === other.___name); | |
parent = all ? all.___name : null; | |
return (callerId === id || base) ? id : callerId; | |
} | |
function guard(clas, name) { | |
var id = classId++; | |
function GuardedClass() { | |
// Wrap new calls | |
if (this instanceof GuardedClass) { | |
// Add the class reference | |
addReadOnly(this, '___class', GuardedClass); | |
// Call the actual ctor | |
clas.apply(this, arguments); | |
// Protect member variables | |
for(var i in this) { | |
if (this.hasOwnProperty(i)) { | |
shield(i, this, id, GuardedClass); | |
} | |
} | |
// Make sure to keep track of "super" call via Base.prototype.call(this, ...); | |
} else { | |
// This makes access to base class members work | |
this.___class.___bases[GuardedClass.___id] = true; | |
// So in order to get the above you need to call the base | |
// ctor in your sub class ctor | |
clas.apply(this, arguments); | |
} | |
} | |
// Add classId to ctor and make it unchangeable | |
addReadOnly(clas, '___classId', id); | |
// Add internal state to the wrapper | |
addReadOnly(GuardedClass, '___id', id); | |
addReadOnly(GuardedClass, '___bases', {}); | |
addReadOnly(GuardedClass, '___name', name); | |
// Copy ENUMs and other things from the original ctor | |
for(var e in clas) { | |
if (clas.hasOwnProperty(e)) { | |
GuardedClass[e] = clas[e]; | |
// Kill off the original references | |
delete clas[e]; | |
} | |
} | |
// Create a clone of the prototype | |
GuardedClass.prototype = {}; | |
var proto = clas.prototype; | |
for(var i in proto) { | |
if (proto.hasOwnProperty(i)) { | |
var cloned; | |
if (typeof proto[i] === 'function') { | |
// Clone functions and patch inner functions to have the classId | |
// TODO find a cleaner way of doing this | |
var code = proto[i].toString(); | |
code = code.replace(regex, 'function $1($2) {$1.___classId = ' + id + ';'); | |
code = code.replace(/^[^\(]+/, 'function ' + i); | |
// Replace anonymous inner function names | |
code = code.replace(/function\s*?\(/g, function() { | |
return 'function ' + name.replace(/\./g, '') + i | |
+ 'innerFunction' + (++innerID) + '('; | |
}); | |
// TODO... patch ctor! | |
cloned = eval('(' + code + ')'); | |
addReadOnly(cloned, '___classId', id); | |
} else { | |
cloned = proto[i]; | |
} | |
GuardedClass.prototype[i] = cloned; | |
shield(i, GuardedClass.prototype, id, GuardedClass); | |
} | |
} | |
// Keep track of all the classes that are guarded | |
classes[id] = GuardedClass; | |
// We do not freeze here since enums and stuff on the class will | |
// silently fail to update | |
Object.seal(GuardedClass); | |
// Protect the prototype | |
Object.freeze(GuardedClass.prototype); | |
Object.seal(GuardedClass.prototype); | |
return GuardedClass; | |
} | |
function addReadOnly(obj, name, value) { | |
Object.defineProperty(obj, name, { | |
configurable: false, | |
enumerable: false, | |
value: value | |
}); | |
} | |
function AccessError(key, clas, method, level, f, value) { | |
var id = clas.___id, | |
cid = getClassId(f, id), | |
type = typeof value === 'function' ? 'method' : 'property'; | |
level = level === 1 ? 'protected' : 'private'; | |
method = method === 'get' ? 'GET' : 'SET'; | |
var message = method + ' of ' + level + ' ' + type + ' "' + key + '" of [' | |
+ clas.___name + ' (#' + id + ')] by '; | |
if (cid !== -1) { | |
message += '[' + classes[cid].___name + '.' + f.caller.name + ' (#' + cid + ')]'; | |
} else { | |
message += '[function ' + (f.caller.name || '(anonymous)') + ']'; | |
} | |
this.name = 'AccessError'; | |
this.message = message; | |
if (Error.captureStackTrace) { | |
Error.captureStackTrace(this, f); | |
} | |
} | |
AccessError.prototype = new Error(); | |
exports.guard = guard; | |
})(typeof window !== 'undefined' ? window : exports); | |
function Foo() { | |
this.__private = 2; | |
this._protected = 3; | |
console.log('ctor call'); | |
Bar.call(this, this); // this allows later calls to Bar.prototype.* from within Foo | |
} | |
Foo.prototype = { | |
publicMethod: function(value) { | |
this._protected = value; | |
this._protectedMethod(); | |
this.__privateMethod(); | |
return this.__private; | |
}, | |
__privateMethod: function() { | |
function inner() { | |
console.log('private'); | |
} | |
inner(); | |
this.__private2(); | |
}, | |
_protectedMethod: function() { | |
console.log('protected'); | |
}, | |
test: function() { | |
console.log('proto call'); | |
Bar.prototype.test2.call(this); | |
}, | |
__private2: function() { | |
} | |
}; | |
function Bar(foo) { | |
// foo._protectedMethod(); | |
this.__foo = foo; | |
} | |
Bar.prototype = { | |
test: function() { | |
console.log('priv'); | |
this.__foo.__privateMethod(); | |
}, | |
// Access needs to work here | |
test2: function() { | |
console.log('sub'); | |
this.__privateMethod(); | |
}, | |
__privateMethod: function() { | |
} | |
}; | |
Foo = exports.guard(Foo, 'Foo'); | |
Bar = exports.guard(Bar, 'Bar'); | |
var foo = new Foo(); | |
foo.publicMethod(); | |
foo.test(); | |
var bar = new Bar(foo); | |
bar.test2(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment