blob: 7ab768e4f32ea08938e306f041bc5493e5f86eb7 [file] [log] [blame]
// 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.safe');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.html.legacyconversions');
goog.require('goog.json');
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.
*
* This class makes use of goog.html.legacyconversions and provides no
* HTML-type-safe alternative. As such, it is not compatible with
* code that sets goog.html.legacyconversions.ALLOW_LEGACY_CONVERSIONS to
* false.
*
* @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.IE) {
document.execCommand('Stop');
} else if (goog.userAgent.GECKO) {
window.stop();
} else {
throw 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 {string} HTML of input element with that name and value.
* @private
*/
goog.net.CrossDomainRpc.createInputHtml_ = function(name, value) {
return '<textarea name="' + name + '">' +
goog.net.CrossDomainRpc.escapeAmpersand_(value) + '</textarea>';
};
/**
* Escapes ampersand so that XML/HTML entities are submitted as is because
* browser unescapes them when they are put into a text area.
* @param {*} value Value to escape.
* @return {*} Value with ampersand escaped, if value is a string;
* otherwise the value itself is returned.
* @private
*/
goog.net.CrossDomainRpc.escapeAmpersand_ = function(value) {
return value && (goog.isString(value) || value.constructor == String) ?
value.replace(/&/g, '&amp;') : 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 = document.getElementsByTagName('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 = document.getElementsByTagName('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 Error(
'No suitable dummy resource specified or detected for this page');
}
if (goog.userAgent.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. 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_ = /** @type {!HTMLIFrameElement} */ (
document.createElement('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 requestFrameContent = '<body><form method="' +
(opt_method == 'GET' ? 'GET' : 'POST') + '" action="' +
uri + '">' + inputs.join('') + '</form></body>';
var requestFrameContentHtml = goog.html.legacyconversions.safeHtmlFromString(
requestFrameContent);
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.IE) {
responseData = decodeURIComponent(responseData);
}
this.status = Number(params.get('status'));
this.responseText = responseData;
this.responseTextIsJson_ = params.get('isDataJson') == 'true';
this.responseHeaders = goog.json.unsafeParse(
/** @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) {
/** @preserveTry */
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
*/
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_ ?
goog.json.unsafeParse(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.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(*): IE actually does encode only space to %20 and decodes that
* automatically when you do location.href or location.hash.
*/
if (!goog.userAgent.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 = document.createElement('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 = document.createElement('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);