From 98bfed02bb81e31ff1e931e6ac68d53485a32475 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 24 Dec 2014 15:04:31 -0700 Subject: [PATCH 1/3] JSLint --- lib/XMLHttpRequest.js | 212 ++++++++++++++++++++++-------------------- 1 file changed, 110 insertions(+), 102 deletions(-) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 4b7cab4..9ab79be 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -1,3 +1,4 @@ +/*jslint node:true, vars:true, todo:true, stupid:true, regexp:true*/ /** * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object. * @@ -16,6 +17,7 @@ var Url = require("url") , fs = require('fs'); exports.XMLHttpRequest = function() { + 'use strict'; /** * Private variables */ @@ -37,7 +39,7 @@ exports.XMLHttpRequest = function() { // Set some default headers var defaultHeaders = { "User-Agent": "node-XMLHttpRequest", - "Accept": "*/*", + "Accept": "*/*" }; var headers = defaultHeaders; @@ -113,6 +115,27 @@ exports.XMLHttpRequest = function() { * Private methods */ + /** + * Changes readyState and calls onreadystatechange. + * + * @param int state New state + */ + var setState = function(state) { + if (state === self.LOADING || self.readyState !== state) { + self.readyState = state; + + if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) { + self.dispatchEvent("readystatechange"); + } + + if (self.readyState === self.DONE && !errorFlag) { + self.dispatchEvent("load"); + // @TODO figure out InspectorInstrumentation::didLoadXHR(cookie) + self.dispatchEvent("loadend"); + } + } + }; + /** * Check if the specified header is allowed. * @@ -183,7 +206,7 @@ exports.XMLHttpRequest = function() { * @param string value Header value */ this.setRequestHeader = function(header, value) { - if (this.readyState != this.OPENED) { + if (this.readyState !== this.OPENED) { throw "INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN"; } if (!isAllowedHttpHeader(header)) { @@ -226,11 +249,13 @@ exports.XMLHttpRequest = function() { return ""; } var result = ""; - - for (var i in response.headers) { - // Cookie headers are excluded - if (i !== "set-cookie" && i !== "set-cookie2") { - result += i + ": " + response.headers[i] + "\r\n"; + var i; + for (i in response.headers) { + if (response.headers.hasOwnProperty(i)) { + // Cookie headers are excluded + if (i !== "set-cookie" && i !== "set-cookie2") { + result += i + ": " + response.headers[i] + "\r\n"; + } } } return result.substr(0, result.length - 2); @@ -257,7 +282,7 @@ exports.XMLHttpRequest = function() { * @param string data Optional data to send as request body. */ this.send = function(data) { - if (this.readyState != this.OPENED) { + if (this.readyState !== this.OPENED) { throw "INVALID_STATE_ERR: connection must be opened before send() is called"; } @@ -273,6 +298,7 @@ exports.XMLHttpRequest = function() { case 'https:': ssl = true; // SSL & non-SSL both need host, no break here. + /* falls through */ case 'http:': host = url.hostname; break; @@ -323,21 +349,21 @@ exports.XMLHttpRequest = function() { // to use http://localhost:port/path var port = url.port || (ssl ? 443 : 80); // Add query string if one is used - var uri = url.pathname + (url.search ? url.search : ''); + var uri = url.pathname + (url.search || ''); // Set the Host header or the server may reject the request - headers["Host"] = host; + headers.Host = host; if (!((ssl && port === 443) || port === 80)) { - headers["Host"] += ':' + url.port; + headers.Host += ':' + url.port; } // Set Basic Auth if necessary if (settings.user) { - if (typeof settings.password == "undefined") { + if (settings.password === undefined) { settings.password = ""; } var authBuf = new Buffer(settings.user + ":" + settings.password); - headers["Authorization"] = "Basic " + authBuf.toString("base64"); + headers.Authorization = "Basic " + authBuf.toString("base64"); } // Set content length header @@ -364,82 +390,84 @@ exports.XMLHttpRequest = function() { agent: false }; + var doRequest; + // Reset error flag errorFlag = false; - // Handle async requests - if (settings.async) { - // Use the proper protocol - var doRequest = ssl ? https.request : http.request; + // Error handler for the request + function errorHandler(error) { + self.handleError(error); + } - // Request is being sent, set send flag - sendFlag = true; + // Handler for the response + function responseHandler(resp) { + // Set response var to the response we got back + // This is so it remains accessable outside this scope + response = resp; + // Check for redirect + // @TODO Prevent looped redirects + if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) { + // Change URL to the redirect location + settings.url = response.headers.location; + url = Url.parse(settings.url); + // Set host var in case it's used later + host = url.hostname; + // Options for the new request + var newOptions = { + hostname: url.hostname, + port: url.port, + path: url.path, + method: response.statusCode === 303 ? 'GET' : settings.method, + headers: headers + }; + + // Issue the new request + request = doRequest(newOptions, responseHandler).on('error', errorHandler); + request.end(); + // @TODO Check if an XHR event needs to be fired here + return; + } - // As per spec, this is called here for historical reasons. - self.dispatchEvent("readystatechange"); + response.setEncoding("utf8"); - // Handler for the response - function responseHandler(resp) { - // Set response var to the response we got back - // This is so it remains accessable outside this scope - response = resp; - // Check for redirect - // @TODO Prevent looped redirects - if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) { - // Change URL to the redirect location - settings.url = response.headers.location; - var url = Url.parse(settings.url); - // Set host var in case it's used later - host = url.hostname; - // Options for the new request - var newOptions = { - hostname: url.hostname, - port: url.port, - path: url.path, - method: response.statusCode === 303 ? 'GET' : settings.method, - headers: headers - }; - - // Issue the new request - request = doRequest(newOptions, responseHandler).on('error', errorHandler); - request.end(); - // @TODO Check if an XHR event needs to be fired here - return; - } + setState(self.HEADERS_RECEIVED); + self.status = response.statusCode; - response.setEncoding("utf8"); + response.on('data', function(chunk) { + // Make sure there's some data + if (chunk) { + self.responseText += chunk; + } + // Don't emit state changes if the connection has been aborted. + if (sendFlag) { + setState(self.LOADING); + } + }); - setState(self.HEADERS_RECEIVED); - self.status = response.statusCode; + response.on('end', function() { + if (sendFlag) { + // Discard the 'end' event if the connection has been aborted + setState(self.DONE); + sendFlag = false; + } + }); - response.on('data', function(chunk) { - // Make sure there's some data - if (chunk) { - self.responseText += chunk; - } - // Don't emit state changes if the connection has been aborted. - if (sendFlag) { - setState(self.LOADING); - } - }); + response.on('error', function(error) { + self.handleError(error); + }); + } - response.on('end', function() { - if (sendFlag) { - // Discard the 'end' event if the connection has been aborted - setState(self.DONE); - sendFlag = false; - } - }); + // Handle async requests + if (settings.async) { + // Use the proper protocol + doRequest = ssl ? https.request : http.request; - response.on('error', function(error) { - self.handleError(error); - }); - } + // Request is being sent, set send flag + sendFlag = true; - // Error handler for the request - function errorHandler(error) { - self.handleError(error); - } + // As per spec, this is called here for historical reasons. + self.dispatchEvent("readystatechange"); // Create the request request = doRequest(options, responseHandler).on('error', errorHandler); @@ -483,7 +511,6 @@ exports.XMLHttpRequest = function() { + "req.end();"; // Start the other Node Process, executing this string var syncProc = spawn(process.argv[0], ["-e", execString]); - var statusText; while(fs.existsSync(syncFile)) { // Wait while the sync file is empty } @@ -544,7 +571,7 @@ exports.XMLHttpRequest = function() { * Adds an event listener. Preferred method of binding to events. */ this.addEventListener = function(event, callback) { - if (!(event in listeners)) { + if (!(listeners.hasOwnProperty(event))) { listeners[event] = []; } // Currently allows duplicate callbacks. Should it? @@ -556,7 +583,7 @@ exports.XMLHttpRequest = function() { * Only works on the matching funciton, cannot be a copy. */ this.removeEventListener = function(event, callback) { - if (event in listeners) { + if (listeners.hasOwnProperty(event)) { // Filter will return a new array with the callback removed listeners[event] = listeners[event].filter(function(ev) { return ev !== callback; @@ -571,31 +598,12 @@ exports.XMLHttpRequest = function() { if (typeof self["on" + event] === "function") { self["on" + event](); } - if (event in listeners) { - for (var i = 0, len = listeners[event].length; i < len; i++) { + var i, len; + if (listeners.hasOwnProperty(event)) { + for (i = 0, len = listeners[event].length; i < len; i++) { listeners[event][i].call(self); } } }; - /** - * Changes readyState and calls onreadystatechange. - * - * @param int state New state - */ - var setState = function(state) { - if (state == self.LOADING || self.readyState !== state) { - self.readyState = state; - - if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) { - self.dispatchEvent("readystatechange"); - } - - if (self.readyState === self.DONE && !errorFlag) { - self.dispatchEvent("load"); - // @TODO figure out InspectorInstrumentation::didLoadXHR(cookie) - self.dispatchEvent("loadend"); - } - } - }; }; From 890442b612b68f1b4ba309d841774869119c1fc6 Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 24 Dec 2014 15:18:47 -0700 Subject: [PATCH 2/3] url.protocol can return (only?) null when no protocol in URL --- lib/XMLHttpRequest.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 9ab79be..2d8009a 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -308,6 +308,7 @@ exports.XMLHttpRequest = function() { break; case undefined: + case null: case '': host = "localhost"; break; From c6e0050f8df00e3be298e7da9ec45161cd9cebae Mon Sep 17 00:00:00 2001 From: Brett Zamir Date: Wed, 24 Dec 2014 17:30:52 -0700 Subject: [PATCH 3/3] Undo use strict so as to allow arguments.callee to determine origin paths --- .gitignore | 1 + lib/XMLHttpRequest.js | 35 ++++++++++++++++++++++++++++++++--- package.json | 4 ++++ 3 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6fd10a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/* \ No newline at end of file diff --git a/lib/XMLHttpRequest.js b/lib/XMLHttpRequest.js index 2d8009a..6af4ab4 100644 --- a/lib/XMLHttpRequest.js +++ b/lib/XMLHttpRequest.js @@ -1,4 +1,4 @@ -/*jslint node:true, vars:true, todo:true, stupid:true, regexp:true*/ +/*jslint node:true, vars:true, todo:true, stupid:true, regexp:true, sloppy: true*/ /** * Wrapper for built-in http.js to emulate the browser XMLHttpRequest object. * @@ -12,12 +12,14 @@ * @license MIT */ +require('array.prototype.find'); +require('string.prototype.includes'); + var Url = require("url") , spawn = require("child_process").spawn , fs = require('fs'); exports.XMLHttpRequest = function() { - 'use strict'; /** * Private variables */ @@ -293,6 +295,17 @@ exports.XMLHttpRequest = function() { var ssl = false, local = false; var url = Url.parse(settings.url); var host; + + function getStack () { + var orig = Error.prepareStackTrace; + Error.prepareStackTrace = function(_, stack){ return stack; }; + var err = new Error(); + Error.captureStackTrace(err, arguments.callee); + var stack = err.stack; + Error.prepareStackTrace = orig; + return stack; + } + // Determine the server switch (url.protocol) { case 'https:': @@ -310,7 +323,23 @@ exports.XMLHttpRequest = function() { case undefined: case null: case '': - host = "localhost"; + var stack = getStack(); + var path = require('path'); + var basePath = path.dirname(stack.reverse().find(function (item) { + var filename = item.getFileName(); + var idx = filename.search(/[\/\\]node_modules[\/\\]/); + if (idx === -1) { // Should be a user file, as a node executable like nodeunit ought to have node_modules in the path + return true; + } + // Should be a user file because its last "node_modules" contains this XMLHttpRequest file (i.e., XMLHttpRequest is a dependency of some kind) + if (__dirname.includes(filename.slice(0, idx))) { + return true; + } + return false; + }).getFileName()); + var pathName = path.resolve(basePath, settings.url); + url = {pathname: pathName}; + local = true; break; default: diff --git a/package.json b/package.json index d6bf689..a71c1e7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ , "engines": { "node": ">=0.4.0" } +, "dependencies": { + "array.prototype.find": "1.x" + , "string.prototype.includes": "1.x" + } , "directories": { "lib": "./lib" , "example": "./example"