/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Definition of the ChannelRequest class. The ChannelRequest
 * object encapsulates the logic for making a single request, either for the
 * forward channel, back channel, or test channel, to the server. It contains
 * the logic for the three types of transports we use in the BrowserChannel:
 * XMLHTTP, Trident ActiveX (ie only), and Image request. It provides timeout
 * detection. This class is part of the BrowserChannel implementation and is not
 * for use by normal application code.
 */


goog.provide('goog.net.ChannelRequest');
goog.provide('goog.net.ChannelRequest.Error');

goog.require('goog.Timer');
goog.require('goog.async.Throttle');
goog.require('goog.dispose');
goog.require('goog.dom.TagName');
goog.require('goog.dom.safe');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.html.SafeUrl');
goog.require('goog.html.uncheckedconversions');
goog.require('goog.net.ErrorCode');
goog.require('goog.net.EventType');
goog.require('goog.net.XmlHttp');
goog.require('goog.net.browserchannelinternal.ServerReachability');
goog.require('goog.net.browserchannelinternal.hooks');
goog.require('goog.net.browserchannelinternal.stats');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.string.Const');
goog.require('goog.userAgent');
goog.requireType('goog.Uri');
goog.requireType('goog.net.BrowserChannel');
goog.requireType('goog.net.BrowserTestChannel');
goog.requireType('goog.net.ChannelDebug');
goog.requireType('goog.net.XhrIo');


/**
 * Creates a ChannelRequest object which encapsulates a request to the server.
 * A new ChannelRequest is created for each request to the server.
 *
 * @param {goog.net.BrowserChannel|goog.net.BrowserTestChannel} channel
 *     The BrowserChannel that owns this request.
 * @param {goog.net.ChannelDebug} channelDebug A ChannelDebug to use for
 *     logging.
 * @param {string=} opt_sessionId  The session id for the channel.
 * @param {string|number=} opt_requestId  The request id for this request.
 * @param {number=} opt_retryId  The retry id for this request.
 * @constructor
 */
goog.net.ChannelRequest = function(
    channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {
  'use strict';
  /**
   * The BrowserChannel object that owns the request.
   * @type {goog.net.BrowserChannel|goog.net.BrowserTestChannel}
   * @private
   */
  this.channel_ = channel;

  /**
   * The channel debug to use for logging
   * @type {goog.net.ChannelDebug}
   * @private
   */
  this.channelDebug_ = channelDebug;

  /**
   * The Session ID for the channel.
   * @type {string|undefined}
   * @private
   */
  this.sid_ = opt_sessionId;

  /**
   * The RID (request ID) for the request.
   * @type {string|number|undefined}
   * @private
   */
  this.rid_ = opt_requestId;


  /**
   * The attempt number of the current request.
   * @type {number}
   * @private
   */
  this.retryId_ = opt_retryId || 1;


  /**
   * The timeout in ms before failing the request.
   * @type {number}
   * @private
   */
  this.timeout_ = goog.net.ChannelRequest.TIMEOUT_MS;

  /**
   * An object to keep track of the channel request event listeners.
   * @type {!goog.events.EventHandler<!goog.net.ChannelRequest>}
   * @private
   */
  this.eventHandler_ = new goog.events.EventHandler(this);

  /**
   * A timer for polling responseText in browsers that don't fire
   * onreadystatechange during incremental loading of responseText.
   * @type {goog.Timer}
   * @private
   */
  this.pollingTimer_ = new goog.Timer();

  this.pollingTimer_.setInterval(goog.net.ChannelRequest.POLLING_INTERVAL_MS);
};


/**
 * Extra HTTP headers to add to all the requests sent to the server.
 * @type {?Object}
 * @private
 */
goog.net.ChannelRequest.prototype.extraHeaders_ = null;


/**
 * Whether the request was successful. This is only set to true after the
 * request successfuly completes.
 * @type {boolean}
 * @private
 */
goog.net.ChannelRequest.prototype.successful_ = false;


/**
 * The TimerID of the timer used to detect if the request has timed-out.
 * @type {?number}
 * @private
 */
goog.net.ChannelRequest.prototype.watchDogTimerId_ = null;


/**
 * The time in the future when the request will timeout.
 * @type {?number}
 * @private
 */
goog.net.ChannelRequest.prototype.watchDogTimeoutTime_ = null;


/**
 * The time the request started.
 * @type {?number}
 * @private
 */
goog.net.ChannelRequest.prototype.requestStartTime_ = null;


/**
 * The type of request (XMLHTTP, IMG, Trident)
 * @type {?number}
 * @private
 */
goog.net.ChannelRequest.prototype.type_ = null;


/**
 * The base Uri for the request. The includes all the parameters except the
 * one that indicates the retry number.
 * @type {goog.Uri?}
 * @private
 */
goog.net.ChannelRequest.prototype.baseUri_ = null;


/**
 * The request Uri that was actually used for the most recent request attempt.
 * @type {goog.Uri?}
 * @private
 */
goog.net.ChannelRequest.prototype.requestUri_ = null;


/**
 * The post data, if the request is a post.
 * @type {?string}
 * @private
 */
goog.net.ChannelRequest.prototype.postData_ = null;


/**
 * The XhrLte request if the request is using XMLHTTP
 * @type {?goog.net.XhrIo}
 * @private
 */
goog.net.ChannelRequest.prototype.xmlHttp_ = null;


/**
 * The position of where the next unprocessed chunk starts in the response
 * text.
 * @type {number}
 * @private
 */
goog.net.ChannelRequest.prototype.xmlHttpChunkStart_ = 0;


/**
 * The Trident instance if the request is using Trident.
 * @type {?Object}
 * @private
 */
goog.net.ChannelRequest.prototype.trident_ = null;


/**
 * The verb (Get or Post) for the request.
 * @type {?string}
 * @private
 */
goog.net.ChannelRequest.prototype.verb_ = null;


/**
 * The last error if the request failed.
 * @type {?goog.net.ChannelRequest.Error}
 * @private
 */
goog.net.ChannelRequest.prototype.lastError_ = null;


/**
 * The last status code received.
 * @type {number}
 * @private
 */
goog.net.ChannelRequest.prototype.lastStatusCode_ = -1;


/**
 * Whether to send the Connection:close header as part of the request.
 * @type {boolean}
 * @private
 */
goog.net.ChannelRequest.prototype.sendClose_ = true;


/**
 * Whether the request has been cancelled due to a call to cancel.
 * @type {boolean}
 * @private
 */
goog.net.ChannelRequest.prototype.cancelled_ = false;


/**
 * A throttle time in ms for readystatechange events for the backchannel.
 * Useful for throttling when ready state is INTERACTIVE (partial data).
 * If set to zero no throttle is used.
 *
 * @see goog.net.BrowserChannel.prototype.readyStateChangeThrottleMs_
 *
 * @type {number}
 * @private
 */
goog.net.ChannelRequest.prototype.readyStateChangeThrottleMs_ = 0;


/**
 * The throttle for readystatechange events for the current request, or null
 * if there is none.
 * @type {?goog.async.Throttle}
 * @private
 */
goog.net.ChannelRequest.prototype.readyStateChangeThrottle_ = null;


/**
 * Default timeout in MS for a request. The server must return data within this
 * time limit for the request to not timeout.
 * @type {number}
 */
goog.net.ChannelRequest.TIMEOUT_MS = 45 * 1000;


/**
 * How often to poll (in MS) for changes to responseText in browsers that don't
 * fire onreadystatechange during incremental loading of responseText.
 * @type {number}
 */
goog.net.ChannelRequest.POLLING_INTERVAL_MS = 250;


/**
 * Minimum version of Safari that receives a non-null responseText in ready
 * state interactive.
 * @type {string}
 * @private
 */
goog.net.ChannelRequest.MIN_WEBKIT_FOR_INTERACTIVE_ = '420+';


/**
 * Enum for channel requests type
 * @enum {number}
 * @private
 */
goog.net.ChannelRequest.Type_ = {
  /**
   * XMLHTTP requests.
   */
  XML_HTTP: 1,

  /**
   * IMG requests.
   */
  IMG: 2,

  /**
   * Requests that use the MSHTML ActiveX control.
   */
  TRIDENT: 3,
};


/**
 * Enum type for identifying a ChannelRequest error.
 * @enum {number}
 */
goog.net.ChannelRequest.Error = {
  /**
   * Errors due to a non-200 status code.
   */
  STATUS: 0,

  /**
   * Errors due to no data being returned.
   */
  NO_DATA: 1,

  /**
   * Errors due to a timeout.
   */
  TIMEOUT: 2,

  /**
   * Errors due to the server returning an unknown.
   */
  UNKNOWN_SESSION_ID: 3,

  /**
   * Errors due to bad data being received.
   */
  BAD_DATA: 4,

  /**
   * Errors due to the handler throwing an exception.
   */
  HANDLER_EXCEPTION: 5,

  /**
   * The browser declared itself offline during the request.
   */
  BROWSER_OFFLINE: 6,

  /**
   * IE is blocking ActiveX streaming.
   */
  ACTIVE_X_BLOCKED: 7,
};

/**
 * Instantiates a ChannelRequest with the given parameters. Overidden in tests.
 *
 * @param {goog.net.BrowserChannel|goog.net.BrowserTestChannel} channel
 *     The BrowserChannel that owns this request.
 * @param {goog.net.ChannelDebug} channelDebug A ChannelDebug to use for
 *     logging.
 * @param {string=} opt_sessionId  The session id for the channel.
 * @param {string|number=} opt_requestId  The request id for this request.
 * @param {number=} opt_retryId  The retry id for this request.
 * @return {!goog.net.ChannelRequest} The created channel request.
 */
goog.net.ChannelRequest.createChannelRequest = function(
    channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId) {
  'use strict';
  return new goog.net.ChannelRequest(
      channel, channelDebug, opt_sessionId, opt_requestId, opt_retryId);
};


/**
 * Returns a useful error string for debugging based on the specified error
 * code.
 * @param {goog.net.ChannelRequest.Error} errorCode The error code.
 * @param {number} statusCode The HTTP status code.
 * @return {string} The error string for the given code combination.
 */
goog.net.ChannelRequest.errorStringFromCode = function(errorCode, statusCode) {
  'use strict';
  switch (errorCode) {
    case goog.net.ChannelRequest.Error.STATUS:
      return 'Non-200 return code (' + statusCode + ')';
    case goog.net.ChannelRequest.Error.NO_DATA:
      return 'XMLHTTP failure (no data)';
    case goog.net.ChannelRequest.Error.TIMEOUT:
      return 'HttpConnection timeout';
    default:
      return 'Unknown error';
  }
};


/**
 * Sentinel value used to indicate an invalid chunk in a multi-chunk response.
 * @type {Object}
 * @private
 */
goog.net.ChannelRequest.INVALID_CHUNK_ = {};


/**
 * Sentinel value used to indicate an incomplete chunk in a multi-chunk
 * response.
 * @type {Object}
 * @private
 */
goog.net.ChannelRequest.INCOMPLETE_CHUNK_ = {};


/**
 * Returns whether XHR streaming is supported on this browser.
 *
 * If XHR streaming is not supported, we will try to use an ActiveXObject
 * to create a Forever IFrame.
 *
 * @return {boolean} Whether XHR streaming is supported.
 * @see http://code.google.com/p/closure-library/issues/detail?id=346
 */
goog.net.ChannelRequest.supportsXhrStreaming = function() {
  'use strict';
  return !goog.userAgent.IE || goog.userAgent.isDocumentModeOrHigher(10);
};


/**
 * Sets extra HTTP headers to add to all the requests sent to the server.
 *
 * @param {Object} extraHeaders The HTTP headers.
 */
goog.net.ChannelRequest.prototype.setExtraHeaders = function(extraHeaders) {
  'use strict';
  this.extraHeaders_ = extraHeaders;
};


/**
 * Sets the timeout for a request
 *
 * @param {number} timeout   The timeout in MS for when we fail the request.
 */
goog.net.ChannelRequest.prototype.setTimeout = function(timeout) {
  'use strict';
  this.timeout_ = timeout;
};


/**
 * Sets the throttle for handling onreadystatechange events for the request.
 *
 * @param {number} throttle The throttle in ms.  A value of zero indicates
 *     no throttle.
 */
goog.net.ChannelRequest.prototype.setReadyStateChangeThrottle = function(
    throttle) {
  'use strict';
  this.readyStateChangeThrottleMs_ = throttle;
};


/**
 * Uses XMLHTTP to send an HTTP POST to the server.
 *
 * @param {goog.Uri} uri  The uri of the request.
 * @param {string} postData  The data for the post body.
 * @param {boolean} decodeChunks  Whether to the result is expected to be
 *     encoded for chunking and thus requires decoding.
 */
goog.net.ChannelRequest.prototype.xmlHttpPost = function(
    uri, postData, decodeChunks) {
  'use strict';
  this.type_ = goog.net.ChannelRequest.Type_.XML_HTTP;
  this.baseUri_ = uri.clone().makeUnique();
  this.postData_ = postData;
  /** @suppress {strictMissingProperties} Added to tighten compiler checks */
  this.decodeChunks_ = decodeChunks;
  this.sendXmlHttp_(null /* hostPrefix */);
};


/**
 * Uses XMLHTTP to send an HTTP GET to the server.
 *
 * @param {goog.Uri} uri  The uri of the request.
 * @param {boolean} decodeChunks  Whether to the result is expected to be
 *     encoded for chunking and thus requires decoding.
 * @param {?string} hostPrefix  The host prefix, if we might be using a
 *     secondary domain.  Note that it should also be in the URL, adding this
 *     won't cause it to be added to the URL.
 * @param {boolean=} opt_noClose   Whether to request that the tcp/ip connection
 *     should be closed.
 */
goog.net.ChannelRequest.prototype.xmlHttpGet = function(
    uri, decodeChunks, hostPrefix, opt_noClose) {
  'use strict';
  this.type_ = goog.net.ChannelRequest.Type_.XML_HTTP;
  this.baseUri_ = uri.clone().makeUnique();
  this.postData_ = null;
  /** @suppress {strictMissingProperties} Added to tighten compiler checks */
  this.decodeChunks_ = decodeChunks;
  if (opt_noClose) {
    this.sendClose_ = false;
  }
  this.sendXmlHttp_(hostPrefix);
};


/**
 * Sends a request via XMLHTTP according to the current state of the
 * ChannelRequest object.
 *
 * @param {?string} hostPrefix The host prefix, if we might be using a secondary
 *     domain.
 * @private
 */
goog.net.ChannelRequest.prototype.sendXmlHttp_ = function(hostPrefix) {
  'use strict';
  this.requestStartTime_ = Date.now();
  this.ensureWatchDogTimer_();

  // clone the base URI to create the request URI. The request uri has the
  // attempt number as a parameter which helps in debugging.
  this.requestUri_ = this.baseUri_.clone();
  this.requestUri_.setParameterValues('t', this.retryId_);

  // send the request either as a POST or GET
  this.xmlHttpChunkStart_ = 0;
  const useSecondaryDomains = this.channel_.shouldUseSecondaryDomains();
  this.xmlHttp_ =
      this.channel_.createXhrIo(useSecondaryDomains ? hostPrefix : null);

  if (this.readyStateChangeThrottleMs_ > 0) {
    this.readyStateChangeThrottle_ = new goog.async.Throttle(
        goog.bind(this.xmlHttpHandler_, this, this.xmlHttp_),
        this.readyStateChangeThrottleMs_);
  }

  this.eventHandler_.listen(
      this.xmlHttp_, goog.net.EventType.READY_STATE_CHANGE,
      this.readyStateChangeHandler_);

  const headers =
      this.extraHeaders_ ? goog.object.clone(this.extraHeaders_) : {};
  if (this.postData_) {
    // todo (jonp) - use POST constant when Dan defines it
    this.verb_ = 'POST';
    headers['Content-Type'] = 'application/x-www-form-urlencoded';
    this.xmlHttp_.send(this.requestUri_, this.verb_, this.postData_, headers);
  } else {
    // todo (jonp) - use GET constant when Dan defines it
    this.verb_ = 'GET';

    // If the user agent is webkit, we cannot send the close header since it is
    // disallowed by the browser.  If we attempt to set the "Connection: close"
    // header in WEBKIT browser, it will actually causes an error message.
    if (this.sendClose_ && !goog.userAgent.WEBKIT) {
      headers['Connection'] = 'close';
    }
    this.xmlHttp_.send(this.requestUri_, this.verb_, null, headers);
  }
  this.channel_.notifyServerReachabilityEvent(
      goog.net.browserchannelinternal.ServerReachability.REQUEST_MADE);
  this.channelDebug_.xmlHttpChannelRequest(
      this.verb_, this.requestUri_, this.rid_, this.retryId_, this.postData_);
};


/**
 * Handles a readystatechange event.
 * @param {goog.events.Event} evt The event.
 * @private
 */
goog.net.ChannelRequest.prototype.readyStateChangeHandler_ = function(evt) {
  'use strict';
  const xhr = /** @type {goog.net.XhrIo} */ (evt.target);
  const throttle = this.readyStateChangeThrottle_;
  if (throttle &&
      xhr.getReadyState() == goog.net.XmlHttp.ReadyState.INTERACTIVE) {
    // Only throttle in the partial data case.
    this.channelDebug_.debug('Throttling readystatechange.');
    throttle.fire();
  } else {
    // If we haven't throttled, just handle response directly.
    this.xmlHttpHandler_(xhr);
  }
};


/**
 * XmlHttp handler
 * @param {goog.net.XhrIo} xmlhttp The XhrIo object for the current request.
 * @private
 */
goog.net.ChannelRequest.prototype.xmlHttpHandler_ = function(xmlhttp) {
  'use strict';
  goog.net.browserchannelinternal.hooks.onStartExecution();


  try {
    if (xmlhttp == this.xmlHttp_) {
      this.onXmlHttpReadyStateChanged_();
    } else {
      this.channelDebug_.warning(
          'Called back with an ' +
          'unexpected xmlhttp');
    }
  } catch (ex) {
    this.channelDebug_.debug('Failed call to OnXmlHttpReadyStateChanged_');
    if (this.xmlHttp_ && this.xmlHttp_.getResponseText()) {
      this.channelDebug_.dumpException(
          ex, 'ResponseText: ' + this.xmlHttp_.getResponseText());
    } else {
      this.channelDebug_.dumpException(ex, 'No response text');
    }
  } finally {
    goog.net.browserchannelinternal.hooks.onEndExecution();
  }
};


/**
 * Called by the readystate handler for XMLHTTP requests.
 *
 * @private
 * @suppress {strictMissingProperties} Added to tighten compiler checks
 */
goog.net.ChannelRequest.prototype.onXmlHttpReadyStateChanged_ = function() {
  'use strict';
  const readyState = this.xmlHttp_.getReadyState();
  const errorCode = this.xmlHttp_.getLastErrorCode();
  const statusCode = this.xmlHttp_.getStatus();
  // If it is Safari less than 420+, there is a bug that causes null to be
  // in the responseText on ready state interactive so we must wait for
  // ready state complete.
  if (!goog.net.ChannelRequest.supportsXhrStreaming() ||
      (goog.userAgent.WEBKIT &&
       !goog.userAgent.isVersionOrHigher(
           goog.net.ChannelRequest.MIN_WEBKIT_FOR_INTERACTIVE_))) {
    if (readyState < goog.net.XmlHttp.ReadyState.COMPLETE) {
      // not yet ready
      return;
    }
  } else {
    // we get partial results in browsers that support ready state interactive.
    // We also make sure that getResponseText is not null in interactive mode
    // before we continue.  However, we don't do it in Opera because it only
    // fire readyState == INTERACTIVE once.  We need the following code to poll
    if (readyState < goog.net.XmlHttp.ReadyState.INTERACTIVE ||
        readyState == goog.net.XmlHttp.ReadyState.INTERACTIVE &&
            !this.xmlHttp_.getResponseText()) {
      // not yet ready
      return;
    }
  }

  // Dispatch any appropriate network events.
  if (!this.cancelled_ && readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&
      errorCode != goog.net.ErrorCode.ABORT) {
    // Pretty conservative, these are the only known scenarios which we'd
    // consider indicative of a truly non-functional network connection.
    if (errorCode == goog.net.ErrorCode.TIMEOUT || statusCode <= 0) {
      this.channel_.notifyServerReachabilityEvent(
          goog.net.browserchannelinternal.ServerReachability.REQUEST_FAILED);
    } else {
      this.channel_.notifyServerReachabilityEvent(
          goog.net.browserchannelinternal.ServerReachability.REQUEST_SUCCEEDED);
    }
  }

  // got some data so cancel the watchdog timer
  this.cancelWatchDogTimer_();

  const status = this.xmlHttp_.getStatus();
  this.lastStatusCode_ = status;
  const responseText = this.xmlHttp_.getResponseText();
  if (!responseText) {
    this.channelDebug_.debug(
        'No response text for uri ' + this.requestUri_ + ' status ' + status);
  }
  this.successful_ = (status == 200);

  this.channelDebug_.xmlHttpChannelResponseMetaData(
      /** @type {string} */ (this.verb_), this.requestUri_, this.rid_,
      this.retryId_, readyState, status);

  if (!this.successful_) {
    if (status == 400 && responseText.indexOf('Unknown SID') > 0) {
      // the server error string will include 'Unknown SID' which indicates the
      // server doesn't know about the session (maybe it got restarted, maybe
      // the user got moved to another server, etc.,). Handlers can special
      // case this error
      this.lastError_ = goog.net.ChannelRequest.Error.UNKNOWN_SESSION_ID;
      goog.net.browserchannelinternal.stats.notifyStatEvent(
          goog.net.browserchannelinternal.stats.Stat
              .REQUEST_UNKNOWN_SESSION_ID);
      this.channelDebug_.warning('XMLHTTP Unknown SID (' + this.rid_ + ')');
    } else {
      this.lastError_ = goog.net.ChannelRequest.Error.STATUS;
      goog.net.browserchannelinternal.stats.notifyStatEvent(
          goog.net.browserchannelinternal.stats.Stat.REQUEST_BAD_STATUS);
      this.channelDebug_.warning(
          'XMLHTTP Bad status ' + status + ' (' + this.rid_ + ')');
    }
    this.cleanup_();
    this.dispatchFailure_();
    return;
  }

  if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
    this.cleanup_();
  }

  if (this.decodeChunks_) {
    this.decodeNextChunks_(readyState, responseText);
  } else {
    this.channelDebug_.xmlHttpChannelResponseText(
        this.rid_, responseText, null);
    this.safeOnRequestData_(responseText);
  }

  if (!this.successful_) {
    return;
  }

  if (!this.cancelled_) {
    if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
      this.channel_.onRequestComplete(this);
    } else {
      // The default is false, the result from this callback shouldn't carry
      // over to the next callback, otherwise the request looks successful if
      // the watchdog timer gets called
      this.successful_ = false;
      this.ensureWatchDogTimer_();
    }
  }
};


/**
 * Decodes the next set of available chunks in the response.
 * @param {number} readyState The value of readyState.
 * @param {string} responseText The value of responseText.
 * @private
 */
goog.net.ChannelRequest.prototype.decodeNextChunks_ = function(
    readyState, responseText) {
  'use strict';
  let decodeNextChunksSuccessful = true;
  while (!this.cancelled_ && this.xmlHttpChunkStart_ < responseText.length) {
    const chunkText = this.getNextChunk_(responseText);
    if (chunkText == goog.net.ChannelRequest.INCOMPLETE_CHUNK_) {
      if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE) {
        // should have consumed entire response when the request is done
        this.lastError_ = goog.net.ChannelRequest.Error.BAD_DATA;
        goog.net.browserchannelinternal.stats.notifyStatEvent(
            goog.net.browserchannelinternal.stats.Stat.REQUEST_INCOMPLETE_DATA);
        decodeNextChunksSuccessful = false;
      }
      this.channelDebug_.xmlHttpChannelResponseText(
          this.rid_, null, '[Incomplete Response]');
      break;
    } else if (chunkText == goog.net.ChannelRequest.INVALID_CHUNK_) {
      this.lastError_ = goog.net.ChannelRequest.Error.BAD_DATA;
      goog.net.browserchannelinternal.stats.notifyStatEvent(
          goog.net.browserchannelinternal.stats.Stat.REQUEST_BAD_DATA);
      this.channelDebug_.xmlHttpChannelResponseText(
          this.rid_, responseText, '[Invalid Chunk]');
      decodeNextChunksSuccessful = false;
      break;
    } else {
      this.channelDebug_.xmlHttpChannelResponseText(
          this.rid_, /** @type {string} */ (chunkText), null);
      this.safeOnRequestData_(/** @type {string} */ (chunkText));
    }
  }
  if (readyState == goog.net.XmlHttp.ReadyState.COMPLETE &&
      responseText.length == 0) {
    // also an error if we didn't get any response
    this.lastError_ = goog.net.ChannelRequest.Error.NO_DATA;
    goog.net.browserchannelinternal.stats.notifyStatEvent(
        goog.net.browserchannelinternal.stats.Stat.REQUEST_NO_DATA);
    decodeNextChunksSuccessful = false;
  }
  this.successful_ = this.successful_ && decodeNextChunksSuccessful;
  if (!decodeNextChunksSuccessful) {
    // malformed response - we make this trigger retry logic
    this.channelDebug_.xmlHttpChannelResponseText(
        this.rid_, responseText, '[Invalid Chunked Response]');
    this.cleanup_();
    this.dispatchFailure_();
  }
};


/**
 * Polls the response for new data.
 * @private
 */
goog.net.ChannelRequest.prototype.pollResponse_ = function() {
  'use strict';
  const readyState = this.xmlHttp_.getReadyState();
  const responseText = this.xmlHttp_.getResponseText();
  if (this.xmlHttpChunkStart_ < responseText.length) {
    this.cancelWatchDogTimer_();
    this.decodeNextChunks_(readyState, responseText);
    if (this.successful_ &&
        readyState != goog.net.XmlHttp.ReadyState.COMPLETE) {
      this.ensureWatchDogTimer_();
    }
  }
};


/**
 * Starts a polling interval for changes to responseText of the
 * XMLHttpRequest, for browsers that don't fire onreadystatechange
 * as data comes in incrementally.  This timer is disabled in
 * cleanup_().
 * @private
 */
goog.net.ChannelRequest.prototype.startPolling_ = function() {
  'use strict';
  this.eventHandler_.listen(
      this.pollingTimer_, goog.Timer.TICK, this.pollResponse_);
  this.pollingTimer_.start();
};


/**
 * Returns the next chunk of a chunk-encoded response. This is not standard
 * HTTP chunked encoding because browsers don't expose the chunk boundaries to
 * the application through XMLHTTP. So we have an additional chunk encoding at
 * the application level that lets us tell where the beginning and end of
 * individual responses are so that we can only try to eval a complete JS array.
 *
 * The encoding is the size of the chunk encoded as a decimal string followed
 * by a newline followed by the data.
 *
 * @param {string} responseText The response text from the XMLHTTP response.
 * @return {string|Object} The next chunk string or a sentinel object
 *                         indicating a special condition.
 * @private
 */
goog.net.ChannelRequest.prototype.getNextChunk_ = function(responseText) {
  'use strict';
  const sizeStartIndex = this.xmlHttpChunkStart_;
  const sizeEndIndex = responseText.indexOf('\n', sizeStartIndex);
  if (sizeEndIndex == -1) {
    return goog.net.ChannelRequest.INCOMPLETE_CHUNK_;
  }

  const sizeAsString = responseText.substring(sizeStartIndex, sizeEndIndex);
  const size = Number(sizeAsString);
  if (isNaN(size)) {
    return goog.net.ChannelRequest.INVALID_CHUNK_;
  }

  const chunkStartIndex = sizeEndIndex + 1;
  if (chunkStartIndex + size > responseText.length) {
    return goog.net.ChannelRequest.INCOMPLETE_CHUNK_;
  }

  const chunkText = responseText.slice(chunkStartIndex, chunkStartIndex + size);
  this.xmlHttpChunkStart_ = chunkStartIndex + size;
  return chunkText;
};


/**
 * Uses the Trident htmlfile ActiveX control to send a GET request in IE. This
 * is the innovation discovered that lets us get intermediate results in
 * Internet Explorer.  Thanks to http://go/kev
 * @param {goog.Uri} uri The uri to request from.
 * @param {boolean} usingSecondaryDomain Whether to use a secondary domain.
 */
goog.net.ChannelRequest.prototype.tridentGet = function(
    uri, usingSecondaryDomain) {
  'use strict';
  this.type_ = goog.net.ChannelRequest.Type_.TRIDENT;
  this.baseUri_ = uri.clone().makeUnique();
  this.tridentGet_(usingSecondaryDomain);
};


/**
 * Starts the Trident request.
 * @param {boolean} usingSecondaryDomain Whether to use a secondary domain.
 * @private
 * @suppress {strictMissingProperties} Added to tighten compiler checks
 */
goog.net.ChannelRequest.prototype.tridentGet_ = function(usingSecondaryDomain) {
  'use strict';
  this.requestStartTime_ = Date.now();
  this.ensureWatchDogTimer_();

  const hostname = usingSecondaryDomain ? window.location.hostname : '';
  this.requestUri_ = this.baseUri_.clone();
  this.requestUri_.setParameterValue('DOMAIN', hostname);
  this.requestUri_.setParameterValue('t', this.retryId_);

  try {
    this.trident_ = new ActiveXObject('htmlfile');
  } catch (e) {
    this.channelDebug_.severe('ActiveX blocked');
    this.cleanup_();

    this.lastError_ = goog.net.ChannelRequest.Error.ACTIVE_X_BLOCKED;
    goog.net.browserchannelinternal.stats.notifyStatEvent(
        goog.net.browserchannelinternal.stats.Stat.ACTIVE_X_BLOCKED);
    this.dispatchFailure_();
    return;
  }

  // Using goog.html.SafeHtml.create() might be viable here but since
  // this code is now superseded by
  // closure/labs/net/webchannel/channelrequest.js it's not worth risking
  // the performance regressions and bugs that might result. Instead we
  // do an unchecked conversion. Please be extra careful if modifying
  // the HTML construction in this code, it's brittle and so it's easy to make
  // mistakes.

  let body = '<html><body>';
  if (usingSecondaryDomain) {
    const escapedHostname =
        goog.net.ChannelRequest.escapeForStringInScript_(hostname);
    body += '<script>document.domain="' + escapedHostname + '"</scr' +
        'ipt>';
  }
  body += '</body></html>';
  const bodyHtml = goog.html.uncheckedconversions
                       .safeHtmlFromStringKnownToSatisfyTypeContract(
                           goog.string.Const.from('b/12014412'), body);

  this.trident_.open();
  goog.dom.safe.documentWrite(
      /** @type {!Document} */ (this.trident_), bodyHtml);
  this.trident_.close();

  /** @suppress {strictMissingProperties} Added to tighten compiler checks */
  this.trident_.parentWindow['m'] = goog.bind(this.onTridentRpcMessage_, this);
  /** @suppress {strictMissingProperties} Added to tighten compiler checks */
  this.trident_.parentWindow['d'] = goog.bind(this.onTridentDone_, this, true);
  /** @suppress {strictMissingProperties} Added to tighten compiler checks */
  this.trident_.parentWindow['rpcClose'] =
      goog.bind(this.onTridentDone_, this, false);

  /** @suppress {strictMissingProperties} Added to tighten compiler checks */
  const div = this.trident_.createElement(String(goog.dom.TagName.DIV));
  this.trident_.parentWindow.document.body.appendChild(div);

  const safeUrl = goog.html.SafeUrl.sanitize(this.requestUri_.toString());
  const sanitizedEscapedUrl =
      goog.string.htmlEscape(goog.html.SafeUrl.unwrap(safeUrl));
  const iframeHtml =
      goog.html.uncheckedconversions
          .safeHtmlFromStringKnownToSatisfyTypeContract(
              goog.string.Const.from('b/12014412'),
              '<iframe src="' + sanitizedEscapedUrl + '"></iframe>');
  goog.dom.safe.setInnerHtml(div, iframeHtml);

  this.channelDebug_.tridentChannelRequest(
      'GET', this.requestUri_, this.rid_, this.retryId_);
  this.channel_.notifyServerReachabilityEvent(
      goog.net.browserchannelinternal.ServerReachability.REQUEST_MADE);
};


/**
 * JavaScript-escapes a string so that it can be included inside a JS string.
 * Since the JS string is expected to be inside a <script>, HTML-escaping
 * cannot be used and thus '<' and '>' are also JS-escaped.
 * @param {string} string
 * @return {string}
 * @private
 */
goog.net.ChannelRequest.escapeForStringInScript_ = function(string) {
  'use strict';
  let escaped = '';
  for (let i = 0; i < string.length; i++) {
    const c = string.charAt(i);
    if (c == '<') {
      escaped += '\\x3c';
    } else if (c == '>') {
      escaped += '\\x3e';
    } else {
      // This will escape both " and '.
      escaped += goog.string.escapeChar(c);
    }
  }
  return escaped;
};


/**
 * Callback from the Trident htmlfile ActiveX control for when a new message
 * is received.
 *
 * @param {string} msg The data payload.
 * @private
 */
goog.net.ChannelRequest.prototype.onTridentRpcMessage_ = function(msg) {
  'use strict';
  // need to do async b/c this gets called off of the context of the ActiveX
  goog.net.browserchannelinternal.hooks.setTimeout(
      goog.bind(this.onTridentRpcMessageAsync_, this, msg), 0);
};


/**
 * Callback from the Trident htmlfile ActiveX control for when a new message
 * is received.
 *
 * @param {string} msg  The data payload.
 * @private
 */
goog.net.ChannelRequest.prototype.onTridentRpcMessageAsync_ = function(msg) {
  'use strict';
  if (this.cancelled_) {
    return;
  }
  this.channelDebug_.tridentChannelResponseText(this.rid_, msg);
  this.cancelWatchDogTimer_();
  this.safeOnRequestData_(msg);
  this.ensureWatchDogTimer_();
};


/**
 * Callback from the Trident htmlfile ActiveX control for when the request
 * is complete
 *
 * @param {boolean} successful Whether the request successfully completed.
 * @private
 */
goog.net.ChannelRequest.prototype.onTridentDone_ = function(successful) {
  'use strict';
  // need to do async b/c this gets called off of the context of the ActiveX
  goog.net.browserchannelinternal.hooks.setTimeout(
      goog.bind(this.onTridentDoneAsync_, this, successful), 0);
};


/**
 * Callback from the Trident htmlfile ActiveX control for when the request
 * is complete
 *
 * @param {boolean} successful Whether the request successfully completed.
 * @private
 */
goog.net.ChannelRequest.prototype.onTridentDoneAsync_ = function(successful) {
  'use strict';
  if (this.cancelled_) {
    return;
  }
  this.channelDebug_.tridentChannelResponseDone(this.rid_, successful);
  this.cleanup_();
  this.successful_ = successful;
  this.channel_.onRequestComplete(this);
  this.channel_.notifyServerReachabilityEvent(
      goog.net.browserchannelinternal.ServerReachability.BACK_CHANNEL_ACTIVITY);
};


/**
 * Uses an IMG tag to send an HTTP get to the server. This is only currently
 * used to terminate the connection, as an IMG tag is the most reliable way to
 * send something to the server while the page is getting torn down.
 * @param {goog.Uri} uri The uri to send a request to.
 */
goog.net.ChannelRequest.prototype.sendUsingImgTag = function(uri) {
  'use strict';
  this.type_ = goog.net.ChannelRequest.Type_.IMG;
  this.baseUri_ = uri.clone().makeUnique();
  this.imgTagGet_();
};


/**
 * Starts the IMG request.
 *
 * @private
 */
goog.net.ChannelRequest.prototype.imgTagGet_ = function() {
  'use strict';
  new Image().src = this.baseUri_.toString();
  this.requestStartTime_ = Date.now();
  this.ensureWatchDogTimer_();
};


/**
 * Cancels the request no matter what the underlying transport is.
 */
goog.net.ChannelRequest.prototype.cancel = function() {
  'use strict';
  this.cancelled_ = true;
  this.cleanup_();
};


/**
 * Ensures that there is watchdog timeout which is used to ensure that
 * the connection completes in time.
 *
 * @private
 */
goog.net.ChannelRequest.prototype.ensureWatchDogTimer_ = function() {
  'use strict';
  this.watchDogTimeoutTime_ = Date.now() + this.timeout_;
  this.startWatchDogTimer_(this.timeout_);
};


/**
 * Starts the watchdog timer which is used to ensure that the connection
 * completes in time.
 * @param {number} time The number of milliseconds to wait.
 * @private
 */
goog.net.ChannelRequest.prototype.startWatchDogTimer_ = function(time) {
  'use strict';
  if (this.watchDogTimerId_ != null) {
    // assertion
    throw new Error('WatchDog timer not null');
  }
  this.watchDogTimerId_ = goog.net.browserchannelinternal.hooks.setTimeout(
      goog.bind(this.onWatchDogTimeout_, this), time);
};


/**
 * Cancels the watchdog timer if it has been started.
 *
 * @private
 */
goog.net.ChannelRequest.prototype.cancelWatchDogTimer_ = function() {
  'use strict';
  if (this.watchDogTimerId_) {
    goog.global.clearTimeout(this.watchDogTimerId_);
    this.watchDogTimerId_ = null;
  }
};


/**
 * Called when the watchdog timer is triggered. It also handles a case where it
 * is called too early which we suspect may be happening sometimes
 * (not sure why)
 *
 * @private
 * @suppress {strictPrimitiveOperators}
 */
goog.net.ChannelRequest.prototype.onWatchDogTimeout_ = function() {
  'use strict';
  this.watchDogTimerId_ = null;
  const now = Date.now();
  if (now - this.watchDogTimeoutTime_ >= 0) {
    this.handleTimeout_();
  } else {
    // got called too early for some reason
    this.channelDebug_.warning('WatchDog timer called too early');
    this.startWatchDogTimer_(this.watchDogTimeoutTime_ - now);
  }
};


/**
 * Called when the request has actually timed out. Will cleanup and notify the
 * channel of the failure.
 *
 * @private
 */
goog.net.ChannelRequest.prototype.handleTimeout_ = function() {
  'use strict';
  if (this.successful_) {
    // Should never happen.
    this.channelDebug_.severe(
        'Received watchdog timeout even though request loaded successfully');
  }

  this.channelDebug_.timeoutResponse(this.requestUri_);
  // IMG requests never notice if they were successful, and always 'time out'.
  // This fact says nothing about reachability.
  if (this.type_ != goog.net.ChannelRequest.Type_.IMG) {
    this.channel_.notifyServerReachabilityEvent(
        goog.net.browserchannelinternal.ServerReachability.REQUEST_FAILED);
  }
  this.cleanup_();

  // set error and dispatch failure
  this.lastError_ = goog.net.ChannelRequest.Error.TIMEOUT;
  goog.net.browserchannelinternal.stats.notifyStatEvent(
      goog.net.browserchannelinternal.stats.Stat.REQUEST_TIMEOUT);
  this.dispatchFailure_();
};


/**
 * Notifies the channel that this request failed.
 * @private
 */
goog.net.ChannelRequest.prototype.dispatchFailure_ = function() {
  'use strict';
  if (this.channel_.isClosed() || this.cancelled_) {
    return;
  }

  this.channel_.onRequestComplete(this);
};


/**
 * Cleans up the objects used to make the request. This function is
 * idempotent.
 *
 * @private
 */
goog.net.ChannelRequest.prototype.cleanup_ = function() {
  'use strict';
  this.cancelWatchDogTimer_();

  goog.dispose(this.readyStateChangeThrottle_);
  this.readyStateChangeThrottle_ = null;

  // Stop the polling timer, if necessary.
  this.pollingTimer_.stop();

  // Unhook all event handlers.
  this.eventHandler_.removeAll();

  if (this.xmlHttp_) {
    // clear out this.xmlHttp_ before aborting so we handle getting reentered
    // inside abort
    const xmlhttp = this.xmlHttp_;
    this.xmlHttp_ = null;
    xmlhttp.abort();
    xmlhttp.dispose();
  }

  if (this.trident_) {
    this.trident_ = null;
  }
};


/**
 * Indicates whether the request was successful. Only valid after the handler
 * is called to indicate completion of the request.
 *
 * @return {boolean} True if the request succeeded.
 */
goog.net.ChannelRequest.prototype.getSuccess = function() {
  'use strict';
  return this.successful_;
};


/**
 * If the request was not successful, returns the reason.
 *
 * @return {?goog.net.ChannelRequest.Error}  The last error.
 */
goog.net.ChannelRequest.prototype.getLastError = function() {
  'use strict';
  return this.lastError_;
};


/**
 * Returns the status code of the last request.
 * @return {number} The status code of the last request.
 */
goog.net.ChannelRequest.prototype.getLastStatusCode = function() {
  'use strict';
  return this.lastStatusCode_;
};


/**
 * Returns the session id for this channel.
 *
 * @return {string|undefined} The session ID.
 */
goog.net.ChannelRequest.prototype.getSessionId = function() {
  'use strict';
  return this.sid_;
};


/**
 * Returns the request id for this request. Each request has a unique request
 * id and the request IDs are a sequential increasing count.
 *
 * @return {string|number|undefined} The request ID.
 */
goog.net.ChannelRequest.prototype.getRequestId = function() {
  'use strict';
  return this.rid_;
};


/**
 * Returns the data for a post, if this request is a post.
 *
 * @return {?string} The POST data provided by the request initiator.
 */
goog.net.ChannelRequest.prototype.getPostData = function() {
  'use strict';
  return this.postData_;
};


/**
 * Returns the time that the request started, if it has started.
 *
 * @return {?number} The time the request started, as returned by Date.now().
 */
goog.net.ChannelRequest.prototype.getRequestStartTime = function() {
  'use strict';
  return this.requestStartTime_;
};


/**
 * Helper to call the callback's onRequestData, which catches any
 * exception and cleans up the request.
 * @param {string} data The request data.
 * @private
 */
goog.net.ChannelRequest.prototype.safeOnRequestData_ = function(data) {
  'use strict';
  try {
    this.channel_.onRequestData(this, data);
    this.channel_.notifyServerReachabilityEvent(
        goog.net.browserchannelinternal.ServerReachability
            .BACK_CHANNEL_ACTIVITY);
  } catch (e) {
    // Dump debug info, but keep going without closing the channel.
    this.channelDebug_.dumpException(e, 'Error in httprequest callback');
  }
};
