You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					739 lines
				
				22 KiB
			
		
		
			
		
	
	
					739 lines
				
				22 KiB
			| 
											2 years ago
										 | /** | ||
|  |  * XmlHttpRequest implementation that uses TLS and flash SocketPool. | ||
|  |  * | ||
|  |  * @author Dave Longley | ||
|  |  * | ||
|  |  * Copyright (c) 2010-2013 Digital Bazaar, Inc. | ||
|  |  */ | ||
|  | var forge = require('./forge'); | ||
|  | require('./socket'); | ||
|  | require('./http'); | ||
|  | 
 | ||
|  | /* XHR API */ | ||
|  | var xhrApi = module.exports = forge.xhr = forge.xhr || {}; | ||
|  | 
 | ||
|  | (function($) { | ||
|  | 
 | ||
|  | // logging category
 | ||
|  | var cat = 'forge.xhr'; | ||
|  | 
 | ||
|  | /* | ||
|  | XMLHttpRequest interface definition from: | ||
|  | http://www.w3.org/TR/XMLHttpRequest
 | ||
|  | 
 | ||
|  | interface XMLHttpRequest { | ||
|  |   // event handler
 | ||
|  |   attribute EventListener onreadystatechange; | ||
|  | 
 | ||
|  |   // state
 | ||
|  |   const unsigned short UNSENT = 0; | ||
|  |   const unsigned short OPENED = 1; | ||
|  |   const unsigned short HEADERS_RECEIVED = 2; | ||
|  |   const unsigned short LOADING = 3; | ||
|  |   const unsigned short DONE = 4; | ||
|  |   readonly attribute unsigned short readyState; | ||
|  | 
 | ||
|  |   // request
 | ||
|  |   void open(in DOMString method, in DOMString url); | ||
|  |   void open(in DOMString method, in DOMString url, in boolean async); | ||
|  |   void open(in DOMString method, in DOMString url, | ||
|  |             in boolean async, in DOMString user); | ||
|  |   void open(in DOMString method, in DOMString url, | ||
|  |             in boolean async, in DOMString user, in DOMString password); | ||
|  |   void setRequestHeader(in DOMString header, in DOMString value); | ||
|  |   void send(); | ||
|  |   void send(in DOMString data); | ||
|  |   void send(in Document data); | ||
|  |   void abort(); | ||
|  | 
 | ||
|  |   // response
 | ||
|  |   DOMString getAllResponseHeaders(); | ||
|  |   DOMString getResponseHeader(in DOMString header); | ||
|  |   readonly attribute DOMString responseText; | ||
|  |   readonly attribute Document responseXML; | ||
|  |   readonly attribute unsigned short status; | ||
|  |   readonly attribute DOMString statusText; | ||
|  | }; | ||
|  | */ | ||
|  | 
 | ||
|  | // readyStates
 | ||
|  | var UNSENT = 0; | ||
|  | var OPENED = 1; | ||
|  | var HEADERS_RECEIVED = 2; | ||
|  | var LOADING = 3; | ||
|  | var DONE = 4; | ||
|  | 
 | ||
|  | // exceptions
 | ||
|  | var INVALID_STATE_ERR = 11; | ||
|  | var SYNTAX_ERR = 12; | ||
|  | var SECURITY_ERR = 18; | ||
|  | var NETWORK_ERR = 19; | ||
|  | var ABORT_ERR = 20; | ||
|  | 
 | ||
|  | // private flash socket pool vars
 | ||
|  | var _sp = null; | ||
|  | var _policyPort = 0; | ||
|  | var _policyUrl = null; | ||
|  | 
 | ||
|  | // default client (used if no special URL provided when creating an XHR)
 | ||
|  | var _client = null; | ||
|  | 
 | ||
|  | // all clients including the default, key'd by full base url
 | ||
|  | // (multiple cross-domain http clients are permitted so there may be more
 | ||
|  | // than one client in this map)
 | ||
|  | // TODO: provide optional clean up API for non-default clients
 | ||
|  | var _clients = {}; | ||
|  | 
 | ||
|  | // the default maximum number of concurrents connections per client
 | ||
|  | var _maxConnections = 10; | ||
|  | 
 | ||
|  | var net = forge.net; | ||
|  | var http = forge.http; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Initializes flash XHR support. | ||
|  |  * | ||
|  |  * @param options: | ||
|  |  *   url: the default base URL to connect to if xhr URLs are relative, | ||
|  |  *     ie: https://myserver.com.
 | ||
|  |  *   flashId: the dom ID of the flash SocketPool. | ||
|  |  *   policyPort: the port that provides the server's flash policy, 0 to use | ||
|  |  *     the flash default. | ||
|  |  *   policyUrl: the policy file URL to use instead of a policy port. | ||
|  |  *   msie: true if browser is internet explorer, false if not. | ||
|  |  *   connections: the maximum number of concurrent connections. | ||
|  |  *   caCerts: a list of PEM-formatted certificates to trust. | ||
|  |  *   cipherSuites: an optional array of cipher suites to use, | ||
|  |  *     see forge.tls.CipherSuites. | ||
|  |  *   verify: optional TLS certificate verify callback to use (see forge.tls | ||
|  |  *     for details). | ||
|  |  *   getCertificate: an optional callback used to get a client-side | ||
|  |  *     certificate (see forge.tls for details). | ||
|  |  *   getPrivateKey: an optional callback used to get a client-side private | ||
|  |  *     key (see forge.tls for details). | ||
|  |  *   getSignature: an optional callback used to get a client-side signature | ||
|  |  *     (see forge.tls for details). | ||
|  |  *   persistCookies: true to use persistent cookies via flash local storage, | ||
|  |  *     false to only keep cookies in javascript. | ||
|  |  *   primeTlsSockets: true to immediately connect TLS sockets on their | ||
|  |  *     creation so that they will cache TLS sessions for reuse. | ||
|  |  */ | ||
|  | xhrApi.init = function(options) { | ||
|  |   forge.log.debug(cat, 'initializing', options); | ||
|  | 
 | ||
|  |   // update default policy port and max connections
 | ||
|  |   _policyPort = options.policyPort || _policyPort; | ||
|  |   _policyUrl = options.policyUrl || _policyUrl; | ||
|  |   _maxConnections = options.connections || _maxConnections; | ||
|  | 
 | ||
|  |   // create the flash socket pool
 | ||
|  |   _sp = net.createSocketPool({ | ||
|  |     flashId: options.flashId, | ||
|  |     policyPort: _policyPort, | ||
|  |     policyUrl: _policyUrl, | ||
|  |     msie: options.msie || false | ||
|  |   }); | ||
|  | 
 | ||
|  |   // create default http client
 | ||
|  |   _client = http.createClient({ | ||
|  |     url: options.url || ( | ||
|  |       window.location.protocol + '//' + window.location.host), | ||
|  |     socketPool: _sp, | ||
|  |     policyPort: _policyPort, | ||
|  |     policyUrl: _policyUrl, | ||
|  |     connections: options.connections || _maxConnections, | ||
|  |     caCerts: options.caCerts, | ||
|  |     cipherSuites: options.cipherSuites, | ||
|  |     persistCookies: options.persistCookies || true, | ||
|  |     primeTlsSockets: options.primeTlsSockets || false, | ||
|  |     verify: options.verify, | ||
|  |     getCertificate: options.getCertificate, | ||
|  |     getPrivateKey: options.getPrivateKey, | ||
|  |     getSignature: options.getSignature | ||
|  |   }); | ||
|  |   _clients[_client.url.origin] = _client; | ||
|  | 
 | ||
|  |   forge.log.debug(cat, 'ready'); | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Called to clean up the clients and socket pool. | ||
|  |  */ | ||
|  | xhrApi.cleanup = function() { | ||
|  |   // destroy all clients
 | ||
|  |   for(var key in _clients) { | ||
|  |     _clients[key].destroy(); | ||
|  |   } | ||
|  |   _clients = {}; | ||
|  |   _client = null; | ||
|  | 
 | ||
|  |   // destroy socket pool
 | ||
|  |   _sp.destroy(); | ||
|  |   _sp = null; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Sets a cookie. | ||
|  |  * | ||
|  |  * @param cookie the cookie with parameters: | ||
|  |  *   name: the name of the cookie. | ||
|  |  *   value: the value of the cookie. | ||
|  |  *   comment: an optional comment string. | ||
|  |  *   maxAge: the age of the cookie in seconds relative to created time. | ||
|  |  *   secure: true if the cookie must be sent over a secure protocol. | ||
|  |  *   httpOnly: true to restrict access to the cookie from javascript | ||
|  |  *     (inaffective since the cookies are stored in javascript). | ||
|  |  *   path: the path for the cookie. | ||
|  |  *   domain: optional domain the cookie belongs to (must start with dot). | ||
|  |  *   version: optional version of the cookie. | ||
|  |  *   created: creation time, in UTC seconds, of the cookie. | ||
|  |  */ | ||
|  | xhrApi.setCookie = function(cookie) { | ||
|  |   // default cookie expiration to never
 | ||
|  |   cookie.maxAge = cookie.maxAge || -1; | ||
|  | 
 | ||
|  |   // if the cookie's domain is set, use the appropriate client
 | ||
|  |   if(cookie.domain) { | ||
|  |     // add the cookies to the applicable domains
 | ||
|  |     for(var key in _clients) { | ||
|  |       var client = _clients[key]; | ||
|  |       if(http.withinCookieDomain(client.url, cookie) && | ||
|  |         client.secure === cookie.secure) { | ||
|  |         client.setCookie(cookie); | ||
|  |       } | ||
|  |     } | ||
|  |   } else { | ||
|  |     // use the default domain
 | ||
|  |     // FIXME: should a null domain cookie be added to all clients? should
 | ||
|  |     // this be an option?
 | ||
|  |     _client.setCookie(cookie); | ||
|  |   } | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Gets a cookie. | ||
|  |  * | ||
|  |  * @param name the name of the cookie. | ||
|  |  * @param path an optional path for the cookie (if there are multiple cookies | ||
|  |  *          with the same name but different paths). | ||
|  |  * @param domain an optional domain for the cookie (if not using the default | ||
|  |  *          domain). | ||
|  |  * | ||
|  |  * @return the cookie, cookies (if multiple matches), or null if not found. | ||
|  |  */ | ||
|  | xhrApi.getCookie = function(name, path, domain) { | ||
|  |   var rval = null; | ||
|  | 
 | ||
|  |   if(domain) { | ||
|  |     // get the cookies from the applicable domains
 | ||
|  |     for(var key in _clients) { | ||
|  |       var client = _clients[key]; | ||
|  |       if(http.withinCookieDomain(client.url, domain)) { | ||
|  |         var cookie = client.getCookie(name, path); | ||
|  |         if(cookie !== null) { | ||
|  |           if(rval === null) { | ||
|  |             rval = cookie; | ||
|  |           } else if(!forge.util.isArray(rval)) { | ||
|  |             rval = [rval, cookie]; | ||
|  |           } else { | ||
|  |             rval.push(cookie); | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   } else { | ||
|  |     // get cookie from default domain
 | ||
|  |     rval = _client.getCookie(name, path); | ||
|  |   } | ||
|  | 
 | ||
|  |   return rval; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Removes a cookie. | ||
|  |  * | ||
|  |  * @param name the name of the cookie. | ||
|  |  * @param path an optional path for the cookie (if there are multiple cookies | ||
|  |  *          with the same name but different paths). | ||
|  |  * @param domain an optional domain for the cookie (if not using the default | ||
|  |  *          domain). | ||
|  |  * | ||
|  |  * @return true if a cookie was removed, false if not. | ||
|  |  */ | ||
|  | xhrApi.removeCookie = function(name, path, domain) { | ||
|  |   var rval = false; | ||
|  | 
 | ||
|  |   if(domain) { | ||
|  |     // remove the cookies from the applicable domains
 | ||
|  |     for(var key in _clients) { | ||
|  |       var client = _clients[key]; | ||
|  |       if(http.withinCookieDomain(client.url, domain)) { | ||
|  |         if(client.removeCookie(name, path)) { | ||
|  |            rval = true; | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |   } else { | ||
|  |     // remove cookie from default domain
 | ||
|  |     rval = _client.removeCookie(name, path); | ||
|  |   } | ||
|  | 
 | ||
|  |   return rval; | ||
|  | }; | ||
|  | 
 | ||
|  | /** | ||
|  |  * Creates a new XmlHttpRequest. By default the base URL, flash policy port, | ||
|  |  * etc, will be used. However, an XHR can be created to point at another | ||
|  |  * cross-domain URL. | ||
|  |  * | ||
|  |  * @param options: | ||
|  |  *   logWarningOnError: If true and an HTTP error status code is received then | ||
|  |  *     log a warning, otherwise log a verbose message. | ||
|  |  *   verbose: If true be very verbose in the output including the response | ||
|  |  *     event and response body, otherwise only include status, timing, and | ||
|  |  *     data size. | ||
|  |  *   logError: a multi-var log function for warnings that takes the log | ||
|  |  *     category as the first var. | ||
|  |  *   logWarning: a multi-var log function for warnings that takes the log | ||
|  |  *     category as the first var. | ||
|  |  *   logDebug: a multi-var log function for warnings that takes the log | ||
|  |  *     category as the first var. | ||
|  |  *   logVerbose: a multi-var log function for warnings that takes the log | ||
|  |  *     category as the first var. | ||
|  |  *   url: the default base URL to connect to if xhr URLs are relative, | ||
|  |  *     eg: https://myserver.com, and note that the following options will be
 | ||
|  |  *     ignored if the URL is absent or the same as the default base URL. | ||
|  |  *   policyPort: the port that provides the server's flash policy, 0 to use | ||
|  |  *     the flash default. | ||
|  |  *   policyUrl: the policy file URL to use instead of a policy port. | ||
|  |  *   connections: the maximum number of concurrent connections. | ||
|  |  *   caCerts: a list of PEM-formatted certificates to trust. | ||
|  |  *   cipherSuites: an optional array of cipher suites to use, see | ||
|  |  *     forge.tls.CipherSuites. | ||
|  |  *   verify: optional TLS certificate verify callback to use (see forge.tls | ||
|  |  *     for details). | ||
|  |  *   getCertificate: an optional callback used to get a client-side | ||
|  |  *     certificate. | ||
|  |  *   getPrivateKey: an optional callback used to get a client-side private key. | ||
|  |  *   getSignature: an optional callback used to get a client-side signature. | ||
|  |  *   persistCookies: true to use persistent cookies via flash local storage, | ||
|  |  *     false to only keep cookies in javascript. | ||
|  |  *   primeTlsSockets: true to immediately connect TLS sockets on their | ||
|  |  *     creation so that they will cache TLS sessions for reuse. | ||
|  |  * | ||
|  |  * @return the XmlHttpRequest. | ||
|  |  */ | ||
|  | xhrApi.create = function(options) { | ||
|  |   // set option defaults
 | ||
|  |   options = $.extend({ | ||
|  |     logWarningOnError: true, | ||
|  |     verbose: false, | ||
|  |     logError: function() {}, | ||
|  |     logWarning: function() {}, | ||
|  |     logDebug: function() {}, | ||
|  |     logVerbose: function() {}, | ||
|  |     url: null | ||
|  |   }, options || {}); | ||
|  | 
 | ||
|  |   // private xhr state
 | ||
|  |   var _state = { | ||
|  |     // the http client to use
 | ||
|  |     client: null, | ||
|  |     // request storage
 | ||
|  |     request: null, | ||
|  |     // response storage
 | ||
|  |     response: null, | ||
|  |     // asynchronous, true if doing asynchronous communication
 | ||
|  |     asynchronous: true, | ||
|  |     // sendFlag, true if send has been called
 | ||
|  |     sendFlag: false, | ||
|  |     // errorFlag, true if a network error occurred
 | ||
|  |     errorFlag: false | ||
|  |   }; | ||
|  | 
 | ||
|  |   // private log functions
 | ||
|  |   var _log = { | ||
|  |     error: options.logError || forge.log.error, | ||
|  |     warning: options.logWarning || forge.log.warning, | ||
|  |     debug: options.logDebug || forge.log.debug, | ||
|  |     verbose: options.logVerbose || forge.log.verbose | ||
|  |   }; | ||
|  | 
 | ||
|  |   // create public xhr interface
 | ||
|  |   var xhr = { | ||
|  |     // an EventListener
 | ||
|  |     onreadystatechange: null, | ||
|  |     // readonly, the current readyState
 | ||
|  |     readyState: UNSENT, | ||
|  |     // a string with the response entity-body
 | ||
|  |     responseText: '', | ||
|  |     // a Document for response entity-bodies that are XML
 | ||
|  |     responseXML: null, | ||
|  |     // readonly, returns the HTTP status code (i.e. 404)
 | ||
|  |     status: 0, | ||
|  |     // readonly, returns the HTTP status message (i.e. 'Not Found')
 | ||
|  |     statusText: '' | ||
|  |   }; | ||
|  | 
 | ||
|  |   // determine which http client to use
 | ||
|  |   if(options.url === null) { | ||
|  |     // use default
 | ||
|  |     _state.client = _client; | ||
|  |   } else { | ||
|  |     var url; | ||
|  |     try { | ||
|  |       url = new URL(options.url); | ||
|  |     } catch(e) { | ||
|  |       var error = new Error('Invalid url.'); | ||
|  |       error.details = { | ||
|  |         url: options.url | ||
|  |       }; | ||
|  |     } | ||
|  | 
 | ||
|  |     // find client
 | ||
|  |     if(url.origin in _clients) { | ||
|  |       // client found
 | ||
|  |       _state.client = _clients[url.origin]; | ||
|  |     } else { | ||
|  |       // create client
 | ||
|  |       _state.client = http.createClient({ | ||
|  |         url: options.url, | ||
|  |         socketPool: _sp, | ||
|  |         policyPort: options.policyPort || _policyPort, | ||
|  |         policyUrl: options.policyUrl || _policyUrl, | ||
|  |         connections: options.connections || _maxConnections, | ||
|  |         caCerts: options.caCerts, | ||
|  |         cipherSuites: options.cipherSuites, | ||
|  |         persistCookies: options.persistCookies || true, | ||
|  |         primeTlsSockets: options.primeTlsSockets || false, | ||
|  |         verify: options.verify, | ||
|  |         getCertificate: options.getCertificate, | ||
|  |         getPrivateKey: options.getPrivateKey, | ||
|  |         getSignature: options.getSignature | ||
|  |       }); | ||
|  |       _clients[url.origin] = _state.client; | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Opens the request. This method will create the HTTP request to send. | ||
|  |    * | ||
|  |    * @param method the HTTP method (i.e. 'GET'). | ||
|  |    * @param url the relative url (the HTTP request path). | ||
|  |    * @param async always true, ignored. | ||
|  |    * @param user always null, ignored. | ||
|  |    * @param password always null, ignored. | ||
|  |    */ | ||
|  |   xhr.open = function(method, url, async, user, password) { | ||
|  |     // 1. validate Document if one is associated
 | ||
|  |     // TODO: not implemented (not used yet)
 | ||
|  | 
 | ||
|  |     // 2. validate method token
 | ||
|  |     // 3. change method to uppercase if it matches a known
 | ||
|  |     // method (here we just require it to be uppercase, and
 | ||
|  |     // we do not allow the standard methods)
 | ||
|  |     // 4. disallow CONNECT, TRACE, or TRACK with a security error
 | ||
|  |     switch(method) { | ||
|  |     case 'DELETE': | ||
|  |     case 'GET': | ||
|  |     case 'HEAD': | ||
|  |     case 'OPTIONS': | ||
|  |     case 'PATCH': | ||
|  |     case 'POST': | ||
|  |     case 'PUT': | ||
|  |       // valid method
 | ||
|  |       break; | ||
|  |     case 'CONNECT': | ||
|  |     case 'TRACE': | ||
|  |     case 'TRACK': | ||
|  |       throw new Error('CONNECT, TRACE and TRACK methods are disallowed'); | ||
|  |     default: | ||
|  |       throw new Error('Invalid method: ' + method); | ||
|  |     } | ||
|  | 
 | ||
|  |     // TODO: other validation steps in algorithm are not implemented
 | ||
|  | 
 | ||
|  |     // 19. set send flag to false
 | ||
|  |     // set response body to null
 | ||
|  |     // empty list of request headers
 | ||
|  |     // set request method to given method
 | ||
|  |     // set request URL
 | ||
|  |     // set username, password
 | ||
|  |     // set asychronous flag
 | ||
|  |     _state.sendFlag = false; | ||
|  |     xhr.responseText = ''; | ||
|  |     xhr.responseXML = null; | ||
|  | 
 | ||
|  |     // custom: reset status and statusText
 | ||
|  |     xhr.status = 0; | ||
|  |     xhr.statusText = ''; | ||
|  | 
 | ||
|  |     // create the HTTP request
 | ||
|  |     _state.request = http.createRequest({ | ||
|  |       method: method, | ||
|  |       path: url | ||
|  |     }); | ||
|  | 
 | ||
|  |     // 20. set state to OPENED
 | ||
|  |     xhr.readyState = OPENED; | ||
|  | 
 | ||
|  |     // 21. dispatch onreadystatechange
 | ||
|  |     if(xhr.onreadystatechange) { | ||
|  |        xhr.onreadystatechange(); | ||
|  |     } | ||
|  |   }; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Adds an HTTP header field to the request. | ||
|  |    * | ||
|  |    * @param header the name of the header field. | ||
|  |    * @param value the value of the header field. | ||
|  |    */ | ||
|  |   xhr.setRequestHeader = function(header, value) { | ||
|  |     // 1. if state is not OPENED or send flag is true, raise exception
 | ||
|  |     if(xhr.readyState != OPENED || _state.sendFlag) { | ||
|  |       throw new Error('XHR not open or sending'); | ||
|  |     } | ||
|  | 
 | ||
|  |     // TODO: other validation steps in spec aren't implemented
 | ||
|  | 
 | ||
|  |     // set header
 | ||
|  |     _state.request.setField(header, value); | ||
|  |   }; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Sends the request and any associated data. | ||
|  |    * | ||
|  |    * @param data a string or Document object to send, null to send no data. | ||
|  |    */ | ||
|  |   xhr.send = function(data) { | ||
|  |     // 1. if state is not OPENED or 2. send flag is true, raise
 | ||
|  |     // an invalid state exception
 | ||
|  |     if(xhr.readyState != OPENED || _state.sendFlag) { | ||
|  |       throw new Error('XHR not open or sending'); | ||
|  |     } | ||
|  | 
 | ||
|  |     // 3. ignore data if method is GET or HEAD
 | ||
|  |     if(data && | ||
|  |       _state.request.method !== 'GET' && | ||
|  |       _state.request.method !== 'HEAD') { | ||
|  |       // handle non-IE case
 | ||
|  |       if(typeof(XMLSerializer) !== 'undefined') { | ||
|  |         if(data instanceof Document) { | ||
|  |           var xs = new XMLSerializer(); | ||
|  |           _state.request.body = xs.serializeToString(data); | ||
|  |         } else { | ||
|  |           _state.request.body = data; | ||
|  |         } | ||
|  |       } else { | ||
|  |         // poorly implemented IE case
 | ||
|  |         if(typeof(data.xml) !== 'undefined') { | ||
|  |           _state.request.body = data.xml; | ||
|  |         } else { | ||
|  |           _state.request.body = data; | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     // 4. release storage mutex (not used)
 | ||
|  | 
 | ||
|  |     // 5. set error flag to false
 | ||
|  |     _state.errorFlag = false; | ||
|  | 
 | ||
|  |     // 6. if asynchronous is true (must be in this implementation)
 | ||
|  | 
 | ||
|  |     // 6.1 set send flag to true
 | ||
|  |     _state.sendFlag = true; | ||
|  | 
 | ||
|  |     // 6.2 dispatch onreadystatechange
 | ||
|  |     if(xhr.onreadystatechange) { | ||
|  |       xhr.onreadystatechange(); | ||
|  |     } | ||
|  | 
 | ||
|  |     // create send options
 | ||
|  |     var options = {}; | ||
|  |     options.request = _state.request; | ||
|  |     options.headerReady = function(e) { | ||
|  |       // make cookies available for ease of use/iteration
 | ||
|  |       xhr.cookies = _state.client.cookies; | ||
|  | 
 | ||
|  |       // TODO: update document.cookie with any cookies where the
 | ||
|  |       // script's domain matches
 | ||
|  | 
 | ||
|  |       // headers received
 | ||
|  |       xhr.readyState = HEADERS_RECEIVED; | ||
|  |       xhr.status = e.response.code; | ||
|  |       xhr.statusText = e.response.message; | ||
|  |       _state.response = e.response; | ||
|  |       if(xhr.onreadystatechange) { | ||
|  |         xhr.onreadystatechange(); | ||
|  |       } | ||
|  |       if(!_state.response.aborted) { | ||
|  |         // now loading body
 | ||
|  |         xhr.readyState = LOADING; | ||
|  |         if(xhr.onreadystatechange) { | ||
|  |            xhr.onreadystatechange(); | ||
|  |         } | ||
|  |       } | ||
|  |     }; | ||
|  |     options.bodyReady = function(e) { | ||
|  |       xhr.readyState = DONE; | ||
|  |       var ct = e.response.getField('Content-Type'); | ||
|  |       // Note: this null/undefined check is done outside because IE
 | ||
|  |       // dies otherwise on a "'null' is null" error
 | ||
|  |       if(ct) { | ||
|  |         if(ct.indexOf('text/xml') === 0 || | ||
|  |           ct.indexOf('application/xml') === 0 || | ||
|  |           ct.indexOf('+xml') !== -1) { | ||
|  |           try { | ||
|  |             var doc = new ActiveXObject('MicrosoftXMLDOM'); | ||
|  |             doc.async = false; | ||
|  |             doc.loadXML(e.response.body); | ||
|  |             xhr.responseXML = doc; | ||
|  |           } catch(ex) { | ||
|  |             var parser = new DOMParser(); | ||
|  |             xhr.responseXML = parser.parseFromString(ex.body, 'text/xml'); | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       var length = 0; | ||
|  |       if(e.response.body !== null) { | ||
|  |         xhr.responseText = e.response.body; | ||
|  |         length = e.response.body.length; | ||
|  |       } | ||
|  |       // build logging output
 | ||
|  |       var req = _state.request; | ||
|  |       var output = | ||
|  |         req.method + ' ' + req.path + ' ' + | ||
|  |         xhr.status + ' ' + xhr.statusText + ' ' + | ||
|  |         length + 'B ' + | ||
|  |         (e.request.connectTime + e.request.time + e.response.time) + | ||
|  |         'ms'; | ||
|  |       var lFunc; | ||
|  |       if(options.verbose) { | ||
|  |         lFunc = (xhr.status >= 400 && options.logWarningOnError) ? | ||
|  |           _log.warning : _log.verbose; | ||
|  |         lFunc(cat, output, | ||
|  |           e, e.response.body ? '\n' + e.response.body : '\nNo content'); | ||
|  |       } else { | ||
|  |         lFunc = (xhr.status >= 400 && options.logWarningOnError) ? | ||
|  |           _log.warning : _log.debug; | ||
|  |         lFunc(cat, output); | ||
|  |       } | ||
|  |       if(xhr.onreadystatechange) { | ||
|  |         xhr.onreadystatechange(); | ||
|  |       } | ||
|  |     }; | ||
|  |     options.error = function(e) { | ||
|  |       var req = _state.request; | ||
|  |       _log.error(cat, req.method + ' ' + req.path, e); | ||
|  | 
 | ||
|  |       // 1. set response body to null
 | ||
|  |       xhr.responseText = ''; | ||
|  |       xhr.responseXML = null; | ||
|  | 
 | ||
|  |       // 2. set error flag to true (and reset status)
 | ||
|  |       _state.errorFlag = true; | ||
|  |       xhr.status = 0; | ||
|  |       xhr.statusText = ''; | ||
|  | 
 | ||
|  |       // 3. set state to done
 | ||
|  |       xhr.readyState = DONE; | ||
|  | 
 | ||
|  |       // 4. asyc flag is always true, so dispatch onreadystatechange
 | ||
|  |       if(xhr.onreadystatechange) { | ||
|  |         xhr.onreadystatechange(); | ||
|  |       } | ||
|  |     }; | ||
|  | 
 | ||
|  |     // 7. send request
 | ||
|  |     _state.client.send(options); | ||
|  |   }; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Aborts the request. | ||
|  |    */ | ||
|  |   xhr.abort = function() { | ||
|  |     // 1. abort send
 | ||
|  |     // 2. stop network activity
 | ||
|  |     _state.request.abort(); | ||
|  | 
 | ||
|  |     // 3. set response to null
 | ||
|  |     xhr.responseText = ''; | ||
|  |     xhr.responseXML = null; | ||
|  | 
 | ||
|  |     // 4. set error flag to true (and reset status)
 | ||
|  |     _state.errorFlag = true; | ||
|  |     xhr.status = 0; | ||
|  |     xhr.statusText = ''; | ||
|  | 
 | ||
|  |     // 5. clear user headers
 | ||
|  |     _state.request = null; | ||
|  |     _state.response = null; | ||
|  | 
 | ||
|  |     // 6. if state is DONE or UNSENT, or if OPENED and send flag is false
 | ||
|  |     if(xhr.readyState === DONE || xhr.readyState === UNSENT || | ||
|  |      (xhr.readyState === OPENED && !_state.sendFlag)) { | ||
|  |       // 7. set ready state to unsent
 | ||
|  |       xhr.readyState = UNSENT; | ||
|  |     } else { | ||
|  |       // 6.1 set state to DONE
 | ||
|  |       xhr.readyState = DONE; | ||
|  | 
 | ||
|  |       // 6.2 set send flag to false
 | ||
|  |       _state.sendFlag = false; | ||
|  | 
 | ||
|  |       // 6.3 dispatch onreadystatechange
 | ||
|  |       if(xhr.onreadystatechange) { | ||
|  |         xhr.onreadystatechange(); | ||
|  |       } | ||
|  | 
 | ||
|  |       // 7. set state to UNSENT
 | ||
|  |       xhr.readyState = UNSENT; | ||
|  |     } | ||
|  |   }; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Gets all response headers as a string. | ||
|  |    * | ||
|  |    * @return the HTTP-encoded response header fields. | ||
|  |    */ | ||
|  |   xhr.getAllResponseHeaders = function() { | ||
|  |     var rval = ''; | ||
|  |     if(_state.response !== null) { | ||
|  |       var fields = _state.response.fields; | ||
|  |       $.each(fields, function(name, array) { | ||
|  |         $.each(array, function(i, value) { | ||
|  |           rval += name + ': ' + value + '\r\n'; | ||
|  |         }); | ||
|  |       }); | ||
|  |     } | ||
|  |     return rval; | ||
|  |   }; | ||
|  | 
 | ||
|  |   /** | ||
|  |    * Gets a single header field value or, if there are multiple | ||
|  |    * fields with the same name, a comma-separated list of header | ||
|  |    * values. | ||
|  |    * | ||
|  |    * @return the header field value(s) or null. | ||
|  |    */ | ||
|  |   xhr.getResponseHeader = function(header) { | ||
|  |     var rval = null; | ||
|  |     if(_state.response !== null) { | ||
|  |       if(header in _state.response.fields) { | ||
|  |         rval = _state.response.fields[header]; | ||
|  |         if(forge.util.isArray(rval)) { | ||
|  |           rval = rval.join(); | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  |     return rval; | ||
|  |   }; | ||
|  | 
 | ||
|  |   return xhr; | ||
|  | }; | ||
|  | 
 | ||
|  | })(jQuery); |