text: // Copyright 2006 The Closure Library Authors. All Rights Reserved.
  //
  // Licensed under the Apache License, Version 2.0 (the "License");
  // you may not use this file except in compliance with the License.
  // You may obtain a copy of the License at
  //
  //      http://www.apache.org/licenses/LICENSE-2.0
  //
  // Unless required by applicable law or agreed to in writing, software
  // distributed under the License is distributed on an "AS-IS" BASIS,
  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  // See the License for the specific language governing permissions and
  // limitations under the License.
  
  /**
   * @fileoverview Cross domain RPC library using the <a
   * href="http://go/xd2_design" target="_top">XD2 approach</a>.
   *
   * <h5>Protocol</h5>
   * Client sends a request across domain via a form submission.  Server
   * receives these parameters: "xdpe:request-id", "xdpe:dummy-uri" ("xdpe" for
   * "cross domain parameter to echo back") and other user parameters prefixed
   * with "xdp" (for "cross domain parameter").  Headers are passed as parameters
   * prefixed with "xdh" (for "cross domain header").  Only strings are supported
   * for parameters and headers.  A GET method is mapped to a form GET.  All
   * other methods are mapped to a POST.  Server is expected to produce a
   * HTML response such as the following:
   * <pre>
   * &lt;body&gt;
   * &lt;script type="text/javascript"
   *     src="path-to-crossdomainrpc.js"&gt;&lt;/script&gt;
   * var currentDirectory = location.href.substring(
   *     0, location.href.lastIndexOf('/')
   * );
   *
   * // echo all parameters prefixed with "xdpe:"
   * var echo = {};
   * echo[goog.net.CrossDomainRpc.PARAM_ECHO_REQUEST_ID] =
   *     &lt;value of parameter "xdpe:request-id"&gt;;
   * echo[goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI] =
   *     &lt;value of parameter "xdpe:dummy-uri"&gt;;
   *
   * goog.net.CrossDomainRpc.sendResponse(
   *     '({"result":"&lt;responseInJSON"})',
   *     true,    // is JSON
   *     echo,    // parameters to echo back
   *     status,  // response status code
   *     headers  // response headers
   * );
   * &lt;/script&gt;
   * &lt;/body&gt;
   * </pre>
   *
   * <h5>Server Side</h5>
   * For an example of the server side, refer to the following files:
   * <ul>
   * <li>http://go/xdservletfilter.java</li>
   * <li>http://go/xdservletrequest.java</li>
   * <li>http://go/xdservletresponse.java</li>
   * </ul>
   *
   * <h5>System Requirements</h5>
   * Tested on IE6, IE7, Firefox 2.0 and Safari nightly r23841.
   */
  
  goog.provide('goog.net.CrossDomainRpc');
  
  goog.require('goog.Uri');
  goog.require('goog.dom');
  goog.require('goog.dom.TagName');
  goog.require('goog.dom.safe');
  goog.require('goog.events');
  goog.require('goog.events.EventTarget');
  goog.require('goog.events.EventType');
  goog.require('goog.html.SafeHtml');
  goog.require('goog.log');
  goog.require('goog.net.EventType');
  goog.require('goog.net.HttpStatus');
  goog.require('goog.string');
  goog.require('goog.userAgent');
  
  
  
  /**
   * Creates a new instance of cross domain RPC.
   *
   * @extends {goog.events.EventTarget}
   * @constructor
   * @final
   */
  goog.net.CrossDomainRpc = function() {
    goog.events.EventTarget.call(this);
  };
  goog.inherits(goog.net.CrossDomainRpc, goog.events.EventTarget);
  
  
  /**
   * Cross-domain response iframe marker.
   * @type {string}
   * @private
   */
  goog.net.CrossDomainRpc.RESPONSE_MARKER_ = 'xdrp';
  
  
  /**
   * Use a fallback dummy resource if none specified or detected.
   * @type {boolean}
   * @private
   */
  goog.net.CrossDomainRpc.useFallBackDummyResource_ = true;
  
  
  /** @type {Object} */
  goog.net.CrossDomainRpc.prototype.responseHeaders;
  
  
  /** @type {string} */
  goog.net.CrossDomainRpc.prototype.responseText;
  
  
  /** @type {number} */
  goog.net.CrossDomainRpc.prototype.status;
  
  
  /** @type {number} */
  goog.net.CrossDomainRpc.prototype.timeWaitedAfterResponseReady_;
  
  
  /** @private {boolean} */
  goog.net.CrossDomainRpc.prototype.responseTextIsJson_;
  
  
  /** @private {boolean} */
  goog.net.CrossDomainRpc.prototype.responseReady_;
  
  
  /** @private {!HTMLIFrameElement} */
  goog.net.CrossDomainRpc.prototype.requestFrame_;
  
  
  /** @private {goog.events.Key} */
  goog.net.CrossDomainRpc.prototype.loadListenerKey_;
  
  
  /**
   * Checks to see if we are executing inside a response iframe.  This is the
   * case when this page is used as a dummy resource to gain caller's domain.
   * @return {*} True if we are executing inside a response iframe; false
   *     otherwise.
   * @private
   */
  goog.net.CrossDomainRpc.isInResponseIframe_ = function() {
    return window.location &&
        (window.location.hash.indexOf(goog.net.CrossDomainRpc.RESPONSE_MARKER_) ==
             1 ||
         window.location.search.indexOf(
             goog.net.CrossDomainRpc.RESPONSE_MARKER_) == 1);
  };
  
  
  /**
   * Stops execution of the rest of the page if this page is loaded inside a
   *    response iframe.
   */
  if (goog.net.CrossDomainRpc.isInResponseIframe_()) {
    if (goog.userAgent.EDGE_OR_IE) {
      document.execCommand('Stop');
    } else if (goog.userAgent.GECKO) {
      window.stop();
    } else {
      throw new Error('stopped');
    }
  }
  
  
  /**
   * Sets the URI for a dummy resource on caller's domain.  This function is
   * used for specifying a particular resource to use rather than relying on
   * auto detection.
   * @param {string} dummyResourceUri URI to dummy resource on the same domain
   *    of caller's page.
   */
  goog.net.CrossDomainRpc.setDummyResourceUri = function(dummyResourceUri) {
    goog.net.CrossDomainRpc.dummyResourceUri_ = dummyResourceUri;
  };
  
  
  /**
   * Sets whether a fallback dummy resource ("/robots.txt" on Firefox and Safari
   * and current page on IE) should be used when a suitable dummy resource is
   * not available.
   * @param {boolean} useFallBack Whether to use fallback or not.
   */
  goog.net.CrossDomainRpc.setUseFallBackDummyResource = function(useFallBack) {
    goog.net.CrossDomainRpc.useFallBackDummyResource_ = useFallBack;
  };
  
  
  /**
   * Sends a request across domain.
   * @param {string} uri Uri to make request to.
   * @param {Function=} opt_continuation Continuation function to be called
   *     when request is completed.  Takes one argument of an event object
   *     whose target has the following properties: "status" is the HTTP
   *     response status code, "responseText" is the response text,
   *     and "headers" is an object with all response headers.  The event
   *     target's getResponseJson() method returns a JavaScript object evaluated
   *     from the JSON response or undefined if response is not JSON.
   * @param {string=} opt_method Method of request. Default is POST.
   * @param {Object=} opt_params Parameters. Each property is turned into a
   *     request parameter.
   * @param {Object=} opt_headers Map of headers of the request.
   */
  goog.net.CrossDomainRpc.send = function(
      uri, opt_continuation, opt_method, opt_params, opt_headers) {
    var xdrpc = new goog.net.CrossDomainRpc();
    if (opt_continuation) {
      goog.events.listen(xdrpc, goog.net.EventType.COMPLETE, opt_continuation);
    }
    goog.events.listen(xdrpc, goog.net.EventType.READY, xdrpc.reset);
    xdrpc.sendRequest(uri, opt_method, opt_params, opt_headers);
  };
  
  
  /**
   * Sets debug mode to true or false.  When debug mode is on, response iframes
   * are visible and left behind after their use is finished.
   * @param {boolean} flag Flag to indicate intention to turn debug model on
   *     (true) or off (false).
   */
  goog.net.CrossDomainRpc.setDebugMode = function(flag) {
    goog.net.CrossDomainRpc.debugMode_ = flag;
  };
  
  
  /**
   * Logger for goog.net.CrossDomainRpc
   * @type {goog.log.Logger}
   * @private
   */
  goog.net.CrossDomainRpc.logger_ = goog.log.getLogger('goog.net.CrossDomainRpc');
  
  
  /**
   * Creates the HTML of an input element
   * @param {string} name Name of input element.
   * @param {*} value Value of input element.
   * @return {!goog.html.SafeHtml} HTML of input element with that name and value.
   * @private
   */
  goog.net.CrossDomainRpc.createInputHtml_ = function(name, value) {
    return goog.html.SafeHtml.create('textarea', {'name': name}, String(value));
  };
  
  
  /**
   * Finds a dummy resource that can be used by response to gain domain of
   * requester's page.
   * @return {string} URI of the resource to use.
   * @private
   */
  goog.net.CrossDomainRpc.getDummyResourceUri_ = function() {
    if (goog.net.CrossDomainRpc.dummyResourceUri_) {
      return goog.net.CrossDomainRpc.dummyResourceUri_;
    }
  
    // find a style sheet if not on IE, which will attempt to save style sheet
    if (goog.userAgent.GECKO) {
      var links = goog.dom.getElementsByTagName(goog.dom.TagName.LINK);
      for (var i = 0; i < links.length; i++) {
        var link = links[i];
        // find a link which is on the same domain as this page
        // cannot use one with '?' or '#' in its URL as it will confuse
        // goog.net.CrossDomainRpc.getFramePayload_()
        if (link.rel == 'stylesheet' &&
            goog.Uri.haveSameDomain(link.href, window.location.href) &&
            link.href.indexOf('?') < 0) {
          return goog.net.CrossDomainRpc.removeHash_(link.href);
        }
      }
    }
  
    var images = goog.dom.getElementsByTagName(goog.dom.TagName.IMG);
    for (var i = 0; i < images.length; i++) {
      var image = images[i];
      // find a link which is on the same domain as this page
      // cannot use one with '?' or '#' in its URL as it will confuse
      // goog.net.CrossDomainRpc.getFramePayload_()
      if (goog.Uri.haveSameDomain(image.src, window.location.href) &&
          image.src.indexOf('?') < 0) {
        return goog.net.CrossDomainRpc.removeHash_(image.src);
      }
    }
  
    if (!goog.net.CrossDomainRpc.useFallBackDummyResource_) {
      throw new Error(
          'No suitable dummy resource specified or detected for this page');
    }
  
    if (goog.userAgent.EDGE_OR_IE) {
      // use this page as the dummy resource; remove hash from URL if any
      return goog.net.CrossDomainRpc.removeHash_(window.location.href);
    } else {
      /**
       * Try to use "http://<this-domain>/robots.txt" which may exist.  Even if
       * it does not, an error page is returned and is a good dummy resource to
       * use on Firefox and Safari.  An existing resource is faster because it
       * is cached.
       */
      var locationHref = window.location.href;
      var rootSlash = locationHref.indexOf('/', locationHref.indexOf('//') + 2);
      var rootHref = locationHref.substring(0, rootSlash);
      return rootHref + '/robots.txt';
    }
  };
  
  
  /**
   * Removes everything at and after hash from URI
   * @param {string} uri Uri to to remove hash.
   * @return {string} Uri with its hash and all characters after removed.
   * @private
   */
  goog.net.CrossDomainRpc.removeHash_ = function(uri) {
    return uri.split('#')[0];
  };
  
  
  // ------------
  // request side
  
  
  /**
   * next request id used to support multiple XD requests at the same time
   * @type {number}
   * @private
   */
  goog.net.CrossDomainRpc.nextRequestId_ = 0;
  
  
  /**
   * Header prefix.
   * @type {string}
   */
  goog.net.CrossDomainRpc.HEADER = 'xdh:';
  
  
  /**
   * Parameter prefix.
   * @type {string}
   */
  goog.net.CrossDomainRpc.PARAM = 'xdp:';
  
  
  /**
   * Parameter to echo prefix.
   * @type {string}
   */
  goog.net.CrossDomainRpc.PARAM_ECHO = 'xdpe:';
  
  
  /**
   * Parameter to echo: request id
   * @type {string}
   */
  goog.net.CrossDomainRpc.PARAM_ECHO_REQUEST_ID =
      goog.net.CrossDomainRpc.PARAM_ECHO + 'request-id';
  
  
  /**
   * Parameter to echo: dummy resource URI
   * @type {string}
   */
  goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI =
      goog.net.CrossDomainRpc.PARAM_ECHO + 'dummy-uri';
  
  
  /**
   * Cross-domain request marker.
   * @type {string}
   * @private
   */
  goog.net.CrossDomainRpc.REQUEST_MARKER_ = 'xdrq';
  
  
  /**
   * Sends a request across domain.
   * @param {string} uri Uri to make request to.
   * @param {string=} opt_method Method of request, 'GET' or 'POST' (uppercase).
   *     Default is 'POST'.
   * @param {Object=} opt_params Parameters. Each property is turned into a
   *     request parameter.
   * @param {Object=} opt_headers Map of headers of the request.
   */
  goog.net.CrossDomainRpc.prototype.sendRequest = function(
      uri, opt_method, opt_params, opt_headers) {
    // create request frame
    var requestFrame = this.requestFrame_ =
        goog.dom.createElement(goog.dom.TagName.IFRAME);
    var requestId = goog.net.CrossDomainRpc.nextRequestId_++;
    requestFrame.id = goog.net.CrossDomainRpc.REQUEST_MARKER_ + '-' + requestId;
    if (!goog.net.CrossDomainRpc.debugMode_) {
      requestFrame.style.position = 'absolute';
      requestFrame.style.top = '-5000px';
      requestFrame.style.left = '-5000px';
    }
    document.body.appendChild(requestFrame);
  
    // build inputs
    var inputs = [];
  
    // add request id
    inputs.push(
        goog.net.CrossDomainRpc.createInputHtml_(
            goog.net.CrossDomainRpc.PARAM_ECHO_REQUEST_ID, requestId));
  
    // add dummy resource uri
    var dummyUri = goog.net.CrossDomainRpc.getDummyResourceUri_();
    goog.log.fine(goog.net.CrossDomainRpc.logger_, 'dummyUri: ' + dummyUri);
    inputs.push(
        goog.net.CrossDomainRpc.createInputHtml_(
            goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI, dummyUri));
  
    // add parameters
    if (opt_params) {
      for (var name in opt_params) {
        var value = opt_params[name];
        inputs.push(
            goog.net.CrossDomainRpc.createInputHtml_(
                goog.net.CrossDomainRpc.PARAM + name, value));
      }
    }
  
    // add headers
    if (opt_headers) {
      for (var name in opt_headers) {
        var value = opt_headers[name];
        inputs.push(
            goog.net.CrossDomainRpc.createInputHtml_(
                goog.net.CrossDomainRpc.HEADER + name, value));
      }
    }
  
    var requestFrameContentHtml = goog.html.SafeHtml.create(
        'body', {},
        goog.html.SafeHtml.create(
            'form',
            {'method': opt_method == 'GET' ? 'GET' : 'POST', 'action': uri},
            inputs));
    var requestFrameDoc = goog.dom.getFrameContentDocument(requestFrame);
    requestFrameDoc.open();
    goog.dom.safe.documentWrite(requestFrameDoc, requestFrameContentHtml);
    requestFrameDoc.close();
  
    requestFrameDoc.forms[0].submit();
    requestFrameDoc = null;
  
    this.loadListenerKey_ =
        goog.events.listen(requestFrame, goog.events.EventType.LOAD, function() {
          goog.log.fine(goog.net.CrossDomainRpc.logger_, 'response ready');
          this.responseReady_ = true;
        }, false, this);
  
    this.receiveResponse_();
  };
  
  
  /**
   * period of response polling (ms)
   * @type {number}
   * @private
   */
  goog.net.CrossDomainRpc.RESPONSE_POLLING_PERIOD_ = 50;
  
  
  /**
   * timeout from response comes back to sendResponse is called (ms)
   * @type {number}
   * @private
   */
  goog.net.CrossDomainRpc.SEND_RESPONSE_TIME_OUT_ = 500;
  
  
  /**
   * Receives response by polling to check readiness of response and then
   *     reads response frames and assembles response data
   * @private
   */
  goog.net.CrossDomainRpc.prototype.receiveResponse_ = function() {
    this.timeWaitedAfterResponseReady_ = 0;
    var responseDetectorHandle = window.setInterval(goog.bind(function() {
      this.detectResponse_(responseDetectorHandle);
    }, this), goog.net.CrossDomainRpc.RESPONSE_POLLING_PERIOD_);
  };
  
  
  /**
   * Detects response inside request frame
   * @param {number} responseDetectorHandle Handle of detector.
   * @private
   */
  goog.net.CrossDomainRpc.prototype.detectResponse_ = function(
      responseDetectorHandle) {
    var requestFrameWindow = this.requestFrame_.contentWindow;
    var grandChildrenLength = requestFrameWindow.frames.length;
    var responseInfoFrame = null;
    if (grandChildrenLength > 0 &&
        goog.net.CrossDomainRpc.isResponseInfoFrame_(
            responseInfoFrame =
                requestFrameWindow.frames[grandChildrenLength - 1])) {
      goog.log.fine(goog.net.CrossDomainRpc.logger_, 'xd response ready');
  
      var responseInfoPayload =
          goog.net.CrossDomainRpc.getFramePayload_(responseInfoFrame)
              .substring(1);
      var params = new goog.Uri.QueryData(responseInfoPayload);
  
      var chunks = [];
      var numChunks = Number(params.get('n'));
      goog.log.fine(
          goog.net.CrossDomainRpc.logger_,
          'xd response number of chunks: ' + numChunks);
      for (var i = 0; i < numChunks; i++) {
        var responseFrame = requestFrameWindow.frames[i];
        if (!responseFrame || !responseFrame.location ||
            !responseFrame.location.href) {
          // On Safari 3.0, it is sometimes the case that the
          // iframe exists but doesn't have a same domain href yet.
          goog.log.fine(
              goog.net.CrossDomainRpc.logger_, 'xd response iframe not ready');
          return;
        }
        var responseChunkPayload =
            goog.net.CrossDomainRpc.getFramePayload_(responseFrame);
        // go past "chunk="
        var chunkIndex =
            responseChunkPayload.indexOf(goog.net.CrossDomainRpc.PARAM_CHUNK_) +
            goog.net.CrossDomainRpc.PARAM_CHUNK_.length + 1;
        var chunk = responseChunkPayload.substring(chunkIndex);
        chunks.push(chunk);
      }
  
      window.clearInterval(responseDetectorHandle);
  
      var responseData = chunks.join('');
      // Payload is not encoded to begin with on IE. Decode in other cases only.
      if (!goog.userAgent.EDGE_OR_IE) {
        responseData = decodeURIComponent(responseData);
      }
  
      this.status = Number(params.get('status'));
      this.responseText = responseData;
      this.responseTextIsJson_ = params.get('isDataJson') == 'true';
      this.responseHeaders = /** @type {?Object} */ (JSON.parse(
          /** @type {string} */ (params.get('headers'))));
  
      this.dispatchEvent(goog.net.EventType.READY);
      this.dispatchEvent(goog.net.EventType.COMPLETE);
    } else {
      if (this.responseReady_) {
        /* The response has come back. But the first response iframe has not
         * been created yet. If this lasts long enough, it is an error.
         */
        this.timeWaitedAfterResponseReady_ +=
            goog.net.CrossDomainRpc.RESPONSE_POLLING_PERIOD_;
        if (this.timeWaitedAfterResponseReady_ >
            goog.net.CrossDomainRpc.SEND_RESPONSE_TIME_OUT_) {
          goog.log.fine(goog.net.CrossDomainRpc.logger_, 'xd response timed out');
          window.clearInterval(responseDetectorHandle);
  
          this.status = goog.net.HttpStatus.INTERNAL_SERVER_ERROR;
          this.responseText = 'response timed out';
  
          this.dispatchEvent(goog.net.EventType.READY);
          this.dispatchEvent(goog.net.EventType.ERROR);
          this.dispatchEvent(goog.net.EventType.COMPLETE);
        }
      }
    }
  };
  
  
  /**
   * Checks whether a frame is response info frame.
   * @param {Object} frame Frame to check.
   * @return {boolean} True if frame is a response info frame; false otherwise.
   * @private
   */
  goog.net.CrossDomainRpc.isResponseInfoFrame_ = function(frame) {
  
    try {
      return goog.net.CrossDomainRpc.getFramePayload_(frame).indexOf(
                 goog.net.CrossDomainRpc.RESPONSE_INFO_MARKER_) == 1;
    } catch (e) {
      // frame not ready for same-domain access yet
      return false;
    }
  };
  
  
  /**
   * Returns the payload of a frame (value after # or ? on the URL).  This value
   * is URL encoded except IE, where the value is not encoded to begin with.
   * @param {Object} frame Frame.
   * @return {string} Payload of that frame.
   * @private
   * @suppress {strictMissingProperties} Part of the go/strict_warnings_migration
   */
  goog.net.CrossDomainRpc.getFramePayload_ = function(frame) {
    var href = frame.location.href;
    var question = href.indexOf('?');
    var hash = href.indexOf('#');
    // On IE, beucase the URL is not encoded, we can have a case where ?
    // is the delimiter before payload and # in payload or # as the delimiter
    // and ? in payload.  So here we treat whoever is the first as the delimiter.
    var delimiter =
        question < 0 ? hash : hash < 0 ? question : Math.min(question, hash);
    return href.substring(delimiter);
  };
  
  
  /**
   * If response is JSON, evaluates it to a JavaScript object and
   * returns it; otherwise returns undefined.
   * @return {Object|undefined} JavaScript object if response is in JSON
   *     or undefined.
   */
  goog.net.CrossDomainRpc.prototype.getResponseJson = function() {
    return this.responseTextIsJson_ ?
        /** @type {?Object} */ (JSON.parse(this.responseText)) :
        undefined;
  };
  
  
  /**
   * @return {boolean} Whether the request completed with a success.
   */
  goog.net.CrossDomainRpc.prototype.isSuccess = function() {
    // Definition similar to goog.net.XhrIo.prototype.isSuccess.
    switch (this.status) {
      case goog.net.HttpStatus.OK:
      case goog.net.HttpStatus.NOT_MODIFIED:
        return true;
  
      default:
        return false;
    }
  };
  
  
  /**
   * Removes request iframe used.
   */
  goog.net.CrossDomainRpc.prototype.reset = function() {
    if (!goog.net.CrossDomainRpc.debugMode_) {
      goog.log.fine(
          goog.net.CrossDomainRpc.logger_,
          'request frame removed: ' + this.requestFrame_.id);
      goog.events.unlistenByKey(this.loadListenerKey_);
      this.requestFrame_.parentNode.removeChild(this.requestFrame_);
    }
    delete this.requestFrame_;
  };
  
  
  // -------------
  // response side
  
  
  /**
   * Name of response info iframe.
   * @type {string}
   * @private
   */
  goog.net.CrossDomainRpc.RESPONSE_INFO_MARKER_ =
      goog.net.CrossDomainRpc.RESPONSE_MARKER_ + '-info';
  
  
  /**
   * Maximal chunk size.  IE can only handle 4095 bytes on its URL.
   * 16MB has been tested on Firefox.  But 1MB is a practical size.
   * @type {number}
   * @private
   */
  goog.net.CrossDomainRpc.MAX_CHUNK_SIZE_ =
      goog.userAgent.EDGE_OR_IE ? 4095 : 1024 * 1024;
  
  
  /**
   * Query parameter 'chunk'.
   * @type {string}
   * @private
   */
  goog.net.CrossDomainRpc.PARAM_CHUNK_ = 'chunk';
  
  
  /**
   * Prefix before data chunk for passing other parameters.
   * type String
   * @private
   */
  goog.net.CrossDomainRpc.CHUNK_PREFIX_ =
      goog.net.CrossDomainRpc.RESPONSE_MARKER_ + '=1&' +
      goog.net.CrossDomainRpc.PARAM_CHUNK_ + '=';
  
  
  /**
   * Makes response available for grandparent (requester)'s receiveResponse
   * call to pick up by creating a series of iframes pointed to the dummy URI
   * with a payload (value after either ? or #) carrying a chunk of response
   * data and a response info iframe that tells the grandparent (requester) the
   * readiness of response.
   * @param {string} data Response data (string or JSON string).
   * @param {boolean} isDataJson true if data is a JSON string; false if just a
   *     string.
   * @param {Object} echo Parameters to echo back
   *     "xdpe:request-id": Server that produces the response needs to
   *     copy it here to support multiple current XD requests on the same page.
   *     "xdpe:dummy-uri": URI to a dummy resource that response
   *     iframes point to to gain the domain of the client.  This can be an
   *     image (IE) or a CSS file (FF) found on the requester's page.
   *     Server should copy value from request parameter "xdpe:dummy-uri".
   * @param {number} status HTTP response status code.
   * @param {string} headers Response headers in JSON format.
   */
  goog.net.CrossDomainRpc.sendResponse = function(
      data, isDataJson, echo, status, headers) {
    var dummyUri = echo[goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI];
  
    // since the dummy-uri can be specified by the user, verify that it doesn't
    // use any other protocols. (Specifically we don't want users to use a
    // dummy-uri beginning with "javascript:").
    if (!goog.string.caseInsensitiveStartsWith(dummyUri, 'http://') &&
        !goog.string.caseInsensitiveStartsWith(dummyUri, 'https://')) {
      dummyUri = 'http://' + dummyUri;
    }
  
    // usable chunk size is max less dummy URI less chunk prefix length
    // TODO(user): Figure out why we need to do "- 1" below
    var chunkSize = goog.net.CrossDomainRpc.MAX_CHUNK_SIZE_ - dummyUri.length -
        1 -  // payload delimiter ('#' or '?')
        goog.net.CrossDomainRpc.CHUNK_PREFIX_.length - 1;
  
    /*
     * Here we used to do URI encoding of data before we divide it into chunks
     * and decode on the receiving end.  We don't do this any more on IE for the
     * following reasons.
     *
     * 1) On IE, calling decodeURIComponent on a relatively large string is
     *   extremely slow (~22s for 160KB).  So even a moderate amount of data
     *   makes this library pretty much useless.  Fortunately, we can actually
     *   put unencoded data on IE's URL and get it back reliably.  So we are
     *   completely skipping encoding and decoding on IE.  When we call
     *   getFrameHash_ to get it back, the value is still intact(*) and unencoded.
     * 2) On Firefox, we have to call decodeURIComponent because location.hash
     *   does decoding by itself.  Fortunately, decodeURIComponent is not slow
     *   on Firefox.
     * 3) Safari automatically encodes everything you put on URL and it does not
     *   automatically decode when you access it via location.hash or
     *   location.href.  So we encode it here and decode it in detectResponse_().
     *
     * Note(user): IE actually does encode only space to %20 and decodes that
     *   automatically when you do location.href or location.hash.
     */
    if (!goog.userAgent.EDGE_OR_IE) {
      data = encodeURIComponent(data);
    }
  
    var numChunksToSend = Math.ceil(data.length / chunkSize);
    if (numChunksToSend == 0) {
      goog.net.CrossDomainRpc.createResponseInfo_(
          dummyUri, numChunksToSend, isDataJson, status, headers);
    } else {
      var numChunksSent = 0;
      var checkToCreateResponseInfo_ = function() {
        if (++numChunksSent == numChunksToSend) {
          goog.net.CrossDomainRpc.createResponseInfo_(
              dummyUri, numChunksToSend, isDataJson, status, headers);
        }
      };
  
      for (var i = 0; i < numChunksToSend; i++) {
        var chunkStart = i * chunkSize;
        var chunkEnd = chunkStart + chunkSize;
        var chunk = chunkEnd > data.length ? data.substring(chunkStart) :
                                             data.substring(chunkStart, chunkEnd);
  
        var responseFrame = goog.dom.createElement(goog.dom.TagName.IFRAME);
        responseFrame.src = dummyUri +
            goog.net.CrossDomainRpc.getPayloadDelimiter_(dummyUri) +
            goog.net.CrossDomainRpc.CHUNK_PREFIX_ + chunk;
        document.body.appendChild(responseFrame);
  
        // We used to call the function below when handling load event of
        // responseFrame.  But that event does not fire on IE when current
        // page is used as the dummy resource (because its loading is stopped?).
        // It also does not fire sometimes on Firefox.  So now we call it
        // directly.
        checkToCreateResponseInfo_();
      }
    }
  };
  
  
  /**
   * Creates a response info iframe to indicate completion of sendResponse
   * @param {string} dummyUri URI to a dummy resource.
   * @param {number} numChunks Total number of chunks.
   * @param {boolean} isDataJson Whether response is a JSON string or just string.
   * @param {number} status HTTP response status code.
   * @param {string} headers Response headers in JSON format.
   * @private
   */
  goog.net.CrossDomainRpc.createResponseInfo_ = function(
      dummyUri, numChunks, isDataJson, status, headers) {
    var responseInfoFrame = goog.dom.createElement(goog.dom.TagName.IFRAME);
    document.body.appendChild(responseInfoFrame);
    responseInfoFrame.src = dummyUri +
        goog.net.CrossDomainRpc.getPayloadDelimiter_(dummyUri) +
        goog.net.CrossDomainRpc.RESPONSE_INFO_MARKER_ + '=1&n=' + numChunks +
        '&isDataJson=' + isDataJson + '&status=' + status + '&headers=' +
        encodeURIComponent(headers);
  };
  
  
  /**
   * Returns payload delimiter, either "#" when caller's page is not used as
   * the dummy resource or "?" when it is, in which case caching issues prevent
   * response frames to gain the caller's domain.
   * @param {string} dummyUri URI to resource being used as dummy resource.
   * @return {string} Either "?" when caller's page is used as dummy resource or
   *     "#" if it is not.
   * @private
   */
  goog.net.CrossDomainRpc.getPayloadDelimiter_ = function(dummyUri) {
    return goog.net.CrossDomainRpc.REFERRER_ == dummyUri ? '?' : '#';
  };
  
  
  /**
   * Removes all parameters (after ? or #) from URI.
   * @param {string} uri URI to remove parameters from.
   * @return {string} URI with all parameters removed.
   * @private
   */
  goog.net.CrossDomainRpc.removeUriParams_ = function(uri) {
    // remove everything after question mark
    var question = uri.indexOf('?');
    if (question > 0) {
      uri = uri.substring(0, question);
    }
  
    // remove everything after hash mark
    var hash = uri.indexOf('#');
    if (hash > 0) {
      uri = uri.substring(0, hash);
    }
  
    return uri;
  };
  
  
  /**
   * Gets a response header.
   * @param {string} name Name of response header.
   * @return {string|undefined} Value of response header; undefined if not found.
   */
  goog.net.CrossDomainRpc.prototype.getResponseHeader = function(name) {
    return goog.isObject(this.responseHeaders) ? this.responseHeaders[name] :
                                                 undefined;
  };
  
  
  /**
   * Referrer of current document with all parameters after "?" and "#" stripped.
   * @type {string}
   * @private
   */
  goog.net.CrossDomainRpc.REFERRER_ =
      goog.net.CrossDomainRpc.removeUriParams_(document.referrer);
  