Adding float-no-zero branch hosted build

This commit is contained in:
ahocevar
2014-03-07 10:55:12 +01:00
parent 84cad42f6d
commit bd9092199b
1664 changed files with 731463 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,618 @@
// 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 Definition of the BrowserTestChannel class. A
* BrowserTestChannel is used during the first part of channel negotiation
* with the server to create the channel. It helps us determine whether we're
* behind a buffering proxy. It also runs the logic to see if the channel
* has been blocked by a network administrator. This class is part of the
* BrowserChannel implementation and is not for use by normal application code.
*
*/
goog.provide('goog.net.BrowserTestChannel');
goog.require('goog.json.EvalJsonProcessor');
goog.require('goog.net.ChannelRequest');
goog.require('goog.net.ChannelRequest.Error');
goog.require('goog.net.tmpnetwork');
goog.require('goog.string.Parser');
goog.require('goog.userAgent');
/**
* Encapsulates the logic for a single BrowserTestChannel.
*
* @constructor
* @param {goog.net.BrowserChannel} channel The BrowserChannel that owns this
* test channel.
* @param {goog.net.ChannelDebug} channelDebug A ChannelDebug to use for
* logging.
*/
goog.net.BrowserTestChannel = function(channel, channelDebug) {
/**
* The BrowserChannel that owns this test channel
* @type {goog.net.BrowserChannel}
* @private
*/
this.channel_ = channel;
/**
* The channel debug to use for logging
* @type {goog.net.ChannelDebug}
* @private
*/
this.channelDebug_ = channelDebug;
/**
* Parser for a response payload. Defaults to use
* {@code goog.json.unsafeParse}. The parser should return an array.
* @type {goog.string.Parser}
* @private
*/
this.parser_ = new goog.json.EvalJsonProcessor(null, true);
};
/**
* Extra HTTP headers to add to all the requests sent to the server.
* @type {Object}
* @private
*/
goog.net.BrowserTestChannel.prototype.extraHeaders_ = null;
/**
* The test request.
* @type {goog.net.ChannelRequest}
* @private
*/
goog.net.BrowserTestChannel.prototype.request_ = null;
/**
* Whether we have received the first result as an intermediate result. This
* helps us determine whether we're behind a buffering proxy.
* @type {boolean}
* @private
*/
goog.net.BrowserTestChannel.prototype.receivedIntermediateResult_ = false;
/**
* The time when the test request was started. We use timing in IE as
* a heuristic for whether we're behind a buffering proxy.
* @type {?number}
* @private
*/
goog.net.BrowserTestChannel.prototype.startTime_ = null;
/**
* The time for of the first result part. We use timing in IE as a
* heuristic for whether we're behind a buffering proxy.
* @type {?number}
* @private
*/
goog.net.BrowserTestChannel.prototype.firstTime_ = null;
/**
* The time for of the last result part. We use timing in IE as a
* heuristic for whether we're behind a buffering proxy.
* @type {?number}
* @private
*/
goog.net.BrowserTestChannel.prototype.lastTime_ = null;
/**
* The relative path for test requests.
* @type {?string}
* @private
*/
goog.net.BrowserTestChannel.prototype.path_ = null;
/**
* The state of the state machine for this object.
*
* @type {?number}
* @private
*/
goog.net.BrowserTestChannel.prototype.state_ = null;
/**
* The last status code received.
* @type {number}
* @private
*/
goog.net.BrowserTestChannel.prototype.lastStatusCode_ = -1;
/**
* A subdomain prefix for using a subdomain in IE for the backchannel
* requests.
* @type {?string}
* @private
*/
goog.net.BrowserTestChannel.prototype.hostPrefix_ = null;
/**
* A subdomain prefix for testing whether the channel was disabled by
* a network administrator;
* @type {?string}
* @private
*/
goog.net.BrowserTestChannel.prototype.blockedPrefix_ = null;
/**
* Enum type for the browser test channel state machine
* @enum {number}
* @private
*/
goog.net.BrowserTestChannel.State_ = {
/**
* The state for the BrowserTestChannel state machine where we making the
* initial call to get the server configured parameters.
*/
INIT: 0,
/**
* The state for the BrowserTestChannel state machine where we're checking to
* see if the channel has been blocked.
*/
CHECKING_BLOCKED: 1,
/**
* The state for the BrowserTestChannel state machine where we're checking to
* se if we're behind a buffering proxy.
*/
CONNECTION_TESTING: 2
};
/**
* Time in MS for waiting for the request to see if the channel is blocked.
* If the response takes longer than this many ms, we assume the request has
* failed.
* @type {number}
* @private
*/
goog.net.BrowserTestChannel.BLOCKED_TIMEOUT_ = 5000;
/**
* Number of attempts to try to see if the check to see if we're blocked
* succeeds. Sometimes the request can fail because of flaky network conditions
* and checking multiple times reduces false positives.
* @type {number}
* @private
*/
goog.net.BrowserTestChannel.BLOCKED_RETRIES_ = 3;
/**
* Time in ms between retries of the blocked request
* @type {number}
* @private
*/
goog.net.BrowserTestChannel.BLOCKED_PAUSE_BETWEEN_RETRIES_ = 2000;
/**
* Time between chunks in the test connection that indicates that we
* are not behind a buffering proxy. This value should be less than or
* equals to the time between chunks sent from the server.
* @type {number}
* @private
*/
goog.net.BrowserTestChannel.MIN_TIME_EXPECTED_BETWEEN_DATA_ = 500;
/**
* Sets extra HTTP headers to add to all the requests sent to the server.
*
* @param {Object} extraHeaders The HTTP headers.
*/
goog.net.BrowserTestChannel.prototype.setExtraHeaders = function(extraHeaders) {
this.extraHeaders_ = extraHeaders;
};
/**
* Sets a new parser for the response payload. A custom parser may be set to
* avoid using eval(), for example.
* By default, the parser uses {@code goog.json.unsafeParse}.
* @param {!goog.string.Parser} parser Parser.
*/
goog.net.BrowserTestChannel.prototype.setParser = function(parser) {
this.parser_ = parser;
};
/**
* Starts the test channel. This initiates connections to the server.
*
* @param {string} path The relative uri for the test connection.
*/
goog.net.BrowserTestChannel.prototype.connect = function(path) {
this.path_ = path;
var sendDataUri = this.channel_.getForwardChannelUri(this.path_);
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.TEST_STAGE_ONE_START);
this.startTime_ = goog.now();
// If the channel already has the result of the first test, then skip it.
var firstTestResults = this.channel_.getFirstTestResults();
if (goog.isDefAndNotNull(firstTestResults)) {
this.hostPrefix_ = this.channel_.correctHostPrefix(firstTestResults[0]);
this.blockedPrefix_ = firstTestResults[1];
if (this.blockedPrefix_) {
this.state_ = goog.net.BrowserTestChannel.State_.CHECKING_BLOCKED;
this.checkBlocked_();
} else {
this.state_ = goog.net.BrowserTestChannel.State_.CONNECTION_TESTING;
this.connectStage2_();
}
return;
}
// the first request returns server specific parameters
sendDataUri.setParameterValues('MODE', 'init');
this.request_ = goog.net.BrowserChannel.createChannelRequest(
this, this.channelDebug_);
this.request_.setExtraHeaders(this.extraHeaders_);
this.request_.xmlHttpGet(sendDataUri, false /* decodeChunks */,
null /* hostPrefix */, true /* opt_noClose */);
this.state_ = goog.net.BrowserTestChannel.State_.INIT;
};
/**
* Checks to see whether the channel is blocked. This is for implementing the
* feature that allows network administrators to block Gmail Chat. The
* strategy to determine if we're blocked is to try to load an image off a
* special subdomain that network administrators will block access to if they
* are trying to block chat. For Gmail Chat, the subdomain is
* chatenabled.mail.google.com.
* @private
*/
goog.net.BrowserTestChannel.prototype.checkBlocked_ = function() {
var uri = this.channel_.createDataUri(this.blockedPrefix_,
'/mail/images/cleardot.gif');
uri.makeUnique();
goog.net.tmpnetwork.testLoadImageWithRetries(uri.toString(),
goog.net.BrowserTestChannel.BLOCKED_TIMEOUT_,
goog.bind(this.checkBlockedCallback_, this),
goog.net.BrowserTestChannel.BLOCKED_RETRIES_,
goog.net.BrowserTestChannel.BLOCKED_PAUSE_BETWEEN_RETRIES_);
this.notifyServerReachabilityEvent(
goog.net.BrowserChannel.ServerReachability.REQUEST_MADE);
};
/**
* Callback for testLoadImageWithRetries to check if browser channel is
* blocked.
* @param {boolean} succeeded Whether the request succeeded.
* @private
*/
goog.net.BrowserTestChannel.prototype.checkBlockedCallback_ = function(
succeeded) {
if (succeeded) {
this.state_ = goog.net.BrowserTestChannel.State_.CONNECTION_TESTING;
this.connectStage2_();
} else {
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.CHANNEL_BLOCKED);
this.channel_.testConnectionBlocked(this);
}
// We don't dispatch a REQUEST_FAILED server reachability event when the
// block request fails, as such a failure is not a good signal that the
// server has actually become unreachable.
if (succeeded) {
this.notifyServerReachabilityEvent(
goog.net.BrowserChannel.ServerReachability.REQUEST_SUCCEEDED);
}
};
/**
* Begins the second stage of the test channel where we test to see if we're
* behind a buffering proxy. The server sends back a multi-chunked response
* with the first chunk containing the content '1' and then two seconds later
* sending the second chunk containing the content '2'. Depending on how we
* receive the content, we can tell if we're behind a buffering proxy.
* @private
*/
goog.net.BrowserTestChannel.prototype.connectStage2_ = function() {
this.channelDebug_.debug('TestConnection: starting stage 2');
// If the second test results are available, skip its execution.
var secondTestResults = this.channel_.getSecondTestResults();
if (goog.isDefAndNotNull(secondTestResults)) {
this.channelDebug_.debug(
'TestConnection: skipping stage 2, precomputed result is '
+ secondTestResults ? 'Buffered' : 'Unbuffered');
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_START);
if (secondTestResults) { // Buffered/Proxy connection
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.PROXY);
this.channel_.testConnectionFinished(this, false);
} else { // Unbuffered/NoProxy connection
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
}
return; // Skip the test
}
this.request_ = goog.net.BrowserChannel.createChannelRequest(
this, this.channelDebug_);
this.request_.setExtraHeaders(this.extraHeaders_);
var recvDataUri = this.channel_.getBackChannelUri(this.hostPrefix_,
/** @type {string} */ (this.path_));
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_START);
if (!goog.net.ChannelRequest.supportsXhrStreaming()) {
recvDataUri.setParameterValues('TYPE', 'html');
this.request_.tridentGet(recvDataUri, Boolean(this.hostPrefix_));
} else {
recvDataUri.setParameterValues('TYPE', 'xmlhttp');
this.request_.xmlHttpGet(recvDataUri, false /** decodeChunks */,
this.hostPrefix_, false /** opt_noClose */);
}
};
/**
* Factory method for XhrIo objects.
* @param {?string} hostPrefix The host prefix, if we need an XhrIo object
* capable of calling a secondary domain.
* @return {!goog.net.XhrIo} New XhrIo object.
*/
goog.net.BrowserTestChannel.prototype.createXhrIo = function(hostPrefix) {
return this.channel_.createXhrIo(hostPrefix);
};
/**
* Aborts the test channel.
*/
goog.net.BrowserTestChannel.prototype.abort = function() {
if (this.request_) {
this.request_.cancel();
this.request_ = null;
}
this.lastStatusCode_ = -1;
};
/**
* Returns whether the test channel is closed. The ChannelRequest object expects
* this method to be implemented on its handler.
*
* @return {boolean} Whether the channel is closed.
*/
goog.net.BrowserTestChannel.prototype.isClosed = function() {
return false;
};
/**
* Callback from ChannelRequest for when new data is received
*
* @param {goog.net.ChannelRequest} req The request object.
* @param {string} responseText The text of the response.
*/
goog.net.BrowserTestChannel.prototype.onRequestData =
function(req, responseText) {
this.lastStatusCode_ = req.getLastStatusCode();
if (this.state_ == goog.net.BrowserTestChannel.State_.INIT) {
this.channelDebug_.debug('TestConnection: Got data for stage 1');
if (!responseText) {
this.channelDebug_.debug('TestConnection: Null responseText');
// The server should always send text; something is wrong here
this.channel_.testConnectionFailure(this,
goog.net.ChannelRequest.Error.BAD_DATA);
return;
}
/** @preserveTry */
try {
var respArray = this.parser_.parse(responseText);
} catch (e) {
this.channelDebug_.dumpException(e);
this.channel_.testConnectionFailure(this,
goog.net.ChannelRequest.Error.BAD_DATA);
return;
}
this.hostPrefix_ = this.channel_.correctHostPrefix(respArray[0]);
this.blockedPrefix_ = respArray[1];
} else if (this.state_ ==
goog.net.BrowserTestChannel.State_.CONNECTION_TESTING) {
if (this.receivedIntermediateResult_) {
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_DATA_TWO);
this.lastTime_ = goog.now();
} else {
// '11111' is used instead of '1' to prevent a small amount of buffering
// by Safari.
if (responseText == '11111') {
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_DATA_ONE);
this.receivedIntermediateResult_ = true;
this.firstTime_ = goog.now();
if (this.checkForEarlyNonBuffered_()) {
// If early chunk detection is on, and we passed the tests,
// assume HTTP_OK, cancel the test and turn on noproxy mode.
this.lastStatusCode_ = 200;
this.request_.cancel();
this.channelDebug_.debug(
'Test connection succeeded; using streaming connection');
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
}
} else {
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_DATA_BOTH);
this.firstTime_ = this.lastTime_ = goog.now();
this.receivedIntermediateResult_ = false;
}
}
}
};
/**
* Callback from ChannelRequest that indicates a request has completed.
*
* @param {goog.net.ChannelRequest} req The request object.
*/
goog.net.BrowserTestChannel.prototype.onRequestComplete =
function(req) {
this.lastStatusCode_ = this.request_.getLastStatusCode();
if (!this.request_.getSuccess()) {
this.channelDebug_.debug(
'TestConnection: request failed, in state ' + this.state_);
if (this.state_ == goog.net.BrowserTestChannel.State_.INIT) {
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.TEST_STAGE_ONE_FAILED);
} else if (this.state_ ==
goog.net.BrowserTestChannel.State_.CONNECTION_TESTING) {
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.TEST_STAGE_TWO_FAILED);
}
this.channel_.testConnectionFailure(this,
/** @type {goog.net.ChannelRequest.Error} */
(this.request_.getLastError()));
return;
}
if (this.state_ == goog.net.BrowserTestChannel.State_.INIT) {
this.channelDebug_.debug(
'TestConnection: request complete for initial check');
if (this.blockedPrefix_) {
this.state_ = goog.net.BrowserTestChannel.State_.CHECKING_BLOCKED;
this.checkBlocked_();
} else {
this.state_ = goog.net.BrowserTestChannel.State_.CONNECTION_TESTING;
this.connectStage2_();
}
} else if (this.state_ ==
goog.net.BrowserTestChannel.State_.CONNECTION_TESTING) {
this.channelDebug_.debug('TestConnection: request complete for stage 2');
var goodConn = false;
if (!goog.net.ChannelRequest.supportsXhrStreaming()) {
// we always get Trident responses in separate calls to
// onRequestData, so we have to check the time they came
var ms = this.lastTime_ - this.firstTime_;
if (ms < 200) {
// TODO: need to empirically verify that this number is OK
// for slow computers
goodConn = false;
} else {
goodConn = true;
}
} else {
goodConn = this.receivedIntermediateResult_;
}
if (goodConn) {
this.channelDebug_.debug(
'Test connection succeeded; using streaming connection');
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.NOPROXY);
this.channel_.testConnectionFinished(this, true);
} else {
this.channelDebug_.debug(
'Test connection failed; not using streaming');
goog.net.BrowserChannel.notifyStatEvent(
goog.net.BrowserChannel.Stat.PROXY);
this.channel_.testConnectionFinished(this, false);
}
}
};
/**
* Returns the last status code received for a request.
* @return {number} The last status code received for a request.
*/
goog.net.BrowserTestChannel.prototype.getLastStatusCode = function() {
return this.lastStatusCode_;
};
/**
* @return {boolean} Whether we should be using secondary domains when the
* server instructs us to do so.
*/
goog.net.BrowserTestChannel.prototype.shouldUseSecondaryDomains = function() {
return this.channel_.shouldUseSecondaryDomains();
};
/**
* Gets whether this channel is currently active. This is used to determine the
* length of time to wait before retrying.
*
* @param {goog.net.BrowserChannel} browserChannel The browser channel.
* @return {boolean} Whether the channel is currently active.
*/
goog.net.BrowserTestChannel.prototype.isActive =
function(browserChannel) {
return this.channel_.isActive();
};
/**
* @return {boolean} True if test stage 2 detected a non-buffered
* channel early and early no buffering detection is enabled.
* @private
*/
goog.net.BrowserTestChannel.prototype.checkForEarlyNonBuffered_ =
function() {
var ms = this.firstTime_ - this.startTime_;
// we always get Trident responses in separate calls to
// onRequestData, so we have to check the time that the first came in
// and verify that the data arrived before the second portion could
// have been sent. For all other browser's we skip the timing test.
return goog.net.ChannelRequest.supportsXhrStreaming() ||
ms < goog.net.BrowserTestChannel.MIN_TIME_EXPECTED_BETWEEN_DATA_;
};
/**
* Notifies the channel of a fine grained network event.
* @param {goog.net.BrowserChannel.ServerReachability} reachabilityType The
* reachability event type.
*/
goog.net.BrowserTestChannel.prototype.notifyServerReachabilityEvent =
function(reachabilityType) {
this.channel_.notifyServerReachabilityEvent(reachabilityType);
};
@@ -0,0 +1,181 @@
// Copyright 2008 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 Loads a list of URIs in bulk. All requests must be a success
* in order for the load to be considered a success.
*
*/
goog.provide('goog.net.BulkLoader');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.log');
goog.require('goog.net.BulkLoaderHelper');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo');
/**
* Class used to load multiple URIs.
* @param {Array.<string|goog.Uri>} uris The URIs to load.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.net.BulkLoader = function(uris) {
goog.events.EventTarget.call(this);
/**
* The bulk loader helper.
* @type {goog.net.BulkLoaderHelper}
* @private
*/
this.helper_ = new goog.net.BulkLoaderHelper(uris);
/**
* The handler for managing events.
* @type {goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
};
goog.inherits(goog.net.BulkLoader, goog.events.EventTarget);
/**
* A logger.
* @type {goog.log.Logger}
* @private
*/
goog.net.BulkLoader.prototype.logger_ =
goog.log.getLogger('goog.net.BulkLoader');
/**
* Gets the response texts, in order.
* @return {Array.<string>} The response texts.
*/
goog.net.BulkLoader.prototype.getResponseTexts = function() {
return this.helper_.getResponseTexts();
};
/**
* Gets the request Uris.
* @return {Array.<string>} The request URIs, in order.
*/
goog.net.BulkLoader.prototype.getRequestUris = function() {
return this.helper_.getUris();
};
/**
* Starts the process of loading the URIs.
*/
goog.net.BulkLoader.prototype.load = function() {
var eventHandler = this.eventHandler_;
var uris = this.helper_.getUris();
goog.log.info(this.logger_,
'Starting load of code with ' + uris.length + ' uris.');
for (var i = 0; i < uris.length; i++) {
var xhrIo = new goog.net.XhrIo();
eventHandler.listen(xhrIo,
goog.net.EventType.COMPLETE,
goog.bind(this.handleEvent_, this, i));
xhrIo.send(uris[i]);
}
};
/**
* Handles all events fired by the XhrManager.
* @param {number} id The id of the request.
* @param {goog.events.Event} e The event.
* @private
*/
goog.net.BulkLoader.prototype.handleEvent_ = function(id, e) {
goog.log.info(this.logger_, 'Received event "' + e.type + '" for id ' + id +
' with uri ' + this.helper_.getUri(id));
var xhrIo = /** @type {goog.net.XhrIo} */ (e.target);
if (xhrIo.isSuccess()) {
this.handleSuccess_(id, xhrIo);
} else {
this.handleError_(id, xhrIo);
}
};
/**
* Handles when a request is successful (i.e., completed and response received).
* Stores thhe responseText and checks if loading is complete.
* @param {number} id The id of the request.
* @param {goog.net.XhrIo} xhrIo The XhrIo objects that was used.
* @private
*/
goog.net.BulkLoader.prototype.handleSuccess_ = function(
id, xhrIo) {
// Save the response text.
this.helper_.setResponseText(id, xhrIo.getResponseText());
// Check if all response texts have been received.
if (this.helper_.isLoadComplete()) {
this.finishLoad_();
}
xhrIo.dispose();
};
/**
* Handles when a request has ended in error (i.e., all retries completed and
* none were successful). Cancels loading of the URI's.
* @param {number|string} id The id of the request.
* @param {goog.net.XhrIo} xhrIo The XhrIo objects that was used.
* @private
*/
goog.net.BulkLoader.prototype.handleError_ = function(
id, xhrIo) {
// TODO(user): Abort all pending requests.
// Dispatch the ERROR event.
this.dispatchEvent(goog.net.EventType.ERROR);
xhrIo.dispose();
};
/**
* Finishes the load of the URI's. Dispatches the SUCCESS event.
* @private
*/
goog.net.BulkLoader.prototype.finishLoad_ = function() {
goog.log.info(this.logger_, 'All uris loaded.');
// Dispatch the SUCCESS event.
this.dispatchEvent(goog.net.EventType.SUCCESS);
};
/** @override */
goog.net.BulkLoader.prototype.disposeInternal = function() {
goog.net.BulkLoader.superClass_.disposeInternal.call(this);
this.eventHandler_.dispose();
this.eventHandler_ = null;
this.helper_.dispose();
this.helper_ = null;
};
@@ -0,0 +1,127 @@
// Copyright 2008 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 Helper class to load a list of URIs in bulk. All URIs
* must be a successfully loaded in order for the entire load to be considered
* a success.
*
*/
goog.provide('goog.net.BulkLoaderHelper');
goog.require('goog.Disposable');
goog.require('goog.log');
/**
* Helper class used to load multiple URIs.
* @param {Array.<string|goog.Uri>} uris The URIs to load.
* @constructor
* @extends {goog.Disposable}
*/
goog.net.BulkLoaderHelper = function(uris) {
goog.Disposable.call(this);
/**
* The URIs to load.
* @type {Array.<string|goog.Uri>}
* @private
*/
this.uris_ = uris;
/**
* The response from the XHR's.
* @type {Array.<string>}
* @private
*/
this.responseTexts_ = [];
};
goog.inherits(goog.net.BulkLoaderHelper, goog.Disposable);
/**
* A logger.
* @type {goog.log.Logger}
* @private
*/
goog.net.BulkLoaderHelper.prototype.logger_ =
goog.log.getLogger('goog.net.BulkLoaderHelper');
/**
* Gets the URI by id.
* @param {number} id The id.
* @return {string|goog.Uri} The URI specified by the id.
*/
goog.net.BulkLoaderHelper.prototype.getUri = function(id) {
return this.uris_[id];
};
/**
* Gets the URIs.
* @return {Array.<string|goog.Uri>} The URIs.
*/
goog.net.BulkLoaderHelper.prototype.getUris = function() {
return this.uris_;
};
/**
* Gets the response texts.
* @return {Array.<string>} The response texts.
*/
goog.net.BulkLoaderHelper.prototype.getResponseTexts = function() {
return this.responseTexts_;
};
/**
* Sets the response text by id.
* @param {number} id The id.
* @param {string} responseText The response texts.
*/
goog.net.BulkLoaderHelper.prototype.setResponseText = function(
id, responseText) {
this.responseTexts_[id] = responseText;
};
/**
* Determines if the load of the URIs is complete.
* @return {boolean} TRUE iff the load is complete.
*/
goog.net.BulkLoaderHelper.prototype.isLoadComplete = function() {
var responseTexts = this.responseTexts_;
if (responseTexts.length == this.uris_.length) {
for (var i = 0; i < responseTexts.length; i++) {
if (!goog.isDefAndNotNull(responseTexts[i])) {
return false;
}
}
return true;
}
return false;
};
/** @override */
goog.net.BulkLoaderHelper.prototype.disposeInternal = function() {
goog.net.BulkLoaderHelper.superClass_.disposeInternal.call(this);
this.uris_ = null;
this.responseTexts_ = null;
};
@@ -0,0 +1,300 @@
// 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 Definition of the ChannelDebug class. ChannelDebug provides
* a utility for tracing and debugging the BrowserChannel requests.
*
*/
/**
* Namespace for BrowserChannel
*/
goog.provide('goog.net.ChannelDebug');
goog.require('goog.json');
goog.require('goog.log');
/**
* Logs and keeps a buffer of debugging info for the Channel.
*
* @constructor
*/
goog.net.ChannelDebug = function() {
/**
* The logger instance.
* @const
* @private
*/
this.logger_ = goog.log.getLogger('goog.net.BrowserChannel');
};
/**
* Gets the logger used by this ChannelDebug.
* @return {goog.debug.Logger} The logger used by this ChannelDebug.
*/
goog.net.ChannelDebug.prototype.getLogger = function() {
return this.logger_;
};
/**
* Logs that the browser went offline during the lifetime of a request.
* @param {goog.Uri} url The URL being requested.
*/
goog.net.ChannelDebug.prototype.browserOfflineResponse = function(url) {
this.info('BROWSER_OFFLINE: ' + url);
};
/**
* Logs an XmlHttp request..
* @param {string} verb The request type (GET/POST).
* @param {goog.Uri} uri The request destination.
* @param {string|number|undefined} id The request id.
* @param {number} attempt Which attempt # the request was.
* @param {?string} postData The data posted in the request.
*/
goog.net.ChannelDebug.prototype.xmlHttpChannelRequest =
function(verb, uri, id, attempt, postData) {
this.info(
'XMLHTTP REQ (' + id + ') [attempt ' + attempt + ']: ' +
verb + '\n' + uri + '\n' +
this.maybeRedactPostData_(postData));
};
/**
* Logs the meta data received from an XmlHttp request.
* @param {string} verb The request type (GET/POST).
* @param {goog.Uri} uri The request destination.
* @param {string|number|undefined} id The request id.
* @param {number} attempt Which attempt # the request was.
* @param {goog.net.XmlHttp.ReadyState} readyState The ready state.
* @param {number} statusCode The HTTP status code.
*/
goog.net.ChannelDebug.prototype.xmlHttpChannelResponseMetaData =
function(verb, uri, id, attempt, readyState, statusCode) {
this.info(
'XMLHTTP RESP (' + id + ') [ attempt ' + attempt + ']: ' +
verb + '\n' + uri + '\n' + readyState + ' ' + statusCode);
};
/**
* Logs the response data received from an XmlHttp request.
* @param {string|number|undefined} id The request id.
* @param {?string} responseText The response text.
* @param {?string=} opt_desc Optional request description.
*/
goog.net.ChannelDebug.prototype.xmlHttpChannelResponseText =
function(id, responseText, opt_desc) {
this.info(
'XMLHTTP TEXT (' + id + '): ' +
this.redactResponse_(responseText) +
(opt_desc ? ' ' + opt_desc : ''));
};
/**
* Logs a Trident ActiveX request.
* @param {string} verb The request type (GET/POST).
* @param {goog.Uri} uri The request destination.
* @param {string|number|undefined} id The request id.
* @param {number} attempt Which attempt # the request was.
*/
goog.net.ChannelDebug.prototype.tridentChannelRequest =
function(verb, uri, id, attempt) {
this.info(
'TRIDENT REQ (' + id + ') [ attempt ' + attempt + ']: ' +
verb + '\n' + uri);
};
/**
* Logs the response text received from a Trident ActiveX request.
* @param {string|number|undefined} id The request id.
* @param {string} responseText The response text.
*/
goog.net.ChannelDebug.prototype.tridentChannelResponseText =
function(id, responseText) {
this.info(
'TRIDENT TEXT (' + id + '): ' +
this.redactResponse_(responseText));
};
/**
* Logs the done response received from a Trident ActiveX request.
* @param {string|number|undefined} id The request id.
* @param {boolean} successful Whether the request was successful.
*/
goog.net.ChannelDebug.prototype.tridentChannelResponseDone =
function(id, successful) {
this.info(
'TRIDENT TEXT (' + id + '): ' + successful ? 'success' : 'failure');
};
/**
* Logs a request timeout.
* @param {goog.Uri} uri The uri that timed out.
*/
goog.net.ChannelDebug.prototype.timeoutResponse = function(uri) {
this.info('TIMEOUT: ' + uri);
};
/**
* Logs a debug message.
* @param {string} text The message.
*/
goog.net.ChannelDebug.prototype.debug = function(text) {
this.info(text);
};
/**
* Logs an exception
* @param {Error} e The error or error event.
* @param {string=} opt_msg The optional message, defaults to 'Exception'.
*/
goog.net.ChannelDebug.prototype.dumpException = function(e, opt_msg) {
this.severe((opt_msg || 'Exception') + e);
};
/**
* Logs an info message.
* @param {string} text The message.
*/
goog.net.ChannelDebug.prototype.info = function(text) {
goog.log.info(this.logger_, text);
};
/**
* Logs a warning message.
* @param {string} text The message.
*/
goog.net.ChannelDebug.prototype.warning = function(text) {
goog.log.warning(this.logger_, text);
};
/**
* Logs a severe message.
* @param {string} text The message.
*/
goog.net.ChannelDebug.prototype.severe = function(text) {
goog.log.error(this.logger_, text);
};
/**
* Removes potentially private data from a response so that we don't
* accidentally save private and personal data to the server logs.
* @param {?string} responseText A JSON response to clean.
* @return {?string} The cleaned response.
* @private
*/
goog.net.ChannelDebug.prototype.redactResponse_ = function(responseText) {
// first check if it's not JS - the only non-JS should be the magic cookie
if (!responseText ||
/** @suppress {missingRequire}. The require creates a circular
* dependency.
*/
responseText == goog.net.BrowserChannel.MAGIC_RESPONSE_COOKIE) {
return responseText;
}
/** @preserveTry */
try {
var responseArray = goog.json.unsafeParse(responseText);
if (responseArray) {
for (var i = 0; i < responseArray.length; i++) {
if (goog.isArray(responseArray[i])) {
this.maybeRedactArray_(responseArray[i]);
}
}
}
return goog.json.serialize(responseArray);
} catch (e) {
this.debug('Exception parsing expected JS array - probably was not JS');
return responseText;
}
};
/**
* Removes data from a response array that may be sensitive.
* @param {Array} array The array to clean.
* @private
*/
goog.net.ChannelDebug.prototype.maybeRedactArray_ = function(array) {
if (array.length < 2) {
return;
}
var dataPart = array[1];
if (!goog.isArray(dataPart)) {
return;
}
if (dataPart.length < 1) {
return;
}
var type = dataPart[0];
if (type != 'noop' && type != 'stop') {
// redact all fields in the array
for (var i = 1; i < dataPart.length; i++) {
dataPart[i] = '';
}
}
};
/**
* Removes potentially private data from a request POST body so that we don't
* accidentally save private and personal data to the server logs.
* @param {?string} data The data string to clean.
* @return {?string} The data string with sensitive data replaced by 'redacted'.
* @private
*/
goog.net.ChannelDebug.prototype.maybeRedactPostData_ = function(data) {
if (!data) {
return null;
}
var out = '';
var params = data.split('&');
for (var i = 0; i < params.length; i++) {
var param = params[i];
var keyValue = param.split('=');
if (keyValue.length > 1) {
var key = keyValue[0];
var value = keyValue[1];
var keyParts = key.split('_');
if (keyParts.length >= 2 && keyParts[1] == 'type') {
out += key + '=' + value + '&';
} else {
out += key + '=' + 'redacted' + '&';
}
}
}
return out;
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,370 @@
// 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 Functions for setting, getting and deleting cookies.
*
* @author arv@google.com (Erik Arvidsson)
*/
goog.provide('goog.net.Cookies');
goog.provide('goog.net.cookies');
/**
* A class for handling browser cookies.
* @param {Document} context The context document to get/set cookies on.
* @constructor
*/
goog.net.Cookies = function(context) {
/**
* The context document to get/set cookies on
* @type {Document}
* @private
*/
this.document_ = context;
};
/**
* Static constant for the size of cookies. Per the spec, there's a 4K limit
* to the size of a cookie. To make sure users can't break this limit, we
* should truncate long cookies at 3950 bytes, to be extra careful with dumb
* browsers/proxies that interpret 4K as 4000 rather than 4096.
* @type {number}
*/
goog.net.Cookies.MAX_COOKIE_LENGTH = 3950;
/**
* RegExp used to split the cookies string.
* @type {RegExp}
* @private
*/
goog.net.Cookies.SPLIT_RE_ = /\s*;\s*/;
/**
* Returns true if cookies are enabled.
* @return {boolean} True if cookies are enabled.
*/
goog.net.Cookies.prototype.isEnabled = function() {
return navigator.cookieEnabled;
};
/**
* We do not allow '=', ';', or white space in the name.
*
* NOTE: The following are allowed by this method, but should be avoided for
* cookies handled by the server.
* - any name starting with '$'
* - 'Comment'
* - 'Domain'
* - 'Expires'
* - 'Max-Age'
* - 'Path'
* - 'Secure'
* - 'Version'
*
* @param {string} name Cookie name.
* @return {boolean} Whether name is valid.
*
* @see <a href="http://tools.ietf.org/html/rfc2109">RFC 2109</a>
* @see <a href="http://tools.ietf.org/html/rfc2965">RFC 2965</a>
*/
goog.net.Cookies.prototype.isValidName = function(name) {
return !(/[;=\s]/.test(name));
};
/**
* We do not allow ';' or line break in the value.
*
* Spec does not mention any illegal characters, but in practice semi-colons
* break parsing and line breaks truncate the name.
*
* @param {string} value Cookie value.
* @return {boolean} Whether value is valid.
*
* @see <a href="http://tools.ietf.org/html/rfc2109">RFC 2109</a>
* @see <a href="http://tools.ietf.org/html/rfc2965">RFC 2965</a>
*/
goog.net.Cookies.prototype.isValidValue = function(value) {
return !(/[;\r\n]/.test(value));
};
/**
* Sets a cookie. The max_age can be -1 to set a session cookie. To remove and
* expire cookies, use remove() instead.
*
* Neither the {@code name} nor the {@code value} are encoded in any way. It is
* up to the callers of {@code get} and {@code set} (as well as all the other
* methods) to handle any possible encoding and decoding.
*
* @throws {!Error} If the {@code name} fails #goog.net.cookies.isValidName.
* @throws {!Error} If the {@code value} fails #goog.net.cookies.isValidValue.
*
* @param {string} name The cookie name.
* @param {string} value The cookie value.
* @param {number=} opt_maxAge The max age in seconds (from now). Use -1 to
* set a session cookie. If not provided, the default is -1
* (i.e. set a session cookie).
* @param {?string=} opt_path The path of the cookie. If not present then this
* uses the full request path.
* @param {?string=} opt_domain The domain of the cookie, or null to not
* specify a domain attribute (browser will use the full request host name).
* If not provided, the default is null (i.e. let browser use full request
* host name).
* @param {boolean=} opt_secure Whether the cookie should only be sent over
* a secure channel.
*/
goog.net.Cookies.prototype.set = function(
name, value, opt_maxAge, opt_path, opt_domain, opt_secure) {
if (!this.isValidName(name)) {
throw Error('Invalid cookie name "' + name + '"');
}
if (!this.isValidValue(value)) {
throw Error('Invalid cookie value "' + value + '"');
}
if (!goog.isDef(opt_maxAge)) {
opt_maxAge = -1;
}
var domainStr = opt_domain ? ';domain=' + opt_domain : '';
var pathStr = opt_path ? ';path=' + opt_path : '';
var secureStr = opt_secure ? ';secure' : '';
var expiresStr;
// Case 1: Set a session cookie.
if (opt_maxAge < 0) {
expiresStr = '';
// Case 2: Expire the cookie.
// Note: We don't tell people about this option in the function doc because
// we prefer people to use ExpireCookie() to expire cookies.
} else if (opt_maxAge == 0) {
// Note: Don't use Jan 1, 1970 for date because NS 4.76 will try to convert
// it to local time, and if the local time is before Jan 1, 1970, then the
// browser will ignore the Expires attribute altogether.
var pastDate = new Date(1970, 1 /*Feb*/, 1); // Feb 1, 1970
expiresStr = ';expires=' + pastDate.toUTCString();
// Case 3: Set a persistent cookie.
} else {
var futureDate = new Date(goog.now() + opt_maxAge * 1000);
expiresStr = ';expires=' + futureDate.toUTCString();
}
this.setCookie_(name + '=' + value + domainStr + pathStr +
expiresStr + secureStr);
};
/**
* Returns the value for the first cookie with the given name.
* @param {string} name The name of the cookie to get.
* @param {string=} opt_default If not found this is returned instead.
* @return {string|undefined} The value of the cookie. If no cookie is set this
* returns opt_default or undefined if opt_default is not provided.
*/
goog.net.Cookies.prototype.get = function(name, opt_default) {
var nameEq = name + '=';
var parts = this.getParts_();
for (var i = 0, part; part = parts[i]; i++) {
// startsWith
if (part.lastIndexOf(nameEq, 0) == 0) {
return part.substr(nameEq.length);
}
if (part == name) {
return '';
}
}
return opt_default;
};
/**
* Removes and expires a cookie.
* @param {string} name The cookie name.
* @param {string=} opt_path The path of the cookie, or null to expire a cookie
* set at the full request path. If not provided, the default is '/'
* (i.e. path=/).
* @param {string=} opt_domain The domain of the cookie, or null to expire a
* cookie set at the full request host name. If not provided, the default is
* null (i.e. cookie at full request host name).
* @return {boolean} Whether the cookie existed before it was removed.
*/
goog.net.Cookies.prototype.remove = function(name, opt_path, opt_domain) {
var rv = this.containsKey(name);
this.set(name, '', 0, opt_path, opt_domain);
return rv;
};
/**
* Gets the names for all the cookies.
* @return {Array.<string>} An array with the names of the cookies.
*/
goog.net.Cookies.prototype.getKeys = function() {
return this.getKeyValues_().keys;
};
/**
* Gets the values for all the cookies.
* @return {Array.<string>} An array with the values of the cookies.
*/
goog.net.Cookies.prototype.getValues = function() {
return this.getKeyValues_().values;
};
/**
* @return {boolean} Whether there are any cookies for this document.
*/
goog.net.Cookies.prototype.isEmpty = function() {
return !this.getCookie_();
};
/**
* @return {number} The number of cookies for this document.
*/
goog.net.Cookies.prototype.getCount = function() {
var cookie = this.getCookie_();
if (!cookie) {
return 0;
}
return this.getParts_().length;
};
/**
* Returns whether there is a cookie with the given name.
* @param {string} key The name of the cookie to test for.
* @return {boolean} Whether there is a cookie by that name.
*/
goog.net.Cookies.prototype.containsKey = function(key) {
// substring will return empty string if the key is not found, so the get
// function will only return undefined
return goog.isDef(this.get(key));
};
/**
* Returns whether there is a cookie with the given value. (This is an O(n)
* operation.)
* @param {string} value The value to check for.
* @return {boolean} Whether there is a cookie with that value.
*/
goog.net.Cookies.prototype.containsValue = function(value) {
// this O(n) in any case so lets do the trivial thing.
var values = this.getKeyValues_().values;
for (var i = 0; i < values.length; i++) {
if (values[i] == value) {
return true;
}
}
return false;
};
/**
* Removes all cookies for this document. Note that this will only remove
* cookies from the current path and domain. If there are cookies set using a
* subpath and/or another domain these will still be there.
*/
goog.net.Cookies.prototype.clear = function() {
var keys = this.getKeyValues_().keys;
for (var i = keys.length - 1; i >= 0; i--) {
this.remove(keys[i]);
}
};
/**
* Private helper function to allow testing cookies without depending on the
* browser.
* @param {string} s The cookie string to set.
* @private
*/
goog.net.Cookies.prototype.setCookie_ = function(s) {
this.document_.cookie = s;
};
/**
* Private helper function to allow testing cookies without depending on the
* browser. IE6 can return null here.
* @return {?string} Returns the {@code document.cookie}.
* @private
*/
goog.net.Cookies.prototype.getCookie_ = function() {
return this.document_.cookie;
};
/**
* @return {!Array.<string>} The cookie split on semi colons.
* @private
*/
goog.net.Cookies.prototype.getParts_ = function() {
return (this.getCookie_() || '').
split(goog.net.Cookies.SPLIT_RE_);
};
/**
* Gets the names and values for all the cookies.
* @return {Object} An object with keys and values.
* @private
*/
goog.net.Cookies.prototype.getKeyValues_ = function() {
var parts = this.getParts_();
var keys = [], values = [], index, part;
for (var i = 0; part = parts[i]; i++) {
index = part.indexOf('=');
if (index == -1) { // empty name
keys.push('');
values.push(part);
} else {
keys.push(part.substring(0, index));
values.push(part.substring(index + 1));
}
}
return {keys: keys, values: values};
};
/**
* A static default instance.
* @type {goog.net.Cookies}
*/
goog.net.cookies = new goog.net.Cookies(document);
/**
* Define the constant on the instance in order not to break many references to
* it.
* @type {number}
* @deprecated Use goog.net.Cookies.MAX_COOKIE_LENGTH instead.
*/
goog.net.cookies.MAX_COOKIE_LENGTH = goog.net.Cookies.MAX_COOKIE_LENGTH;
@@ -0,0 +1,849 @@
// 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.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
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
* @extends {goog.events.EventTarget}
* @constructor
*/
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;
/**
* 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_ = 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 requestFrameDoc = goog.dom.getFrameContentDocument(requestFrame);
requestFrameDoc.open();
requestFrameDoc.write(requestFrameContent);
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);
@@ -0,0 +1,130 @@
// Copyright 2007 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 Error codes shared between goog.net.IframeIo and
* goog.net.XhrIo.
*/
goog.provide('goog.net.ErrorCode');
/**
* Error codes
* @enum {number}
*/
goog.net.ErrorCode = {
/**
* There is no error condition.
*/
NO_ERROR: 0,
/**
* The most common error from iframeio, unfortunately, is that the browser
* responded with an error page that is classed as a different domain. The
* situations, are when a browser error page is shown -- 404, access denied,
* DNS failure, connection reset etc.)
*
*/
ACCESS_DENIED: 1,
/**
* Currently the only case where file not found will be caused is when the
* code is running on the local file system and a non-IE browser makes a
* request to a file that doesn't exist.
*/
FILE_NOT_FOUND: 2,
/**
* If Firefox shows a browser error page, such as a connection reset by
* server or access denied, then it will fail silently without the error or
* load handlers firing.
*/
FF_SILENT_ERROR: 3,
/**
* Custom error provided by the client through the error check hook.
*/
CUSTOM_ERROR: 4,
/**
* Exception was thrown while processing the request.
*/
EXCEPTION: 5,
/**
* The Http response returned a non-successful http status code.
*/
HTTP_ERROR: 6,
/**
* The request was aborted.
*/
ABORT: 7,
/**
* The request timed out.
*/
TIMEOUT: 8,
/**
* The resource is not available offline.
*/
OFFLINE: 9
};
/**
* Returns a friendly error message for an error code. These messages are for
* debugging and are not localized.
* @param {goog.net.ErrorCode} errorCode An error code.
* @return {string} A message for debugging.
*/
goog.net.ErrorCode.getDebugMessage = function(errorCode) {
switch (errorCode) {
case goog.net.ErrorCode.NO_ERROR:
return 'No Error';
case goog.net.ErrorCode.ACCESS_DENIED:
return 'Access denied to content document';
case goog.net.ErrorCode.FILE_NOT_FOUND:
return 'File not found';
case goog.net.ErrorCode.FF_SILENT_ERROR:
return 'Firefox silently errored';
case goog.net.ErrorCode.CUSTOM_ERROR:
return 'Application custom error';
case goog.net.ErrorCode.EXCEPTION:
return 'An exception occurred';
case goog.net.ErrorCode.HTTP_ERROR:
return 'Http response at 400 or 500 level';
case goog.net.ErrorCode.ABORT:
return 'Request was aborted';
case goog.net.ErrorCode.TIMEOUT:
return 'Request timed out';
case goog.net.ErrorCode.OFFLINE:
return 'The resource is not available offline';
default:
return 'Unrecognized error code';
}
};
@@ -0,0 +1,37 @@
// 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 Common events for the network classes.
*/
goog.provide('goog.net.EventType');
/**
* Event names for network events
* @enum {string}
*/
goog.net.EventType = {
COMPLETE: 'complete',
SUCCESS: 'success',
ERROR: 'error',
ABORT: 'abort',
READY: 'ready',
READY_STATE_CHANGE: 'readystatechange',
TIMEOUT: 'timeout',
INCREMENTAL_DATA: 'incrementaldata',
PROGRESS: 'progress'
};
@@ -0,0 +1,743 @@
// Copyright 2011 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 A class for downloading remote files and storing them
* locally using the HTML5 FileSystem API.
*
* The directory structure is of the form /HASH/URL/BASENAME:
*
* The HASH portion is a three-character slice of the hash of the URL. Since the
* filesystem has a limit of about 5000 files per directory, this should divide
* the downloads roughly evenly among about 5000 directories, thus allowing for
* at most 5000^2 downloads.
*
* The URL portion is the (sanitized) full URL used for downloading the file.
* This is used to ensure that each file ends up in a different location, even
* if the HASH and BASENAME are the same.
*
* The BASENAME portion is the basename of the URL. It's used for the filename
* proper so that the local filesystem: URL will be downloaded to a file with a
* recognizable name.
*
*/
goog.provide('goog.net.FileDownloader');
goog.provide('goog.net.FileDownloader.Error');
goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('goog.async.Deferred');
goog.require('goog.crypt.hash32');
goog.require('goog.debug.Error');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.fs');
goog.require('goog.fs.DirectoryEntry');
goog.require('goog.fs.Error');
goog.require('goog.fs.FileSaver');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XhrIoPool');
goog.require('goog.object');
/**
* A class for downloading remote files and storing them locally using the
* HTML5 filesystem API.
*
* @param {!goog.fs.DirectoryEntry} dir The directory in which the downloaded
* files are stored. This directory should be solely managed by
* FileDownloader.
* @param {goog.net.XhrIoPool=} opt_pool The pool of XhrIo objects to use for
* downloading files.
* @constructor
* @extends {goog.Disposable}
*/
goog.net.FileDownloader = function(dir, opt_pool) {
goog.base(this);
/**
* The directory in which the downloaded files are stored.
* @type {!goog.fs.DirectoryEntry}
* @private
*/
this.dir_ = dir;
/**
* The pool of XHRs to use for capturing.
* @type {!goog.net.XhrIoPool}
* @private
*/
this.pool_ = opt_pool || new goog.net.XhrIoPool();
/**
* A map from URLs to active downloads running for those URLs.
* @type {!Object.<!goog.net.FileDownloader.Download_>}
* @private
*/
this.downloads_ = {};
/**
* The handler for URL capturing events.
* @type {!goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
};
goog.inherits(goog.net.FileDownloader, goog.Disposable);
/**
* Download a remote file and save its contents to the filesystem. A given file
* is uniquely identified by its URL string; this means that the relative and
* absolute URLs for a single file are considered different for the purposes of
* the FileDownloader.
*
* Returns a Deferred that will contain the downloaded blob. If there's an error
* while downloading the URL, this Deferred will be passed the
* {@link goog.net.FileDownloader.Error} object as an errback.
*
* If a download is already in progress for the given URL, this will return the
* deferred blob for that download. If the URL has already been downloaded, this
* will fail once it tries to save the downloaded blob.
*
* When a download is in progress, all Deferreds returned for that download will
* be branches of a single parent. If all such branches are cancelled, or if one
* is cancelled with opt_deepCancel set, then the download will be cancelled as
* well.
*
* @param {string} url The URL of the file to download.
* @return {!goog.async.Deferred} The deferred result blob.
*/
goog.net.FileDownloader.prototype.download = function(url) {
if (this.isDownloading(url)) {
return this.downloads_[url].deferred.branch(true /* opt_propagateCancel */);
}
var download = new goog.net.FileDownloader.Download_(url, this);
this.downloads_[url] = download;
this.pool_.getObject(goog.bind(this.gotXhr_, this, download));
return download.deferred.branch(true /* opt_propagateCancel */);
};
/**
* Return a Deferred that will fire once no download is active for a given URL.
* If there's no download active for that URL when this is called, the deferred
* will fire immediately; otherwise, it will fire once the download is complete,
* whether or not it succeeds.
*
* @param {string} url The URL of the download to wait for.
* @return {!goog.async.Deferred} The Deferred that will fire when the download
* is complete.
*/
goog.net.FileDownloader.prototype.waitForDownload = function(url) {
var deferred = new goog.async.Deferred();
if (this.isDownloading(url)) {
this.downloads_[url].deferred.addBoth(function() {
deferred.callback(null);
}, this);
} else {
deferred.callback(null);
}
return deferred;
};
/**
* Returns whether or not there is an active download for a given URL.
*
* @param {string} url The URL of the download to check.
* @return {boolean} Whether or not there is an active download for the URL.
*/
goog.net.FileDownloader.prototype.isDownloading = function(url) {
return url in this.downloads_;
};
/**
* Load a downloaded blob from the filesystem. Will fire a deferred error if the
* given URL has not yet been downloaded.
*
* @param {string} url The URL of the blob to load.
* @return {!goog.async.Deferred} The deferred Blob object. The callback will be
* passed the blob. If a file API error occurs while loading the blob, that
* error will be passed to the errback.
*/
goog.net.FileDownloader.prototype.getDownloadedBlob = function(url) {
return this.getFile_(url).
addCallback(function(fileEntry) { return fileEntry.file(); });
};
/**
* Get the local filesystem: URL for a downloaded file. This is different from
* the blob: URL that's available from getDownloadedBlob(). If the end user
* accesses the filesystem: URL, the resulting file's name will be determined by
* the download filename as opposed to an arbitrary GUID. In addition, the
* filesystem: URL is connected to a filesystem location, so if the download is
* removed then that URL will become invalid.
*
* Warning: in Chrome 12, some filesystem: URLs are opened inline. This means
* that e.g. HTML pages given to the user via filesystem: URLs will be opened
* and processed by the browser.
*
* @param {string} url The URL of the file to get the URL of.
* @return {!goog.async.Deferred} The deferred filesystem: URL. The callback
* will be passed the URL. If a file API error occurs while loading the
* blob, that error will be passed to the errback.
*/
goog.net.FileDownloader.prototype.getLocalUrl = function(url) {
return this.getFile_(url).
addCallback(function(fileEntry) { return fileEntry.toUrl(); });
};
/**
* Return (deferred) whether or not a URL has been downloaded. Will fire a
* deferred error if something goes wrong when determining this.
*
* @param {string} url The URL to check.
* @return {!goog.async.Deferred} The deferred boolean. The callback will be
* passed the boolean. If a file API error occurs while checking the
* existence of the downloaded URL, that error will be passed to the
* errback.
*/
goog.net.FileDownloader.prototype.isDownloaded = function(url) {
var deferred = new goog.async.Deferred();
var blobDeferred = this.getDownloadedBlob(url);
blobDeferred.addCallback(function() {
deferred.callback(true);
});
blobDeferred.addErrback(function(err) {
if (err.code == goog.fs.Error.ErrorCode.NOT_FOUND) {
deferred.callback(false);
} else {
deferred.errback(err);
}
});
return deferred;
};
/**
* Remove a URL from the FileDownloader.
*
* This returns a Deferred. If the removal is completed successfully, its
* callback will be called without any value. If the removal fails, its errback
* will be called with the {@link goog.fs.Error}.
*
* @param {string} url The URL to remove.
* @return {!goog.async.Deferred} The deferred used for registering callbacks on
* success or on error.
*/
goog.net.FileDownloader.prototype.remove = function(url) {
return this.getDir_(url, goog.fs.DirectoryEntry.Behavior.DEFAULT).
addCallback(function(dir) { return dir.removeRecursively(); });
};
/**
* Save a blob for a given URL. This works just as through the blob were
* downloaded form that URL, except you specify the blob and no HTTP request is
* made.
*
* If the URL is currently being downloaded, it's indeterminate whether the blob
* being set or the blob being downloaded will end up in the filesystem.
* Whichever one doesn't get saved will have an error. To ensure that one or the
* other takes precedence, use {@link #waitForDownload} to allow the download to
* complete before setting the blob.
*
* @param {string} url The URL at which to set the blob.
* @param {!Blob} blob The blob to set.
* @param {string=} opt_name The name of the file. If this isn't given, it's
* determined from the URL.
* @return {!goog.async.Deferred} The deferred used for registering callbacks on
* success or on error. This can be cancelled just like a {@link #download}
* Deferred. The objects passed to the errback will be
* {@link goog.net.FileDownloader.Error}s.
*/
goog.net.FileDownloader.prototype.setBlob = function(url, blob, opt_name) {
var name = this.sanitize_(opt_name || this.urlToName_(url));
var download = new goog.net.FileDownloader.Download_(url, this);
this.downloads_[url] = download;
download.blob = blob;
this.getDir_(download.url, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE).
addCallback(function(dir) {
return dir.getFile(
name, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE);
}).
addCallback(goog.bind(this.fileSuccess_, this, download)).
addErrback(goog.bind(this.error_, this, download));
return download.deferred.branch(true /* opt_propagateCancel */);
};
/**
* The callback called when an XHR becomes available from the XHR pool.
*
* @param {!goog.net.FileDownloader.Download_} download The download object for
* this download.
* @param {!goog.net.XhrIo} xhr The XhrIo object for downloading the page.
* @private
*/
goog.net.FileDownloader.prototype.gotXhr_ = function(download, xhr) {
if (download.cancelled) {
this.freeXhr_(xhr);
return;
}
this.eventHandler_.listen(
xhr, goog.net.EventType.SUCCESS,
goog.bind(this.xhrSuccess_, this, download));
this.eventHandler_.listen(
xhr, [goog.net.EventType.ERROR, goog.net.EventType.ABORT],
goog.bind(this.error_, this, download));
this.eventHandler_.listen(
xhr, goog.net.EventType.READY,
goog.bind(this.freeXhr_, this, xhr));
download.xhr = xhr;
xhr.setResponseType(goog.net.XhrIo.ResponseType.ARRAY_BUFFER);
xhr.send(download.url);
};
/**
* The callback called when an XHR succeeds in downloading a remote file.
*
* @param {!goog.net.FileDownloader.Download_} download The download object for
* this download.
* @private
*/
goog.net.FileDownloader.prototype.xhrSuccess_ = function(download) {
if (download.cancelled) {
return;
}
var name = this.sanitize_(this.getName_(
/** @type {!goog.net.XhrIo} */ (download.xhr)));
var resp = /** @type {ArrayBuffer} */ (download.xhr.getResponse());
if (!resp) {
// This should never happen - it indicates the XHR hasn't completed, has
// failed or has been cleaned up. If it does happen (eg. due to a bug
// somewhere) we don't want to pass null to getBlob - it's not valid and
// triggers a bug in some versions of WebKit causing it to crash.
this.error_(download);
return;
}
download.blob = goog.fs.getBlob(resp);
delete download.xhr;
this.getDir_(download.url, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE).
addCallback(function(dir) {
return dir.getFile(
name, goog.fs.DirectoryEntry.Behavior.CREATE_EXCLUSIVE);
}).
addCallback(goog.bind(this.fileSuccess_, this, download)).
addErrback(goog.bind(this.error_, this, download));
};
/**
* The callback called when a file that will be used for saving a file is
* successfully opened.
*
* @param {!goog.net.FileDownloader.Download_} download The download object for
* this download.
* @param {!goog.fs.FileEntry} file The newly-opened file object.
* @private
*/
goog.net.FileDownloader.prototype.fileSuccess_ = function(download, file) {
if (download.cancelled) {
file.remove();
return;
}
download.file = file;
file.createWriter().
addCallback(goog.bind(this.fileWriterSuccess_, this, download)).
addErrback(goog.bind(this.error_, this, download));
};
/**
* The callback called when a file writer is succesfully created for writing a
* file to the filesystem.
*
* @param {!goog.net.FileDownloader.Download_} download The download object for
* this download.
* @param {!goog.fs.FileWriter} writer The newly-created file writer object.
* @private
*/
goog.net.FileDownloader.prototype.fileWriterSuccess_ = function(
download, writer) {
if (download.cancelled) {
download.file.remove();
return;
}
download.writer = writer;
writer.write(/** @type {!Blob} */ (download.blob));
this.eventHandler_.listenOnce(
writer,
goog.fs.FileSaver.EventType.WRITE_END,
goog.bind(this.writeEnd_, this, download));
};
/**
* The callback called when file writing ends, whether or not it's successful.
*
* @param {!goog.net.FileDownloader.Download_} download The download object for
* this download.
* @private
*/
goog.net.FileDownloader.prototype.writeEnd_ = function(download) {
if (download.cancelled || download.writer.getError()) {
this.error_(download, download.writer.getError());
return;
}
delete this.downloads_[download.url];
download.deferred.callback(download.blob);
};
/**
* The error callback for all asynchronous operations. Ensures that all stages
* of a given download are cleaned up, and emits the error event.
*
* @param {!goog.net.FileDownloader.Download_} download The download object for
* this download.
* @param {goog.fs.Error=} opt_err The file error object. Only defined if the
* error was raised by the file API.
* @private
*/
goog.net.FileDownloader.prototype.error_ = function(download, opt_err) {
if (download.file) {
download.file.remove();
}
if (download.cancelled) {
return;
}
delete this.downloads_[download.url];
download.deferred.errback(
new goog.net.FileDownloader.Error(download, opt_err));
};
/**
* Abort the download of the given URL.
*
* @param {!goog.net.FileDownloader.Download_} download The download to abort.
* @private
*/
goog.net.FileDownloader.prototype.cancel_ = function(download) {
goog.dispose(download);
delete this.downloads_[download.url];
};
/**
* Get the directory for a given URL. If the directory already exists when this
* is called, it will contain exactly one file: the downloaded file.
*
* This not only calls the FileSystem API's getFile method, but attempts to
* distribute the files so that they don't overload the filesystem. The spec
* says directories can't contain more than 5000 files
* (http://www.w3.org/TR/file-system-api/#directories), so this ensures that
* each file is put into a subdirectory based on its SHA1 hash.
*
* All parameters are the same as in the FileSystem API's Entry#getFile method.
*
* @param {string} url The URL corresponding to the directory to get.
* @param {goog.fs.DirectoryEntry.Behavior} behavior The behavior to pass to the
* underlying method.
* @return {!goog.async.Deferred} The deferred DirectoryEntry object.
* @private
*/
goog.net.FileDownloader.prototype.getDir_ = function(url, behavior) {
// 3 hex digits provide 16**3 = 4096 different possible dirnames, which is
// less than the maximum of 5000 entries. Downloaded files should be
// distributed roughly evenly throughout the directories due to the hash
// function, allowing many more than 5000 files to be downloaded.
//
// The leading ` ensures that no illegal dirnames are accidentally used. % was
// previously used, but Chrome has a bug (as of 12.0.725.0 dev) where
// filenames are URL-decoded before checking their validity, so filenames
// containing e.g. '%3f' (the URL-encoding of :, an invalid character) are
// rejected.
var dirname = '`' + Math.abs(goog.crypt.hash32.encodeString(url)).
toString(16).substring(0, 3);
return this.dir_.
getDirectory(dirname, goog.fs.DirectoryEntry.Behavior.CREATE).
addCallback(function(dir) {
return dir.getDirectory(this.sanitize_(url), behavior);
}, this);
};
/**
* Get the file for a given URL. This will only retrieve files that have already
* been saved; it shouldn't be used for creating the file in the first place.
* This is because the filename isn't necessarily determined by the URL, but by
* the headers of the XHR response.
*
* @param {string} url The URL corresponding to the file to get.
* @return {!goog.async.Deferred} The deferred FileEntry object.
* @private
*/
goog.net.FileDownloader.prototype.getFile_ = function(url) {
return this.getDir_(url, goog.fs.DirectoryEntry.Behavior.DEFAULT).
addCallback(function(dir) {
return dir.listDirectory().addCallback(function(files) {
goog.asserts.assert(files.length == 1);
// If the filesystem somehow gets corrupted and we end up with an
// empty directory here, it makes sense to just return the normal
// file-not-found error.
return files[0] || dir.getFile('file');
});
});
};
/**
* Sanitize a string so it can be safely used as a file or directory name for
* the FileSystem API.
*
* @param {string} str The string to sanitize.
* @return {string} The sanitized string.
* @private
*/
goog.net.FileDownloader.prototype.sanitize_ = function(str) {
// Add a prefix, since certain prefixes are disallowed for paths. None of the
// disallowed prefixes start with '`'. We use ` rather than % for escaping the
// filename due to a Chrome bug (as of 12.0.725.0 dev) where filenames are
// URL-decoded before checking their validity, so filenames containing e.g.
// '%3f' (the URL-encoding of :, an invalid character) are rejected.
return '`' + str.replace(/[\/\\<>:?*"|%`]/g, encodeURIComponent).
replace(/%/g, '`');
};
/**
* Gets the filename specified by the XHR. This first attempts to parse the
* Content-Disposition header for a filename and, failing that, falls back on
* deriving the filename from the URL.
*
* @param {!goog.net.XhrIo} xhr The XHR containing the response headers.
* @return {string} The filename.
* @private
*/
goog.net.FileDownloader.prototype.getName_ = function(xhr) {
var disposition = xhr.getResponseHeader('Content-Disposition');
var match = disposition &&
disposition.match(/^attachment *; *filename="(.*)"$/i);
if (match) {
// The Content-Disposition header allows for arbitrary backslash-escaped
// characters (usually " and \). We want to unescape them before using them
// in the filename.
return match[1].replace(/\\(.)/g, '$1');
}
return this.urlToName_(xhr.getLastUri());
};
/**
* Extracts the basename from a URL.
*
* @param {string} url The URL.
* @return {string} The basename.
* @private
*/
goog.net.FileDownloader.prototype.urlToName_ = function(url) {
var segments = url.split('/');
return segments[segments.length - 1];
};
/**
* Remove all event listeners for an XHR and release it back into the pool.
*
* @param {!goog.net.XhrIo} xhr The XHR to free.
* @private
*/
goog.net.FileDownloader.prototype.freeXhr_ = function(xhr) {
goog.events.removeAll(xhr);
this.pool_.addFreeObject(xhr);
};
/** @override */
goog.net.FileDownloader.prototype.disposeInternal = function() {
delete this.dir_;
goog.dispose(this.eventHandler_);
delete this.eventHandler_;
goog.object.forEach(this.downloads_, function(download) {
download.deferred.cancel();
}, this);
delete this.downloads_;
goog.dispose(this.pool_);
delete this.pool_;
goog.base(this, 'disposeInternal');
};
/**
* The error object for FileDownloader download errors.
*
* @param {!goog.net.FileDownloader.Download_} download The download object for
* the download in question.
* @param {goog.fs.Error=} opt_fsErr The file error object, if this was a file
* error.
*
* @constructor
* @extends {goog.debug.Error}
*/
goog.net.FileDownloader.Error = function(download, opt_fsErr) {
goog.base(this, 'Error capturing URL ' + download.url);
/**
* The URL the event relates to.
* @type {string}
*/
this.url = download.url;
if (download.xhr) {
this.xhrStatus = download.xhr.getStatus();
this.xhrErrorCode = download.xhr.getLastErrorCode();
this.message += ': XHR failed with status ' + this.xhrStatus +
' (error code ' + this.xhrErrorCode + ')';
} else if (opt_fsErr) {
this.fileError = opt_fsErr;
this.message += ': file API failed (' + opt_fsErr.message + ')';
}
};
goog.inherits(goog.net.FileDownloader.Error, goog.debug.Error);
/**
* The status of the XHR. Only set if the error was caused by an XHR failure.
* @type {number|undefined}
*/
goog.net.FileDownloader.Error.prototype.xhrStatus;
/**
* The error code of the XHR. Only set if the error was caused by an XHR
* failure.
* @type {goog.net.ErrorCode|undefined}
*/
goog.net.FileDownloader.Error.prototype.xhrErrorCode;
/**
* The file API error. Only set if the error was caused by the file API.
* @type {goog.fs.Error|undefined}
*/
goog.net.FileDownloader.Error.prototype.fileError;
/**
* A struct containing the data for a single download.
*
* @param {string} url The URL for the file being downloaded.
* @param {!goog.net.FileDownloader} downloader The parent FileDownloader.
* @extends {goog.Disposable}
* @constructor
* @private
*/
goog.net.FileDownloader.Download_ = function(url, downloader) {
goog.base(this);
/**
* The URL for the file being downloaded.
* @type {string}
*/
this.url = url;
/**
* The Deferred that will be fired when the download is complete.
* @type {!goog.async.Deferred}
*/
this.deferred = new goog.async.Deferred(
goog.bind(downloader.cancel_, downloader, this));
/**
* Whether this download has been cancelled by the user.
* @type {boolean}
*/
this.cancelled = false;
/**
* The XhrIo object for downloading the file. Only set once it's been
* retrieved from the pool.
* @type {goog.net.XhrIo}
*/
this.xhr = null;
/**
* The name of the blob being downloaded. Only sey once the XHR has completed,
* if it completed successfully.
* @type {?string}
*/
this.name = null;
/**
* The downloaded blob. Only set once the XHR has completed, if it completed
* successfully.
* @type {Blob}
*/
this.blob = null;
/**
* The file entry where the blob is to be stored. Only set once it's been
* loaded from the filesystem.
* @type {goog.fs.FileEntry}
*/
this.file = null;
/**
* The file writer for writing the blob to the filesystem. Only set once it's
* been loaded from the filesystem.
* @type {goog.fs.FileWriter}
*/
this.writer = null;
};
goog.inherits(goog.net.FileDownloader.Download_, goog.Disposable);
/** @override */
goog.net.FileDownloader.Download_.prototype.disposeInternal = function() {
this.cancelled = true;
if (this.xhr) {
this.xhr.abort();
} else if (this.writer && this.writer.getReadyState() ==
goog.fs.FileSaver.ReadyState.WRITING) {
this.writer.abort();
}
goog.base(this, 'disposeInternal');
};
@@ -0,0 +1,111 @@
// Copyright 2011 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 Constants for HTTP status codes.
*/
goog.provide('goog.net.HttpStatus');
/**
* HTTP Status Codes defined in RFC 2616.
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
* @enum {number}
*/
goog.net.HttpStatus = {
// Informational 1xx
CONTINUE: 100,
SWITCHING_PROTOCOLS: 101,
// Successful 2xx
OK: 200,
CREATED: 201,
ACCEPTED: 202,
NON_AUTHORITATIVE_INFORMATION: 203,
NO_CONTENT: 204,
RESET_CONTENT: 205,
PARTIAL_CONTENT: 206,
// Redirection 3xx
MULTIPLE_CHOICES: 300,
MOVED_PERMANENTLY: 301,
FOUND: 302,
SEE_OTHER: 303,
NOT_MODIFIED: 304,
USE_PROXY: 305,
TEMPORARY_REDIRECT: 307,
// Client Error 4xx
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
PAYMENT_REQUIRED: 402,
FORBIDDEN: 403,
NOT_FOUND: 404,
METHOD_NOT_ALLOWED: 405,
NOT_ACCEPTABLE: 406,
PROXY_AUTHENTICATION_REQUIRED: 407,
REQUEST_TIMEOUT: 408,
CONFLICT: 409,
GONE: 410,
LENGTH_REQUIRED: 411,
PRECONDITION_FAILED: 412,
REQUEST_ENTITY_TOO_LARGE: 413,
REQUEST_URI_TOO_LONG: 414,
UNSUPPORTED_MEDIA_TYPE: 415,
REQUEST_RANGE_NOT_SATISFIABLE: 416,
EXPECTATION_FAILED: 417,
// Server Error 5xx
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
HTTP_VERSION_NOT_SUPPORTED: 505,
/*
* IE returns this code for 204 due to its use of URLMon, which returns this
* code for 'Operation Aborted'. The status text is 'Unknown', the response
* headers are ''. Known to occur on IE 6 on XP through IE9 on Win7.
*/
QUIRK_IE_NO_CONTENT: 1223
};
/**
* Returns whether the given status should be considered successful.
*
* Successful codes are OK (200), CREATED (201), ACCEPTED (202),
* NO CONTENT (204), PARTIAL CONTENT (206), NOT MODIFIED (304),
* and IE's no content code (1223).
*
* @param {number} status The status code to test.
* @return {boolean} Whether the status code should be considered successful.
*/
goog.net.HttpStatus.isSuccess = function(status) {
switch (status) {
case goog.net.HttpStatus.OK:
case goog.net.HttpStatus.CREATED:
case goog.net.HttpStatus.ACCEPTED:
case goog.net.HttpStatus.NO_CONTENT:
case goog.net.HttpStatus.PARTIAL_CONTENT:
case goog.net.HttpStatus.NOT_MODIFIED:
case goog.net.HttpStatus.QUIRK_IE_NO_CONTENT:
return true;
default:
return false;
}
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,200 @@
// Copyright 2008 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 Class that can be used to determine when an iframe is loaded.
*/
goog.provide('goog.net.IframeLoadMonitor');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.userAgent');
/**
* The correct way to determine whether an iframe has completed loading
* is different in IE and Firefox. This class abstracts above these
* differences, providing a consistent interface for:
* <ol>
* <li> Determing if an iframe is currently loaded
* <li> Listening for an iframe that is not currently loaded, to finish loading
* </ol>
*
* @param {HTMLIFrameElement} iframe An iframe.
* @param {boolean=} opt_hasContent Does the loaded iframe have content.
* @extends {goog.events.EventTarget}
* @constructor
*/
goog.net.IframeLoadMonitor = function(iframe, opt_hasContent) {
goog.base(this);
/**
* Iframe whose load state is monitored by this IframeLoadMonitor
* @type {HTMLIFrameElement}
* @private
*/
this.iframe_ = iframe;
/**
* Whether or not the loaded iframe has any content.
* @type {boolean}
* @private
*/
this.hasContent_ = !!opt_hasContent;
/**
* Whether or not the iframe is loaded.
* @type {boolean}
* @private
*/
this.isLoaded_ = this.isLoadedHelper_();
if (!this.isLoaded_) {
// IE 6 (and lower?) does not reliably fire load events, so listen to
// readystatechange.
// IE 7 does not reliably fire readystatechange events but listening on load
// seems to work just fine.
var isIe6OrLess =
goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('7');
var loadEvtType = isIe6OrLess ?
goog.events.EventType.READYSTATECHANGE : goog.events.EventType.LOAD;
this.onloadListenerKey_ = goog.events.listen(
this.iframe_, loadEvtType, this.handleLoad_, false, this);
// Sometimes we still don't get the event callback, so we'll poll just to
// be safe.
this.intervalId_ = window.setInterval(
goog.bind(this.handleLoad_, this),
goog.net.IframeLoadMonitor.POLL_INTERVAL_MS_);
}
};
goog.inherits(goog.net.IframeLoadMonitor, goog.events.EventTarget);
/**
* Event type dispatched by a goog.net.IframeLoadMonitor when it internal iframe
* finishes loading for the first time after construction of the
* goog.net.IframeLoadMonitor
* @type {string}
*/
goog.net.IframeLoadMonitor.LOAD_EVENT = 'ifload';
/**
* Poll interval for polling iframe load states in milliseconds.
* @type {number}
* @private
*/
goog.net.IframeLoadMonitor.POLL_INTERVAL_MS_ = 100;
/**
* Key for iframe load listener, or null if not currently listening on the
* iframe for a load event.
* @type {goog.events.Key}
* @private
*/
goog.net.IframeLoadMonitor.prototype.onloadListenerKey_ = null;
/**
* Returns whether or not the iframe is loaded.
* @return {boolean} whether or not the iframe is loaded.
*/
goog.net.IframeLoadMonitor.prototype.isLoaded = function() {
return this.isLoaded_;
};
/**
* Stops the poll timer if this IframeLoadMonitor is currently polling.
* @private
*/
goog.net.IframeLoadMonitor.prototype.maybeStopTimer_ = function() {
if (this.intervalId_) {
window.clearInterval(this.intervalId_);
this.intervalId_ = null;
}
};
/**
* Returns the iframe whose load state this IframeLoader monitors.
* @return {HTMLIFrameElement} the iframe whose load state this IframeLoader
* monitors.
*/
goog.net.IframeLoadMonitor.prototype.getIframe = function() {
return this.iframe_;
};
/** @override */
goog.net.IframeLoadMonitor.prototype.disposeInternal = function() {
delete this.iframe_;
this.maybeStopTimer_();
goog.events.unlistenByKey(this.onloadListenerKey_);
goog.net.IframeLoadMonitor.superClass_.disposeInternal.call(this);
};
/**
* Returns whether or not the iframe is loaded. Determines this by inspecting
* browser dependent properties of the iframe.
* @return {boolean} whether or not the iframe is loaded.
* @private
*/
goog.net.IframeLoadMonitor.prototype.isLoadedHelper_ = function() {
var isLoaded = false;
/** @preserveTry */
try {
// IE will reliably have readyState set to complete if the iframe is loaded
// For everything else, the iframe is loaded if there is a body and if the
// body should have content the firstChild exists. Firefox can fire
// the LOAD event and then a few hundred ms later replace the
// contentDocument once the content is loaded.
isLoaded = goog.userAgent.IE ? this.iframe_.readyState == 'complete' :
!!goog.dom.getFrameContentDocument(this.iframe_).body &&
(!this.hasContent_ ||
!!goog.dom.getFrameContentDocument(this.iframe_).body.firstChild);
} catch (e) {
// Ignore these errors. This just means that the iframe is not loaded
// IE will throw error reading readyState if the iframe is not appended
// to the dom yet.
// Firefox will throw error getting the iframe body if the iframe is not
// fully loaded.
}
return isLoaded;
};
/**
* Handles an event indicating that the loading status of the iframe has
* changed. In Firefox this is a goog.events.EventType.LOAD event, in IE
* this is a goog.events.EventType.READYSTATECHANGED
* @private
*/
goog.net.IframeLoadMonitor.prototype.handleLoad_ = function() {
// Only do the handler if the iframe is loaded.
if (this.isLoadedHelper_()) {
this.maybeStopTimer_();
goog.events.unlistenByKey(this.onloadListenerKey_);
this.onloadListenerKey_ = null;
this.isLoaded_ = true;
this.dispatchEvent(goog.net.IframeLoadMonitor.LOAD_EVENT);
}
};
@@ -0,0 +1,292 @@
// Copyright 2008 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 Image loader utility class. Useful when an application needs
* to preload multiple images, for example so they can be sized.
*
* @author attila@google.com (Attila Bodis)
* @author zachlloyd@google.com (Zachary Lloyd)
* @author jonemerson@google.com (Jon Emerson)
*/
goog.provide('goog.net.ImageLoader');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.net.EventType');
goog.require('goog.object');
goog.require('goog.userAgent');
/**
* Image loader utility class. Raises a {@link goog.events.EventType.LOAD}
* event for each image loaded, with an {@link Image} object as the target of
* the event, normalized to have {@code naturalHeight} and {@code naturalWidth}
* attributes.
*
* To use this class, run:
*
* <pre>
* var imageLoader = new goog.net.ImageLoader();
* goog.events.listen(imageLoader, goog.net.EventType.COMPLETE,
* function(e) { ... });
* imageLoader.addImage("image_id", "http://path/to/image.gif");
* imageLoader.start();
* </pre>
*
* The start() method must be called to start image loading. Images can be
* added and removed after loading has started, but only those images added
* before start() was called will be loaded until start() is called again.
* A goog.net.EventType.COMPLETE event will be dispatched only once all
* outstanding images have completed uploading.
*
* @param {Element=} opt_parent An optional parent element whose document object
* should be used to load images.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.net.ImageLoader = function(opt_parent) {
goog.events.EventTarget.call(this);
/**
* Map of image IDs to their image src, used to keep track of the images to
* load. Once images have started loading, they're removed from this map.
* @type {!Object.<string, string>}
* @private
*/
this.imageIdToUrlMap_ = {};
/**
* Map of image IDs to their image element, used only for images that are in
* the process of loading. Used to clean-up event listeners and to know
* when we've completed loading images.
* @type {!Object.<string, !Element>}
* @private
*/
this.imageIdToImageMap_ = {};
/**
* Event handler object, used to keep track of onload and onreadystatechange
* listeners.
* @type {!goog.events.EventHandler}
* @private
*/
this.handler_ = new goog.events.EventHandler(this);
/**
* The parent element whose document object will be used to load images.
* Useful if you want to load the images from a window other than the current
* window in order to control the Referer header sent when the image is
* loaded.
* @type {Element|undefined}
* @private
*/
this.parent_ = opt_parent;
};
goog.inherits(goog.net.ImageLoader, goog.events.EventTarget);
/**
* An array of event types to listen to on images. This is browser dependent.
* Internet Explorer doesn't reliably raise LOAD events on images, so we must
* use READY_STATE_CHANGE. If the image is cached locally, IE won't fire the
* LOAD event while the onreadystate event is fired always. On the other hand,
* the ERROR event is always fired whenever the image is not loaded successfully
* no matter whether it's cached or not.
* @type {!Array.<string>}
* @private
*/
goog.net.ImageLoader.IMAGE_LOAD_EVENTS_ = [
goog.userAgent.IE ? goog.net.EventType.READY_STATE_CHANGE :
goog.events.EventType.LOAD,
goog.net.EventType.ABORT,
goog.net.EventType.ERROR
];
/**
* Adds an image to the image loader, and associates it with the given ID
* string. If an image with that ID already exists, it is silently replaced.
* When the image in question is loaded, the target of the LOAD event will be
* an {@code Image} object with {@code id} and {@code src} attributes based on
* these arguments.
* @param {string} id The ID of the image to load.
* @param {string|Image} image Either the source URL of the image or the HTML
* image element itself (or any object with a {@code src} property, really).
*/
goog.net.ImageLoader.prototype.addImage = function(id, image) {
var src = goog.isString(image) ? image : image.src;
if (src) {
// For now, we just store the source URL for the image.
this.imageIdToUrlMap_[id] = src;
}
};
/**
* Removes the image associated with the given ID string from the image loader.
* If the image was previously loading, removes any listeners for its events
* and dispatches a COMPLETE event if all remaining images have now completed.
* @param {string} id The ID of the image to remove.
*/
goog.net.ImageLoader.prototype.removeImage = function(id) {
delete this.imageIdToUrlMap_[id];
var image = this.imageIdToImageMap_[id];
if (image) {
delete this.imageIdToImageMap_[id];
// Stop listening for events on the image.
this.handler_.unlisten(image, goog.net.ImageLoader.IMAGE_LOAD_EVENTS_,
this.onNetworkEvent_);
// If this was the last image, raise a COMPLETE event.
if (goog.object.isEmpty(this.imageIdToImageMap_) &&
goog.object.isEmpty(this.imageIdToUrlMap_)) {
this.dispatchEvent(goog.net.EventType.COMPLETE);
}
}
};
/**
* Starts loading all images in the image loader in parallel. Raises a LOAD
* event each time an image finishes loading, and a COMPLETE event after all
* images have finished loading.
*/
goog.net.ImageLoader.prototype.start = function() {
// Iterate over the keys, rather than the full object, to essentially clone
// the initial queued images in case any event handlers decide to add more
// images before this loop has finished executing.
var imageIdToUrlMap = this.imageIdToUrlMap_;
goog.array.forEach(goog.object.getKeys(imageIdToUrlMap),
function(id) {
var src = imageIdToUrlMap[id];
if (src) {
delete imageIdToUrlMap[id];
this.loadImage_(src, id);
}
}, this);
};
/**
* Creates an {@code Image} object with the specified ID and source URL, and
* listens for network events raised as the image is loaded.
* @param {string} src The image source URL.
* @param {string} id The unique ID of the image to load.
* @private
*/
goog.net.ImageLoader.prototype.loadImage_ = function(src, id) {
if (this.isDisposed()) {
// When loading an image in IE7 (and maybe IE8), the error handler
// may fire before we yield JS control. If the error handler
// dispose the ImageLoader, this method will throw exception.
return;
}
var image;
if (this.parent_) {
var dom = goog.dom.getDomHelper(this.parent_);
image = dom.createDom('img');
} else {
image = new Image();
}
this.handler_.listen(image, goog.net.ImageLoader.IMAGE_LOAD_EVENTS_,
this.onNetworkEvent_);
this.imageIdToImageMap_[id] = image;
image.id = id;
image.src = src;
};
/**
* Handles net events (READY_STATE_CHANGE, LOAD, ABORT, and ERROR).
* @param {goog.events.Event} evt The network event to handle.
* @private
*/
goog.net.ImageLoader.prototype.onNetworkEvent_ = function(evt) {
var image = /** @type {Element} */ (evt.currentTarget);
if (!image) {
return;
}
if (evt.type == goog.net.EventType.READY_STATE_CHANGE) {
// This implies that the user agent is IE; see loadImage_().
// Noe that this block is used to check whether the image is ready to
// dispatch the COMPLETE event.
if (image.readyState == goog.net.EventType.COMPLETE) {
// This is the IE equivalent of a LOAD event.
evt.type = goog.events.EventType.LOAD;
} else {
// This may imply that the load failed.
// Note that the image has only the following states:
// * uninitialized
// * loading
// * complete
// When the ERROR or the ABORT event is fired, the readyState
// will be either uninitialized or loading and we'd ignore those states
// since they will be handled separately (eg: evt.type = 'ERROR').
// Notes from MSDN : The states through which an object passes are
// determined by that object. An object can skip certain states
// (for example, interactive) if the state does not apply to that object.
// see http://msdn.microsoft.com/en-us/library/ms534359(VS.85).aspx
// The image is not loaded, ignore.
return;
}
}
// Add natural width/height properties for non-Gecko browsers.
if (typeof image.naturalWidth == 'undefined') {
if (evt.type == goog.events.EventType.LOAD) {
image.naturalWidth = image.width;
image.naturalHeight = image.height;
} else {
// This implies that the image fails to be loaded.
image.naturalWidth = 0;
image.naturalHeight = 0;
}
}
// Redispatch the event on behalf of the image. Note that the external
// listener may dispose this instance.
this.dispatchEvent({type: evt.type, target: image});
if (this.isDisposed()) {
// If instance was disposed by listener, exit this function.
return;
}
this.removeImage(image.id);
};
/** @override */
goog.net.ImageLoader.prototype.disposeInternal = function() {
delete this.imageIdToUrlMap_;
delete this.imageIdToImageMap_;
goog.dispose(this.handler_);
goog.net.ImageLoader.superClass_.disposeInternal.call(this);
};
@@ -0,0 +1,511 @@
// Copyright 2011 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 This file contains classes to handle IPv4 and IPv6 addresses.
* This implementation is mostly based on Google's project:
* http://code.google.com/p/ipaddr-py/.
*
*/
goog.provide('goog.net.IpAddress');
goog.provide('goog.net.Ipv4Address');
goog.provide('goog.net.Ipv6Address');
goog.require('goog.array');
goog.require('goog.math.Integer');
goog.require('goog.object');
goog.require('goog.string');
/**
* Abstract class defining an IP Address.
*
* Please use goog.net.IpAddress static methods or
* goog.net.Ipv4Address/Ipv6Address classes.
*
* @param {!goog.math.Integer} address The Ip Address.
* @param {number} version The version number (4, 6).
* @constructor
*/
goog.net.IpAddress = function(address, version) {
/**
* The IP Address.
* @type {!goog.math.Integer}
* @private
*/
this.ip_ = address;
/**
* The IP Address version.
* @type {number}
* @private
*/
this.version_ = version;
/**
* The IPAddress, as string.
* @type {string}
* @private
*/
this.ipStr_ = '';
};
/**
* @return {number} The IP Address version.
*/
goog.net.IpAddress.prototype.getVersion = function() {
return this.version_;
};
/**
* @param {!goog.net.IpAddress} other The other IP Address.
* @return {boolean} true if the IP Addresses are equal.
*/
goog.net.IpAddress.prototype.equals = function(other) {
return (this.version_ == other.getVersion() &&
this.ip_.equals(other.toInteger()));
};
/**
* @return {goog.math.Integer} The IP Address, as an Integer.
*/
goog.net.IpAddress.prototype.toInteger = function() {
return /** @type {goog.math.Integer} */ (goog.object.clone(this.ip_));
};
/**
* @return {string} The IP Address, as an URI string following RFC 3986.
*/
goog.net.IpAddress.prototype.toUriString = goog.abstractMethod;
/**
* @return {string} The IP Address, as a string.
* @override
*/
goog.net.IpAddress.prototype.toString = goog.abstractMethod;
/**
* Parses an IP Address in a string.
* If the string is malformed, the function will simply return null
* instead of raising an exception.
*
* @param {string} address The IP Address.
* @see {goog.net.Ipv4Address}
* @see {goog.net.Ipv6Address}
* @return {goog.net.IpAddress} The IP Address or null.
*/
goog.net.IpAddress.fromString = function(address) {
try {
if (address.indexOf(':') != -1) {
return new goog.net.Ipv6Address(address);
}
return new goog.net.Ipv4Address(address);
} catch (e) {
// Both constructors raise exception if the address is malformed (ie.
// invalid). The user of this function should not care about catching
// the exception, espcially if it's used to validate an user input.
return null;
}
};
/**
* Tries to parse a string represented as a host portion of an URI.
* See RFC 3986 for more details on IPv6 addresses inside URI.
* If the string is malformed, the function will simply return null
* instead of raising an exception.
*
* @param {string} address A RFC 3986 encoded IP address.
* @see {goog.net.Ipv4Address}
* @see {goog.net.Ipv6Address}
* @return {goog.net.IpAddress} The IP Address.
*/
goog.net.IpAddress.fromUriString = function(address) {
try {
if (goog.string.startsWith(address, '[') &&
goog.string.endsWith(address, ']')) {
return new goog.net.Ipv6Address(
address.substring(1, address.length - 1));
}
return new goog.net.Ipv4Address(address);
} catch (e) {
// Both constructors raise exception if the address is malformed (ie.
// invalid). The user of this function should not care about catching
// the exception, espcially if it's used to validate an user input.
return null;
}
};
/**
* Takes a string or a number and returns a IPv4 Address.
*
* This constructor accepts strings and instance of goog.math.Integer.
* If you pass a goog.math.Integer, make sure that its sign is set to positive.
* @param {(string|!goog.math.Integer)} address The address to store.
* @extends {goog.net.IpAddress}
* @constructor
*/
goog.net.Ipv4Address = function(address) {
var ip = goog.math.Integer.ZERO;
if (address instanceof goog.math.Integer) {
if (address.getSign() != 0 ||
address.lessThan(goog.math.Integer.ZERO) ||
address.greaterThan(goog.net.Ipv4Address.MAX_ADDRESS_)) {
throw Error('The address does not look like an IPv4.');
} else {
ip = goog.object.clone(address);
}
} else {
if (!goog.net.Ipv4Address.REGEX_.test(address)) {
throw Error(address + ' does not look like an IPv4 address.');
}
var octets = address.split('.');
if (octets.length != 4) {
throw Error(address + ' does not look like an IPv4 address.');
}
for (var i = 0; i < octets.length; i++) {
var parsedOctet = goog.string.toNumber(octets[i]);
if (isNaN(parsedOctet) ||
parsedOctet < 0 || parsedOctet > 255 ||
(octets[i].length != 1 && goog.string.startsWith(octets[i], '0'))) {
throw Error('In ' + address + ', octet ' + i + ' is not valid');
}
var intOctet = goog.math.Integer.fromNumber(parsedOctet);
ip = ip.shiftLeft(8).or(intOctet);
}
}
goog.base(this, /** @type {!goog.math.Integer} */ (ip), 4);
};
goog.inherits(goog.net.Ipv4Address, goog.net.IpAddress);
/**
* Regular expression matching all the allowed chars for IPv4.
* @type {RegExp}
* @private
* @const
*/
goog.net.Ipv4Address.REGEX_ = /^[0-9.]*$/;
/**
* The Maximum length for a netmask (aka, the number of bits for IPv4).
* @type {number}
* @const
*/
goog.net.Ipv4Address.MAX_NETMASK_LENGTH = 32;
/**
* The Maximum address possible for IPv4.
* @type {goog.math.Integer}
* @private
* @const
*/
goog.net.Ipv4Address.MAX_ADDRESS_ = goog.math.Integer.ONE.shiftLeft(
goog.net.Ipv4Address.MAX_NETMASK_LENGTH).subtract(goog.math.Integer.ONE);
/**
* @override
*/
goog.net.Ipv4Address.prototype.toString = function() {
if (this.ipStr_) {
return this.ipStr_;
}
var ip = this.ip_.getBitsUnsigned(0);
var octets = [];
for (var i = 3; i >= 0; i--) {
octets[i] = String((ip & 0xff));
ip = ip >>> 8;
}
this.ipStr_ = octets.join('.');
return this.ipStr_;
};
/**
* @override
*/
goog.net.Ipv4Address.prototype.toUriString = function() {
return this.toString();
};
/**
* Takes a string or a number and returns an IPv6 Address.
*
* This constructor accepts strings and instance of goog.math.Integer.
* If you pass a goog.math.Integer, make sure that its sign is set to positive.
* @param {(string|!goog.math.Integer)} address The address to store.
* @constructor
* @extends {goog.net.IpAddress}
*/
goog.net.Ipv6Address = function(address) {
var ip = goog.math.Integer.ZERO;
if (address instanceof goog.math.Integer) {
if (address.getSign() != 0 ||
address.lessThan(goog.math.Integer.ZERO) ||
address.greaterThan(goog.net.Ipv6Address.MAX_ADDRESS_)) {
throw Error('The address does not look like a valid IPv6.');
} else {
ip = goog.object.clone(address);
}
} else {
if (!goog.net.Ipv6Address.REGEX_.test(address)) {
throw Error(address + ' is not a valid IPv6 address.');
}
var splitColon = address.split(':');
if (splitColon[splitColon.length - 1].indexOf('.') != -1) {
var newHextets = goog.net.Ipv6Address.dottedQuadtoHextets_(
splitColon[splitColon.length - 1]);
goog.array.removeAt(splitColon, splitColon.length - 1);
goog.array.extend(splitColon, newHextets);
address = splitColon.join(':');
}
var splitDoubleColon = address.split('::');
if (splitDoubleColon.length > 2 ||
(splitDoubleColon.length == 1 && splitColon.length != 8)) {
throw Error(address + ' is not a valid IPv6 address.');
}
var ipArr;
if (splitDoubleColon.length > 1) {
ipArr = goog.net.Ipv6Address.explode_(splitDoubleColon);
} else {
ipArr = splitColon;
}
if (ipArr.length != 8) {
throw Error(address + ' is not a valid IPv6 address');
}
for (var i = 0; i < ipArr.length; i++) {
var parsedHextet = goog.math.Integer.fromString(ipArr[i], 16);
if (parsedHextet.lessThan(goog.math.Integer.ZERO) ||
parsedHextet.greaterThan(goog.net.Ipv6Address.MAX_HEXTET_VALUE_)) {
throw Error(ipArr[i] + ' in ' + address + ' is not a valid hextet.');
}
ip = ip.shiftLeft(16).or(parsedHextet);
}
}
goog.base(this, /** @type {!goog.math.Integer} */ (ip), 6);
};
goog.inherits(goog.net.Ipv6Address, goog.net.IpAddress);
/**
* Regular expression matching all allowed chars for an IPv6.
* @type {RegExp}
* @private
* @const
*/
goog.net.Ipv6Address.REGEX_ = /^([a-fA-F0-9]*:){2}[a-fA-F0-9:.]*$/;
/**
* The Maximum length for a netmask (aka, the number of bits for IPv6).
* @type {number}
* @const
*/
goog.net.Ipv6Address.MAX_NETMASK_LENGTH = 128;
/**
* The maximum value of a hextet.
* @type {goog.math.Integer}
* @private
* @const
*/
goog.net.Ipv6Address.MAX_HEXTET_VALUE_ = goog.math.Integer.fromInt(65535);
/**
* The Maximum address possible for IPv6.
* @type {goog.math.Integer}
* @private
* @const
*/
goog.net.Ipv6Address.MAX_ADDRESS_ = goog.math.Integer.ONE.shiftLeft(
goog.net.Ipv6Address.MAX_NETMASK_LENGTH).subtract(goog.math.Integer.ONE);
/**
* @override
*/
goog.net.Ipv6Address.prototype.toString = function() {
if (this.ipStr_) {
return this.ipStr_;
}
var outputArr = [];
for (var i = 3; i >= 0; i--) {
var bits = this.ip_.getBitsUnsigned(i);
var firstHextet = bits >>> 16;
var secondHextet = bits & 0xffff;
outputArr.push(firstHextet.toString(16));
outputArr.push(secondHextet.toString(16));
}
outputArr = goog.net.Ipv6Address.compress_(outputArr);
this.ipStr_ = outputArr.join(':');
return this.ipStr_;
};
/**
* @override
*/
goog.net.Ipv6Address.prototype.toUriString = function() {
return '[' + this.toString() + ']';
};
/**
* This method is in charge of expanding/exploding an IPv6 string from its
* compressed form.
* @private
* @param {!Array.<string>} address An IPv6 address split around '::'.
* @return {Array.<string>} The expanded version of the IPv6.
*/
goog.net.Ipv6Address.explode_ = function(address) {
var basePart = address[0].split(':');
var secondPart = address[1].split(':');
if (basePart.length == 1 && basePart[0] == '') {
basePart = [];
}
if (secondPart.length == 1 && secondPart[0] == '') {
secondPart = [];
}
// Now we fill the gap with 0.
var gap = 8 - (basePart.length + secondPart.length);
if (gap < 1) {
return [];
}
goog.array.extend(basePart, goog.array.repeat('0', gap));
// Now we merge the basePart + gap + secondPart
goog.array.extend(basePart, secondPart);
return basePart;
};
/**
* This method is in charge of compressing an expanded IPv6 array of hextets.
* @private
* @param {!Array.<string>} hextets The array of hextet.
* @return {Array.<string>} The compressed version of this array.
*/
goog.net.Ipv6Address.compress_ = function(hextets) {
var bestStart = -1;
var start = -1;
var bestSize = 0;
var size = 0;
for (var i = 0; i < hextets.length; i++) {
if (hextets[i] == '0') {
size++;
if (start == -1) {
start = i;
}
if (size > bestSize) {
bestSize = size;
bestStart = start;
}
} else {
start = -1;
size = 0;
}
}
if (bestSize > 0) {
if ((bestStart + bestSize) == hextets.length) {
hextets.push('');
}
hextets.splice(bestStart, bestSize, '');
if (bestStart == 0) {
hextets = [''].concat(hextets);
}
}
return hextets;
};
/**
* This method will convert an IPv4 to a list of 2 hextets.
*
* For instance, 1.2.3.4 will be converted to ['0102', '0304'].
* @private
* @param {string} quads An IPv4 as a string.
* @return {Array.<string>} A list of 2 hextets.
*/
goog.net.Ipv6Address.dottedQuadtoHextets_ = function(quads) {
var ip4 = new goog.net.Ipv4Address(quads).toInteger();
var bits = ip4.getBitsUnsigned(0);
var hextets = [];
hextets.push(((bits >>> 16) & 0xffff).toString(16));
hextets.push((bits & 0xffff).toString(16));
return hextets;
};
/**
* @return {boolean} true if the IPv6 contains a mapped IPv4.
*/
goog.net.Ipv6Address.prototype.isMappedIpv4Address = function() {
return (this.ip_.getBitsUnsigned(3) == 0 &&
this.ip_.getBitsUnsigned(2) == 0 &&
this.ip_.getBitsUnsigned(1) == 0xffff);
};
/**
* Will return the mapped IPv4 address in this IPv6 address.
* @return {goog.net.Ipv4Address} an IPv4 or null.
*/
goog.net.Ipv6Address.prototype.getMappedIpv4Address = function() {
if (!this.isMappedIpv4Address()) {
return null;
}
var newIpv4 = new goog.math.Integer([this.ip_.getBitsUnsigned(0)], 0);
return new goog.net.Ipv4Address(newIpv4);
};
@@ -0,0 +1,366 @@
// Copyright 2011 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 A utility to load JavaScript files via DOM script tags.
* Refactored from goog.net.Jsonp. Works cross-domain.
*
*/
goog.provide('goog.net.jsloader');
goog.provide('goog.net.jsloader.Error');
goog.provide('goog.net.jsloader.ErrorCode');
goog.provide('goog.net.jsloader.Options');
goog.require('goog.array');
goog.require('goog.async.Deferred');
goog.require('goog.debug.Error');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
/**
* The name of the property of goog.global under which the JavaScript
* verification object is stored by the loaded script.
* @type {string}
* @private
*/
goog.net.jsloader.GLOBAL_VERIFY_OBJS_ = 'closure_verification';
/**
* The default length of time, in milliseconds, we are prepared to wait for a
* load request to complete.
* @type {number}
*/
goog.net.jsloader.DEFAULT_TIMEOUT = 5000;
/**
* Optional parameters for goog.net.jsloader.send.
* timeout: The length of time, in milliseconds, we are prepared to wait
* for a load request to complete. Default it 5 seconds.
* document: The HTML document under which to load the JavaScript. Default is
* the current document.
* cleanupWhenDone: If true clean up the script tag after script completes to
* load. This is important if you just want to read data from the JavaScript
* and then throw it away. Default is false.
*
* @typedef {{
* timeout: (number|undefined),
* document: (HTMLDocument|undefined),
* cleanupWhenDone: (boolean|undefined)
* }}
*/
goog.net.jsloader.Options;
/**
* Scripts (URIs) waiting to be loaded.
* @type {Array.<string>}
* @private
*/
goog.net.jsloader.scriptsToLoad_ = [];
/**
* Loads and evaluates the JavaScript files at the specified URIs, guaranteeing
* the order of script loads.
*
* Because we have to load the scripts in serial (load script 1, exec script 1,
* load script 2, exec script 2, and so on), this will be slower than doing
* the network fetches in parallel.
*
* If you need to load a large number of scripts but dependency order doesn't
* matter, you should just call goog.net.jsloader.load N times.
*
* If you need to load a large number of scripts on the same domain,
* you may want to use goog.module.ModuleLoader.
*
* @param {Array.<string>} uris The URIs to load.
* @param {goog.net.jsloader.Options=} opt_options Optional parameters. See
* goog.net.jsloader.options documentation for details.
*/
goog.net.jsloader.loadMany = function(uris, opt_options) {
// Loading the scripts in serial introduces asynchronosity into the flow.
// Therefore, there are race conditions where client A can kick off the load
// sequence for client B, even though client A's scripts haven't all been
// loaded yet.
//
// To work around this issue, all module loads share a queue.
if (!uris.length) {
return;
}
var isAnotherModuleLoading = goog.net.jsloader.scriptsToLoad_.length;
goog.array.extend(goog.net.jsloader.scriptsToLoad_, uris);
if (isAnotherModuleLoading) {
// jsloader is still loading some other scripts.
// In order to prevent the race condition noted above, we just add
// these URIs to the end of the scripts' queue and return.
return;
}
uris = goog.net.jsloader.scriptsToLoad_;
var popAndLoadNextScript = function() {
var uri = uris.shift();
var deferred = goog.net.jsloader.load(uri, opt_options);
if (uris.length) {
deferred.addBoth(popAndLoadNextScript);
}
};
popAndLoadNextScript();
};
/**
* Loads and evaluates a JavaScript file.
* When the script loads, a user callback is called.
* It is the client's responsibility to verify that the script ran successfully.
*
* @param {string} uri The URI of the JavaScript.
* @param {goog.net.jsloader.Options=} opt_options Optional parameters. See
* goog.net.jsloader.Options documentation for details.
* @return {!goog.async.Deferred} The deferred result, that may be used to add
* callbacks and/or cancel the transmission.
* The error callback will be called with a single goog.net.jsloader.Error
* parameter.
*/
goog.net.jsloader.load = function(uri, opt_options) {
var options = opt_options || {};
var doc = options.document || document;
var script = goog.dom.createElement(goog.dom.TagName.SCRIPT);
var request = {script_: script, timeout_: undefined};
var deferred = new goog.async.Deferred(goog.net.jsloader.cancel_, request);
// Set a timeout.
var timeout = null;
var timeoutDuration = goog.isDefAndNotNull(options.timeout) ?
options.timeout : goog.net.jsloader.DEFAULT_TIMEOUT;
if (timeoutDuration > 0) {
timeout = window.setTimeout(function() {
goog.net.jsloader.cleanup_(script, true);
deferred.errback(new goog.net.jsloader.Error(
goog.net.jsloader.ErrorCode.TIMEOUT,
'Timeout reached for loading script ' + uri));
}, timeoutDuration);
request.timeout_ = timeout;
}
// Hang the user callback to be called when the script completes to load.
// NOTE(user): This callback will be called in IE even upon error. In any
// case it is the client's responsibility to verify that the script ran
// successfully.
script.onload = script.onreadystatechange = function() {
if (!script.readyState || script.readyState == 'loaded' ||
script.readyState == 'complete') {
var removeScriptNode = options.cleanupWhenDone || false;
goog.net.jsloader.cleanup_(script, removeScriptNode, timeout);
deferred.callback(null);
}
};
// Add an error callback.
// NOTE(user): Not supported in IE.
script.onerror = function() {
goog.net.jsloader.cleanup_(script, true, timeout);
deferred.errback(new goog.net.jsloader.Error(
goog.net.jsloader.ErrorCode.LOAD_ERROR,
'Error while loading script ' + uri));
};
// Add the script element to the document.
goog.dom.setProperties(script, {
'type': 'text/javascript',
'charset': 'UTF-8',
// NOTE(user): Safari never loads the script if we don't set
// the src attribute before appending.
'src': uri
});
var scriptParent = goog.net.jsloader.getScriptParentElement_(doc);
scriptParent.appendChild(script);
return deferred;
};
/**
* Loads a JavaScript file and verifies it was evaluated successfully, using a
* verification object.
* The verification object is set by the loaded JavaScript at the end of the
* script.
* We verify this object was set and return its value in the success callback.
* If the object is not defined we trigger an error callback.
*
* @param {string} uri The URI of the JavaScript.
* @param {string} verificationObjName The name of the verification object that
* the loaded script should set.
* @param {goog.net.jsloader.Options} options Optional parameters. See
* goog.net.jsloader.Options documentation for details.
* @return {!goog.async.Deferred} The deferred result, that may be used to add
* callbacks and/or cancel the transmission.
* The success callback will be called with a single parameter containing
* the value of the verification object.
* The error callback will be called with a single goog.net.jsloader.Error
* parameter.
*/
goog.net.jsloader.loadAndVerify = function(uri, verificationObjName, options) {
// Define the global objects variable.
if (!goog.global[goog.net.jsloader.GLOBAL_VERIFY_OBJS_]) {
goog.global[goog.net.jsloader.GLOBAL_VERIFY_OBJS_] = {};
}
var verifyObjs = goog.global[goog.net.jsloader.GLOBAL_VERIFY_OBJS_];
// Verify that the expected object does not exist yet.
if (goog.isDef(verifyObjs[verificationObjName])) {
// TODO(user): Error or reset variable?
return goog.async.Deferred.fail(new goog.net.jsloader.Error(
goog.net.jsloader.ErrorCode.VERIFY_OBJECT_ALREADY_EXISTS,
'Verification object ' + verificationObjName + ' already defined.'));
}
// Send request to load the JavaScript.
var sendDeferred = goog.net.jsloader.load(uri, options);
// Create a deferred object wrapping the send result.
var deferred = new goog.async.Deferred(
goog.bind(sendDeferred.cancel, sendDeferred));
// Call user back with object that was set by the script.
sendDeferred.addCallback(function() {
var result = verifyObjs[verificationObjName];
if (goog.isDef(result)) {
deferred.callback(result);
delete verifyObjs[verificationObjName];
} else {
// Error: script was not loaded properly.
deferred.errback(new goog.net.jsloader.Error(
goog.net.jsloader.ErrorCode.VERIFY_ERROR,
'Script ' + uri + ' loaded, but verification object ' +
verificationObjName + ' was not defined.'));
}
});
// Pass error to new deferred object.
sendDeferred.addErrback(function(error) {
if (goog.isDef(verifyObjs[verificationObjName])) {
delete verifyObjs[verificationObjName];
}
deferred.errback(error);
});
return deferred;
};
/**
* Gets the DOM element under which we should add new script elements.
* How? Take the first head element, and if not found take doc.documentElement,
* which always exists.
*
* @param {!HTMLDocument} doc The relevant document.
* @return {!Element} The script parent element.
* @private
*/
goog.net.jsloader.getScriptParentElement_ = function(doc) {
var headElements = doc.getElementsByTagName(goog.dom.TagName.HEAD);
if (!headElements || goog.array.isEmpty(headElements)) {
return doc.documentElement;
} else {
return headElements[0];
}
};
/**
* Cancels a given request.
* @this {{script_: Element, timeout_: number}} The request context.
* @private
*/
goog.net.jsloader.cancel_ = function() {
var request = this;
if (request && request.script_) {
var scriptNode = request.script_;
if (scriptNode && scriptNode.tagName == 'SCRIPT') {
goog.net.jsloader.cleanup_(scriptNode, true, request.timeout_);
}
}
};
/**
* Removes the script node and the timeout.
*
* @param {Node} scriptNode The node to be cleaned up.
* @param {boolean} removeScriptNode If true completely remove the script node.
* @param {?number=} opt_timeout The timeout handler to cleanup.
* @private
*/
goog.net.jsloader.cleanup_ = function(scriptNode, removeScriptNode,
opt_timeout) {
if (goog.isDefAndNotNull(opt_timeout)) {
goog.global.clearTimeout(opt_timeout);
}
scriptNode.onload = goog.nullFunction;
scriptNode.onerror = goog.nullFunction;
scriptNode.onreadystatechange = goog.nullFunction;
// Do this after a delay (removing the script node of a running script can
// confuse older IEs).
if (removeScriptNode) {
window.setTimeout(function() {
goog.dom.removeNode(scriptNode);
}, 0);
}
};
/**
* Possible error codes for jsloader.
* @enum {number}
*/
goog.net.jsloader.ErrorCode = {
LOAD_ERROR: 0,
TIMEOUT: 1,
VERIFY_ERROR: 2,
VERIFY_OBJECT_ALREADY_EXISTS: 3
};
/**
* A jsloader error.
*
* @param {goog.net.jsloader.ErrorCode} code The error code.
* @param {string=} opt_message Additional message.
* @constructor
* @extends {goog.debug.Error}
*/
goog.net.jsloader.Error = function(code, opt_message) {
var msg = 'Jsloader error (code #' + code + ')';
if (opt_message) {
msg += ': ' + opt_message;
}
goog.base(this, msg);
/**
* The code for this error.
*
* @type {goog.net.jsloader.ErrorCode}
*/
this.code = code;
};
goog.inherits(goog.net.jsloader.Error, goog.debug.Error);
@@ -0,0 +1,338 @@
// 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.
// The original file lives here: http://go/cross_domain_channel.js
/**
* @fileoverview Implements a cross-domain communication channel. A
* typical web page is prevented by browser security from sending
* request, such as a XMLHttpRequest, to other servers than the ones
* from which it came. The Jsonp class provides a workound, by
* using dynamically generated script tags. Typical usage:.
*
* var jsonp = new goog.net.Jsonp(new goog.Uri('http://my.host.com/servlet'));
* var payload = { 'foo': 1, 'bar': true };
* jsonp.send(payload, function(reply) { alert(reply) });
*
* This script works in all browsers that are currently supported by
* the Google Maps API, which is IE 6.0+, Firefox 0.8+, Safari 1.2.4+,
* Netscape 7.1+, Mozilla 1.4+, Opera 8.02+.
*
*/
goog.provide('goog.net.Jsonp');
goog.require('goog.Uri');
goog.require('goog.net.jsloader');
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
//
// This class allows us (Google) to send data from non-Google and thus
// UNTRUSTED pages to our servers. Under NO CIRCUMSTANCES return
// anything sensitive, such as session or cookie specific data. Return
// only data that you want parties external to Google to have. Also
// NEVER use this method to send data from web pages to untrusted
// servers, or redirects to unknown servers (www.google.com/cache,
// /q=xx&btnl, /url, www.googlepages.com, etc.)
//
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
/**
* Creates a new cross domain channel that sends data to the specified
* host URL. By default, if no reply arrives within 5s, the channel
* assumes the call failed to complete successfully.
*
* @param {goog.Uri|string} uri The Uri of the server side code that receives
* data posted through this channel (e.g.,
* "http://maps.google.com/maps/geo").
*
* @param {string=} opt_callbackParamName The parameter name that is used to
* specify the callback. Defaults to "callback".
*
* @constructor
*/
goog.net.Jsonp = function(uri, opt_callbackParamName) {
/**
* The uri_ object will be used to encode the payload that is sent to the
* server.
* @type {goog.Uri}
* @private
*/
this.uri_ = new goog.Uri(uri);
/**
* This is the callback parameter name that is added to the uri.
* @type {string}
* @private
*/
this.callbackParamName_ = opt_callbackParamName ?
opt_callbackParamName : 'callback';
/**
* The length of time, in milliseconds, this channel is prepared
* to wait for for a request to complete. The default value is 5 seconds.
* @type {number}
* @private
*/
this.timeout_ = 5000;
};
/**
* The name of the property of goog.global under which the callback is
* stored.
*/
goog.net.Jsonp.CALLBACKS = '_callbacks_';
/**
* Used to generate unique callback IDs. The counter must be global because
* all channels share a common callback object.
* @private
*/
goog.net.Jsonp.scriptCounter_ = 0;
/**
* Sets the length of time, in milliseconds, this channel is prepared
* to wait for for a request to complete. If the call is not competed
* within the set time span, it is assumed to have failed. To wait
* indefinitely for a request to complete set the timout to a negative
* number.
*
* @param {number} timeout The length of time before calls are
* interrupted.
*/
goog.net.Jsonp.prototype.setRequestTimeout = function(timeout) {
this.timeout_ = timeout;
};
/**
* Returns the current timeout value, in milliseconds.
*
* @return {number} The timeout value.
*/
goog.net.Jsonp.prototype.getRequestTimeout = function() {
return this.timeout_;
};
/**
* Sends the given payload to the URL specified at the construction
* time. The reply is delivered to the given replyCallback. If the
* errorCallback is specified and the reply does not arrive within the
* timeout period set on this channel, the errorCallback is invoked
* with the original payload.
*
* If no reply callback is specified, then the response is expected to
* consist of calls to globally registered functions. No &callback=
* URL parameter will be sent in the request, and the script element
* will be cleaned up after the timeout.
*
* @param {Object=} opt_payload Name-value pairs. If given, these will be
* added as parameters to the supplied URI as GET parameters to the
* given server URI.
*
* @param {Function=} opt_replyCallback A function expecting one
* argument, called when the reply arrives, with the response data.
*
* @param {Function=} opt_errorCallback A function expecting one
* argument, called on timeout, with the payload (if given), otherwise
* null.
*
* @param {string=} opt_callbackParamValue Value to be used as the
* parameter value for the callback parameter (callbackParamName).
* To be used when the value needs to be fixed by the client for a
* particular request, to make use of the cached responses for the request.
* NOTE: If multiple requests are made with the same
* opt_callbackParamValue, only the last call will work whenever the
* response comes back.
*
* @return {Object} A request descriptor that may be used to cancel this
* transmission, or null, if the message may not be cancelled.
*/
goog.net.Jsonp.prototype.send = function(opt_payload,
opt_replyCallback,
opt_errorCallback,
opt_callbackParamValue) {
var payload = opt_payload || null;
var id = opt_callbackParamValue ||
'_' + (goog.net.Jsonp.scriptCounter_++).toString(36) +
goog.now().toString(36);
if (!goog.global[goog.net.Jsonp.CALLBACKS]) {
goog.global[goog.net.Jsonp.CALLBACKS] = {};
}
// Create a new Uri object onto which this payload will be added
var uri = this.uri_.clone();
if (payload) {
goog.net.Jsonp.addPayloadToUri_(payload, uri);
}
if (opt_replyCallback) {
var reply = goog.net.Jsonp.newReplyHandler_(id, opt_replyCallback);
goog.global[goog.net.Jsonp.CALLBACKS][id] = reply;
uri.setParameterValues(this.callbackParamName_,
goog.net.Jsonp.CALLBACKS + '.' + id);
}
var deferred = goog.net.jsloader.load(uri.toString(),
{timeout: this.timeout_, cleanupWhenDone: true});
var error = goog.net.Jsonp.newErrorHandler_(id, payload, opt_errorCallback);
deferred.addErrback(error);
return {id_: id, deferred_: deferred};
};
/**
* Cancels a given request. The request must be exactly the object returned by
* the send method.
*
* @param {Object} request The request object returned by the send method.
*/
goog.net.Jsonp.prototype.cancel = function(request) {
if (request) {
if (request.deferred_) {
request.deferred_.cancel();
}
if (request.id_) {
goog.net.Jsonp.cleanup_(request.id_, false);
}
}
};
/**
* Creates a timeout callback that calls the given timeoutCallback with the
* original payload.
*
* @param {string} id The id of the script node.
* @param {Object} payload The payload that was sent to the server.
* @param {Function=} opt_errorCallback The function called on timeout.
* @return {!Function} A zero argument function that handles callback duties.
* @private
*/
goog.net.Jsonp.newErrorHandler_ = function(id,
payload,
opt_errorCallback) {
/**
* When we call across domains with a request, this function is the
* timeout handler. Once it's done executing the user-specified
* error-handler, it removes the script node and original function.
*/
return function() {
goog.net.Jsonp.cleanup_(id, false);
if (opt_errorCallback) {
opt_errorCallback(payload);
}
};
};
/**
* Creates a reply callback that calls the given replyCallback with data
* returned by the server.
*
* @param {string} id The id of the script node.
* @param {Function} replyCallback The function called on reply.
* @return {Function} A reply callback function.
* @private
*/
goog.net.Jsonp.newReplyHandler_ = function(id, replyCallback) {
/**
* This function is the handler for the all-is-well response. It
* clears the error timeout handler, calls the user's handler, then
* removes the script node and itself.
*
* @param {...Object} var_args The response data sent from the server.
*/
return function(var_args) {
goog.net.Jsonp.cleanup_(id, true);
replyCallback.apply(undefined, arguments);
};
};
/**
* Removes the script node and reply handler with the given id.
*
* @param {string} id The id of the script node to be removed.
* @param {boolean} deleteReplyHandler If true, delete the reply handler
* instead of setting it to nullFunction (if we know the callback could
* never be called again).
* @private
*/
goog.net.Jsonp.cleanup_ = function(id, deleteReplyHandler) {
if (goog.global[goog.net.Jsonp.CALLBACKS][id]) {
if (deleteReplyHandler) {
delete goog.global[goog.net.Jsonp.CALLBACKS][id];
} else {
// Removing the script tag doesn't necessarily prevent the script
// from firing, so we make the callback a noop.
goog.global[goog.net.Jsonp.CALLBACKS][id] = goog.nullFunction;
}
}
};
/**
* Returns URL encoded payload. The payload should be a map of name-value
* pairs, in the form {"foo": 1, "bar": true, ...}. If the map is empty,
* the URI will be unchanged.
*
* <p>The method uses hasOwnProperty() to assure the properties are on the
* object, not on its prototype.
*
* @param {!Object} payload A map of value name pairs to be encoded.
* A value may be specified as an array, in which case a query parameter
* will be created for each value, e.g.:
* {"foo": [1,2]} will encode to "foo=1&foo=2".
*
* @param {!goog.Uri} uri A Uri object onto which the payload key value pairs
* will be encoded.
*
* @return {!goog.Uri} A reference to the Uri sent as a parameter.
* @private
*/
goog.net.Jsonp.addPayloadToUri_ = function(payload, uri) {
for (var name in payload) {
// NOTE(user): Safari/1.3 doesn't have hasOwnProperty(). In that
// case, we iterate over all properties as a very lame workaround.
if (!payload.hasOwnProperty || payload.hasOwnProperty(name)) {
uri.setParameterValues(name, payload[name]);
}
}
return uri;
};
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
//
// This class allows us (Google) to send data from non-Google and thus
// UNTRUSTED pages to our servers. Under NO CIRCUMSTANCES return
// anything sensitive, such as session or cookie specific data. Return
// only data that you want parties external to Google to have. Also
// NEVER use this method to send data from web pages to untrusted
// servers, or redirects to unknown servers (www.google.com/cache,
// /q=xx&btnl, /url, www.googlepages.com, etc.)
//
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
@@ -0,0 +1,319 @@
// Copyright 2007 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 Mock of IframeIo for unit testing.
*/
goog.provide('goog.net.MockIFrameIo');
goog.require('goog.events.EventTarget');
goog.require('goog.json');
goog.require('goog.net.ErrorCode');
goog.require('goog.net.EventType');
goog.require('goog.net.IframeIo');
/**
* Mock implenetation of goog.net.IframeIo. This doesn't provide a mock
* implementation for all cases, but it's not too hard to add them as needed.
* @param {goog.testing.TestQueue} testQueue Test queue for inserting test
* events.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.net.MockIFrameIo = function(testQueue) {
goog.events.EventTarget.call(this);
/**
* Queue of events write to
* @type {goog.testing.TestQueue}
* @private
*/
this.testQueue_ = testQueue;
};
goog.inherits(goog.net.MockIFrameIo, goog.events.EventTarget);
/**
* Whether MockIFrameIo is active.
* @type {boolean}
* @private
*/
goog.net.MockIFrameIo.prototype.active_ = false;
/**
* Last content.
* @type {string}
* @private
*/
goog.net.MockIFrameIo.prototype.lastContent_ = '';
/**
* Last error code.
* @type {goog.net.ErrorCode}
* @private
*/
goog.net.MockIFrameIo.prototype.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR;
/**
* Last error message.
* @type {string}
* @private
*/
goog.net.MockIFrameIo.prototype.lastError_ = '';
/**
* Last custom error.
* @type {Object}
* @private
*/
goog.net.MockIFrameIo.prototype.lastCustomError_ = null;
/**
* Last URI.
* @type {goog.Uri}
* @private
*/
goog.net.MockIFrameIo.prototype.lastUri_ = null;
/**
* Simulates the iframe send.
*
* @param {goog.Uri|string} uri Uri of the request.
* @param {string=} opt_method Default is GET, POST uses a form to submit the
* request.
* @param {boolean=} opt_noCache Append a timestamp to the request to avoid
* caching.
* @param {Object|goog.structs.Map=} opt_data Map of key-value pairs.
*/
goog.net.MockIFrameIo.prototype.send = function(uri, opt_method, opt_noCache,
opt_data) {
if (this.active_) {
throw Error('[goog.net.IframeIo] Unable to send, already active.');
}
this.testQueue_.enqueue(['s', uri, opt_method, opt_noCache, opt_data]);
this.complete_ = false;
this.active_ = true;
};
/**
* Simulates the iframe send from a form.
* @param {Element} form Form element used to send the request to the server.
* @param {string=} opt_uri Uri to set for the destination of the request, by
* default the uri will come from the form.
* @param {boolean=} opt_noCache Append a timestamp to the request to avoid
* caching.
*/
goog.net.MockIFrameIo.prototype.sendFromForm = function(form, opt_uri,
opt_noCache) {
if (this.active_) {
throw Error('[goog.net.IframeIo] Unable to send, already active.');
}
this.testQueue_.enqueue(['s', form, opt_uri, opt_noCache]);
this.complete_ = false;
this.active_ = true;
};
/**
* Simulates aborting the current Iframe request.
* @param {goog.net.ErrorCode=} opt_failureCode Optional error code to use -
* defaults to ABORT.
*/
goog.net.MockIFrameIo.prototype.abort = function(opt_failureCode) {
if (this.active_) {
this.testQueue_.enqueue(['a', opt_failureCode]);
this.complete_ = false;
this.active_ = false;
this.success_ = false;
this.lastErrorCode_ = opt_failureCode || goog.net.ErrorCode.ABORT;
this.dispatchEvent(goog.net.EventType.ABORT);
this.simulateReady();
}
};
/**
* Simulates receive of incremental data.
* @param {Object} data Data.
*/
goog.net.MockIFrameIo.prototype.simulateIncrementalData = function(data) {
this.dispatchEvent(new goog.net.IframeIo.IncrementalDataEvent(data));
};
/**
* Simulates the iframe is done.
* @param {goog.net.ErrorCode} errorCode The error code for any error that
* should be simulated.
*/
goog.net.MockIFrameIo.prototype.simulateDone = function(errorCode) {
if (errorCode) {
this.success_ = false;
this.lastErrorCode_ = goog.net.ErrorCode.HTTP_ERROR;
this.lastError_ = this.getLastError();
this.dispatchEvent(goog.net.EventType.ERROR);
} else {
this.success_ = true;
this.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR;
this.dispatchEvent(goog.net.EventType.SUCCESS);
}
this.complete_ = true;
this.dispatchEvent(goog.net.EventType.COMPLETE);
};
/**
* Simulates the IFrame is ready for the next request.
*/
goog.net.MockIFrameIo.prototype.simulateReady = function() {
this.dispatchEvent(goog.net.EventType.READY);
};
/**
* @return {boolean} True if transfer is complete.
*/
goog.net.MockIFrameIo.prototype.isComplete = function() {
return this.complete_;
};
/**
* @return {boolean} True if transfer was successful.
*/
goog.net.MockIFrameIo.prototype.isSuccess = function() {
return this.success_;
};
/**
* @return {boolean} True if a transfer is in progress.
*/
goog.net.MockIFrameIo.prototype.isActive = function() {
return this.active_;
};
/**
* Returns the last response text (i.e. the text content of the iframe).
* Assumes plain text!
* @return {string} Result from the server.
*/
goog.net.MockIFrameIo.prototype.getResponseText = function() {
return this.lastContent_;
};
/**
* Parses the content as JSON. This is a safe parse and may throw an error
* if the response is malformed.
* @return {Object} The parsed content.
*/
goog.net.MockIFrameIo.prototype.getResponseJson = function() {
return goog.json.parse(this.lastContent_);
};
/**
* Get the uri of the last request.
* @return {goog.Uri} Uri of last request.
*/
goog.net.MockIFrameIo.prototype.getLastUri = function() {
return this.lastUri_;
};
/**
* Gets the last error code.
* @return {goog.net.ErrorCode} Last error code.
*/
goog.net.MockIFrameIo.prototype.getLastErrorCode = function() {
return this.lastErrorCode_;
};
/**
* Gets the last error message.
* @return {string} Last error message.
*/
goog.net.MockIFrameIo.prototype.getLastError = function() {
return goog.net.ErrorCode.getDebugMessage(this.lastErrorCode_);
};
/**
* Gets the last custom error.
* @return {Object} Last custom error.
*/
goog.net.MockIFrameIo.prototype.getLastCustomError = function() {
return this.lastCustomError_;
};
/**
* Sets the callback function used to check if a loaded IFrame is in an error
* state.
* @param {Function} fn Callback that expects a document object as it's single
* argument.
*/
goog.net.MockIFrameIo.prototype.setErrorChecker = function(fn) {
this.errorChecker_ = fn;
};
/**
* Gets the callback function used to check if a loaded IFrame is in an error
* state.
* @return {Function} A callback that expects a document object as it's single
* argument.
*/
goog.net.MockIFrameIo.prototype.getErrorChecker = function() {
return this.errorChecker_;
};
/**
* Returns the number of milliseconds after which an incomplete request will be
* aborted, or 0 if no timeout is set.
* @return {number} Timeout interval in milliseconds.
*/
goog.net.MockIFrameIo.prototype.getTimeoutInterval = function() {
return this.timeoutInterval_;
};
/**
* Sets the number of milliseconds after which an incomplete request will be
* aborted and a {@link goog.net.EventType.TIMEOUT} event raised; 0 means no
* timeout is set.
* @param {number} ms Timeout interval in milliseconds; 0 means none.
*/
goog.net.MockIFrameIo.prototype.setTimeoutInterval = function(ms) {
// TODO (pupius) - never used - doesn't look like timeouts were implemented
this.timeoutInterval_ = Math.max(0, ms);
};
@@ -0,0 +1,117 @@
// Copyright 2008 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 Class that can be used to determine when multiple iframes have
* been loaded. Refactored from static APIs in IframeLoadMonitor.
*/
goog.provide('goog.net.MultiIframeLoadMonitor');
goog.require('goog.events');
goog.require('goog.net.IframeLoadMonitor');
/**
* Provides a wrapper around IframeLoadMonitor, to allow the caller to wait for
* multiple iframes to load.
*
* @param {Array.<HTMLIFrameElement>} iframes Array of iframe elements to
* wait until they are loaded.
* @param {function():void} callback The callback to invoke once the frames have
* loaded.
* @param {boolean=} opt_hasContent true if the monitor should wait until the
* iframes have content (body.firstChild != null).
* @constructor
*/
goog.net.MultiIframeLoadMonitor = function(iframes, callback, opt_hasContent) {
/**
* Array of IframeLoadMonitors we use to track the loaded status of any
* currently unloaded iframes.
* @type {Array.<goog.net.IframeLoadMonitor>}
* @private
*/
this.pendingIframeLoadMonitors_ = [];
/**
* Callback which is invoked when all of the iframes are loaded.
* @type {function():void}
* @private
*/
this.callback_ = callback;
for (var i = 0; i < iframes.length; i++) {
var iframeLoadMonitor = new goog.net.IframeLoadMonitor(
iframes[i], opt_hasContent);
if (iframeLoadMonitor.isLoaded()) {
// Already loaded - don't need to wait
iframeLoadMonitor.dispose();
} else {
// Iframe isn't loaded yet - register to be notified when it is
// loaded, and track this monitor so we can dispose later as
// required.
this.pendingIframeLoadMonitors_.push(iframeLoadMonitor);
goog.events.listen(
iframeLoadMonitor, goog.net.IframeLoadMonitor.LOAD_EVENT, this);
}
}
if (!this.pendingIframeLoadMonitors_.length) {
// All frames were already loaded
this.callback_();
}
};
/**
* Handles a pending iframe load monitor load event.
* @param {goog.events.Event} e The goog.net.IframeLoadMonitor.LOAD_EVENT event.
*/
goog.net.MultiIframeLoadMonitor.prototype.handleEvent = function(e) {
var iframeLoadMonitor = e.target;
// iframeLoadMonitor is now loaded, remove it from the array of
// pending iframe load monitors.
for (var i = 0; i < this.pendingIframeLoadMonitors_.length; i++) {
if (this.pendingIframeLoadMonitors_[i] == iframeLoadMonitor) {
this.pendingIframeLoadMonitors_.splice(i, 1);
break;
}
}
// Disposes of the iframe load monitor. We created this iframe load monitor
// and installed the single listener on it, so it is safe to dispose it
// in the middle of this event handler.
iframeLoadMonitor.dispose();
// If there are no more pending iframe load monitors, all the iframes
// have loaded, and so we invoke the callback.
if (!this.pendingIframeLoadMonitors_.length) {
this.callback_();
}
};
/**
* Stops monitoring the iframes, cleaning up any associated resources. In
* general, the object cleans up its own resources before invoking the
* callback, so this API should only be used if the caller wants to stop the
* monitoring before the iframes are loaded (for example, if the caller is
* implementing a timeout).
*/
goog.net.MultiIframeLoadMonitor.prototype.stopMonitoring = function() {
for (var i = 0; i < this.pendingIframeLoadMonitors_.length; i++) {
this.pendingIframeLoadMonitors_[i].dispose();
}
this.pendingIframeLoadMonitors_.length = 0;
};
@@ -0,0 +1,47 @@
// Copyright 2012 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 Base class for objects monitoring and exposing runtime
* network status information.
*/
goog.provide('goog.net.NetworkStatusMonitor');
goog.require('goog.events.Listenable');
/**
* Base class for network status information providers.
* @interface
* @extends {goog.events.Listenable}
*/
goog.net.NetworkStatusMonitor = function() {};
/**
* Enum for the events dispatched by the OnlineHandler.
* @enum {string}
*/
goog.net.NetworkStatusMonitor.EventType = {
ONLINE: 'online',
OFFLINE: 'offline'
};
/**
* @return {boolean} Whether the system is online or otherwise.
*/
goog.net.NetworkStatusMonitor.prototype.isOnline;
@@ -0,0 +1,383 @@
// Copyright 2007 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 Definition of goog.net.NetworkTester.
*/
goog.provide('goog.net.NetworkTester');
goog.require('goog.Timer');
goog.require('goog.Uri');
goog.require('goog.log');
/**
* Creates an instance of goog.net.NetworkTester which can be used to test
* for internet connectivity by seeing if an image can be loaded from
* google.com. It can also be tested with other URLs.
* @param {Function} callback Callback that is called when the test completes.
* The callback takes a single boolean parameter. True indicates the URL
* was reachable, false indicates it wasn't.
* @param {Object=} opt_handler Handler object for the callback.
* @param {goog.Uri=} opt_uri URI to use for testing.
* @constructor
*/
goog.net.NetworkTester = function(callback, opt_handler, opt_uri) {
/**
* Callback that is called when the test completes.
* The callback takes a single boolean parameter. True indicates the URL was
* reachable, false indicates it wasn't.
* @type {Function}
* @private
*/
this.callback_ = callback;
/**
* Handler object for the callback.
* @type {Object|undefined}
* @private
*/
this.handler_ = opt_handler;
if (!opt_uri) {
// set the default URI to be based on the cleardot image at google.com
// We need to add a 'rand' to make sure the response is not fulfilled
// by browser cache. Use protocol-relative URLs to avoid insecure content
// warnings in IE.
opt_uri = new goog.Uri('//www.google.com/images/cleardot.gif');
opt_uri.makeUnique();
}
/**
* Uri to use for test. Defaults to using an image off of google.com
* @type {goog.Uri}
* @private
*/
this.uri_ = opt_uri;
};
/**
* Default timeout
* @type {number}
*/
goog.net.NetworkTester.DEFAULT_TIMEOUT_MS = 10000;
/**
* Logger object
* @type {goog.log.Logger}
* @private
*/
goog.net.NetworkTester.prototype.logger_ =
goog.log.getLogger('goog.net.NetworkTester');
/**
* Timeout for test
* @type {number}
* @private
*/
goog.net.NetworkTester.prototype.timeoutMs_ =
goog.net.NetworkTester.DEFAULT_TIMEOUT_MS;
/**
* Whether we've already started running.
* @type {boolean}
* @private
*/
goog.net.NetworkTester.prototype.running_ = false;
/**
* Number of retries to attempt
* @type {number}
* @private
*/
goog.net.NetworkTester.prototype.retries_ = 0;
/**
* Attempt number we're on
* @type {number}
* @private
*/
goog.net.NetworkTester.prototype.attempt_ = 0;
/**
* Pause between retries in milliseconds.
* @type {number}
* @private
*/
goog.net.NetworkTester.prototype.pauseBetweenRetriesMs_ = 0;
/**
* Timer for timeouts.
* @type {?number}
* @private
*/
goog.net.NetworkTester.prototype.timeoutTimer_ = null;
/**
* Timer for pauses between retries.
* @type {?number}
* @private
*/
goog.net.NetworkTester.prototype.pauseTimer_ = null;
/**
* Returns the timeout in milliseconds.
* @return {number} Timeout in milliseconds.
*/
goog.net.NetworkTester.prototype.getTimeout = function() {
return this.timeoutMs_;
};
/**
* Sets the timeout in milliseconds.
* @param {number} timeoutMs Timeout in milliseconds.
*/
goog.net.NetworkTester.prototype.setTimeout = function(timeoutMs) {
this.timeoutMs_ = timeoutMs;
};
/**
* Returns the numer of retries to attempt.
* @return {number} Number of retries to attempt.
*/
goog.net.NetworkTester.prototype.getNumRetries = function() {
return this.retries_;
};
/**
* Sets the timeout in milliseconds.
* @param {number} retries Number of retries to attempt.
*/
goog.net.NetworkTester.prototype.setNumRetries = function(retries) {
this.retries_ = retries;
};
/**
* Returns the pause between retries in milliseconds.
* @return {number} Pause between retries in milliseconds.
*/
goog.net.NetworkTester.prototype.getPauseBetweenRetries = function() {
return this.pauseBetweenRetriesMs_;
};
/**
* Sets the pause between retries in milliseconds.
* @param {number} pauseMs Pause between retries in milliseconds.
*/
goog.net.NetworkTester.prototype.setPauseBetweenRetries = function(pauseMs) {
this.pauseBetweenRetriesMs_ = pauseMs;
};
/**
* Returns the uri to use for the test.
* @return {goog.Uri} The uri for the test.
*/
goog.net.NetworkTester.prototype.getUri = function() {
return this.uri_;
};
/**
* Sets the uri to use for the test.
* @param {goog.Uri} uri The uri for the test.
*/
goog.net.NetworkTester.prototype.setUri = function(uri) {
this.uri_ = uri;
};
/**
* Returns whether the tester is currently running.
* @return {boolean} True if it's running, false if it's not running.
*/
goog.net.NetworkTester.prototype.isRunning = function() {
return this.running_;
};
/**
* Starts the process of testing the network.
*/
goog.net.NetworkTester.prototype.start = function() {
if (this.running_) {
throw Error('NetworkTester.start called when already running');
}
this.running_ = true;
goog.log.info(this.logger_, 'Starting');
this.attempt_ = 0;
this.startNextAttempt_();
};
/**
* Stops the testing of the network. This is a noop if not running.
*/
goog.net.NetworkTester.prototype.stop = function() {
this.cleanupCallbacks_();
this.running_ = false;
};
/**
* Starts the next attempt to load an image.
* @private
*/
goog.net.NetworkTester.prototype.startNextAttempt_ = function() {
this.attempt_++;
if (goog.net.NetworkTester.getNavigatorOffline_()) {
goog.log.info(this.logger_, 'Browser is set to work offline.');
// Call in a timeout to make async like the rest.
goog.Timer.callOnce(goog.bind(this.onResult, this, false), 0);
} else {
goog.log.info(this.logger_, 'Loading image (attempt ' + this.attempt_ +
') at ' + this.uri_);
this.image_ = new Image();
this.image_.onload = goog.bind(this.onImageLoad_, this);
this.image_.onerror = goog.bind(this.onImageError_, this);
this.image_.onabort = goog.bind(this.onImageAbort_, this);
this.timeoutTimer_ = goog.Timer.callOnce(this.onImageTimeout_,
this.timeoutMs_, this);
this.image_.src = String(this.uri_);
}
};
/**
* @return {boolean} Whether navigator.onLine returns false.
* @private
*/
goog.net.NetworkTester.getNavigatorOffline_ = function() {
return 'onLine' in navigator && !navigator.onLine;
};
/**
* Callback for the image successfully loading.
* @private
*/
goog.net.NetworkTester.prototype.onImageLoad_ = function() {
goog.log.info(this.logger_, 'Image loaded');
this.onResult(true);
};
/**
* Callback for the image failing to load.
* @private
*/
goog.net.NetworkTester.prototype.onImageError_ = function() {
goog.log.info(this.logger_, 'Image load error');
this.onResult(false);
};
/**
* Callback for the image load being aborted.
* @private
*/
goog.net.NetworkTester.prototype.onImageAbort_ = function() {
goog.log.info(this.logger_, 'Image load aborted');
this.onResult(false);
};
/**
* Callback for the image load timing out.
* @private
*/
goog.net.NetworkTester.prototype.onImageTimeout_ = function() {
goog.log.info(this.logger_, 'Image load timed out');
this.onResult(false);
};
/**
* Handles a successful or failed result.
* @param {boolean} succeeded Whether the image load succeeded.
*/
goog.net.NetworkTester.prototype.onResult = function(succeeded) {
this.cleanupCallbacks_();
if (succeeded) {
this.running_ = false;
this.callback_.call(this.handler_, true);
} else {
if (this.attempt_ <= this.retries_) {
if (this.pauseBetweenRetriesMs_) {
this.pauseTimer_ = goog.Timer.callOnce(this.onPauseFinished_,
this.pauseBetweenRetriesMs_, this);
} else {
this.startNextAttempt_();
}
} else {
this.running_ = false;
this.callback_.call(this.handler_, false);
}
}
};
/**
* Callback for the pause between retry timer.
* @private
*/
goog.net.NetworkTester.prototype.onPauseFinished_ = function() {
this.pauseTimer_ = null;
this.startNextAttempt_();
};
/**
* Cleans up the handlers and timer associated with the image.
* @private
*/
goog.net.NetworkTester.prototype.cleanupCallbacks_ = function() {
// clear handlers to avoid memory leaks
// NOTE(user): Nullified individually to avoid compiler warnings
// (BUG 658126)
if (this.image_) {
this.image_.onload = null;
this.image_.onerror = null;
this.image_.onabort = null;
this.image_ = null;
}
if (this.timeoutTimer_) {
goog.Timer.clear(this.timeoutTimer_);
this.timeoutTimer_ = null;
}
if (this.pauseTimer_) {
goog.Timer.clear(this.pauseTimer_);
this.pauseTimer_ = null;
}
};
@@ -0,0 +1,23 @@
// Copyright 2011 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.
// All Rights Reserved
/**
* @fileoverview Test #1 of jsloader.
*/
goog.provide('goog.net.testdata.jsloader_test1');
goog.setTestOnly('jsloader_test1');
window['test1'] = 'Test #1 loaded';
@@ -0,0 +1,23 @@
// Copyright 2011 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.
// All Rights Reserved
/**
* @fileoverview Test #2 of jsloader.
*/
goog.provide('goog.net.testdata.jsloader_test2');
goog.setTestOnly('jsloader_test2');
window['closure_verification']['test2'] = 'Test #2 loaded';
@@ -0,0 +1,23 @@
// Copyright 2011 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.
// All Rights Reserved
/**
* @fileoverview Test #3 of jsloader.
*/
goog.provide('goog.net.testdata.jsloader_test3');
goog.setTestOnly('jsloader_test3');
window['test3Callback']('Test #3 loaded');
@@ -0,0 +1,23 @@
// Copyright 2011 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.
// All Rights Reserved
/**
* @fileoverview Test #4 of jsloader.
*/
goog.provide('goog.net.testdata.jsloader_test4');
goog.setTestOnly('jsloader_test4');
window['test4Callback']('Test #4 loaded');
@@ -0,0 +1,164 @@
// 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 tmpnetwork.js contains some temporary networking functions
* for browserchannel which will be moved at a later date.
*/
/**
* Namespace for BrowserChannel
*/
goog.provide('goog.net.tmpnetwork');
goog.require('goog.Uri');
goog.require('goog.net.ChannelDebug');
/**
* Default timeout to allow for google.com pings.
* @type {number}
*/
goog.net.tmpnetwork.GOOGLECOM_TIMEOUT = 10000;
/**
* Pings the network to check if an error is a server error or user's network
* error.
*
* @param {Function} callback The function to call back with results.
* @param {goog.Uri?=} opt_imageUri The URI of an image to use for the network
* test. You *must* provide an image URI; the default behavior is provided
* for compatibility with existing code, but the search team does not want
* people using images served off of google.com for this purpose. The
* default will go away when all usages have been changed.
*/
goog.net.tmpnetwork.testGoogleCom = function(callback, opt_imageUri) {
// We need to add a 'rand' to make sure the response is not fulfilled
// by browser cache.
var uri = opt_imageUri;
if (!uri) {
uri = new goog.Uri('//www.google.com/images/cleardot.gif');
uri.makeUnique();
}
goog.net.tmpnetwork.testLoadImage(uri.toString(),
goog.net.tmpnetwork.GOOGLECOM_TIMEOUT, callback);
};
/**
* Test loading the given image, retrying if necessary.
* @param {string} url URL to the iamge.
* @param {number} timeout Milliseconds before giving up.
* @param {Function} callback Function to call with results.
* @param {number} retries The number of times to retry.
* @param {number=} opt_pauseBetweenRetriesMS Optional number of milliseconds
* between retries - defaults to 0.
*/
goog.net.tmpnetwork.testLoadImageWithRetries = function(url, timeout, callback,
retries, opt_pauseBetweenRetriesMS) {
var channelDebug = new goog.net.ChannelDebug();
channelDebug.debug('TestLoadImageWithRetries: ' + opt_pauseBetweenRetriesMS);
if (retries == 0) {
// no more retries, give up
callback(false);
return;
}
var pauseBetweenRetries = opt_pauseBetweenRetriesMS || 0;
retries--;
goog.net.tmpnetwork.testLoadImage(url, timeout, function(succeeded) {
if (succeeded) {
callback(true);
} else {
// try again
goog.global.setTimeout(function() {
goog.net.tmpnetwork.testLoadImageWithRetries(url, timeout, callback,
retries, pauseBetweenRetries);
}, pauseBetweenRetries);
}
});
};
/**
* Test loading the given image.
* @param {string} url URL to the iamge.
* @param {number} timeout Milliseconds before giving up.
* @param {Function} callback Function to call with results.
*/
goog.net.tmpnetwork.testLoadImage = function(url, timeout, callback) {
var channelDebug = new goog.net.ChannelDebug();
channelDebug.debug('TestLoadImage: loading ' + url);
var img = new Image();
img.onload = function() {
try {
channelDebug.debug('TestLoadImage: loaded');
goog.net.tmpnetwork.clearImageCallbacks_(img);
callback(true);
} catch (e) {
channelDebug.dumpException(e);
}
};
img.onerror = function() {
try {
channelDebug.debug('TestLoadImage: error');
goog.net.tmpnetwork.clearImageCallbacks_(img);
callback(false);
} catch (e) {
channelDebug.dumpException(e);
}
};
img.onabort = function() {
try {
channelDebug.debug('TestLoadImage: abort');
goog.net.tmpnetwork.clearImageCallbacks_(img);
callback(false);
} catch (e) {
channelDebug.dumpException(e);
}
};
img.ontimeout = function() {
try {
channelDebug.debug('TestLoadImage: timeout');
goog.net.tmpnetwork.clearImageCallbacks_(img);
callback(false);
} catch (e) {
channelDebug.dumpException(e);
}
};
goog.global.setTimeout(function() {
if (img.ontimeout) {
img.ontimeout();
}
}, timeout);
img.src = url;
};
/**
* Clear handlers to avoid memory leaks.
* @param {Image} img The image to clear handlers from.
* @private
*/
goog.net.tmpnetwork.clearImageCallbacks_ = function(img) {
// NOTE(user): Nullified individually to avoid compiler warnings
// (BUG 658126)
img.onload = null;
img.onerror = null;
img.onabort = null;
img.ontimeout = null;
};
@@ -0,0 +1,505 @@
// Copyright 2011 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 Definition of the WebSocket class. A WebSocket provides a
* bi-directional, full-duplex communications channel, over a single TCP socket.
*
* See http://dev.w3.org/html5/websockets/
* for the full HTML5 WebSocket API.
*
* Typical usage will look like this:
*
* var ws = new goog.net.WebSocket();
*
* var handler = new goog.events.EventHandler();
* handler.listen(ws, goog.net.WebSocket.EventType.OPENED, onOpen);
* handler.listen(ws, goog.net.WebSocket.EventType.MESSAGE, onMessage);
*
* try {
* ws.open('ws://127.0.0.1:4200');
* } catch (e) {
* ...
* }
*
*/
goog.provide('goog.net.WebSocket');
goog.provide('goog.net.WebSocket.ErrorEvent');
goog.provide('goog.net.WebSocket.EventType');
goog.provide('goog.net.WebSocket.MessageEvent');
goog.require('goog.Timer');
goog.require('goog.asserts');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.log');
/**
* Class encapsulating the logic for using a WebSocket.
*
* @param {boolean=} opt_autoReconnect True if the web socket should
* automatically reconnect or not. This is true by default.
* @param {function(number):number=} opt_getNextReconnect A function for
* obtaining the time until the next reconnect attempt. Given the reconnect
* attempt count (which is a positive integer), the function should return a
* positive integer representing the milliseconds to the next reconnect
* attempt. The default function used is an exponential back-off. Note that
* this function is never called if auto reconnect is disabled.
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.net.WebSocket = function(opt_autoReconnect, opt_getNextReconnect) {
goog.base(this);
/**
* True if the web socket should automatically reconnect or not.
* @type {boolean}
* @private
*/
this.autoReconnect_ = goog.isDef(opt_autoReconnect) ?
opt_autoReconnect : true;
/**
* A function for obtaining the time until the next reconnect attempt.
* Given the reconnect attempt count (which is a positive integer), the
* function should return a positive integer representing the milliseconds to
* the next reconnect attempt.
* @type {function(number):number}
* @private
*/
this.getNextReconnect_ = opt_getNextReconnect ||
goog.net.WebSocket.EXPONENTIAL_BACKOFF_;
/**
* The time, in milliseconds, that must elapse before the next attempt to
* reconnect.
* @type {number}
* @private
*/
this.nextReconnect_ = this.getNextReconnect_(this.reconnectAttempt_);
};
goog.inherits(goog.net.WebSocket, goog.events.EventTarget);
/**
* The actual web socket that will be used to send/receive messages.
* @type {WebSocket}
* @private
*/
goog.net.WebSocket.prototype.webSocket_ = null;
/**
* The URL to which the web socket will connect.
* @type {?string}
* @private
*/
goog.net.WebSocket.prototype.url_ = null;
/**
* The subprotocol name used when establishing the web socket connection.
* @type {string|undefined}
* @private
*/
goog.net.WebSocket.prototype.protocol_ = undefined;
/**
* True if a call to the close callback is expected or not.
* @type {boolean}
* @private
*/
goog.net.WebSocket.prototype.closeExpected_ = false;
/**
* Keeps track of the number of reconnect attempts made since the last
* successful connection.
* @type {number}
* @private
*/
goog.net.WebSocket.prototype.reconnectAttempt_ = 0;
/**
* The logger for this class.
* @type {goog.log.Logger}
* @private
*/
goog.net.WebSocket.prototype.logger_ = goog.log.getLogger(
'goog.net.WebSocket');
/**
* The events fired by the web socket.
* @enum {string} The event types for the web socket.
*/
goog.net.WebSocket.EventType = {
/**
* Fired when an attempt to open the WebSocket fails or there is a connection
* failure after a successful connection has been established.
*/
CLOSED: goog.events.getUniqueId('closed'),
/**
* Fired when the WebSocket encounters an error.
*/
ERROR: goog.events.getUniqueId('error'),
/**
* Fired when a new message arrives from the WebSocket.
*/
MESSAGE: goog.events.getUniqueId('message'),
/**
* Fired when the WebSocket connection has been established.
*/
OPENED: goog.events.getUniqueId('opened')
};
/**
* The various states of the web socket.
* @enum {number} The states of the web socket.
* @private
*/
goog.net.WebSocket.ReadyState_ = {
// This is the initial state during construction.
CONNECTING: 0,
// This is when the socket is actually open and ready for data.
OPEN: 1,
// This is when the socket is in the middle of a close handshake.
// Note that this is a valid state even if the OPEN state was never achieved.
CLOSING: 2,
// This is when the socket is actually closed.
CLOSED: 3
};
/**
* The maximum amount of time between reconnect attempts for the exponential
* back-off in milliseconds.
* @type {number}
* @private
*/
goog.net.WebSocket.EXPONENTIAL_BACKOFF_CEILING_ = 60 * 1000;
/**
* Computes the next reconnect time given the number of reconnect attempts since
* the last successful connection.
*
* @param {number} attempt The number of reconnect attempts since the last
* connection.
* @return {number} The time, in milliseconds, until the next reconnect attempt.
* @const
* @private
*/
goog.net.WebSocket.EXPONENTIAL_BACKOFF_ = function(attempt) {
var time = Math.pow(2, attempt) * 1000;
return Math.min(time, goog.net.WebSocket.EXPONENTIAL_BACKOFF_CEILING_);
};
/**
* Installs exception protection for all entry points introduced by
* goog.net.WebSocket instances which are not protected by
* {@link goog.debug.ErrorHandler#protectWindowSetTimeout},
* {@link goog.debug.ErrorHandler#protectWindowSetInterval}, or
* {@link goog.events.protectBrowserEventEntryPoint}.
*
* @param {!goog.debug.ErrorHandler} errorHandler Error handler with which to
* protect the entry points.
*/
goog.net.WebSocket.protectEntryPoints = function(errorHandler) {
goog.net.WebSocket.prototype.onOpen_ = errorHandler.protectEntryPoint(
goog.net.WebSocket.prototype.onOpen_);
goog.net.WebSocket.prototype.onClose_ = errorHandler.protectEntryPoint(
goog.net.WebSocket.prototype.onClose_);
goog.net.WebSocket.prototype.onMessage_ = errorHandler.protectEntryPoint(
goog.net.WebSocket.prototype.onMessage_);
goog.net.WebSocket.prototype.onError_ = errorHandler.protectEntryPoint(
goog.net.WebSocket.prototype.onError_);
};
/**
* Creates and opens the actual WebSocket. Only call this after attaching the
* appropriate listeners to this object. If listeners aren't registered, then
* the {@code goog.net.WebSocket.EventType.OPENED} event might be missed.
*
* @param {string} url The URL to which to connect.
* @param {string=} opt_protocol The subprotocol to use. The connection will
* only be established if the server reports that it has selected this
* subprotocol. The subprotocol name must all be a non-empty ASCII string
* with no control characters and no spaces in them (i.e. only characters
* in the range U+0021 to U+007E).
*/
goog.net.WebSocket.prototype.open = function(url, opt_protocol) {
// Sanity check. This works only in modern browsers.
goog.asserts.assert(goog.global['WebSocket'],
'This browser does not support WebSocket');
// Don't do anything if the web socket is already open.
goog.asserts.assert(!this.isOpen(), 'The WebSocket is already open');
// Clear any pending attempts to reconnect.
this.clearReconnectTimer_();
// Construct the web socket.
this.url_ = url;
this.protocol_ = opt_protocol;
// This check has to be made otherwise you get protocol mismatch exceptions
// for passing undefined, null, '', or [].
if (this.protocol_) {
goog.log.info(this.logger_, 'Opening the WebSocket on ' + this.url_ +
' with protocol ' + this.protocol_);
this.webSocket_ = new WebSocket(this.url_, this.protocol_);
} else {
goog.log.info(this.logger_, 'Opening the WebSocket on ' + this.url_);
this.webSocket_ = new WebSocket(this.url_);
}
// Register the event handlers. Note that it is not possible for these
// callbacks to be missed because it is registered after the web socket is
// instantiated. Because of the synchronous nature of JavaScript, this code
// will execute before the browser creates the resource and makes any calls
// to these callbacks.
this.webSocket_.onopen = goog.bind(this.onOpen_, this);
this.webSocket_.onclose = goog.bind(this.onClose_, this);
this.webSocket_.onmessage = goog.bind(this.onMessage_, this);
this.webSocket_.onerror = goog.bind(this.onError_, this);
};
/**
* Closes the web socket connection.
*/
goog.net.WebSocket.prototype.close = function() {
// Clear any pending attempts to reconnect.
this.clearReconnectTimer_();
// Attempt to close only if the web socket was created.
if (this.webSocket_) {
goog.log.info(this.logger_, 'Closing the WebSocket.');
// Close is expected here since it was a direct call. Close is considered
// unexpected when opening the connection fails or there is some other form
// of connection loss after being connected.
this.closeExpected_ = true;
this.webSocket_.close();
this.webSocket_ = null;
}
};
/**
* Sends the message over the web socket.
*
* @param {string} message The message to send.
*/
goog.net.WebSocket.prototype.send = function(message) {
// Make sure the socket is ready to go before sending a message.
goog.asserts.assert(this.isOpen(), 'Cannot send without an open socket');
// Send the message and let onError_ be called if it fails thereafter.
this.webSocket_.send(message);
};
/**
* Checks to see if the web socket is open or not.
*
* @return {boolean} True if the web socket is open, false otherwise.
*/
goog.net.WebSocket.prototype.isOpen = function() {
return !!this.webSocket_ &&
this.webSocket_.readyState == goog.net.WebSocket.ReadyState_.OPEN;
};
/**
* Called when the web socket has connected.
*
* @private
*/
goog.net.WebSocket.prototype.onOpen_ = function() {
goog.log.info(this.logger_, 'WebSocket opened on ' + this.url_);
this.dispatchEvent(goog.net.WebSocket.EventType.OPENED);
// Set the next reconnect interval.
this.reconnectAttempt_ = 0;
this.nextReconnect_ = this.getNextReconnect_(this.reconnectAttempt_);
};
/**
* Called when the web socket has closed.
*
* @param {!Event} event The close event.
* @private
*/
goog.net.WebSocket.prototype.onClose_ = function(event) {
goog.log.info(this.logger_, 'The WebSocket on ' + this.url_ + ' closed.');
// Firing this event allows handlers to query the URL.
this.dispatchEvent(goog.net.WebSocket.EventType.CLOSED);
// Always clear out the web socket on a close event.
this.webSocket_ = null;
// See if this is an expected call to onClose_.
if (this.closeExpected_) {
goog.log.info(this.logger_, 'The WebSocket closed normally.');
// Only clear out the URL if this is a normal close.
this.url_ = null;
this.protocol_ = undefined;
} else {
// Unexpected, so try to reconnect.
goog.log.error(this.logger_, 'The WebSocket disconnected unexpectedly: ' +
event.data);
// Only try to reconnect if it is enabled.
if (this.autoReconnect_) {
// Log the reconnect attempt.
var seconds = Math.floor(this.nextReconnect_ / 1000);
goog.log.info(this.logger_,
'Seconds until next reconnect attempt: ' + seconds);
// Actually schedule the timer.
this.reconnectTimer_ = goog.Timer.callOnce(
goog.bind(this.open, this, this.url_, this.protocol_),
this.nextReconnect_, this);
// Set the next reconnect interval.
this.reconnectAttempt_++;
this.nextReconnect_ = this.getNextReconnect_(this.reconnectAttempt_);
}
}
this.closeExpected_ = false;
};
/**
* Called when a new message arrives from the server.
*
* @param {MessageEvent} event The web socket message event.
* @private
*/
goog.net.WebSocket.prototype.onMessage_ = function(event) {
var message = /** @type {string} */ (event.data);
this.dispatchEvent(new goog.net.WebSocket.MessageEvent(message));
};
/**
* Called when there is any error in communication.
*
* @param {Event} event The error event containing the error data.
* @private
*/
goog.net.WebSocket.prototype.onError_ = function(event) {
var data = /** @type {string} */ (event.data);
goog.log.error(this.logger_, 'An error occurred: ' + data);
this.dispatchEvent(new goog.net.WebSocket.ErrorEvent(data));
};
/**
* Clears the reconnect timer.
*
* @private
*/
goog.net.WebSocket.prototype.clearReconnectTimer_ = function() {
if (goog.isDefAndNotNull(this.reconnectTimer_)) {
goog.Timer.clear(this.reconnectTimer_);
}
this.reconnectTimer_ = null;
};
/** @override */
goog.net.WebSocket.prototype.disposeInternal = function() {
goog.base(this, 'disposeInternal');
this.close();
};
/**
* Object representing a new incoming message event.
*
* @param {string} message The raw message coming from the web socket.
* @extends {goog.events.Event}
* @constructor
*/
goog.net.WebSocket.MessageEvent = function(message) {
goog.base(this, goog.net.WebSocket.EventType.MESSAGE);
/**
* The new message from the web socket.
* @type {string}
*/
this.message = message;
};
goog.inherits(goog.net.WebSocket.MessageEvent, goog.events.Event);
/**
* Object representing an error event. This is fired whenever an error occurs
* on the web socket.
*
* @param {string} data The error data.
* @extends {goog.events.Event}
* @constructor
*/
goog.net.WebSocket.ErrorEvent = function(data) {
goog.base(this, goog.net.WebSocket.EventType.ERROR);
/**
* The error data coming from the web socket.
* @type {string}
*/
this.data = data;
};
goog.inherits(goog.net.WebSocket.ErrorEvent, goog.events.Event);
// Register the WebSocket as an entry point, so that it can be monitored for
// exception handling, etc.
goog.debug.entryPointRegistry.register(
/**
* @param {function(!Function): !Function} transformer The transforming
* function.
*/
function(transformer) {
goog.net.WebSocket.prototype.onOpen_ =
transformer(goog.net.WebSocket.prototype.onOpen_);
goog.net.WebSocket.prototype.onClose_ =
transformer(goog.net.WebSocket.prototype.onClose_);
goog.net.WebSocket.prototype.onMessage_ =
transformer(goog.net.WebSocket.prototype.onMessage_);
goog.net.WebSocket.prototype.onError_ =
transformer(goog.net.WebSocket.prototype.onError_);
});
@@ -0,0 +1,68 @@
// Copyright 2010 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 Implementation of XmlHttpFactory which allows construction from
* simple factory methods.
* @author dbk@google.com (David Barrett-Kahn)
*/
goog.provide('goog.net.WrapperXmlHttpFactory');
goog.require('goog.net.XmlHttpFactory');
/**
* An xhr factory subclass which can be constructed using two factory methods.
* This exists partly to allow the preservation of goog.net.XmlHttp.setFactory()
* with an unchanged signature.
* @param {function() : !(XMLHttpRequest|GearsHttpRequest)} xhrFactory A
* function which returns a new XHR object.
* @param {function() : !Object} optionsFactory A function which returns the
* options associated with xhr objects from this factory.
* @extends {goog.net.XmlHttpFactory}
* @constructor
*/
goog.net.WrapperXmlHttpFactory = function(xhrFactory, optionsFactory) {
goog.net.XmlHttpFactory.call(this);
/**
* XHR factory method.
* @type {function() : !(XMLHttpRequest|GearsHttpRequest)}
* @private
*/
this.xhrFactory_ = xhrFactory;
/**
* Options factory method.
* @type {function() : !Object}
* @private
*/
this.optionsFactory_ = optionsFactory;
};
goog.inherits(goog.net.WrapperXmlHttpFactory, goog.net.XmlHttpFactory);
/** @override */
goog.net.WrapperXmlHttpFactory.prototype.createInstance = function() {
return this.xhrFactory_();
};
/** @override */
goog.net.WrapperXmlHttpFactory.prototype.getOptions = function() {
return this.optionsFactory_();
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,80 @@
// 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 Creates a pool of XhrIo objects to use. This allows multiple
* XhrIo objects to be grouped together and requests will use next available
* XhrIo object.
*
*/
goog.provide('goog.net.XhrIoPool');
goog.require('goog.net.XhrIo');
goog.require('goog.structs');
goog.require('goog.structs.PriorityPool');
/**
* A pool of XhrIo objects.
* @param {goog.structs.Map=} opt_headers Map of default headers to add to every
* request.
* @param {number=} opt_minCount Minimum number of objects (Default: 1).
* @param {number=} opt_maxCount Maximum number of objects (Default: 10).
* @constructor
* @extends {goog.structs.PriorityPool}
*/
goog.net.XhrIoPool = function(opt_headers, opt_minCount, opt_maxCount) {
goog.structs.PriorityPool.call(this, opt_minCount, opt_maxCount);
/**
* Map of default headers to add to every request.
* @type {goog.structs.Map|undefined}
* @private
*/
this.headers_ = opt_headers;
};
goog.inherits(goog.net.XhrIoPool, goog.structs.PriorityPool);
/**
* Creates an instance of an XhrIo object to use in the pool.
* @return {goog.net.XhrIo} The created object.
* @override
*/
goog.net.XhrIoPool.prototype.createObject = function() {
var xhrIo = new goog.net.XhrIo();
var headers = this.headers_;
if (headers) {
goog.structs.forEach(headers, function(value, key) {
xhrIo.headers.set(key, value);
});
}
return xhrIo;
};
/**
* Determine if an object has become unusable and should not be used.
* @param {Object} obj The object to test.
* @return {boolean} Whether the object can be reused, which is true if the
* object is not disposed and not active.
* @override
*/
goog.net.XhrIoPool.prototype.objectCanBeReused = function(obj) {
// An active XhrIo object should never be used.
var xhr = /** @type {goog.net.XhrIo} */ (obj);
return !xhr.isDisposed() && !xhr.isActive();
};
@@ -0,0 +1,787 @@
// 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 Manages a pool of XhrIo's. This handles all the details of
* dealing with the XhrPool and provides a simple interface for sending requests
* and managing events.
*
* This class supports queueing & prioritization of requests (XhrIoPool
* handles this) and retrying of requests.
*
* The events fired by the XhrManager are an aggregation of the events of
* each of its XhrIo objects (with some filtering, i.e., ERROR only called
* when there are no more retries left). For this reason, all send requests have
* to have an id, so that the user of this object can know which event is for
* which request.
*
*/
goog.provide('goog.net.XhrManager');
goog.provide('goog.net.XhrManager.Event');
goog.provide('goog.net.XhrManager.Request');
goog.require('goog.Disposable');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.net.ErrorCode');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XhrIoPool');
goog.require('goog.structs');
goog.require('goog.structs.Map');
// TODO(user): Add some time in between retries.
/**
* A manager of an XhrIoPool.
* @param {number=} opt_maxRetries Max. number of retries (Default: 1).
* @param {goog.structs.Map=} opt_headers Map of default headers to add to every
* request.
* @param {number=} opt_minCount Min. number of objects (Default: 1).
* @param {number=} opt_maxCount Max. number of objects (Default: 10).
* @param {number=} opt_timeoutInterval Timeout (in ms) before aborting an
* attempt (Default: 0ms).
* @constructor
* @extends {goog.events.EventTarget}
*/
goog.net.XhrManager = function(
opt_maxRetries,
opt_headers,
opt_minCount,
opt_maxCount,
opt_timeoutInterval) {
goog.base(this);
/**
* Maximum number of retries for a given request
* @type {number}
* @private
*/
this.maxRetries_ = goog.isDef(opt_maxRetries) ? opt_maxRetries : 1;
/**
* Timeout interval for an attempt of a given request.
* @type {number}
* @private
*/
this.timeoutInterval_ =
goog.isDef(opt_timeoutInterval) ? Math.max(0, opt_timeoutInterval) : 0;
/**
* The pool of XhrIo's to use.
* @type {goog.net.XhrIoPool}
* @private
*/
this.xhrPool_ = new goog.net.XhrIoPool(
opt_headers, opt_minCount, opt_maxCount);
/**
* Map of ID's to requests.
* @type {goog.structs.Map}
* @private
*/
this.requests_ = new goog.structs.Map();
/**
* The event handler.
* @type {goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
};
goog.inherits(goog.net.XhrManager, goog.events.EventTarget);
/**
* Error to throw when a send is attempted with an ID that the manager already
* has registered for another request.
* @type {string}
* @private
*/
goog.net.XhrManager.ERROR_ID_IN_USE_ = '[goog.net.XhrManager] ID in use';
/**
* The goog.net.EventType's to listen/unlisten for on the XhrIo object.
* @type {Array.<goog.net.EventType>}
* @private
*/
goog.net.XhrManager.XHR_EVENT_TYPES_ = [
goog.net.EventType.READY,
goog.net.EventType.COMPLETE,
goog.net.EventType.SUCCESS,
goog.net.EventType.ERROR,
goog.net.EventType.ABORT,
goog.net.EventType.TIMEOUT];
/**
* Sets the number of milliseconds after which an incomplete request will be
* aborted. Zero means no timeout is set.
* @param {number} ms Timeout interval in milliseconds; 0 means none.
*/
goog.net.XhrManager.prototype.setTimeoutInterval = function(ms) {
this.timeoutInterval_ = Math.max(0, ms);
};
/**
* Returns the number of requests either in flight, or waiting to be sent.
* The count will include the current request if used within a COMPLETE event
* handler or callback.
* @return {number} The number of requests in flight or pending send.
*/
goog.net.XhrManager.prototype.getOutstandingCount = function() {
return this.requests_.getCount();
};
/**
* Returns an array of request ids that are either in flight, or waiting to
* be sent. The id of the current request will be included if used within a
* COMPLETE event handler or callback.
* @return {!Array.<string>} Request ids in flight or pending send.
*/
goog.net.XhrManager.prototype.getOutstandingRequestIds = function() {
return this.requests_.getKeys();
};
/**
* Registers the given request to be sent. Throws an error if a request
* already exists with the given ID.
* NOTE: It is not sent immediately. It is queued and will be sent when an
* XhrIo object becomes available, taking into account the request's
* priority.
* @param {string} id The id of the request.
* @param {string} url Uri to make the request too.
* @param {string=} opt_method Send method, default: GET.
* @param {ArrayBuffer|Blob|Document|FormData|string=} opt_content Post data.
* @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the
* request.
* @param {*=} opt_priority The priority of the request. A smaller value means a
* higher priority.
* @param {Function=} opt_callback Callback function for when request is
* complete. The only param is the event object from the COMPLETE event.
* @param {number=} opt_maxRetries The maximum number of times the request
* should be retried.
* @param {goog.net.XhrIo.ResponseType=} opt_responseType The response type of
* this request; defaults to goog.net.XhrIo.ResponseType.DEFAULT.
* @return {goog.net.XhrManager.Request} The queued request object.
*/
goog.net.XhrManager.prototype.send = function(
id,
url,
opt_method,
opt_content,
opt_headers,
opt_priority,
opt_callback,
opt_maxRetries,
opt_responseType) {
var requests = this.requests_;
// Check if there is already a request with the given id.
if (requests.get(id)) {
throw Error(goog.net.XhrManager.ERROR_ID_IN_USE_);
}
// Make the Request object.
var request = new goog.net.XhrManager.Request(
url,
goog.bind(this.handleEvent_, this, id),
opt_method,
opt_content,
opt_headers,
opt_callback,
goog.isDef(opt_maxRetries) ? opt_maxRetries : this.maxRetries_,
opt_responseType);
this.requests_.set(id, request);
// Setup the callback for the pool.
var callback = goog.bind(this.handleAvailableXhr_, this, id);
this.xhrPool_.getObject(callback, opt_priority);
return request;
};
/**
* Aborts the request associated with id.
* @param {string} id The id of the request to abort.
* @param {boolean=} opt_force If true, remove the id now so it can be reused.
* No events are fired and the callback is not called when forced.
*/
goog.net.XhrManager.prototype.abort = function(id, opt_force) {
var request = this.requests_.get(id);
if (request) {
var xhrIo = request.xhrIo;
request.setAborted(true);
if (opt_force) {
if (xhrIo) {
// We remove listeners to make sure nothing gets called if a new request
// with the same id is made.
this.removeXhrListener_(xhrIo, request.getXhrEventCallback());
goog.events.listenOnce(
xhrIo,
goog.net.EventType.READY,
function() { this.xhrPool_.releaseObject(xhrIo); },
false,
this);
}
this.requests_.remove(id);
}
if (xhrIo) {
xhrIo.abort();
}
}
};
/**
* Handles when an XhrIo object becomes available. Sets up the events, fires
* the READY event, and starts the process to send the request.
* @param {string} id The id of the request the XhrIo is for.
* @param {goog.net.XhrIo} xhrIo The available XhrIo object.
* @private
*/
goog.net.XhrManager.prototype.handleAvailableXhr_ = function(id, xhrIo) {
var request = this.requests_.get(id);
// Make sure the request doesn't already have an XhrIo attached. This can
// happen if a forced abort occurs before an XhrIo is available, and a new
// request with the same id is made.
if (request && !request.xhrIo) {
this.addXhrListener_(xhrIo, request.getXhrEventCallback());
// Set properties for the XhrIo.
xhrIo.setTimeoutInterval(this.timeoutInterval_);
xhrIo.setResponseType(request.getResponseType());
// Add a reference to the XhrIo object to the request.
request.xhrIo = xhrIo;
// Notify the listeners.
this.dispatchEvent(new goog.net.XhrManager.Event(
goog.net.EventType.READY, this, id, xhrIo));
// Send the request.
this.retry_(id, xhrIo);
// If the request was aborted before it got an XhrIo object, abort it now.
if (request.getAborted()) {
xhrIo.abort();
}
} else {
// If the request has an XhrIo object already, or no request exists, just
// return the XhrIo back to the pool.
this.xhrPool_.releaseObject(xhrIo);
}
};
/**
* Handles all events fired by the XhrIo object for a given request.
* @param {string} id The id of the request.
* @param {goog.events.Event} e The event.
* @return {Object} The return value from the handler, if any.
* @private
*/
goog.net.XhrManager.prototype.handleEvent_ = function(id, e) {
var xhrIo = /** @type {goog.net.XhrIo} */(e.target);
switch (e.type) {
case goog.net.EventType.READY:
this.retry_(id, xhrIo);
break;
case goog.net.EventType.COMPLETE:
return this.handleComplete_(id, xhrIo, e);
case goog.net.EventType.SUCCESS:
this.handleSuccess_(id, xhrIo);
break;
// A timeout is handled like an error.
case goog.net.EventType.TIMEOUT:
case goog.net.EventType.ERROR:
this.handleError_(id, xhrIo);
break;
case goog.net.EventType.ABORT:
this.handleAbort_(id, xhrIo);
break;
}
return null;
};
/**
* Attempts to retry the given request. If the request has already attempted
* the maximum number of retries, then it removes the request and releases
* the XhrIo object back into the pool.
* @param {string} id The id of the request.
* @param {goog.net.XhrIo} xhrIo The XhrIo object.
* @private
*/
goog.net.XhrManager.prototype.retry_ = function(id, xhrIo) {
var request = this.requests_.get(id);
// If the request has not completed and it is below its max. retries.
if (request && !request.getCompleted() && !request.hasReachedMaxRetries()) {
request.increaseAttemptCount();
xhrIo.send(request.getUrl(), request.getMethod(), request.getContent(),
request.getHeaders());
} else {
if (request) {
// Remove the events on the XhrIo objects.
this.removeXhrListener_(xhrIo, request.getXhrEventCallback());
// Remove the request.
this.requests_.remove(id);
}
// Release the XhrIo object back into the pool.
this.xhrPool_.releaseObject(xhrIo);
}
};
/**
* Handles the complete of a request. Dispatches the COMPLETE event and sets the
* the request as completed if the request has succeeded, or is done retrying.
* @param {string} id The id of the request.
* @param {goog.net.XhrIo} xhrIo The XhrIo object.
* @param {goog.events.Event} e The original event.
* @return {Object} The return value from the callback, if any.
* @private
*/
goog.net.XhrManager.prototype.handleComplete_ = function(id, xhrIo, e) {
// Only if the request is done processing should a COMPLETE event be fired.
var request = this.requests_.get(id);
if (xhrIo.getLastErrorCode() == goog.net.ErrorCode.ABORT ||
xhrIo.isSuccess() || request.hasReachedMaxRetries()) {
this.dispatchEvent(new goog.net.XhrManager.Event(
goog.net.EventType.COMPLETE, this, id, xhrIo));
// If the request exists, we mark it as completed and call the callback
if (request) {
request.setCompleted(true);
// Call the complete callback as if it was set as a COMPLETE event on the
// XhrIo directly.
if (request.getCompleteCallback()) {
return request.getCompleteCallback().call(xhrIo, e);
}
}
}
return null;
};
/**
* Handles the abort of an underlying XhrIo object.
* @param {string} id The id of the request.
* @param {goog.net.XhrIo} xhrIo The XhrIo object.
* @private
*/
goog.net.XhrManager.prototype.handleAbort_ = function(id, xhrIo) {
// Fire event.
// NOTE: The complete event should always be fired before the abort event, so
// the bulk of the work is done in handleComplete.
this.dispatchEvent(new goog.net.XhrManager.Event(
goog.net.EventType.ABORT, this, id, xhrIo));
};
/**
* Handles the success of a request. Dispatches the SUCCESS event and sets the
* the request as completed.
* @param {string} id The id of the request.
* @param {goog.net.XhrIo} xhrIo The XhrIo object.
* @private
*/
goog.net.XhrManager.prototype.handleSuccess_ = function(id, xhrIo) {
// Fire event.
// NOTE: We don't release the XhrIo object from the pool here.
// It is released in the retry method, when we know it is back in the
// ready state.
this.dispatchEvent(new goog.net.XhrManager.Event(
goog.net.EventType.SUCCESS, this, id, xhrIo));
};
/**
* Handles the error of a request. If the request has not reach its maximum
* number of retries, then it lets the request retry naturally (will let the
* request hit the READY state). Else, it dispatches the ERROR event.
* @param {string} id The id of the request.
* @param {goog.net.XhrIo} xhrIo The XhrIo object.
* @private
*/
goog.net.XhrManager.prototype.handleError_ = function(id, xhrIo) {
var request = this.requests_.get(id);
// If the maximum number of retries has been reached.
if (request.hasReachedMaxRetries()) {
// Fire event.
// NOTE: We don't release the XhrIo object from the pool here.
// It is released in the retry method, when we know it is back in the
// ready state.
this.dispatchEvent(new goog.net.XhrManager.Event(
goog.net.EventType.ERROR, this, id, xhrIo));
}
};
/**
* Remove listeners for XHR events on an XhrIo object.
* @param {goog.net.XhrIo} xhrIo The object to stop listenening to events on.
* @param {Function} func The callback to remove from event handling.
* @param {string|Array.<string>=} opt_types Event types to remove listeners
* for. Defaults to XHR_EVENT_TYPES_.
* @private
*/
goog.net.XhrManager.prototype.removeXhrListener_ = function(xhrIo,
func,
opt_types) {
var types = opt_types || goog.net.XhrManager.XHR_EVENT_TYPES_;
this.eventHandler_.unlisten(xhrIo, types, func);
};
/**
* Adds a listener for XHR events on an XhrIo object.
* @param {goog.net.XhrIo} xhrIo The object listen to events on.
* @param {Function} func The callback when the event occurs.
* @param {string|Array.<string>=} opt_types Event types to attach listeners to.
* Defaults to XHR_EVENT_TYPES_.
* @private
*/
goog.net.XhrManager.prototype.addXhrListener_ = function(xhrIo,
func,
opt_types) {
var types = opt_types || goog.net.XhrManager.XHR_EVENT_TYPES_;
this.eventHandler_.listen(xhrIo, types, func);
};
/** @override */
goog.net.XhrManager.prototype.disposeInternal = function() {
goog.net.XhrManager.superClass_.disposeInternal.call(this);
this.xhrPool_.dispose();
this.xhrPool_ = null;
this.eventHandler_.dispose();
this.eventHandler_ = null;
// Call dispose on each request.
var requests = this.requests_;
goog.structs.forEach(requests, function(value, key) {
value.dispose();
});
requests.clear();
this.requests_ = null;
};
/**
* An event dispatched by XhrManager.
*
* @param {goog.net.EventType} type Event Type.
* @param {goog.net.XhrManager} target Reference to the object that is the
* target of this event.
* @param {string} id The id of the request this event is for.
* @param {goog.net.XhrIo} xhrIo The XhrIo object of the request.
* @constructor
* @extends {goog.events.Event}
*/
goog.net.XhrManager.Event = function(type, target, id, xhrIo) {
goog.events.Event.call(this, type, target);
/**
* The id of the request this event is for.
* @type {string}
*/
this.id = id;
/**
* The XhrIo object of the request.
* @type {goog.net.XhrIo}
*/
this.xhrIo = xhrIo;
};
goog.inherits(goog.net.XhrManager.Event, goog.events.Event);
/**
* An encapsulation of everything needed to make a Xhr request.
* NOTE: This is used internal to the XhrManager.
*
* @param {string} url Uri to make the request too.
* @param {Function} xhrEventCallback Callback attached to the events of the
* XhrIo object of the request.
* @param {string=} opt_method Send method, default: GET.
* @param {ArrayBuffer|Blob|Document|FormData|string=} opt_content Post data.
* @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the
* request.
* @param {Function=} opt_callback Callback function for when request is
* complete. NOTE: Only 1 callback supported across all events.
* @param {number=} opt_maxRetries The maximum number of times the request
* should be retried (Default: 1).
* @param {goog.net.XhrIo.ResponseType=} opt_responseType The response type of
* this request; defaults to goog.net.XhrIo.ResponseType.DEFAULT.
*
* @constructor
* @extends {goog.Disposable}
*/
goog.net.XhrManager.Request = function(url, xhrEventCallback, opt_method,
opt_content, opt_headers, opt_callback, opt_maxRetries, opt_responseType) {
goog.Disposable.call(this);
/**
* Uri to make the request too.
* @type {string}
* @private
*/
this.url_ = url;
/**
* Send method.
* @type {string}
* @private
*/
this.method_ = opt_method || 'GET';
/**
* Post data.
* @type {ArrayBuffer|Blob|Document|FormData|string|undefined}
* @private
*/
this.content_ = opt_content;
/**
* Map of headers
* @type {Object|goog.structs.Map|null}
* @private
*/
this.headers_ = opt_headers || null;
/**
* The maximum number of times the request should be retried.
* @type {number}
* @private
*/
this.maxRetries_ = goog.isDef(opt_maxRetries) ? opt_maxRetries : 1;
/**
* The number of attempts so far.
* @type {number}
* @private
*/
this.attemptCount_ = 0;
/**
* Whether the request has been completed.
* @type {boolean}
* @private
*/
this.completed_ = false;
/**
* Whether the request has been aborted.
* @type {boolean}
* @private
*/
this.aborted_ = false;
/**
* Callback attached to the events of the XhrIo object.
* @type {Function|undefined}
* @private
*/
this.xhrEventCallback_ = xhrEventCallback;
/**
* Callback function called when request is complete.
* @type {Function|undefined}
* @private
*/
this.completeCallback_ = opt_callback;
/**
* A response type to set on this.xhrIo when it's populated.
* @type {!goog.net.XhrIo.ResponseType}
* @private
*/
this.responseType_ = opt_responseType || goog.net.XhrIo.ResponseType.DEFAULT;
/**
* The XhrIo instance handling this request. Set in handleAvailableXhr.
* @type {goog.net.XhrIo}
*/
this.xhrIo = null;
};
goog.inherits(goog.net.XhrManager.Request, goog.Disposable);
/**
* Gets the uri.
* @return {string} The uri to make the request to.
*/
goog.net.XhrManager.Request.prototype.getUrl = function() {
return this.url_;
};
/**
* Gets the send method.
* @return {string} The send method.
*/
goog.net.XhrManager.Request.prototype.getMethod = function() {
return this.method_;
};
/**
* Gets the post data.
* @return {ArrayBuffer|Blob|Document|FormData|string|undefined}
* The post data.
*/
goog.net.XhrManager.Request.prototype.getContent = function() {
return this.content_;
};
/**
* Gets the map of headers.
* @return {Object|goog.structs.Map} The map of headers.
*/
goog.net.XhrManager.Request.prototype.getHeaders = function() {
return this.headers_;
};
/**
* Gets the maximum number of times the request should be retried.
* @return {number} The maximum number of times the request should be retried.
*/
goog.net.XhrManager.Request.prototype.getMaxRetries = function() {
return this.maxRetries_;
};
/**
* Gets the number of attempts so far.
* @return {number} The number of attempts so far.
*/
goog.net.XhrManager.Request.prototype.getAttemptCount = function() {
return this.attemptCount_;
};
/**
* Increases the number of attempts so far.
*/
goog.net.XhrManager.Request.prototype.increaseAttemptCount = function() {
this.attemptCount_++;
};
/**
* Returns whether the request has reached the maximum number of retries.
* @return {boolean} Whether the request has reached the maximum number of
* retries.
*/
goog.net.XhrManager.Request.prototype.hasReachedMaxRetries = function() {
return this.attemptCount_ > this.maxRetries_;
};
/**
* Sets the completed status.
* @param {boolean} complete The completed status.
*/
goog.net.XhrManager.Request.prototype.setCompleted = function(complete) {
this.completed_ = complete;
};
/**
* Gets the completed status.
* @return {boolean} The completed status.
*/
goog.net.XhrManager.Request.prototype.getCompleted = function() {
return this.completed_;
};
/**
* Sets the aborted status.
* @param {boolean} aborted True if the request was aborted, otherwise False.
*/
goog.net.XhrManager.Request.prototype.setAborted = function(aborted) {
this.aborted_ = aborted;
};
/**
* Gets the aborted status.
* @return {boolean} True if request was aborted, otherwise False.
*/
goog.net.XhrManager.Request.prototype.getAborted = function() {
return this.aborted_;
};
/**
* Gets the callback attached to the events of the XhrIo object.
* @return {Function|undefined} The callback attached to the events of the
* XhrIo object.
*/
goog.net.XhrManager.Request.prototype.getXhrEventCallback = function() {
return this.xhrEventCallback_;
};
/**
* Gets the callback for when the request is complete.
* @return {Function|undefined} The callback for when the request is complete.
*/
goog.net.XhrManager.Request.prototype.getCompleteCallback = function() {
return this.completeCallback_;
};
/**
* Gets the response type that will be set on this request's XhrIo when it's
* available.
* @return {!goog.net.XhrIo.ResponseType} The response type to be set
* when an XhrIo becomes available to this request.
*/
goog.net.XhrManager.Request.prototype.getResponseType = function() {
return this.responseType_;
};
/** @override */
goog.net.XhrManager.Request.prototype.disposeInternal = function() {
goog.net.XhrManager.Request.superClass_.disposeInternal.call(this);
delete this.xhrEventCallback_;
delete this.completeCallback_;
};
@@ -0,0 +1,228 @@
// 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 Low level handling of XMLHttpRequest.
* @author arv@google.com (Erik Arvidsson)
* @author dbk@google.com (David Barrett-Kahn)
*/
goog.provide('goog.net.DefaultXmlHttpFactory');
goog.provide('goog.net.XmlHttp');
goog.provide('goog.net.XmlHttp.OptionType');
goog.provide('goog.net.XmlHttp.ReadyState');
goog.require('goog.net.WrapperXmlHttpFactory');
goog.require('goog.net.XmlHttpFactory');
/**
* Static class for creating XMLHttpRequest objects.
* @return {!(XMLHttpRequest|GearsHttpRequest)} A new XMLHttpRequest object.
*/
goog.net.XmlHttp = function() {
return goog.net.XmlHttp.factory_.createInstance();
};
/**
* @define {boolean} Whether to assume XMLHttpRequest exists. Setting this to
* true strips the ActiveX probing code.
*/
goog.define('goog.net.XmlHttp.ASSUME_NATIVE_XHR', false);
/**
* Gets the options to use with the XMLHttpRequest objects obtained using
* the static methods.
* @return {Object} The options.
*/
goog.net.XmlHttp.getOptions = function() {
return goog.net.XmlHttp.factory_.getOptions();
};
/**
* Type of options that an XmlHttp object can have.
* @enum {number}
*/
goog.net.XmlHttp.OptionType = {
/**
* Whether a goog.nullFunction should be used to clear the onreadystatechange
* handler instead of null.
*/
USE_NULL_FUNCTION: 0,
/**
* NOTE(user): In IE if send() errors on a *local* request the readystate
* is still changed to COMPLETE. We need to ignore it and allow the
* try/catch around send() to pick up the error.
*/
LOCAL_REQUEST_ERROR: 1
};
/**
* Status constants for XMLHTTP, matches:
* http://msdn.microsoft.com/library/default.asp?url=/library/
* en-us/xmlsdk/html/0e6a34e4-f90c-489d-acff-cb44242fafc6.asp
* @enum {number}
*/
goog.net.XmlHttp.ReadyState = {
/**
* Constant for when xmlhttprequest.readyState is uninitialized
*/
UNINITIALIZED: 0,
/**
* Constant for when xmlhttprequest.readyState is loading.
*/
LOADING: 1,
/**
* Constant for when xmlhttprequest.readyState is loaded.
*/
LOADED: 2,
/**
* Constant for when xmlhttprequest.readyState is in an interactive state.
*/
INTERACTIVE: 3,
/**
* Constant for when xmlhttprequest.readyState is completed
*/
COMPLETE: 4
};
/**
* The global factory instance for creating XMLHttpRequest objects.
* @type {goog.net.XmlHttpFactory}
* @private
*/
goog.net.XmlHttp.factory_;
/**
* Sets the factories for creating XMLHttpRequest objects and their options.
* @param {Function} factory The factory for XMLHttpRequest objects.
* @param {Function} optionsFactory The factory for options.
* @deprecated Use setGlobalFactory instead.
*/
goog.net.XmlHttp.setFactory = function(factory, optionsFactory) {
goog.net.XmlHttp.setGlobalFactory(new goog.net.WrapperXmlHttpFactory(
/** @type {function() : !(XMLHttpRequest|GearsHttpRequest)} */ (factory),
/** @type {function() : !Object}*/ (optionsFactory)));
};
/**
* Sets the global factory object.
* @param {!goog.net.XmlHttpFactory} factory New global factory object.
*/
goog.net.XmlHttp.setGlobalFactory = function(factory) {
goog.net.XmlHttp.factory_ = factory;
};
/**
* Default factory to use when creating xhr objects. You probably shouldn't be
* instantiating this directly, but rather using it via goog.net.XmlHttp.
* @extends {goog.net.XmlHttpFactory}
* @constructor
*/
goog.net.DefaultXmlHttpFactory = function() {
goog.net.XmlHttpFactory.call(this);
};
goog.inherits(goog.net.DefaultXmlHttpFactory, goog.net.XmlHttpFactory);
/** @override */
goog.net.DefaultXmlHttpFactory.prototype.createInstance = function() {
var progId = this.getProgId_();
if (progId) {
return new ActiveXObject(progId);
} else {
return new XMLHttpRequest();
}
};
/** @override */
goog.net.DefaultXmlHttpFactory.prototype.internalGetOptions = function() {
var progId = this.getProgId_();
var options = {};
if (progId) {
options[goog.net.XmlHttp.OptionType.USE_NULL_FUNCTION] = true;
options[goog.net.XmlHttp.OptionType.LOCAL_REQUEST_ERROR] = true;
}
return options;
};
/**
* The ActiveX PROG ID string to use to create xhr's in IE. Lazily initialized.
* @type {string|undefined}
* @private
*/
goog.net.DefaultXmlHttpFactory.prototype.ieProgId_;
/**
* Initialize the private state used by other functions.
* @return {string} The ActiveX PROG ID string to use to create xhr's in IE.
* @private
*/
goog.net.DefaultXmlHttpFactory.prototype.getProgId_ = function() {
if (goog.net.XmlHttp.ASSUME_NATIVE_XHR) {
return '';
}
// The following blog post describes what PROG IDs to use to create the
// XMLHTTP object in Internet Explorer:
// http://blogs.msdn.com/xmlteam/archive/2006/10/23/using-the-right-version-of-msxml-in-internet-explorer.aspx
// However we do not (yet) fully trust that this will be OK for old versions
// of IE on Win9x so we therefore keep the last 2.
if (!this.ieProgId_ && typeof XMLHttpRequest == 'undefined' &&
typeof ActiveXObject != 'undefined') {
// Candidate Active X types.
var ACTIVE_X_IDENTS = ['MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP'];
for (var i = 0; i < ACTIVE_X_IDENTS.length; i++) {
var candidate = ACTIVE_X_IDENTS[i];
/** @preserveTry */
try {
new ActiveXObject(candidate);
// NOTE(user): cannot assign progid and return candidate in one line
// because JSCompiler complaings: BUG 658126
this.ieProgId_ = candidate;
return candidate;
} catch (e) {
// do nothing; try next choice
}
}
// couldn't find any matches
throw Error('Could not create ActiveXObject. ActiveX might be disabled,' +
' or MSXML might not be installed');
}
return /** @type {string} */ (this.ieProgId_);
};
//Set the global factory to an instance of the default factory.
goog.net.XmlHttp.setGlobalFactory(new goog.net.DefaultXmlHttpFactory());
@@ -0,0 +1,64 @@
// Copyright 2010 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 Interface for a factory for creating XMLHttpRequest objects
* and metadata about them.
* @author dbk@google.com (David Barrett-Kahn)
*/
goog.provide('goog.net.XmlHttpFactory');
/**
* Abstract base class for an XmlHttpRequest factory.
* @constructor
*/
goog.net.XmlHttpFactory = function() {
};
/**
* Cache of options - we only actually call internalGetOptions once.
* @type {Object}
* @private
*/
goog.net.XmlHttpFactory.prototype.cachedOptions_ = null;
/**
* @return {!(XMLHttpRequest|GearsHttpRequest)} A new XMLHttpRequest instance.
*/
goog.net.XmlHttpFactory.prototype.createInstance = goog.abstractMethod;
/**
* @return {Object} Options describing how xhr objects obtained from this
* factory should be used.
*/
goog.net.XmlHttpFactory.prototype.getOptions = function() {
return this.cachedOptions_ ||
(this.cachedOptions_ = this.internalGetOptions());
};
/**
* Override this method in subclasses to preserve the caching offered by
* getOptions().
* @return {Object} Options describing how xhr objects obtained from this
* factory should be used.
* @protected
*/
goog.net.XmlHttpFactory.prototype.internalGetOptions = goog.abstractMethod;
@@ -0,0 +1,805 @@
// Copyright 2007 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 Provides the class CrossPageChannel, the main class in
* goog.net.xpc.
*
* @see ../../demos/xpc/index.html
*/
goog.provide('goog.net.xpc.CrossPageChannel');
goog.require('goog.Disposable');
goog.require('goog.Uri');
goog.require('goog.async.Deferred');
goog.require('goog.async.Delay');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.json');
goog.require('goog.messaging.AbstractChannel');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.FrameElementMethodTransport');
goog.require('goog.net.xpc.IframePollingTransport');
goog.require('goog.net.xpc.IframeRelayTransport');
goog.require('goog.net.xpc.NativeMessagingTransport');
goog.require('goog.net.xpc.NixTransport');
goog.require('goog.net.xpc.Transport');
goog.require('goog.userAgent');
/**
* A communication channel between two documents from different domains.
* Provides asynchronous messaging.
*
* @param {Object} cfg Channel configuration object.
* @param {goog.dom.DomHelper=} opt_domHelper The optional dom helper to
* use for looking up elements in the dom.
* @constructor
* @extends {goog.messaging.AbstractChannel}
*/
goog.net.xpc.CrossPageChannel = function(cfg, opt_domHelper) {
goog.base(this);
for (var i = 0, uriField; uriField = goog.net.xpc.UriCfgFields[i]; i++) {
if (uriField in cfg && !/^https?:\/\//.test(cfg[uriField])) {
throw Error('URI ' + cfg[uriField] + ' is invalid for field ' + uriField);
}
}
/**
* The configuration for this channel.
* @type {Object}
* @private
*/
this.cfg_ = cfg;
/**
* The name of the channel.
* @type {string}
*/
this.name = this.cfg_[goog.net.xpc.CfgFields.CHANNEL_NAME] ||
goog.net.xpc.getRandomString(10);
/**
* The dom helper to use for accessing the dom.
* @type {goog.dom.DomHelper}
* @private
*/
this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
/**
* Collects deferred function calls which will be made once the connection
* has been fully set up.
* @type {!Array.<function()>}
* @private
*/
this.deferredDeliveries_ = [];
/**
* An event handler used to listen for load events on peer iframes.
* @type {!goog.events.EventHandler}
* @private
*/
this.peerLoadHandler_ = new goog.events.EventHandler(this);
// If LOCAL_POLL_URI or PEER_POLL_URI is not available, try using
// robots.txt from that host.
cfg[goog.net.xpc.CfgFields.LOCAL_POLL_URI] =
cfg[goog.net.xpc.CfgFields.LOCAL_POLL_URI] ||
goog.uri.utils.getHost(this.domHelper_.getWindow().location.href) +
'/robots.txt';
// PEER_URI is sometimes undefined in tests.
cfg[goog.net.xpc.CfgFields.PEER_POLL_URI] =
cfg[goog.net.xpc.CfgFields.PEER_POLL_URI] ||
goog.uri.utils.getHost(cfg[goog.net.xpc.CfgFields.PEER_URI] || '') +
'/robots.txt';
goog.net.xpc.channels[this.name] = this;
goog.events.listen(window, 'unload',
goog.net.xpc.CrossPageChannel.disposeAll_);
goog.log.info(goog.net.xpc.logger, 'CrossPageChannel created: ' + this.name);
};
goog.inherits(goog.net.xpc.CrossPageChannel, goog.messaging.AbstractChannel);
/**
* Regexp for escaping service names.
* @type {RegExp}
* @private
*/
goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_ESCAPE_RE_ =
new RegExp('^%*' + goog.net.xpc.TRANSPORT_SERVICE_ + '$');
/**
* Regexp for unescaping service names.
* @type {RegExp}
* @private
*/
goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_UNESCAPE_RE_ =
new RegExp('^%+' + goog.net.xpc.TRANSPORT_SERVICE_ + '$');
/**
* A delay between the transport reporting as connected and the calling of the
* connection callback. Sometimes used to paper over timing vulnerabilities.
* @type {goog.async.Delay}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.connectionDelay_ = null;
/**
* A deferred which is set to non-null while a peer iframe is being created
* but has not yet thrown its load event, and which fires when that load event
* arrives.
* @type {goog.async.Deferred}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.peerWindowDeferred_ = null;
/**
* The transport.
* @type {goog.net.xpc.Transport?}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.transport_ = null;
/**
* The channel state.
* @type {number}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.state_ =
goog.net.xpc.ChannelStates.NOT_CONNECTED;
/**
* @override
* @return {boolean} Whether the channel is connected.
*/
goog.net.xpc.CrossPageChannel.prototype.isConnected = function() {
return this.state_ == goog.net.xpc.ChannelStates.CONNECTED;
};
/**
* Reference to the window-object of the peer page.
* @type {Object}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.peerWindowObject_ = null;
/**
* Reference to the iframe-element.
* @type {Object}
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.iframeElement_ = null;
/**
* Returns the configuration object for this channel.
* Package private. Do not call from outside goog.net.xpc.
*
* @return {Object} The configuration object for this channel.
*/
goog.net.xpc.CrossPageChannel.prototype.getConfig = function() {
return this.cfg_;
};
/**
* Returns a reference to the iframe-element.
* Package private. Do not call from outside goog.net.xpc.
*
* @return {Object} A reference to the iframe-element.
*/
goog.net.xpc.CrossPageChannel.prototype.getIframeElement = function() {
return this.iframeElement_;
};
/**
* Sets the window object the foreign document resides in.
*
* @param {Object} peerWindowObject The window object of the peer.
*/
goog.net.xpc.CrossPageChannel.prototype.setPeerWindowObject =
function(peerWindowObject) {
this.peerWindowObject_ = peerWindowObject;
};
/**
* Returns the window object the foreign document resides in.
* Package private. Do not call from outside goog.net.xpc.
*
* @return {Object} The window object of the peer.
*/
goog.net.xpc.CrossPageChannel.prototype.getPeerWindowObject = function() {
return this.peerWindowObject_;
};
/**
* Determines whether the peer window is available (e.g. not closed).
* Package private. Do not call from outside goog.net.xpc.
*
* @return {boolean} Whether the peer window is available.
*/
goog.net.xpc.CrossPageChannel.prototype.isPeerAvailable = function() {
// NOTE(user): This check is not reliable in IE, where a document in an
// iframe does not get unloaded when removing the iframe element from the DOM.
// TODO(user): Find something that works in IE as well.
// NOTE(user): "!this.peerWindowObject_.closed" evaluates to 'false' in IE9
// sometimes even though typeof(this.peerWindowObject_.closed) is boolean and
// this.peerWindowObject_.closed evaluates to 'false'. Casting it to a Boolean
// results in sane evaluation. When this happens, it's in the inner iframe
// when querying its parent's 'closed' status. Note that this is a different
// case than mibuerge@'s note above.
try {
return !!this.peerWindowObject_ && !Boolean(this.peerWindowObject_.closed);
} catch (e) {
// If the window is closing, an error may be thrown.
return false;
}
};
/**
* Determine which transport type to use for this channel / useragent.
* @return {goog.net.xpc.TransportTypes|undefined} The best transport type.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.determineTransportType_ = function() {
var transportType;
if (goog.isFunction(document.postMessage) ||
goog.isFunction(window.postMessage) ||
// IE8 supports window.postMessage, but
// typeof window.postMessage returns "object"
(goog.userAgent.IE && window.postMessage)) {
transportType = goog.net.xpc.TransportTypes.NATIVE_MESSAGING;
} else if (goog.userAgent.GECKO) {
transportType = goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD;
} else if (goog.userAgent.IE &&
this.cfg_[goog.net.xpc.CfgFields.PEER_RELAY_URI]) {
transportType = goog.net.xpc.TransportTypes.IFRAME_RELAY;
} else if (goog.userAgent.IE && goog.net.xpc.NixTransport.isNixSupported()) {
transportType = goog.net.xpc.TransportTypes.NIX;
} else {
transportType = goog.net.xpc.TransportTypes.IFRAME_POLLING;
}
return transportType;
};
/**
* Creates the transport for this channel. Chooses from the available
* transport based on the user agent and the configuration.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.createTransport_ = function() {
// return, if the transport has already been created
if (this.transport_) {
return;
}
if (!this.cfg_[goog.net.xpc.CfgFields.TRANSPORT]) {
this.cfg_[goog.net.xpc.CfgFields.TRANSPORT] =
this.determineTransportType_();
}
switch (this.cfg_[goog.net.xpc.CfgFields.TRANSPORT]) {
case goog.net.xpc.TransportTypes.NATIVE_MESSAGING:
var protocolVersion = this.cfg_[
goog.net.xpc.CfgFields.NATIVE_TRANSPORT_PROTOCOL_VERSION] || 2;
this.transport_ = new goog.net.xpc.NativeMessagingTransport(
this,
this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME],
this.domHelper_,
!!this.cfg_[goog.net.xpc.CfgFields.ONE_SIDED_HANDSHAKE],
protocolVersion);
break;
case goog.net.xpc.TransportTypes.NIX:
this.transport_ = new goog.net.xpc.NixTransport(this, this.domHelper_);
break;
case goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD:
this.transport_ =
new goog.net.xpc.FrameElementMethodTransport(this, this.domHelper_);
break;
case goog.net.xpc.TransportTypes.IFRAME_RELAY:
this.transport_ =
new goog.net.xpc.IframeRelayTransport(this, this.domHelper_);
break;
case goog.net.xpc.TransportTypes.IFRAME_POLLING:
this.transport_ =
new goog.net.xpc.IframePollingTransport(this, this.domHelper_);
break;
}
if (this.transport_) {
goog.log.info(goog.net.xpc.logger,
'Transport created: ' + this.transport_.getName());
} else {
throw Error('CrossPageChannel: No suitable transport found!');
}
};
/**
* Returns the transport type in use for this channel.
* @return {number} Transport-type identifier.
*/
goog.net.xpc.CrossPageChannel.prototype.getTransportType = function() {
return this.transport_.getType();
};
/**
* Returns the tranport name in use for this channel.
* @return {string} The transport name.
*/
goog.net.xpc.CrossPageChannel.prototype.getTransportName = function() {
return this.transport_.getName();
};
/**
* @return {Object} Configuration-object to be used by the peer to
* initialize the channel.
*/
goog.net.xpc.CrossPageChannel.prototype.getPeerConfiguration = function() {
var peerCfg = {};
peerCfg[goog.net.xpc.CfgFields.CHANNEL_NAME] = this.name;
peerCfg[goog.net.xpc.CfgFields.TRANSPORT] =
this.cfg_[goog.net.xpc.CfgFields.TRANSPORT];
peerCfg[goog.net.xpc.CfgFields.ONE_SIDED_HANDSHAKE] =
this.cfg_[goog.net.xpc.CfgFields.ONE_SIDED_HANDSHAKE];
if (this.cfg_[goog.net.xpc.CfgFields.LOCAL_RELAY_URI]) {
peerCfg[goog.net.xpc.CfgFields.PEER_RELAY_URI] =
this.cfg_[goog.net.xpc.CfgFields.LOCAL_RELAY_URI];
}
if (this.cfg_[goog.net.xpc.CfgFields.LOCAL_POLL_URI]) {
peerCfg[goog.net.xpc.CfgFields.PEER_POLL_URI] =
this.cfg_[goog.net.xpc.CfgFields.LOCAL_POLL_URI];
}
if (this.cfg_[goog.net.xpc.CfgFields.PEER_POLL_URI]) {
peerCfg[goog.net.xpc.CfgFields.LOCAL_POLL_URI] =
this.cfg_[goog.net.xpc.CfgFields.PEER_POLL_URI];
}
var role = this.cfg_[goog.net.xpc.CfgFields.ROLE];
if (role) {
peerCfg[goog.net.xpc.CfgFields.ROLE] =
role == goog.net.xpc.CrossPageChannelRole.INNER ?
goog.net.xpc.CrossPageChannelRole.OUTER :
goog.net.xpc.CrossPageChannelRole.INNER;
}
return peerCfg;
};
/**
* Creates the iframe containing the peer page in a specified parent element.
* This method does not connect the channel, connect() still has to be called
* separately.
*
* @param {!Element} parentElm The container element the iframe is appended to.
* @param {Function=} opt_configureIframeCb If present, this function gets
* called with the iframe element as parameter to allow setting properties
* on it before it gets added to the DOM. If absent, the iframe's width and
* height are set to '100%'.
* @param {boolean=} opt_addCfgParam Whether to add the peer configuration as
* URL parameter (default: true).
* @return {!HTMLIFrameElement} The iframe element.
*/
goog.net.xpc.CrossPageChannel.prototype.createPeerIframe = function(
parentElm, opt_configureIframeCb, opt_addCfgParam) {
goog.log.info(goog.net.xpc.logger, 'createPeerIframe()');
var iframeId = this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID];
if (!iframeId) {
// Create a randomized ID for the iframe element to avoid
// bfcache-related issues.
iframeId = this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID] =
'xpcpeer' + goog.net.xpc.getRandomString(4);
}
// TODO(user) Opera creates a history-entry when creating an iframe
// programmatically as follows. Find a way which avoids this.
var iframeElm = goog.dom.getDomHelper(parentElm).createElement('IFRAME');
iframeElm.id = iframeElm.name = iframeId;
if (opt_configureIframeCb) {
opt_configureIframeCb(iframeElm);
} else {
iframeElm.style.width = iframeElm.style.height = '100%';
}
this.cleanUpIncompleteConnection_();
this.peerWindowDeferred_ =
new goog.async.Deferred(undefined, this);
var peerUri = this.getPeerUri(opt_addCfgParam);
this.peerLoadHandler_.listenOnce(iframeElm, 'load',
this.peerWindowDeferred_.callback, false, this.peerWindowDeferred_);
if (goog.userAgent.GECKO || goog.userAgent.WEBKIT) {
// Appending the iframe in a timeout to avoid a weird fastback issue, which
// is present in Safari and Gecko.
window.setTimeout(
goog.bind(function() {
parentElm.appendChild(iframeElm);
iframeElm.src = peerUri.toString();
goog.log.info(goog.net.xpc.logger,
'peer iframe created (' + iframeId + ')');
}, this), 1);
} else {
iframeElm.src = peerUri.toString();
parentElm.appendChild(iframeElm);
goog.log.info(goog.net.xpc.logger,
'peer iframe created (' + iframeId + ')');
}
return /** @type {!HTMLIFrameElement} */ (iframeElm);
};
/**
* Clean up after any incomplete attempt to establish and connect to a peer
* iframe.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.cleanUpIncompleteConnection_ =
function() {
if (this.peerWindowDeferred_) {
this.peerWindowDeferred_.cancel();
this.peerWindowDeferred_ = null;
}
this.deferredDeliveries_.length = 0;
this.peerLoadHandler_.removeAll();
};
/**
* Returns the peer URI, with an optional URL parameter for configuring the peer
* window.
*
* @param {boolean=} opt_addCfgParam Whether to add the peer configuration as
* URL parameter (default: true).
* @return {!goog.Uri} The peer URI.
*/
goog.net.xpc.CrossPageChannel.prototype.getPeerUri = function(opt_addCfgParam) {
var peerUri = this.cfg_[goog.net.xpc.CfgFields.PEER_URI];
if (goog.isString(peerUri)) {
peerUri = this.cfg_[goog.net.xpc.CfgFields.PEER_URI] =
new goog.Uri(peerUri);
}
// Add the channel configuration used by the peer as URL parameter.
if (opt_addCfgParam !== false) {
peerUri.setParameterValue('xpc',
goog.json.serialize(
this.getPeerConfiguration()));
}
return peerUri;
};
/**
* Initiates connecting the channel. When this method is called, all the
* information needed to connect the channel has to be available.
*
* @override
* @param {Function=} opt_connectCb The function to be called when the
* channel has been connected and is ready to be used.
*/
goog.net.xpc.CrossPageChannel.prototype.connect = function(opt_connectCb) {
this.connectCb_ = opt_connectCb || goog.nullFunction;
// If we know of a peer window whose creation has been requested but is not
// complete, peerWindowDeferred_ will be non-null, and we should block on it.
if (this.peerWindowDeferred_) {
this.peerWindowDeferred_.addCallback(this.continueConnection_);
} else {
this.continueConnection_();
}
};
/**
* Continues the connection process once we're as sure as we can be that the
* peer iframe has been created.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.continueConnection_ = function() {
goog.log.info(goog.net.xpc.logger, 'continueConnection_()');
this.peerWindowDeferred_ = null;
if (this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]) {
this.iframeElement_ = this.domHelper_.getElement(
this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]);
}
if (this.iframeElement_) {
var winObj = this.iframeElement_.contentWindow;
// accessing the window using contentWindow doesn't work in safari
if (!winObj) {
winObj = window.frames[this.cfg_[goog.net.xpc.CfgFields.IFRAME_ID]];
}
this.setPeerWindowObject(winObj);
}
// if the peer window object has not been set at this point, we assume
// being in an iframe and the channel is meant to be to the containing page
if (!this.peerWindowObject_) {
// throw an error if we are in the top window (== not in an iframe)
if (window == window.top) {
throw Error(
"CrossPageChannel: Can't connect, peer window-object not set.");
} else {
this.setPeerWindowObject(window.parent);
}
}
this.createTransport_();
this.transport_.connect();
// Now we run any deferred deliveries collected while connection was deferred.
while (this.deferredDeliveries_.length > 0) {
this.deferredDeliveries_.shift()();
}
};
/**
* Closes the channel.
*/
goog.net.xpc.CrossPageChannel.prototype.close = function() {
this.cleanUpIncompleteConnection_();
this.state_ = goog.net.xpc.ChannelStates.CLOSED;
goog.dispose(this.transport_);
this.transport_ = null;
this.connectCb_ = null;
goog.dispose(this.connectionDelay_);
this.connectionDelay_ = null;
goog.log.info(goog.net.xpc.logger, 'Channel "' + this.name + '" closed');
};
/**
* Package-private.
* Called by the transport when the channel is connected.
* @param {number=} opt_delay Delay this number of milliseconds before calling
* the connection callback. Usage is discouraged, but can be used to paper
* over timing vulnerabilities when there is no alternative.
*/
goog.net.xpc.CrossPageChannel.prototype.notifyConnected = function(opt_delay) {
if (this.isConnected() ||
(this.connectionDelay_ && this.connectionDelay_.isActive())) {
return;
}
this.state_ = goog.net.xpc.ChannelStates.CONNECTED;
goog.log.info(goog.net.xpc.logger, 'Channel "' + this.name + '" connected');
goog.dispose(this.connectionDelay_);
if (opt_delay) {
this.connectionDelay_ =
new goog.async.Delay(this.connectCb_, opt_delay);
this.connectionDelay_.start();
} else {
this.connectionDelay_ = null;
this.connectCb_();
}
};
/**
* Alias for notifyConected, for backward compatibility reasons.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.notifyConnected_ =
goog.net.xpc.CrossPageChannel.prototype.notifyConnected;
/**
* Called by the transport in case of an unrecoverable failure.
* Package private. Do not call from outside goog.net.xpc.
*/
goog.net.xpc.CrossPageChannel.prototype.notifyTransportError = function() {
goog.log.info(goog.net.xpc.logger, 'Transport Error');
this.close();
};
/** @override */
goog.net.xpc.CrossPageChannel.prototype.send = function(serviceName, payload) {
if (!this.isConnected()) {
goog.log.error(goog.net.xpc.logger, 'Can\'t send. Channel not connected.');
return;
}
// Check if the peer is still around.
if (!this.isPeerAvailable()) {
goog.log.error(goog.net.xpc.logger, 'Peer has disappeared.');
this.close();
return;
}
if (goog.isObject(payload)) {
payload = goog.json.serialize(payload);
}
// Partially URL-encode the service name because some characters (: and |) are
// used as delimiters for some transports, and we want to allow those
// characters in service names.
this.transport_.send(this.escapeServiceName_(serviceName), payload);
};
/**
* Delivers messages to the appropriate service-handler. Named xpcDeliver to
* avoid name conflict with {@code deliver} function in superclass
* goog.messaging.AbstractChannel.
*
* Package private. Do not call from outside goog.net.xpc.
*
* @param {string} serviceName The name of the port.
* @param {string} payload The payload.
* @param {string=} opt_origin An optional origin for the message, where the
* underlying transport makes that available. If this is specified, and
* the PEER_HOSTNAME parameter was provided, they must match or the message
* will be rejected.
*/
goog.net.xpc.CrossPageChannel.prototype.xpcDeliver = function(
serviceName, payload, opt_origin) {
// This check covers the very rare (but producable) case where the inner frame
// becomes ready and sends its setup message while the outer frame is
// deferring its connect method waiting for the inner frame to be ready. The
// resulting deferral ensures the message will not be processed until the
// channel is fully configured.
if (this.peerWindowDeferred_) {
this.deferredDeliveries_.push(
goog.bind(this.xpcDeliver, this, serviceName, payload, opt_origin));
return;
}
// Check whether the origin of the message is as expected.
if (!this.isMessageOriginAcceptable_(opt_origin)) {
goog.log.warning(goog.net.xpc.logger,
'Message received from unapproved origin "' +
opt_origin + '" - rejected.');
return;
}
if (this.isDisposed()) {
goog.log.warning(goog.net.xpc.logger,
'CrossPageChannel::xpcDeliver(): Disposed.');
} else if (!serviceName ||
serviceName == goog.net.xpc.TRANSPORT_SERVICE_) {
this.transport_.transportServiceHandler(payload);
} else {
// only deliver messages if connected
if (this.isConnected()) {
this.deliver(this.unescapeServiceName_(serviceName), payload);
} else {
goog.log.info(goog.net.xpc.logger,
'CrossPageChannel::xpcDeliver(): Not connected.');
}
}
};
/**
* Escape the user-provided service name for sending across the channel. This
* URL-encodes certain special characters so they don't conflict with delimiters
* used by some of the transports, and adds a special prefix if the name
* conflicts with the reserved transport service name.
*
* This is the opposite of {@link #unescapeServiceName_}.
*
* @param {string} name The name of the service to escape.
* @return {string} The escaped service name.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.escapeServiceName_ = function(name) {
if (goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_ESCAPE_RE_.test(name)) {
name = '%' + name;
}
return name.replace(/[%:|]/g, encodeURIComponent);
};
/**
* Unescape the escaped service name that was sent across the channel. This is
* the opposite of {@link #escapeServiceName_}.
*
* @param {string} name The name of the service to unescape.
* @return {string} The unescaped service name.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.unescapeServiceName_ = function(name) {
name = name.replace(/%[0-9a-f]{2}/gi, decodeURIComponent);
if (goog.net.xpc.CrossPageChannel.TRANSPORT_SERVICE_UNESCAPE_RE_.test(name)) {
return name.substring(1);
} else {
return name;
}
};
/**
* Returns the role of this channel (either inner or outer).
* @return {number} The role of this channel.
*/
goog.net.xpc.CrossPageChannel.prototype.getRole = function() {
var role = this.cfg_[goog.net.xpc.CfgFields.ROLE];
if (role) {
return role;
} else {
return window.parent == this.peerWindowObject_ ?
goog.net.xpc.CrossPageChannelRole.INNER :
goog.net.xpc.CrossPageChannelRole.OUTER;
}
};
/**
* Returns whether an incoming message with the given origin is acceptable.
* If an incoming request comes with a specified (non-empty) origin, and the
* PEER_HOSTNAME config parameter has also been provided, the two must match,
* or the message is unacceptable.
* @param {string=} opt_origin The origin associated with the incoming message.
* @return {boolean} Whether the message is acceptable.
* @private
*/
goog.net.xpc.CrossPageChannel.prototype.isMessageOriginAcceptable_ = function(
opt_origin) {
var peerHostname = this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME];
return goog.string.isEmptySafe(opt_origin) ||
goog.string.isEmptySafe(peerHostname) ||
opt_origin == this.cfg_[goog.net.xpc.CfgFields.PEER_HOSTNAME];
};
/** @override */
goog.net.xpc.CrossPageChannel.prototype.disposeInternal = function() {
this.close();
this.peerWindowObject_ = null;
this.iframeElement_ = null;
delete goog.net.xpc.channels[this.name];
goog.dispose(this.peerLoadHandler_);
delete this.peerLoadHandler_;
goog.base(this, 'disposeInternal');
};
/**
* Disposes all channels.
* @private
*/
goog.net.xpc.CrossPageChannel.disposeAll_ = function() {
for (var name in goog.net.xpc.channels) {
goog.dispose(goog.net.xpc.channels[name]);
}
};
@@ -0,0 +1,30 @@
// Copyright 2007 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 Provides the enum for the role of the CrossPageChannel.
*
*/
goog.provide('goog.net.xpc.CrossPageChannelRole');
/**
* The role of the peer.
* @enum {number}
*/
goog.net.xpc.CrossPageChannelRole = {
OUTER: 0,
INNER: 1
};
@@ -0,0 +1,254 @@
// Copyright 2007 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 Contains the frame element method transport for cross-domain
* communication. It exploits the fact that FF lets a page in an
* iframe call a method on the iframe-element it is contained in, even if the
* containing page is from a different domain.
*
*/
goog.provide('goog.net.xpc.FrameElementMethodTransport');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
/**
* Frame-element method transport.
*
* Firefox allows a document within an iframe to call methods on the
* iframe-element added by the containing document.
* NOTE(user): Tested in all FF versions starting from 1.0
*
* @param {goog.net.xpc.CrossPageChannel} channel The channel this transport
* belongs to.
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for finding
* the correct window.
* @constructor
* @extends {goog.net.xpc.Transport}
*/
goog.net.xpc.FrameElementMethodTransport = function(channel, opt_domHelper) {
goog.base(this, opt_domHelper);
/**
* The channel this transport belongs to.
* @type {goog.net.xpc.CrossPageChannel}
* @private
*/
this.channel_ = channel;
// To transfer messages, this transport basically uses normal function calls,
// which are synchronous. To avoid endless recursion, the delivery has to
// be artificially made asynchronous.
/**
* Array for queued messages.
* @type {Array}
* @private
*/
this.queue_ = [];
/**
* Callback function which wraps deliverQueued_.
* @type {Function}
* @private
*/
this.deliverQueuedCb_ = goog.bind(this.deliverQueued_, this);
};
goog.inherits(goog.net.xpc.FrameElementMethodTransport, goog.net.xpc.Transport);
/**
* The transport type.
* @type {number}
* @protected
* @override
*/
goog.net.xpc.FrameElementMethodTransport.prototype.transportType =
goog.net.xpc.TransportTypes.FRAME_ELEMENT_METHOD;
/**
* Flag used to enforce asynchronous messaging semantics.
* @type {boolean}
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.recursive_ = false;
/**
* Timer used to enforce asynchronous message delivery.
* @type {number}
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.timer_ = 0;
/**
* Holds the function to send messages to the peer
* (once it becomes available).
* @type {Function}
* @private
*/
goog.net.xpc.FrameElementMethodTransport.outgoing_ = null;
/**
* Connect this transport.
* @override
*/
goog.net.xpc.FrameElementMethodTransport.prototype.connect = function() {
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER) {
// get shortcut to iframe-element
this.iframeElm_ = this.channel_.getIframeElement();
// add the gateway function to the iframe-element
// (to be called by the peer)
this.iframeElm_['XPC_toOuter'] = goog.bind(this.incoming_, this);
// at this point we just have to wait for a notification from the peer...
} else {
this.attemptSetup_();
}
};
/**
* Only used from within an iframe. Attempts to attach the method
* to be used for sending messages by the containing document. Has to
* wait until the containing document has finished. Therefore calls
* itself in a timeout if not successful.
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.attemptSetup_ = function() {
var retry = true;
/** @preserveTry */
try {
if (!this.iframeElm_) {
// throws security exception when called too early
this.iframeElm_ = this.getWindow().frameElement;
}
// check if iframe-element and the gateway-function to the
// outer-frame are present
// TODO(user) Make sure the following code doesn't throw any exceptions
if (this.iframeElm_ && this.iframeElm_['XPC_toOuter']) {
// get a reference to the gateway function
this.outgoing_ = this.iframeElm_['XPC_toOuter'];
// attach the gateway function the other document will use
this.iframeElm_['XPC_toOuter']['XPC_toInner'] =
goog.bind(this.incoming_, this);
// stop retrying
retry = false;
// notify outer frame
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_);
// notify channel that the transport is ready
this.channel_.notifyConnected();
}
}
catch (e) {
goog.log.error(goog.net.xpc.logger,
'exception caught while attempting setup: ' + e);
}
// retry necessary?
if (retry) {
if (!this.attemptSetupCb_) {
this.attemptSetupCb_ = goog.bind(this.attemptSetup_, this);
}
this.getWindow().setTimeout(this.attemptSetupCb_, 100);
}
};
/**
* Handles transport service messages.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.FrameElementMethodTransport.prototype.transportServiceHandler =
function(payload) {
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER &&
!this.channel_.isConnected() && payload == goog.net.xpc.SETUP_ACK_) {
// get a reference to the gateway function
this.outgoing_ = this.iframeElm_['XPC_toOuter']['XPC_toInner'];
// notify the channel we're ready
this.channel_.notifyConnected();
} else {
throw Error('Got unexpected transport message.');
}
};
/**
* Process incoming message.
* @param {string} serviceName The name of the service the message is to be
* delivered to.
* @param {string} payload The message to process.
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.incoming_ =
function(serviceName, payload) {
if (!this.recursive_ && this.queue_.length == 0) {
this.channel_.xpcDeliver(serviceName, payload);
}
else {
this.queue_.push({serviceName: serviceName, payload: payload});
if (this.queue_.length == 1) {
this.timer_ = this.getWindow().setTimeout(this.deliverQueuedCb_, 1);
}
}
};
/**
* Delivers queued messages.
* @private
*/
goog.net.xpc.FrameElementMethodTransport.prototype.deliverQueued_ =
function() {
while (this.queue_.length) {
var msg = this.queue_.shift();
this.channel_.xpcDeliver(msg.serviceName, msg.payload);
}
};
/**
* Send a message
* @param {string} service The name off the service the message is to be
* delivered to.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.FrameElementMethodTransport.prototype.send =
function(service, payload) {
this.recursive_ = true;
this.outgoing_(service, payload);
this.recursive_ = false;
};
/** @override */
goog.net.xpc.FrameElementMethodTransport.prototype.disposeInternal =
function() {
goog.net.xpc.FrameElementMethodTransport.superClass_.disposeInternal.call(
this);
this.outgoing_ = null;
this.iframeElm_ = null;
};
@@ -0,0 +1,921 @@
// Copyright 2007 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 Contains the iframe polling transport.
*/
goog.provide('goog.net.xpc.IframePollingTransport');
goog.provide('goog.net.xpc.IframePollingTransport.Receiver');
goog.provide('goog.net.xpc.IframePollingTransport.Sender');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
goog.require('goog.userAgent');
/**
* Iframe polling transport. Uses hidden iframes to transfer data
* in the fragment identifier of the URL. The peer polls the iframe's location
* for changes.
* Unfortunately, in Safari this screws up the history, because Safari doesn't
* allow to call location.replace() on a window containing a document from a
* different domain (last version tested: 2.0.4).
*
* @param {goog.net.xpc.CrossPageChannel} channel The channel this
* transport belongs to.
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for finding
* the correct window.
* @constructor
* @extends {goog.net.xpc.Transport}
*/
goog.net.xpc.IframePollingTransport = function(channel, opt_domHelper) {
goog.base(this, opt_domHelper);
/**
* The channel this transport belongs to.
* @type {goog.net.xpc.CrossPageChannel}
* @private
*/
this.channel_ = channel;
/**
* The URI used to send messages.
* @type {string}
* @private
*/
this.sendUri_ =
this.channel_.getConfig()[goog.net.xpc.CfgFields.PEER_POLL_URI];
/**
* The URI which is polled for incoming messages.
* @type {string}
* @private
*/
this.rcvUri_ =
this.channel_.getConfig()[goog.net.xpc.CfgFields.LOCAL_POLL_URI];
/**
* The queue to hold messages which can't be sent immediately.
* @type {Array}
* @private
*/
this.sendQueue_ = [];
};
goog.inherits(goog.net.xpc.IframePollingTransport, goog.net.xpc.Transport);
/**
* The number of times the inner frame will check for evidence of the outer
* frame before it tries its reconnection sequence. These occur at 100ms
* intervals, making this an effective max waiting period of 500ms.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.pollsBeforeReconnect_ = 5;
/**
* The transport type.
* @type {number}
* @protected
* @override
*/
goog.net.xpc.IframePollingTransport.prototype.transportType =
goog.net.xpc.TransportTypes.IFRAME_POLLING;
/**
* Sequence counter.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.sequence_ = 0;
/**
* Flag indicating whether we are waiting for an acknoledgement.
* @type {boolean}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.waitForAck_ = false;
/**
* Flag indicating if channel has been initialized.
* @type {boolean}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.initialized_ = false;
/**
* Reconnection iframe created by inner peer.
* @type {Element}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.reconnectFrame_ = null;
/**
* The string used to prefix all iframe names and IDs.
* @type {string}
*/
goog.net.xpc.IframePollingTransport.IFRAME_PREFIX = 'googlexpc';
/**
* Returns the name/ID of the message frame.
* @return {string} Name of message frame.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.getMsgFrameName_ = function() {
return goog.net.xpc.IframePollingTransport.IFRAME_PREFIX + '_' +
this.channel_.name + '_msg';
};
/**
* Returns the name/ID of the ack frame.
* @return {string} Name of ack frame.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.getAckFrameName_ = function() {
return goog.net.xpc.IframePollingTransport.IFRAME_PREFIX + '_' +
this.channel_.name + '_ack';
};
/**
* Determines whether the channel is still available. The channel is
* unavailable if the transport was disposed or the peer is no longer
* available.
* @return {boolean} Whether the channel is available.
*/
goog.net.xpc.IframePollingTransport.prototype.isChannelAvailable = function() {
return !this.isDisposed() && this.channel_.isPeerAvailable();
};
/**
* Safely retrieves the frames from the peer window. If an error is thrown
* (e.g. the window is closing) an empty frame object is returned.
* @return {!Object.<!Window>} The frames from the peer window.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.getPeerFrames_ = function() {
try {
if (this.isChannelAvailable()) {
return this.channel_.getPeerWindowObject().frames || {};
}
} catch (e) {
// An error may be thrown if the window is closing.
goog.log.fine(goog.net.xpc.logger, 'error retrieving peer frames');
}
return {};
};
/**
* Safely retrieves the peer frame with the specified name.
* @param {string} frameName The name of the peer frame to retrieve.
* @return {Window} The peer frame with the specified name.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.getPeerFrame_ = function(
frameName) {
return this.getPeerFrames_()[frameName];
};
/**
* Connects this transport.
* @override
*/
goog.net.xpc.IframePollingTransport.prototype.connect = function() {
if (!this.isChannelAvailable()) {
// When the channel is unavailable there is no peer to poll so stop trying
// to connect.
return;
}
goog.log.fine(goog.net.xpc.logger, 'transport connect called');
if (!this.initialized_) {
goog.log.fine(goog.net.xpc.logger, 'initializing...');
this.constructSenderFrames_();
this.initialized_ = true;
}
this.checkForeignFramesReady_();
};
/**
* Creates the iframes which are used to send messages (and acknowledgements)
* to the peer. Sender iframes contain a document from a different origin and
* therefore their content can't be accessed.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.constructSenderFrames_ =
function() {
var name = this.getMsgFrameName_();
this.msgIframeElm_ = this.constructSenderFrame_(name);
this.msgWinObj_ = this.getWindow().frames[name];
name = this.getAckFrameName_();
this.ackIframeElm_ = this.constructSenderFrame_(name);
this.ackWinObj_ = this.getWindow().frames[name];
};
/**
* Constructs a sending frame the the given id.
* @param {string} id The id.
* @return {Element} The constructed frame.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.constructSenderFrame_ =
function(id) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'constructing sender frame: ' + id);
var ifr = goog.dom.createElement('iframe');
var s = ifr.style;
s.position = 'absolute';
s.top = '-10px'; s.left = '10px'; s.width = '1px'; s.height = '1px';
ifr.id = ifr.name = id;
ifr.src = this.sendUri_ + '#INITIAL';
this.getWindow().document.body.appendChild(ifr);
return ifr;
};
/**
* The protocol for reconnecting is for the inner frame to change channel
* names, and then communicate the new channel name to the outer peer.
* The outer peer looks in a predefined location for the channel name
* upate. It is important to use a completely new channel name, as this
* will ensure that all messaging iframes are not in the bfcache.
* Otherwise, Safari may pollute the history when modifying the location
* of bfcached iframes.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.maybeInnerPeerReconnect_ =
function() {
// Reconnection has been found to not function on some browsers (eg IE7), so
// it's important that the mechanism only be triggered as a last resort. As
// such, we poll a number of times to find the outer iframe before triggering
// it.
if (this.reconnectFrame_ || this.pollsBeforeReconnect_-- > 0) {
return;
}
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'Inner peer reconnect triggered.');
this.channel_.name = goog.net.xpc.getRandomString(10);
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'switching channels: ' + this.channel_.name);
this.deconstructSenderFrames_();
this.initialized_ = false;
// Communicate new channel name to outer peer.
this.reconnectFrame_ = this.constructSenderFrame_(
goog.net.xpc.IframePollingTransport.IFRAME_PREFIX +
'_reconnect_' + this.channel_.name);
};
/**
* Scans inner peer for a reconnect message, which will be used to update
* the outer peer's channel name. If a reconnect message is found, the
* sender frames will be cleaned up to make way for the new sender frames.
* Only called by the outer peer.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.outerPeerReconnect_ = function() {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'outerPeerReconnect called');
var frames = this.getPeerFrames_();
var length = frames.length;
for (var i = 0; i < length; i++) {
var frameName;
try {
if (frames[i] && frames[i].name) {
frameName = frames[i].name;
}
} catch (e) {
// Do nothing.
}
if (!frameName) {
continue;
}
var message = frameName.split('_');
if (message.length == 3 &&
message[0] == goog.net.xpc.IframePollingTransport.IFRAME_PREFIX &&
message[1] == 'reconnect') {
// This is a legitimate reconnect message from the peer. Start using
// the peer provided channel name, and start a connection over from
// scratch.
this.channel_.name = message[2];
this.deconstructSenderFrames_();
this.initialized_ = false;
break;
}
}
};
/**
* Cleans up the existing sender frames owned by this peer. Only called by
* the outer peer.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.deconstructSenderFrames_ =
function() {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'deconstructSenderFrames called');
if (this.msgIframeElm_) {
this.msgIframeElm_.parentNode.removeChild(this.msgIframeElm_);
this.msgIframeElm_ = null;
this.msgWinObj_ = null;
}
if (this.ackIframeElm_) {
this.ackIframeElm_.parentNode.removeChild(this.ackIframeElm_);
this.ackIframeElm_ = null;
this.ackWinObj_ = null;
}
};
/**
* Checks if the frames in the peer's page are ready. These contain a
* document from the own domain and are the ones messages are received through.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.checkForeignFramesReady_ =
function() {
// check if the connected iframe ready
if (!(this.isRcvFrameReady_(this.getMsgFrameName_()) &&
this.isRcvFrameReady_(this.getAckFrameName_()))) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'foreign frames not (yet) present');
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.INNER) {
// The outer peer might need a short time to get its frames ready, as
// CrossPageChannel prevents them from getting created until the inner
// peer's frame has thrown its loaded event. This method is a noop for
// the first few times it's called, and then allows the reconnection
// sequence to begin.
this.maybeInnerPeerReconnect_();
} else if (this.channel_.getRole() ==
goog.net.xpc.CrossPageChannelRole.OUTER) {
// The inner peer is either not loaded yet, or the receiving
// frames are simply missing. Since we cannot discern the two cases, we
// should scan for a reconnect message from the inner peer.
this.outerPeerReconnect_();
}
// start a timer to check again
this.getWindow().setTimeout(goog.bind(this.connect, this), 100);
} else {
goog.log.fine(goog.net.xpc.logger, 'foreign frames present');
// Create receivers.
this.msgReceiver_ = new goog.net.xpc.IframePollingTransport.Receiver(
this,
this.getPeerFrame_(this.getMsgFrameName_()),
goog.bind(this.processIncomingMsg, this));
this.ackReceiver_ = new goog.net.xpc.IframePollingTransport.Receiver(
this,
this.getPeerFrame_(this.getAckFrameName_()),
goog.bind(this.processIncomingAck, this));
this.checkLocalFramesPresent_();
}
};
/**
* Checks if the receiving frame is ready.
* @param {string} frameName Which receiving frame to check.
* @return {boolean} Whether the receiving frame is ready.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.isRcvFrameReady_ =
function(frameName) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'checking for receive frame: ' + frameName);
/** @preserveTry */
try {
var winObj = this.getPeerFrame_(frameName);
if (!winObj || winObj.location.href.indexOf(this.rcvUri_) != 0) {
return false;
}
} catch (e) {
return false;
}
return true;
};
/**
* Checks if the iframes created in the own document are ready.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.checkLocalFramesPresent_ =
function() {
// Are the sender frames ready?
// These contain a document from the peer's domain, therefore we can only
// check if the frame itself is present.
var frames = this.getPeerFrames_();
if (!(frames[this.getAckFrameName_()] &&
frames[this.getMsgFrameName_()])) {
// start a timer to check again
if (!this.checkLocalFramesPresentCb_) {
this.checkLocalFramesPresentCb_ = goog.bind(
this.checkLocalFramesPresent_, this);
}
this.getWindow().setTimeout(this.checkLocalFramesPresentCb_, 100);
goog.log.fine(goog.net.xpc.logger, 'local frames not (yet) present');
} else {
// Create senders.
this.msgSender_ = new goog.net.xpc.IframePollingTransport.Sender(
this.sendUri_, this.msgWinObj_);
this.ackSender_ = new goog.net.xpc.IframePollingTransport.Sender(
this.sendUri_, this.ackWinObj_);
goog.log.fine(goog.net.xpc.logger, 'local frames ready');
this.getWindow().setTimeout(goog.bind(function() {
this.msgSender_.send(goog.net.xpc.SETUP);
this.sentConnectionSetup_ = true;
this.waitForAck_ = true;
goog.log.fine(goog.net.xpc.logger, 'SETUP sent');
}, this), 100);
}
};
/**
* Check if connection is ready.
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.checkIfConnected_ = function() {
if (this.sentConnectionSetupAck_ && this.rcvdConnectionSetupAck_) {
this.channel_.notifyConnected();
if (this.deliveryQueue_) {
goog.log.fine(goog.net.xpc.logger, 'delivering queued messages ' +
'(' + this.deliveryQueue_.length + ')');
for (var i = 0, m; i < this.deliveryQueue_.length; i++) {
m = this.deliveryQueue_[i];
this.channel_.xpcDeliver(m.service, m.payload);
}
delete this.deliveryQueue_;
}
} else {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'checking if connected: ' +
'ack sent:' + this.sentConnectionSetupAck_ +
', ack rcvd: ' + this.rcvdConnectionSetupAck_);
}
};
/**
* Processes an incoming message.
* @param {string} raw The complete received string.
*/
goog.net.xpc.IframePollingTransport.prototype.processIncomingMsg =
function(raw) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'msg received: ' + raw);
if (raw == goog.net.xpc.SETUP) {
if (!this.ackSender_) {
// Got SETUP msg, but we can't send an ack.
return;
}
this.ackSender_.send(goog.net.xpc.SETUP_ACK_);
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST, 'SETUP_ACK sent');
this.sentConnectionSetupAck_ = true;
this.checkIfConnected_();
} else if (this.channel_.isConnected() || this.sentConnectionSetupAck_) {
var pos = raw.indexOf('|');
var head = raw.substring(0, pos);
var frame = raw.substring(pos + 1);
// check if it is a framed message
pos = head.indexOf(',');
if (pos == -1) {
var seq = head;
// send acknowledgement
this.ackSender_.send('ACK:' + seq);
this.deliverPayload_(frame);
} else {
var seq = head.substring(0, pos);
// send acknowledgement
this.ackSender_.send('ACK:' + seq);
var partInfo = head.substring(pos + 1).split('/');
var part0 = parseInt(partInfo[0], 10);
var part1 = parseInt(partInfo[1], 10);
// create an array to accumulate the parts if this is the
// first frame of a message
if (part0 == 1) {
this.parts_ = [];
}
this.parts_.push(frame);
// deliver the message if this was the last frame of a message
if (part0 == part1) {
this.deliverPayload_(this.parts_.join(''));
delete this.parts_;
}
}
} else {
goog.log.warning(goog.net.xpc.logger,
'received msg, but channel is not connected');
}
};
/**
* Process an incoming acknowdedgement.
* @param {string} msgStr The incoming ack string to process.
*/
goog.net.xpc.IframePollingTransport.prototype.processIncomingAck =
function(msgStr) {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'ack received: ' + msgStr);
if (msgStr == goog.net.xpc.SETUP_ACK_) {
this.waitForAck_ = false;
this.rcvdConnectionSetupAck_ = true;
// send the next frame
this.checkIfConnected_();
} else if (this.channel_.isConnected()) {
if (!this.waitForAck_) {
goog.log.warning(goog.net.xpc.logger, 'got unexpected ack');
return;
}
var seq = parseInt(msgStr.split(':')[1], 10);
if (seq == this.sequence_) {
this.waitForAck_ = false;
this.sendNextFrame_();
} else {
goog.log.warning(goog.net.xpc.logger, 'got ack with wrong sequence');
}
} else {
goog.log.warning(goog.net.xpc.logger,
'received ack, but channel not connected');
}
};
/**
* Sends a frame (message part).
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.sendNextFrame_ = function() {
// do nothing if we are waiting for an acknowledgement or the
// queue is emtpy
if (this.waitForAck_ || !this.sendQueue_.length) {
return;
}
var s = this.sendQueue_.shift();
++this.sequence_;
this.msgSender_.send(this.sequence_ + s);
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'msg sent: ' + this.sequence_ + s);
this.waitForAck_ = true;
};
/**
* Delivers a message.
* @param {string} s The complete message string ("<service_name>:<payload>").
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.deliverPayload_ = function(s) {
// determine the service name and the payload
var pos = s.indexOf(':');
var service = s.substr(0, pos);
var payload = s.substring(pos + 1);
// deliver the message
if (!this.channel_.isConnected()) {
// as valid messages can come in before a SETUP_ACK has
// been received (because subchannels for msgs and acks are independent),
// delay delivery of early messages until after 'connect'-event
(this.deliveryQueue_ || (this.deliveryQueue_ = [])).
push({service: service, payload: payload});
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'queued delivery');
} else {
this.channel_.xpcDeliver(service, payload);
}
};
// ---- send message ----
/**
* Maximal frame length.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.prototype.MAX_FRAME_LENGTH_ = 3800;
/**
* Sends a message. Splits it in multiple frames if too long (exceeds IE's
* URL-length maximum.
* Wireformat: <seq>[,<frame_no>/<#frames>]|<frame_content>
*
* @param {string} service Name of service this the message has to be delivered.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.IframePollingTransport.prototype.send =
function(service, payload) {
var frame = service + ':' + payload;
// put in queue
if (!goog.userAgent.IE || payload.length <= this.MAX_FRAME_LENGTH_) {
this.sendQueue_.push('|' + frame);
}
else {
var l = payload.length;
var num = Math.ceil(l / this.MAX_FRAME_LENGTH_); // number of frames
var pos = 0;
var i = 1;
while (pos < l) {
this.sendQueue_.push(',' + i + '/' + num + '|' +
frame.substr(pos, this.MAX_FRAME_LENGTH_));
i++;
pos += this.MAX_FRAME_LENGTH_;
}
}
this.sendNextFrame_();
};
/** @override */
goog.net.xpc.IframePollingTransport.prototype.disposeInternal = function() {
goog.base(this, 'disposeInternal');
var receivers = goog.net.xpc.IframePollingTransport.receivers_;
goog.array.remove(receivers, this.msgReceiver_);
goog.array.remove(receivers, this.ackReceiver_);
this.msgReceiver_ = this.ackReceiver_ = null;
goog.dom.removeNode(this.msgIframeElm_);
goog.dom.removeNode(this.ackIframeElm_);
this.msgIframeElm_ = this.ackIframeElm_ = null;
this.msgWinObj_ = this.ackWinObj_ = null;
};
/**
* Array holding all Receiver-instances.
* @type {Array.<goog.net.xpc.IframePollingTransport.Receiver>}
* @private
*/
goog.net.xpc.IframePollingTransport.receivers_ = [];
/**
* Short polling interval.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.TIME_POLL_SHORT_ = 10;
/**
* Long polling interval.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.TIME_POLL_LONG_ = 100;
/**
* Period how long to use TIME_POLL_SHORT_ before raising polling-interval
* to TIME_POLL_LONG_ after an activity.
* @type {number}
* @private
*/
goog.net.xpc.IframePollingTransport.TIME_SHORT_POLL_AFTER_ACTIVITY_ =
1000;
/**
* Polls all receivers.
* @private
*/
goog.net.xpc.IframePollingTransport.receive_ = function() {
var receivers = goog.net.xpc.IframePollingTransport.receivers_;
var receiver;
var rcvd = false;
/** @preserveTry */
try {
for (var i = 0; receiver = receivers[i]; i++) {
rcvd = rcvd || receiver.receive();
}
} catch (e) {
goog.log.info(goog.net.xpc.logger, 'receive_() failed: ' + e);
// Notify the channel that the transport had an error.
receiver.transport_.channel_.notifyTransportError();
// notifyTransportError() closes the channel and disposes the transport.
// If there are no other channels present, this.receivers_ will now be empty
// and there is no need to keep polling.
if (!receivers.length) {
return;
}
}
var now = goog.now();
if (rcvd) {
goog.net.xpc.IframePollingTransport.lastActivity_ = now;
}
// Schedule next check.
var t = now - goog.net.xpc.IframePollingTransport.lastActivity_ <
goog.net.xpc.IframePollingTransport.TIME_SHORT_POLL_AFTER_ACTIVITY_ ?
goog.net.xpc.IframePollingTransport.TIME_POLL_SHORT_ :
goog.net.xpc.IframePollingTransport.TIME_POLL_LONG_;
goog.net.xpc.IframePollingTransport.rcvTimer_ = window.setTimeout(
goog.net.xpc.IframePollingTransport.receiveCb_, t);
};
/**
* Callback that wraps receive_ to be used in timers.
* @type {Function}
* @private
*/
goog.net.xpc.IframePollingTransport.receiveCb_ = goog.bind(
goog.net.xpc.IframePollingTransport.receive_,
goog.net.xpc.IframePollingTransport);
/**
* Starts the polling loop.
* @private
*/
goog.net.xpc.IframePollingTransport.startRcvTimer_ = function() {
goog.log.fine(goog.net.xpc.logger, 'starting receive-timer');
goog.net.xpc.IframePollingTransport.lastActivity_ = goog.now();
if (goog.net.xpc.IframePollingTransport.rcvTimer_) {
window.clearTimeout(goog.net.xpc.IframePollingTransport.rcvTimer_);
}
goog.net.xpc.IframePollingTransport.rcvTimer_ = window.setTimeout(
goog.net.xpc.IframePollingTransport.receiveCb_,
goog.net.xpc.IframePollingTransport.TIME_POLL_SHORT_);
};
/**
* goog.net.xpc.IframePollingTransport.Sender
*
* Utility class to send message-parts to a document from a different origin.
*
* @constructor
* @param {string} url The url the other document will use for polling.
* @param {Object} windowObj The frame used for sending information to.
*/
goog.net.xpc.IframePollingTransport.Sender = function(url, windowObj) {
/**
* The URI used to sending messages.
* @type {string}
* @private
*/
this.sendUri_ = url;
/**
* The window object of the iframe used to send messages.
* The script instantiating the Sender won't have access to
* the content of sendFrame_.
* @type {Object}
* @private
*/
this.sendFrame_ = windowObj;
/**
* Cycle counter (used to make sure that sending two identical messages sent
* in direct succession can be recognized as such by the receiver).
* @type {number}
* @private
*/
this.cycle_ = 0;
};
/**
* Sends a message-part (frame) to the peer.
* The message-part is encoded and put in the fragment identifier
* of the URL used for sending (and belongs to the origin/domain of the peer).
* @param {string} payload The message to send.
*/
goog.net.xpc.IframePollingTransport.Sender.prototype.send = function(payload) {
this.cycle_ = ++this.cycle_ % 2;
var url = this.sendUri_ + '#' + this.cycle_ + encodeURIComponent(payload);
// TODO(user) Find out if try/catch is still needed
/** @preserveTry */
try {
// safari doesn't allow to call location.replace()
if (goog.userAgent.WEBKIT) {
this.sendFrame_.location.href = url;
} else {
this.sendFrame_.location.replace(url);
}
} catch (e) {
goog.log.error(goog.net.xpc.logger, 'sending failed', e);
}
// Restart receiver timer on short polling interval, to support use-cases
// where we need to capture responses quickly.
goog.net.xpc.IframePollingTransport.startRcvTimer_();
};
/**
* goog.net.xpc.IframePollingTransport.Receiver
*
* @constructor
* @param {goog.net.xpc.IframePollingTransport} transport The transport to
* receive from.
* @param {Object} windowObj The window-object to poll for location-changes.
* @param {Function} callback The callback-function to be called when
* location has changed.
*/
goog.net.xpc.IframePollingTransport.Receiver = function(transport,
windowObj,
callback) {
/**
* The transport to receive from.
* @type {goog.net.xpc.IframePollingTransport}
* @private
*/
this.transport_ = transport;
this.rcvFrame_ = windowObj;
this.cb_ = callback;
this.currentLoc_ = this.rcvFrame_.location.href.split('#')[0] + '#INITIAL';
goog.net.xpc.IframePollingTransport.receivers_.push(this);
goog.net.xpc.IframePollingTransport.startRcvTimer_();
};
/**
* Polls the location of the receiver-frame for changes.
* @return {boolean} Whether a change has been detected.
*/
goog.net.xpc.IframePollingTransport.Receiver.prototype.receive = function() {
var loc = this.rcvFrame_.location.href;
if (loc != this.currentLoc_) {
this.currentLoc_ = loc;
var payload = loc.split('#')[1];
if (payload) {
payload = payload.substr(1); // discard first character (cycle)
this.cb_(decodeURIComponent(payload));
}
return true;
} else {
return false;
}
};
@@ -0,0 +1,394 @@
// Copyright 2007 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 Contains the iframe relay tranport.
*/
goog.provide('goog.net.xpc.IframeRelayTransport');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.Transport');
goog.require('goog.userAgent');
/**
* Iframe relay transport. Creates hidden iframes containing a document
* from the peer's origin. Data is transferred in the fragment identifier.
* Therefore the document loaded in the iframes can be served from the
* browser's cache.
*
* @param {goog.net.xpc.CrossPageChannel} channel The channel this
* transport belongs to.
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for finding
* the correct window.
* @constructor
* @extends {goog.net.xpc.Transport}
*/
goog.net.xpc.IframeRelayTransport = function(channel, opt_domHelper) {
goog.base(this, opt_domHelper);
/**
* The channel this transport belongs to.
* @type {goog.net.xpc.CrossPageChannel}
* @private
*/
this.channel_ = channel;
/**
* The URI used to relay data to the peer.
* @type {string}
* @private
*/
this.peerRelayUri_ =
this.channel_.getConfig()[goog.net.xpc.CfgFields.PEER_RELAY_URI];
/**
* The id of the iframe the peer page lives in.
* @type {string}
* @private
*/
this.peerIframeId_ =
this.channel_.getConfig()[goog.net.xpc.CfgFields.IFRAME_ID];
if (goog.userAgent.WEBKIT) {
goog.net.xpc.IframeRelayTransport.startCleanupTimer_();
}
};
goog.inherits(goog.net.xpc.IframeRelayTransport, goog.net.xpc.Transport);
if (goog.userAgent.WEBKIT) {
/**
* Array to keep references to the relay-iframes. Used only if
* there is no way to detect when the iframes are loaded. In that
* case the relay-iframes are removed after a timeout.
* @type {Array.<Object>}
* @private
*/
goog.net.xpc.IframeRelayTransport.iframeRefs_ = [];
/**
* Interval at which iframes are destroyed.
* @type {number}
* @private
*/
goog.net.xpc.IframeRelayTransport.CLEANUP_INTERVAL_ = 1000;
/**
* Time after which a relay-iframe is destroyed.
* @type {number}
* @private
*/
goog.net.xpc.IframeRelayTransport.IFRAME_MAX_AGE_ = 3000;
/**
* The cleanup timer id.
* @type {number}
* @private
*/
goog.net.xpc.IframeRelayTransport.cleanupTimer_ = 0;
/**
* Starts the cleanup timer.
* @private
*/
goog.net.xpc.IframeRelayTransport.startCleanupTimer_ = function() {
if (!goog.net.xpc.IframeRelayTransport.cleanupTimer_) {
goog.net.xpc.IframeRelayTransport.cleanupTimer_ = window.setTimeout(
function() { goog.net.xpc.IframeRelayTransport.cleanup_(); },
goog.net.xpc.IframeRelayTransport.CLEANUP_INTERVAL_);
}
};
/**
* Remove all relay-iframes which are older than the maximal age.
* @param {number=} opt_maxAge The maximal age in milliseconds.
* @private
*/
goog.net.xpc.IframeRelayTransport.cleanup_ = function(opt_maxAge) {
var now = goog.now();
var maxAge =
opt_maxAge || goog.net.xpc.IframeRelayTransport.IFRAME_MAX_AGE_;
while (goog.net.xpc.IframeRelayTransport.iframeRefs_.length &&
now - goog.net.xpc.IframeRelayTransport.iframeRefs_[0].timestamp >=
maxAge) {
var ifr = goog.net.xpc.IframeRelayTransport.iframeRefs_.
shift().iframeElement;
goog.dom.removeNode(ifr);
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST,
'iframe removed');
}
goog.net.xpc.IframeRelayTransport.cleanupTimer_ = window.setTimeout(
goog.net.xpc.IframeRelayTransport.cleanupCb_,
goog.net.xpc.IframeRelayTransport.CLEANUP_INTERVAL_);
};
/**
* Function which wraps cleanup_().
* @private
*/
goog.net.xpc.IframeRelayTransport.cleanupCb_ = function() {
goog.net.xpc.IframeRelayTransport.cleanup_();
};
}
/**
* Maximum sendable size of a payload via a single iframe in IE.
* @type {number}
* @private
*/
goog.net.xpc.IframeRelayTransport.IE_PAYLOAD_MAX_SIZE_ = 1800;
/**
* @typedef {{fragments: !Array.<string>, received: number, expected: number}}
*/
goog.net.xpc.IframeRelayTransport.FragmentInfo;
/**
* Used to track incoming payload fragments. The implementation can process
* incoming fragments from several channels at a time, even if data is
* out-of-order or interleaved.
*
* @type {!Object.<string, !goog.net.xpc.IframeRelayTransport.FragmentInfo>}
* @private
*/
goog.net.xpc.IframeRelayTransport.fragmentMap_ = {};
/**
* The transport type.
* @type {number}
* @override
*/
goog.net.xpc.IframeRelayTransport.prototype.transportType =
goog.net.xpc.TransportTypes.IFRAME_RELAY;
/**
* Connects this transport.
* @override
*/
goog.net.xpc.IframeRelayTransport.prototype.connect = function() {
if (!this.getWindow()['xpcRelay']) {
this.getWindow()['xpcRelay'] =
goog.net.xpc.IframeRelayTransport.receiveMessage_;
}
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP);
};
/**
* Processes an incoming message.
*
* @param {string} channelName The name of the channel.
* @param {string} frame The raw frame content.
* @private
*/
goog.net.xpc.IframeRelayTransport.receiveMessage_ =
function(channelName, frame) {
var pos = frame.indexOf(':');
var header = frame.substr(0, pos);
var payload = frame.substr(pos + 1);
if (!goog.userAgent.IE || (pos = header.indexOf('|')) == -1) {
// First, the easy case.
var service = header;
} else {
// There was a fragment id in the header, so this is a message
// fragment, not a whole message.
var service = header.substr(0, pos);
var fragmentIdStr = header.substr(pos + 1);
// Separate the message id string and the fragment number. Note that
// there may be a single leading + in the argument to parseInt, but
// this is harmless.
pos = fragmentIdStr.indexOf('+');
var messageIdStr = fragmentIdStr.substr(0, pos);
var fragmentNum = parseInt(fragmentIdStr.substr(pos + 1), 10);
var fragmentInfo =
goog.net.xpc.IframeRelayTransport.fragmentMap_[messageIdStr];
if (!fragmentInfo) {
fragmentInfo =
goog.net.xpc.IframeRelayTransport.fragmentMap_[messageIdStr] =
{fragments: [], received: 0, expected: 0};
}
if (goog.string.contains(fragmentIdStr, '++')) {
fragmentInfo.expected = fragmentNum + 1;
}
fragmentInfo.fragments[fragmentNum] = payload;
fragmentInfo.received++;
if (fragmentInfo.received != fragmentInfo.expected) {
return;
}
// We've received all outstanding fragments; combine what we've received
// into payload and fall out to the call to xpcDeliver.
payload = fragmentInfo.fragments.join('');
delete goog.net.xpc.IframeRelayTransport.fragmentMap_[messageIdStr];
}
goog.net.xpc.channels[channelName].
xpcDeliver(service, decodeURIComponent(payload));
};
/**
* Handles transport service messages (internal signalling).
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.IframeRelayTransport.prototype.transportServiceHandler =
function(payload) {
if (payload == goog.net.xpc.SETUP) {
// TODO(user) Safari swallows the SETUP_ACK from the iframe to the
// container after hitting reload.
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_);
this.channel_.notifyConnected();
}
else if (payload == goog.net.xpc.SETUP_ACK_) {
this.channel_.notifyConnected();
}
};
/**
* Sends a message.
*
* @param {string} service Name of service this the message has to be delivered.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.IframeRelayTransport.prototype.send = function(service, payload) {
// If we're on IE and the post-encoding payload is large, split it
// into multiple payloads and send each one separately. Otherwise,
// just send the whole thing.
var encodedPayload = encodeURIComponent(payload);
var encodedLen = encodedPayload.length;
var maxSize = goog.net.xpc.IframeRelayTransport.IE_PAYLOAD_MAX_SIZE_;
if (goog.userAgent.IE && encodedLen > maxSize) {
// A probabilistically-unique string used to link together all fragments
// in this message.
var messageIdStr = goog.string.getRandomString();
for (var startIndex = 0, fragmentNum = 0; startIndex < encodedLen;
fragmentNum++) {
var payloadFragment = encodedPayload.substr(startIndex, maxSize);
startIndex += maxSize;
var fragmentIdStr =
messageIdStr + (startIndex >= encodedLen ? '++' : '+') + fragmentNum;
this.send_(service, payloadFragment, fragmentIdStr);
}
} else {
this.send_(service, encodedPayload);
}
};
/**
* Sends an encoded message or message fragment.
* @param {string} service Name of service this the message has to be delivered.
* @param {string} encodedPayload The message content, URI encoded.
* @param {string=} opt_fragmentIdStr If sending a fragment, a string that
* identifies the fragment.
* @private
*/
goog.net.xpc.IframeRelayTransport.prototype.send_ =
function(service, encodedPayload, opt_fragmentIdStr) {
// IE requires that we create the onload attribute inline, otherwise the
// handler is not triggered
if (goog.userAgent.IE) {
var div = this.getWindow().document.createElement('div');
div.innerHTML = '<iframe onload="this.xpcOnload()"></iframe>';
var ifr = div.childNodes[0];
div = null;
ifr['xpcOnload'] = goog.net.xpc.IframeRelayTransport.iframeLoadHandler_;
} else {
var ifr = this.getWindow().document.createElement('iframe');
if (goog.userAgent.WEBKIT) {
// safari doesn't fire load-events on iframes.
// keep a reference and remove after a timeout.
goog.net.xpc.IframeRelayTransport.iframeRefs_.push({
timestamp: goog.now(),
iframeElement: ifr
});
} else {
goog.events.listen(ifr, 'load',
goog.net.xpc.IframeRelayTransport.iframeLoadHandler_);
}
}
var style = ifr.style;
style.visibility = 'hidden';
style.width = ifr.style.height = '0px';
style.position = 'absolute';
var url = this.peerRelayUri_;
url += '#' + this.channel_.name;
if (this.peerIframeId_) {
url += ',' + this.peerIframeId_;
}
url += '|' + service;
if (opt_fragmentIdStr) {
url += '|' + opt_fragmentIdStr;
}
url += ':' + encodedPayload;
ifr.src = url;
this.getWindow().document.body.appendChild(ifr);
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST, 'msg sent: ' + url);
};
/**
* The iframe load handler. Gets called as method on the iframe element.
* @private
* @this Element
*/
goog.net.xpc.IframeRelayTransport.iframeLoadHandler_ = function() {
goog.log.log(goog.net.xpc.logger, goog.log.Level.FINEST, 'iframe-load');
goog.dom.removeNode(this);
this.xpcOnload = null;
};
/** @override */
goog.net.xpc.IframeRelayTransport.prototype.disposeInternal = function() {
goog.base(this, 'disposeInternal');
if (goog.userAgent.WEBKIT) {
goog.net.xpc.IframeRelayTransport.cleanup_(0);
}
};
@@ -0,0 +1,649 @@
// Copyright 2007 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 Contains the class which uses native messaging
* facilities for cross domain communication.
*
*/
goog.provide('goog.net.xpc.NativeMessagingTransport');
goog.require('goog.Timer');
goog.require('goog.asserts');
goog.require('goog.async.Deferred');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
/**
* The native messaging transport
*
* Uses document.postMessage() to send messages to other documents.
* Receiving is done by listening on 'message'-events on the document.
*
* @param {goog.net.xpc.CrossPageChannel} channel The channel this
* transport belongs to.
* @param {string} peerHostname The hostname (protocol, domain, and port) of the
* peer.
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for
* finding the correct window/document.
* @param {boolean=} opt_oneSidedHandshake If this is true, only the outer
* transport sends a SETUP message and expects a SETUP_ACK. The inner
* transport goes connected when it receives the SETUP.
* @param {number=} opt_protocolVersion Which version of its setup protocol the
* transport should use. The default is '2'.
* @constructor
* @extends {goog.net.xpc.Transport}
*/
goog.net.xpc.NativeMessagingTransport = function(channel, peerHostname,
opt_domHelper, opt_oneSidedHandshake, opt_protocolVersion) {
goog.base(this, opt_domHelper);
/**
* The channel this transport belongs to.
* @type {goog.net.xpc.CrossPageChannel}
* @private
*/
this.channel_ = channel;
/**
* Which version of the transport's protocol should be used.
* @type {number}
* @private
*/
this.protocolVersion_ = opt_protocolVersion || 2;
goog.asserts.assert(this.protocolVersion_ >= 1);
goog.asserts.assert(this.protocolVersion_ <= 2);
/**
* The hostname of the peer. This parameterizes all calls to postMessage, and
* should contain the precise protocol, domain, and port of the peer window.
* @type {string}
* @private
*/
this.peerHostname_ = peerHostname || '*';
/**
* The event handler.
* @type {!goog.events.EventHandler}
* @private
*/
this.eventHandler_ = new goog.events.EventHandler(this);
/**
* Timer for connection reattempts.
* @type {!goog.Timer}
* @private
*/
this.maybeAttemptToConnectTimer_ = new goog.Timer(100, this.getWindow());
/**
* Whether one-sided handshakes are enabled.
* @type {boolean}
* @private
*/
this.oneSidedHandshake_ = !!opt_oneSidedHandshake;
/**
* Fires once we've received our SETUP_ACK message.
* @type {!goog.async.Deferred}
* @private
*/
this.setupAckReceived_ = new goog.async.Deferred();
/**
* Fires once we've sent our SETUP_ACK message.
* @type {!goog.async.Deferred}
* @private
*/
this.setupAckSent_ = new goog.async.Deferred();
/**
* Fires once we're marked connected.
* @type {!goog.async.Deferred}
* @private
*/
this.connected_ = new goog.async.Deferred();
/**
* The unique ID of this side of the connection. Used to determine when a peer
* is reloaded.
* @type {string}
* @private
*/
this.endpointId_ = goog.net.xpc.getRandomString(10);
/**
* The unique ID of the peer. If we get a message from a peer with an ID we
* don't expect, we reset the connection.
* @type {?string}
* @private
*/
this.peerEndpointId_ = null;
// We don't want to mark ourselves connected until we have sent whatever
// message will cause our counterpart in the other frame to also declare
// itself connected, if there is such a message. Otherwise we risk a user
// message being sent in advance of that message, and it being discarded.
if (this.oneSidedHandshake_) {
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.INNER) {
// One sided handshake, inner frame:
// SETUP_ACK must be received.
this.connected_.awaitDeferred(this.setupAckReceived_);
} else {
// One sided handshake, outer frame:
// SETUP_ACK must be sent.
this.connected_.awaitDeferred(this.setupAckSent_);
}
} else {
// Two sided handshake:
// SETUP_ACK has to have been received, and sent.
this.connected_.awaitDeferred(this.setupAckReceived_);
if (this.protocolVersion_ == 2) {
this.connected_.awaitDeferred(this.setupAckSent_);
}
}
this.connected_.addCallback(this.notifyConnected_, this);
this.connected_.callback(true);
this.eventHandler_.
listen(this.maybeAttemptToConnectTimer_, goog.Timer.TICK,
this.maybeAttemptToConnect_);
goog.log.info(goog.net.xpc.logger, 'NativeMessagingTransport created. ' +
'protocolVersion=' + this.protocolVersion_ + ', oneSidedHandshake=' +
this.oneSidedHandshake_ + ', role=' + this.channel_.getRole());
};
goog.inherits(goog.net.xpc.NativeMessagingTransport, goog.net.xpc.Transport);
/**
* Length of the delay in milliseconds between the channel being connected and
* the connection callback being called, in cases where coverage of timing flaws
* is required.
* @type {number}
* @private
*/
goog.net.xpc.NativeMessagingTransport.CONNECTION_DELAY_MS_ = 200;
/**
* Current determination of peer's protocol version, or null for unknown.
* @type {?number}
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.peerProtocolVersion_ = null;
/**
* Flag indicating if this instance of the transport has been initialized.
* @type {boolean}
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.initialized_ = false;
/**
* The transport type.
* @type {number}
* @override
*/
goog.net.xpc.NativeMessagingTransport.prototype.transportType =
goog.net.xpc.TransportTypes.NATIVE_MESSAGING;
/**
* The delimiter used for transport service messages.
* @type {string}
* @private
*/
goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_ = ',';
/**
* Tracks the number of NativeMessagingTransport channels that have been
* initialized but not disposed yet in a map keyed by the UID of the window
* object. This allows for multiple windows to be initiallized and listening
* for messages.
* @type {Object.<number>}
* @private
*/
goog.net.xpc.NativeMessagingTransport.activeCount_ = {};
/**
* Id of a timer user during postMessage sends.
* @type {number}
* @private
*/
goog.net.xpc.NativeMessagingTransport.sendTimerId_ = 0;
/**
* Checks whether the peer transport protocol version could be as indicated.
* @param {number} version The version to check for.
* @return {boolean} Whether the peer transport protocol version is as
* indicated, or null.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.couldPeerVersionBe_ =
function(version) {
return this.peerProtocolVersion_ == null ||
this.peerProtocolVersion_ == version;
};
/**
* Initializes this transport. Registers a listener for 'message'-events
* on the document.
* @param {Window} listenWindow The window to listen to events on.
* @private
*/
goog.net.xpc.NativeMessagingTransport.initialize_ = function(listenWindow) {
var uid = goog.getUid(listenWindow);
var value = goog.net.xpc.NativeMessagingTransport.activeCount_[uid];
if (!goog.isNumber(value)) {
value = 0;
}
if (value == 0) {
// Listen for message-events. These are fired on window in FF3 and on
// document in Opera.
goog.events.listen(
listenWindow.postMessage ? listenWindow : listenWindow.document,
'message',
goog.net.xpc.NativeMessagingTransport.messageReceived_,
false,
goog.net.xpc.NativeMessagingTransport);
}
goog.net.xpc.NativeMessagingTransport.activeCount_[uid] = value + 1;
};
/**
* Processes an incoming message-event.
* @param {goog.events.BrowserEvent} msgEvt The message event.
* @return {boolean} True if message was successfully delivered to a channel.
* @private
*/
goog.net.xpc.NativeMessagingTransport.messageReceived_ = function(msgEvt) {
var data = msgEvt.getBrowserEvent().data;
if (!goog.isString(data)) {
return false;
}
var headDelim = data.indexOf('|');
var serviceDelim = data.indexOf(':');
// make sure we got something reasonable
if (headDelim == -1 || serviceDelim == -1) {
return false;
}
var channelName = data.substring(0, headDelim);
var service = data.substring(headDelim + 1, serviceDelim);
var payload = data.substring(serviceDelim + 1);
goog.log.fine(goog.net.xpc.logger,
'messageReceived: channel=' + channelName +
', service=' + service + ', payload=' + payload);
// Attempt to deliver message to the channel. Keep in mind that it may not
// exist for several reasons, including but not limited to:
// - a malformed message
// - the channel simply has not been created
// - channel was created in a different namespace
// - message was sent to the wrong window
// - channel has become stale (e.g. caching iframes and back clicks)
var channel = goog.net.xpc.channels[channelName];
if (channel) {
channel.xpcDeliver(service, payload, msgEvt.getBrowserEvent().origin);
return true;
}
var transportMessageType =
goog.net.xpc.NativeMessagingTransport.parseTransportPayload_(payload)[0];
// Check if there are any stale channel names that can be updated.
for (var staleChannelName in goog.net.xpc.channels) {
var staleChannel = goog.net.xpc.channels[staleChannelName];
if (staleChannel.getRole() == goog.net.xpc.CrossPageChannelRole.INNER &&
!staleChannel.isConnected() &&
service == goog.net.xpc.TRANSPORT_SERVICE_ &&
(transportMessageType == goog.net.xpc.SETUP ||
transportMessageType == goog.net.xpc.SETUP_NTPV2)) {
// Inner peer received SETUP message but channel names did not match.
// Start using the channel name sent from outer peer. The channel name
// of the inner peer can easily become out of date, as iframe's and their
// JS state get cached in many browsers upon page reload or history
// navigation (particularly Firefox 1.5+). We can trust the outer peer,
// since we only accept postMessage messages from the same hostname that
// originally setup the channel.
goog.log.fine(goog.net.xpc.logger,
'changing channel name to ' + channelName);
staleChannel.name = channelName;
// Remove old stale pointer to channel.
delete goog.net.xpc.channels[staleChannelName];
// Create fresh pointer to channel.
goog.net.xpc.channels[channelName] = staleChannel;
staleChannel.xpcDeliver(service, payload);
return true;
}
}
// Failed to find a channel to deliver this message to, so simply ignore it.
goog.log.info(goog.net.xpc.logger, 'channel name mismatch; message ignored"');
return false;
};
/**
* Handles transport service messages.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.NativeMessagingTransport.prototype.transportServiceHandler =
function(payload) {
var transportParts =
goog.net.xpc.NativeMessagingTransport.parseTransportPayload_(payload);
var transportMessageType = transportParts[0];
var peerEndpointId = transportParts[1];
switch (transportMessageType) {
case goog.net.xpc.SETUP_ACK_:
this.setPeerProtocolVersion_(1);
if (!this.setupAckReceived_.hasFired()) {
this.setupAckReceived_.callback(true);
}
break;
case goog.net.xpc.SETUP_ACK_NTPV2:
if (this.protocolVersion_ == 2) {
this.setPeerProtocolVersion_(2);
if (!this.setupAckReceived_.hasFired()) {
this.setupAckReceived_.callback(true);
}
}
break;
case goog.net.xpc.SETUP:
this.setPeerProtocolVersion_(1);
this.sendSetupAckMessage_(1);
break;
case goog.net.xpc.SETUP_NTPV2:
if (this.protocolVersion_ == 2) {
var prevPeerProtocolVersion = this.peerProtocolVersion_;
this.setPeerProtocolVersion_(2);
this.sendSetupAckMessage_(2);
if ((prevPeerProtocolVersion == 1 || this.peerEndpointId_ != null) &&
this.peerEndpointId_ != peerEndpointId) {
// Send a new SETUP message since the peer has been replaced.
goog.log.info(goog.net.xpc.logger,
'Sending SETUP and changing peer ID to: ' + peerEndpointId);
this.sendSetupMessage_();
}
this.peerEndpointId_ = peerEndpointId;
}
break;
}
};
/**
* Sends a SETUP transport service message of the correct protocol number for
* our current situation.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.sendSetupMessage_ =
function() {
// 'real' (legacy) v1 transports don't know about there being v2 ones out
// there, and we shouldn't either.
goog.asserts.assert(!(this.protocolVersion_ == 1 &&
this.peerProtocolVersion_ == 2));
if (this.protocolVersion_ == 2 && this.couldPeerVersionBe_(2)) {
var payload = goog.net.xpc.SETUP_NTPV2;
payload += goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_;
payload += this.endpointId_;
this.send(goog.net.xpc.TRANSPORT_SERVICE_, payload);
}
// For backward compatibility reasons, the V1 SETUP message can be sent by
// both V1 and V2 transports. Once a V2 transport has 'heard' another V2
// transport it starts ignoring V1 messages, so the V2 message must be sent
// first.
if (this.couldPeerVersionBe_(1)) {
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP);
}
};
/**
* Sends a SETUP_ACK transport service message of the correct protocol number
* for our current situation.
* @param {number} protocolVersion The protocol version of the SETUP message
* which gave rise to this ack message.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.sendSetupAckMessage_ =
function(protocolVersion) {
goog.asserts.assert(this.protocolVersion_ != 1 || protocolVersion != 2,
'Shouldn\'t try to send a v2 setup ack in v1 mode.');
if (this.protocolVersion_ == 2 && this.couldPeerVersionBe_(2) &&
protocolVersion == 2) {
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_NTPV2);
} else if (this.couldPeerVersionBe_(1) && protocolVersion == 1) {
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_);
} else {
return;
}
if (!this.setupAckSent_.hasFired()) {
this.setupAckSent_.callback(true);
}
};
/**
* Attempts to set the peer protocol number. Downgrades from 2 to 1 are not
* permitted.
* @param {number} version The new protocol number.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.setPeerProtocolVersion_ =
function(version) {
if (version > this.peerProtocolVersion_) {
this.peerProtocolVersion_ = version;
}
if (this.peerProtocolVersion_ == 1) {
if (!this.setupAckSent_.hasFired() && !this.oneSidedHandshake_) {
this.setupAckSent_.callback(true);
}
this.peerEndpointId_ = null;
}
};
/**
* Connects this transport.
* @override
*/
goog.net.xpc.NativeMessagingTransport.prototype.connect = function() {
goog.net.xpc.NativeMessagingTransport.initialize_(this.getWindow());
this.initialized_ = true;
this.maybeAttemptToConnect_();
};
/**
* Connects to other peer. In the case of the outer peer, the setup messages are
* likely sent before the inner peer is ready to receive them. Therefore, this
* function will continue trying to send the SETUP message until the inner peer
* responds. In the case of the inner peer, it will occasionally have its
* channel name fall out of sync with the outer peer, particularly during
* soft-reloads and history navigations.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.maybeAttemptToConnect_ =
function() {
// In a one-sided handshake, the outer frame does not send a SETUP message,
// but the inner frame does.
var outerFrame = this.channel_.getRole() ==
goog.net.xpc.CrossPageChannelRole.OUTER;
if ((this.oneSidedHandshake_ && outerFrame) ||
this.channel_.isConnected() ||
this.isDisposed()) {
this.maybeAttemptToConnectTimer_.stop();
return;
}
this.maybeAttemptToConnectTimer_.start();
this.sendSetupMessage_();
};
/**
* Sends a message.
* @param {string} service The name off the service the message is to be
* delivered to.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.NativeMessagingTransport.prototype.send = function(service,
payload) {
var win = this.channel_.getPeerWindowObject();
if (!win) {
goog.log.fine(goog.net.xpc.logger, 'send(): window not ready');
return;
}
this.send = function(service, payload) {
// In IE8 (and perhaps elsewhere), it seems like postMessage is sometimes
// implemented as a synchronous call. That is, calling it synchronously
// calls whatever listeners it has, and control is not returned to the
// calling thread until those listeners are run. This produces different
// ordering to all other browsers, and breaks this protocol. This timer
// callback is introduced to produce standard behavior across all browsers.
var transport = this;
var channelName = this.channel_.name;
var sendFunctor = function() {
transport.sendTimerId_ = 0;
try {
// postMessage is a method of the window object, except in some
// versions of Opera, where it is a method of the document object. It
// also seems that the appearance of postMessage on the peer window
// object can sometimes be delayed.
var obj = win.postMessage ? win : win.document;
if (!obj.postMessage) {
goog.log.warning(goog.net.xpc.logger,
'Peer window had no postMessage function.');
return;
}
obj.postMessage(channelName + '|' + service + ':' + payload,
transport.peerHostname_);
goog.log.fine(goog.net.xpc.logger, 'send(): service=' + service +
' payload=' + payload + ' to hostname=' + transport.peerHostname_);
} catch (error) {
// There is some evidence (not totally convincing) that postMessage can
// be missing or throw errors during a narrow timing window during
// startup. This protects against that.
goog.log.warning(goog.net.xpc.logger,
'Error performing postMessage, ignoring.', error);
}
};
this.sendTimerId_ = goog.Timer.callOnce(sendFunctor, 0);
};
this.send(service, payload);
};
/**
* Notify the channel that this transport is connected. If either transport is
* protocol v1, a short delay is required to paper over timing vulnerabilities
* in that protocol version.
* @private
*/
goog.net.xpc.NativeMessagingTransport.prototype.notifyConnected_ =
function() {
var delay = (this.protocolVersion_ == 1 || this.peerProtocolVersion_ == 1) ?
goog.net.xpc.NativeMessagingTransport.CONNECTION_DELAY_MS_ : undefined;
this.channel_.notifyConnected(delay);
};
/** @override */
goog.net.xpc.NativeMessagingTransport.prototype.disposeInternal = function() {
if (this.initialized_) {
var listenWindow = this.getWindow();
var uid = goog.getUid(listenWindow);
var value = goog.net.xpc.NativeMessagingTransport.activeCount_[uid];
goog.net.xpc.NativeMessagingTransport.activeCount_[uid] = value - 1;
if (value == 1) {
goog.events.unlisten(
listenWindow.postMessage ? listenWindow : listenWindow.document,
'message',
goog.net.xpc.NativeMessagingTransport.messageReceived_,
false,
goog.net.xpc.NativeMessagingTransport);
}
}
if (this.sendTimerId_) {
goog.Timer.clear(this.sendTimerId_);
this.sendTimerId_ = 0;
}
goog.dispose(this.eventHandler_);
delete this.eventHandler_;
goog.dispose(this.maybeAttemptToConnectTimer_);
delete this.maybeAttemptToConnectTimer_;
this.setupAckReceived_.cancel();
delete this.setupAckReceived_;
this.setupAckSent_.cancel();
delete this.setupAckSent_;
this.connected_.cancel();
delete this.connected_;
// Cleaning up this.send as it is an instance method, created in
// goog.net.xpc.NativeMessagingTransport.prototype.send and has a closure over
// this.channel_.peerWindowObject_.
delete this.send;
goog.base(this, 'disposeInternal');
};
/**
* Parse a transport service payload message. For v1, it is simply expected to
* be 'SETUP' or 'SETUP_ACK'. For v2, an example setup message is
* 'SETUP_NTPV2,abc123', where the second part is the endpoint id. The v2 setup
* ack message is simply 'SETUP_ACK_NTPV2'.
* @param {string} payload The payload.
* @return {!Array.<?string>} An array with the message type as the first member
* and the endpoint id as the second, if one was sent, or null otherwise.
* @private
*/
goog.net.xpc.NativeMessagingTransport.parseTransportPayload_ =
function(payload) {
var transportParts = /** @type {!Array.<?string>} */ (payload.split(
goog.net.xpc.NativeMessagingTransport.MESSAGE_DELIMITER_));
transportParts[1] = transportParts[1] || null;
return transportParts;
};
@@ -0,0 +1,479 @@
// Copyright 2008 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 Contains the NIX (Native IE XDC) method transport for
* cross-domain communication. It exploits the fact that Internet Explorer
* allows a window that is the parent of an iframe to set said iframe window's
* opener property to an object. This object can be a function that in turn
* can be used to send a message despite same-origin constraints. Note that
* this function, if a pure JavaScript object, opens up the possibilitiy of
* gaining a hold of the context of the other window and in turn, attacking
* it. This implementation therefore wraps the JavaScript objects used inside
* a VBScript class. Since VBScript objects are passed in JavaScript as a COM
* wrapper (like DOM objects), they are thus opaque to JavaScript
* (except for the interface they expose). This therefore provides a safe
* method of transport.
*
*
* Initially based on FrameElementTransport which shares some similarities
* to this method.
*/
goog.provide('goog.net.xpc.NixTransport');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
goog.require('goog.reflect');
/**
* NIX method transport.
*
* NOTE(user): NIX method tested in all IE versions starting from 6.0.
*
* @param {goog.net.xpc.CrossPageChannel} channel The channel this transport
* belongs to.
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for finding
* the correct window.
* @constructor
* @extends {goog.net.xpc.Transport}
*/
goog.net.xpc.NixTransport = function(channel, opt_domHelper) {
goog.base(this, opt_domHelper);
/**
* The channel this transport belongs to.
* @type {goog.net.xpc.CrossPageChannel}
* @private
*/
this.channel_ = channel;
/**
* The authorization token, if any, used by this transport.
* @type {?string}
* @private
*/
this.authToken_ = channel[goog.net.xpc.CfgFields.AUTH_TOKEN] || '';
/**
* The authorization token, if any, that must be sent by the other party
* for setup to occur.
* @type {?string}
* @private
*/
this.remoteAuthToken_ =
channel[goog.net.xpc.CfgFields.REMOTE_AUTH_TOKEN] || '';
// Conduct the setup work for NIX in general, if need be.
goog.net.xpc.NixTransport.conductGlobalSetup_(this.getWindow());
// Setup aliases so that VBScript can call these methods
// on the transport class, even if they are renamed during
// compression.
this[goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE] = this.handleMessage_;
this[goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL] = this.createChannel_;
};
goog.inherits(goog.net.xpc.NixTransport, goog.net.xpc.Transport);
// Consts for NIX. VBScript doesn't allow items to start with _ for some
// reason, so we need to make these names quite unique, as they will go into
// the global namespace.
/**
* Global name of the Wrapper VBScript class.
* Note that this class will be stored in the *global*
* namespace (i.e. window in browsers).
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_WRAPPER = 'GCXPC____NIXVBS_wrapper';
/**
* Global name of the GetWrapper VBScript function. This
* constant is used by JavaScript to call this function.
* Note that this function will be stored in the *global*
* namespace (i.e. window in browsers).
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_GET_WRAPPER = 'GCXPC____NIXVBS_get_wrapper';
/**
* The name of the handle message method used by the wrapper class
* when calling the transport.
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE = 'GCXPC____NIXJS_handle_message';
/**
* The name of the create channel method used by the wrapper class
* when calling the transport.
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL = 'GCXPC____NIXJS_create_channel';
/**
* A "unique" identifier that is stored in the wrapper
* class so that the wrapper can be distinguished from
* other objects easily.
* @type {string}
*/
goog.net.xpc.NixTransport.NIX_ID_FIELD = 'GCXPC____NIXVBS_container';
/**
* Determines if the installed version of IE supports accessing window.opener
* after it has been set to a non-Window/null value. NIX relies on this being
* possible.
* @return {boolean} Whether window.opener behavior is compatible with NIX.
*/
goog.net.xpc.NixTransport.isNixSupported = function() {
var isSupported = false;
try {
var oldOpener = window.opener;
// The compiler complains (as it should!) if we set window.opener to
// something other than a window or null.
window.opener = /** @type {Window} */ ({});
isSupported = goog.reflect.canAccessProperty(window, 'opener');
window.opener = oldOpener;
} catch (e) { }
return isSupported;
};
/**
* Conducts the global setup work for the NIX transport method.
* This function creates and then injects into the page the
* VBScript code necessary to create the NIX wrapper class.
* Note that this method can be called multiple times, as
* it internally checks whether the work is necessary before
* proceeding.
* @param {Window} listenWindow The window containing the affected page.
* @private
*/
goog.net.xpc.NixTransport.conductGlobalSetup_ = function(listenWindow) {
if (listenWindow['nix_setup_complete']) {
return;
}
// Inject the VBScript code needed.
var vbscript =
// We create a class to act as a wrapper for
// a Javascript call, to prevent a break in of
// the context.
'Class ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n ' +
// An internal member for keeping track of the
// transport for which this wrapper exists.
'Private m_Transport\n' +
// An internal member for keeping track of the
// auth token associated with the context that
// created this wrapper. Used for validation
// purposes.
'Private m_Auth\n' +
// Method for internally setting the value
// of the m_Transport property. We have the
// isEmpty check to prevent the transport
// from being overridden with an illicit
// object by a malicious party.
'Public Sub SetTransport(transport)\n' +
'If isEmpty(m_Transport) Then\n' +
'Set m_Transport = transport\n' +
'End If\n' +
'End Sub\n' +
// Method for internally setting the value
// of the m_Auth property. We have the
// isEmpty check to prevent the transport
// from being overridden with an illicit
// object by a malicious party.
'Public Sub SetAuth(auth)\n' +
'If isEmpty(m_Auth) Then\n' +
'm_Auth = auth\n' +
'End If\n' +
'End Sub\n' +
// Returns the auth token to the gadget, so it can
// confirm a match before initiating the connection
'Public Function GetAuthToken()\n ' +
'GetAuthToken = m_Auth\n' +
'End Function\n' +
// A wrapper method which causes a
// message to be sent to the other context.
'Public Sub SendMessage(service, payload)\n ' +
'Call m_Transport.' +
goog.net.xpc.NixTransport.NIX_HANDLE_MESSAGE + '(service, payload)\n' +
'End Sub\n' +
// Method for setting up the inner->outer
// channel.
'Public Sub CreateChannel(channel)\n ' +
'Call m_Transport.' +
goog.net.xpc.NixTransport.NIX_CREATE_CHANNEL + '(channel)\n' +
'End Sub\n' +
// An empty field with a unique identifier to
// prevent the code from confusing this wrapper
// with a run-of-the-mill value found in window.opener.
'Public Sub ' + goog.net.xpc.NixTransport.NIX_ID_FIELD + '()\n ' +
'End Sub\n' +
'End Class\n ' +
// Function to get a reference to the wrapper.
'Function ' +
goog.net.xpc.NixTransport.NIX_GET_WRAPPER + '(transport, auth)\n' +
'Dim wrap\n' +
'Set wrap = New ' + goog.net.xpc.NixTransport.NIX_WRAPPER + '\n' +
'wrap.SetTransport transport\n' +
'wrap.SetAuth auth\n' +
'Set ' + goog.net.xpc.NixTransport.NIX_GET_WRAPPER + ' = wrap\n' +
'End Function';
try {
listenWindow.execScript(vbscript, 'vbscript');
listenWindow['nix_setup_complete'] = true;
}
catch (e) {
goog.log.error(goog.net.xpc.logger,
'exception caught while attempting global setup: ' + e);
}
};
/**
* The transport type.
* @type {number}
* @protected
* @override
*/
goog.net.xpc.NixTransport.prototype.transportType =
goog.net.xpc.TransportTypes.NIX;
/**
* Keeps track of whether the local setup has completed (i.e.
* the initial work towards setting the channel up has been
* completed for this end).
* @type {boolean}
* @private
*/
goog.net.xpc.NixTransport.prototype.localSetupCompleted_ = false;
/**
* The NIX channel used to talk to the other page. This
* object is in fact a reference to a VBScript class
* (see above) and as such, is in fact a COM wrapper.
* When using this object, make sure to not access methods
* without calling them, otherwise a COM error will be thrown.
* @type {Object}
* @private
*/
goog.net.xpc.NixTransport.prototype.nixChannel_ = null;
/**
* Connect this transport.
* @override
*/
goog.net.xpc.NixTransport.prototype.connect = function() {
if (this.channel_.getRole() == goog.net.xpc.CrossPageChannelRole.OUTER) {
this.attemptOuterSetup_();
} else {
this.attemptInnerSetup_();
}
};
/**
* Attempts to setup the channel from the perspective
* of the outer (read: container) page. This method
* will attempt to create a NIX wrapper for this transport
* and place it into the "opener" property of the inner
* page's window object. If it fails, it will continue
* to loop until it does so.
*
* @private
*/
goog.net.xpc.NixTransport.prototype.attemptOuterSetup_ = function() {
if (this.localSetupCompleted_) {
return;
}
// Get shortcut to iframe-element that contains the inner
// page.
var innerFrame = this.channel_.getIframeElement();
try {
// Attempt to place the NIX wrapper object into the inner
// frame's opener property.
var theWindow = this.getWindow();
var getWrapper = theWindow[goog.net.xpc.NixTransport.NIX_GET_WRAPPER];
innerFrame.contentWindow.opener = getWrapper(this, this.authToken_);
this.localSetupCompleted_ = true;
}
catch (e) {
goog.log.error(goog.net.xpc.logger,
'exception caught while attempting setup: ' + e);
}
// If the retry is necessary, reattempt this setup.
if (!this.localSetupCompleted_) {
this.getWindow().setTimeout(goog.bind(this.attemptOuterSetup_, this), 100);
}
};
/**
* Attempts to setup the channel from the perspective
* of the inner (read: iframe) page. This method
* will attempt to *read* the opener object from the
* page's opener property. If it succeeds, this object
* is saved into nixChannel_ and the channel is confirmed
* with the container by calling CreateChannel with an instance
* of a wrapper for *this* page. Note that if this method
* fails, it will continue to loop until it succeeds.
*
* @private
*/
goog.net.xpc.NixTransport.prototype.attemptInnerSetup_ = function() {
if (this.localSetupCompleted_) {
return;
}
try {
var opener = this.getWindow().opener;
// Ensure that the object contained inside the opener
// property is in fact a NIX wrapper.
if (opener && goog.net.xpc.NixTransport.NIX_ID_FIELD in opener) {
this.nixChannel_ = opener;
// Ensure that the NIX channel given to use is valid.
var remoteAuthToken = this.nixChannel_['GetAuthToken']();
if (remoteAuthToken != this.remoteAuthToken_) {
goog.log.error(goog.net.xpc.logger,
'Invalid auth token from other party');
return;
}
// Complete the construction of the channel by sending our own
// wrapper to the container via the channel they gave us.
var theWindow = this.getWindow();
var getWrapper = theWindow[goog.net.xpc.NixTransport.NIX_GET_WRAPPER];
this.nixChannel_['CreateChannel'](getWrapper(this, this.authToken_));
this.localSetupCompleted_ = true;
// Notify channel that the transport is ready.
this.channel_.notifyConnected();
}
}
catch (e) {
goog.log.error(goog.net.xpc.logger,
'exception caught while attempting setup: ' + e);
return;
}
// If the retry is necessary, reattempt this setup.
if (!this.localSetupCompleted_) {
this.getWindow().setTimeout(goog.bind(this.attemptInnerSetup_, this), 100);
}
};
/**
* Internal method called by the inner page, via the
* NIX wrapper, to complete the setup of the channel.
*
* @param {Object} channel The NIX wrapper of the
* inner page.
* @private
*/
goog.net.xpc.NixTransport.prototype.createChannel_ = function(channel) {
// Verify that the channel is in fact a NIX wrapper.
if (typeof channel != 'unknown' ||
!(goog.net.xpc.NixTransport.NIX_ID_FIELD in channel)) {
goog.log.error(goog.net.xpc.logger,
'Invalid NIX channel given to createChannel_');
}
this.nixChannel_ = channel;
// Ensure that the NIX channel given to use is valid.
var remoteAuthToken = this.nixChannel_['GetAuthToken']();
if (remoteAuthToken != this.remoteAuthToken_) {
goog.log.error(goog.net.xpc.logger, 'Invalid auth token from other party');
return;
}
// Indicate to the CrossPageChannel that the channel is setup
// and ready to use.
this.channel_.notifyConnected();
};
/**
* Internal method called by the other page, via the NIX wrapper,
* to deliver a message.
* @param {string} serviceName The name of the service the message is to be
* delivered to.
* @param {string} payload The message to process.
* @private
*/
goog.net.xpc.NixTransport.prototype.handleMessage_ =
function(serviceName, payload) {
/** @this {goog.net.xpc.NixTransport} */
var deliveryHandler = function() {
this.channel_.xpcDeliver(serviceName, payload);
};
this.getWindow().setTimeout(goog.bind(deliveryHandler, this), 1);
};
/**
* Sends a message.
* @param {string} service The name of the service the message is to be
* delivered to.
* @param {string} payload The message content.
* @override
*/
goog.net.xpc.NixTransport.prototype.send = function(service, payload) {
// Verify that the NIX channel we have is valid.
if (typeof(this.nixChannel_) !== 'unknown') {
goog.log.error(goog.net.xpc.logger, 'NIX channel not connected');
}
// Send the message via the NIX wrapper object.
this.nixChannel_['SendMessage'](service, payload);
};
/** @override */
goog.net.xpc.NixTransport.prototype.disposeInternal = function() {
goog.base(this, 'disposeInternal');
this.nixChannel_ = null;
};
@@ -0,0 +1,72 @@
// Copyright 2007 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 Standalone script to be included in the relay-document
* used by goog.net.xpc.IframeRelayTransport. This script will decode the
* fragment identifier, determine the target window object and deliver
* the data to it.
*
*/
goog.provide('goog.net.xpc.relay');
(function() {
// Decode the fragement identifier.
// location.href is expected to be structured as follows:
// <url>#<channel_name>[,<iframe_id>]|<data>
// Get the fragment identifier.
var raw = window.location.hash;
if (!raw) {
return;
}
if (raw.charAt(0) == '#') {
raw = raw.substring(1);
}
var pos = raw.indexOf('|');
var head = raw.substring(0, pos).split(',');
var channelName = head[0];
var iframeId = head.length == 2 ? head[1] : null;
var frame = raw.substring(pos + 1);
// Find the window object of the peer.
//
// The general structure of the frames looks like this:
// - peer1
// - relay2
// - peer2
// - relay1
//
// We are either relay1 or relay2.
var win;
if (iframeId) {
// We are relay2 and need to deliver the data to peer2.
win = window.parent.frames[iframeId];
} else {
// We are relay1 and need to deliver the data to peer1.
win = window.parent.parent;
}
// Deliver the data.
try {
win['xpcRelay'](channelName, frame);
} catch (e) {
// Nothing useful can be done here.
// It would be great to inform the sender the delivery of this message
// failed, but this is not possible because we are already in the receiver's
// domain at this point.
}
})();
@@ -0,0 +1,105 @@
// Copyright 2007 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 Contains the base class for transports.
*
*/
goog.provide('goog.net.xpc.Transport');
goog.require('goog.Disposable');
goog.require('goog.dom');
goog.require('goog.net.xpc');
/**
* The base class for transports.
* @param {goog.dom.DomHelper=} opt_domHelper The dom helper to use for
* finding the window objects.
* @constructor
* @extends {goog.Disposable};
*/
goog.net.xpc.Transport = function(opt_domHelper) {
goog.Disposable.call(this);
/**
* The dom helper to use for finding the window objects to reference.
* @type {goog.dom.DomHelper}
* @private
*/
this.domHelper_ = opt_domHelper || goog.dom.getDomHelper();
};
goog.inherits(goog.net.xpc.Transport, goog.Disposable);
/**
* The transport type.
* @type {number}
* @protected
*/
goog.net.xpc.Transport.prototype.transportType = 0;
/**
* @return {number} The transport type identifier.
*/
goog.net.xpc.Transport.prototype.getType = function() {
return this.transportType;
};
/**
* Returns the window associated with this transport instance.
* @return {Window} The window to use.
*/
goog.net.xpc.Transport.prototype.getWindow = function() {
return this.domHelper_.getWindow();
};
/**
* Return the transport name.
* @return {string} the transport name.
*/
goog.net.xpc.Transport.prototype.getName = function() {
return goog.net.xpc.TransportNames[this.transportType] || '';
};
/**
* Handles transport service messages (internal signalling).
* @param {string} payload The message content.
*/
goog.net.xpc.Transport.prototype.transportServiceHandler = goog.abstractMethod;
/**
* Connects this transport.
* The transport implementation is expected to call
* CrossPageChannel.prototype.notifyConnected when the channel is ready
* to be used.
*/
goog.net.xpc.Transport.prototype.connect = goog.abstractMethod;
/**
* Sends a message.
* @param {string} service The name off the service the message is to be
* delivered to.
* @param {string} payload The message content.
*/
goog.net.xpc.Transport.prototype.send = goog.abstractMethod;
@@ -0,0 +1,293 @@
// Copyright 2007 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 Provides the namesspace for client-side communication
* between pages originating from different domains (it works also
* with pages from the same domain, but doing that is kinda
* pointless).
*
* The only publicly visible class is goog.net.xpc.CrossPageChannel.
*
* Note: The preferred name for the main class would have been
* CrossDomainChannel. But as there already is a class named like
* that (which serves a different purpose) in the maps codebase,
* CrossPageChannel was chosen to avoid confusion.
*
* CrossPageChannel abstracts the underlying transport mechanism to
* provide a common interface in all browsers.
*
*/
/*
TODO(user)
- resolve fastback issues in Safari (IframeRelayTransport)
*/
/**
* Namespace for CrossPageChannel
*/
goog.provide('goog.net.xpc');
goog.provide('goog.net.xpc.CfgFields');
goog.provide('goog.net.xpc.ChannelStates');
goog.provide('goog.net.xpc.TransportNames');
goog.provide('goog.net.xpc.TransportTypes');
goog.provide('goog.net.xpc.UriCfgFields');
goog.require('goog.log');
/**
* Enum used to identify transport types.
* @enum {number}
*/
goog.net.xpc.TransportTypes = {
NATIVE_MESSAGING: 1,
FRAME_ELEMENT_METHOD: 2,
IFRAME_RELAY: 3,
IFRAME_POLLING: 4,
FLASH: 5,
NIX: 6
};
/**
* Enum containing transport names. These need to correspond to the
* transport class names for createTransport_() to work.
* @type {Object}
*/
goog.net.xpc.TransportNames = {
'1': 'NativeMessagingTransport',
'2': 'FrameElementMethodTransport',
'3': 'IframeRelayTransport',
'4': 'IframePollingTransport',
'5': 'FlashTransport',
'6': 'NixTransport'
};
// TODO(user): Add auth token support to other methods.
/**
* Field names used on configuration object.
* @type {Object}
*/
goog.net.xpc.CfgFields = {
/**
* Channel name identifier.
* Both peers have to be initialized with
* the same channel name. If not present, a channel name is
* generated (which then has to transferred to the peer somehow).
*/
CHANNEL_NAME: 'cn',
/**
* Authorization token. If set, NIX will use this authorization token
* to validate the setup.
*/
AUTH_TOKEN: 'at',
/**
* Remote party's authorization token. If set, NIX will validate this
* authorization token against that sent by the other party.
*/
REMOTE_AUTH_TOKEN: 'rat',
/**
* The URI of the peer page.
*/
PEER_URI: 'pu',
/**
* Ifame-ID identifier.
* The id of the iframe element the peer-document lives in.
*/
IFRAME_ID: 'ifrid',
/**
* Transport type identifier.
* The transport type to use. Possible values are entries from
* goog.net.xpc.TransportTypes. If not present, the transport is
* determined automatically based on the useragent's capabilities.
*/
TRANSPORT: 'tp',
/**
* Local relay URI identifier (IframeRelayTransport-specific).
* The URI (can't contain a fragment identifier) used by the peer to
* relay data through.
*/
LOCAL_RELAY_URI: 'lru',
/**
* Peer relay URI identifier (IframeRelayTransport-specific).
* The URI (can't contain a fragment identifier) used to relay data
* to the peer.
*/
PEER_RELAY_URI: 'pru',
/**
* Local poll URI identifier (IframePollingTransport-specific).
* The URI (can't contain a fragment identifier)which is polled
* to receive data from the peer.
*/
LOCAL_POLL_URI: 'lpu',
/**
* Local poll URI identifier (IframePollingTransport-specific).
* The URI (can't contain a fragment identifier) used to send data
* to the peer.
*/
PEER_POLL_URI: 'ppu',
/**
* The hostname of the peer window, including protocol, domain, and port
* (if specified). Used for security sensitive applications that make
* use of NativeMessagingTransport (i.e. most applications).
*/
PEER_HOSTNAME: 'ph',
/**
* Usually both frames using a connection initially send a SETUP message to
* each other, and each responds with a SETUP_ACK. A frame marks itself
* connected when it receives that SETUP_ACK. If this parameter is true
* however, the channel it is passed to will not send a SETUP, but rather will
* wait for one from its peer and mark itself connected when that arrives.
* Peer iframes created using such a channel will send SETUP however, and will
* wait for SETUP_ACK before marking themselves connected. The goal is to
* cope with a situation where the availability of the URL for the peer frame
* cannot be relied on, eg when the application is offline. Without this
* setting, the primary frame will attempt to send its SETUP message every
* 100ms, forever. This floods the javascript console with uncatchable
* security warnings, and fruitlessly burns CPU. There is one scenario this
* mode will not support, and that is reconnection by the outer frame, ie the
* creation of a new channel object to connect to a peer iframe which was
* already communicating with a previous channel object of the same name. If
* that behavior is needed, this mode should not be used. Reconnection by
* inner frames is supported in this mode however.
*/
ONE_SIDED_HANDSHAKE: 'osh',
/**
* The frame role (inner or outer). Used to explicitly indicate the role for
* each peer whenever the role cannot be reliably determined (e.g. the two
* peer windows are not parent/child frames). If unspecified, the role will
* be dynamically determined, assuming a parent/child frame setup.
*/
ROLE: 'role',
/**
* Which version of the native transport startup protocol should be used, the
* default being '2'. Version 1 had various timing vulnerabilities, which
* had to be compensated for by introducing delays, and is deprecated. V1
* and V2 are broadly compatible, although the more robust timing and lack
* of delays is not gained unless both sides are using V2. The only
* unsupported case of cross-protocol interoperation is where a connection
* starts out with V2 at both ends, and one of the ends reconnects as a V1.
* All other initial startup and reconnection scenarios are supported.
*/
NATIVE_TRANSPORT_PROTOCOL_VERSION: 'nativeProtocolVersion'
};
/**
* Config properties that need to be URL sanitized.
* @type {Array}.<string>
*/
goog.net.xpc.UriCfgFields = [
goog.net.xpc.CfgFields.PEER_URI,
goog.net.xpc.CfgFields.LOCAL_RELAY_URI,
goog.net.xpc.CfgFields.PEER_RELAY_URI,
goog.net.xpc.CfgFields.LOCAL_POLL_URI,
goog.net.xpc.CfgFields.PEER_POLL_URI
];
/**
* @enum {number}
*/
goog.net.xpc.ChannelStates = {
NOT_CONNECTED: 1,
CONNECTED: 2,
CLOSED: 3
};
/**
* The name of the transport service (used for internal signalling).
* @type {string}
* @suppress {underscore}
*/
goog.net.xpc.TRANSPORT_SERVICE_ = 'tp';
/**
* Transport signaling message: setup.
* @type {string}
*/
goog.net.xpc.SETUP = 'SETUP';
/**
* Transport signaling message: setup for native transport protocol v2.
* @type {string}
*/
goog.net.xpc.SETUP_NTPV2 = 'SETUP_NTPV2';
/**
* Transport signaling message: setup acknowledgement.
* @type {string}
* @suppress {underscore}
*/
goog.net.xpc.SETUP_ACK_ = 'SETUP_ACK';
/**
* Transport signaling message: setup acknowledgement.
* @type {string}
*/
goog.net.xpc.SETUP_ACK_NTPV2 = 'SETUP_ACK_NTPV2';
/**
* Object holding active channels.
* Package private. Do not call from outside goog.net.xpc.
*
* @type {Object.<string, goog.net.xpc.CrossPageChannel>}
*/
goog.net.xpc.channels = {};
/**
* Returns a random string.
* @param {number} length How many characters the string shall contain.
* @param {string=} opt_characters The characters used.
* @return {string} The random string.
*/
goog.net.xpc.getRandomString = function(length, opt_characters) {
var chars = opt_characters || goog.net.xpc.randomStringCharacters_;
var charsLength = chars.length;
var s = '';
while (length-- > 0) {
s += chars.charAt(Math.floor(Math.random() * charsLength));
}
return s;
};
/**
* The default characters used for random string generation.
* @type {string}
* @private
*/
goog.net.xpc.randomStringCharacters_ =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
/**
* The logger.
* @type {goog.log.Logger}
*/
goog.net.xpc.logger = goog.log.getLogger('goog.net.xpc');