| /** | 
|  * 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.full] = _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 = http.parseUrl(options.url); | 
|     if(!url) { | 
|       var error = new Error('Invalid url.'); | 
|       error.details = { | 
|         url: options.url | 
|       }; | 
|     } | 
|   | 
|     // find client | 
|     if(url.full in _clients) { | 
|       // client found | 
|       _state.client = _clients[url.full]; | 
|     } 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.full] = _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); |