PageRenderTime 29ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/public/js/lib/loop-protect/loop-protect.js

https://gitlab.com/Aaeinstein54/FreeCodeCamp
JavaScript | 437 lines | 409 code | 16 blank | 12 comment | 18 complexity | ae40b0f2d074489dce1f650dfc981cd0 MD5 | raw file
  1. if (typeof DEBUG === 'undefined') { DEBUG = true; }
  2. (function (root, factory) {
  3. 'use strict';
  4. /*global define*/
  5. if (typeof define === 'function' && define.amd) {
  6. define(factory(root));
  7. } else if (typeof exports === 'object') {
  8. module.exports = factory(root);
  9. } else {
  10. root.loopProtect = factory(root);
  11. }
  12. })(this, function loopProtectModule(root) {
  13. /*global DEBUG*/
  14. 'use strict';
  15. var debug = null;
  16. // the standard loops - note that recursive is not supported
  17. var re = /\b(for|while|do)\b/g;
  18. var reSingle = /\b(for|while|do)\b/;
  19. var labelRe = /\b(?!default:)([a-z_]{1}\w+:)/i;
  20. var comments = /(?:\/\*(?:[\s\S]*?)\*\/)|(?:([\s;])+\/\/(?:.*)$)/gm;
  21. var loopTimeout = 1000;
  22. var loopProtect = rewriteLoops;
  23. // used in the loop detection
  24. loopProtect.counters = {};
  25. // expose debug info
  26. loopProtect.debug = function debugSwitch(state) {
  27. debug = state ? function () {
  28. console.log.apply(console, [].slice.apply(arguments));
  29. } : function () {};
  30. };
  31. loopProtect.debug(false); // off by default
  32. // the method - as this could be aliased to something else
  33. loopProtect.alias = 'loopProtect';
  34. function inMultilineComment(lineNum, lines) {
  35. if (lineNum === 0) {
  36. return false;
  37. }
  38. var j = lineNum;
  39. var closeCommentTags = 1; // let's assume we're inside a comment
  40. var closePos = -1;
  41. var openPos = -1;
  42. do {
  43. DEBUG && debug('looking backwards ' + lines[j]); // jshint ignore:line
  44. closePos = lines[j].indexOf('*/');
  45. openPos = lines[j].indexOf('/*');
  46. if (closePos !== -1) {
  47. closeCommentTags++;
  48. }
  49. // check for single line /* comment */ formatted comments
  50. if (closePos === lines[j].length - 2 && openPos !== -1) {
  51. closeCommentTags--;
  52. }
  53. if (openPos !== -1) {
  54. closeCommentTags--;
  55. if (closeCommentTags === 0) {
  56. DEBUG && debug('- exit: part of a multiline comment'); // jshint ignore:line
  57. return true;
  58. }
  59. }
  60. j -= 1;
  61. } while (j !== 0);
  62. return false;
  63. }
  64. function inCommentOrString(index, line) {
  65. var character;
  66. while (--index > -1) {
  67. character = line.substr(index, 1);
  68. if (character === '"' || character === '\'' || character === '.') {
  69. // our loop keyword was actually either in a string or a property, so let's exit and ignore this line
  70. DEBUG && debug('- exit: matched inside a string or property key'); // jshint ignore:line
  71. return true;
  72. }
  73. if (character === '/' || character === '*') {
  74. // looks like a comment, go back one to confirm or not
  75. --index;
  76. if (character === '/') {
  77. // we've found a comment, so let's exit and ignore this line
  78. DEBUG && debug('- exit: part of a comment'); // jshint ignore:line
  79. return true;
  80. }
  81. }
  82. }
  83. return false;
  84. }
  85. function directlyBeforeLoop(index, lineNum, lines) {
  86. reSingle.lastIndex = 0;
  87. labelRe.lastIndex = 0;
  88. var beforeLoop = false;
  89. var theRest = lines.slice(lineNum).join('\n').substr(index).replace(labelRe, '');
  90. theRest.replace(reSingle, function commentStripper(match, capture, i) {
  91. var target = theRest.substr(0, i).replace(comments, '').trim();
  92. DEBUG && debug('- directlyBeforeLoop: ' + target); // jshint ignore:line
  93. if (target.length === 0) {
  94. beforeLoop = true;
  95. }
  96. // strip comments out of the target, and if there's nothing else
  97. // it's a valid label...I hope!
  98. });
  99. return beforeLoop;
  100. }
  101. /**
  102. * Look for for, while and do loops, and inserts *just* at the start of the
  103. * loop, a check function.
  104. */
  105. function rewriteLoops(code, offset) {
  106. var recompiled = [];
  107. var lines = code.split('\n');
  108. var disableLoopProtection = false;
  109. var method = loopProtect.alias + '.protect';
  110. var ignore = {};
  111. var pushonly = {};
  112. var labelPostion = null;
  113. function insertReset(lineNum, line, matchPosition) {
  114. // recompile the line with the reset **just** before the actual loop
  115. // so that we insert in to the correct location (instead of possibly
  116. // outside the logic
  117. return line.slice(0, matchPosition) + ';' + method + '({ line: ' + lineNum + ', reset: true }); ' + line.slice(matchPosition);
  118. }
  119. if (!offset) {
  120. offset = 0;
  121. }
  122. lines.forEach(function eachLine(line, lineNum) {
  123. // reset our regexp each time.
  124. re.lastIndex = 0;
  125. labelRe.lastIndex = 0;
  126. if (disableLoopProtection) {
  127. return;
  128. }
  129. if (line.toLowerCase().indexOf('noprotect') !== -1) {
  130. disableLoopProtection = true;
  131. }
  132. var index = -1;
  133. var matchPosition = -1;
  134. var originalLineNum = lineNum;
  135. // +1 since we're humans and don't read lines numbers from zero
  136. var printLineNumber = lineNum - offset + 1;
  137. var character = '';
  138. // special case for `do` loops, as they're end with `while`
  139. var dofound = false;
  140. var findwhile = false;
  141. var terminator = false;
  142. var matches = line.match(re) || [];
  143. var match = matches.length ? matches[0] : '';
  144. var labelMatch = line.match(labelRe) || [];
  145. var openBrackets = 0;
  146. var openBraces = 0;
  147. if (labelMatch.length) {
  148. DEBUG && debug('- label match'); // jshint ignore:line
  149. index = line.indexOf(labelMatch[1]);
  150. if (!inCommentOrString(index, line)) {
  151. if (!inMultilineComment(lineNum, lines)) {
  152. if (directlyBeforeLoop(index, lineNum, lines)) {
  153. DEBUG && debug('- found a label: "' + labelMatch[0] + '"'); // jshint ignore:line
  154. labelPostion = lineNum;
  155. } else {
  156. DEBUG && debug('- ignored "label", false positive'); // jshint ignore:line
  157. }
  158. } else {
  159. DEBUG && debug('- ignored label in multline comment'); // jshint ignore:line
  160. }
  161. } else {
  162. DEBUG && debug('- ignored label in string or comment'); // jshint ignore:line
  163. }
  164. }
  165. if (ignore[lineNum]) {
  166. DEBUG && debug(' -exit: ignoring line ' + lineNum +': ' + line); // jshint ignore:line
  167. return;
  168. }
  169. if (pushonly[lineNum]) {
  170. DEBUG && debug('- exit: ignoring, but adding line ' + lineNum + ': ' + line); // jshint ignore:line
  171. recompiled.push(line);
  172. return;
  173. }
  174. // if there's more than one match, we just ignore this kind of loop
  175. // otherwise I'm going to be writing a full JavaScript lexer...and god
  176. // knows I've got better things to be doing.
  177. if (match && matches.length === 1 && line.indexOf('jsbin') === -1) {
  178. DEBUG && debug('match on ' + match + '\n'); // jshint ignore:line
  179. // there's a special case for protecting `do` loops, we need to first
  180. // prtect the `do`, but then ignore the closing `while` statement, so
  181. // we reset the search state for this special case.
  182. dofound = match === 'do';
  183. // make sure this is an actual loop command by searching backwards
  184. // to ensure it's not a string, comment or object property
  185. matchPosition = index = line.indexOf(match);
  186. // first we need to walk backwards to ensure that our match isn't part
  187. // of a string or part of a comment
  188. if (inCommentOrString(index, line)) {
  189. recompiled.push(line);
  190. return;
  191. }
  192. // it's quite possible we're in the middle of a multiline
  193. // comment, so we'll cycle up looking for an opening comment,
  194. // and if there's one (and not a closing `*/`), then we'll
  195. // ignore this line as a comment
  196. if (inMultilineComment(lineNum, lines)) {
  197. recompiled.push(line);
  198. return;
  199. }
  200. // now work our way forward to look for '{'
  201. index = line.indexOf(match) + match.length;
  202. if (index === line.length) {
  203. if (index === line.length && lineNum < (lines.length-1)) {
  204. // move to the next line
  205. DEBUG && debug('- moving to next line'); // jshint ignore:line
  206. recompiled.push(line);
  207. lineNum++;
  208. line = lines[lineNum];
  209. ignore[lineNum] = true;
  210. index = 0;
  211. }
  212. }
  213. while (index < line.length) {
  214. character = line.substr(index, 1);
  215. // DEBUG && debug(character, index); // jshint ignore:line
  216. if (character === '(') {
  217. openBrackets++;
  218. }
  219. if (character === ')') {
  220. openBrackets--;
  221. if (openBrackets === 0 && terminator === false) {
  222. terminator = index;
  223. }
  224. }
  225. if (character === '{') {
  226. openBraces++;
  227. }
  228. if (character === '}') {
  229. openBraces--;
  230. }
  231. if (openBrackets === 0 && (character === ';' || character === '{')) {
  232. // if we're a non-curlies loop, then convert to curlies to get our code inserted
  233. if (character === ';') {
  234. if (lineNum !== originalLineNum) {
  235. DEBUG && debug('- multiline inline loop'); // jshint ignore:line
  236. // affect the compiled line
  237. recompiled[originalLineNum] = recompiled[originalLineNum].substring(0, terminator + 1) + '{\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n' + recompiled[originalLineNum].substring(terminator + 1);
  238. line += '\n}\n';
  239. } else {
  240. // simpler
  241. DEBUG && debug('- single line inline loop'); // jshint ignore:line
  242. line = line.substring(0, terminator + 1) + '{\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n' + line.substring(terminator + 1) + '\n}\n';
  243. }
  244. } else if (character === '{') {
  245. DEBUG && debug('- multiline with braces'); // jshint ignore:line
  246. var insert = ';\nif (' + method + '({ line: ' + printLineNumber + ' })) break;\n';
  247. line = line.substring(0, index + 1) + insert + line.substring(index + 1);
  248. index += insert.length;
  249. }
  250. // work out where to put the reset
  251. if (lineNum === originalLineNum && labelPostion === null) {
  252. DEBUG && debug('- simple reset insert'); // jshint ignore:line
  253. line = insertReset(printLineNumber, line, matchPosition);
  254. index += (';' + method + '({ line: ' + lineNum + ', reset: true }); ').length;
  255. } else {
  256. // insert the reset above the originalLineNum OR if this loop used
  257. // a label, we have to insert the reset *above* the label
  258. if (labelPostion === null) {
  259. DEBUG && debug('- reset inserted above original line'); // jshint ignore:line
  260. recompiled[originalLineNum] = insertReset(printLineNumber, recompiled[originalLineNum], matchPosition);
  261. } else {
  262. DEBUG && debug('- reset inserted above matched label on line ' + labelPostion); // jshint ignore:line
  263. if (recompiled[labelPostion] === undefined) {
  264. labelPostion--;
  265. matchPosition = 0;
  266. }
  267. recompiled[labelPostion] = insertReset(printLineNumber, recompiled[labelPostion], matchPosition);
  268. labelPostion = null;
  269. }
  270. }
  271. recompiled.push(line);
  272. if (!dofound) {
  273. return;
  274. } else {
  275. DEBUG && debug('searching for closing `while` statement for: ' + line); // jshint ignore:line
  276. // cycle forward until we find the close brace, after which should
  277. // be our while statement to ignore
  278. findwhile = false;
  279. while (index < line.length) {
  280. character = line.substr(index, 1);
  281. if (character === '{') {
  282. openBraces++;
  283. }
  284. if (character === '}') {
  285. openBraces--;
  286. }
  287. if (openBraces === 0) {
  288. findwhile = true;
  289. } else {
  290. findwhile = false;
  291. }
  292. if (openBraces === 0) {
  293. DEBUG && debug('outside of closure, looking for `while` statement: ' + line); // jshint ignore:line
  294. }
  295. if (findwhile && line.indexOf('while') !== -1) {
  296. DEBUG && debug('- exit as we found `while`: ' + line); // jshint ignore:line
  297. pushonly[lineNum] = true;
  298. return;
  299. }
  300. index++;
  301. if (index === line.length && lineNum < (lines.length-1)) {
  302. lineNum++;
  303. line = lines[lineNum];
  304. DEBUG && debug(line); // jshint ignore:line
  305. index = 0;
  306. }
  307. }
  308. return;
  309. }
  310. }
  311. index++;
  312. if (index === line.length && lineNum < (lines.length-1)) {
  313. // move to the next line
  314. DEBUG && debug('- moving to next line'); // jshint ignore:line
  315. recompiled.push(line);
  316. lineNum++;
  317. line = lines[lineNum];
  318. ignore[lineNum] = true;
  319. index = 0;
  320. }
  321. }
  322. } else {
  323. // else we're a regular line, and we shouldn't be touched
  324. DEBUG && debug('regular line ' + line); // jshint ignore:line
  325. recompiled.push(line);
  326. }
  327. });
  328. DEBUG && debug('---- source ----'); // jshint ignore:line
  329. DEBUG && debug(code); // jshint ignore:line
  330. DEBUG && debug('---- rewrite ---'); // jshint ignore:line
  331. DEBUG && debug(recompiled.join('\n')); // jshint ignore:line
  332. DEBUG && debug(''); // jshint ignore:line
  333. return disableLoopProtection ? code : recompiled.join('\n');
  334. };
  335. /**
  336. * Injected code in to user's code to **try** to protect against infinite
  337. * loops cropping up in the code, and killing the browser. Returns true
  338. * when the loops has been running for more than 100ms.
  339. */
  340. loopProtect.protect = function protect(state) {
  341. loopProtect.counters[state.line] = loopProtect.counters[state.line] || {};
  342. var line = loopProtect.counters[state.line];
  343. var now = (new Date()).getTime();
  344. if (state.reset) {
  345. line.time = now;
  346. line.hit = 0;
  347. line.last = 0;
  348. }
  349. line.hit++;
  350. if ((now - line.time) > loopTimeout) {//} && line.hit !== line.last+1) {
  351. loopProtect.hit(state.line);
  352. // Returning true prevents the loop running again
  353. return true;
  354. }
  355. line.last++;
  356. return false;
  357. };
  358. loopProtect.hit = function hit(line) {
  359. var msg = 'Exiting potential infinite loop at line ' + line + '. To disable loop protection: add "// noprotect" to your code';
  360. if (root.proxyConsole) {
  361. root.proxyConsole.error(msg);
  362. } else {
  363. console.error(msg);
  364. }
  365. };
  366. loopProtect.reset = function reset() {
  367. // reset the counters
  368. loopProtect.counters = {};
  369. };
  370. return loopProtect;
  371. });