react / wstein / node_modules / jest-cli / node_modules / jsdom / node_modules / xmlhttprequest / lib / XMLHttpRequest.js
81144 views/**1* Wrapper for built-in http.js to emulate the browser XMLHttpRequest object.2*3* This can be used with JS designed for browsers to improve reuse of code and4* allow the use of existing libraries.5*6* Usage: include("XMLHttpRequest.js") and use XMLHttpRequest per W3C specs.7*8* @author Dan DeFelippi <[email protected]>9* @contributor David Ellis <[email protected]>10* @license MIT11*/1213var Url = require("url");14var spawn = require("child_process").spawn;15var fs = require("fs");1617exports.XMLHttpRequest = function() {18"use strict";1920/**21* Private variables22*/23var self = this;24var http = require("http");25var https = require("https");2627// Holds http.js objects28var request;29var response;3031// Request settings32var settings = {};3334// Disable header blacklist.35// Not part of XHR specs.36var disableHeaderCheck = false;3738// Set some default headers39var defaultHeaders = {40"User-Agent": "node-XMLHttpRequest",41"Accept": "*/*",42};4344var headers = defaultHeaders;4546// These headers are not user setable.47// The following are allowed but banned in the spec:48// * user-agent49var forbiddenRequestHeaders = [50"accept-charset",51"accept-encoding",52"access-control-request-headers",53"access-control-request-method",54"connection",55"content-length",56"content-transfer-encoding",57"cookie",58"cookie2",59"date",60"expect",61"host",62"keep-alive",63"origin",64"referer",65"te",66"trailer",67"transfer-encoding",68"upgrade",69"via"70];7172// These request methods are not allowed73var forbiddenRequestMethods = [74"TRACE",75"TRACK",76"CONNECT"77];7879// Send flag80var sendFlag = false;81// Error flag, used when errors occur or abort is called82var errorFlag = false;8384// Event listeners85var listeners = {};8687/**88* Constants89*/9091this.UNSENT = 0;92this.OPENED = 1;93this.HEADERS_RECEIVED = 2;94this.LOADING = 3;95this.DONE = 4;9697/**98* Public vars99*/100101// Current state102this.readyState = this.UNSENT;103104// default ready state change handler in case one is not set or is set late105this.onreadystatechange = null;106107// Result & response108this.responseText = "";109this.responseXML = "";110this.status = null;111this.statusText = null;112113/**114* Private methods115*/116117/**118* Check if the specified header is allowed.119*120* @param string header Header to validate121* @return boolean False if not allowed, otherwise true122*/123var isAllowedHttpHeader = function(header) {124return disableHeaderCheck || (header && forbiddenRequestHeaders.indexOf(header.toLowerCase()) === -1);125};126127/**128* Check if the specified method is allowed.129*130* @param string method Request method to validate131* @return boolean False if not allowed, otherwise true132*/133var isAllowedHttpMethod = function(method) {134return (method && forbiddenRequestMethods.indexOf(method) === -1);135};136137/**138* Public methods139*/140141/**142* Open the connection. Currently supports local server requests.143*144* @param string method Connection method (eg GET, POST)145* @param string url URL for the connection.146* @param boolean async Asynchronous connection. Default is true.147* @param string user Username for basic authentication (optional)148* @param string password Password for basic authentication (optional)149*/150this.open = function(method, url, async, user, password) {151this.abort();152errorFlag = false;153154// Check for valid request method155if (!isAllowedHttpMethod(method)) {156throw "SecurityError: Request method not allowed";157}158159settings = {160"method": method,161"url": url.toString(),162"async": (typeof async !== "boolean" ? true : async),163"user": user || null,164"password": password || null165};166167setState(this.OPENED);168};169170/**171* Disables or enables isAllowedHttpHeader() check the request. Enabled by default.172* This does not conform to the W3C spec.173*174* @param boolean state Enable or disable header checking.175*/176this.setDisableHeaderCheck = function(state) {177disableHeaderCheck = state;178};179180/**181* Sets a header for the request.182*183* @param string header Header name184* @param string value Header value185*/186this.setRequestHeader = function(header, value) {187if (this.readyState !== this.OPENED) {188throw "INVALID_STATE_ERR: setRequestHeader can only be called when state is OPEN";189}190if (!isAllowedHttpHeader(header)) {191console.warn("Refused to set unsafe header \"" + header + "\"");192return;193}194if (sendFlag) {195throw "INVALID_STATE_ERR: send flag is true";196}197headers[header] = value;198};199200/**201* Gets a header from the server response.202*203* @param string header Name of header to get.204* @return string Text of the header or null if it doesn't exist.205*/206this.getResponseHeader = function(header) {207if (typeof header === "string"208&& this.readyState > this.OPENED209&& response210&& response.headers211&& response.headers[header.toLowerCase()]212&& !errorFlag213) {214return response.headers[header.toLowerCase()];215}216217return null;218};219220/**221* Gets all the response headers.222*223* @return string A string with all response headers separated by CR+LF224*/225this.getAllResponseHeaders = function() {226if (this.readyState < this.HEADERS_RECEIVED || errorFlag) {227return "";228}229var result = "";230231for (var i in response.headers) {232// Cookie headers are excluded233if (i !== "set-cookie" && i !== "set-cookie2") {234result += i + ": " + response.headers[i] + "\r\n";235}236}237return result.substr(0, result.length - 2);238};239240/**241* Gets a request header242*243* @param string name Name of header to get244* @return string Returns the request header or empty string if not set245*/246this.getRequestHeader = function(name) {247// @TODO Make this case insensitive248if (typeof name === "string" && headers[name]) {249return headers[name];250}251252return "";253};254255/**256* Sends the request to the server.257*258* @param string data Optional data to send as request body.259*/260this.send = function(data) {261if (this.readyState !== this.OPENED) {262throw "INVALID_STATE_ERR: connection must be opened before send() is called";263}264265if (sendFlag) {266throw "INVALID_STATE_ERR: send has already been called";267}268269var ssl = false, local = false;270var url = Url.parse(settings.url);271var host;272// Determine the server273switch (url.protocol) {274case "https:":275ssl = true;276// SSL & non-SSL both need host, no break here.277case "http:":278host = url.hostname;279break;280281case "file:":282local = true;283break;284285case undefined:286case "":287host = "localhost";288break;289290default:291throw "Protocol not supported.";292}293294// Load files off the local filesystem (file://)295if (local) {296if (settings.method !== "GET") {297throw "XMLHttpRequest: Only GET method is supported";298}299300if (settings.async) {301fs.readFile(url.pathname, "utf8", function(error, data) {302if (error) {303self.handleError(error);304} else {305self.status = 200;306self.responseText = data;307setState(self.DONE);308}309});310} else {311try {312this.responseText = fs.readFileSync(url.pathname, "utf8");313this.status = 200;314setState(self.DONE);315} catch(e) {316this.handleError(e);317}318}319320return;321}322323// Default to port 80. If accessing localhost on another port be sure324// to use http://localhost:port/path325var port = url.port || (ssl ? 443 : 80);326// Add query string if one is used327var uri = url.pathname + (url.search ? url.search : "");328329// Set the Host header or the server may reject the request330headers.Host = host;331if (!((ssl && port === 443) || port === 80)) {332headers.Host += ":" + url.port;333}334335// Set Basic Auth if necessary336if (settings.user) {337if (typeof settings.password === "undefined") {338settings.password = "";339}340var authBuf = new Buffer(settings.user + ":" + settings.password);341headers.Authorization = "Basic " + authBuf.toString("base64");342}343344// Set content length header345if (settings.method === "GET" || settings.method === "HEAD") {346data = null;347} else if (data) {348headers["Content-Length"] = Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);349350if (!headers["Content-Type"]) {351headers["Content-Type"] = "text/plain;charset=UTF-8";352}353} else if (settings.method === "POST") {354// For a post with no data set Content-Length: 0.355// This is required by buggy servers that don't meet the specs.356headers["Content-Length"] = 0;357}358359var options = {360host: host,361port: port,362path: uri,363method: settings.method,364headers: headers,365agent: false366};367368// Reset error flag369errorFlag = false;370371// Handle async requests372if (settings.async) {373// Use the proper protocol374var doRequest = ssl ? https.request : http.request;375376// Request is being sent, set send flag377sendFlag = true;378379// As per spec, this is called here for historical reasons.380self.dispatchEvent("readystatechange");381382// Handler for the response383var responseHandler = function responseHandler(resp) {384// Set response var to the response we got back385// This is so it remains accessable outside this scope386response = resp;387// Check for redirect388// @TODO Prevent looped redirects389if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 303 || response.statusCode === 307) {390// Change URL to the redirect location391settings.url = response.headers.location;392var url = Url.parse(settings.url);393// Set host var in case it's used later394host = url.hostname;395// Options for the new request396var newOptions = {397hostname: url.hostname,398port: url.port,399path: url.path,400method: response.statusCode === 303 ? "GET" : settings.method,401headers: headers402};403404// Issue the new request405request = doRequest(newOptions, responseHandler).on("error", errorHandler);406request.end();407// @TODO Check if an XHR event needs to be fired here408return;409}410411response.setEncoding("utf8");412413setState(self.HEADERS_RECEIVED);414self.status = response.statusCode;415416response.on("data", function(chunk) {417// Make sure there's some data418if (chunk) {419self.responseText += chunk;420}421// Don't emit state changes if the connection has been aborted.422if (sendFlag) {423setState(self.LOADING);424}425});426427response.on("end", function() {428if (sendFlag) {429// Discard the end event if the connection has been aborted430setState(self.DONE);431sendFlag = false;432}433});434435response.on("error", function(error) {436self.handleError(error);437});438};439440// Error handler for the request441var errorHandler = function errorHandler(error) {442self.handleError(error);443};444445// Create the request446request = doRequest(options, responseHandler).on("error", errorHandler);447448// Node 0.4 and later won't accept empty data. Make sure it's needed.449if (data) {450request.write(data);451}452453request.end();454455self.dispatchEvent("loadstart");456} else { // Synchronous457// Create a temporary file for communication with the other Node process458var contentFile = ".node-xmlhttprequest-content-" + process.pid;459var syncFile = ".node-xmlhttprequest-sync-" + process.pid;460fs.writeFileSync(syncFile, "", "utf8");461// The async request the other Node process executes462var execString = "var http = require('http'), https = require('https'), fs = require('fs');"463+ "var doRequest = http" + (ssl ? "s" : "") + ".request;"464+ "var options = " + JSON.stringify(options) + ";"465+ "var responseText = '';"466+ "var req = doRequest(options, function(response) {"467+ "response.setEncoding('utf8');"468+ "response.on('data', function(chunk) {"469+ " responseText += chunk;"470+ "});"471+ "response.on('end', function() {"472+ "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText}}), 'utf8');"473+ "fs.unlinkSync('" + syncFile + "');"474+ "});"475+ "response.on('error', function(error) {"476+ "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');"477+ "fs.unlinkSync('" + syncFile + "');"478+ "});"479+ "}).on('error', function(error) {"480+ "fs.writeFileSync('" + contentFile + "', JSON.stringify({err: error}), 'utf8');"481+ "fs.unlinkSync('" + syncFile + "');"482+ "});"483+ (data ? "req.write('" + JSON.stringify(data).slice(1,-1).replace(/'/g, "\\'") + "');":"")484+ "req.end();";485// Start the other Node Process, executing this string486var syncProc = spawn(process.argv[0], ["-e", execString]);487while(fs.existsSync(syncFile)) {488// Wait while the sync file is empty489}490var resp = JSON.parse(fs.readFileSync(contentFile, 'utf8'));491// Kill the child process once the file has data492syncProc.stdin.end();493// Remove the temporary file494fs.unlinkSync(contentFile);495496if (resp.err) {497self.handleError(resp.err);498} else {499response = resp.data;500self.status = resp.data.statusCode;501self.responseText = resp.data.text;502setState(self.DONE);503}504}505};506507/**508* Called when an error is encountered to deal with it.509*/510this.handleError = function(error) {511this.status = 503;512this.statusText = error;513this.responseText = error.stack;514errorFlag = true;515setState(this.DONE);516};517518/**519* Aborts a request.520*/521this.abort = function() {522if (request) {523request.abort();524request = null;525}526527headers = defaultHeaders;528this.responseText = "";529this.responseXML = "";530531errorFlag = true;532533if (this.readyState !== this.UNSENT534&& (this.readyState !== this.OPENED || sendFlag)535&& this.readyState !== this.DONE) {536sendFlag = false;537setState(this.DONE);538}539this.readyState = this.UNSENT;540};541542/**543* Adds an event listener. Preferred method of binding to events.544*/545this.addEventListener = function(event, callback) {546if (!(event in listeners)) {547listeners[event] = [];548}549// Currently allows duplicate callbacks. Should it?550listeners[event].push(callback);551};552553/**554* Remove an event callback that has already been bound.555* Only works on the matching funciton, cannot be a copy.556*/557this.removeEventListener = function(event, callback) {558if (event in listeners) {559// Filter will return a new array with the callback removed560listeners[event] = listeners[event].filter(function(ev) {561return ev !== callback;562});563}564};565566/**567* Dispatch any events, including both "on" methods and events attached using addEventListener.568*/569this.dispatchEvent = function(event) {570if (typeof self["on" + event] === "function") {571self["on" + event]();572}573if (event in listeners) {574for (var i = 0, len = listeners[event].length; i < len; i++) {575listeners[event][i].call(self);576}577}578};579580/**581* Changes readyState and calls onreadystatechange.582*583* @param int state New state584*/585var setState = function(state) {586if (state == self.LOADING || self.readyState !== state) {587self.readyState = state;588589if (settings.async || self.readyState < self.OPENED || self.readyState === self.DONE) {590self.dispatchEvent("readystatechange");591}592593if (self.readyState === self.DONE && !errorFlag) {594self.dispatchEvent("load");595// @TODO figure out InspectorInstrumentation::didLoadXHR(cookie)596self.dispatchEvent("loadend");597}598}599};600};601602603