diff --git a/.gitignore b/.gitignore
index d19f89c7fc..d6eee2bf65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ node_modules
*.iml
.DS_Store
Thumbs.db
+bower_components
diff --git a/.jshintrc b/.jshintrc
index 163fa952ec..38058dd40b 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -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.
+}
\ No newline at end of file
diff --git a/app.js b/app.js
index 185f2dfa6c..6e8a2909a8 100644
--- a/app.js
+++ b/app.js
@@ -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;
diff --git a/controllers/bonfire.js b/controllers/bonfire.js
index 80455274fa..3192985c79 100644
--- a/controllers/bonfire.js
+++ b/controllers/bonfire.js
@@ -16,4 +16,5 @@ exports.index = function(req, res) {
cc: req.user ? req.user.challengesHash : undefined
});
});
-};
\ No newline at end of file
+};
+
diff --git a/public/js/.jshintrc b/public/js/.jshintrc
index 15deb7e9cc..728dab028a 100644
--- a/public/js/.jshintrc
+++ b/public/js/.jshintrc
@@ -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.
+}
\ No newline at end of file
diff --git a/public/js/lib/bonfire/bonfire.js b/public/js/lib/bonfire/bonfire.js
new file mode 100644
index 0000000000..e4cb64f853
--- /dev/null
+++ b/public/js/lib/bonfire/bonfire.js
@@ -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,'>').
+ replace(/\n/g, '
').
+ replace(/ /g, ' ');
+};
+
+
+// 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();
diff --git a/public/js/lib/bonfire/plugin.js b/public/js/lib/bonfire/plugin.js
new file mode 100644
index 0000000000..d2cc73b68b
--- /dev/null
+++ b/public/js/lib/bonfire/plugin.js
@@ -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});
diff --git a/public/js/lib/jailed/_JailedSite.js b/public/js/lib/jailed/_JailedSite.js
new file mode 100644
index 0000000000..a61b3d9bea
--- /dev/null
+++ b/public/js/lib/jailed/_JailedSite.js
@@ -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;
+ }
+
+
+})();
+
diff --git a/public/js/lib/jailed/_frame.html b/public/js/lib/jailed/_frame.html
new file mode 100644
index 0000000000..97d5bb947e
--- /dev/null
+++ b/public/js/lib/jailed/_frame.html
@@ -0,0 +1 @@
+
diff --git a/public/js/lib/jailed/_frame.js b/public/js/lib/jailed/_frame.js
new file mode 100644
index 0000000000..edf1b51793
--- /dev/null
+++ b/public/js/lib/jailed/_frame.js
@@ -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);
+});
+
diff --git a/public/js/lib/jailed/_pluginCore.js b/public/js/lib/jailed/_pluginCore.js
new file mode 100644
index 0000000000..6f75c07cc3
--- /dev/null
+++ b/public/js/lib/jailed/_pluginCore.js
@@ -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();
+ }
+
+})();
+
diff --git a/public/js/lib/jailed/_pluginNode.js b/public/js/lib/jailed/_pluginNode.js
new file mode 100644
index 0000000000..f3daa6f7e5
--- /dev/null
+++ b/public/js/lib/jailed/_pluginNode.js
@@ -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;
+
diff --git a/public/js/lib/jailed/_pluginWeb.js b/public/js/lib/jailed/_pluginWeb.js
new file mode 100644
index 0000000000..e3254155ec
--- /dev/null
+++ b/public/js/lib/jailed/_pluginWeb.js
@@ -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;
+
+})();
+
diff --git a/public/js/lib/jailed/jailed.js b/public/js/lib/jailed/jailed.js
new file mode 100644
index 0000000000..4650b8cbe3
--- /dev/null
+++ b/public/js/lib/jailed/jailed.js
@@ -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
+ *
+ * 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;
+
+}));
+
diff --git a/views/bonfire/bonfire.jade b/views/bonfire/bonfire.jade
index 812831673c..1ff3a8b5dd 100644
--- a/views/bonfire/bonfire.jade
+++ b/views/bonfire/bonfire.jade
@@ -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