freeCodeCamp/public/js/lib/jailed/jailed.js

781 lines
22 KiB
JavaScript
Raw Normal View History

/**
* @fileoverview Jailed - safe yet flexible sandbox
* @version 0.2.0
*
* @license MIT, see http://github.com/asvd/jailed
* Copyright (c) 2014 asvd <heliosframework@gmail.com>
*
* Main library script, the only one to be loaded by a developer into
* the application. Other scrips shipped along will be loaded by the
* library either here (application site), or into the plugin site
* (Worker/child process):
*
* _JailedSite.js loaded into both applicaiton and plugin sites
* _frame.html sandboxed frame (web)
* _frame.js sandboxed frame code (web)
* _pluginWeb.js platform-dependent plugin routines (web)
* _pluginNode.js platform-dependent plugin routines (Node.js)
* _pluginCore.js common plugin site protocol implementation
*/
var __jailed__path__;
if (typeof window == 'undefined') {
// Node.js
__jailed__path__ = __dirname + '/';
} else {
// web
var scripts = document.getElementsByTagName('script');
__jailed__path__ = scripts[scripts.length-1].src
.split('?')[0]
.split('/')
.slice(0, -1)
.join('/')+'/';
}
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['exports'], factory);
} else if (typeof exports !== 'undefined') {
factory(exports);
} else {
factory((root.jailed = {}));
}
}(this, function (exports) {
var isNode = typeof window == 'undefined';
/**
* A special kind of event:
* - which can only be emitted once;
* - executes a set of subscribed handlers upon emission;
* - if a handler is subscribed after the event was emitted, it
* will be invoked immideately.
*
* Used for the events which only happen once (or do not happen at
* all) during a single plugin lifecycle - connect, disconnect and
* connection failure
*/
var Whenable = function() {
this._emitted = false;
this._handlers = [];
}
/**
* Emits the Whenable event, calls all the handlers already
* subscribed, switches the object to the 'emitted' state (when
* all future subscibed listeners will be immideately issued
* instead of being stored)
*/
Whenable.prototype.emit = function(){
if (!this._emitted) {
this._emitted = true;
var handler;
while(handler = this._handlers.pop()) {
setTimeout(handler,0);
}
}
}
/**
* Saves the provided function as a handler for the Whenable
* event. This handler will then be called upon the event emission
* (if it has not been emitted yet), or will be scheduled for
* immediate issue (if the event has already been emmitted before)
*
* @param {Function} handler to subscribe for the event
*/
Whenable.prototype.whenEmitted = function(handler){
handler = this._checkHandler(handler);
if (this._emitted) {
setTimeout(handler, 0);
} else {
this._handlers.push(handler);
}
}
/**
* Checks if the provided object is suitable for being subscribed
* to the event (= is a function), throws an exception if not
*
* @param {Object} obj to check for being subscribable
*
* @throws {Exception} if object is not suitable for subscription
*
* @returns {Object} the provided object if yes
*/
Whenable.prototype._checkHandler = function(handler){
var type = typeof handler;
if (type != 'function') {
var msg =
'A function may only be subsribed to the event, '
+ type
+ ' was provided instead'
throw new Error(msg);
}
return handler;
}
/**
* Initializes the library site for Node.js environment (loads
* _JailedSite.js)
*/
var initNode = function() {
require('./_JailedSite.js');
}
/**
* Initializes the library site for web environment (loads
* _JailedSite.js)
*/
var platformInit;
var initWeb = function() {
// loads additional script to the application environment
var load = function(path, cb) {
var script = document.createElement('script');
script.src = path;
var clear = function() {
script.onload = null;
script.onerror = null;
script.onreadystatechange = null;
script.parentNode.removeChild(script);
}
var success = function() {
clear();
cb();
}
script.onerror = clear;
script.onload = success;
script.onreadystatechange = function() {
var state = script.readyState;
if (state==='loaded' || state==='complete') {
success();
}
}
document.body.appendChild(script);
}
platformInit = new Whenable;
var origOnload = window.onload || function(){};
window.onload = function(){
origOnload();
load(
__jailed__path__+'_JailedSite.js',
function(){ platformInit.emit(); }
);
}
}
var BasicConnection;
/**
* Creates the platform-dependent BasicConnection object in the
* Node.js environment
*/
var basicConnectionNode = function() {
var childProcess = require('child_process');
/**
* Platform-dependent implementation of the BasicConnection
* object, initializes the plugin site and provides the basic
* messaging-based connection with it
*
* For Node.js the plugin is created as a forked process
*/
BasicConnection = function() {
this._disconnected = false;
this._messageHandler = function(){};
this._disconnectHandler = function(){};
this._process = childProcess.fork(
__jailed__path__+'_pluginNode.js'
);
var me = this;
this._process.on('message', function(m){
me._messageHandler(m);
});
this._process.on('exit', function(m){
me._disconnected = true;
me._disconnectHandler(m);
});
}
/**
* Sets-up the handler to be called upon the BasicConnection
* initialization is completed.
*
* For Node.js the connection is fully initialized within the
* constructor, so simply calls the provided handler.
*
* @param {Function} handler to be called upon connection init
*/
BasicConnection.prototype.whenInit = function(handler) {
handler();
}
/**
* Sends a message to the plugin site
*
* @param {Object} data to send
*/
BasicConnection.prototype.send = function(data) {
if (!this._disconnected) {
this._process.send(data);
}
}
/**
* Adds a handler for a message received from the plugin site
*
* @param {Function} handler to call upon a message
*/
BasicConnection.prototype.onMessage = function(handler) {
this._messageHandler = function(data) {
// broken stack would break the IPC in Node.js
try {
handler(data);
} catch (e) {
console.error();
console.error(e.stack);
}
}
}
/**
* Adds a handler for the event of plugin disconnection
* (= plugin process exit)
*
* @param {Function} handler to call upon a disconnect
*/
BasicConnection.prototype.onDisconnect = function(handler) {
this._disconnectHandler = handler;
}
/**
* Disconnects the plugin (= kills the forked process)
*/
BasicConnection.prototype.disconnect = function() {
this._process.kill('SIGKILL');
this._disconnected = true;
}
}
/**
* Creates the platform-dependent BasicConnection object in the
* web-browser environment
*/
var basicConnectionWeb = function() {
var perm = ['allow-scripts'];
if (__jailed__path__.substr(0,7).toLowerCase() == 'file://') {
// local instance requires extra permission
perm.push('allow-same-origin');
}
// frame element to be cloned
var sample = document.createElement('iframe');
sample.src = __jailed__path__ + '_frame.html';
sample.sandbox = perm.join(' ');
sample.style.display = 'none';
/**
* Platform-dependent implementation of the BasicConnection
* object, initializes the plugin site and provides the basic
* messaging-based connection with it
*
* For the web-browser environment, the plugin is created as a
* Worker in a sandbaxed frame
*/
BasicConnection = function() {
this._init = new Whenable;
this._disconnected = false;
var me = this;
platformInit.whenEmitted(function() {
if (!me._disconnected) {
me._frame = sample.cloneNode(false);
document.body.appendChild(me._frame);
window.addEventListener('message', function (e) {
if (e.origin === "null" &&
e.source === me._frame.contentWindow) {
if (e.data.type == 'initialized') {
me._init.emit();
} else {
me._messageHandler(e.data);
}
}
});
}
});
}
/**
* Sets-up the handler to be called upon the BasicConnection
* initialization is completed.
*
* For the web-browser environment, the handler is issued when
* the plugin worker successfully imported and executed the
* _pluginWeb.js, and replied to the application site with the
* initImprotSuccess message.
*
* @param {Function} handler to be called upon connection init
*/
BasicConnection.prototype.whenInit = function(handler) {
this._init.whenEmitted(handler);
}
/**
* Sends a message to the plugin site
*
* @param {Object} data to send
*/
BasicConnection.prototype.send = function(data) {
this._frame.contentWindow.postMessage(
{type: 'message', data: data}, '*'
);
}
/**
* Adds a handler for a message received from the plugin site
*
* @param {Function} handler to call upon a message
*/
BasicConnection.prototype.onMessage = function(handler) {
this._messageHandler = handler;
}
/**
* Adds a handler for the event of plugin disconnection
* (not used in case of Worker)
*
* @param {Function} handler to call upon a disconnect
*/
BasicConnection.prototype.onDisconnect = function(){};
/**
* Disconnects the plugin (= kills the frame)
*/
BasicConnection.prototype.disconnect = function() {
if (!this._disconnected) {
this._disconnected = true;
if (typeof this._frame != 'undefined') {
this._frame.parentNode.removeChild(this._frame);
} // otherwise farme is not yet created
}
}
}
if (isNode) {
initNode();
basicConnectionNode();
} else {
initWeb();
basicConnectionWeb();
}
/**
* Application-site Connection object constructon, reuses the
* platform-dependent BasicConnection declared above in order to
* communicate with the plugin environment, implements the
* application-site protocol of the interraction: provides some
* methods for loading scripts and executing the given code in the
* plugin
*/
var Connection = function(){
this._platformConnection = new BasicConnection;
this._importCallbacks = {};
this._executeSCb = function(){};
this._executeFCb = function(){};
this._messageHandler = function(){};
var me = this;
this.whenInit = function(cb){
me._platformConnection.whenInit(cb);
};
this._platformConnection.onMessage(function(m) {
switch(m.type) {
case 'message':
me._messageHandler(m.data);
break;
case 'importSuccess':
me._handleImportSuccess(m.url);
break;
case 'importFailure':
me._handleImportFailure(m.url);
break;
case 'executeSuccess':
me._executeSCb();
break;
case 'executeFailure':
me._executeFCb();
break;
}
});
}
/**
* Tells the plugin to load a script with the given path, and to
* execute it. Callbacks executed upon the corresponding responce
* message from the plugin site
*
* @param {String} path of a script to load
* @param {Function} sCb to call upon success
* @param {Function} fCb to call upon failure
*/
Connection.prototype.importScript = function(path, sCb, fCb) {
var f = function(){};
this._importCallbacks[path] = {sCb: sCb||f, fCb: fCb||f};
this._platformConnection.send({type: 'import', url: path});
}
/**
* Tells the plugin to load a script with the given path, and to
* execute it in the JAILED environment. Callbacks executed upon
* the corresponding responce message from the plugin site
*
* @param {String} path of a script to load
* @param {Function} sCb to call upon success
* @param {Function} fCb to call upon failure
*/
Connection.prototype.importJailedScript = function(path, sCb, fCb) {
var f = function(){};
this._importCallbacks[path] = {sCb: sCb||f, fCb: fCb||f};
this._platformConnection.send({type: 'importJailed', url: path});
}
/**
* Sends the code to the plugin site in order to have it executed
* in the JAILED enviroment. Assuming the execution may only be
* requested once by the Plugin object, which means a single set
* of callbacks is enough (unlike importing additional scripts)
*
* @param {String} code code to execute
* @param {Function} sCb to call upon success
* @param {Function} fCb to call upon failure
*/
Connection.prototype.execute = function(code, sCb, fCb) {
this._executeSCb = sCb||function(){};
this._executeFCb = fCb||function(){};
this._platformConnection.send({type: 'execute', code: code});
}
/**
* Adds a handler for a message received from the plugin site
*
* @param {Function} handler to call upon a message
*/
Connection.prototype.onMessage = function(handler) {
this._messageHandler = handler;
}
/**
* Adds a handler for a disconnect message received from the
* plugin site
*
* @param {Function} handler to call upon disconnect
*/
Connection.prototype.onDisconnect = function(handler) {
this._platformConnection.onDisconnect(handler);
}
/**
* Sends a message to the plugin
*
* @param {Object} data of the message to send
*/
Connection.prototype.send = function(data) {
this._platformConnection.send({
type: 'message',
data: data
});
}
/**
* Handles import succeeded message from the plugin
*
* @param {String} url of a script loaded by the plugin
*/
Connection.prototype._handleImportSuccess = function(url) {
var sCb = this._importCallbacks[url].sCb;
this._importCallbacks[url] = null;
delete this._importCallbacks[url];
sCb();
}
/**
* Handles import failure message from the plugin
*
* @param {String} url of a script loaded by the plugin
*/
Connection.prototype._handleImportFailure = function(url) {
var fCb = this._importCallbacks[url].fCb;
this._importCallbacks[url] = null;
delete this._importCallbacks[url];
fCb();
}
/**
* Disconnects the plugin when it is not needed anymore
*/
Connection.prototype.disconnect = function() {
this._platformConnection.disconnect();
}
/**
* Plugin constructor, represents a plugin initialized by a script
* with the given path
*
* @param {String} url of a plugin source
* @param {Object} _interface to provide for the plugin
*/
var Plugin = function(url, _interface) {
this._path = url;
this._initialInterface = _interface||{};
this._connect();
}
/**
* DynamicPlugin constructor, represents a plugin initialized by a
* string containing the code to be executed
*
* @param {String} code of the plugin
* @param {Object} _interface to provide to the plugin
*/
var DynamicPlugin = function(code, _interface) {
this._code = code;
this._initialInterface = _interface||{};
this._connect();
}
/**
* Creates the connection to the plugin site
*/
DynamicPlugin.prototype._connect =
Plugin.prototype._connect = function() {
this.remote = null;
this._connect = new Whenable;
this._fail = new Whenable;
this._disconnect = new Whenable;
var me = this;
// binded failure callback
this._fCb = function(){
me._fail.emit();
me.disconnect();
}
this._connection = new Connection;
this._connection.whenInit(function(){
me._init();
});
}
/**
* Creates the Site object for the plugin, and then loads the
* common routines (_JailedSite.js)
*/
DynamicPlugin.prototype._init =
Plugin.prototype._init = function() {
this._site = new JailedSite(this._connection);
var me = this;
this._site.onDisconnect(function() {
me._disconnect.emit();
});
var sCb = function() {
me._loadCore();
}
this._connection.importScript(
__jailed__path__+'_JailedSite.js', sCb, this._fCb
);
}
/**
* Loads the core scirpt into the plugin
*/
DynamicPlugin.prototype._loadCore =
Plugin.prototype._loadCore = function() {
var me = this;
var sCb = function() {
me._sendInterface();
}
this._connection.importScript(
__jailed__path__+'_pluginCore.js', sCb, this._fCb
);
}
/**
* Sends to the remote site a signature of the interface provided
* upon the Plugin creation
*/
DynamicPlugin.prototype._sendInterface =
Plugin.prototype._sendInterface = function() {
var me = this;
this._site.onInterfaceSetAsRemote(function() {
if (!me._connected) {
me._loadPlugin();
}
});
this._site.setInterface(this._initialInterface);
}
/**
* Loads the plugin body (loads the plugin url in case of the
* Plugin)
*/
Plugin.prototype._loadPlugin = function() {
var me = this;
var sCb = function() {
me._requestRemote();
}
this._connection.importJailedScript(this._path, sCb, this._fCb);
}
/**
* Loads the plugin body (executes the code in case of the
* DynamicPlugin)
*/
DynamicPlugin.prototype._loadPlugin = function() {
var me = this;
var sCb = function() {
me._requestRemote();
}
this._connection.execute(this._code, sCb, this._fCb);
}
/**
* Requests the remote interface from the plugin (which was
* probably set by the plugin during its initialization), emits
* the connect event when done, then the plugin is fully usable
* (meaning both the plugin and the application can use the
* interfaces provided to each other)
*/
DynamicPlugin.prototype._requestRemote =
Plugin.prototype._requestRemote = function() {
var me = this;
this._site.onRemoteUpdate(function(){
me.remote = me._site.getRemote();
me._connect.emit();
});
this._site.requestRemote();
}
/**
* Disconnects the plugin immideately
*/
DynamicPlugin.prototype.disconnect =
Plugin.prototype.disconnect = function() {
this._connection.disconnect();
this._disconnect.emit();
}
/**
* Saves the provided function as a handler for the connection
* failure Whenable event
*
* @param {Function} handler to be issued upon disconnect
*/
DynamicPlugin.prototype.whenFailed =
Plugin.prototype.whenFailed = function(handler) {
this._fail.whenEmitted(handler);
}
/**
* Saves the provided function as a handler for the connection
* success Whenable event
*
* @param {Function} handler to be issued upon connection
*/
DynamicPlugin.prototype.whenConnected =
Plugin.prototype.whenConnected = function(handler) {
this._connect.whenEmitted(handler);
}
/**
* Saves the provided function as a handler for the connection
* failure Whenable event
*
* @param {Function} handler to be issued upon connection failure
*/
DynamicPlugin.prototype.whenDisconnected =
Plugin.prototype.whenDisconnected = function(handler) {
this._disconnect.whenEmitted(handler);
}
exports.Plugin = Plugin;
exports.DynamicPlugin = DynamicPlugin;
}));