/public/js/lib/loop-protect/loop-protect.js
JavaScript | 437 lines | 409 code | 16 blank | 12 comment | 18 complexity | ae40b0f2d074489dce1f650dfc981cd0 MD5 | raw file
- 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(?!default:)([a-z_]{1}\w+:)/i;
- var comments = /(?:\/\*(?:[\s\S]*?)\*\/)|(?:([\s;])+\/\/(?:.*)$)/gm;
- var loopTimeout = 1000;
- 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 {
- DEBUG && debug('looking backwards ' + lines[j]); // jshint ignore:line
- closePos = lines[j].indexOf('*/');
- openPos = lines[j].indexOf('/*');
- if (closePos !== -1) {
- closeCommentTags++;
- }
- // check for single line /* comment */ formatted comments
- if (closePos === lines[j].length - 2 && openPos !== -1) {
- closeCommentTags--;
- }
- if (openPos !== -1) {
- closeCommentTags--;
- if (closeCommentTags === 0) {
- DEBUG && debug('- exit: part of a multiline comment'); // jshint ignore:line
- return true;
- }
- }
- j -= 1;
- } 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) > loopTimeout) {//} && line.hit !== line.last+1) {
- 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;
- });