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