Add loop-protect
Remove webworkers
This commit is contained in:
@ -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));
|
17
client/commonFramework/add-loop-protect.js
Normal file
17
client/commonFramework/add-loop-protect.js
Normal file
@ -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);
|
@ -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));
|
@ -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));
|
@ -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)
|
||||
.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$(`
|
||||
<h1>${err}</h1>
|
||||
`).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) => {
|
||||
|
@ -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)
|
||||
.map(() => {
|
||||
if (challengeType !== challengeTypes.HTML) {
|
||||
return `<script>;${addLoopProtect(combinedCode)}/**/</script>`;
|
||||
}
|
||||
|
||||
return addLoopProtect(combinedCode);
|
||||
})
|
||||
.flatMap(code => updatePreview$(code))
|
||||
.flatMap(code => {
|
||||
if (common.challengeType === common.challengeTypes.HTML) {
|
||||
let output;
|
||||
|
||||
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()
|
||||
}));
|
||||
});
|
||||
if (
|
||||
challengeType === challengeTypes.HTML &&
|
||||
common.hasJs(code)
|
||||
) {
|
||||
output = common.getJsOutput(common.getJsFromHtml(code));
|
||||
} else if (challengeType !== challengeTypes.HTML) {
|
||||
output = common.getJsOutput(combinedCode);
|
||||
}
|
||||
|
||||
// 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()
|
||||
}));
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// run tests
|
||||
// for now these are running in the browser
|
||||
return common.runTests$({
|
||||
data,
|
||||
code,
|
||||
userTests,
|
||||
return common.runPreviewTests$({
|
||||
tests: common.tests.slice(),
|
||||
originalCode,
|
||||
output: data.output.replace(/\\\"/gi, '')
|
||||
});
|
||||
output
|
||||
});
|
||||
});
|
||||
};
|
||||
|
23
client/commonFramework/get-iframe.js
Normal file
23
client/commonFramework/get-iframe.js
Normal file
@ -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);
|
@ -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);
|
||||
|
@ -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 = `
|
||||
<script>
|
||||
window.$ = parent.$.proxy(parent.$.fn.find, parent.$(document));
|
||||
</script>
|
||||
<link
|
||||
rel='stylesheet'
|
||||
href='//cdnjs.cloudflare.com/ajax/libs/animate.css/3.2.0/animate.min.css'
|
||||
@ -33,15 +39,10 @@ window.common = (function(global) {
|
||||
|
||||
// runPreviewTests$ should be set up in the preview window
|
||||
common.runPreviewTests$ =
|
||||
() => 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 => `<script>${script}</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);
|
||||
};
|
||||
|
275
client/faux.js
275
client/faux.js
@ -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($);
|
@ -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);
|
||||
});
|
||||
|
15
gulpfile.js
15
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))
|
||||
|
@ -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",
|
||||
|
21
public/js/lib/loop-protect/LICENSE.md
Normal file
21
public/js/lib/loop-protect/LICENSE.md
Normal file
@ -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.
|
432
public/js/lib/loop-protect/loop-protect.js
Normal file
432
public/js/lib/loop-protect/loop-protect.js
Normal file
@ -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;
|
||||
});
|
Reference in New Issue
Block a user