├── .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 |
--------------------------------------------------------------------------------