Skip to content

Instantly share code, notes, and snippets.

@BonsaiDen
Created September 5, 2011 22:01
Show Gist options
  • Save BonsaiDen/1196020 to your computer and use it in GitHub Desktop.
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.
/**
* 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