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.
		
		
		
		
		
			
		
			
				
					
					
						
							736 lines
						
					
					
						
							22 KiB
						
					
					
				
			
		
		
	
	
							736 lines
						
					
					
						
							22 KiB
						
					
					
				/** | 
						|
 * 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);
 | 
						|
 |