diff --git a/client/commonFramework/add-faux-stream.js b/client/commonFramework/add-faux-stream.js
deleted file mode 100644
index 17a57e212c..0000000000
--- a/client/commonFramework/add-faux-stream.js
+++ /dev/null
@@ -1,24 +0,0 @@
-window.common = (function(global) {
- const {
- common = { init: [] }
- } = global;
-
-
- const faux$ = common.getScriptContent$('/js/faux.js').shareReplay();
-
- common.hasJs = function hasJs(code = '') {
- return code.match(/\<\s?script\s?\>/gi) &&
- code.match(/\<\s?\/\s?script\s?\>/gi);
- };
-
- common.addFaux$ = function addFaux$(code) {
- // grab user javaScript
- var scriptCode = code
- .split(/\<\s?script\s?\>/gi)[1]
- .split(/\<\s?\/\s?script\s?\>/gi)[0];
-
- return faux$.map(faux => faux + scriptCode);
- };
-
- return common;
-}(window));
diff --git a/client/commonFramework/add-loop-protect.js b/client/commonFramework/add-loop-protect.js
new file mode 100644
index 0000000000..65aebd0ed4
--- /dev/null
+++ b/client/commonFramework/add-loop-protect.js
@@ -0,0 +1,17 @@
+window.common = (function(global) {
+ const {
+ loopProtect,
+ common = { init: [] }
+ } = global;
+
+ loopProtect.hit = function hit(line) {
+ var err = `Error: Exiting potential infinite loop at line ${line}.`;
+ console.error(err);
+ };
+
+ common.addLoopProtect = function addLoopProtect(code = '') {
+ return loopProtect(code);
+ };
+
+ return common;
+})(window);
diff --git a/client/commonFramework/add-test-to-string.js b/client/commonFramework/add-test-to-string.js
deleted file mode 100644
index 52e78a82b1..0000000000
--- a/client/commonFramework/add-test-to-string.js
+++ /dev/null
@@ -1,38 +0,0 @@
-window.common = (function({ common = { init: [] }}) {
-
- var BDDregex = new RegExp(
- '(expect(\\s+)?\\(.*\\;)|' +
- '(assert(\\s+)?\\(.*\\;)|' +
- '(assert\\.\\w.*\\;)|' +
- '(.*\\.should\\..*\\;)/'
- );
-
- common.addTestsToString = function({ code, tests = [], ...rest }) {
- const userTests = [];
-
- code = tests.reduce((code, test) => code + test + '\n', code + '\n');
-
- var counter = 0;
- var match = BDDregex.exec(code);
-
- while (match) {
- var replacement = '//' + counter + common.salt;
- code = code.substring(0, match.index) +
- replacement +
- code.substring(match.index + match[0].length);
-
- userTests.push({
- err: null,
- text: match[0],
- line: counter
- });
-
- counter++;
- match = BDDregex.exec(code);
- }
-
- return { ...rest, code, userTests };
- };
-
- return common;
-}(window));
diff --git a/client/commonFramework/detect-loops-stream.js b/client/commonFramework/detect-loops-stream.js
deleted file mode 100644
index e60c49eea9..0000000000
--- a/client/commonFramework/detect-loops-stream.js
+++ /dev/null
@@ -1,79 +0,0 @@
-window.common = (function(global) {
- const {
- jailed,
- document: doc,
- Rx: { Observable, Disposable },
- common = { init: [] }
- } = global;
-
- if (!jailed) {
- return (code, cb) => cb(new Error('Could not load jailed plugin'));
- }
-
- // obtaining absolute path of this script
- var scripts = doc.getElementsByTagName('script');
- var path = scripts[scripts.length - 1].src
- .split('?')[0]
- .split('/')
- .slice(0, -1)
- .join('/') + '/';
-
- var Sandbox = {
- startTimeout() {
- this.timeoutId = setTimeout(() => {
- this.error = new Error('Plugin failed to initialize');
- this.destroyPlugin();
- }, 3000);
- },
- cancelTimout() {
- if (this.timeoutId) {
- clearTimeout(this.timeoutId);
- this.timeoutId = null;
- }
- },
- createPlugin() {
- this.plugin = new jailed.Plugin(path + 'plugin.js');
- this.startTimeout();
- this.plugin.whenConnected(() => {
- this.cancelTimout();
- });
- },
- destroyPlugin() {
- this.plugin.disconnect();
- }
- };
-
-
- // sends the input to the plugin for evaluation
- common.detectLoops$ = function detectLoops$({ code = '', ...rest }) {
- return Observable.create(function(observer) {
- const sandbox = Object.create(Sandbox);
-
- sandbox.createPlugin();
- sandbox.plugin.whenConnected(() => {
- sandbox.plugin.remote.run(code, (err, data) => {
- observer.onNext({ ...rest, err, code, data });
- observer.onCompleted();
- });
- });
-
- sandbox.plugin.whenDisconnected(() => {
- if (sandbox.disposed) {
- return null;
- }
-
- if (sandbox.error) {
- observer.onNext({ ...rest, err: sandbox.error, code, data: {} });
- }
- observer.onCompleted();
- });
-
- return new Disposable(() => {
- sandbox.disposed = true;
- sandbox.destroyPlugin();
- });
- });
- };
-
- return common;
-}(window));
diff --git a/client/commonFramework/end.js b/client/commonFramework/end.js
index 0c9e914521..811e154192 100644
--- a/client/commonFramework/end.js
+++ b/client/commonFramework/end.js
@@ -28,18 +28,7 @@ $(document).ready(function() {
// only run for HTML
.filter(() => common.challengeType === challengeTypes.HTML)
.flatMap(code => {
- if (
- common.hasJs(code)
- ) {
- return common.detectUnsafeCode$(code)
- .flatMap(code => common.detectLoops$(code))
- .flatMap(
- ({ err }) => err ? Observable.throw(err) : Observable.just(code)
- )
- .flatMap(code => common.updatePreview$(code))
- .catch(err => Observable.just({ err }));
- }
- return Observable.just(code)
+ return common.detectUnsafeCode$(code)
.flatMap(code => common.updatePreview$(code))
.catch(err => Observable.just({ err }));
})
@@ -66,12 +55,12 @@ $(document).ready(function() {
.catch(err => Observable.just({ err }));
})
.subscribe(
- ({ err, output, original }) => {
+ ({ err, output, originalCode }) => {
if (err) {
console.error(err);
return common.updateOutputDisplay('' + err);
}
- common.codeStorage.updateStorage(challengeName, original);
+ common.codeStorage.updateStorage(challengeName, originalCode);
common.updateOutputDisplay('' + output);
},
(err) => {
@@ -102,11 +91,11 @@ $(document).ready(function() {
if (common.challengeType === common.challengeTypes.HTML) {
return common.updatePreview$(`
${err}
- `).subscribe(() => {});
+ `).first().subscribe(() => {});
}
return common.updateOutputDisplay('' + err);
}
- common.updateOutputDisplay(output);
+ common.updateOutputDisplay('' + output);
common.displayTestResults(tests);
if (solved) {
common.showCompletion();
@@ -153,12 +142,12 @@ $(document).ready(function() {
.flatMap(() => common.executeChallenge$())
.catch(err => Observable.just({ err }))
.subscribe(
- ({ err, original, tests }) => {
+ ({ err, originalCode, tests }) => {
if (err) {
console.error(err);
return common.updateOutputDisplay('' + err);
}
- common.codeStorage.updateStorage(challengeName, original);
+ common.codeStorage.updateStorage(challengeName, originalCode);
common.displayTestResults(tests);
},
(err) => {
diff --git a/client/commonFramework/execute-challenge-stream.js b/client/commonFramework/execute-challenge-stream.js
index 181868e581..21aabb5b31 100644
--- a/client/commonFramework/execute-challenge-stream.js
+++ b/client/commonFramework/execute-challenge-stream.js
@@ -1,16 +1,17 @@
-// what are the executeChallenge functions?
-// Should be responsible for starting after a submit action
-// Should not be responsible for displaying results
-// Should return results
-// should grab editor value
-// depends on main editor
window.common = (function(global) {
const {
ga,
- Rx: { Observable },
common = { init: [] }
} = global;
+ const {
+ addLoopProtect,
+ detectUnsafeCode$,
+ updatePreview$,
+ challengeType,
+ challengeTypes
+ } = common;
+
let attempts = 0;
common.executeChallenge$ = function executeChallenge$() {
@@ -18,67 +19,39 @@ window.common = (function(global) {
const originalCode = code;
const head = common.arrayToNewLineString(common.head);
const tail = common.arrayToNewLineString(common.tail);
+ const combinedCode = head + code + tail;
attempts++;
ga('send', 'event', 'Challenge', 'ran-code', common.challengeName);
// run checks for unsafe code
- return common.detectUnsafeCode$(code)
+ return detectUnsafeCode$(code)
// add head and tail and detect loops
- .map(code => head + code + tail)
- .flatMap(code => {
- if (common.challengeType === common.challengeTypes.HTML) {
-
- if (common.hasJs(code)) {
- // html has a script code
- // add faux code and test in webworker
- return common.addFaux$(code)
- .flatMap(code => common.detectLoops$(code))
- .flatMap(({ err }) => {
- if (err) {
- return Observable.throw(err);
- }
- return common.updatePreview$(code)
- .flatMap(() => common.runPreviewTests$({
- code,
- tests: common.tests.slice()
- }));
- });
- }
-
- // no script code detected in html code
- // Update preview and run tests in iframe
- return common.updatePreview$(code)
- .flatMap(code => common.runPreviewTests$({
- code,
- tests: common.tests.slice()
- }));
+ .map(() => {
+ if (challengeType !== challengeTypes.HTML) {
+ return ``;
}
- // js challenge
- // remove comments and add tests to string
- return Observable.just(common.addTestsToString(Object.assign(
- {
- code: common.removeComments(code),
- tests: common.tests.slice()
- }
- )))
- .flatMap(common.detectLoops$)
- .flatMap(({ err, code, data, userTests }) => {
- if (err) {
- return Observable.throw(err);
- }
+ return addLoopProtect(combinedCode);
+ })
+ .flatMap(code => updatePreview$(code))
+ .flatMap(code => {
+ let output;
- // run tests
- // for now these are running in the browser
- return common.runTests$({
- data,
- code,
- userTests,
- originalCode,
- output: data.output.replace(/\\\"/gi, '')
- });
+ if (
+ challengeType === challengeTypes.HTML &&
+ common.hasJs(code)
+ ) {
+ output = common.getJsOutput(common.getJsFromHtml(code));
+ } else if (challengeType !== challengeTypes.HTML) {
+ output = common.getJsOutput(combinedCode);
+ }
+
+ return common.runPreviewTests$({
+ tests: common.tests.slice(),
+ originalCode,
+ output
});
});
};
diff --git a/client/commonFramework/get-iframe.js b/client/commonFramework/get-iframe.js
new file mode 100644
index 0000000000..012dce7b50
--- /dev/null
+++ b/client/commonFramework/get-iframe.js
@@ -0,0 +1,23 @@
+window.common = (function(global) {
+ const {
+ common = { init: [] },
+ document: doc
+ } = global;
+
+ common.getIframe = function getIframe(id = 'preview') {
+ let previewFrame = doc.getElementById(id);
+
+ // create and append a hidden preview frame
+ if (!previewFrame) {
+ previewFrame = doc.createElement('iframe');
+ previewFrame.id = id;
+ previewFrame.setAttribute('style', 'display: none');
+ doc.body.appendChild(previewFrame);
+ }
+
+ return previewFrame.contentDocument ||
+ previewFrame.contentWindow.document;
+ };
+
+ return common;
+})(window);
diff --git a/client/commonFramework/init.js b/client/commonFramework/init.js
index 09add87bf7..ff4130efd1 100644
--- a/client/commonFramework/init.js
+++ b/client/commonFramework/init.js
@@ -102,5 +102,19 @@ window.common = (function(global) {
});
};
+ const openScript = /\<\s?script\s?\>/gi;
+ const closingScript = /\<\s?\/\s?script\s?\>/gi;
+
+ // detects if there is JavaScript in the first script tag
+ common.hasJs = function hasJs(code) {
+ return !!common.getJsFromHtml(code);
+ };
+
+ // grabs the content from the first script tag in the code
+ common.getJsFromHtml = function getJsFromHtml(code) {
+ // grab user javaScript
+ return (code.split(openScript)[1] || '').split(closingScript)[0] || '';
+ };
+
return common;
})(window);
diff --git a/client/commonFramework/update-preview.js b/client/commonFramework/update-preview.js
index 8e2b7d8bcb..744c73e2b2 100644
--- a/client/commonFramework/update-preview.js
+++ b/client/commonFramework/update-preview.js
@@ -4,7 +4,13 @@ window.common = (function(global) {
common = { init: [] }
} = global;
+ // the first script tag here is to proxy jQuery
+ // We use the same jQuery on the main window but we change the
+ // context to that of the iframe.
var libraryIncludes = `
+
Observable.throw({ err: new Error('run preview not enabled') });
+ () => Observable.throw(new Error('run preview not enabled'));
common.updatePreview$ = function updatePreview$(code = '') {
- const previewFrame = document.getElementById('preview');
- const preview = previewFrame.contentDocument ||
- previewFrame.contentWindow.document;
- if (!preview) {
- return Observable.just(code);
- }
+ const preview = common.getIframe('preview');
return iFrameScript$
.map(script => ``)
@@ -55,7 +56,10 @@ window.common = (function(global) {
// now we filter false values and wait for the first true
return common.previewReady$
.filter(ready => ready)
- .first();
+ .first()
+ // the delay here is to give code within the iframe
+ // control to run
+ .delay(100);
})
.map(() => code);
};
diff --git a/client/faux.js b/client/faux.js
deleted file mode 100644
index ca4563f0d8..0000000000
--- a/client/faux.js
+++ /dev/null
@@ -1,275 +0,0 @@
-/* eslint-disable no-unused-vars */
-var document = {};
-var navigator = function() {
- this.geolocation = function() {
- this.getCurrentPosition = function() {
- this.coords = {latitude: '', longitude: '' };
- return this;
- };
- return this;
- };
- return this;
-};
-function $() {
- if (!(this instanceof $)) {
- return new $();
- }
-}
-function returnThis() { return this; }
-function return$() { return $; }
-var methods = [
- 'add',
- 'addBack',
- 'addClass',
- 'after',
- 'ajaxComplete',
- 'ajaxError',
- 'ajaxSend',
- 'ajaxStart',
- 'ajaxStop',
- 'ajaxSuccess',
- 'andSelf',
- 'animate',
- 'append',
- 'appendTo',
- 'attr',
- 'before',
- 'bind',
- 'blur',
- 'callbacksadd',
- 'callbacksdisable',
- 'callbacksdisabled',
- 'callbacksempty',
- 'callbacksfire',
- 'callbacksfired',
- 'callbacksfireWith',
- 'callbackshas',
- 'callbackslock',
- 'callbackslocked',
- 'callbacksremove',
- 'change',
- 'children',
- 'clearQueue',
- 'click',
- 'clone',
- 'closest',
- 'contents',
- 'context',
- 'css',
- 'data',
- 'dblclick',
- 'delay',
- 'delegate',
- 'dequeue',
- 'detach',
- 'die',
- 'each',
- 'empty',
- 'end',
- 'eq',
- 'error',
- 'fadeIn',
- 'fadeOut',
- 'fadeTo',
- 'fadeToggle',
- 'filter',
- 'find',
- 'finish',
- 'first',
- 'focus',
- 'focusin',
- 'focusout',
- 'get',
- 'has',
- 'hasClass',
- 'height',
- 'hide',
- 'hover',
- 'html',
- 'index',
- 'innerHeight',
- 'innerWidth',
- 'insertAfter',
- 'insertBefore',
- 'is',
- 'jQuery',
- 'jquery',
- 'keydown',
- 'keypress',
- 'keyup',
- 'last',
- 'length',
- 'live',
- 'load',
- 'load',
- 'map',
- 'mousedown',
- 'mouseenter',
- 'mouseleave',
- 'mousemove',
- 'mouseout',
- 'mouseover',
- 'mouseup',
- 'next',
- 'nextAll',
- 'nextUntil',
- 'not',
- 'off',
- 'offset',
- 'offsetParent',
- 'on',
- 'one',
- 'outerHeight',
- 'outerWidth',
- 'parent',
- 'parents',
- 'parentsUntil',
- 'position',
- 'prepend',
- 'prependTo',
- 'prev',
- 'prevAll',
- 'prevUntil',
- 'promise',
- 'prop',
- 'pushStack',
- 'queue',
- 'ready',
- 'remove',
- 'removeAttr',
- 'removeClass',
- 'removeData',
- 'removeProp',
- 'replaceAll',
- 'replaceWith',
- 'resize',
- 'scroll',
- 'scrollLeft',
- 'scrollTop',
- 'select',
- 'selector',
- 'serialize',
- 'serializeArray',
- 'show',
- 'siblings',
- 'size',
- 'slice',
- 'slideDown',
- 'slideToggle',
- 'slideUp',
- 'stop',
- 'submit',
- 'text',
- 'toArray',
- 'toggle',
- 'toggle',
- 'toggleClass',
- 'trigger',
- 'triggerHandler',
- 'unbind',
- 'undelegate',
- 'unload',
- 'unwrap',
- 'val',
- 'width',
- 'wrap',
- 'wrapAll',
- 'wrapInner'
-];
-
-var statics = [
- 'ajax',
- 'ajaxPrefilter',
- 'ajaxSetup',
- 'ajaxTransport',
- 'boxModel',
- 'browser',
- 'Callbacks',
- 'contains',
- 'cssHooks',
- 'cssNumber',
- 'data',
- 'Deferred',
- 'dequeue',
- 'each',
- 'error',
- 'extend',
- 'fnextend',
- 'fxinterval',
- 'fxoff',
- 'get',
- 'getJSON',
- 'getScript',
- 'globalEval',
- 'grep',
- 'hasData',
- 'holdReady',
- 'inArray',
- 'isArray',
- 'isEmptyObject',
- 'isFunction',
- 'isNumeric',
- 'isPlainObject',
- 'isWindow',
- 'isXMLDoc',
- 'makeArray',
- 'map',
- 'merge',
- 'noConflict',
- 'noop',
- 'now',
- 'param',
- 'parseHTML',
- 'parseJSON',
- 'parseXML',
- 'post',
- 'proxy',
- 'queue',
- 'removeData',
- 'sub',
- 'support',
- 'trim',
- 'type',
- 'unique',
- 'when',
- 'always',
- 'done',
- 'fail',
- 'isRejected',
- 'isResolved',
- 'notify',
- 'notifyWith',
- 'pipe',
- 'progress',
- 'promise',
- 'reject',
- 'rejectWith',
- 'resolve',
- 'resolveWith',
- 'state',
- 'then',
- 'currentTarget',
- 'data',
- 'delegateTarget',
- 'isDefaultPrevented',
- 'isImmediatePropagationStopped',
- 'isPropagationStopped',
- 'metaKey',
- 'namespace',
- 'pageX',
- 'pageY',
- 'preventDefault',
- 'relatedTarget',
- 'result',
- 'stopImmediatePropagation',
- 'stopPropagation',
- 'target',
- 'timeStamp',
- 'type',
- 'which'
-];
-
-var $Proto = {};
-methods.forEach(method => $Proto[method] = returnThis);
-statics.forEach(staticMeth => $[staticMeth] = return$);
-$.prototype = Object.create($);
diff --git a/client/iFrameScripts.js b/client/iFrameScripts.js
index 7bbaa3eb10..3011d4272e 100644
--- a/client/iFrameScripts.js
+++ b/client/iFrameScripts.js
@@ -1,21 +1,43 @@
/* eslint-disable no-undef, no-unused-vars, no-native-reassign */
-window.__$ = parent.$;
-window.__$(function() {
+// the $ on the iframe window object is the same
+// as the one used on the main site, but
+// uses the iframe document as the context
+window.$(document).ready(function() {
var _ = parent._;
var Rx = parent.Rx;
var chai = parent.chai;
var assert = chai.assert;
var tests = parent.tests;
var common = parent.common;
- var editor = common.editor.getValue();
- // change the context of $ so it uses the iFrame for testing
- var $ = __$.proxy(__$.fn.find, __$(document));
+
+ common.getJsOutput = function evalJs(code = '') {
+ let output;
+ try {
+ /* eslint-disable no-eval */
+ output = eval(code);
+ /* eslint-enable no-eval */
+ } catch (e) {
+ window.__err = e;
+ }
+ return output;
+ };
common.runPreviewTests$ =
- function runPreviewTests$({ tests = [], ...rest }) {
+ function runPreviewTests$({
+ tests = [],
+ originalCode,
+ ...rest
+ }) {
+ const code = originalCode;
+ const editor = { getValue() { return originalCode; } };
+ if (window.__err) {
+ return Rx.Observable.throw(window.__err);
+ }
+
return Rx.Observable.from(tests)
.map(test => {
const userTest = {};
+ common.appendToOutputDisplay('');
try {
/* eslint-disable no-eval */
eval(test);
@@ -32,12 +54,13 @@ window.__$(function() {
return userTest;
})
.toArray()
- .map(tests => ({ ...rest, tests }));
+ .map(tests => ({ ...rest, tests, originalCode }));
};
// now that the runPreviewTest$ is defined
// we set the subject to true
// this will let the updatePreview
// script now that we are ready.
+ console.log('second');
common.previewReady$.onNext(true);
});
diff --git a/gulpfile.js b/gulpfile.js
index 3720c9ad72..b8311461b3 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -18,6 +18,7 @@ var Rx = require('rx'),
uglify = require('gulp-uglify'),
merge = require('merge-stream'),
babel = require('gulp-babel'),
+ sourcemaps = require('gulp-sourcemaps'),
// react app
webpack = require('webpack-stream'),
@@ -85,7 +86,8 @@ var paths = {
'public/bower_components/CodeMirror/mode/xml/xml.js',
'public/bower_components/CodeMirror/mode/css/css.js',
'public/bower_components/CodeMirror/mode/htmlmixed/htmlmixed.js',
- 'node_modules/emmet-codemirror/dist/emmet.js'
+ 'node_modules/emmet-codemirror/dist/emmet.js',
+ 'public/js/lib/loop-protect/loop-protect.js'
],
vendorMain: [
@@ -103,20 +105,20 @@ var paths = {
js: [
'client/main.js',
'client/iFrameScripts.js',
- 'client/plugin.js',
- 'client/faux.js'
+ 'client/plugin.js'
],
commonFramework: [
'init',
'bindings',
'add-test-to-string',
- 'add-faux-stream',
'code-storage',
'code-uri',
+ 'add-loop-protect',
+ 'get-iframe',
+ 'update-preview',
'create-editor',
'detect-unsafe-code-stream',
- 'detect-loops-stream',
'display-test-results',
'execute-challenge-stream',
'output-display',
@@ -125,7 +127,6 @@ var paths = {
'run-tests-stream',
'show-completion',
'step-challenge',
- 'update-preview',
'end'
],
@@ -434,7 +435,9 @@ gulp.task('dependents', ['js'], function() {
return gulp.src(formatCommonFrameworkPaths.call(paths.commonFramework))
.pipe(plumber({ errorHandler: errorHandler }))
.pipe(babel())
+ .pipe(__DEV__ ? sourcemaps.init() : gutil.noop())
.pipe(concat('commonFramework.js'))
+ .pipe(__DEV__ ? sourcemaps.write() : gutil.noop())
.pipe(__DEV__ ? gutil.noop() : uglify())
.pipe(revReplace({ manifest: manifest }))
.pipe(gulp.dest(dest))
diff --git a/package.json b/package.json
index 6a9b3990dc..01be061ba0 100644
--- a/package.json
+++ b/package.json
@@ -134,6 +134,7 @@
"browser-sync": "^2.9.12",
"chai": "^3.4.0",
"envify": "^3.4.0",
+ "gulp-sourcemaps": "^1.6.0",
"istanbul": "~0.4.0",
"jsonlint": "^1.6.2",
"loopback-component-explorer": "^2.1.1",
diff --git a/public/js/lib/loop-protect/LICENSE.md b/public/js/lib/loop-protect/LICENSE.md
new file mode 100644
index 0000000000..9c4f96a8ec
--- /dev/null
+++ b/public/js/lib/loop-protect/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 JS Bin Ltd
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/public/js/lib/loop-protect/loop-protect.js b/public/js/lib/loop-protect/loop-protect.js
new file mode 100644
index 0000000000..5050605b9e
--- /dev/null
+++ b/public/js/lib/loop-protect/loop-protect.js
@@ -0,0 +1,432 @@
+if (typeof DEBUG === 'undefined') { DEBUG = true; }
+
+(function (root, factory) {
+ 'use strict';
+ /*global define*/
+ if (typeof define === 'function' && define.amd) {
+ define(factory(root));
+ } else if (typeof exports === 'object') {
+ module.exports = factory(root);
+ } else {
+ root.loopProtect = factory(root);
+ }
+})(this, function loopProtectModule(root) {
+ /*global DEBUG*/
+ 'use strict';
+ var debug = null;
+
+ // the standard loops - note that recursive is not supported
+ var re = /\b(for|while|do)\b/g;
+ var reSingle = /\b(for|while|do)\b/;
+ var labelRe = /\b([a-z_]{1}\w+:)/i;
+ var comments = /(?:\/\*(?:[\s\S]*?)\*\/)|(?:([\s;])+\/\/(?:.*)$)/gm;
+
+ var loopProtect = rewriteLoops;
+
+ // used in the loop detection
+ loopProtect.counters = {};
+
+ // expose debug info
+ loopProtect.debug = function debugSwitch(state) {
+ debug = state ? function () {
+ console.log.apply(console, [].slice.apply(arguments));
+ } : function () {};
+ };
+
+ loopProtect.debug(false); // off by default
+
+ // the method - as this could be aliased to something else
+ loopProtect.alias = 'loopProtect';
+
+ function inMultilineComment(lineNum, lines) {
+ if (lineNum === 0) {
+ return false;
+ }
+
+ var j = lineNum;
+ var closeCommentTags = 1; // let's assume we're inside a comment
+ var closePos = -1;
+ var openPos = -1;
+
+ do {
+ j -= 1;
+ DEBUG && debug('looking backwards ' + lines[j]); // jshint ignore:line
+ closePos = lines[j].indexOf('*/');
+ openPos = lines[j].indexOf('/*');
+
+ if (closePos !== -1) {
+ closeCommentTags++;
+ }
+
+ if (openPos !== -1) {
+ closeCommentTags--;
+
+ if (closeCommentTags === 0) {
+ DEBUG && debug('- exit: part of a multiline comment'); // jshint ignore:line
+ return true;
+ }
+ }
+ } while (j !== 0);
+
+ return false;
+ }
+
+ function inCommentOrString(index, line) {
+ var character;
+ while (--index > -1) {
+ character = line.substr(index, 1);
+ if (character === '"' || character === '\'' || character === '.') {
+ // our loop keyword was actually either in a string or a property, so let's exit and ignore this line
+ DEBUG && debug('- exit: matched inside a string or property key'); // jshint ignore:line
+ return true;
+ }
+ if (character === '/' || character === '*') {
+ // looks like a comment, go back one to confirm or not
+ --index;
+ if (character === '/') {
+ // we've found a comment, so let's exit and ignore this line
+ DEBUG && debug('- exit: part of a comment'); // jshint ignore:line
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ function directlyBeforeLoop(index, lineNum, lines) {
+ reSingle.lastIndex = 0;
+ labelRe.lastIndex = 0;
+ var beforeLoop = false;
+
+ var theRest = lines.slice(lineNum).join('\n').substr(index).replace(labelRe, '');
+ theRest.replace(reSingle, function commentStripper(match, capture, i) {
+ var target = theRest.substr(0, i).replace(comments, '').trim();
+ DEBUG && debug('- directlyBeforeLoop: ' + target); // jshint ignore:line
+ if (target.length === 0) {
+ beforeLoop = true;
+ }
+ // strip comments out of the target, and if there's nothing else
+ // it's a valid label...I hope!
+ });
+
+ return beforeLoop;
+ }
+
+ /**
+ * Look for for, while and do loops, and inserts *just* at the start of the
+ * loop, a check function.
+ */
+ function rewriteLoops(code, offset) {
+ var recompiled = [];
+ var lines = code.split('\n');
+ var disableLoopProtection = false;
+ var method = loopProtect.alias + '.protect';
+ var ignore = {};
+ var pushonly = {};
+ var labelPostion = null;
+
+ function insertReset(lineNum, line, matchPosition) {
+ // recompile the line with the reset **just** before the actual loop
+ // so that we insert in to the correct location (instead of possibly
+ // outside the logic
+ return line.slice(0, matchPosition) + ';' + method + '({ line: ' + lineNum + ', reset: true }); ' + line.slice(matchPosition);
+ };
+
+ if (!offset) {
+ offset = 0;
+ }
+
+ lines.forEach(function eachLine(line, lineNum) {
+ // reset our regexp each time.
+ re.lastIndex = 0;
+ labelRe.lastIndex = 0;
+
+ if (disableLoopProtection) {
+ return;
+ }
+
+ if (line.toLowerCase().indexOf('noprotect') !== -1) {
+ disableLoopProtection = true;
+ }
+
+ var index = -1;
+ var matchPosition = -1;
+ var originalLineNum = lineNum;
+ // +1 since we're humans and don't read lines numbers from zero
+ var printLineNumber = lineNum - offset + 1;
+ var character = '';
+ // special case for `do` loops, as they're end with `while`
+ var dofound = false;
+ var findwhile = false;
+ var terminator = false;
+ var matches = line.match(re) || [];
+ var match = matches.length ? matches[0] : '';
+ var labelMatch = line.match(labelRe) || [];
+ var openBrackets = 0;
+ var openBraces = 0;
+
+ if (labelMatch.length) {
+ DEBUG && debug('- label match'); // jshint ignore:line
+ index = line.indexOf(labelMatch[1]);
+ if (!inCommentOrString(index, line)) {
+ if (!inMultilineComment(lineNum, lines)) {
+ if (directlyBeforeLoop(index, lineNum, lines)) {
+ DEBUG && debug('- found a label: "' + labelMatch[0] + '"'); // jshint ignore:line
+ labelPostion = lineNum;
+ } else {
+ DEBUG && debug('- ignored "label", false positive'); // jshint ignore:line
+ }
+ } else {
+ DEBUG && debug('- ignored label in multline comment'); // jshint ignore:line
+ }
+ } else {
+ DEBUG && debug('- ignored label in string or comment'); // jshint ignore:line
+ }
+ }
+
+ if (ignore[lineNum]) {
+ DEBUG && debug(' -exit: ignoring line ' + lineNum +': ' + line); // jshint ignore:line
+ return;
+ }
+
+ if (pushonly[lineNum]) {
+ DEBUG && debug('- exit: ignoring, but adding line ' + lineNum + ': ' + line); // jshint ignore:line
+ recompiled.push(line);
+ return;
+ }
+
+ // if there's more than one match, we just ignore this kind of loop
+ // otherwise I'm going to be writing a full JavaScript lexer...and god
+ // knows I've got better things to be doing.
+ if (match && matches.length === 1 && line.indexOf('jsbin') === -1) {
+ DEBUG && debug('match on ' + match + '\n'); // jshint ignore:line
+
+ // there's a special case for protecting `do` loops, we need to first
+ // prtect the `do`, but then ignore the closing `while` statement, so
+ // we reset the search state for this special case.
+ dofound = match === 'do';
+
+ // make sure this is an actual loop command by searching backwards
+ // to ensure it's not a string, comment or object property
+ matchPosition = index = line.indexOf(match);
+
+ // first we need to walk backwards to ensure that our match isn't part
+ // of a string or part of a comment
+ if (inCommentOrString(index, line)) {
+ recompiled.push(line);
+ return;
+ }
+
+ // it's quite possible we're in the middle of a multiline
+ // comment, so we'll cycle up looking for an opening comment,
+ // and if there's one (and not a closing `*/`), then we'll
+ // ignore this line as a comment
+ if (inMultilineComment(lineNum, lines)) {
+ recompiled.push(line);
+ return;
+ }
+
+ // now work our way forward to look for '{'
+ index = line.indexOf(match) + match.length;
+
+ if (index === line.length) {
+ if (index === line.length && lineNum < (lines.length-1)) {
+ // move to the next line
+ DEBUG && debug('- moving to next line'); // jshint ignore:line
+ recompiled.push(line);
+ lineNum++;
+ line = lines[lineNum];
+ ignore[lineNum] = true;
+ index = 0;
+ }
+
+ }
+
+ while (index < line.length) {
+ character = line.substr(index, 1);
+ // DEBUG && debug(character, index); // jshint ignore:line
+
+ if (character === '(') {
+ openBrackets++;
+ }
+
+ if (character === ')') {
+ openBrackets--;
+
+ if (openBrackets === 0 && terminator === false) {
+ terminator = index;
+ }
+ }
+
+ if (character === '{') {
+ openBraces++;
+ }
+
+ if (character === '}') {
+ openBraces--;
+ }
+
+ if (openBrackets === 0 && (character === ';' || character === '{')) {
+ // if we're a non-curlies loop, then convert to curlies to get our code inserted
+ if (character === ';') {
+ if (lineNum !== originalLineNum) {
+ DEBUG && debug('- multiline inline loop'); // jshint ignore:line
+ // affect the compiled line
+ recompiled[originalLineNum] = recompiled[originalLineNum].substring(0, terminator + 1) + '{\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n' + recompiled[originalLineNum].substring(terminator + 1);
+ line += '\n}\n';
+ } else {
+ // simpler
+ DEBUG && debug('- single line inline loop'); // jshint ignore:line
+ line = line.substring(0, terminator + 1) + '{\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n' + line.substring(terminator + 1) + '\n}\n';
+ }
+
+ } else if (character === '{') {
+ DEBUG && debug('- multiline with braces'); // jshint ignore:line
+ var insert = ';\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n';
+ line = line.substring(0, index + 1) + insert + line.substring(index + 1);
+
+ index += insert.length;
+ }
+
+ // work out where to put the reset
+ if (lineNum === originalLineNum && labelPostion === null) {
+ DEBUG && debug('- simple reset insert'); // jshint ignore:line
+ line = insertReset(printLineNumber, line, matchPosition);
+ index += (';' + method + '({ line: ' + lineNum + ', reset: true }); ').length;
+ } else {
+ // insert the reset above the originalLineNum OR if this loop used
+ // a label, we have to insert the reset *above* the label
+ if (labelPostion === null) {
+ DEBUG && debug('- reset inserted above original line'); // jshint ignore:line
+ recompiled[originalLineNum] = insertReset(printLineNumber, recompiled[originalLineNum], matchPosition);
+ } else {
+ DEBUG && debug('- reset inserted above matched label on line ' + labelPostion); // jshint ignore:line
+ if (recompiled[labelPostion] === undefined) {
+ labelPostion--;
+ matchPosition = 0;
+ }
+ recompiled[labelPostion] = insertReset(printLineNumber, recompiled[labelPostion], matchPosition);
+ labelPostion = null;
+ }
+ }
+
+ recompiled.push(line);
+
+ if (!dofound) {
+ return;
+ } else {
+ DEBUG && debug('searching for closing `while` statement for: ' + line); // jshint ignore:line
+ // cycle forward until we find the close brace, after which should
+ // be our while statement to ignore
+ findwhile = false;
+ while (index < line.length) {
+ character = line.substr(index, 1);
+
+ if (character === '{') {
+ openBraces++;
+ }
+
+ if (character === '}') {
+ openBraces--;
+ }
+
+ if (openBraces === 0) {
+ findwhile = true;
+ } else {
+ findwhile = false;
+ }
+
+ if (openBraces === 0) {
+ DEBUG && debug('outside of closure, looking for `while` statement: ' + line); // jshint ignore:line
+ }
+
+ if (findwhile && line.indexOf('while') !== -1) {
+ DEBUG && debug('- exit as we found `while`: ' + line); // jshint ignore:line
+ pushonly[lineNum] = true;
+ return;
+ }
+
+ index++;
+
+ if (index === line.length && lineNum < (lines.length-1)) {
+ lineNum++;
+ line = lines[lineNum];
+ DEBUG && debug(line); // jshint ignore:line
+ index = 0;
+ }
+ }
+ return;
+ }
+ }
+
+ index++;
+
+ if (index === line.length && lineNum < (lines.length-1)) {
+ // move to the next line
+ DEBUG && debug('- moving to next line'); // jshint ignore:line
+ recompiled.push(line);
+ lineNum++;
+ line = lines[lineNum];
+ ignore[lineNum] = true;
+ index = 0;
+ }
+ }
+ } else {
+ // else we're a regular line, and we shouldn't be touched
+ DEBUG && debug('regular line ' + line); // jshint ignore:line
+ recompiled.push(line);
+ }
+ });
+
+ DEBUG && debug('---- source ----'); // jshint ignore:line
+ DEBUG && debug(code); // jshint ignore:line
+ DEBUG && debug('---- rewrite ---'); // jshint ignore:line
+ DEBUG && debug(recompiled.join('\n')); // jshint ignore:line
+ DEBUG && debug(''); // jshint ignore:line
+
+ return disableLoopProtection ? code : recompiled.join('\n');
+ };
+
+ /**
+ * Injected code in to user's code to **try** to protect against infinite
+ * loops cropping up in the code, and killing the browser. Returns true
+ * when the loops has been running for more than 100ms.
+ */
+ loopProtect.protect = function protect(state) {
+ loopProtect.counters[state.line] = loopProtect.counters[state.line] || {};
+ var line = loopProtect.counters[state.line];
+ var now = (new Date()).getTime();
+
+ if (state.reset) {
+ line.time = now;
+ line.hit = 0;
+ line.last = 0;
+ }
+
+ line.hit++;
+ if ((now - line.time) > 100) {//} && line.hit !== line.last+1) {
+ // We've spent over 100ms on this loop... smells infinite.
+ loopProtect.hit(state.line);
+ // Returning true prevents the loop running again
+ return true;
+ }
+ line.last++;
+ return false;
+ };
+
+ loopProtect.hit = function hit(line) {
+ var msg = 'Exiting potential infinite loop at line ' + line + '. To disable loop protection: add "// noprotect" to your code';
+ if (root.proxyConsole) {
+ root.proxyConsole.error(msg);
+ } else {
+ console.error(msg);
+ }
+ };
+
+ loopProtect.reset = function reset() {
+ // reset the counters
+ loopProtect.counters = {};
+ };
+
+ return loopProtect;
+});