Spaces:
Running
Running
| Game.prototype.verbotenWords = [ | |
| '.call', 'call(', 'apply', 'bind', // prevents arbitrary code execution | |
| 'prototype', // prevents messing with prototypes | |
| 'debugger', // prevents pausing execution | |
| 'delete', // prevents removing items | |
| 'constructor', // prevents retrieval of Function using an instance of it | |
| 'window', // prevents setting "window.[...] = map", etc. | |
| 'top', // prevents user code from escaping the iframe | |
| 'validate', 'onExit', 'objective', // don't let players rewrite these methods | |
| '\\u' // prevents usage of arbitrary code through unicode escape characters, see issue #378 | |
| ]; | |
| Game.prototype.allowedTime = 2000; // for infinite loop prevention | |
| var DummyDisplay = function () { | |
| this.clear = function () {}; | |
| this.drawAll = function () {}; | |
| this.drawObject = function () {}; | |
| this.drawText = function () {}; | |
| this.writeStatus = function () {}; | |
| }; | |
| Game.prototype.validate = function(allCode, playerCode, restartingLevelFromScript) { | |
| var game = this; | |
| try { | |
| for (var i = 0; i < this.verbotenWords.length; i++) { | |
| var badWord = this.verbotenWords[i]; | |
| if (playerCode.indexOf(badWord) > -1) { | |
| throw "You are not allowed to use '" + badWord + "'!"; | |
| } | |
| } | |
| var dummyMap = new Map(new DummyDisplay(), this); | |
| dummyMap._dummy = true; | |
| dummyMap._setProperties(this.editor.getProperties().mapProperties); | |
| // modify the code to always check time to prevent infinite loops | |
| allCode = allCode.replace(/\)\s*{/g, ") {"); // converts Allman indentation -> K&R | |
| allCode = allCode.replace(/\n\s*while\s*\((.*)\)/g, "\nfor (dummy=0;$1;)"); // while -> for | |
| allCode = $.map(allCode.split('\n'), function (line, i) { | |
| return line.replace(/for\s*\((.*);(.*);(.*)\)\s*{/g, | |
| "for ($1, startTime = Date.now();$2;$3){" + | |
| "if (Date.now() - startTime > " + game.allowedTime + ") {" + | |
| "throw '[Line " + (i+1) + "] TimeOutException: Maximum loop execution time of " + game.allowedTime + " ms exceeded.';" + | |
| "}"); | |
| }).join('\n'); | |
| allCode = "'use strict';var validateLevel,onExit,objective\n"+allCode; | |
| allCode = allCode+"\n({startLevel:startLevel,validateLevel:validateLevel,onExit:onExit,objective:objective})"; | |
| if (this._debugMode) { | |
| console.log(allCode); | |
| } | |
| var allowjQuery = dummyMap._properties.showDummyDom; | |
| // setup iframe in which code is run. As a side effect, this sets `this._eval` | |
| // and `this.SyntaxError` correctly. | |
| var userEnv = this.initIframe(allowjQuery); | |
| // evaluate the code to get startLevel() and (opt) validateLevel() methods | |
| var userOutput = this._eval(allCode); | |
| // start the level on a dummy map to validate | |
| this._setPlayerCodeRunning(true); | |
| userOutput.startLevel(dummyMap); | |
| this._setPlayerCodeRunning(false); | |
| // re-run to check if the player messed with startLevel | |
| this._startOfStartLevelReached = false; | |
| this._endOfStartLevelReached = false; | |
| dummyMap._reset(); | |
| this._setPlayerCodeRunning(true); | |
| userOutput.startLevel(dummyMap); | |
| this._setPlayerCodeRunning(false); | |
| // does startLevel() execute fully? | |
| // (if we're restarting a level after editing a script, we can't test for this | |
| // - nor do we care) | |
| if (!this._startOfStartLevelReached && !restartingLevelFromScript) { | |
| throw 'startLevel() has been tampered with!'; | |
| } | |
| if (!this._endOfStartLevelReached && !restartingLevelFromScript) { | |
| throw 'startLevel() returned prematurely!'; | |
| } | |
| this.validateLevel = function () { return true; }; | |
| // does validateLevel() succeed? | |
| if (typeof(userOutput.validateLevel) === "function") { | |
| this.validateLevel = userOutput.validateLevel; | |
| this._setPlayerCodeRunning(true); | |
| userOutput.validateLevel(dummyMap); | |
| this._setPlayerCodeRunning(false); | |
| } | |
| dummyMap._clearIntervals(); | |
| this.onExit = function () { return true; }; | |
| if (typeof userOutput.onExit === "function") { | |
| this.onExit = userOutput.onExit; | |
| } | |
| this.objective = function () { return false; }; | |
| if (typeof userOutput.objective === "function") { | |
| this.objective = userOutput.objective; | |
| } | |
| return userOutput.startLevel; | |
| } catch (e) { | |
| // cleanup | |
| this._setPlayerCodeRunning(false); | |
| if (dummyMap) { | |
| dummyMap._clearIntervals(); | |
| } | |
| var exceptionText = e.toString(); | |
| if (e instanceof this.SyntaxError) { | |
| var lineNum = this.findSyntaxError(allCode, e.message); | |
| if (lineNum) { | |
| exceptionText = "[Line " + lineNum + "] " + exceptionText; | |
| } | |
| } | |
| this.display.appendError(exceptionText); | |
| // throw e; // for debugging | |
| return null; | |
| } | |
| }; | |
| // makes sure nothing un-kosher happens during a callback within the game | |
| // e.g. item collison; function phone | |
| Game.prototype.validateCallback = function(callback, throwExceptions) { | |
| var savedException = null; | |
| var exceptionFound = false; | |
| try { | |
| // run the callback and check for forbidden method calls | |
| try { | |
| this._setPlayerCodeRunning(true); | |
| var result = callback(); | |
| this._setPlayerCodeRunning(false); | |
| } catch (e) { | |
| // cleanup | |
| this._setPlayerCodeRunning(false); | |
| if (e.toString().indexOf("Forbidden method call") > -1 || | |
| e.toString().indexOf("Attempt to modify private property") > -1 || | |
| e.toString().indexOf("Attempt to read private property") > -1) { | |
| // display error, disable player movement | |
| this.display.appendError(e.toString(), "%c{red}Please reload the level."); | |
| this.sound.playSound('static'); | |
| this.map.getPlayer()._canMove = false; | |
| this.map._callbackValidationFailed = true; | |
| this.map._clearIntervals(); | |
| // throw e; // for debugging | |
| return; | |
| } else { | |
| // other exceptions are fine here, but be sure to run validation before passing them up | |
| savedException = e; | |
| exceptionFound = true; | |
| } | |
| } | |
| // check if validator still passes | |
| try { | |
| if (typeof(this.validateLevel) === 'function') { | |
| this._setPlayerCodeRunning(true); | |
| this.validateLevel(this.map); | |
| this._setPlayerCodeRunning(false); | |
| } | |
| } catch (e) { | |
| this._setPlayerCodeRunning(false); | |
| // validation failed - not much to do here but restart the level, unfortunately | |
| this.display.appendError(e.toString(), "%c{red}Validation failed! Please reload the level."); | |
| // play error sound | |
| this.sound.playSound('static'); | |
| // disable player movement | |
| this.map.getPlayer()._canMove = false; | |
| this.map._callbackValidationFailed = true; | |
| this.map._clearIntervals(); | |
| return; | |
| } | |
| // refresh the map (unless it refreshes automatically), just in case | |
| if(!this.map._properties.refreshRate) { | |
| this.map.refresh(); | |
| } | |
| if(exceptionFound) { | |
| throw savedException; | |
| } | |
| return result; | |
| } catch (e) { | |
| this.map.writeStatus(e.toString()); | |
| // throw e; // for debugging | |
| if (throwExceptions) { | |
| throw e; | |
| } | |
| } | |
| }; | |
| Game.prototype.validateAndRunScript = function (code) { | |
| try { | |
| // Game.prototype.blah => game.blah | |
| code = code.replace(/Game.prototype/, 'this'); | |
| // Blah => game._blahPrototype | |
| code = code.replace(/function Map/, 'this._mapPrototype = function'); | |
| code = code.replace(/function Player/, 'this._playerPrototype = function'); | |
| new Function(code).bind(this).call(); // bind the function to current instance of game! | |
| if (this._mapPrototype) { | |
| // re-initialize map if necessary | |
| this.map._reset(); // for cleanup | |
| this.map = new this._mapPrototype(this.display, this); | |
| } | |
| // re-initialize objects if necessary | |
| this.objects = this.getListOfObjects(); | |
| // and restart current level from saved state | |
| var savedState = this.editor.getGoodState(this._currentLevel); | |
| this._evalLevelCode(savedState['code'], savedState['playerCode'], false, true); | |
| } catch (e) { | |
| this.display.writeStatus(e.toString()); | |
| //throw e; // for debugging | |
| } | |
| } | |
| var allowedGlobals = { | |
| // These four are allowed primarily to avoid confusing the programmer | |
| 'Object':true, | |
| 'Array':true, | |
| 'String':true, | |
| 'Number':true, | |
| // Math.Floor and Math.random are used in many levels | |
| 'Math':true, | |
| // parseInt is used in a few bonus levels | |
| 'parseInt':true, | |
| // Date is used by the infinite loop prevention code | |
| 'Date':true | |
| } | |
| Game.prototype.initIframe = function(allowjQuery){ | |
| var iframe = $("#user_code")[0]; | |
| // reset any state in the iframe | |
| iframe.src = "about:blank"; | |
| var iframewindow = iframe.contentWindow; | |
| if (iframewindow.eval) { | |
| this._eval = iframewindow.eval; | |
| this.SyntaxError = iframewindow.SyntaxError; | |
| } | |
| // delete any unwated global variables in the iframe | |
| function purgeObject(object) { | |
| var globals = Object.getOwnPropertyNames(object); | |
| for (var i = 0;i < globals.length;i++) { | |
| var variable = globals[i]; | |
| if (!allowedGlobals.hasOwnProperty(variable)) { | |
| delete object[variable]; | |
| } | |
| } | |
| var prototype = Object.getPrototypeOf(object); | |
| if (prototype && prototype != iframewindow.Object.prototype) { | |
| purgeObject(prototype); | |
| } | |
| } | |
| purgeObject(iframewindow); | |
| // document can't be deleted, so purge it instead | |
| purgeObject(iframewindow.document); | |
| // add in any necessary global variables | |
| iframewindow.ROT = {Map: {DividedMaze: ROT.Map.DividedMaze }} | |
| if (allowjQuery) { | |
| // this is not secure, however it doesn't matter since the only level | |
| // with showDummyDom set has no editable code | |
| iframewindow.$ = iframewindow.jQuery = jQuery; | |
| } | |
| return iframewindow; | |
| } | |
| // Object security | |
| // takes an object and modifies it so that all properties starting with `_` | |
| // throw an error when accessed in level code, | |
| // and that all functions are unwritable | |
| Game.prototype.secureObject = function(object, objecttype) { | |
| for (var prop in object) { | |
| if(prop == "_startOfStartLevelReached" || prop == "_endOfStartLevelReached"){ | |
| // despite starting with an _, these two properties are intended to be called from map code | |
| continue; | |
| } | |
| if(prop[0] == "_"){ | |
| this.secureProperty(object, prop, objecttype); | |
| } else if (typeof object[prop] == "function") { | |
| Object.defineProperty(object, prop, { | |
| configurable:false, | |
| writable:false | |
| }); | |
| } | |
| } | |
| } | |
| Game.prototype.secureProperty = function(object, prop, objecttype){ | |
| var val = object[prop]; | |
| var game = this; | |
| Object.defineProperty(object, prop, { | |
| configurable:false, | |
| enumerable:false, | |
| get:function(){ | |
| if (game._isPlayerCodeRunning()) { | |
| throw "Attempt to read private property " + objecttype + "." + prop; | |
| } | |
| return val; | |
| }, | |
| set:function(newValue){ | |
| if(game._isPlayerCodeRunning()) { | |
| throw "Attempt to modify private property " + objecttype + "." + prop; | |
| } | |
| val = newValue | |
| } | |
| }); | |
| } | |
| // awful awful awful method that tries to find the line | |
| // of code where a given error occurs | |
| Game.prototype.findSyntaxError = function(code, errorMsg) { | |
| var lines = code.split('\n'); | |
| // One line at the top is the added declarations and doesn't | |
| // correspond to any real editor code | |
| var phantomLines = 1; | |
| for (var i = 1; i <= lines.length; i++) { | |
| var line = lines[i - 1]; | |
| var startStartLevel = "map._startOfStartLevelReached()"; | |
| var endStartLevel = "map._endOfStartLevelReached()"; | |
| if (line == startStartLevel || line == endStartLevel ) { | |
| // This line was added by the editor and doesn't show up to the user | |
| // so shouldn't be counted. | |
| phantomLines += 1; | |
| } | |
| var testCode = lines.slice(0, i).join('\n'); | |
| try { | |
| this._eval("'use strict';" + testCode); | |
| } catch (e) { | |
| if (e.message === errorMsg) { | |
| return i - phantomLines; | |
| } | |
| } | |
| } | |
| return null; | |
| }; | |