Work on bonfire and make better .jshintrc files

This commit is contained in:
webdev
2015-01-16 18:58:27 -05:00
parent ca1f9068b8
commit adac5f6352
15 changed files with 2084 additions and 209 deletions

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ node_modules
*.iml
.DS_Store
Thumbs.db
bower_components

View File

@ -1,48 +1,40 @@
{
/*
* ENVIRONMENTS
* =================
*/
// Define globals exposed by Node.js.
"node": true,
/*
* ENFORCING OPTIONS
* =================
*/
// Force all variable names to use either camelCase style or UPPER_CASE
// with underscores.
"camelcase": true,
// Prohibit use of == and != in favor of === and !==.
"eqeqeq": true,
// Suppress warnings about == null comparisons.
"eqnull": true,
// Enforce tab width of 2 spaces.
"indent": 2,
// Prohibit use of a variable before it is defined.
"latedef": true,
// Require capitalized names for constructor functions.
"newcap": true,
// Enforce use of single quotation marks for strings.
"quotmark": "single",
// Prohibit trailing whitespace.
"trailing": true,
// Prohibit use of explicitly undeclared variables.
"undef": true,
// Warn when variables are defined but never used.
"unused": true,
// Enforce line length to 80 characters
"maxlen": 80
}
"node": true, // Enable globals available when code is running inside of the NodeJS runtime environment.
"browser": true, // Standard browser globals e.g. `window`, `document`.
"esnext": true, // Allow ES.next specific features such as `const` and `let`.
"bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.).
"camelcase": false, // Permit only camelcase for `var` and `object indexes`.
"curly": false, // Require {} for every new block or scope.
"eqeqeq": true, // Require triple equals i.e. `===`.
"immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
"latedef": true, // Prohibit variable use before definition.
"newcap": true, // Require capitalization of all constructor functions e.g. `new F()`.
"noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`.
"regexp": true, // Prohibit `.` and `[^...]` in regular expressions.
"undef": true, // Require all non-global variables be declared before they are used.
"unused": false, // Warn unused variables.
"strict": false, // Require `use strict` pragma in every file.
"trailing": true, // Prohibit trailing whitespaces.
"smarttabs": false, // Suppresses warnings about mixed tabs and spaces
"globals": { // Globals variables.
"jasmine": true,
"angular": true,
"ApplicationConfiguration": true
},
"predef": [ // Extra globals.
"define",
"require",
"exports",
"module",
"describe",
"before",
"beforeEach",
"after",
"afterEach",
"it",
"inject",
"expect"
],
"devel": true, // Allow development statements e.g. `console.log();`.
"noempty": true // Prohibit use of empty blocks.
}

183
app.js
View File

@ -24,8 +24,8 @@ var express = require('express'),
connectAssets = require('connect-assets'),
/**
* Controllers (route handlers).
*/
* Controllers (route handlers).
*/
homeController = require('./controllers/home'),
challengesController = require('./controllers/challenges'),
resourcesController = require('./controllers/resources'),
@ -34,13 +34,13 @@ var express = require('express'),
bonfireController = require('./controllers/bonfire'),
/**
* User model
*/
* User model
*/
User = require('./models/User'),
/**
* API keys and Passport configuration.
*/
* API keys and Passport configuration.
*/
secrets = require('./config/secrets'),
passportConf = require('./config/passport');
@ -53,10 +53,10 @@ var app = express();
* Connect to MongoDB.
*/
mongoose.connect(secrets.db);
mongoose.connection.on('error', function() {
console.error(
'MongoDB Connection Error. Please make sure that MongoDB is running.'
);
mongoose.connection.on('error', function () {
console.error(
'MongoDB Connection Error. Please make sure that MongoDB is running.'
);
});
/**
@ -68,20 +68,20 @@ app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(compress());
var oneYear = 31557600000;
app.use(express.static(__dirname + '/public', { maxAge: oneYear }));
app.use(express.static(__dirname + '/public', {maxAge: oneYear}));
app.use(connectAssets({
paths: [
path.join(__dirname, 'public/css'),
path.join(__dirname, 'public/js')
path.join(__dirname, 'public/css'),
path.join(__dirname, 'public/js')
],
helperContext: app.locals
}));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.urlencoded({extended: true}));
app.use(expressValidator({
customValidators: {
matchRegex: function(param, regex) {
matchRegex: function (param, regex) {
return regex.test(param);
}
}
@ -137,50 +137,50 @@ app.use(helmet.contentSecurityPolicy({
defaultSrc: trusted,
scriptSrc: ['*.optimizely.com', '*.aspnetcdn.com'].concat(trusted),
'connect-src': [
'ws://*.rafflecopter.com',
'wss://*.rafflecopter.com',
'https://*.rafflecopter.com',
'ws://www.freecodecamp.com',
'http://www.freecodecamp.com'
'ws://*.rafflecopter.com',
'wss://*.rafflecopter.com',
'https://*.rafflecopter.com',
'ws://www.freecodecamp.com',
'http://www.freecodecamp.com'
].concat(trusted),
styleSrc: trusted,
imgSrc: [
'*.evernote.com',
'*.amazonaws.com',
'data:',
'*.licdn.com',
'*.gravatar.com',
'*.youtube.com',
'*.akamaihd.net',
'graph.facebook.com',
'*.githubusercontent.com',
'*.googleusercontent.com',
'*' /* allow all input since we have user submitted images for public profile*/
'*.evernote.com',
'*.amazonaws.com',
'data:',
'*.licdn.com',
'*.gravatar.com',
'*.youtube.com',
'*.akamaihd.net',
'graph.facebook.com',
'*.githubusercontent.com',
'*.googleusercontent.com',
'*' /* allow all input since we have user submitted images for public profile*/
].concat(trusted),
fontSrc: ['*.googleapis.com'].concat(trusted),
mediaSrc: [
'*.amazonaws.com',
'*.twitter.com'
'*.amazonaws.com',
'*.twitter.com'
].concat(trusted),
frameSrc: [
'*.gitter.im',
'*.vimeo.com',
'*.twitter.com',
'*.rafflecopter.com',
'*.youtube.com'
'*.gitter.im',
'*.vimeo.com',
'*.twitter.com',
'*.rafflecopter.com',
'*.youtube.com'
].concat(trusted),
reportOnly: false, // set to true if you only want to report errors
setAllHeaders: false, // set to true if you want to set all headers
safari5: false // set to true if you want to force buggy CSP in Safari 5
}));
app.use(function(req, res, next) {
app.use(function (req, res, next) {
// Make user object available in templates.
res.locals.user = req.user;
next();
});
app.use(function(req, res, next) {
app.use(function (req, res, next) {
// Remember original destination before login.
var path = req.path.split('/')[1];
if (/auth|login|logout|signup|fonts|favicon/i.test(path)) {
@ -191,7 +191,7 @@ app.use(function(req, res, next) {
});
app.use(
express.static(path.join(__dirname, 'public'), { maxAge: 31557600000 })
express.static(path.join(__dirname, 'public'), {maxAge: 31557600000})
);
/**
@ -200,8 +200,8 @@ app.use(
app.get('/', homeController.index);
app.get(
'/resources/interview-questions',
resourcesController.interviewQuestions);
'/resources/interview-questions',
resourcesController.interviewQuestions);
app.get('/learn-to-code', resourcesController.learnToCode);
app.get('/privacy', resourcesController.privacy);
app.get('/jquery-exercises', resourcesController.jqueryExercises);
@ -215,16 +215,16 @@ app.get('/control-shortcuts', resourcesController.deployAWebsite);
app.get('/stats', resourcesController.stats);
app.get(
'/pair-program-with-team-viewer',
resourcesController.pairProgramWithTeamViewer
'/pair-program-with-team-viewer',
resourcesController.pairProgramWithTeamViewer
);
app.get(
'/done-with-first-100-hours',
resourcesController.doneWithFirst100Hours
'/done-with-first-100-hours',
resourcesController.doneWithFirst100Hours
);
app.get(
'/programmer-interview-questions-app',
resourcesController.programmerInterviewQuestionsApp
'/programmer-interview-questions-app',
resourcesController.programmerInterviewQuestionsApp
);
app.get('/about', resourcesController.about);
@ -244,9 +244,9 @@ app.post('/nonprofits', contactController.postContact);
// # Protected routes, user must be logged in.
app.post(
'/update-progress',
passportConf.isAuthenticated,
userController.updateProgress
'/update-progress',
passportConf.isAuthenticated,
userController.updateProgress
);
app.get(
@ -274,13 +274,15 @@ app.get('/account/unlink/:provider', userController.getOauthUnlink);
* and updates user.challengesHash & user.challengesCompleted
*
*/
app.post('/completed_challenge', function(req, res) {
app.post('/completed_challenge', function (req, res) {
req.user.challengesHash[parseInt(req.body.challengeNumber)] =
Math.round(+ new Date() / 1000);
Math.round(+new Date() / 1000);
var ch = req.user.challengesHash;
var p = 0;
for (var k in ch) {
if (ch[k] > 0) { p += 1; }
if (ch[k] > 0) {
p += 1;
}
}
req.user.points = p;
req.user.save();
@ -291,60 +293,60 @@ app.post('/completed_challenge', function(req, res) {
*/
var passportOptions = {
successRedirect: '/',
failureRedirect: '/login'
successRedirect: '/',
failureRedirect: '/login'
};
app.get('/auth/twitter', passport.authenticate('twitter'));
app.get(
'/auth/twitter/callback',
passport.authenticate('twitter', {
successRedirect: '/',
failureRedirect: '/login'
})
'/auth/twitter/callback',
passport.authenticate('twitter', {
successRedirect: '/',
failureRedirect: '/login'
})
);
app.get(
'/auth/linkedin',
passport.authenticate('linkedin', {
state: 'SOME STATE'
})
'/auth/linkedin',
passport.authenticate('linkedin', {
state: 'SOME STATE'
})
);
app.get(
'/auth/linkedin/callback',
passport.authenticate('linkedin', passportOptions)
'/auth/linkedin/callback',
passport.authenticate('linkedin', passportOptions)
);
app.get(
'/auth/facebook',
passport.authenticate('facebook', { scope: ['email', 'user_location'] })
'/auth/facebook',
passport.authenticate('facebook', {scope: ['email', 'user_location']})
);
app.get(
'/auth/facebook/callback',
passport.authenticate('facebook', passportOptions), function(req, res) {
res.redirect(req.session.returnTo || '/');
}
'/auth/facebook/callback',
passport.authenticate('facebook', passportOptions), function (req, res) {
res.redirect(req.session.returnTo || '/');
}
);
app.get('/auth/github', passport.authenticate('github'));
app.get(
'/auth/github/callback',
passport.authenticate('github', passportOptions), function(req, res) {
res.redirect(req.session.returnTo || '/');
}
'/auth/github/callback',
passport.authenticate('github', passportOptions), function (req, res) {
res.redirect(req.session.returnTo || '/');
}
);
app.get(
'/auth/google',
passport.authenticate('google', { scope: 'profile email' })
'/auth/google',
passport.authenticate('google', {scope: 'profile email'})
);
app.get(
'/auth/google/callback',
passport.authenticate('google', passportOptions), function(req, res) {
res.redirect(req.session.returnTo || '/');
}
'/auth/google/callback',
passport.authenticate('google', passportOptions), function (req, res) {
res.redirect(req.session.returnTo || '/');
}
);
/**
@ -352,7 +354,6 @@ app.get(
*/
app.get('/bonfire', bonfireController.index);
/**
* 500 Error Handler.
@ -362,12 +363,12 @@ app.use(errorHandler());
/**
* Start Express server.
*/
app.listen(app.get('port'), function() {
console.log(
'FreeCodeCamp server listening on port %d in %s mode',
app.get('port'),
app.get('env')
);
app.listen(app.get('port'), function () {
console.log(
'FreeCodeCamp server listening on port %d in %s mode',
app.get('port'),
app.get('env')
);
});
module.exports = app;

View File

@ -16,4 +16,5 @@ exports.index = function(req, res) {
cc: req.user ? req.user.challengesHash : undefined
});
});
};
};

View File

@ -1,54 +1,40 @@
{
/*
* ENVIRONMENTS
* =================
*/
// Define globals exposed by modern browsers.
"browser": true,
// Define globals exposed by jQuery.
"jquery": true,
/*
* ENFORCING OPTIONS
* =================
*/
// Force all variable names to use either camelCase style or UPPER_CASE
// with underscores.
"camelcase": true,
// Prohibit use of == and != in favor of === and !==.
"eqeqeq": true,
// Suppress warnings about == null comparisons.
"eqnull": true,
// Enforce tab width of 2 spaces.
"indent": 2,
// Prohibit use of a variable before it is defined.
"latedef": true,
// Require capitalized names for constructor functions.
"newcap": true,
// Enforce use of single quotation marks for strings.
"quotmark": "single",
// Prohibit trailing whitespace.
"trailing": true,
// Prohibit use of explicitly undeclared variables.
"undef": true,
// Warn when variables are defined but never used.
"unused": true,
// Enforce line length to 80 characters
"maxlen": 80,
// Enforce placing 'use strict' at the top function scope
"strict": true'
}
"node": true, // Enable globals available when code is running inside of the NodeJS runtime environment.
"browser": true, // Standard browser globals e.g. `window`, `document`.
"esnext": true, // Allow ES.next specific features such as `const` and `let`.
"bitwise": false, // Prohibit bitwise operators (&, |, ^, etc.).
"camelcase": false, // Permit only camelcase for `var` and `object indexes`.
"curly": false, // Require {} for every new block or scope.
"eqeqeq": true, // Require triple equals i.e. `===`.
"immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
"latedef": true, // Prohibit variable use before definition.
"newcap": true, // Require capitalization of all constructor functions e.g. `new F()`.
"noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`.
"regexp": true, // Prohibit `.` and `[^...]` in regular expressions.
"undef": true, // Require all non-global variables be declared before they are used.
"unused": false, // Warn unused variables.
"strict": false, // Require `use strict` pragma in every file.
"trailing": true, // Prohibit trailing whitespaces.
"smarttabs": false, // Suppresses warnings about mixed tabs and spaces
"globals": { // Globals variables.
"jasmine": true,
"angular": true,
"ApplicationConfiguration": true
},
"predef": [ // Extra globals.
"define",
"require",
"exports",
"module",
"describe",
"before",
"beforeEach",
"after",
"afterEach",
"it",
"inject",
"expect"
],
"devel": true, // Allow development statements e.g. `console.log();`.
"noempty": true // Prohibit use of empty blocks.
}

View File

@ -0,0 +1,104 @@
// sends the input to the plugin for evaluation
var submit = function(code) {
// postpone the evaluation until the plugin is initialized
plugin.whenConnected(
function() {
if (requests == 0) {
startLoading();
}
requests++;
plugin.remote.run(code);
}
);
};
// prepares the string to be printed on the terminal
var escape = function(msg) {
return msg.
replace(/&/g,'&').
replace(/</g,'&lt;').
replace(/>/g,'&gt;').
replace(/\n/g, '<br/>').
replace(/ /g, '&nbsp;');
};
// puts the message on the terminal
var print = function(cls, msg) {
codeOutput.setValue(escape(msg));
};
// will restart the plugin if it does not respond
var disconnectTimeout = null;
var startLoading = function() {
disconnectTimeout = setTimeout(disconnect, 3000);
};
var endLoading = function() {
clearTimeout(disconnectTimeout);
};
var disconnect = function() {
plugin.disconnect();
};
// interface provided to the plugin
var api = {
output: function(data) {
if (!--requests) {
endLoading();
}
print('separator');
print('input', data.input);
if (data.error) {
print('message', data.error);
} else {
print('output', data.output);
}
}
};
// obtaining absolute path of this script
var scripts = document.getElementsByTagName('script');
var path = scripts[scripts.length-1].src
.split('?')[0]
.split('/')
.slice(0, -1)
.join('/')+'/';
var requests;
// (re)initializes the plugin
var reset = function() {
requests = 0;
plugin = new jailed.Plugin(path+'plugin.js', api);
plugin.whenDisconnected( function() {
// give some time to handle the last responce
setTimeout( function() {
endLoading();
while (el.terminal.hasChildNodes()) {
el.terminal.removeChild(el.terminal.childNodes[0]);
}
print('message', 'Your code took too long to execute. Check for an infinite loop or recursion.');
reset();
}, 10);
});
}
// initialize everything
var plugin = null;
reset();

View File

@ -0,0 +1,68 @@
// executes the given code and handles the result
var run = function(code) {
var result = {
input: code,
output: null,
error: null
};
try {
result.output = stringify(runHidden(code));
} catch(e) {
result.error = e.message;
}
application.remote.output(result);
};
// protects even the worker scope from being accessed
var runHidden = function(code) {
var indexedDB = null;
var location = null;
var navigator = null;
var onerror = null;
var onmessage = null;
var performance = null;
var self = null;
var webkitIndexedDB = null;
var postMessage = null;
var close = null;
var openDatabase = null;
var openDatabaseSync = null;
var webkitRequestFileSystem = null;
var webkitRequestFileSystemSync = null;
var webkitResolveLocalFileSystemSyncURL = null;
var webkitResolveLocalFileSystemURL = null;
var addEventListener = null;
var dispatchEvent = null;
var removeEventListener = null;
var dump = null;
var onoffline = null;
var ononline = null;
var importScripts = null;
var console = null;
var application = null;
return eval(code);
}
// converts the output into a string
var stringify = function(output) {
var result;
if (typeof output == 'undefined') {
result = 'undefined';
} else if (output === null) {
result = 'null';
} else {
result = JSON.stringify(output) || output.toString();
}
return result;
}
application.setInterface({run:run});

View File

@ -0,0 +1,432 @@
/**
* Contains the JailedSite object used both by the application
* site, and by each plugin
*/
(function(){
/**
* JailedSite object represents a single site in the
* communication protocol between the application and the plugin
*
* @param {Object} connection a special object allowing to send
* and receive messages from the opposite site (basically it
* should only provide send() and onMessage() methods)
*/
JailedSite = function(connection) {
this._interface = {};
this._remote = null;
this._remoteUpdateHandler = function(){};
this._getInterfaceHandler = function(){};
this._interfaceSetAsRemoteHandler = function(){};
this._disconnectHandler = function(){};
this._store = new ReferenceStore;
var me = this;
this._connection = connection;
this._connection.onMessage(
function(data){ me._processMessage(data); }
);
this._connection.onDisconnect(
function(m){
me._disconnectHandler(m);
}
);
}
/**
* Set a handler to be called when the remote site updates its
* interface
*
* @param {Function} handler
*/
JailedSite.prototype.onRemoteUpdate = function(handler) {
this._remoteUpdateHandler = handler;
}
/**
* Set a handler to be called when received a responce from the
* remote site reporting that the previously provided interface
* has been succesfully set as remote for that site
*
* @param {Function} handler
*/
JailedSite.prototype.onInterfaceSetAsRemote = function(handler) {
this._interfaceSetAsRemoteHandler = handler;
}
/**
* Set a handler to be called when the remote site requests to
* (re)send the interface. Used to detect an initialzation
* completion without sending additional request, since in fact
* 'getInterface' request is only sent by application at the last
* step of the plugin initialization
*
* @param {Function} handler
*/
JailedSite.prototype.onGetInterface = function(handler) {
this._getInterfaceHandler = handler;
}
/**
* @returns {Object} set of remote interface methods
*/
JailedSite.prototype.getRemote = function() {
return this._remote;
}
/**
* Sets the interface of this site making it available to the
* remote site by sending a message with a set of methods names
*
* @param {Object} _interface to set
*/
JailedSite.prototype.setInterface = function(_interface) {
this._interface = _interface;
this._sendInterface();
}
/**
* Sends the actual interface to the remote site upon it was
* updated or by a special request of the remote site
*/
JailedSite.prototype._sendInterface = function() {
var names = [];
for (var name in this._interface) {
if (this._interface.hasOwnProperty(name)) {
names.push(name);
}
}
this._connection.send({type:'setInterface', api: names});
}
/**
* Handles a message from the remote site
*/
JailedSite.prototype._processMessage = function(data) {
switch(data.type) {
case 'method':
var method = this._interface[data.name];
var args = this._unwrap(data.args);
method.apply(null, args);
break;
case 'callback':
var method = this._store.fetch(data.id)[data.num];
var args = this._unwrap(data.args);
method.apply(null, args);
break;
case 'setInterface':
this._setRemote(data.api);
break;
case 'getInterface':
this._sendInterface();
this._getInterfaceHandler();
break;
case 'interfaceSetAsRemote':
this._interfaceSetAsRemoteHandler();
break;
case 'disconnect':
this._disconnectHandler();
this._connection.disconnect();
break;
}
}
/**
* Sends a requests to the remote site asking it to provide its
* current interface
*/
JailedSite.prototype.requestRemote = function() {
this._connection.send({type:'getInterface'});
}
/**
* Sets the new remote interface provided by the other site
*
* @param {Array} names list of function names
*/
JailedSite.prototype._setRemote = function(names) {
this._remote = {};
var i, name;
for (i = 0; i < names.length; i++) {
name = names[i];
this._remote[name] = this._genRemoteMethod(name);
}
this._remoteUpdateHandler();
this._reportRemoteSet();
}
/**
* Generates the wrapped function corresponding to a single remote
* method. When the generated function is called, it will send the
* corresponding message to the remote site asking it to execute
* the particular method of its interface
*
* @param {String} name of the remote method
*
* @returns {Function} wrapped remote method
*/
JailedSite.prototype._genRemoteMethod = function(name) {
var me = this;
var remoteMethod = function() {
me._connection.send({
type: 'method',
name: name,
args: me._wrap(arguments)
});
};
return remoteMethod;
}
/**
* Sends a responce reporting that interface just provided by the
* remote site was sucessfully set by this site as remote
*/
JailedSite.prototype._reportRemoteSet = function() {
this._connection.send({type:'interfaceSetAsRemote'});
}
/**
* Prepares the provided set of remote method arguments for
* sending to the remote site, replaces all the callbacks with
* identifiers
*
* @param {Array} args to wrap
*
* @returns {Array} wrapped arguments
*/
JailedSite.prototype._wrap = function(args) {
var wrapped = [];
var callbacks = {};
var callbacksPresent = false;
for (var i = 0; i < args.length; i++) {
if (typeof args[i] == 'function') {
callbacks[i] = args[i];
wrapped[i] = {type: 'callback', num : i};
callbacksPresent = true;
} else {
wrapped[i] = {type: 'argument', value : args[i]};
}
}
var result = {args: wrapped};
if (callbacksPresent) {
result.callbackId = this._store.put(callbacks);
}
return result;
}
/**
* Unwraps the set of arguments delivered from the remote site,
* replaces all callback identifiers with a function which will
* initiate sending that callback identifier back to other site
*
* @param {Object} args to unwrap
*
* @returns {Array} unwrapped args
*/
JailedSite.prototype._unwrap = function(args) {
var called = false;
// wraps each callback so that the only one could be called
var once = function(cb) {
return function() {
if (!called) {
called = true;
cb.apply(this, arguments);
} else {
var msg =
'A callback from this set has already been executed';
throw new Error(msg);
}
};
}
var result = [];
var i, arg, cb, me = this;
for (i = 0; i < args.args.length; i++) {
arg = args.args[i];
if (arg.type == 'argument') {
result.push(arg.value);
} else {
cb = once(
this._genRemoteCallback(args.callbackId, i)
);
result.push(cb);
}
}
return result;
}
/**
* Generates the wrapped function corresponding to a single remote
* callback. When the generated function is called, it will send
* the corresponding message to the remote site asking it to
* execute the particular callback previously saved during a call
* by the remote site a method from the interface of this site
*
* @param {Number} id of the remote callback to execute
* @param {Number} argNum argument index of the callback
*
* @returns {Function} wrapped remote callback
*/
JailedSite.prototype._genRemoteCallback = function(id, argNum) {
var me = this;
var remoteCallback = function() {
me._connection.send({
type : 'callback',
id : id,
num : argNum,
args : me._wrap(arguments)
});
};
return remoteCallback;
}
/**
* Sends the notification message and breaks the connection
*/
JailedSite.prototype.disconnect = function() {
this._connection.send({type: 'disconnect'});
this._connection.disconnect();
}
/**
* Set a handler to be called when received a disconnect message
* from the remote site
*
* @param {Function} handler
*/
JailedSite.prototype.onDisconnect = function(handler) {
this._disconnectHandler = handler;
}
/**
* ReferenceStore is a special object which stores other objects
* and provides the references (number) instead. This reference
* may then be sent over a json-based communication channel (IPC
* to another Node.js process or a message to the Worker). Other
* site may then provide the reference in the responce message
* implying the given object should be activated.
*
* Primary usage for the ReferenceStore is a storage for the
* callbacks, which therefore makes it possible to initiate a
* callback execution by the opposite site (which normally cannot
* directly execute functions over the communication channel).
*
* Each stored object can only be fetched once and is not
* available for the second time. Each stored object must be
* fetched, since otherwise it will remain stored forever and
* consume memory.
*
* Stored object indeces are simply the numbers, which are however
* released along with the objects, and are later reused again (in
* order to postpone the overflow, which should not likely happen,
* but anyway).
*/
var ReferenceStore = function() {
this._store = {}; // stored object
this._indices = [0]; // smallest available indices
}
/**
* @function _genId() generates the new reference id
*
* @returns {Number} smallest available id and reserves it
*/
ReferenceStore.prototype._genId = function() {
var id;
if (this._indices.length == 1) {
id = this._indices[0]++;
} else {
id = this._indices.shift();
}
return id;
}
/**
* Releases the given reference id so that it will be available by
* another object stored
*
* @param {Number} id to release
*/
ReferenceStore.prototype._releaseId = function(id) {
for (var i = 0; i < this._indices.length; i++) {
if (id < this._indices[i]) {
this._indices.splice(i, 0, id);
break;
}
}
// cleaning-up the sequence tail
for (i = this._indices.length-1; i >= 0; i--) {
if (this._indices[i]-1 == this._indices[i-1]) {
this._indices.pop();
} else {
break;
}
}
}
/**
* Stores the given object and returns the refernce id instead
*
* @param {Object} obj to store
*
* @returns {Number} reference id of the stored object
*/
ReferenceStore.prototype.put = function(obj) {
var id = this._genId();
this._store[id] = obj;
return id;
}
/**
* Retrieves previously stored object and releases its reference
*
* @param {Number} id of an object to retrieve
*/
ReferenceStore.prototype.fetch = function(id) {
var obj = this._store[id];
this._store[id] = null;
delete this._store[id];
this._releaseId(id);
return obj;
}
})();

View File

@ -0,0 +1 @@
<script src="_frame.js"></script>

View File

@ -0,0 +1,49 @@
/**
* Contains the code executed in the sandboxed frame under web-browser
*
* Creates a Web-Worker inside the frame, sets up the communication
* between the worker and the parent window
*/
var scripts = document.getElementsByTagName('script');
var __jailed__path__ = scripts[scripts.length-1].src
.split('?')[0]
.split('/')
.slice(0, -1)
.join('/')+'/';
// creating worker as a blob enables import of local files
var blobCode = [
' self.addEventListener("message", function(m){ ',
' if (m.data.type == "initImport") { ',
' importScripts(m.data.url); ',
' self.postMessage({type: "initialized"}); ',
' } ',
' }); '
].join('\n');
var blobUrl = window.URL.createObjectURL(
new Blob([blobCode])
);
var worker = new Worker(blobUrl);
// telling worker to load _pluginWeb.js (see blob code above)
worker.postMessage({
type: 'initImport',
url: __jailed__path__ + '_pluginWeb.js'
});
// forwarding messages between the worker and parent window
worker.addEventListener('message', function(m) {
parent.postMessage(m.data, '*');
});
window.addEventListener('message', function(m) {
worker.postMessage(m.data);
});

View File

@ -0,0 +1,95 @@
/**
* Core plugin script loaded into the plugin process/thread.
*
* Initializes the plugin-site API global methods.
*/
(function(){
// localize
var site = new JailedSite(connection);
delete JailedSite;
delete connection;
site.onGetInterface(function(){
launchConnected();
});
site.onRemoteUpdate(function(){
application.remote = site.getRemote();
});
/**
* Simplified clone of Whenable instance (the object can not be
* placed into a shared script, because the main library needs it
* before the additional scripts may load)
*/
var connected = false;
var connectedHandlers = [];
var launchConnected = function() {
if (!connected) {
connected = true;
var handler;
while(handler = connectedHandlers.pop()) {
handler();
}
}
}
var 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;
}
/**
* Sets a function executed after the connection to the
* application is estaplished, and the initial interface-exchange
* messaging is completed
*
* @param {Function} handler to be called upon initialization
*/
application.whenConnected = function(handler) {
handler = checkHandler(handler);
if (connected) {
handler();
} else {
connectedHandlers.push(handler);
}
}
/**
* Sets the plugin interface available to the application
*
* @param {Object} _interface to set
*/
application.setInterface = function(_interface) {
site.setInterface(_interface);
}
/**
* Disconnects the plugin from the application (sending
* notification message) and destroys itself
*/
application.disconnect = function(_interface) {
site.disconnect();
}
})();

View File

@ -0,0 +1,267 @@
/**
* Contains the routines loaded by the plugin process under Node.js
*
* Initializes the Node.js environment version of the
* platform-dependent connection object for the plugin site
*/
application = {};
connection = {};
/**
* Prints error message and its stack
*
* @param {Object} msg stack provided by error.stack or a message
*/
var printError = function(msg) {
console.error();
console.error(msg);
}
/**
* Event lisener for the plugin message
*/
process.on('message', function(m) {
switch(m.type){
case 'import':
importScript(m.url);
break;
case 'importJailed':
importScriptJailed(m.url);
break;
case 'execute':
execute(m.code);
break;
case 'message':
// unhandled exception would break the IPC channel
try {
conn._messageHandler(m.data);
} catch(e) {
printError(e.stack);
}
break;
}
});
/**
* Checks if the given path is remote
*
* @param {String} path to check
* @returns {Boolean} true if path is remote
*/
var isRemote = function(path) {
return (path.substr(0,7).toLowerCase() == 'http://' ||
path.substr(0,8).toLowerCase() == 'https://');
}
/**
* Loads and executes the JavaScript file with the given url
*
* @param {String} url of the script to load
*/
var importScript = function(url) {
var sCb = function() {
process.send({type: 'importSuccess', url: url});
}
var fCb = function() {
process.send({type: 'importFailure', url: url});
}
var run = function(code) {
executeNormal(code, url, sCb, fCb);
}
if (isRemote(url)) {
loadRemote(url, run, fCb);
} else {
try {
run(loadLocal(url));
} catch(e) {
printError(e.stack);
fCb();
}
}
}
/**
* Loads and executes the JavaScript file with the given url in a
* jailed environment
*
* @param {String} url of the script to load
*/
var importScriptJailed = function(url) {
var sCb = function() {
process.send({type: 'importSuccess', url: url});
}
var fCb = function() {
process.send({type: 'importFailure', url: url});
}
var run = function(code) {
executeJailed(code, url, sCb, fCb);
}
if (isRemote(url)) {
loadRemote(url, run, fCb);
} else {
try {
run(loadLocal(url));
} catch (e) {
printError(e.stack);
fCb();
}
}
}
/**
* Executes the given code in the jailed environment, sends the
* corresponding message to the application site when succeeded/failed
*
* @param {String} code to execute
*/
var execute = function(code) {
var sCb = function() {
process.send({type: 'executeSuccess'});
}
var fCb = function() {
process.send({type: 'executeFailure'});
}
executeJailed(code, 'DYNAMIC PLUGIN', sCb, fCb);
}
/**
* Executes the given code in the current environment / scope, runs
* the corresponding callback when done
*
* @param {String} code to execute
* @param {String} url of the script (for displaying the stack)
* @param {Function} sCb
* @param {Function} fCb
*/
var executeNormal = function(code, url, sCb, fCb) {
var err = null;
try {
require('vm').runInThisContext(code, url);
sCb();
} catch (e) {
printError(e.stack);
fCb();
}
}
/**
* Executes the given code in a jailed environment, runs the
* corresponding callback when done
*
* @param {String} code to execute
* @param {String} url of the script (for displaying the stack)
* @param {Function} sCb
* @param {Function} fCb
*/
var executeJailed = function(code, url, sCb, fCb) {
var vm = require('vm');
var sandbox = {};
var expose = [
'application',
'setTimeout',
'setInterval',
'clearTimeout',
'clearInterval'
];
for (var i = 0; i < expose.length; i++) {
sandbox[expose[i]] = global[expose[i]];
}
code = '"use strict";\n'+code;
try {
vm.runInNewContext(code, vm.createContext(sandbox), url);
sCb();
} catch (e) {
printError(e.stack);
fCb();
}
}
/**
* Loads local file and
*
* @param {String} path of the file to read
*
* @returns {String} file contents
*/
var loadLocal = function(path) {
return require("fs").readFileSync(path).toString();
}
/**
* Downloads the script by remote url and provides its content as a
* string to the callback
*
* @param {String} url of the remote module to load
* @param {Function} sCb success callback
* @param {Function} fCb failure callback
*/
var loadRemote = function(url, sCb, fCb) {
var receive = function(res) {
if (res.statusCode != 200) {
var msg = 'Failed to load ' + url + '\n' +
'HTTP responce status code: ' + res.statusCode;
printError(msg);
fCb();
} else {
var content = '';
res.on('end', function(){ sCb(content); });
res.on(
'readable',
function() {
var chunk = res.read();
content += chunk.toString();
}
);
}
}
try {
require('http').get(url, receive).on('error', fCb);
} catch (e) {
printError(e.stack);
fCb();
}
}
/**
* Connection object provided to the SandboxedSite constructor, plugin
* site implementation for the Node.js environment
*/
var conn = {
disconnect: function(){ process.exit(); },
send: function(data) {
process.send({type: 'message', data: data});
},
onMessage: function(h){ conn._messageHandler = h; },
_messageHandler: function(){},
onDisconnect: function() {}
};
connection = conn;

View File

@ -0,0 +1,96 @@
/**
* Contains the routines loaded by the plugin Worker under web-browser.
*
* Initializes the web environment version of the platform-dependent
* connection object for the plugin site
*/
self.application = {};
self.connection = {};
(function(){
/**
* Event lisener for the plugin message
*/
self.addEventListener('message', function(e){
var m = e.data.data;
switch (m.type) {
case 'import':
case 'importJailed': // already jailed in the Worker
importScript(m.url);
break;
case 'execute':
execute(m.code);
break;
case 'message':
conn._messageHandler(m.data);
break;
}
});
/**
* Loads and executes the JavaScript file with the given url
*
* @param {String} url to load
*/
var importScript = function(url) {
var error = null;
try {
importScripts(url);
} catch (e) {
error = e;
}
if (error) {
self.postMessage({type: 'importFailure', url: url});
throw error;
} else {
self.postMessage({type: 'importSuccess', url: url});
}
}
/**
* Executes the given code in a jailed environment. For web
* implementation, we're already jailed in the worker, so simply
* eval()
*
* @param {String} code code to execute
*/
var execute = function(code) {
try {
eval(code);
} catch (e) {
self.postMessage({type: 'executeFailure'});
throw e;
}
self.postMessage({type: 'executeSuccess'});
}
/**
* Connection object provided to the JailedSite constructor,
* plugin site implementation for the web-based environment.
* Global will be then cleared to prevent exposure into the
* Worker, so we put this local connection object into a closure
*/
var conn = {
disconnect: function(){ self.close(); },
send: function(data) {
self.postMessage({type: 'message', data: data});
},
onMessage: function(h){ conn._messageHandler = h; },
_messageHandler: function(){},
onDisconnect: function() {}
};
connection = conn;
})();

View File

@ -0,0 +1,780 @@
/**
* @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;
}));

View File

@ -10,6 +10,8 @@ block content
link(rel='stylesheet', href='/js/lib/codemirror/addon/lint/lint.css')
link(rel='stylesheet', href='/js/lib/codemirror/theme/monokai.css')
script(src='/js/lib/codemirror/mode/javascript/javascript.js')
script(src='js/lib/jailed/jailed.js')
script(src='/js/lib/bonfire/bonfire.js')
.row
.col-sm-12.col-md-8.col-xs-12
@ -24,6 +26,7 @@ block content
textarea#codeOutput
#submitButton.btn.btn-primary.btn-big.btn-block Run my code
#hintButton.btn.btn-info.btn-big.btn-block Show me hints
script.
var widgets = [];
var myCodeMirror = CodeMirror.fromTextArea(document.getElementById("codeEditor"), {
@ -33,22 +36,12 @@ block content
runnable: true,
autoCloseBrackets: true,
gutters: ["CodeMirror-lint-markers"],
onKeyEvent : doLinting()
onKeyEvent : doLinting
});
var editor = myCodeMirror;
myCodeMirror.setValue('2*2');
myCodeMirror.setSize("100%", 500);
$('#submitButton').on('click', function () {
$('#codeOutput').empty();
var js = myCodeMirror.getValue();
var s = document.createElement('script');
s.textContent = js;
try {
$('#codeOutput').append(eval(s.textContent));
} catch (e) {
$('#codeOutput').append(e);
}
});
var codeOutput = CodeMirror.fromTextArea(document.getElementById("codeOutput"), {
lineNumbers: false,
@ -56,7 +49,7 @@ block content
theme: 'monokai',
readOnly: 'nocursor'
});
codeOutput.setSize("100%", 30);
codeOutput.setSize("100%", 100);
var info = editor.getScrollInfo();
var after = editor.charCoords({line: editor.getCursor().line + 1, ch: 0}, "local").top;
@ -66,8 +59,8 @@ block content
editor.operation(function () {
for (var i = 0; i < widgets.length; ++i)
editor.removeLineWidget(widgets[i]);
widgets.length = 0;
JSHINT(editor.getValue());
widgets.length = 0;
JSHINT(editor.getValue());
for (var i = 0; i < JSHINT.errors.length; ++i) {
var err = JSHINT.errors[i];
if (!err) continue;
@ -78,12 +71,21 @@ block content
msg.appendChild(document.createTextNode(err.reason));
msg.className = "lint-error";
widgets.push(editor.addLineWidget(err.line - 1, msg, {
coverGutter: false,
noHScroll: true
coverGutter: false,
noHScroll: true
}));
}
});
};
$('#submitButton').on('click', function () {
$('#codeOutput').empty();
var js = myCodeMirror.getValue();
submit(js);
console.log('submitted');
});
.col-sm-12.col-md-4.col-xs-12
include ../partials/challenges