├── .gitignore ├── README.md ├── test ├── test.html └── test.js ├── package.json ├── Gruntfile.js ├── karma.conf.js └── lib └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | javascript-sandbox 2 | ================== 3 | 4 | Sandbox for running user entered code, either from tests or to get the result. 5 | 6 | Setup 7 | ================== 8 | After cloning this project, run npm install. 9 | 10 | Development 11 | ================== 12 | Run grunt develop, and start modifying files under /lib, or start writing tests in /test 13 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Mocha Tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-sandbox", 3 | "version": "1.0.2", 4 | "description": "Lightweight javascript sandbox.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/codeschool/javascript-sandbox.git" 12 | }, 13 | "keywords": [ 14 | "javascript", 15 | "sandbox" 16 | ], 17 | "author": "Adam Fortuna", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/codeschool/javascript-sandbox/issues" 21 | }, 22 | "homepage": "https://github.com/codeschool/javascript-sandbox", 23 | "devDependencies": { 24 | "browserify": "~3.24.1", 25 | "chai": "~1.8.1", 26 | "grunt": "^0.4.5", 27 | "grunt-browserify": "~1.3.0", 28 | "grunt-contrib-clean": "~0.5.0", 29 | "grunt-contrib-connect": "^0.11.2", 30 | "grunt-contrib-watch": "~0.5.3", 31 | "grunt-karma": "^0.12.0", 32 | "karma": "^0.13.9", 33 | "karma-chrome-launcher": "^0.2.0", 34 | "karma-mocha": "^0.2.0", 35 | "karma-phantomjs-launcher": "^0.2.1", 36 | "karma-sinon-chai": "^1.0.0", 37 | "mocha": "^2.2.5", 38 | "phantomjs": "^1.9.18" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.loadNpmTasks('grunt-browserify'); 4 | grunt.loadNpmTasks("grunt-contrib-clean"); 5 | grunt.loadNpmTasks("grunt-contrib-watch"); 6 | grunt.loadNpmTasks("grunt-contrib-connect"); 7 | grunt.loadNpmTasks('grunt-karma'); 8 | 9 | // Project configuration. 10 | grunt.initConfig({ 11 | clean: ["build/javascript-sandbox.js"], 12 | connect: { 13 | server: { 14 | options: { 15 | port: 4000 16 | } 17 | } 18 | }, 19 | browserify: { 20 | common: { 21 | src: ['lib/**/*.js'], 22 | dest: 'build/javascript-sandbox.js', 23 | options: { 24 | alias: 'lib/index.js:javascript-sandbox' 25 | } 26 | } 27 | }, 28 | karma: { 29 | unit: { 30 | configFile: 'karma.conf.js', 31 | background: true 32 | }, 33 | continuous: { 34 | configFile: 'karma.conf.js', 35 | singleRun: true, 36 | browsers: ['PhantomJS'] 37 | } 38 | }, 39 | watch: { 40 | //run unit tests with karma (server needs to be already running) 41 | karma: { 42 | files: ['lib/**/*.js', 'test/**/*.js'], 43 | tasks: ['browserify', 'karma:unit:run'] 44 | } 45 | } 46 | }); 47 | 48 | // Default task(s). 49 | grunt.registerTask('default', ['clean', 'browserify', 'karma:continuous']); 50 | grunt.registerTask('develop', ['clean', 'browserify', 'karma:unit:start', 'watch']); 51 | }; 52 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Fri Jan 24 2014 17:11:21 GMT-0500 (EST) 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | 7 | // base path, that will be used to resolve files and exclude 8 | basePath: '', 9 | 10 | 11 | // frameworks to use 12 | frameworks: ['mocha', 'sinon-chai'], 13 | 14 | 15 | // list of files / patterns to load in the browser 16 | files: [ 17 | "build/**/*.js", 18 | "test/**/*.js" 19 | ], 20 | 21 | 22 | // list of files to exclude 23 | exclude: [ 24 | 25 | ], 26 | 27 | 28 | // test results reporter to use 29 | // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' 30 | reporters: ['progress'], 31 | 32 | 33 | // web server port 34 | port: 9876, 35 | 36 | 37 | // enable / disable colors in the output (reporters and logs) 38 | colors: true, 39 | 40 | 41 | // level of logging 42 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 43 | logLevel: config.LOG_INFO, 44 | 45 | 46 | // enable / disable watching file and executing tests whenever any file changes 47 | autoWatch: false, 48 | 49 | 50 | // Start these browsers, currently available: 51 | // - Chrome 52 | // - ChromeCanary 53 | // - Firefox 54 | // - Opera (has to be installed with `npm install karma-opera-launcher`) 55 | // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) 56 | // - PhantomJS 57 | // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) 58 | browsers: ['Chrome'], 59 | 60 | 61 | // If browser does not capture in given timeout [ms], kill it 62 | captureTimeout: 60000, 63 | 64 | 65 | // Continuous Integration mode 66 | // if true, it capture browsers, run tests and exit 67 | singleRun: false 68 | }); 69 | }; 70 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var blacklist = [ 2 | 'console.log', 3 | 'alert', 4 | 'confirm', 5 | 'prompt' 6 | ]; 7 | 8 | var polyfills = { 9 | console: { 10 | assert: function() {}, 11 | count: function() {}, 12 | dir: function() {}, 13 | dirxml: function() {}, 14 | debug: function() {}, 15 | error: function() {}, 16 | group: function() {}, 17 | groupCollapsed: function() {}, 18 | groupEnd: function() {}, 19 | info: function() {}, 20 | log: function() {}, 21 | markTimeline: function() {}, 22 | memory: function() {}, 23 | profile: function() {}, 24 | profileEnd: function() {}, 25 | table: function() {}, 26 | time: function() {}, 27 | timeEnd: function() {}, 28 | timeline: function() {}, 29 | timelineEnd: function() {}, 30 | timeStamp: function() {}, 31 | trace: function() {}, 32 | warn: function() {} 33 | } 34 | }; 35 | 36 | function blacklistify(blacklist) { 37 | return '' + blacklist + ' = function() {} '; 38 | } 39 | 40 | function Sandbox(options) { 41 | options = options || {} 42 | var parentElement = options.parentElement || document.body; 43 | 44 | // Code will be run in an iframe 45 | if(options.iframe) { 46 | this.iframe = options.iframe; 47 | } else { 48 | this.iframe = document.createElement('iframe'); 49 | this.iframe.style.display = 'none'; 50 | } 51 | parentElement.appendChild(this.iframe); 52 | 53 | // quiet stubs out all loud functions (log, alert, etc) 54 | options.quiet = options.quiet || false; 55 | 56 | // blacklisted functions will be overridden 57 | options.blacklist = options.blacklist || (options.quiet ? blacklist : []); 58 | for(var i = 0, len = options.blacklist.length; i < len; i++) { 59 | this.iframe.contentWindow.eval(blacklistify(options.blacklist[i])); 60 | } 61 | 62 | // Load the HTML in 63 | if(options.html) { 64 | var iframeDocument = this.iframe.contentWindow.document; 65 | iframeDocument.open(); 66 | iframeDocument.write(options.html); 67 | iframeDocument.close(); 68 | } 69 | 70 | // Copy over all variables to the iFrame 71 | // This MUST happen after the document is written because IE11 seems to reinitialize the 72 | // contentWindow after a document.close(); 73 | var win = this.iframe.contentWindow; 74 | var variables = options.variables || {}; 75 | var nestedKeys; 76 | Object.keys(variables).forEach(function (key) { 77 | nestedKeys = key.split('.'); 78 | nameSpaceFor(win, nestedKeys)[nestedKeys[nestedKeys.length-1]] = variables[key]; 79 | }); 80 | 81 | for (var polyfill in polyfills) { 82 | var object = this.get(polyfill) || {}; 83 | if (!object) { 84 | this.set(polyfill, object); 85 | } 86 | 87 | for (var method in polyfills[polyfill]) { 88 | if (!object[method]) { 89 | object[method] = polyfills[polyfill][method]; 90 | } 91 | } 92 | } 93 | 94 | // Evaluate the javascript. 95 | if(options.javascript) { 96 | this.evaluate(options.javascript); 97 | } 98 | } 99 | 100 | 101 | // Used for getting variables under a namespace for redefining 102 | // ie, console.log 103 | function nameSpaceFor(namespace, keys) { 104 | if(keys.length == 1) { 105 | return namespace; 106 | } else { 107 | return nameSpaceFor(namespace[keys[0]], keys.slice(1,keys.length)); 108 | } 109 | } 110 | 111 | // When we evaluate, we'll need to take into account: 112 | // Setup the HTML? 113 | // Run the JavaScript 114 | // 115 | Sandbox.prototype.evaluate = function (code) { 116 | var result; 117 | try { 118 | result = this.iframe.contentWindow.eval(code); 119 | } 120 | catch (error) { 121 | var stack = error.stack; 122 | if (stack) { 123 | var stackLines = stack.split(/\n/); 124 | if (stackLines.length > 0) { 125 | // find first line with anonymous code and a line number 126 | for (var current = 0, len = stackLines.length; current < len; current++) { 127 | var currentLine = stackLines[current]; 128 | 129 | // Detect Chrome and IE line numbers. 130 | var matches = currentLine.match(/(\\:|eval code:)\s*(\d+):(\d+)/); 131 | if (matches && matches.length === 4) { 132 | error.line = parseInt(matches[2], 10); 133 | error.character = parseInt(matches[3], 10); 134 | break; 135 | } 136 | 137 | // Detect PhantomJS... why? cause unit tests. 138 | matches = currentLine.match(/at\s+\:(\d+)/); 139 | if (matches && matches.length == 2) { 140 | error.line = parseInt(matches[1], 10); 141 | break; 142 | } 143 | } 144 | } 145 | } 146 | throw error; 147 | } 148 | return result; 149 | }; 150 | 151 | Sandbox.prototype.exec = function(/*...*/) { 152 | var context = this.iframe.contentWindow, 153 | args = [].slice.call(arguments), 154 | functionToExec = args.shift(); 155 | 156 | // Pass in the context as the first argument. 157 | args.unshift(context); 158 | 159 | return functionToExec.apply(context, args); 160 | }; 161 | 162 | Sandbox.prototype.get = function(property) { 163 | var context = this.iframe.contentWindow; 164 | return context[property]; 165 | }; 166 | 167 | Sandbox.prototype.set = function(property, value) { 168 | var context = this.iframe.contentWindow; 169 | context[property] = value; 170 | }; 171 | 172 | Sandbox.prototype.destroy = function () { 173 | if (this.iframe) { 174 | this.iframe.parentNode.removeChild(this.iframe); 175 | this.iframe = null; 176 | } 177 | }; 178 | 179 | module.exports = Sandbox; 180 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var Sandbox = require('javascript-sandbox'); 2 | var assert = chai.assert; 3 | 4 | // Needed to detect IE, since it reports line numbers & character positions differently. 5 | function detectBrowser() { 6 | var ua = window.navigator.userAgent; 7 | 8 | var msie = ua.indexOf('MSIE '); 9 | if (msie > 0) { 10 | // IE 10 or older => return version number 11 | return "IE"; 12 | } 13 | 14 | var trident = ua.indexOf('Trident/'); 15 | if (trident > 0) { 16 | return "IE"; 17 | } 18 | 19 | var edge = ua.indexOf('Edge/'); 20 | if (edge > 0) { 21 | return "IE"; 22 | } 23 | 24 | var phantomJS = ua.indexOf('PhantomJS/'); 25 | if (phantomJS > 0) { 26 | return "PHANTOM"; 27 | } 28 | 29 | // other browser 30 | return false; 31 | } 32 | 33 | describe("Sandbox", function() { 34 | var sandbox; 35 | 36 | describe("#new(options=null)", function() { 37 | 38 | it("inserted an iframe into the document", function() { 39 | sandbox = new Sandbox(); 40 | 41 | assert.ok(sandbox.iframe) 42 | assert.ok(sandbox.iframe.parentNode) 43 | assert.equal(document.getElementsByTagName('iframe').length, 1, "More than one iframe was created!"); 44 | 45 | sandbox.destroy(); 46 | }); 47 | 48 | it("set variables on the iframe", function() { 49 | var test1 = "test1"; 50 | var test2 = 2; 51 | var options = { 52 | variables: { 53 | test1: test1, 54 | test2: test2, 55 | sandbox: sandbox 56 | } 57 | }; 58 | 59 | sandbox = new Sandbox(options); 60 | 61 | for (var variable in options.variables) { 62 | assert.equal(options.variables[variable], sandbox.iframe.contentWindow.eval(variable)); 63 | } 64 | 65 | sandbox.destroy(); 66 | }); 67 | 68 | it("sets nested variables on the iframe", function() { 69 | var logStub = sinon.stub; 70 | var options = { 71 | variables: { 72 | 'console.log': logStub 73 | } 74 | }; 75 | 76 | sandbox = new Sandbox(options); 77 | assert.equal(sandbox.get('console')['log'], logStub); 78 | sandbox.destroy(); 79 | }); 80 | 81 | it("executes optional javascript", function() { 82 | assert.throws(function() { 83 | new Sandbox({ 84 | javascript: "throw {message: 'ha! ha!', toString:function() {return this.message}}" 85 | }); 86 | }, 'ha! ha!'); 87 | }); 88 | }) 89 | 90 | describe("#evaluate(code)", function() { 91 | beforeEach(function() { 92 | sandbox = new Sandbox(); 93 | }) 94 | 95 | afterEach(function() { 96 | sandbox.destroy(); 97 | }) 98 | 99 | it("executes user's code", function() { 100 | var code = "window.document.getElementsByTagName('body')"; 101 | var iframeBody = sandbox.evaluate(code)[0]; 102 | 103 | assert.ok(iframeBody, "iframe body was retrieved using user's code."); 104 | assert(iframeBody == sandbox.iframe.contentWindow.document.body, "user code returned the current window body, meaning the code was executed in the wrong context."); 105 | }) 106 | 107 | it('throws uncaught exceptions', function() { 108 | assert.throws(function() { 109 | sandbox.evaluate("throw {message:'ha! ha!', toString:function() {return this.message}};"); 110 | }, 'ha! ha!'); 111 | }); 112 | 113 | it('throws uncaught exceptions with line numbers', function() { 114 | var thrownError = null; 115 | 116 | try { 117 | sandbox.evaluate("throw new Error('ha! ha!');"); 118 | } 119 | catch (error) { 120 | thrownError = error; 121 | } 122 | 123 | assert(thrownError); 124 | assert(thrownError.line); 125 | assert(thrownError.line === 1); 126 | 127 | var browser = detectBrowser(); 128 | switch (browser) { 129 | case "IE": 130 | assert(thrownError.character === 1); 131 | break; 132 | case "PHANTOM": 133 | assert(!thrownError.character); 134 | break; 135 | default: 136 | assert(thrownError.character); 137 | assert(thrownError.character === 7); 138 | } 139 | }); 140 | 141 | it('throws non-error exceptions with no line numbers', function() { 142 | var thrownError = null; 143 | 144 | try { 145 | sandbox.evaluate("throw {message:'ha! ha!'};"); 146 | } 147 | catch (error) { 148 | thrownError = error; 149 | } 150 | 151 | assert(thrownError); 152 | var browser = detectBrowser(); 153 | switch (browser) { 154 | case "PHANTOM": 155 | assert(thrownError.line === 1); 156 | break; 157 | default: 158 | assert(!thrownError.line); 159 | } 160 | assert(!thrownError.character); 161 | }); 162 | }); 163 | 164 | describe("#new(options={quiet:true})", function() { 165 | var alertStub; 166 | 167 | beforeEach(function() { 168 | sandbox = new Sandbox({ 169 | quiet: true 170 | }); 171 | alertStub = sinon.stub(sandbox.iframe.contentWindow, "alert"); 172 | }) 173 | 174 | afterEach(function() { 175 | sandbox.iframe.contentWindow.alert.restore(); 176 | sandbox.destroy(); 177 | }) 178 | 179 | it("does not execute code that interrupts browser interaction", function() { 180 | sandbox.evaluate("alert('Pay attention to meee!!')"); 181 | 182 | assert.equal(1, alertStub.callCount, "Alert should have been called, but not performed any actions."); 183 | }) 184 | }); 185 | 186 | describe("#exec(function, arguments, context)", function() { 187 | beforeEach(function() { 188 | sandbox = new Sandbox(); 189 | }) 190 | 191 | afterEach(function() { 192 | sandbox.destroy(); 193 | }) 194 | 195 | it("called a function in the iframe", function() { 196 | var iframeDocument = sandbox.exec(function(window) { 197 | return window.document; 198 | }); 199 | 200 | assert.equal(iframeDocument, sandbox.iframe.contentWindow.document, "Was not able to retrieve the iframe document."); 201 | }) 202 | 203 | it("called a function with args", function() { 204 | var localArg1 = "my arg"; 205 | var iframeArg1 = sandbox.exec(function(window, arg1) { 206 | return arg1 207 | }, localArg1); 208 | 209 | assert.equal(localArg1, iframeArg1, "Argument wasn't passed to the function."); 210 | }) 211 | 212 | it("called a function with multiple args", function() { 213 | var localArg1 = "my arg"; 214 | var localArg2 = "my additional arg"; 215 | var iframeArgs = sandbox.exec(function(window, arg1, arg2) { 216 | return [arg1, arg2]; 217 | }, localArg1, localArg2); 218 | 219 | assert.equal(localArg1, iframeArgs[0], "Argument 1 wasn't passed to the function."); 220 | assert.equal(localArg2, iframeArgs[1], "Argument 2 wasn't passed to the function."); 221 | }) 222 | 223 | it('throws uncaught exceptions', function() { 224 | assert.throws(function() { 225 | sandbox.exec(function() { 226 | throw {message: 'ha! ha!', toString:function() {return this.message}}; 227 | }); 228 | }, 'ha! ha!'); 229 | }); 230 | }); 231 | 232 | describe("#get(propertyName)", function() { 233 | beforeEach(function() { 234 | sandbox = new Sandbox(); 235 | }) 236 | 237 | afterEach(function() { 238 | sandbox.destroy(); 239 | }) 240 | 241 | it("retrieved a property from the iframe", function() { 242 | var myVar = "A new variable that I just set."; 243 | sandbox.iframe.contentWindow.myVar = myVar; 244 | 245 | var sandboxMyVar = sandbox.get('myVar'); 246 | assert.equal(myVar, sandboxMyVar, "The variable was not retrieved from the sandbox."); 247 | }) 248 | }) 249 | 250 | describe("#set(propertyName, value)", function() { 251 | beforeEach(function() { 252 | sandbox = new Sandbox(); 253 | }) 254 | 255 | afterEach(function() { 256 | sandbox.destroy(); 257 | }) 258 | 259 | it("retrieved a property from the iframe", function() { 260 | var myVar = "A new variable that I just set."; 261 | sandbox.set('myVar', myVar); 262 | 263 | var iframeMyVar = sandbox.iframe.contentWindow.myVar; 264 | assert.equal(myVar, iframeMyVar, "The variable was not set in the sandbox."); 265 | }) 266 | }) 267 | 268 | describe("polyfills", function() { 269 | beforeEach(function() { 270 | sandbox = new Sandbox(); 271 | }) 272 | 273 | afterEach(function() { 274 | sandbox.destroy(); 275 | }) 276 | 277 | it("has polyfilled methods", function() { 278 | var console = sandbox.get('console'); 279 | assert(console.time); 280 | assert(console.timeEnd); 281 | assert(console.debug); 282 | assert(console.warn); 283 | assert(console.info); 284 | assert(console.group); 285 | assert(console.groupEnd); 286 | assert(console.count); 287 | }) 288 | }) 289 | }); 290 | --------------------------------------------------------------------------------