Add vectortile branch

This commit is contained in:
Andreas Hocevar
2015-09-10 15:23:57 +02:00
parent 57ee7f52fd
commit 962473c3ab
3201 changed files with 978141 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.BrowserChannel
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.BrowserChannelTest');
</script>
</head>
<body>
<div id="debug" style="font-size: small">
</div>
</body>
</html>
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');
/**
* 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.
* @final
*/
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,182 @@
// 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}
* @final
*/
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<!goog.net.BulkLoader>}
* @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,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.BulkLoader
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.BulkLoaderTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,235 @@
// 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.
goog.provide('goog.net.BulkLoaderTest');
goog.setTestOnly('goog.net.BulkLoaderTest');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.net.BulkLoader');
goog.require('goog.net.EventType');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
/**
* Test interval between sending uri requests to the server.
*/
var DELAY_INTERVAL_BETWEEN_URI_REQUESTS = 5;
/**
* Test interval before a response is received for a URI request.
*/
var DELAY_INTERVAL_FOR_URI_LOAD = 15;
var clock;
var loadSuccess, loadError;
var successResponseTexts;
function setUpPage() {
clock = new goog.testing.MockClock(true);
}
function tearDownPage() {
clock.dispose();
}
function setUp() {
loadSuccess = false;
loadError = false;
successResponseTexts = [];
}
/**
* Gets the successful bulkloader for the specified uris with some
* modifications for testability.
* <ul>
* <li> Added onSuccess methods to simulate success while loading uris.
* <li> The send function of the XhrManager used by the bulkloader
* calls the onSuccess function after a specified time interval.
* </ul>
* @param {Array<string>} uris The URIs.
*/
function getSuccessfulBulkLoader(uris) {
var bulkLoader = new goog.net.BulkLoader(uris);
bulkLoader.load = function() {
var uris = this.helper_.getUris();
for (var i = 0; i < uris.length; i++) {
// This clock tick simulates a delay for processing every URI.
clock.tick(DELAY_INTERVAL_BETWEEN_URI_REQUESTS);
// This timeout determines how many ticks after the send request
// all the URIs will complete loading. This delays the load of
// the first uri and every subsequent uri by 15 ticks.
setTimeout(goog.bind(this.onSuccess, this, i, uris[i]),
DELAY_INTERVAL_FOR_URI_LOAD);
}
};
bulkLoader.onSuccess = function(id, uri) {
var xhrIo = {
getResponseText: function() {return uri;},
isSuccess: function() {return true;},
dispose: function() {}
};
this.handleEvent_(id, new goog.events.Event(
goog.net.EventType.COMPLETE, xhrIo));
};
var eventHandler = new goog.events.EventHandler();
eventHandler.listen(bulkLoader,
goog.net.EventType.SUCCESS,
handleSuccess);
eventHandler.listen(bulkLoader,
goog.net.EventType.ERROR,
handleError);
return bulkLoader;
}
/**
* Gets the non-successful bulkloader for the specified uris with some
* modifications for testability.
* <ul>
* <li> Added onSuccess and onError methods to simulate success and error
* while loading uris.
* <li> The send function of the XhrManager used by the bulkloader
* calls the onSuccess or onError function after a specified time
* interval.
* </ul>
* @param {Array<string>} uris The URIs.
*/
function getNonSuccessfulBulkLoader(uris) {
var bulkLoader = new goog.net.BulkLoader(uris);
bulkLoader.load = function() {
var uris = this.helper_.getUris();
for (var i = 0; i < uris.length; i++) {
// This clock tick simulates a delay for processing every URI.
clock.tick(DELAY_INTERVAL_BETWEEN_URI_REQUESTS);
// This timeout determines how many ticks after the send request
// all the URIs will complete loading in error. This delays the load
// of the first uri and every subsequent uri by 15 ticks. The URI
// with id == 2 is in error.
if (i != 2) {
setTimeout(goog.bind(this.onSuccess, this, i, uris[i]),
DELAY_INTERVAL_FOR_URI_LOAD);
} else {
setTimeout(goog.bind(this.onError, this, i, uris[i]),
DELAY_INTERVAL_FOR_URI_LOAD);
}
}
};
bulkLoader.onSuccess = function(id, uri) {
var xhrIo = {
getResponseText: function() {return uri;},
isSuccess: function() {return true;},
dispose: function() {}
};
this.handleEvent_(id, new goog.events.Event(
goog.net.EventType.COMPLETE, xhrIo));
};
bulkLoader.onError = function(id) {
var xhrIo = {
getResponseText: function() {return null;},
isSuccess: function() {return false;},
dispose: function() {}
};
this.handleEvent_(id, new goog.events.Event(
goog.net.EventType.ERROR, xhrIo));
};
var eventHandler = new goog.events.EventHandler();
eventHandler.listen(bulkLoader,
goog.net.EventType.SUCCESS,
handleSuccess);
eventHandler.listen(bulkLoader,
goog.net.EventType.ERROR,
handleError);
return bulkLoader;
}
function handleSuccess(e) {
loadSuccess = true;
successResponseTexts = e.target.getResponseTexts();
}
function handleError(e) {
loadError = true;
}
/**
* Test successful loading of URIs using the bulkloader.
*/
function testBulkLoaderLoadSuccess() {
var uris = ['a', 'b', 'c'];
var bulkLoader = getSuccessfulBulkLoader(uris);
assertArrayEquals(uris, bulkLoader.getRequestUris());
bulkLoader.load();
clock.tick(2);
assertFalse(
'The bulk loader is not yet loaded (after 2 ticks)', loadSuccess);
clock.tick(3);
assertFalse(
'The bulk loader is not yet loaded (after 5 ticks)', loadSuccess);
clock.tick(5);
assertFalse(
'The bulk loader is not yet loaded (after 10 ticks)', loadSuccess);
clock.tick(5);
assertTrue('The bulk loader is loaded (after 15 ticks)', loadSuccess);
assertArrayEquals('Ensure that the response texts are present',
successResponseTexts, uris);
}
/**
* Test error loading URIs using the bulkloader.
*/
function testBulkLoaderLoadError() {
var uris = ['a', 'b', 'c'];
var bulkLoader = getNonSuccessfulBulkLoader(uris);
bulkLoader.load();
clock.tick(2);
assertFalse(
'The bulk loader is not yet loaded (after 2 ticks)', loadError);
clock.tick(3);
assertFalse(
'The bulk loader is not yet loaded (after 5 ticks)', loadError);
clock.tick(5);
assertFalse(
'The bulk loader is not yet loaded (after 10 ticks)', loadError);
clock.tick(5);
assertFalse(
'The bulk loader is not loaded successfully (after 15 ticks)',
loadSuccess);
assertTrue(
'The bulk loader is loaded in error (after 15 ticks)', loadError);
}
@@ -0,0 +1,119 @@
// 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');
/**
* Helper class used to load multiple URIs.
* @param {Array<string|goog.Uri>} uris The URIs to load.
* @constructor
* @extends {goog.Disposable}
* @final
*/
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);
/**
* 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 {?goog.debug.Logger}
*/
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,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.ChannelRequest
</title>
<script src="../base.js" type="text/javascript">
</script>
<script type="text/javascript">
goog.require('goog.net.ChannelRequestTest');
</script>
</head>
<body>
</body>
</html>
@@ -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.
goog.provide('goog.net.ChannelRequestTest');
goog.setTestOnly('goog.net.ChannelRequestTest');
goog.require('goog.Uri');
goog.require('goog.functions');
goog.require('goog.net.BrowserChannel');
goog.require('goog.net.ChannelDebug');
goog.require('goog.net.ChannelRequest');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.net.XhrIo');
goog.require('goog.testing.recordFunction');
var channelRequest;
var mockBrowserChannel;
var mockClock;
var stubs;
var xhrIo;
/**
* Time to wait for a network request to time out, before aborting.
*/
var WATCHDOG_TIME = 2000;
/**
* Time to throttle readystatechange events.
*/
var THROTTLE_TIME = 500;
/**
* A really long time - used to make sure no more timeouts will fire.
*/
var ALL_DAY_MS = 1000 * 60 * 60 * 24;
function setUp() {
mockClock = new goog.testing.MockClock();
mockClock.install();
stubs = new goog.testing.PropertyReplacer();
}
function tearDown() {
stubs.reset();
mockClock.uninstall();
}
/**
* Constructs a duck-type BrowserChannel that tracks the completed requests.
* @constructor
*/
function MockBrowserChannel() {
this.reachabilityEvents = {};
this.isClosed = function() {
return false;
};
this.isActive = function() {
return true;
};
this.shouldUseSecondaryDomains = function() {
return false;
};
this.completedRequests = [];
this.notifyServerReachabilityEvent = function(reachabilityType) {
if (!this.reachabilityEvents[reachabilityType]) {
this.reachabilityEvents[reachabilityType] = 0;
}
this.reachabilityEvents[reachabilityType]++;
};
this.onRequestComplete = function(request) {
this.completedRequests.push(request);
};
this.onRequestData = function(request, data) {};
}
/**
* Creates a real ChannelRequest object, with some modifications for
* testability:
* <ul>
* <li>The BrowserChannel is a MockBrowserChannel.
* <li>The new watchdogTimeoutCallCount property tracks onWatchDogTimeout_()
* calls.
* <li>The timeout is set to WATCHDOG_TIME.
* </ul>
*/
function createChannelRequest() {
xhrIo = new goog.testing.net.XhrIo();
xhrIo.abort = xhrIo.abort || function() {
this.active_ = false;
};
// Install mock browser channel and no-op debug logger.
mockBrowserChannel = new MockBrowserChannel();
channelRequest = new goog.net.ChannelRequest(
mockBrowserChannel,
new goog.net.ChannelDebug());
// Install test XhrIo.
mockBrowserChannel.createXhrIo = function() {
return xhrIo;
};
// Install watchdogTimeoutCallCount.
channelRequest.watchdogTimeoutCallCount = 0;
channelRequest.originalOnWatchDogTimeout = channelRequest.onWatchDogTimeout_;
channelRequest.onWatchDogTimeout_ = function() {
this.watchdogTimeoutCallCount++;
return this.originalOnWatchDogTimeout();
};
channelRequest.setTimeout(WATCHDOG_TIME);
}
/**
* Run through the lifecycle of a long lived request, checking that the right
* network events are reported.
*/
function testNetworkEvents() {
createChannelRequest();
channelRequest.xmlHttpPost(new goog.Uri('some_uri'), 'some_postdata', true);
checkReachabilityEvents(1, 0, 0, 0);
if (goog.net.ChannelRequest.supportsXhrStreaming()) {
xhrIo.simulatePartialResponse('17\nI am a BC Message');
checkReachabilityEvents(1, 0, 0, 1);
xhrIo.simulatePartialResponse('23\nI am another BC Message');
checkReachabilityEvents(1, 0, 0, 2);
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 2);
} else {
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 0);
}
}
/**
* Test throttling of readystatechange events.
*/
function testNetworkEvents_throttleReadyStateChange() {
createChannelRequest();
channelRequest.setReadyStateChangeThrottle(THROTTLE_TIME);
var recordedHandler =
goog.testing.recordFunction(channelRequest.xmlHttpHandler_);
stubs.set(channelRequest, 'xmlHttpHandler_', recordedHandler);
channelRequest.xmlHttpPost(new goog.Uri('some_uri'), 'some_postdata', true);
assertEquals(1, recordedHandler.getCallCount());
checkReachabilityEvents(1, 0, 0, 0);
if (goog.net.ChannelRequest.supportsXhrStreaming()) {
xhrIo.simulatePartialResponse('17\nI am a BC Message');
checkReachabilityEvents(1, 0, 0, 1);
assertEquals(3, recordedHandler.getCallCount());
// Second event should be throttled
xhrIo.simulatePartialResponse('23\nI am another BC Message');
assertEquals(3, recordedHandler.getCallCount());
xhrIo.simulatePartialResponse('27\nI am yet another BC Message');
assertEquals(3, recordedHandler.getCallCount());
mockClock.tick(THROTTLE_TIME);
checkReachabilityEvents(1, 0, 0, 3);
// Only one more call because of throttling.
assertEquals(4, recordedHandler.getCallCount());
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 3);
assertEquals(5, recordedHandler.getCallCount());
} else {
xhrIo.simulateResponse(200, '16\Final BC Message');
checkReachabilityEvents(1, 1, 0, 0);
}
}
/**
* Make sure that the request "completes" with an error when the timeout
* expires.
*/
function testRequestTimeout() {
createChannelRequest();
channelRequest.xmlHttpPost(new goog.Uri('some_uri'), 'some_postdata', true);
assertEquals(0, channelRequest.watchdogTimeoutCallCount);
assertEquals(0, channelRequest.channel_.completedRequests.length);
// Watchdog timeout.
mockClock.tick(WATCHDOG_TIME);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
assertFalse(channelRequest.getSuccess());
// Make sure no more timers are firing.
mockClock.tick(ALL_DAY_MS);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
checkReachabilityEvents(1, 0, 1, 0);
}
function testRequestTimeoutWithUnexpectedException() {
createChannelRequest();
channelRequest.channel_.createXhrIo = goog.functions.error('Weird error');
try {
channelRequest.xmlHttpGet(new goog.Uri('some_uri'), true, null);
fail('Expected error');
} catch (e) {
assertEquals('Weird error', e.message);
}
assertEquals(0, channelRequest.watchdogTimeoutCallCount);
assertEquals(0, channelRequest.channel_.completedRequests.length);
// Watchdog timeout.
mockClock.tick(WATCHDOG_TIME);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
assertFalse(channelRequest.getSuccess());
// Make sure no more timers are firing.
mockClock.tick(ALL_DAY_MS);
assertEquals(1, channelRequest.watchdogTimeoutCallCount);
assertEquals(1, channelRequest.channel_.completedRequests.length);
checkReachabilityEvents(0, 0, 1, 0);
}
function testActiveXBlocked() {
createChannelRequest();
stubs.set(goog.global, 'ActiveXObject',
goog.functions.error('Active X blocked'));
channelRequest.tridentGet(new goog.Uri('some_uri'), false);
assertFalse(channelRequest.getSuccess());
assertEquals(goog.net.ChannelRequest.Error.ACTIVE_X_BLOCKED,
channelRequest.getLastError());
checkReachabilityEvents(0, 0, 0, 0);
}
/**
* This is a private method but we rely on it to avoid XSS, so it's important
* to verify it works properly.
*/
function testEscapeForStringInScript() {
var actual = goog.net.ChannelRequest.escapeForStringInScript_('"\'<>');
assertEquals('\\"\\\'\\x3c\\x3e', actual);
}
function checkReachabilityEvents(reqMade, reqSucceeded, reqFail, backChannel) {
var Reachability = goog.net.BrowserChannel.ServerReachability;
assertEquals(reqMade,
mockBrowserChannel.reachabilityEvents[Reachability.REQUEST_MADE] || 0);
assertEquals(reqSucceeded,
mockBrowserChannel.reachabilityEvents[Reachability.REQUEST_SUCCEEDED] ||
0);
assertEquals(reqFail,
mockBrowserChannel.reachabilityEvents[Reachability.REQUEST_FAILED] || 0);
assertEquals(backChannel,
mockBrowserChannel.reachabilityEvents[
Reachability.BACK_CHANNEL_ACTIVITY] ||
0);
}
@@ -0,0 +1,371 @@
// 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
* @final
*/
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: Remove the cookie.
// Note: We don't tell people about this option in the function doc because
// we prefer people to use remove() to remove 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,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.cookies
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.cookiesTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,265 @@
// 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.
goog.provide('goog.net.cookiesTest');
goog.setTestOnly('goog.net.cookiesTest');
goog.require('goog.array');
goog.require('goog.net.cookies');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
var cookies = goog.net.cookies;
var baseCount = 0;
var stubs = new goog.testing.PropertyReplacer();
function checkForCookies() {
if (!cookies.isEnabled()) {
var message = 'Cookies must be enabled to run this test.';
if (location.protocol == 'file:') {
message += '\nNote that cookies for local files are disabled in some ' +
'browsers.\nThey can be enabled in Chrome with the ' +
'--enable-file-cookies flag.';
}
fail(message);
}
}
function setUp() {
checkForCookies();
// Make sure there are no cookies set by previous, bad tests.
cookies.clear();
baseCount = cookies.getCount();
}
function tearDown() {
// Clear up after ourselves.
cookies.clear();
stubs.reset();
}
function testIsEnabled() {
assertEquals(navigator.cookieEnabled, cookies.isEnabled());
}
function testCount() {
// setUp empties the cookies
cookies.set('testa', 'A');
assertEquals(baseCount + 1, cookies.getCount());
cookies.set('testb', 'B');
cookies.set('testc', 'C');
assertEquals(baseCount + 3, cookies.getCount());
cookies.remove('testa');
cookies.remove('testb');
assertEquals(baseCount + 1, cookies.getCount());
cookies.remove('testc');
assertEquals(baseCount + 0, cookies.getCount());
}
function testSet() {
cookies.set('testa', 'testb');
assertEquals('testb', cookies.get('testa'));
cookies.remove('testa');
assertEquals(undefined, cookies.get('testa'));
// check for invalid characters in name and value
}
function testGetKeys() {
cookies.set('testa', 'A');
cookies.set('testb', 'B');
cookies.set('testc', 'C');
var keys = cookies.getKeys();
assertTrue(goog.array.contains(keys, 'testa'));
assertTrue(goog.array.contains(keys, 'testb'));
assertTrue(goog.array.contains(keys, 'testc'));
}
function testGetValues() {
cookies.set('testa', 'A');
cookies.set('testb', 'B');
cookies.set('testc', 'C');
var values = cookies.getValues();
assertTrue(goog.array.contains(values, 'A'));
assertTrue(goog.array.contains(values, 'B'));
assertTrue(goog.array.contains(values, 'C'));
}
function testContainsKey() {
assertFalse(cookies.containsKey('testa'));
cookies.set('testa', 'A');
assertTrue(cookies.containsKey('testa'));
cookies.set('testb', 'B');
assertTrue(cookies.containsKey('testb'));
cookies.remove('testb');
assertFalse(cookies.containsKey('testb'));
cookies.remove('testa');
assertFalse(cookies.containsKey('testa'));
}
function testContainsValue() {
assertFalse(cookies.containsValue('A'));
cookies.set('testa', 'A');
assertTrue(cookies.containsValue('A'));
cookies.set('testb', 'B');
assertTrue(cookies.containsValue('B'));
cookies.remove('testb');
assertFalse(cookies.containsValue('B'));
cookies.remove('testa');
assertFalse(cookies.containsValue('A'));
}
function testIsEmpty() {
// we cannot guarantee that we have no cookies so testing for the true
// case cannot be done without a mock document.cookie
cookies.set('testa', 'A');
assertFalse(cookies.isEmpty());
cookies.set('testb', 'B');
assertFalse(cookies.isEmpty());
cookies.remove('testb');
assertFalse(cookies.isEmpty());
cookies.remove('testa');
}
function testRemove() {
assertFalse(
'1. Cookie should not contain "testa"', cookies.containsKey('testa'));
cookies.set('testa', 'A', undefined, '/');
assertTrue('2. Cookie should contain "testa"', cookies.containsKey('testa'));
cookies.remove('testa', '/');
assertFalse(
'3. Cookie should not contain "testa"', cookies.containsKey('testa'));
cookies.set('testa', 'A');
assertTrue('4. Cookie should contain "testa"', cookies.containsKey('testa'));
cookies.remove('testa');
assertFalse(
'5. Cookie should not contain "testa"', cookies.containsKey('testa'));
}
function testStrangeValue() {
// This ensures that the pattern key2=value in the value does not match
// the key2 cookie.
var value = 'testb=bbb';
var value2 = 'ccc';
cookies.set('testa', value);
cookies.set('testb', value2);
assertEquals(value, cookies.get('testa'));
assertEquals(value2, cookies.get('testb'));
}
function testSetCookiePath() {
assertEquals('foo=bar;path=/xyz',
mockSetCookie('foo', 'bar', -1, '/xyz'));
}
function testSetCookieDomain() {
assertEquals('foo=bar;domain=google.com',
mockSetCookie('foo', 'bar', -1, null, 'google.com'));
}
function testSetCookieSecure() {
assertEquals('foo=bar;secure',
mockSetCookie('foo', 'bar', -1, null, null, true));
}
function testSetCookieMaxAgeZero() {
var result = mockSetCookie('foo', 'bar', 0);
var pattern = new RegExp(
'foo=bar;expires=' + new Date(1970, 1, 1).toUTCString());
if (!result.match(pattern)) {
fail('expected match against ' + pattern + ' got ' + result);
}
}
function testGetEmptyCookie() {
var value = '';
cookies.set('test', value);
assertEquals(value, cookies.get('test'));
}
function testGetEmptyCookieIE() {
stubs.set(cookies, 'getCookie_', function() {
return 'test1; test2; test3';
});
assertEquals('', cookies.get('test1'));
assertEquals('', cookies.get('test2'));
assertEquals('', cookies.get('test3'));
}
// TODO(chrisn): Testing max age > 0 requires a mock clock.
function mockSetCookie(var_args) {
var setCookie = cookies.setCookie_;
try {
var result;
cookies.setCookie_ = function(arg) {
result = arg;
};
cookies.set.apply(cookies, arguments);
return result;
} finally {
cookies.setCookie_ = setCookie;
}
}
function assertValidName(name) {
assertTrue(name + ' should be valid', cookies.isValidName(name));
}
function assertInvalidName(name) {
assertFalse(name + ' should be invalid', cookies.isValidName(name));
assertThrows(function() {
cookies.set(name, 'value');
});
}
function assertValidValue(val) {
assertTrue(val + ' should be valid', cookies.isValidValue(val));
}
function assertInvalidValue(val) {
assertFalse(val + ' should be invalid', cookies.isValidValue(val));
assertThrows(function() {
cookies.set('name', val);
});
}
function testValidName() {
assertValidName('foo');
assertInvalidName('foo bar');
assertInvalidName('foo=bar');
assertInvalidName('foo;bar');
assertInvalidName('foo\nbar');
}
function testValidValue() {
assertValidValue('foo');
assertValidValue('foo bar');
assertValidValue('foo=bar');
assertInvalidValue('foo;bar');
assertInvalidValue('foo\nbar');
}
@@ -0,0 +1,272 @@
// Copyright 2013 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 contain classes that add support for cross-domain XHR
* requests (see http://www.w3.org/TR/cors/). Most modern browsers are able to
* use a regular XMLHttpRequest for that, but IE 8 use XDomainRequest object
* instead. This file provides an adapter from this object to a goog.net.XhrLike
* and a factory to allow using this with a goog.net.XhrIo instance.
*
* IE 7 and older versions are not supported (given that they do not support
* CORS requests).
*/
goog.provide('goog.net.CorsXmlHttpFactory');
goog.provide('goog.net.IeCorsXhrAdapter');
goog.require('goog.net.HttpStatus');
goog.require('goog.net.XhrLike');
goog.require('goog.net.XmlHttp');
goog.require('goog.net.XmlHttpFactory');
/**
* A factory of XML http request objects that supports cross domain requests.
* This class should be instantiated and passed as the parameter of a
* goog.net.XhrIo constructor to allow cross-domain requests in every browser.
*
* @extends {goog.net.XmlHttpFactory}
* @constructor
* @final
*/
goog.net.CorsXmlHttpFactory = function() {
goog.net.XmlHttpFactory.call(this);
};
goog.inherits(goog.net.CorsXmlHttpFactory, goog.net.XmlHttpFactory);
/** @override */
goog.net.CorsXmlHttpFactory.prototype.createInstance = function() {
var xhr = new XMLHttpRequest();
if (('withCredentials' in xhr)) {
return xhr;
} else if (typeof XDomainRequest != 'undefined') {
return new goog.net.IeCorsXhrAdapter();
} else {
throw Error('Unsupported browser');
}
};
/** @override */
goog.net.CorsXmlHttpFactory.prototype.internalGetOptions = function() {
return {};
};
/**
* An adapter around Internet Explorer's XDomainRequest object that makes it
* look like a standard XMLHttpRequest. This can be used instead of
* XMLHttpRequest to support CORS.
*
* @implements {goog.net.XhrLike}
* @constructor
* @struct
* @final
*/
goog.net.IeCorsXhrAdapter = function() {
/**
* The underlying XDomainRequest used to make the HTTP request.
* @type {!XDomainRequest}
* @private
*/
this.xdr_ = new XDomainRequest();
/**
* The simulated ready state.
* @type {number}
*/
this.readyState = goog.net.XmlHttp.ReadyState.UNINITIALIZED;
/**
* The simulated ready state change callback function.
* @type {Function}
*/
this.onreadystatechange = null;
/**
* The simulated response text parameter.
* @type {?string}
*/
this.responseText = null;
/**
* The simulated status code
* @type {number}
*/
this.status = -1;
/** @override */
this.responseXML = null;
/** @override */
this.statusText = null;
this.xdr_.onload = goog.bind(this.handleLoad_, this);
this.xdr_.onerror = goog.bind(this.handleError_, this);
this.xdr_.onprogress = goog.bind(this.handleProgress_, this);
this.xdr_.ontimeout = goog.bind(this.handleTimeout_, this);
};
/**
* Opens a connection to the provided URL.
* @param {string} method The HTTP method to use. Valid methods include GET and
* POST.
* @param {string} url The URL to contact. The authority of this URL must match
* the authority of the current page's URL (e.g. http or https).
* @param {?boolean=} opt_async Whether the request is asynchronous, defaulting
* to true. XDomainRequest does not support syncronous requests, so setting
* it to false will actually raise an exception.
* @override
*/
goog.net.IeCorsXhrAdapter.prototype.open = function(method, url, opt_async) {
if (goog.isDefAndNotNull(opt_async) && (!opt_async)) {
throw new Error('Only async requests are supported.');
}
this.xdr_.open(method, url);
};
/**
* Sends the request to the remote server. Before calling this function, always
* call {@link open}.
* @param {(ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string)=}
* opt_content The content to send as POSTDATA, if any. Only string data is
* supported by this implementation.
* @override
*/
goog.net.IeCorsXhrAdapter.prototype.send = function(opt_content) {
if (opt_content) {
if (typeof opt_content == 'string') {
this.xdr_.send(opt_content);
} else {
throw new Error('Only string data is supported');
}
} else {
this.xdr_.send();
}
};
/**
* @override
*/
goog.net.IeCorsXhrAdapter.prototype.abort = function() {
this.xdr_.abort();
};
/**
* Sets a request header to send to the remote server. Because this
* implementation does not support request headers, this function does nothing.
* @param {string} key The name of the HTTP header to set. Ignored.
* @param {string} value The value to set for the HTTP header. Ignored.
* @override
*/
goog.net.IeCorsXhrAdapter.prototype.setRequestHeader = function(key, value) {
// Unsupported; ignore the header.
};
/**
* Returns the value of the response header identified by key. This
* implementation only supports the 'content-type' header.
* @param {string} key The request header to fetch. If this parameter is set to
* 'content-type' (case-insensitive), this function returns the value of
* the 'content-type' request header. If this parameter is set to any other
* value, this function always returns an empty string.
* @return {string} The value of the response header, or an empty string if key
* is not 'content-type' (case-insensitive).
* @override
*/
goog.net.IeCorsXhrAdapter.prototype.getResponseHeader = function(key) {
if (key.toLowerCase() == 'content-type') {
return this.xdr_.contentType;
}
return '';
};
/**
* Handles a request that has fully loaded successfully.
* @private
*/
goog.net.IeCorsXhrAdapter.prototype.handleLoad_ = function() {
// IE only calls onload if the status is 200, so the status code must be OK.
this.status = goog.net.HttpStatus.OK;
this.responseText = this.xdr_.responseText;
this.setReadyState_(goog.net.XmlHttp.ReadyState.COMPLETE);
};
/**
* Handles a request that has failed to load.
* @private
*/
goog.net.IeCorsXhrAdapter.prototype.handleError_ = function() {
// IE doesn't tell us what the status code actually is (other than the fact
// that it is not 200), so simulate an INTERNAL_SERVER_ERROR.
this.status = goog.net.HttpStatus.INTERNAL_SERVER_ERROR;
this.responseText = null;
this.setReadyState_(goog.net.XmlHttp.ReadyState.COMPLETE);
};
/**
* Handles a request that timed out.
* @private
*/
goog.net.IeCorsXhrAdapter.prototype.handleTimeout_ = function() {
this.handleError_();
};
/**
* Handles a request that is in the process of loading.
* @private
*/
goog.net.IeCorsXhrAdapter.prototype.handleProgress_ = function() {
// IE only calls onprogress if the status is 200, so the status code must be
// OK.
this.status = goog.net.HttpStatus.OK;
this.setReadyState_(goog.net.XmlHttp.ReadyState.LOADING);
};
/**
* Sets this XHR's ready state and fires the onreadystatechange listener (if one
* is set).
* @param {number} readyState The new ready state.
* @private
*/
goog.net.IeCorsXhrAdapter.prototype.setReadyState_ = function(readyState) {
this.readyState = readyState;
if (this.onreadystatechange) {
this.onreadystatechange();
}
};
/**
* Returns the response headers from the server. This implemntation only returns
* the 'content-type' header.
* @return {string} The headers returned from the server.
* @override
*/
goog.net.IeCorsXhrAdapter.prototype.getAllResponseHeaders = function() {
return 'content-type: ' + this.xdr_.contentType;
};
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2013 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.CorsXmlHttpFactory
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.CorsXmlHttpFactoryTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,44 @@
// Copyright 2013 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.
goog.provide('goog.net.CorsXmlHttpFactoryTest');
goog.setTestOnly('goog.net.CorsXmlHttpFactoryTest');
goog.require('goog.net.CorsXmlHttpFactory');
goog.require('goog.net.IeCorsXhrAdapter');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
function testBrowserSupport() {
var requestFactory = new goog.net.CorsXmlHttpFactory();
if (goog.userAgent.IE) {
if (goog.userAgent.isVersionOrHigher('10')) {
// Continue: IE10 supports CORS requests using native XMLHttpRequest.
} else if (goog.userAgent.isVersionOrHigher('8')) {
assertTrue(
requestFactory.createInstance() instanceof goog.net.IeCorsXhrAdapter);
return;
} else {
try {
requestFactory.createInstance();
fail('Error expected.');
} catch (e) {
assertEquals('Unsupported browser', e.message);
return;
}
}
}
// All other browsers support CORS requests using native XMLHttpRequest.
assertTrue(requestFactory.createInstance() instanceof XMLHttpRequest);
}
@@ -0,0 +1,894 @@
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Cross domain RPC library using the <a
* href="http://go/xd2_design" target="_top">XD2 approach</a>.
*
* <h5>Protocol</h5>
* Client sends a request across domain via a form submission. Server
* receives these parameters: "xdpe:request-id", "xdpe:dummy-uri" ("xdpe" for
* "cross domain parameter to echo back") and other user parameters prefixed
* with "xdp" (for "cross domain parameter"). Headers are passed as parameters
* prefixed with "xdh" (for "cross domain header"). Only strings are supported
* for parameters and headers. A GET method is mapped to a form GET. All
* other methods are mapped to a POST. Server is expected to produce a
* HTML response such as the following:
* <pre>
* &lt;body&gt;
* &lt;script type="text/javascript"
* src="path-to-crossdomainrpc.js"&gt;&lt;/script&gt;
* var currentDirectory = location.href.substring(
* 0, location.href.lastIndexOf('/')
* );
*
* // echo all parameters prefixed with "xdpe:"
* var echo = {};
* echo[goog.net.CrossDomainRpc.PARAM_ECHO_REQUEST_ID] =
* &lt;value of parameter "xdpe:request-id"&gt;;
* echo[goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI] =
* &lt;value of parameter "xdpe:dummy-uri"&gt;;
*
* goog.net.CrossDomainRpc.sendResponse(
* '({"result":"&lt;responseInJSON"})',
* true, // is JSON
* echo, // parameters to echo back
* status, // response status code
* headers // response headers
* );
* &lt;/script&gt;
* &lt;/body&gt;
* </pre>
*
* <h5>Server Side</h5>
* For an example of the server side, refer to the following files:
* <ul>
* <li>http://go/xdservletfilter.java</li>
* <li>http://go/xdservletrequest.java</li>
* <li>http://go/xdservletresponse.java</li>
* </ul>
*
* <h5>System Requirements</h5>
* Tested on IE6, IE7, Firefox 2.0 and Safari nightly r23841.
*
*/
goog.provide('goog.net.CrossDomainRpc');
goog.require('goog.Uri');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.safe');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.html.legacyconversions');
goog.require('goog.json');
goog.require('goog.log');
goog.require('goog.net.EventType');
goog.require('goog.net.HttpStatus');
goog.require('goog.string');
goog.require('goog.userAgent');
/**
* Creates a new instance of cross domain RPC.
*
* This class makes use of goog.html.legacyconversions and provides no
* HTML-type-safe alternative. As such, it is not compatible with
* code that sets goog.html.legacyconversions.ALLOW_LEGACY_CONVERSIONS to
* false.
*
* @extends {goog.events.EventTarget}
* @constructor
* @final
*/
goog.net.CrossDomainRpc = function() {
goog.events.EventTarget.call(this);
};
goog.inherits(goog.net.CrossDomainRpc, goog.events.EventTarget);
/**
* Cross-domain response iframe marker.
* @type {string}
* @private
*/
goog.net.CrossDomainRpc.RESPONSE_MARKER_ = 'xdrp';
/**
* Use a fallback dummy resource if none specified or detected.
* @type {boolean}
* @private
*/
goog.net.CrossDomainRpc.useFallBackDummyResource_ = true;
/** @type {Object} */
goog.net.CrossDomainRpc.prototype.responseHeaders;
/** @type {string} */
goog.net.CrossDomainRpc.prototype.responseText;
/** @type {number} */
goog.net.CrossDomainRpc.prototype.status;
/** @type {number} */
goog.net.CrossDomainRpc.prototype.timeWaitedAfterResponseReady_;
/** @private {boolean} */
goog.net.CrossDomainRpc.prototype.responseTextIsJson_;
/** @private {boolean} */
goog.net.CrossDomainRpc.prototype.responseReady_;
/** @private {!HTMLIFrameElement} */
goog.net.CrossDomainRpc.prototype.requestFrame_;
/** @private {goog.events.Key} */
goog.net.CrossDomainRpc.prototype.loadListenerKey_;
/**
* Checks to see if we are executing inside a response iframe. This is the
* case when this page is used as a dummy resource to gain caller's domain.
* @return {*} True if we are executing inside a response iframe; false
* otherwise.
* @private
*/
goog.net.CrossDomainRpc.isInResponseIframe_ = function() {
return window.location && (window.location.hash.indexOf(
goog.net.CrossDomainRpc.RESPONSE_MARKER_) == 1 ||
window.location.search.indexOf(
goog.net.CrossDomainRpc.RESPONSE_MARKER_) == 1);
};
/**
* Stops execution of the rest of the page if this page is loaded inside a
* response iframe.
*/
if (goog.net.CrossDomainRpc.isInResponseIframe_()) {
if (goog.userAgent.IE) {
document.execCommand('Stop');
} else if (goog.userAgent.GECKO) {
window.stop();
} else {
throw Error('stopped');
}
}
/**
* Sets the URI for a dummy resource on caller's domain. This function is
* used for specifying a particular resource to use rather than relying on
* auto detection.
* @param {string} dummyResourceUri URI to dummy resource on the same domain
* of caller's page.
*/
goog.net.CrossDomainRpc.setDummyResourceUri = function(dummyResourceUri) {
goog.net.CrossDomainRpc.dummyResourceUri_ = dummyResourceUri;
};
/**
* Sets whether a fallback dummy resource ("/robots.txt" on Firefox and Safari
* and current page on IE) should be used when a suitable dummy resource is
* not available.
* @param {boolean} useFallBack Whether to use fallback or not.
*/
goog.net.CrossDomainRpc.setUseFallBackDummyResource = function(useFallBack) {
goog.net.CrossDomainRpc.useFallBackDummyResource_ = useFallBack;
};
/**
* Sends a request across domain.
* @param {string} uri Uri to make request to.
* @param {Function=} opt_continuation Continuation function to be called
* when request is completed. Takes one argument of an event object
* whose target has the following properties: "status" is the HTTP
* response status code, "responseText" is the response text,
* and "headers" is an object with all response headers. The event
* target's getResponseJson() method returns a JavaScript object evaluated
* from the JSON response or undefined if response is not JSON.
* @param {string=} opt_method Method of request. Default is POST.
* @param {Object=} opt_params Parameters. Each property is turned into a
* request parameter.
* @param {Object=} opt_headers Map of headers of the request.
*/
goog.net.CrossDomainRpc.send =
function(uri, opt_continuation, opt_method, opt_params, opt_headers) {
var xdrpc = new goog.net.CrossDomainRpc();
if (opt_continuation) {
goog.events.listen(xdrpc, goog.net.EventType.COMPLETE, opt_continuation);
}
goog.events.listen(xdrpc, goog.net.EventType.READY, xdrpc.reset);
xdrpc.sendRequest(uri, opt_method, opt_params, opt_headers);
};
/**
* Sets debug mode to true or false. When debug mode is on, response iframes
* are visible and left behind after their use is finished.
* @param {boolean} flag Flag to indicate intention to turn debug model on
* (true) or off (false).
*/
goog.net.CrossDomainRpc.setDebugMode = function(flag) {
goog.net.CrossDomainRpc.debugMode_ = flag;
};
/**
* Logger for goog.net.CrossDomainRpc
* @type {goog.log.Logger}
* @private
*/
goog.net.CrossDomainRpc.logger_ =
goog.log.getLogger('goog.net.CrossDomainRpc');
/**
* Creates the HTML of an input element
* @param {string} name Name of input element.
* @param {*} value Value of input element.
* @return {string} HTML of input element with that name and value.
* @private
*/
goog.net.CrossDomainRpc.createInputHtml_ = function(name, value) {
return '<textarea name="' + name + '">' +
goog.net.CrossDomainRpc.escapeAmpersand_(value) + '</textarea>';
};
/**
* Escapes ampersand so that XML/HTML entities are submitted as is because
* browser unescapes them when they are put into a text area.
* @param {*} value Value to escape.
* @return {*} Value with ampersand escaped, if value is a string;
* otherwise the value itself is returned.
* @private
*/
goog.net.CrossDomainRpc.escapeAmpersand_ = function(value) {
return value && (goog.isString(value) || value.constructor == String) ?
value.replace(/&/g, '&amp;') : value;
};
/**
* Finds a dummy resource that can be used by response to gain domain of
* requester's page.
* @return {string} URI of the resource to use.
* @private
*/
goog.net.CrossDomainRpc.getDummyResourceUri_ = function() {
if (goog.net.CrossDomainRpc.dummyResourceUri_) {
return goog.net.CrossDomainRpc.dummyResourceUri_;
}
// find a style sheet if not on IE, which will attempt to save style sheet
if (goog.userAgent.GECKO) {
var links = document.getElementsByTagName(goog.dom.TagName.LINK);
for (var i = 0; i < links.length; i++) {
var link = links[i];
// find a link which is on the same domain as this page
// cannot use one with '?' or '#' in its URL as it will confuse
// goog.net.CrossDomainRpc.getFramePayload_()
if (link.rel == 'stylesheet' &&
goog.Uri.haveSameDomain(link.href, window.location.href) &&
link.href.indexOf('?') < 0) {
return goog.net.CrossDomainRpc.removeHash_(link.href);
}
}
}
var images = document.getElementsByTagName(goog.dom.TagName.IMG);
for (var i = 0; i < images.length; i++) {
var image = images[i];
// find a link which is on the same domain as this page
// cannot use one with '?' or '#' in its URL as it will confuse
// goog.net.CrossDomainRpc.getFramePayload_()
if (goog.Uri.haveSameDomain(image.src, window.location.href) &&
image.src.indexOf('?') < 0) {
return goog.net.CrossDomainRpc.removeHash_(image.src);
}
}
if (!goog.net.CrossDomainRpc.useFallBackDummyResource_) {
throw Error(
'No suitable dummy resource specified or detected for this page');
}
if (goog.userAgent.IE) {
// use this page as the dummy resource; remove hash from URL if any
return goog.net.CrossDomainRpc.removeHash_(window.location.href);
} else {
/**
* Try to use "http://<this-domain>/robots.txt" which may exist. Even if
* it does not, an error page is returned and is a good dummy resource to
* use on Firefox and Safari. An existing resource is faster because it
* is cached.
*/
var locationHref = window.location.href;
var rootSlash = locationHref.indexOf('/', locationHref.indexOf('//') + 2);
var rootHref = locationHref.substring(0, rootSlash);
return rootHref + '/robots.txt';
}
};
/**
* Removes everything at and after hash from URI
* @param {string} uri Uri to to remove hash.
* @return {string} Uri with its hash and all characters after removed.
* @private
*/
goog.net.CrossDomainRpc.removeHash_ = function(uri) {
return uri.split('#')[0];
};
// ------------
// request side
/**
* next request id used to support multiple XD requests at the same time
* @type {number}
* @private
*/
goog.net.CrossDomainRpc.nextRequestId_ = 0;
/**
* Header prefix.
* @type {string}
*/
goog.net.CrossDomainRpc.HEADER = 'xdh:';
/**
* Parameter prefix.
* @type {string}
*/
goog.net.CrossDomainRpc.PARAM = 'xdp:';
/**
* Parameter to echo prefix.
* @type {string}
*/
goog.net.CrossDomainRpc.PARAM_ECHO = 'xdpe:';
/**
* Parameter to echo: request id
* @type {string}
*/
goog.net.CrossDomainRpc.PARAM_ECHO_REQUEST_ID =
goog.net.CrossDomainRpc.PARAM_ECHO + 'request-id';
/**
* Parameter to echo: dummy resource URI
* @type {string}
*/
goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI =
goog.net.CrossDomainRpc.PARAM_ECHO + 'dummy-uri';
/**
* Cross-domain request marker.
* @type {string}
* @private
*/
goog.net.CrossDomainRpc.REQUEST_MARKER_ = 'xdrq';
/**
* Sends a request across domain.
* @param {string} uri Uri to make request to.
* @param {string=} opt_method Method of request. Default is POST.
* @param {Object=} opt_params Parameters. Each property is turned into a
* request parameter.
* @param {Object=} opt_headers Map of headers of the request.
*/
goog.net.CrossDomainRpc.prototype.sendRequest =
function(uri, opt_method, opt_params, opt_headers) {
// create request frame
var requestFrame = this.requestFrame_ = /** @type {!HTMLIFrameElement} */ (
document.createElement(goog.dom.TagName.IFRAME));
var requestId = goog.net.CrossDomainRpc.nextRequestId_++;
requestFrame.id = goog.net.CrossDomainRpc.REQUEST_MARKER_ + '-' + requestId;
if (!goog.net.CrossDomainRpc.debugMode_) {
requestFrame.style.position = 'absolute';
requestFrame.style.top = '-5000px';
requestFrame.style.left = '-5000px';
}
document.body.appendChild(requestFrame);
// build inputs
var inputs = [];
// add request id
inputs.push(goog.net.CrossDomainRpc.createInputHtml_(
goog.net.CrossDomainRpc.PARAM_ECHO_REQUEST_ID, requestId));
// add dummy resource uri
var dummyUri = goog.net.CrossDomainRpc.getDummyResourceUri_();
goog.log.fine(goog.net.CrossDomainRpc.logger_,
'dummyUri: ' + dummyUri);
inputs.push(goog.net.CrossDomainRpc.createInputHtml_(
goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI, dummyUri));
// add parameters
if (opt_params) {
for (var name in opt_params) {
var value = opt_params[name];
inputs.push(goog.net.CrossDomainRpc.createInputHtml_(
goog.net.CrossDomainRpc.PARAM + name, value));
}
}
// add headers
if (opt_headers) {
for (var name in opt_headers) {
var value = opt_headers[name];
inputs.push(goog.net.CrossDomainRpc.createInputHtml_(
goog.net.CrossDomainRpc.HEADER + name, value));
}
}
var requestFrameContent = '<body><form method="' +
(opt_method == 'GET' ? 'GET' : 'POST') + '" action="' +
uri + '">' + inputs.join('') + '</form></body>';
var requestFrameContentHtml = goog.html.legacyconversions.safeHtmlFromString(
requestFrameContent);
var requestFrameDoc = goog.dom.getFrameContentDocument(requestFrame);
requestFrameDoc.open();
goog.dom.safe.documentWrite(requestFrameDoc, requestFrameContentHtml);
requestFrameDoc.close();
requestFrameDoc.forms[0].submit();
requestFrameDoc = null;
this.loadListenerKey_ = goog.events.listen(
requestFrame, goog.events.EventType.LOAD, function() {
goog.log.fine(goog.net.CrossDomainRpc.logger_, 'response ready');
this.responseReady_ = true;
}, false, this);
this.receiveResponse_();
};
/**
* period of response polling (ms)
* @type {number}
* @private
*/
goog.net.CrossDomainRpc.RESPONSE_POLLING_PERIOD_ = 50;
/**
* timeout from response comes back to sendResponse is called (ms)
* @type {number}
* @private
*/
goog.net.CrossDomainRpc.SEND_RESPONSE_TIME_OUT_ = 500;
/**
* Receives response by polling to check readiness of response and then
* reads response frames and assembles response data
* @private
*/
goog.net.CrossDomainRpc.prototype.receiveResponse_ = function() {
this.timeWaitedAfterResponseReady_ = 0;
var responseDetectorHandle = window.setInterval(goog.bind(function() {
this.detectResponse_(responseDetectorHandle);
}, this), goog.net.CrossDomainRpc.RESPONSE_POLLING_PERIOD_);
};
/**
* Detects response inside request frame
* @param {number} responseDetectorHandle Handle of detector.
* @private
*/
goog.net.CrossDomainRpc.prototype.detectResponse_ =
function(responseDetectorHandle) {
var requestFrameWindow = this.requestFrame_.contentWindow;
var grandChildrenLength = requestFrameWindow.frames.length;
var responseInfoFrame = null;
if (grandChildrenLength > 0 &&
goog.net.CrossDomainRpc.isResponseInfoFrame_(responseInfoFrame =
requestFrameWindow.frames[grandChildrenLength - 1])) {
goog.log.fine(goog.net.CrossDomainRpc.logger_,
'xd response ready');
var responseInfoPayload = goog.net.CrossDomainRpc.getFramePayload_(
responseInfoFrame).substring(1);
var params = new goog.Uri.QueryData(responseInfoPayload);
var chunks = [];
var numChunks = Number(params.get('n'));
goog.log.fine(goog.net.CrossDomainRpc.logger_,
'xd response number of chunks: ' + numChunks);
for (var i = 0; i < numChunks; i++) {
var responseFrame = requestFrameWindow.frames[i];
if (!responseFrame || !responseFrame.location ||
!responseFrame.location.href) {
// On Safari 3.0, it is sometimes the case that the
// iframe exists but doesn't have a same domain href yet.
goog.log.fine(goog.net.CrossDomainRpc.logger_,
'xd response iframe not ready');
return;
}
var responseChunkPayload =
goog.net.CrossDomainRpc.getFramePayload_(responseFrame);
// go past "chunk="
var chunkIndex = responseChunkPayload.indexOf(
goog.net.CrossDomainRpc.PARAM_CHUNK_) +
goog.net.CrossDomainRpc.PARAM_CHUNK_.length + 1;
var chunk = responseChunkPayload.substring(chunkIndex);
chunks.push(chunk);
}
window.clearInterval(responseDetectorHandle);
var responseData = chunks.join('');
// Payload is not encoded to begin with on IE. Decode in other cases only.
if (!goog.userAgent.IE) {
responseData = decodeURIComponent(responseData);
}
this.status = Number(params.get('status'));
this.responseText = responseData;
this.responseTextIsJson_ = params.get('isDataJson') == 'true';
this.responseHeaders = goog.json.unsafeParse(
/** @type {string} */ (params.get('headers')));
this.dispatchEvent(goog.net.EventType.READY);
this.dispatchEvent(goog.net.EventType.COMPLETE);
} else {
if (this.responseReady_) {
/* The response has come back. But the first response iframe has not
* been created yet. If this lasts long enough, it is an error.
*/
this.timeWaitedAfterResponseReady_ +=
goog.net.CrossDomainRpc.RESPONSE_POLLING_PERIOD_;
if (this.timeWaitedAfterResponseReady_ >
goog.net.CrossDomainRpc.SEND_RESPONSE_TIME_OUT_) {
goog.log.fine(goog.net.CrossDomainRpc.logger_,
'xd response timed out');
window.clearInterval(responseDetectorHandle);
this.status = goog.net.HttpStatus.INTERNAL_SERVER_ERROR;
this.responseText = 'response timed out';
this.dispatchEvent(goog.net.EventType.READY);
this.dispatchEvent(goog.net.EventType.ERROR);
this.dispatchEvent(goog.net.EventType.COMPLETE);
}
}
}
};
/**
* Checks whether a frame is response info frame.
* @param {Object} frame Frame to check.
* @return {boolean} True if frame is a response info frame; false otherwise.
* @private
*/
goog.net.CrossDomainRpc.isResponseInfoFrame_ = function(frame) {
/** @preserveTry */
try {
return goog.net.CrossDomainRpc.getFramePayload_(frame).indexOf(
goog.net.CrossDomainRpc.RESPONSE_INFO_MARKER_) == 1;
} catch (e) {
// frame not ready for same-domain access yet
return false;
}
};
/**
* Returns the payload of a frame (value after # or ? on the URL). This value
* is URL encoded except IE, where the value is not encoded to begin with.
* @param {Object} frame Frame.
* @return {string} Payload of that frame.
* @private
*/
goog.net.CrossDomainRpc.getFramePayload_ = function(frame) {
var href = frame.location.href;
var question = href.indexOf('?');
var hash = href.indexOf('#');
// On IE, beucase the URL is not encoded, we can have a case where ?
// is the delimiter before payload and # in payload or # as the delimiter
// and ? in payload. So here we treat whoever is the first as the delimiter.
var delimiter = question < 0 ? hash :
hash < 0 ? question : Math.min(question, hash);
return href.substring(delimiter);
};
/**
* If response is JSON, evaluates it to a JavaScript object and
* returns it; otherwise returns undefined.
* @return {Object|undefined} JavaScript object if response is in JSON
* or undefined.
*/
goog.net.CrossDomainRpc.prototype.getResponseJson = function() {
return this.responseTextIsJson_ ?
goog.json.unsafeParse(this.responseText) : undefined;
};
/**
* @return {boolean} Whether the request completed with a success.
*/
goog.net.CrossDomainRpc.prototype.isSuccess = function() {
// Definition similar to goog.net.XhrIo.prototype.isSuccess.
switch (this.status) {
case goog.net.HttpStatus.OK:
case goog.net.HttpStatus.NOT_MODIFIED:
return true;
default:
return false;
}
};
/**
* Removes request iframe used.
*/
goog.net.CrossDomainRpc.prototype.reset = function() {
if (!goog.net.CrossDomainRpc.debugMode_) {
goog.log.fine(goog.net.CrossDomainRpc.logger_,
'request frame removed: ' + this.requestFrame_.id);
goog.events.unlistenByKey(this.loadListenerKey_);
this.requestFrame_.parentNode.removeChild(this.requestFrame_);
}
delete this.requestFrame_;
};
// -------------
// response side
/**
* Name of response info iframe.
* @type {string}
* @private
*/
goog.net.CrossDomainRpc.RESPONSE_INFO_MARKER_ =
goog.net.CrossDomainRpc.RESPONSE_MARKER_ + '-info';
/**
* Maximal chunk size. IE can only handle 4095 bytes on its URL.
* 16MB has been tested on Firefox. But 1MB is a practical size.
* @type {number}
* @private
*/
goog.net.CrossDomainRpc.MAX_CHUNK_SIZE_ =
goog.userAgent.IE ? 4095 : 1024 * 1024;
/**
* Query parameter 'chunk'.
* @type {string}
* @private
*/
goog.net.CrossDomainRpc.PARAM_CHUNK_ = 'chunk';
/**
* Prefix before data chunk for passing other parameters.
* type String
* @private
*/
goog.net.CrossDomainRpc.CHUNK_PREFIX_ =
goog.net.CrossDomainRpc.RESPONSE_MARKER_ + '=1&' +
goog.net.CrossDomainRpc.PARAM_CHUNK_ + '=';
/**
* Makes response available for grandparent (requester)'s receiveResponse
* call to pick up by creating a series of iframes pointed to the dummy URI
* with a payload (value after either ? or #) carrying a chunk of response
* data and a response info iframe that tells the grandparent (requester) the
* readiness of response.
* @param {string} data Response data (string or JSON string).
* @param {boolean} isDataJson true if data is a JSON string; false if just a
* string.
* @param {Object} echo Parameters to echo back
* "xdpe:request-id": Server that produces the response needs to
* copy it here to support multiple current XD requests on the same page.
* "xdpe:dummy-uri": URI to a dummy resource that response
* iframes point to to gain the domain of the client. This can be an
* image (IE) or a CSS file (FF) found on the requester's page.
* Server should copy value from request parameter "xdpe:dummy-uri".
* @param {number} status HTTP response status code.
* @param {string} headers Response headers in JSON format.
*/
goog.net.CrossDomainRpc.sendResponse =
function(data, isDataJson, echo, status, headers) {
var dummyUri = echo[goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI];
// since the dummy-uri can be specified by the user, verify that it doesn't
// use any other protocols. (Specifically we don't want users to use a
// dummy-uri beginning with "javascript:").
if (!goog.string.caseInsensitiveStartsWith(dummyUri, 'http://') &&
!goog.string.caseInsensitiveStartsWith(dummyUri, 'https://')) {
dummyUri = 'http://' + dummyUri;
}
// usable chunk size is max less dummy URI less chunk prefix length
// TODO(user): Figure out why we need to do "- 1" below
var chunkSize = goog.net.CrossDomainRpc.MAX_CHUNK_SIZE_ - dummyUri.length -
1 - // payload delimiter ('#' or '?')
goog.net.CrossDomainRpc.CHUNK_PREFIX_.length - 1;
/*
* Here we used to do URI encoding of data before we divide it into chunks
* and decode on the receiving end. We don't do this any more on IE for the
* following reasons.
*
* 1) On IE, calling decodeURIComponent on a relatively large string is
* extremely slow (~22s for 160KB). So even a moderate amount of data
* makes this library pretty much useless. Fortunately, we can actually
* put unencoded data on IE's URL and get it back reliably. So we are
* completely skipping encoding and decoding on IE. When we call
* getFrameHash_ to get it back, the value is still intact(*) and unencoded.
* 2) On Firefox, we have to call decodeURIComponent because location.hash
* does decoding by itself. Fortunately, decodeURIComponent is not slow
* on Firefox.
* 3) Safari automatically encodes everything you put on URL and it does not
* automatically decode when you access it via location.hash or
* location.href. So we encode it here and decode it in detectResponse_().
*
* Note(*): IE actually does encode only space to %20 and decodes that
* automatically when you do location.href or location.hash.
*/
if (!goog.userAgent.IE) {
data = encodeURIComponent(data);
}
var numChunksToSend = Math.ceil(data.length / chunkSize);
if (numChunksToSend == 0) {
goog.net.CrossDomainRpc.createResponseInfo_(
dummyUri, numChunksToSend, isDataJson, status, headers);
} else {
var numChunksSent = 0;
var checkToCreateResponseInfo_ = function() {
if (++numChunksSent == numChunksToSend) {
goog.net.CrossDomainRpc.createResponseInfo_(
dummyUri, numChunksToSend, isDataJson, status, headers);
}
};
for (var i = 0; i < numChunksToSend; i++) {
var chunkStart = i * chunkSize;
var chunkEnd = chunkStart + chunkSize;
var chunk = chunkEnd > data.length ?
data.substring(chunkStart) :
data.substring(chunkStart, chunkEnd);
var responseFrame = document.createElement(goog.dom.TagName.IFRAME);
responseFrame.src = dummyUri +
goog.net.CrossDomainRpc.getPayloadDelimiter_(dummyUri) +
goog.net.CrossDomainRpc.CHUNK_PREFIX_ + chunk;
document.body.appendChild(responseFrame);
// We used to call the function below when handling load event of
// responseFrame. But that event does not fire on IE when current
// page is used as the dummy resource (because its loading is stopped?).
// It also does not fire sometimes on Firefox. So now we call it
// directly.
checkToCreateResponseInfo_();
}
}
};
/**
* Creates a response info iframe to indicate completion of sendResponse
* @param {string} dummyUri URI to a dummy resource.
* @param {number} numChunks Total number of chunks.
* @param {boolean} isDataJson Whether response is a JSON string or just string.
* @param {number} status HTTP response status code.
* @param {string} headers Response headers in JSON format.
* @private
*/
goog.net.CrossDomainRpc.createResponseInfo_ =
function(dummyUri, numChunks, isDataJson, status, headers) {
var responseInfoFrame = document.createElement(goog.dom.TagName.IFRAME);
document.body.appendChild(responseInfoFrame);
responseInfoFrame.src = dummyUri +
goog.net.CrossDomainRpc.getPayloadDelimiter_(dummyUri) +
goog.net.CrossDomainRpc.RESPONSE_INFO_MARKER_ +
'=1&n=' + numChunks + '&isDataJson=' + isDataJson + '&status=' + status +
'&headers=' + encodeURIComponent(headers);
};
/**
* Returns payload delimiter, either "#" when caller's page is not used as
* the dummy resource or "?" when it is, in which case caching issues prevent
* response frames to gain the caller's domain.
* @param {string} dummyUri URI to resource being used as dummy resource.
* @return {string} Either "?" when caller's page is used as dummy resource or
* "#" if it is not.
* @private
*/
goog.net.CrossDomainRpc.getPayloadDelimiter_ = function(dummyUri) {
return goog.net.CrossDomainRpc.REFERRER_ == dummyUri ? '?' : '#';
};
/**
* Removes all parameters (after ? or #) from URI.
* @param {string} uri URI to remove parameters from.
* @return {string} URI with all parameters removed.
* @private
*/
goog.net.CrossDomainRpc.removeUriParams_ = function(uri) {
// remove everything after question mark
var question = uri.indexOf('?');
if (question > 0) {
uri = uri.substring(0, question);
}
// remove everything after hash mark
var hash = uri.indexOf('#');
if (hash > 0) {
uri = uri.substring(0, hash);
}
return uri;
};
/**
* Gets a response header.
* @param {string} name Name of response header.
* @return {string|undefined} Value of response header; undefined if not found.
*/
goog.net.CrossDomainRpc.prototype.getResponseHeader = function(name) {
return goog.isObject(this.responseHeaders) ?
this.responseHeaders[name] : undefined;
};
/**
* Referrer of current document with all parameters after "?" and "#" stripped.
* @type {string}
* @private
*/
goog.net.CrossDomainRpc.REFERRER_ =
goog.net.CrossDomainRpc.removeUriParams_(document.referrer);
@@ -0,0 +1,7 @@
/*
* Copyright 2010 The Closure Library Authors. All Rights Reserved.
*
* Use of this source code is governed by the Apache License, Version 2.0.
* See the COPYING file for details.
*/
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2007 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.CrossDomainRpc
</title>
<link href="crossdomainrpc_test.css?123" rel="stylesheet" type="text/css">
<link href="crossdomainrpc_test.css#123" rel="stylesheet" type="text/css">
<link href="crossdomainrpc_test.css" rel="stylesheet" type="text/css">
<script src="../base.js">
</script>
<script>
goog.require('goog.net.CrossDomainRpcTest');
</script>
</head>
<body>
<img src="crossdomainrpc_test.gif?123" alt="dummy resource">
<img src="crossdomainrpc_test.gif#123" alt="dummy resource">
<img src="crossdomainrpc_test.gif" alt="dummy resource">
</body>
</html>
@@ -0,0 +1,107 @@
// 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.
goog.provide('goog.net.CrossDomainRpcTest');
goog.setTestOnly('goog.net.CrossDomainRpcTest');
goog.require('goog.Promise');
goog.require('goog.log');
goog.require('goog.net.CrossDomainRpc');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
goog.require('goog.userAgent.product');
function print(o) {
if (Object.prototype.toSource) {
return o.toSource();
} else {
var fragments = [];
fragments.push('{');
var first = true;
for (var p in o) {
if (!first) fragments.push(',');
fragments.push(p);
fragments.push(':"');
fragments.push(o[p]);
fragments.push('"');
first = false;
}
return fragments.join('');
}
}
function testNormalRequest() {
var start = goog.now();
return new goog.Promise(function(resolve, reject) {
goog.net.CrossDomainRpc.send(
'crossdomainrpc_test_response.html',
resolve, 'POST', {xyz: '01234567891123456789'});
}).then(function(e) {
if (e.target.status < 300) {
var elapsed = goog.now() - start;
var responseData = eval(e.target.responseText);
goog.log.fine(goog.net.CrossDomainRpc.logger_,
elapsed + 'ms: [' + responseData.result.length + '] ' +
print(responseData));
assertEquals(16 * 1024, responseData.result.length);
assertEquals(123, e.target.status);
assertEquals(1, e.target.responseHeaders.a);
assertEquals('2', e.target.responseHeaders.b);
} else {
goog.log.fine(goog.net.CrossDomainRpc.logger_, print(e));
fail();
}
});
}
function testErrorRequest() {
// Firefox and Safari do not give a valid error event.
if (goog.userAgent.GECKO || goog.userAgent.product.SAFARI) {
return;
}
return new goog.Promise(function(resolve, reject) {
goog.net.CrossDomainRpc.send(
'http://hoodjimcwaadji.google.com/index.html',
resolve, 'POST', {xyz: '01234567891123456789'});
setTimeout(function() {
reject('CrossDomainRpc.send did not complete within 4000ms');
}, 4000);
}).then(function(e) {
if (e.target.status < 300) {
fail('should have failed requesting a non-existent URI');
} else {
goog.log.fine(goog.net.CrossDomainRpc.logger_,
'expected error seen; event=' + print(e));
}
});
}
function testGetDummyResourceUri() {
var url = goog.net.CrossDomainRpc.getDummyResourceUri_();
assertTrue(
'dummy resource URL should not contain "?"', url.indexOf('?') < 0);
assertTrue(
'dummy resource URL should not contain "#"', url.indexOf('#') < 0);
}
function testRemoveHash() {
assertEquals('abc', goog.net.CrossDomainRpc.removeHash_('abc#123'));
assertEquals('abc', goog.net.CrossDomainRpc.removeHash_('abc#12#3'));
}
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2007 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
In reality, this response comes from a different domain. For simplicity of
testing, this response is one the same domain, while exercising the same
functionality.
-->
<title>crossdomainrpc test response</title>
<body>
<script type="text/javascript" src="../base.js"></script>
<script type="text/javascript">
goog.require('goog.Uri.QueryData');
goog.require('goog.dom');
goog.require('goog.events.EventTarget');
goog.require('goog.json');
goog.require('goog.log');
goog.require('goog.net.EventType');
goog.require('goog.userAgent');
</script>
<script type="text/javascript" src="crossdomainrpc.js"></script>
<script type="text/javascript">
function createPayload(size) {
var chars = [];
for (var i = 0; i < size; i++) {
chars.push('0');
}
return chars.join('');
};
var payload = createPayload(16 * 1024);
var currentDirectory = location.href.substring(
0, location.href.lastIndexOf('/')
);
var echo = {};
echo[goog.net.CrossDomainRpc.PARAM_ECHO_REQUEST_ID] = 0;
echo[goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI] = goog.userAgent.IE ?
currentDirectory + '/crossdomainrpc_test.gif' :
currentDirectory + '/crossdomainrpc_test.css';
goog.net.CrossDomainRpc.sendResponse(
'({"result":"' + payload + '"})',
true, // is JSON
echo, // parameters to echo back
123, // response code
'{"a":1,"b":"2"}' // response headers
);
</script>
</body>
</html>
@@ -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,362 @@
// Copyright 2015 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.
goog.provide('goog.net.FetchXmlHttp');
goog.provide('goog.net.FetchXmlHttpFactory');
goog.require('goog.asserts');
goog.require('goog.events.EventTarget');
goog.require('goog.functions');
goog.require('goog.log');
goog.require('goog.net.XhrLike');
goog.require('goog.net.XmlHttpFactory');
/**
* Factory for creating Xhr objects that uses the native fetch() method.
* https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
* Note that this factory is intended for use in Service Worker only.
* @param {!WorkerGlobalScope} worker The Service Worker global scope.
* @extends {goog.net.XmlHttpFactory}
* @struct
* @constructor
*/
goog.net.FetchXmlHttpFactory = function(worker) {
goog.net.FetchXmlHttpFactory.base(this, 'constructor');
/** @private @final {!WorkerGlobalScope} */
this.worker_ = worker;
/** @private {!RequestCredentials|undefined} */
this.credentialsMode_ = undefined;
/** @private {!RequestCache|undefined} */
this.cacheMode_ = undefined;
};
goog.inherits(
goog.net.FetchXmlHttpFactory, goog.net.XmlHttpFactory);
/** @override */
goog.net.FetchXmlHttpFactory.prototype.createInstance = function() {
var instance = new goog.net.FetchXmlHttp(this.worker_);
if (this.credentialsMode_) {
instance.setCredentialsMode(this.credentialsMode_);
}
if (this.cacheMode_) {
instance.setCacheMode(this.cacheMode_);
}
return instance;
};
/** @override */
goog.net.FetchXmlHttpFactory.prototype.internalGetOptions =
goog.functions.constant({});
/**
* @param {!RequestCredentials} credentialsMode The credentials mode of the
* Service Worker fetch.
*/
goog.net.FetchXmlHttpFactory.prototype.setCredentialsMode =
function(credentialsMode) {
this.credentialsMode_ = credentialsMode;
};
/**
* @param {!RequestCache} cacheMode The cache mode of the Service Worker fetch.
*/
goog.net.FetchXmlHttpFactory.prototype.setCacheMode = function(cacheMode) {
this.cacheMode_ = cacheMode;
};
/**
* FetchXmlHttp object constructor.
* @param {!WorkerGlobalScope} worker
* @extends {goog.events.EventTarget}
* @implements {goog.net.XhrLike}
* @constructor
* @struct
*/
goog.net.FetchXmlHttp = function(worker) {
goog.net.FetchXmlHttp.base(this, 'constructor');
/** @private @final {!WorkerGlobalScope} */
this.worker_ = worker;
/** @private {RequestCredentials|undefined} */
this.credentialsMode_ = undefined;
/** @private {RequestCache|undefined} */
this.cacheMode_ = undefined;
/**
* Request state.
* @type {goog.net.FetchXmlHttp.RequestState}
*/
this.readyState = goog.net.FetchXmlHttp.RequestState.UNSENT;
/**
* HTTP status.
* @type {number}
*/
this.status = 0;
/**
* HTTP status string.
* @type {string}
*/
this.statusText = '';
/**
* Content of the response.
* @type {string}
*/
this.responseText = '';
/**
* Document response entity body.
* NOTE: This is always null and not supported by this class.
* @final {null}
*/
this.responseXML = null;
/**
* Method to call when the state changes.
* @type {?function()}
*/
this.onreadystatechange = null;
/** @private {!Headers} */
this.requestHeaders_ = new Headers();
/** @private {?Headers} */
this.responseHeaders_ = null;
/**
* Request method (GET or POST).
* @private {string}
*/
this.method_ = 'GET';
/**
* Request URL.
* @private {string}
*/
this.url_ = '';
/**
* Whether the request is in progress.
* @private {boolean}
*/
this.inProgress_ = false;
/** @private @final {?goog.log.Logger} */
this.logger_ = goog.log.getLogger('goog.net.FetchXmlHttp');
};
goog.inherits(goog.net.FetchXmlHttp, goog.events.EventTarget);
/**
* State of the requests.
* @enum {number}
*/
goog.net.FetchXmlHttp.RequestState = {
UNSENT: 0,
OPENED: 1,
HEADER_RECEIVED: 2,
LOADING: 3,
DONE: 4
};
/** @override */
goog.net.FetchXmlHttp.prototype.open = function(
method, url, opt_async) {
goog.asserts.assert(!!opt_async, 'Only async requests are supported.');
if (this.readyState != goog.net.FetchXmlHttp.RequestState.UNSENT) {
this.abort();
throw Error('Error reopening a connection');
}
this.method_ = method;
this.url_ = url;
this.readyState = goog.net.FetchXmlHttp.RequestState.OPENED;
this.dispatchCallback_();
};
/** @override */
goog.net.FetchXmlHttp.prototype.send = function(opt_data) {
if (this.readyState != goog.net.FetchXmlHttp.RequestState.OPENED) {
this.abort();
throw Error('need to call open() first. ');
}
this.inProgress_ = true;
var requestInit = {
headers: this.requestHeaders_,
method: this.method_,
credentials: this.credentialsMode_,
cache: this.cacheMode_
};
if (opt_data) {
requestInit['body'] = opt_data;
}
this.worker_.fetch(
new Request(this.url_, /** @type {!RequestInit} */ (requestInit))).then(
this.handleResponse_.bind(this), this.handleSendFailure_.bind(this));
};
/** @override */
goog.net.FetchXmlHttp.prototype.abort = function() {
this.responseText = '';
this.requestHeaders_ = new Headers();
this.status = 0;
if (((this.readyState >= goog.net.FetchXmlHttp.RequestState.OPENED) &&
this.inProgress_) &&
(this.readyState != goog.net.FetchXmlHttp.RequestState.DONE)) {
this.readyState = goog.net.FetchXmlHttp.RequestState.DONE;
this.inProgress_ = false;
this.dispatchCallback_();
}
this.readyState = goog.net.FetchXmlHttp.RequestState.UNSENT;
};
/**
* Handles the fetch response.
* @param {!Response} response
* @private
*/
goog.net.FetchXmlHttp.prototype.handleResponse_ = function(response) {
if (!this.inProgress_) {
// The request was aborted, ignore.
return;
}
if (!this.responseHeaders_) {
this.responseHeaders_ = response.headers;
this.readyState = goog.net.FetchXmlHttp.RequestState.HEADER_RECEIVED;
this.dispatchCallback_();
}
// A callback may abort the request.
if (!this.inProgress_) {
// The request was aborted, ignore.
return;
}
this.readyState = goog.net.FetchXmlHttp.RequestState.LOADING;
this.dispatchCallback_();
// A callback may abort the request.
if (!this.inProgress_) {
// The request was aborted, ignore.
return;
}
response.text().then(this.handleResponseText_.bind(this, response),
this.handleSendFailure_.bind(this));
};
/**
* Handles the response text.
* @param {!Response} response
* @param {string} responseText
* @private
*/
goog.net.FetchXmlHttp.prototype.handleResponseText_ = function(
response, responseText) {
if (!this.inProgress_) {
// The request was aborted, ignore.
return;
}
this.status = response.status;
this.statusText = response.statusText;
this.responseText = responseText;
this.readyState = goog.net.FetchXmlHttp.RequestState.DONE;
this.dispatchCallback_();
};
/**
* Handles the send failure.
* @param {*} error
* @private
*/
goog.net.FetchXmlHttp.prototype.handleSendFailure_ = function(error) {
var e = error instanceof Error ? error : Error(error);
goog.log.warning(this.logger_, 'Failed to fetch url ' + this.url_, e);
if (!this.inProgress_) {
// The request was aborted, ignore.
return;
}
this.readyState = goog.net.FetchXmlHttp.RequestState.DONE;
this.dispatchCallback_();
};
/** @override */
goog.net.FetchXmlHttp.prototype.setRequestHeader = function(header, value) {
this.requestHeaders_.append(header, value);
};
/** @override */
goog.net.FetchXmlHttp.prototype.getResponseHeader = function(header) {
return this.responseHeaders_.get(header.toLowerCase()) || '';
};
/** @override */
goog.net.FetchXmlHttp.prototype.getAllResponseHeaders = function() {
// TODO(user): Implement once the Headers extern support entries().
return '';
};
/**
* @param {!RequestCredentials} credentialsMode The credentials mode of the
* Service Worker fetch.
*/
goog.net.FetchXmlHttp.prototype.setCredentialsMode = function(credentialsMode) {
this.credentialsMode_ = credentialsMode;
};
/**
* @param {!RequestCache} cacheMode The cache mode of the Service Worker fetch.
*/
goog.net.FetchXmlHttp.prototype.setCacheMode = function(cacheMode) {
this.cacheMode_ = cacheMode;
};
/**
* Dispatches the callback, if the callback attribute is defined.
* @private
*/
goog.net.FetchXmlHttp.prototype.dispatchCallback_ = function() {
if (this.onreadystatechange) {
this.onreadystatechange.call(this);
}
};
@@ -0,0 +1,296 @@
// Copyright 2015 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.
goog.provide('goog.net.FetchXmlHttpFactoryTest');
goog.setTestOnly('goog.net.FetchXmlHttpFactoryTest');
goog.require('goog.net.FetchXmlHttp');
goog.require('goog.net.FetchXmlHttpFactory');
goog.require('goog.testing.MockControl');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
goog.require('goog.userAgent.product');
goog.require('goog.userAgent.product.isVersion');
/** @type {!goog.testing.MockControl} */
var mockControl;
/** @type {!goog.testing.FunctionMock} */
var fetchMock;
/** @type {!goog.net.FetchXmlHttpFactory} */
var factory;
/** @type {!WorkerGlobalScope} */
var worker;
/**
* Whether the browser supports running this test.
* @return {boolean}
*/
function shouldRunTests() {
return goog.userAgent.product.CHROME && goog.userAgent.product.isVersion(43);
}
function setUp() {
mockControl = new goog.testing.MockControl();
worker = {};
fetchMock = mockControl.createFunctionMock('fetch');
worker.fetch = fetchMock;
factory = new goog.net.FetchXmlHttpFactory(worker);
}
function tearDown() {
mockControl.$tearDown();
}
/**
* Verifies the open method.
*/
function testOpen() {
mockControl.$replayAll();
var xhr = factory.createInstance();
assertEquals(0, xhr.status);
assertEquals('', xhr.responseText);
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.UNSENT);
var onReadyStateChangeHandler = new goog.testing.recordFunction();
xhr.onreadystatechange = onReadyStateChangeHandler;
xhr.open('GET', 'https://www.google.com', true /* opt_async */);
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.OPENED);
onReadyStateChangeHandler.assertCallCount(1);
mockControl.$verifyAll();
}
/**
* Verifies the open method when the ready state is not unsent.
*/
function testOpen_notUnsent() {
mockControl.$replayAll();
var xhr = factory.createInstance();
xhr.open('GET', 'https://www.google.com', true /* opt_async */);
assertThrows(function() {
xhr.open('GET', 'https://www.google.com', true /* opt_async */);
});
mockControl.$verifyAll();
}
/**
* Verifies that synchronous fetches are not supported.
*/
function testOpen_notAsync() {
mockControl.$replayAll();
var xhr = factory.createInstance();
assertThrows(function() {
xhr.open('GET', 'https://www.google.com', false /* opt_async */);
});
mockControl.$verifyAll();
}
/**
* Verifies the send method.
*/
function testSend() {
fetchMock(new Request(
'https://www.google.com', {headers: new Headers(), method: 'GET'})).
$returns(Promise.resolve(createSuccessResponse()));
mockControl.$replayAll();
verifySendSuccess('GET');
}
/**
* Verifies the send method with POST mode.
*/
function testSendPost() {
fetchMock(new Request(
'https://www.google.com', {
headers: new Headers(),
method: 'POST'
})).
$returns(Promise.resolve(createSuccessResponse()));
mockControl.$replayAll();
verifySendSuccess('POST');
}
/**
* Verifies the send method including credentials.
*/
function testSend_includeCredentials() {
factory = new goog.net.FetchXmlHttpFactory(worker);
factory.setCredentialsMode(/** @type {RequestCredentials} */ ('include'));
fetchMock(new Request(
'https://www.google.com', {
headers: new Headers(),
method: 'POST',
credentials: 'include'
})).$returns(Promise.resolve(createSuccessResponse()));
mockControl.$replayAll();
verifySendSuccess('POST');
}
/**
* Verifies the send method setting cache mode.
*/
function testSend_setCacheMode() {
factory = new goog.net.FetchXmlHttpFactory(worker);
factory.setCacheMode(/** @type {RequestCache} */ ('no-cache'));
fetchMock(new Request(
'https://www.google.com', {
headers: new Headers(),
method: 'POST',
cache: 'no-cache'
})).$returns(Promise.resolve(createSuccessResponse()));
mockControl.$replayAll();
verifySendSuccess('POST');
}
/**
* Util function to verify send method is successful.
*/
function verifySendSuccess(sendMethod) {
var xhr = factory.createInstance();
xhr.open(sendMethod, 'https://www.google.com', true /* opt_async */);
xhr.onreadystatechange = function() {
assertEquals(xhr.readyState,
goog.net.FetchXmlHttp.RequestState.HEADER_RECEIVED);
assertEquals(0, xhr.status);
assertEquals('', xhr.responseText);
assertEquals(xhr.getResponseHeader('dummyHeader'), 'dummyHeaderValue');
xhr.onreadystatechange = function() {
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.LOADING);
assertEquals(0, xhr.status);
assertEquals('', xhr.responseText);
xhr.onreadystatechange = function() {
assertEquals(200, xhr.status);
assertEquals('responseBody', xhr.responseText);
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.DONE);
};
};
};
xhr.send();
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.OPENED);
mockControl.$verifyAll();
}
/**
* Verifies the send method in case of error response.
*/
function testSend_error() {
fetchMock(new Request(
'https://www.google.com', {headers: new Headers(), method: 'GET'})).
$returns(Promise.resolve(createFailedResponse()));
mockControl.$replayAll();
var xhr = factory.createInstance();
xhr.open('GET', 'https://www.google.com', true /* opt_async */);
xhr.onreadystatechange = function() {
assertEquals(xhr.readyState,
goog.net.FetchXmlHttp.RequestState.HEADER_RECEIVED);
assertEquals(0, xhr.status);
assertEquals('', xhr.responseText);
assertEquals(xhr.getResponseHeader('dummyHeader'), 'dummyHeaderValue');
xhr.onreadystatechange = function() {
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.LOADING);
assertEquals(0, xhr.status);
assertEquals('', xhr.responseText);
xhr.onreadystatechange = function() {
assertEquals(500, xhr.status);
assertEquals('responseBody', xhr.responseText);
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.DONE);
};
};
};
xhr.send();
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.OPENED);
mockControl.$verifyAll();
}
/**
* Verifies the send method in case of failure to fetch the url.
*/
function testSend_failToFetch() {
var failedPromise = new Promise(function() {
throw Error('failed to fetch');
});
fetchMock(new Request(
'https://www.google.com', {headers: new Headers(), method: 'GET'})).
$returns(failedPromise);
mockControl.$replayAll();
var xhr = factory.createInstance();
xhr.open('GET', 'https://www.google.com', true /* opt_async */);
xhr.onreadystatechange = function() {
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.DONE);
assertEquals(0, xhr.status);
assertEquals('', xhr.responseText);
};
xhr.send();
assertEquals(xhr.readyState, goog.net.FetchXmlHttp.RequestState.OPENED);
mockControl.$verifyAll();
}
/**
* Creates a successful response.
* @return {!Response}
*/
function createSuccessResponse() {
var headers = new Headers();
headers.set('dummyHeader', 'dummyHeaderValue');
return new Response(
'responseBody' /* opt_body */, {status: 200, headers: headers});
}
/**
* Creates a successful response.
* @return {!Response}
*/
function createFailedResponse() {
var headers = new Headers();
headers.set('dummyHeader', 'dummyHeaderValue');
return new Response(
'responseBody' /* opt_body */, {status: 500, headers: headers});
}
@@ -0,0 +1,746 @@
// 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}
* @final
*/
goog.net.FileDownloader = function(dir, opt_pool) {
goog.net.FileDownloader.base(this, 'constructor');
/**
* 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<!goog.net.FileDownloader>}
* @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.name == goog.fs.Error.ErrorName.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.net.FileDownloader.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}
* @final
*/
goog.net.FileDownloader.Error = function(download, opt_fsErr) {
goog.net.FileDownloader.Error.base(
this, 'constructor', '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.net.FileDownloader.Download_.base(this, 'constructor');
/**
* 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.net.FileDownloader.Download_.base(this, 'disposeInternal');
};
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<title>
Closure Unit Tests - goog.net.FileDownloader
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.FileDownloaderTest')
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,348 @@
// 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.
goog.provide('goog.net.FileDownloaderTest');
goog.setTestOnly('goog.net.FileDownloaderTest');
goog.require('goog.fs.Error');
goog.require('goog.net.ErrorCode');
goog.require('goog.net.FileDownloader');
goog.require('goog.net.XhrIo');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.fs');
goog.require('goog.testing.fs.FileSystem');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.net.XhrIoPool');
var xhrIoPool, xhr, fs, dir, downloader;
function setUpPage() {
goog.testing.fs.install(new goog.testing.PropertyReplacer());
}
function setUp() {
xhrIoPool = new goog.testing.net.XhrIoPool();
xhr = xhrIoPool.getXhr();
fs = new goog.testing.fs.FileSystem();
dir = fs.getRoot();
downloader = new goog.net.FileDownloader(dir, xhrIoPool);
}
function tearDown() {
goog.dispose(downloader);
}
function testDownload() {
var promise = downloader.download('/foo/bar').then(function(blob) {
var fileEntry = dir.getFileSync('`3fa/``2Ffoo`2Fbar/`bar');
assertEquals('data', blob.toString());
assertEquals('data', fileEntry.fileSync().toString());
});
xhr.simulateResponse(200, 'data');
assertEquals('/foo/bar', xhr.getLastUri());
assertEquals(goog.net.XhrIo.ResponseType.ARRAY_BUFFER, xhr.getResponseType());
return promise;
}
function testGetDownloadedBlob() {
var promise = downloader.download('/foo/bar').then(function() {
return downloader.getDownloadedBlob('/foo/bar');
}).then(function(blob) {
assertEquals('data', blob.toString());
});
xhr.simulateResponse(200, 'data');
return promise;
}
function testGetLocalUrl() {
var promise = downloader.download('/foo/bar').then(function() {
return downloader.getLocalUrl('/foo/bar');
}).then(function(url) {
assertMatches(/\/`bar$/, url);
});
xhr.simulateResponse(200, 'data');
return promise;
}
function testLocalUrlWithContentDisposition() {
var promise = downloader.download('/foo/bar').then(function() {
return downloader.getLocalUrl('/foo/bar');
}).then(function(url) {
assertMatches(/\/`qux`22bap$/, url);
});
xhr.simulateResponse(
200, 'data', {'Content-Disposition': 'attachment; filename="qux\\"bap"'});
return promise;
}
function testIsDownloaded() {
var promise = downloader.download('/foo/bar').then(function() {
return downloader.isDownloaded('/foo/bar');
}).then(assertTrue).then(function(isDownloaded) {
return downloader.isDownloaded('/foo/baz');
}).then(assertFalse);
xhr.simulateResponse(200, 'data');
return promise;
}
function testRemove() {
var promise = downloader.download('/foo/bar').then(function() {
return downloader.remove('/foo/bar');
}).then(function() {
return downloader.isDownloaded('/foo/bar');
}).then(assertFalse).then(function() {
return downloader.getDownloadedBlob('/foo/bar');
}).then(function() {
fail('Should not be able to download a missing blob.');
}, function(err) {
assertEquals(goog.fs.Error.ErrorCode.NOT_FOUND, err.code);
var download = downloader.download('/foo/bar');
xhr.simulateResponse(200, 'more data');
return download;
}).then(function() {
return downloader.isDownloaded('/foo/bar');
}).then(assertTrue).then(function() {
return downloader.getDownloadedBlob('/foo/bar');
}).then(function(blob) {
assertEquals('more data', blob.toString());
});
xhr.simulateResponse(200, 'data');
return promise;
}
function testSetBlob() {
return downloader.setBlob(
'/foo/bar', goog.testing.fs.getBlob('data')).then(function() {
return downloader.isDownloaded('/foo/bar');
}).then(assertTrue).then(function() {
return downloader.getDownloadedBlob('/foo/bar');
}).then(function(blob) {
assertEquals('data', blob.toString());
});
}
function testSetBlobWithName() {
return downloader.setBlob(
'/foo/bar', goog.testing.fs.getBlob('data'), 'qux').then(function() {
return downloader.getLocalUrl('/foo/bar');
}).then(function(url) {
assertMatches(/\/`qux$/, url);
});
}
function testDownloadDuringDownload() {
var download1 = downloader.download('/foo/bar');
var download2 = downloader.download('/foo/bar');
var promise = download1.then(function() {
return download2;
}).then(function() {
return downloader.getDownloadedBlob('/foo/bar');
}).then(function(blob) {
assertEquals('data', blob.toString());
});
// There should only need to be one response for both downloads, since the
// second should return the same deferred as the first.
xhr.simulateResponse(200, 'data');
return promise;
}
function testGetDownloadedBlobDuringDownload() {
var hasDownloaded = false;
downloader.download('/foo/bar').then(function() {
hasDownloaded = true;
});
var promise = downloader.waitForDownload('/foo/bar').then(function() {
return downloader.getDownloadedBlob('/foo/bar');
}).then(function(blob) {
assertTrue(hasDownloaded);
assertEquals('data', blob.toString());
});
xhr.simulateResponse(200, 'data');
return promise;
}
function testIsDownloadedDuringDownload() {
var hasDownloaded = false;
downloader.download('/foo/bar').then(function() {
hasDownloaded = true;
});
var promise = downloader.waitForDownload('/foo/bar').then(function() {
return downloader.isDownloaded('/foo/bar');
}).then(assertTrue);
xhr.simulateResponse(200, 'data');
return promise;
}
function testRemoveDuringDownload() {
var hasDownloaded = false;
downloader.download('/foo/bar').then(function() {
hasDownloaded = true;
});
var promise = downloader.waitForDownload('/foo/bar').then(function() {
return downloader.remove('/foo/bar');
}).then(function() {
assertTrue(hasDownloaded);
}).then(function() {
return downloader.isDownloaded('/foo/bar');
}).then(assertFalse);
xhr.simulateResponse(200, 'data');
return promise;
}
function testSetBlobDuringDownload() {
var download = downloader.download('/foo/bar');
var promise = downloader.waitForDownload('/foo/bar').then(function() {
return downloader.setBlob('/foo/bar', goog.testing.fs.getBlob('blob data'));
}).then(function() {
fail('Should not be able to set blob during a download.');
}, function(err) {
assertEquals(
goog.fs.Error.ErrorCode.INVALID_MODIFICATION, err.fileError.code);
return download;
}).then(function() {
return downloader.getDownloadedBlob('/foo/bar');
}).then(function(b) {
assertEquals('xhr data', b.toString());
});
xhr.simulateResponse(200, 'xhr data');
return promise;
}
function testDownloadCanceledBeforeXhr() {
var download = downloader.download('/foo/bar');
var promise = download.then(function() {
fail('Download should have been canceled.');
}, function() {
assertEquals('/foo/bar', xhr.getLastUri());
assertEquals(goog.net.ErrorCode.ABORT, xhr.getLastErrorCode());
assertFalse(xhr.isActive());
return downloader.isDownloaded('/foo/bar');
}).then(assertFalse);
download.cancel();
return promise;
}
function testDownloadCanceledAfterXhr() {
var download = downloader.download('/foo/bar');
xhr.simulateResponse(200, 'data');
download.cancel();
return download.then(function() {
fail('Should not succeed after cancellation.');
}, function() {
assertEquals('/foo/bar', xhr.getLastUri());
assertEquals(goog.net.ErrorCode.NO_ERROR, xhr.getLastErrorCode());
assertFalse(xhr.isActive());
return downloader.isDownloaded('/foo/bar');
}).then(assertFalse);
}
function testFailedXhr() {
var promise = downloader.download('/foo/bar').then(function() {
fail('Download should not have succeeded.');
}, function(err) {
assertEquals('/foo/bar', err.url);
assertEquals(404, err.xhrStatus);
assertEquals(goog.net.ErrorCode.HTTP_ERROR, err.xhrErrorCode);
assertUndefined(err.fileError);
return downloader.isDownloaded('/foo/bar');
}).then(assertFalse);
xhr.simulateResponse(404);
return promise;
}
function testFailedDownloadSave() {
var promise = downloader.download('/foo/bar').then(function() {
var download = downloader.download('/foo/bar');
xhr.simulateResponse(200, 'data');
return download;
}).then(function() {
fail('Should not be able to modify an active download.');
}, function(err) {
assertEquals('/foo/bar', err.url);
assertUndefined(err.xhrStatus);
assertUndefined(err.xhrErrorCode);
assertEquals(
goog.fs.Error.ErrorCode.INVALID_MODIFICATION, err.fileError.code);
});
xhr.simulateResponse(200, 'data');
return promise;
}
function testFailedGetDownloadedBlob() {
return downloader.getDownloadedBlob('/foo/bar').then(function() {
fail('Should not be able to get a missing blob.');
}, function(err) {
assertEquals(goog.fs.Error.ErrorCode.NOT_FOUND, err.code);
});
}
function testFailedRemove() {
return downloader.remove('/foo/bar').then(function() {
fail('Should not be able to remove a missing file.');
}, function(err) {
assertEquals(goog.fs.Error.ErrorCode.NOT_FOUND, err.code);
});
}
function testIsDownloading() {
assertFalse(downloader.isDownloading('/foo/bar'));
var promise = downloader.download('/foo/bar').then(function() {
assertFalse(downloader.isDownloading('/foo/bar'));
});
assertTrue(downloader.isDownloading('/foo/bar'));
xhr.simulateResponse(200, 'data');
return promise;
}
function testIsDownloadingWhenCancelled() {
assertFalse(downloader.isDownloading('/foo/bar'));
var deferred = downloader.download('/foo/bar').addErrback(function() {
assertFalse(downloader.isDownloading('/foo/bar'));
});
assertTrue(downloader.isDownloading('/foo/bar'));
deferred.cancel();
}
function assertMatches(expected, actual) {
assert(
'Expected "' + actual + '" to match ' + expected,
expected.test(actual));
}
@@ -0,0 +1,116 @@
// 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 and RFC 6585.
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
* @see http://tools.ietf.org/html/rfc6585
* @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,
PRECONDITION_REQUIRED: 428,
TOO_MANY_REQUESTS: 429,
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
// Server Error 5xx
INTERNAL_SERVER_ERROR: 500,
NOT_IMPLEMENTED: 501,
BAD_GATEWAY: 502,
SERVICE_UNAVAILABLE: 503,
GATEWAY_TIMEOUT: 504,
HTTP_VERSION_NOT_SUPPORTED: 505,
NETWORK_AUTHENTICATION_REQUIRED: 511,
/*
* 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;
}
};
@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2007 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - Iframe/XHR Execution Context
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.iframeXhrTest');
</script>
</head>
<body>
<p>
XmlHttpRequests that initiate from code executed in an iframe, that is then
destroyed, result in an error in FireFox. This test case is used to verify
that Closure's IframeIo and XhrLite do not suffer from this problem. See
<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=369939">
https://bugzilla.mozilla.org/show_bug.cgi?id=369939
</a>
.
</p>
<p>
NOTE(pupius): 14/11/2011 The XhrMonitor code has been removed since the
above bug doesn't manifest in any currently supported versions. This test
is left in place as a way of verifying the problem doesn't resurface.
</p>
</body>
</html>
@@ -0,0 +1,145 @@
// 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.
goog.provide('goog.net.iframeXhrTest');
goog.setTestOnly('goog.net.iframeXhrTest');
goog.require('goog.Timer');
goog.require('goog.debug.Console');
goog.require('goog.debug.LogManager');
goog.require('goog.debug.Logger');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.net.IframeIo');
goog.require('goog.net.XhrIo');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
var c = new goog.debug.Console;
c.setCapturing(true);
goog.debug.LogManager.getRoot().setLevel(goog.debug.Logger.Level.ALL);
// Can't use exportSymbol if we want JsUnit support
top.GG_iframeFn = goog.net.IframeIo.handleIncrementalData;
// Make the dispose time short enough that it will cause the bug to appear
goog.net.IframeIo.IFRAME_DISPOSE_DELAY_MS = 0;
var fileName = 'iframe_xhr_test_response.html';
var iframeio;
// Create an async test case
var testCase = new goog.testing.AsyncTestCase(document.title);
testCase.stepTimeout = 4 * 1000;
testCase.resultCount = 0;
testCase.xhrCount = 0;
testCase.error = null;
/**
* Set up the iframe io and request the test response page.
* @this {goog.testing.AsyncTestCase}
*/
testCase.setUpPage = function() {
testCase.waitForAsync('setUpPage');
iframeio = new goog.net.IframeIo();
goog.events.listen(
iframeio, 'incrementaldata', this.onIframeData, false, this);
goog.events.listen(
iframeio, 'ready', this.onIframeReady, false, this);
iframeio.send(fileName);
};
/** Disposes the iframe object. */
testCase.tearDownPage = function() {
iframeio.dispose();
};
/**
* Handles the packets received from the Iframe incremental results.
* @this {goog.testing.AsyncTestCase}
*/
testCase.onIframeData = function(e) {
this.log('Data received : ' + e.data);
this.resultCount++;
goog.net.XhrIo.send(fileName, goog.bind(this.onXhrData, this));
};
/**
* Handles the iframe becoming ready.
* @this {goog.testing.AsyncTestCase}
*/
testCase.onIframeReady = function(e) {
this.log('Iframe ready');
var me = this;
goog.net.XhrIo.send(fileName, goog.bind(this.onXhrData, this));
};
/**
* Handles the response from an Xhr request.
* @this {goog.testing.AsyncTestCase}
*/
testCase.onXhrData = function(e) {
this.xhrCount++;
// We access status directly so that XhrLite doesn't mask the error that
// would be thrown in FF if this worked correctly.
try {
this.log('Xhr Received: ' + e.target.xhr_.status);
} catch (e) {
this.log('ERROR: ' + e.message);
this.error = e;
}
if (this.xhrCount == 4 && this.resultCount == 3) {
// Wait for the async iframe disposal to fire.
this.log('Test set up finished, waiting 500ms for iframe disposal');
goog.Timer.callOnce(goog.bind(this.continueTesting, this), 0);
}
};
/** The main test function that validates the results were as expected. */
testCase.addNewTest('testResults', function() {
assertEquals('There should be 3 data packets', 3, this.resultCount);
// 3 results + 1 ready
assertEquals('There should be 4 XHR results', 4, this.xhrCount);
if (this.error) {
throw this.error;
}
assertEquals('There should be no iframes left', 0,
document.getElementsByTagName(goog.dom.TagName.IFRAME).length);
});
/** This test only runs on GECKO browsers. */
if (goog.userAgent.GECKO) {
/** Used by the JsUnit test runner. */
var testXhrMonitorWorksForIframeIoRequests = function() {
testCase.reset();
testCase.cycleTests();
};
}
// Standalone Closure Test Runner.
if (goog.userAgent.GECKO) {
G_testRunner.initialize(testCase);
} else {
G_testRunner.setStrict(false);
}
@@ -0,0 +1,17 @@
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
</head>
<body>
<script>top.GG_iframeFn(window, [1, 1, 2, 4, 8]);</script>
<script>top.GG_iframeFn(window, [12, 20, 32, 52, 84]);</script>
<script>top.GG_iframeFn(window, [136, 220, 356, 576, 932]);</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
This is just a file that iframeio_different_base_test.html requests to test
iframeIo.
@@ -0,0 +1,27 @@
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<title>
Closure Unit Tests - goog.net.IframeIo (with different base URL)
</title>
<script>
// We use a different base to reproduce the conditions of crbug.com/66987
var href = window.location.href;
var newHref = href.replace(/net.*/, '');
document.write('<base href="' + newHref + '">');
var baseScript = 'base.js';
document.write('<script src="' + baseScript + '"><\/script>');
</script>
<script>
goog.require('goog.net.iframeIoDifferentBaseTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,34 @@
// 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.
goog.provide('goog.net.iframeIoDifferentBaseTest');
goog.setTestOnly('goog.net.iframeIoDifferentBaseTest');
goog.require('goog.Promise');
goog.require('goog.events');
goog.require('goog.net.EventType');
goog.require('goog.net.IframeIo');
goog.require('goog.testing.jsunit');
function testDifferentBaseUri() {
var io = new goog.net.IframeIo();
return new goog.Promise(function(resolve, reject) {
goog.events.listen(io, goog.net.EventType.COMPLETE, resolve);
io.send('net/iframeio_different_base_test.data');
}).then(function() {
assertNotEquals('File should have expected content.',
-1, io.getResponseText().indexOf('just a file'));
});
}
@@ -0,0 +1,115 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.IframeIo
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.IframeIoTest');
</script>
<style>
html, body {
width: 100%;
height: 100%;
overflow:hidden;
}
#log {
position: absolute;
top: 0px;
width: 50%;
right: 0%;
height: 100%;
overflow: auto;
}
p, input {
font-family: verdana, helvetica, arial, sans-serif;
font-size: small;
margin: 0px;
}
input {
font-family: verdana, helvetica, arial, sans-serif;
font-size: x-small;
}
i {
font-size: 85%;
}
</style>
</head>
<body>
<p>
<b>IframeIo manual tests:</b><br><br>
<i>All operations should have no effect on history.</i><br>
<br>
<i>These tests require the ClosureTestServer<br>
to be running with the IframeIoTestServlet.</i><br>
<br></p>
<p>
<a href="javascript:simpleGet()">Simple GET</a><br>
<a href="javascript:simplePost()">Simple POST</a><br>
<a href="javascript:jsonEcho('GET')">JSON echo (get)</a><br>
<a href="javascript:jsonEcho('POST')">JSON echo (post)</a><br>
<a href="javascript:abort()">Test abort</a>
</p>
<form id="uploadform" action="/iframeio/upload" enctype="multipart/form-data" method="POST">
<p><a href="javascript:sendFromForm()">Upload</a> <input name="userfile" type="file"> (big files should fail)</p>
</form>
<p>
<a href="javascript:incremental()">Incremental results</a><br>
<a href="javascript:redirect1()">Redirect (google.com)</a><br>
<a href="javascript:redirect2()">Redirect (/iframeio/ping)</a><br>
<a href="javascript:localUrl1()">Local request (Win path)</a><br>
<a href="javascript:localUrl2()">Local request (Linux path)</a><br>
<a href="javascript:badUrl()">Out of domain request</a><br>
<a href="javascript:getServerTime(false)">Test cache</a> (Date should stay the same for subsequent tests)<br>
<a href="javascript:getServerTime(true)">Test no-cache</a><br>
<a href="javascript:errorGse404()">GSE 404 Error</a><br>
<a href="javascript:errorGfe()">Simulated GFE Error</a><br>
<a href="javascript:errorGmail()">Simulated Gmail Server Error</a><br><br>
</p>
<form id="testfrm" action="/iframeio/jsonecho" method="POST">
<p><b>Comprehensive Form Post Test:</b><br>
<input name="textinput" type="text" value="Default"> Text Input<br>
Text Area<br>
<textarea name="textarea">Default</textarea><br>
<input name="checkbox1" type="checkbox" checked="checked"> Checkbox, default on<br>
<input name="checkbox2" type="checkbox"> Checkbox, default off<br>
Radio: <input name="radio" type="radio" value="Default" checked="checked"> Default,
<input name="radio" type="radio" value="Foo"> Foo,
<input name="radio" type="radio" value="Bar"> Bar<br>
<select name="select">
<option>One</option>
<option>Two</option>
<option selected="selected">Three (Default)</option>
<option>Four</option>
<option>Five</option>
</select><br>
<select name="selectmultiple">
<option>One</option>
<option selected="selected">Two (Default checked)</option>
<option selected="selected">Three (Default checked)</option>
<option>Four</option>
</select>
<a href="javascript:postForm()">Submit this form</a>
</p>
</form>
<p><br><br>
TODO(pupius):<br>
- Local timeout
</p>
<div id="log"></div>
</body>
</html>
@@ -0,0 +1,311 @@
// 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.
goog.provide('goog.net.IframeIoTest');
goog.setTestOnly('goog.net.IframeIoTest');
goog.require('goog.debug');
goog.require('goog.debug.DivConsole');
goog.require('goog.debug.LogManager');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.log');
goog.require('goog.log.Level');
goog.require('goog.net.IframeIo');
goog.require('goog.testing.events');
goog.require('goog.testing.events.Event');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');
// MANUAL TESTS - The tests should be run in the browser from the Closure Test
// Server
// Set up a logger to track responses
goog.debug.LogManager.getRoot().setLevel(goog.log.Level.INFO);
var logconsole;
var testLogger = goog.log.getLogger('test');
function setUpPage() {
var logconsole = new goog.debug.DivConsole(document.getElementById('log'));
logconsole.setCapturing(true);
}
/** Creates an iframeIo instance and sets up the test environment */
function getTestIframeIo() {
logconsole.addSeparator();
logconsole.getFormatter().resetRelativeTimeStart();
var io = new goog.net.IframeIo();
io.setErrorChecker(checkForError);
goog.events.listen(io, 'success', onSuccess);
goog.events.listen(io, 'error', onError);
goog.events.listen(io, 'ready', onReady);
return io;
}
/**
* Checks for error strings returned by the GSE and error variables that
* the Gmail server and GFE set on certain errors.
*/
function checkForError(doc) {
var win = goog.dom.getWindow(doc);
var text = doc.body.textContent || doc.body.innerText || '';
var gseError = text.match(/([^\n]+)\nError ([0-9]{3})/);
if (gseError) {
return '(Error ' + gseError[2] + ') ' + gseError[1];
} else if (win.gmail_error) {
return win.gmail_error + 700;
} else if (win.rc) {
return 600 + win.rc % 100;
} else {
return null;
}
}
/** Logs the status of an iframeIo object */
function logStatus(i) {
goog.log.fine(testLogger, 'Is complete/success/active: ' +
[i.isComplete(), i.isSuccess(), i.isActive()].join('/'));
}
function onSuccess(e) {
goog.log.warning(testLogger, 'Request Succeeded');
logStatus(e.target);
}
function onError(e) {
goog.log.warning(testLogger, 'Request Errored: ' + e.target.getLastError());
logStatus(e.target);
}
function onReady(e) {
goog.log.info(testLogger,
'Test finished and iframe ready, disposing test object');
e.target.dispose();
}
function simpleGet() {
var io = getTestIframeIo();
goog.events.listen(io, 'complete', onSimpleTestComplete);
io.send('/iframeio/ping', 'GET');
}
function simplePost() {
var io = getTestIframeIo();
goog.events.listen(io, 'complete', onSimpleTestComplete);
io.send('/iframeio/ping', 'POST');
}
function onSimpleTestComplete(e) {
goog.log.info(testLogger, 'ResponseText: ' + e.target.getResponseText());
}
function abort() {
var io = getTestIframeIo();
goog.events.listen(io, 'complete', onAbortComplete);
goog.events.listen(io, 'abort', onAbort);
io.send('/iframeio/ping', 'GET');
io.abort();
}
function onAbortComplete(e) {
goog.log.info(testLogger, 'Hmm, request should have been aborted');
}
function onAbort(e) {
goog.log.info(testLogger, 'Request aborted');
}
function errorGse404() {
var io = getTestIframeIo();
io.send('/iframeio/404', 'GET');
}
function jsonEcho(method) {
var io = getTestIframeIo();
goog.events.listen(io, 'complete', onJsonComplete);
var data = {'p1': 'x', 'p2': 'y', 'p3': 'z', 'r': 10};
io.send('/iframeio/jsonecho?q1=a&q2=b&q3=c&r=5', method, false, data);
}
function onJsonComplete(e) {
goog.log.info(testLogger, 'ResponseText: ' + e.target.getResponseText());
var json = e.target.getResponseJson();
goog.log.info(testLogger,
'ResponseJson:\n' + goog.debug.deepExpose(json, true));
}
function sendFromForm() {
var io = getTestIframeIo();
goog.events.listen(io, 'success', onUploadSuccess);
goog.events.listen(io, 'error', onUploadError);
io.sendFromForm(document.getElementById('uploadform'));
}
function onUploadSuccess(e) {
goog.log.log(testLogger, goog.log.Level.SHOUT, 'Upload Succeeded');
goog.log.info(testLogger, 'ResponseText: ' + e.target.getResponseText());
}
function onUploadError(e) {
goog.log.log(testLogger, goog.log.Level.SHOUT, 'Upload Errored');
goog.log.info(testLogger, 'ResponseText: ' + e.target.getResponseText());
}
function redirect1() {
var io = getTestIframeIo();
io.send('/iframeio/redirect', 'GET');
}
function redirect2() {
var io = getTestIframeIo();
io.send('/iframeio/move', 'GET');
}
function badUrl() {
var io = getTestIframeIo();
io.send('http://news.bbc.co.uk', 'GET');
}
function localUrl1() {
var io = getTestIframeIo();
goog.events.listen(io, 'complete', onLocalSuccess);
io.send('c:\test.txt', 'GET');
}
function localUrl2() {
var io = getTestIframeIo();
goog.events.listen(io, 'success', onLocalSuccess);
io.send('//test.txt', 'GET');
}
function onLocalSuccess(e) {
goog.log.info(testLogger,
'The file was found:\n' + e.target.getResponseText());
}
function getServerTime(noCache) {
var io = getTestIframeIo();
goog.events.listen(io, 'success', onTestCacheSuccess);
io.send('/iframeio/datetime', 'GET', noCache);
}
function onTestCacheSuccess(e) {
goog.log.info(testLogger, 'Date reported: ' + e.target.getResponseText());
}
function errorGmail() {
var io = getTestIframeIo();
goog.events.listen(io, 'error', onGmailError);
io.send('/iframeio/gmailerror', 'GET');
}
function onGmailError(e) {
goog.log.info(testLogger, 'Gmail error: ' + e.target.getLastError());
}
function errorGfe() {
var io = getTestIframeIo();
goog.events.listen(io, 'error', onGfeError);
io.send('/iframeio/gfeerror', 'GET');
}
function onGfeError(e) {
goog.log.info(testLogger, 'GFE error: ' + e.target.getLastError());
}
function incremental() {
var io = getTestIframeIo();
io.send('/iframeio/incremental', 'GET');
}
window['P'] = function(iframe, data) {
var iframeIo = goog.net.IframeIo.getInstanceByName(iframe.name);
goog.log.info(testLogger, 'Data recieved - ' + data);
};
function postForm() {
var io = getTestIframeIo();
goog.events.listen(io, 'complete', onJsonComplete);
io.sendFromForm(document.getElementById('testfrm'));
}
// UNIT TESTS - to be run via the JsUnit testRunner
// TODO(user): How to unit test all of this? Creating a MockIframe could
// help for the IE code path, but since the other browsers require weird
// behaviors this becomes very tricky.
function testGetForm() {
var frm1 = goog.net.IframeIo.getForm_;
var frm2 = goog.net.IframeIo.getForm_;
assertEquals(frm1, frm2);
}
function testAddFormInputs() {
var form = document.createElement(goog.dom.TagName.FORM);
goog.net.IframeIo.addFormInputs_(form, {'a': 1, 'b': 2, 'c': 3});
var inputs = form.getElementsByTagName(goog.dom.TagName.INPUT);
assertEquals(3, inputs.length);
for (var i = 0; i < inputs.length; i++) {
assertEquals('hidden', inputs[i].type);
var n = inputs[i].name;
assertEquals(n == 'a' ? '1' : n == 'b' ? '2' : '3', inputs[i].value);
}
}
function testNotIgnoringResponse() {
// This test can't run in IE because we can't forge the check for
// iframe.readyState = 'complete'.
if (goog.userAgent.IE) {
return;
}
var iframeIo = new goog.net.IframeIo();
iframeIo.send('about:blank');
// Simulate the frame finishing loading.
goog.testing.events.fireBrowserEvent(new goog.testing.events.Event(
goog.events.EventType.LOAD, iframeIo.getRequestIframe()));
assertTrue(iframeIo.isComplete());
}
function testIgnoreResponse() {
var iframeIo = new goog.net.IframeIo();
iframeIo.setIgnoreResponse(true);
iframeIo.send('about:blank');
// Simulate the frame finishing loading.
goog.testing.events.fireBrowserEvent(new goog.testing.events.Event(
goog.events.EventType.LOAD, iframeIo.getRequestIframe()));
// Although the request is complete, the IframeIo isn't paying attention.
assertFalse(iframeIo.isComplete());
}
@@ -0,0 +1,204 @@
// 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 a same-domain 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
* @final
*/
goog.net.IframeLoadMonitor = function(iframe, opt_hasContent) {
goog.net.IframeLoadMonitor.base(this, 'constructor');
/**
* 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 versions before IE11 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.
if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('11')) {
isLoaded = this.iframe_.readyState == 'complete';
} else {
isLoaded = !!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,26 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.IframeLoadMonitor
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.IframeLoadMonitorTest');
</script>
</head>
<body>
<div id="frame_parent">
</div>
</body>
</html>
@@ -0,0 +1,106 @@
// 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.
goog.provide('goog.net.IframeLoadMonitorTest');
goog.setTestOnly('goog.net.IframeLoadMonitorTest');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.net.IframeLoadMonitor');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
var TEST_FRAME_SRCS = ['iframeloadmonitor_test_frame.html',
'iframeloadmonitor_test_frame2.html',
'iframeloadmonitor_test_frame3.html'];
// Create a new test case.
var iframeLoaderTestCase = new goog.testing.AsyncTestCase(document.title);
iframeLoaderTestCase.stepTimeout = 4 * 1000;
// Array holding all iframe load monitors.
iframeLoaderTestCase.iframeLoadMonitors_ = [];
// How many single frames finished loading
iframeLoaderTestCase.singleComplete_ = 0;
/**
* Sets up the test environment, adds tests and sets up the worker pools.
* @this {goog.testing.AsyncTestCase}
*/
iframeLoaderTestCase.setUpPage = function() {
this.log('Setting tests up');
iframeLoaderTestCase.waitForAsync('loading iframes');
var dom = goog.dom.getDomHelper();
// Load single frame
var frame = dom.createDom(goog.dom.TagName.IFRAME);
this.iframeLoadMonitors_.push(new goog.net.IframeLoadMonitor(frame));
goog.events.listen(this.iframeLoadMonitors_[0],
goog.net.IframeLoadMonitor.LOAD_EVENT, this);
var frameParent = dom.getElement('frame_parent');
dom.appendChild(frameParent, frame);
this.log('Loading frame at: ' + TEST_FRAME_SRCS[0]);
frame.src = TEST_FRAME_SRCS[0];
// Load single frame with content check
var frame1 = dom.createDom(goog.dom.TagName.IFRAME);
this.iframeLoadMonitors_.push(new goog.net.IframeLoadMonitor(frame1, true));
goog.events.listen(this.iframeLoadMonitors_[1],
goog.net.IframeLoadMonitor.LOAD_EVENT, this);
var frameParent = dom.getElement('frame_parent');
dom.appendChild(frameParent, frame1);
this.log('Loading frame with content check at: ' + TEST_FRAME_SRCS[0]);
frame1.src = TEST_FRAME_SRCS[0];
};
/**
* Handles any events fired
* @this {goog.testing.AsyncTestCase}
*/
iframeLoaderTestCase.handleEvent = function(e) {
this.log('handleEvent, type: ' + e.type);
if (e.type == goog.net.IframeLoadMonitor.LOAD_EVENT) {
this.singleComplete_++;
this.callbacksComplete();
}
};
/**
* Checks if all the load callbacks are done
* @this {goog.testing.AsyncTestCase}
*/
iframeLoaderTestCase.callbacksComplete = function() {
if (this.singleComplete_ == 2) {
iframeLoaderTestCase.continueTesting();
}
};
/** Tests the results. */
iframeLoaderTestCase.addNewTest('testResults', function() {
this.log('getting test results');
for (var i = 0; i < this.iframeLoadMonitors_.length; i++) {
assertTrue(this.iframeLoadMonitors_[i].isLoaded());
}
});
/** Standalone Closure Test Runner. */
G_testRunner.initialize(iframeLoaderTestCase);
@@ -0,0 +1,12 @@
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head><title>Iframe Load Test Frame 1</title></head>
<body>
Iframe Load Test Frame 1
</body>
</html>
@@ -0,0 +1,12 @@
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head><title>Iframe Load Test Frame 2</title></head>
<body>
Iframe Load Test Frame 2
</body>
</html>
@@ -0,0 +1,12 @@
<html>
<!--
Copyright 2010 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head><title>Iframe Load Test Frame 3</title></head>
<body>
Iframe Load Test Frame 3
</body>
</html>
@@ -0,0 +1,338 @@
// 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)
*/
goog.provide('goog.net.ImageLoader');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
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}
* @final
*/
goog.net.ImageLoader = function(opt_parent) {
goog.events.EventTarget.call(this);
/**
* Map of image IDs to their request including 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<!goog.net.ImageLoader.ImageRequest_>}
* @private
*/
this.imageIdToRequestMap_ = {};
/**
* 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<!goog.net.ImageLoader>}
* @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);
/**
* The type of image request to dispatch, if this is a CORS-enabled image
* request. CORS-enabled images can be reused in canvas elements without them
* being tainted. The server hosting the image should include the appropriate
* CORS header.
* @see https://developer.mozilla.org/en-US/docs/HTML/CORS_Enabled_Image
* @enum {string}
*/
goog.net.ImageLoader.CorsRequestType = {
ANONYMOUS: 'anonymous',
USE_CREDENTIALS: 'use-credentials'
};
/**
* Describes a request for an image. This includes its URL and its CORS-request
* type, if any.
* @typedef {{
* src: string,
* corsRequestType: ?goog.net.ImageLoader.CorsRequestType
* }}
* @private
*/
goog.net.ImageLoader.ImageRequest_;
/**
* An array of event types to listen to on images. This is browser dependent.
*
* For IE 10 and below, Internet Explorer doesn't reliably raise LOAD events
* on images, so we must use READY_STATE_CHANGE. Since 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.
*
* In IE 11, onreadystatechange is removed and replaced with onload:
*
* http://msdn.microsoft.com/en-us/library/ie/ms536957(v=vs.85).aspx
* http://msdn.microsoft.com/en-us/library/ie/bg182625(v=vs.85).aspx
*
* @type {!Array<string>}
* @private
*/
goog.net.ImageLoader.IMAGE_LOAD_EVENTS_ = [
goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('11') ?
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).
* @param {!goog.net.ImageLoader.CorsRequestType=} opt_corsRequestType The type
* of CORS request to use, if any.
*/
goog.net.ImageLoader.prototype.addImage = function(
id, image, opt_corsRequestType) {
var src = goog.isString(image) ? image : image.src;
if (src) {
// For now, we just store the source URL for the image.
this.imageIdToRequestMap_[id] = {
src: src,
corsRequestType: goog.isDef(opt_corsRequestType) ?
opt_corsRequestType : null
};
}
};
/**
* 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.imageIdToRequestMap_[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.imageIdToRequestMap_)) {
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 imageIdToRequestMap = this.imageIdToRequestMap_;
goog.array.forEach(goog.object.getKeys(imageIdToRequestMap),
function(id) {
var imageRequest = imageIdToRequestMap[id];
if (imageRequest) {
delete imageIdToRequestMap[id];
this.loadImage_(imageRequest, 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 {!goog.net.ImageLoader.ImageRequest_} imageRequest The request data.
* @param {string} id The unique ID of the image to load.
* @private
*/
goog.net.ImageLoader.prototype.loadImage_ = function(imageRequest, 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(goog.dom.TagName.IMG);
} else {
image = new Image();
}
if (imageRequest.corsRequestType) {
image.crossOrigin = imageRequest.corsRequestType;
}
this.handler_.listen(image, goog.net.ImageLoader.IMAGE_LOAD_EVENTS_,
this.onNetworkEvent_);
this.imageIdToImageMap_[id] = image;
image.id = id;
image.src = imageRequest.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.imageIdToRequestMap_;
delete this.imageIdToImageMap_;
goog.dispose(this.handler_);
goog.net.ImageLoader.superClass_.disposeInternal.call(this);
};
@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: chrishenry@google.com (Chris Henry)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.ImageLoader
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.ImageLoaderTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,330 @@
// 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.
goog.provide('goog.net.ImageLoaderTest');
goog.setTestOnly('goog.net.ImageLoaderTest');
goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.dispose');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.net.EventType');
goog.require('goog.net.ImageLoader');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var asyncTestCase = goog.testing.AsyncTestCase.createAndInstall(document.title);
// Set the AsyncTestCase timeout to larger value to allow more time
// for images to load.
asyncTestCase.stepTimeout = 5000;
var TEST_EVENT_TYPES = [
goog.events.EventType.LOAD,
goog.net.EventType.COMPLETE,
goog.net.EventType.ERROR
];
/**
* Mapping from test image file name to:
* [expected width, expected height, expected event to be fired].
*/
var TEST_IMAGES = {
'imageloader_testimg1.gif': [20, 20, goog.events.EventType.LOAD],
'imageloader_testimg2.gif': [20, 20, goog.events.EventType.LOAD],
'imageloader_testimg3.gif': [32, 32, goog.events.EventType.LOAD],
'this-is-not-image-1.gif': [0, 0, goog.net.EventType.ERROR],
'this-is-not-image-2.gif': [0, 0, goog.net.EventType.ERROR]
};
var startTime;
var loader;
function setUp() {
startTime = goog.now();
loader = new goog.net.ImageLoader();
// Adds test images to the loader.
var i = 0;
for (var key in TEST_IMAGES) {
var imageId = 'img_' + i++;
loader.addImage(imageId, key);
}
}
function tearDown() {
goog.dispose(loader);
}
/**
* Tests loading image and disposing before loading completes.
*/
function testDisposeInTheMiddleOfLoadingWorks() {
goog.events.listen(loader, TEST_EVENT_TYPES,
goog.partial(handleDisposalImageLoaderEvent, loader));
// waitForAsync before starting loader just in case
// handleDisposalImageLoaderEvent is called from within loader.start
// (before we yield control). This may happen in IE7/IE8.
asyncTestCase.waitForAsync('Waiting for loader handler to fire.');
loader.start();
}
function handleDisposalImageLoaderEvent(loader, e) {
assertFalse('Handler is still invoked after loader is disposed.',
loader.isDisposed());
switch (e.type) {
case goog.net.EventType.COMPLETE:
fail('This test should never get COMPLETE event.');
return;
case goog.events.EventType.LOAD:
case goog.net.EventType.ERROR:
loader.dispose();
break;
}
// Make sure that handler is never called again after disposal before
// marking test as successful.
asyncTestCase.waitForAsync('Wait to ensure that COMPLETE is never fired');
goog.Timer.callOnce(function() {
asyncTestCase.continueTesting();
}, 500);
}
/**
* Tests loading of images until completion.
*/
function testLoadingUntilCompletion() {
var results = {};
goog.events.listen(loader, TEST_EVENT_TYPES,
function(e) {
switch (e.type) {
case goog.events.EventType.LOAD:
var image = e.target;
results[image.src.substring(image.src.lastIndexOf('/') + 1)] =
[image.naturalWidth, image.naturalHeight, e.type];
return;
case goog.net.EventType.ERROR:
var image = e.target;
results[image.src.substring(image.src.lastIndexOf('/') + 1)] =
[image.naturalWidth, image.naturalHeight, e.type];
return;
case goog.net.EventType.COMPLETE:
// Test completes successfully.
asyncTestCase.continueTesting();
assertImagesAreCorrect(results);
return;
}
});
// waitForAsync before starting loader just in case handleImageLoaderEvent
// is called from within loader.start (before we yield control).
// This may happen in IE7/IE8.
asyncTestCase.waitForAsync('Waiting for loader handler to fire.');
loader.start();
}
function assertImagesAreCorrect(results) {
assertEquals(
goog.object.getCount(TEST_IMAGES), goog.object.getCount(results));
goog.object.forEach(TEST_IMAGES, function(value, key) {
// Check if fires the COMPLETE event.
assertTrue('Image is not loaded completely.', key in results);
var image = results[key];
// Check image size.
assertEquals('Image width is not correct', value[0], image[0]);
assertEquals('Image length is not correct', value[1], image[1]);
// Check if fired the correct event.
assertEquals('Event *' + value[2] + '* must be fired', value[2], image[2]);
});
}
/**
* Overrides the loader's loadImage_ method so that it dispatches an image
* loaded event immediately, causing any event listners to receive them
* synchronously. This allows tests to assume synchronous execution.
*/
function makeLoaderSynchronous(loader) {
var originalLoadImage = loader.loadImage_;
loader.loadImage_ = function(request, id) {
originalLoadImage.call(this, request, id);
var event = new goog.events.Event(goog.events.EventType.LOAD);
event.currentTarget = this.imageIdToImageMap_[id];
loader.onNetworkEvent_(event);
};
// Make listen() a no-op.
loader.handler_.listen = goog.nullFunction;
}
/**
* Verifies that if an additional image is added after start() was called, but
* before COMPLETE was dispatched, no COMPLETE event is sent. Verifies COMPLETE
* is finally sent when .start() is called again and all images have now
* completed loading.
*/
function testImagesAddedAfterStart() {
// Use synchronous image loading.
makeLoaderSynchronous(loader);
// Add another image once the first images finishes loading.
goog.events.listenOnce(loader, goog.events.EventType.LOAD, function() {
loader.addImage('extra_image', 'extra_image.gif');
});
// Keep track of the total # of image loads.
var loadRecordFn = goog.testing.recordFunction();
goog.events.listen(loader, goog.events.EventType.LOAD, loadRecordFn);
// Keep track of how many times COMPLETE was dispatched.
var completeRecordFn = goog.testing.recordFunction();
goog.events.listen(loader, goog.net.EventType.COMPLETE, completeRecordFn);
// Start testing.
loader.start();
assertEquals(
'COMPLETE event should not have been dispatched yet: An image was ' +
'added after the initial batch was started.',
0, completeRecordFn.getCallCount());
assertEquals('Just the test images should have loaded',
goog.object.getCount(TEST_IMAGES), loadRecordFn.getCallCount());
loader.start();
assertEquals('COMPLETE should have been dispatched once.',
1, completeRecordFn.getCallCount());
assertEquals('All images should have been loaded',
goog.object.getCount(TEST_IMAGES) + 1, loadRecordFn.getCallCount());
}
/**
* Verifies that more images can be added after an upload starts, and start()
* can be called for them, resulting in just one COMPLETE event once all the
* images have completed.
*/
function testImagesAddedAndStartedAfterStart() {
// Use synchronous image loading.
makeLoaderSynchronous(loader);
// Keep track of the total # of image loads.
var loadRecordFn = goog.testing.recordFunction();
goog.events.listen(loader, goog.events.EventType.LOAD, loadRecordFn);
// Add more images once the first images finishes loading, and call start()
// to get them going.
goog.events.listenOnce(loader, goog.events.EventType.LOAD, function(e) {
loader.addImage('extra_image', 'extra_image.gif');
loader.addImage('extra_image2', 'extra_image2.gif');
loader.start();
});
// Keep track of how many times COMPLETE was dispatched.
var completeRecordFn = goog.testing.recordFunction();
goog.events.listen(loader, goog.net.EventType.COMPLETE, completeRecordFn);
// Start testing. Make sure all 7 images loaded.
loader.start();
assertEquals('COMPLETE should have been dispatched once.',
1, completeRecordFn.getCallCount());
assertEquals('All images should have been loaded',
goog.object.getCount(TEST_IMAGES) + 2, loadRecordFn.getCallCount());
}
/**
* Verifies that if images are removed after loading has started, COMPLETE
* is dispatched once the remaining images have finished.
*/
function testImagesRemovedAfterStart() {
// Use synchronous image loading.
makeLoaderSynchronous(loader);
// Remove 2 images once the first image finishes loading.
goog.events.listenOnce(loader, goog.events.EventType.LOAD, function(e) {
loader.removeImage(
goog.array.peek(goog.object.getKeys(this.imageIdToRequestMap_)));
loader.removeImage(
goog.array.peek(goog.object.getKeys(this.imageIdToRequestMap_)));
});
// Keep track of the total # of image loads.
var loadRecordFn = goog.testing.recordFunction();
goog.events.listen(loader, goog.events.EventType.LOAD, loadRecordFn);
// Keep track of how many times COMPLETE was dispatched.
var completeRecordFn = goog.testing.recordFunction();
goog.events.listen(loader, goog.net.EventType.COMPLETE, completeRecordFn);
// Start testing. Make sure only the 3 images remaining loaded.
loader.start();
assertEquals('COMPLETE should have been dispatched once.',
1, completeRecordFn.getCallCount());
assertEquals('All images should have been loaded',
goog.object.getCount(TEST_IMAGES) - 2, loadRecordFn.getCallCount());
}
/**
* Verifies that the correct image attribute is set when using CORS requests.
*/
function testSetsCorsAttribute() {
// Use synchronous image loading.
makeLoaderSynchronous(loader);
// Verify the crossOrigin attribute of the requested images.
goog.events.listen(loader, goog.events.EventType.LOAD, function(e) {
var image = e.target;
if (image.id == 'cors_request') {
assertEquals(
'CORS requested image should have a crossOrigin attribute set',
'anonymous', image.crossOrigin);
} else {
assertTrue(
'Non-CORS requested images should not have a crossOrigin attribute',
goog.string.isEmptyOrWhitespace(goog.string.makeSafe(image.crossOrigin)));
}
});
// Make a new request for one of the images, this time using CORS.
var srcs = goog.object.getKeys(TEST_IMAGES);
loader.addImage(
'cors_request', srcs[0], goog.net.ImageLoader.CorsRequestType.ANONYMOUS);
loader.start();
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,509 @@
// 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;
};
/**
* @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
* @final
*/
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.net.Ipv4Address.base(
this, 'constructor', /** @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}
* @final
*/
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.net.Ipv6Address.base(
this, 'constructor', /** @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,26 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
Test suite inspired from http://code.google.com/p/ipaddr-py/ and
Google's Guava InetAddresses test suite available on
http://code.google.com/p/guava-libraries/
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Test - goog.net.IpAddress
</title>
<script src="../base.js" type="text/javascript">
</script>
<script type="text/javascript">
goog.require('goog.net.IpAddressTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,220 @@
// 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.
goog.provide('goog.net.IpAddressTest');
goog.setTestOnly('goog.net.IpAddressTest');
goog.require('goog.math.Integer');
goog.require('goog.net.IpAddress');
goog.require('goog.net.Ipv4Address');
goog.require('goog.net.Ipv6Address');
goog.require('goog.testing.jsunit');
function testInvalidStrings() {
assertEquals(null, goog.net.IpAddress.fromString(''));
assertEquals(null, goog.net.IpAddress.fromString('016.016.016.016'));
assertEquals(null, goog.net.IpAddress.fromString('016.016.016'));
assertEquals(null, goog.net.IpAddress.fromString('016.016'));
assertEquals(null, goog.net.IpAddress.fromString('016'));
assertEquals(null, goog.net.IpAddress.fromString('000.000.000.000'));
assertEquals(null, goog.net.IpAddress.fromString('000'));
assertEquals(null,
goog.net.IpAddress.fromString('0x0a.0x0a.0x0a.0x0a'));
assertEquals(null, goog.net.IpAddress.fromString('0x0a.0x0a.0x0a'));
assertEquals(null, goog.net.IpAddress.fromString('0x0a.0x0a'));
assertEquals(null, goog.net.IpAddress.fromString('0x0a'));
assertEquals(null, goog.net.IpAddress.fromString('42.42.42.42.42'));
assertEquals(null, goog.net.IpAddress.fromString('42.42.42'));
assertEquals(null, goog.net.IpAddress.fromString('42.42'));
assertEquals(null, goog.net.IpAddress.fromString('42'));
assertEquals(null, goog.net.IpAddress.fromString('42..42.42'));
assertEquals(null, goog.net.IpAddress.fromString('42..42.42.42'));
assertEquals(null, goog.net.IpAddress.fromString('42.42.42.42.'));
assertEquals(null, goog.net.IpAddress.fromString('42.42.42.42...'));
assertEquals(null, goog.net.IpAddress.fromString('.42.42.42.42'));
assertEquals(null, goog.net.IpAddress.fromString('...42.42.42.42'));
assertEquals(null, goog.net.IpAddress.fromString('42.42.42.-0'));
assertEquals(null, goog.net.IpAddress.fromString('42.42.42.+0'));
assertEquals(null, goog.net.IpAddress.fromString('.'));
assertEquals(null, goog.net.IpAddress.fromString('...'));
assertEquals(null, goog.net.IpAddress.fromString('bogus'));
assertEquals(null, goog.net.IpAddress.fromString('bogus.com'));
assertEquals(null, goog.net.IpAddress.fromString('192.168.0.1.com'));
assertEquals(null,
goog.net.IpAddress.fromString('12345.67899.-54321.-98765'));
assertEquals(null, goog.net.IpAddress.fromString('257.0.0.0'));
assertEquals(null, goog.net.IpAddress.fromString('42.42.42.-42'));
assertEquals(null, goog.net.IpAddress.fromString('3ff3:::1'));
assertEquals(null, goog.net.IpAddress.fromString('3ffe::1.net'));
assertEquals(null, goog.net.IpAddress.fromString('3ffe::1::1'));
assertEquals(null, goog.net.IpAddress.fromString('1::2::3::4:5'));
assertEquals(null, goog.net.IpAddress.fromString('::7:6:5:4:3:2:'));
assertEquals(null, goog.net.IpAddress.fromString(':6:5:4:3:2:1::'));
assertEquals(null, goog.net.IpAddress.fromString('2001::db:::1'));
assertEquals(null, goog.net.IpAddress.fromString('FEDC:9878'));
assertEquals(null, goog.net.IpAddress.fromString('+1.+2.+3.4'));
assertEquals(null, goog.net.IpAddress.fromString('1.2.3.4e0'));
assertEquals(null, goog.net.IpAddress.fromString('::7:6:5:4:3:2:1:0'));
assertEquals(null, goog.net.IpAddress.fromString('7:6:5:4:3:2:1:0::'));
assertEquals(null, goog.net.IpAddress.fromString('9:8:7:6:5:4:3::2:1'));
assertEquals(null, goog.net.IpAddress.fromString('0:1:2:3::4:5:6:7'));
assertEquals(null,
goog.net.IpAddress.fromString('3ffe:0:0:0:0:0:0:0:1'));
assertEquals(null, goog.net.IpAddress.fromString('3ffe::10000'));
assertEquals(null, goog.net.IpAddress.fromString('3ffe::goog'));
assertEquals(null, goog.net.IpAddress.fromString('3ffe::-0'));
assertEquals(null, goog.net.IpAddress.fromString('3ffe::+0'));
assertEquals(null, goog.net.IpAddress.fromString('3ffe::-1'));
assertEquals(null, goog.net.IpAddress.fromString(':'));
assertEquals(null, goog.net.IpAddress.fromString(':::'));
assertEquals(null, goog.net.IpAddress.fromString('a:'));
assertEquals(null, goog.net.IpAddress.fromString('::a:'));
assertEquals(null, goog.net.IpAddress.fromString('0xa::'));
assertEquals(null, goog.net.IpAddress.fromString('::1.2.3'));
assertEquals(null, goog.net.IpAddress.fromString('::1.2.3.4.5'));
assertEquals(null, goog.net.IpAddress.fromString('::1.2.3.4:'));
assertEquals(null, goog.net.IpAddress.fromString('1.2.3.4::'));
assertEquals(null, goog.net.IpAddress.fromString('2001:db8::1:'));
assertEquals(null, goog.net.IpAddress.fromString(':2001:db8::1'));
}
function testVersion() {
var ip4 = goog.net.IpAddress.fromString('1.2.3.4');
assertEquals(ip4.getVersion(), 4);
var ip6 = goog.net.IpAddress.fromString('2001:dead::beef:1');
assertEquals(ip6.getVersion(), 6);
ip6 = goog.net.IpAddress.fromString('::192.168.1.1');
assertEquals(ip6.getVersion(), 6);
}
function testStringIpv4Address() {
assertEquals('192.168.1.1',
new goog.net.Ipv4Address('192.168.1.1').toString());
assertEquals('1.1.1.1',
new goog.net.Ipv4Address('1.1.1.1').toString());
assertEquals('224.56.33.2',
new goog.net.Ipv4Address('224.56.33.2').toString());
assertEquals('255.255.255.255',
new goog.net.Ipv4Address('255.255.255.255').toString());
assertEquals('0.0.0.0',
new goog.net.Ipv4Address('0.0.0.0').toString());
}
function testIntIpv4Address() {
var ip4Str = new goog.net.Ipv4Address('1.1.1.1');
var ip4Int = new goog.net.Ipv4Address(
new goog.math.Integer([16843009], 0));
assertTrue(ip4Str.equals(ip4Int));
assertEquals(ip4Str.toString(), ip4Int.toString());
assertThrows('Ipv4(-1)', goog.partial(goog.net.Ipv4Address,
goog.math.Integer.fromInt(-1)));
assertThrows('Ipv4(2**32)',
goog.partial(goog.net.Ipv4Address,
goog.math.Integer.ONE.shiftLeft(32)));
}
function testStringIpv6Address() {
assertEquals('1:2:3:4:5:6:7:8',
new goog.net.Ipv6Address('1:2:3:4:5:6:7:8').toString());
assertEquals('::1:2:3:4:5:6:7',
new goog.net.Ipv6Address('::1:2:3:4:5:6:7').toString());
assertEquals('1:2:3:4:5:6:7::',
new goog.net.Ipv6Address('1:2:3:4:5:6:7:0').toString());
assertEquals('2001:0:0:4::8',
new goog.net.Ipv6Address('2001:0:0:4:0:0:0:8').toString());
assertEquals('2001::4:5:6:7:8',
new goog.net.Ipv6Address('2001:0:0:4:5:6:7:8').toString());
assertEquals('2001::3:4:5:6:7:8',
new goog.net.Ipv6Address('2001:0:3:4:5:6:7:8').toString());
assertEquals('0:0:3::ffff',
new goog.net.Ipv6Address('0:0:3:0:0:0:0:ffff').toString());
assertEquals('::4:0:0:0:ffff',
new goog.net.Ipv6Address('0:0:0:4:0:0:0:ffff').toString());
assertEquals('::5:0:0:ffff',
new goog.net.Ipv6Address('0:0:0:0:5:0:0:ffff').toString());
assertEquals('1::4:0:0:7:8',
new goog.net.Ipv6Address('1:0:0:4:0:0:7:8').toString());
assertEquals('::',
new goog.net.Ipv6Address('0:0:0:0:0:0:0:0').toString());
assertEquals('::1',
new goog.net.Ipv6Address('0:0:0:0:0:0:0:1').toString());
assertEquals('2001:658:22a:cafe::',
new goog.net.Ipv6Address(
'2001:0658:022a:cafe:0000:0000:0000:0000').toString());
assertEquals('::102:304',
new goog.net.Ipv6Address('::1.2.3.4').toString());
assertEquals('::ffff:303:303',
new goog.net.Ipv6Address('::ffff:3.3.3.3').toString());
assertEquals('::ffff:ffff',
new goog.net.Ipv6Address('::255.255.255.255').toString());
}
function testIntIpv6Address() {
var ip6Str = new goog.net.Ipv6Address('2001::dead:beef:1');
var ip6Int = new goog.net.Ipv6Address(
new goog.math.Integer([3203334145, 57005, 0, 536936448], 0));
assertTrue(ip6Str.equals(ip6Int));
assertEquals(ip6Str.toString(), ip6Int.toString());
assertThrows('Ipv6(-1)', goog.partial(goog.net.Ipv6Address,
goog.math.Integer.fromInt(-1)));
assertThrows('Ipv6(2**128)',
goog.partial(goog.net.Ipv6Address,
goog.math.Integer.ONE.shiftLeft(128)));
}
function testDottedQuadIpv6() {
var ip6 = new goog.net.Ipv6Address('7::0.128.0.127');
ip6 = new goog.net.Ipv6Address('7::0.128.0.128');
ip6 = new goog.net.Ipv6Address('7::128.128.0.127');
ip6 = new goog.net.Ipv6Address('7::0.128.128.127');
}
function testMappedIpv4Address() {
var testAddresses = ['::ffff:1.2.3.4', '::FFFF:102:304'];
var ipv4Str = '1.2.3.4';
var ip1 = new goog.net.Ipv6Address(testAddresses[0]);
var ip2 = new goog.net.Ipv6Address(testAddresses[1]);
var ipv4 = new goog.net.Ipv4Address(ipv4Str);
assertTrue(ip1.isMappedIpv4Address());
assertTrue(ip2.isMappedIpv4Address());
assertTrue(ip1.equals(ip2));
assertTrue(ipv4.equals(ip1.getMappedIpv4Address()));
assertTrue(ipv4.equals(ip2.getMappedIpv4Address()));
}
function testUriString() {
var ip4Str = '192.168.1.1';
var ip4Uri = goog.net.IpAddress.fromUriString(ip4Str);
var ip4 = goog.net.IpAddress.fromString(ip4Str);
assertTrue(ip4Uri.equals(ip4));
var ip6Str = '2001:dead::beef:1';
assertEquals(null, goog.net.IpAddress.fromUriString(ip6Str));
var ip6Uri = goog.net.IpAddress.fromUriString('[' + ip6Str + ']');
var ip6 = goog.net.IpAddress.fromString(ip6Str);
assertTrue(ip6Uri.equals(ip6));
assertEquals(ip6Uri.toString(), ip6Str);
assertEquals(ip6Uri.toUriString(), '[' + ip6Str + ']');
}
@@ -0,0 +1,371 @@
// 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');
goog.require('goog.object');
/**
* 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.
* attributes: Additional attributes to set on the script tag.
*
* @typedef {{
* timeout: (number|undefined),
* document: (HTMLDocument|undefined),
* cleanupWhenDone: (boolean|undefined),
* attributes: (!Object<string, string>|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));
};
var properties = options.attributes || {};
goog.object.extend(properties, {
'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
});
goog.dom.setProperties(script, properties);
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 == goog.dom.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}
* @final
*/
goog.net.jsloader.Error = function(code, opt_message) {
var msg = 'Jsloader error (code #' + code + ')';
if (opt_message) {
msg += ': ' + opt_message;
}
goog.net.jsloader.Error.base(this, 'constructor', 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,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.jsloader
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.jsloaderTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,170 @@
// 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.
goog.provide('goog.net.jsloaderTest');
goog.setTestOnly('goog.net.jsloaderTest');
goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.net.jsloader');
goog.require('goog.net.jsloader.ErrorCode');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
// Initialize the AsyncTestCase.
var testCase = goog.testing.AsyncTestCase.createAndInstall(document.title);
testCase.stepTimeout = 5 * 1000; // 5 seconds
testCase.setUp = function() {
goog.provide = goog.nullFunction;
};
testCase.tearDown = function() {
// Remove all the fake scripts.
var scripts = goog.array.clone(
document.getElementsByTagName(goog.dom.TagName.SCRIPT));
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].src.indexOf('testdata') != -1) {
goog.dom.removeNode(scripts[i]);
}
}
};
// Sunny day scenario for load function.
function testLoad() {
testCase.waitForAsync('testLoad');
window.test1 = null;
var testUrl = 'testdata/jsloader_test1.js';
var result = goog.net.jsloader.load(testUrl);
result.addCallback(function() {
testCase.continueTesting();
var script = result.defaultScope_.script_;
assertNotNull('script created', script);
assertEquals('encoding is utf-8', 'UTF-8', script.charset);
// Check that the URI matches ours.
assertTrue('server URI', script.src.indexOf(testUrl) >= 0);
// Check that the script was really loaded.
assertEquals('verification object', 'Test #1 loaded', window.test1);
});
}
// Sunny day scenario for loadAndVerify function.
function testLoadAndVerify() {
testCase.waitForAsync('testLoadAndVerify');
var testUrl = 'testdata/jsloader_test2.js';
var result = goog.net.jsloader.loadAndVerify(testUrl, 'test2');
result.addCallback(function(verifyObj) {
testCase.continueTesting();
// Check that the verification object has passed ok.
assertEquals('verification object', 'Test #2 loaded', verifyObj);
});
}
// What happens when the verification object is not set by the loaded script?
function testLoadAndVerifyError() {
testCase.waitForAsync('testLoadAndVerifyError');
var testUrl = 'testdata/jsloader_test2.js';
var result = goog.net.jsloader.loadAndVerify(testUrl, 'fake');
result.addErrback(function(error) {
testCase.continueTesting();
// Check that the error code is right.
assertEquals('verification error', goog.net.jsloader.ErrorCode.VERIFY_ERROR,
error.code);
});
}
// Tests that callers can cancel the deferred without error.
function testLoadAndVerifyCancelled() {
var testUrl = 'testdata/jsloader_test2.js';
var result = goog.net.jsloader.loadAndVerify(testUrl, 'test2');
result.cancel();
}
// Test the loadMany function.
function testLoadMany() {
testCase.waitForAsync('testLoadMany');
// Load test #3 and then #1.
window.test1 = null;
var testUrls1 = ['testdata/jsloader_test3.js', 'testdata/jsloader_test1.js'];
goog.net.jsloader.loadMany(testUrls1);
window.test3Callback = function(msg) {
testCase.continueTesting();
// Check that the 1st test was not loaded yet.
assertEquals('verification object', null, window.test1);
// Load test #4, which is supposed to wait for #1 to load.
testCase.waitForAsync('testLoadMany');
var testUrls2 = ['testdata/jsloader_test4.js'];
goog.net.jsloader.loadMany(testUrls2);
};
window.test4Callback = function(msg) {
testCase.continueTesting();
// Check that the 1st test was already loaded.
assertEquals('verification object', 'Test #1 loaded', window.test1);
};
}
// Test the load function with additional options.
function testLoadWithOptions() {
testCase.waitForAsync('testLoadWithOptions');
var testUrl = 'testdata/jsloader_test1.js';
var options = {
attributes: {
'data-attr1' : 'enabled',
'data-attr2' : 'disabled',
},
timeout: undefined, // Use default
cleanupWhenDone: undefined, // Use default
document: undefined // Use default
};
var result = goog.net.jsloader.load(testUrl, options);
result.addCallback(function() {
testCase.continueTesting();
var script = result.defaultScope_.script_;
// Check that the URI matches ours.
assertTrue('server URI', script.src.indexOf(testUrl) >= 0);
// Check that the attributes specified are set on the script tag.
assertEquals('attribute option not applied for attr1',
'enabled', script.getAttribute('data-attr1'));
assertEquals('attribute option not applied for attr2',
'disabled', script.getAttribute('data-attr2'));
});
}
@@ -0,0 +1,340 @@
// 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 workaround 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
* @final
*/
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.
*/
var handler = function(var_args) {
goog.net.Jsonp.cleanup_(id, true);
replyCallback.apply(undefined, arguments);
};
return handler;
};
/**
* 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,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2007 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.Jsonp
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.JsonpTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,321 @@
// 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.
goog.provide('goog.net.JsonpTest');
goog.setTestOnly('goog.net.JsonpTest');
goog.require('goog.net.Jsonp');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
goog.require('goog.userAgent');
// Global vars to facilitate a shared set up function.
var timeoutWasCalled;
var timeoutHandler;
var fakeUrl = 'http://fake-site.eek/';
var originalTimeout;
function setUp() {
timeoutWasCalled = false;
timeoutHandler = null;
originalTimeout = window.setTimeout;
window.setTimeout = function(handler, time) {
timeoutWasCalled = true;
timeoutHandler = handler;
};
}
// Firefox throws a JS error when a script is not found. We catch that here and
// ensure the test case doesn't fail because of it.
var originalOnError = window.onerror;
window.onerror = function(msg, url, line) {
// TODO(user): Safari 3 on the farm returns an object instead of the typcial
// params. Pass through errors for safari for now.
if (goog.userAgent.WEBKIT ||
msg == 'Error loading script' && url.indexOf('fake-site') != -1) {
return true;
} else {
return originalOnError && originalOnError(msg, url, line);
}
};
function tearDown() {
window.setTimeout = originalTimeout;
}
// Quick function records the before-state of the DOM, and then return a
// a function to check that XDC isn't leaving stuff behind.
function newCleanupGuard() {
var bodyChildCount = document.body.childNodes.length;
return function() {
// let any timeout queues finish before we check these:
window.setTimeout(function() {
var propCounter = 0;
// All callbacks should have been deleted or be the null function.
for (var id in goog.global[goog.net.Jsonp.CALLBACKS]) {
if (goog.global[goog.net.Jsonp.CALLBACKS][id] != goog.nullFunction) {
propCounter++;
}
}
assertEquals(
'script cleanup', bodyChildCount, document.body.childNodes.length);
assertEquals('window jsonp array empty', 0, propCounter);
}, 0);
}
}
function getScriptElement(result) {
return result.deferred_.defaultScope_.script_;
}
// Check that send function is sane when things go well.
function testSend() {
var replyReceived;
var jsonp = new goog.net.Jsonp(fakeUrl);
var checkCleanup = newCleanupGuard();
var userCallback = function(data) {
replyReceived = data;
};
var payload = {atisket: 'atasket', basket: 'yellow'};
var result = jsonp.send(payload, userCallback);
var script = getScriptElement(result);
assertNotNull('script created', script);
assertEquals('encoding is utf-8', 'UTF-8', script.charset);
// Check that the URL matches our payload.
assertTrue('payload in url', script.src.indexOf('basket=yellow') > -1);
assertTrue('server url', script.src.indexOf(fakeUrl) == 0);
// Now, we have to track down the name of the callback function, so we can
// call that to simulate a returned request + verify that the callback
// function does not break if it receives a second unexpected parameter.
var callbackName = /callback=([^&]+)/.exec(script.src)[1];
var callbackFunc = eval(callbackName);
callbackFunc({some: 'data', another: ['data', 'right', 'here']},
'unexpected');
assertEquals('input was received', 'right', replyReceived.another[1]);
// Because the callbackFunc calls cleanUp_ and that calls setTimeout which
// we have overwritten, we have to call the timeoutHandler to actually do
// the cleaning.
timeoutHandler();
checkCleanup();
timeoutHandler();
}
// Check that send function is sane when things go well.
function testSendWhenCallbackHasTwoParameters() {
var replyReceived, replyReceived2;
var jsonp = new goog.net.Jsonp(fakeUrl);
var checkCleanup = newCleanupGuard();
var userCallback = function(data, opt_data2) {
replyReceived = data;
replyReceived2 = opt_data2;
};
var payload = {atisket: 'atasket', basket: 'yellow'};
var result = jsonp.send(payload, userCallback);
var script = getScriptElement(result);
// Test a callback function that receives two parameters.
var callbackName = /callback=([^&]+)/.exec(script.src)[1];
var callbackFunc = eval(callbackName);
callbackFunc('param1', {some: 'data', another: ['data', 'right', 'here']});
assertEquals('input was received', 'param1', replyReceived);
assertEquals('second input was received', 'right',
replyReceived2.another[1]);
// Because the callbackFunc calls cleanUp_ and that calls setTimeout which
// we have overwritten, we have to call the timeoutHandler to actually do
// the cleaning.
timeoutHandler();
checkCleanup();
timeoutHandler();
}
// Check that send function works correctly when callback param value is
// specified.
function testSendWithCallbackParamValue() {
var replyReceived;
var jsonp = new goog.net.Jsonp(fakeUrl);
var checkCleanup = newCleanupGuard();
var userCallback = function(data) {
replyReceived = data;
};
var payload = {atisket: 'atasket', basket: 'yellow'};
var result = jsonp.send(payload, userCallback, undefined, 'dummyId');
var script = getScriptElement(result);
assertNotNull('script created', script);
assertEquals('encoding is utf-8', 'UTF-8', script.charset);
// Check that the URL matches our payload.
assertTrue('payload in url', script.src.indexOf('basket=yellow') > -1);
assertTrue('dummyId in url',
script.src.indexOf('callback=_callbacks_.dummyId') > -1);
assertTrue('server url', script.src.indexOf(fakeUrl) == 0);
// Now, we simulate a returned request using the known callback function
// name.
var callbackFunc = _callbacks_.dummyId;
callbackFunc({some: 'data', another: ['data', 'right', 'here']});
assertEquals('input was received', 'right', replyReceived.another[1]);
// Because the callbackFunc calls cleanUp_ and that calls setTimeout which
// we have overwritten, we have to call the timeoutHandler to actually do
// the cleaning.
timeoutHandler();
checkCleanup();
timeoutHandler();
}
// Check that the send function is sane when the thing goes south.
function testSendFailure() {
var replyReceived = false;
var errorReplyReceived = false;
var jsonp = new goog.net.Jsonp(fakeUrl);
var checkCleanup = newCleanupGuard();
var userCallback = function(data) {
replyReceived = data;
};
var userErrorCallback = function(data) {
errorReplyReceived = data;
};
var payload = { justa: 'test' };
jsonp.send(payload, userCallback, userErrorCallback);
assertTrue('timeout called', timeoutWasCalled);
// Now, simulate the time running out, so we go into error mode.
// After jsonp.send(), the timeoutHandler now is the Jsonp.cleanUp_ function.
timeoutHandler();
// But that function also calls a setTimeout(), so it changes the timeout
// handler once again, so to actually clean up we have to call the
// timeoutHandler() once again. Fun!
timeoutHandler();
assertFalse('standard callback not called', replyReceived);
// The user's error handler should be called back with the same payload
// passed back to it.
assertEquals('error handler called', 'test', errorReplyReceived.justa);
// Check that the relevant cleanup has occurred.
checkCleanup();
// Check cleanup just calls setTimeout so we have to call the handler to
// actually check that the cleanup worked.
timeoutHandler();
}
// Check that a cancel call works and cleans up after itself.
function testCancel() {
var checkCleanup = newCleanupGuard();
var successCalled = false;
var successCallback = function() {
successCalled = true;
};
// Send and cancel a request, then make sure it was cleaned up.
var jsonp = new goog.net.Jsonp(fakeUrl);
var requestObject = jsonp.send({test: 'foo'}, successCallback);
jsonp.cancel(requestObject);
for (var key in goog.global[goog.net.Jsonp.CALLBACKS]) {
assertNotEquals('The success callback should have been removed',
goog.global[goog.net.Jsonp.CALLBACKS][key],
successCallback);
}
// Make sure cancelling removes the script tag
checkCleanup();
timeoutHandler();
}
function testPayloadParameters() {
var checkCleanup = newCleanupGuard();
var jsonp = new goog.net.Jsonp(fakeUrl);
var result = jsonp.send({
'foo': 3,
'bar': 'baz'
});
var script = getScriptElement(result);
assertEquals('Payload parameters should have been added to url.',
fakeUrl + '?foo=3&bar=baz',
script.src);
checkCleanup();
timeoutHandler();
}
function testOptionalPayload() {
var checkCleanup = newCleanupGuard();
var errorCallback = goog.testing.recordFunction();
var stubs = new goog.testing.PropertyReplacer();
stubs.set(goog.global, 'setTimeout', function(errorHandler) {
errorHandler();
});
var jsonp = new goog.net.Jsonp(fakeUrl);
var result = jsonp.send(null, null, errorCallback);
var script = getScriptElement(result);
assertEquals('Parameters should not have been added to url.',
fakeUrl, script.src);
// Clear the script hooks because we triggered the error manually.
script.onload = goog.nullFunction;
script.onerror = goog.nullFunction;
script.onreadystatechange = goog.nullFunction;
var errorCallbackArguments = errorCallback.getLastCall().getArguments();
assertEquals(1, errorCallbackArguments.length);
assertNull(errorCallbackArguments[0]);
checkCleanup();
stubs.reset();
}
@@ -0,0 +1,308 @@
// 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}
* @final
*/
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;
/** @private {Function} */
goog.net.MockIFrameIo.prototype.errorChecker_;
/** @private {boolean} */
goog.net.MockIFrameIo.prototype.success_;
/** @private {boolean} */
goog.net.MockIFrameIo.prototype.complete_;
/**
* 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_;
};
@@ -0,0 +1,118 @@
// 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
* @final
*/
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,26 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.MultiIframeLoadMonitor
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.MultiIframeLoadMonitorTest');
</script>
</head>
<body>
<div id="frame_parent">
</div>
</body>
</html>
@@ -0,0 +1,164 @@
// 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.
goog.provide('goog.net.MultiIframeLoadMonitorTest');
goog.setTestOnly('goog.net.MultiIframeLoadMonitorTest');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.net.IframeLoadMonitor');
goog.require('goog.net.MultiIframeLoadMonitor');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.jsunit');
var TEST_FRAME_SRCS = ['iframeloadmonitor_test_frame.html',
'iframeloadmonitor_test_frame2.html',
'iframeloadmonitor_test_frame3.html'];
// Create a new test case.
var iframeLoaderTestCase = new goog.testing.AsyncTestCase(document.title);
iframeLoaderTestCase.stepTimeout = 4 * 1000;
// How many multpile frames finished loading
iframeLoaderTestCase.multipleComplete_ = 0;
iframeLoaderTestCase.numMonitors = 0;
iframeLoaderTestCase.disposeCalled = 0;
/**
* Sets up the test environment, adds tests and sets up the worker pools.
* @this {goog.testing.AsyncTestCase}
*/
iframeLoaderTestCase.setUpPage = function() {
this.log('Setting tests up');
iframeLoaderTestCase.waitForAsync('loading iframes');
var dom = goog.dom.getDomHelper();
// Load multiple with callback
var frame1 = dom.createDom(goog.dom.TagName.IFRAME);
var frame2 = dom.createDom(goog.dom.TagName.IFRAME);
var multiMonitor = new goog.net.MultiIframeLoadMonitor(
[frame1, frame2], goog.bind(this.multipleCallback, this));
this.log('Loading frames at: ' + TEST_FRAME_SRCS[0] + ' and ' +
TEST_FRAME_SRCS[1]);
// Make sure they don't look loaded yet.
assertEquals(0, this.multipleComplete_);
var frameParent = dom.getElement('frame_parent');
dom.appendChild(frameParent, frame1);
frame1.src = TEST_FRAME_SRCS[0];
dom.appendChild(frameParent, frame2);
frame2.src = TEST_FRAME_SRCS[1];
// Load multiple with callback and content check
var frame3 = dom.createDom(goog.dom.TagName.IFRAME);
var frame4 = dom.createDom(goog.dom.TagName.IFRAME);
var multiMonitor = new goog.net.MultiIframeLoadMonitor(
[frame3, frame4], goog.bind(this.multipleContentCallback, this), true);
this.log('Loading frames with content check at: ' + TEST_FRAME_SRCS[1] +
' and ' + TEST_FRAME_SRCS[2]);
dom.appendChild(frameParent, frame3);
frame3.src = TEST_FRAME_SRCS[1];
dom.appendChild(frameParent, frame4);
frame4.src = TEST_FRAME_SRCS[2];
};
/**
* Callback for the multiple frame load test case
* @this {goog.testing.AsyncTestCase}
*/
iframeLoaderTestCase.multipleCallback = function() {
this.log('multiple frames finished loading');
this.multipleComplete_++;
this.multipleCompleteNoContent_ = true;
this.callbacksComplete();
};
/**
* Callback for the multiple frame with content load test case
* @this {goog.testing.AsyncTestCase}
*/
iframeLoaderTestCase.multipleContentCallback = function() {
this.log('multiple frames with content finished loading');
this.multipleComplete_++;
this.multipleCompleteContent_ = true;
this.callbacksComplete();
};
/**
* Checks if all the load callbacks are done
* @this {goog.testing.AsyncTestCase}
*/
iframeLoaderTestCase.callbacksComplete = function() {
if (this.multipleComplete_ == 2) {
iframeLoaderTestCase.continueTesting();
}
};
/** Tests the results. */
iframeLoaderTestCase.addNewTest('testResults', function() {
this.log('getting test results');
assertTrue(this.multipleCompleteNoContent_);
assertTrue(this.multipleCompleteContent_);
});
iframeLoaderTestCase.fakeLoadMonitor = function() {
// Replaces IframeLoadMonitor with a fake version that just tracks
// instantiations/disposals
this.loadMonitorConstructor = goog.net.IframeLoadMonitor;
var that = this;
goog.net.IframeLoadMonitor = function() {
that.numMonitors++;
return {
isLoaded: function() { return false; },
dispose: function() { that.disposeCalled++; },
attachEvent: function() {}
};
};
goog.net.IframeLoadMonitor.LOAD_EVENT = 'ifload';
};
iframeLoaderTestCase.unfakeLoadMonitor = function() {
goog.net.IframeLoadMonitor = this.loadMonitorConstructor;
};
iframeLoaderTestCase.addNewTest('stopMonitoring', function() {
// create two unloaded frames, make sure that load monitors are loaded
// behind the scenes, then make sure they are disposed properly.
this.fakeLoadMonitor();
var dom = goog.dom.getDomHelper();
var frames = [dom.createDom(goog.dom.TagName.IFRAME),
dom.createDom(goog.dom.TagName.IFRAME)];
var multiMonitor = new goog.net.MultiIframeLoadMonitor(
frames,
function() {
fail('should not invoke callback for unloaded rames');
});
assertEquals(frames.length, this.numMonitors);
assertEquals(0, this.disposeCalled);
multiMonitor.stopMonitoring();
assertEquals(frames.length, this.disposeCalled);
this.unfakeLoadMonitor();
});
/** Standalone Closure Test Runner. */
G_testRunner.initialize(iframeLoaderTestCase);
@@ -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,397 @@
// 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 @struct
* @final
*/
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;
/** @private {?Image} */
goog.net.NetworkTester.prototype.image_;
/**
* 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_;
};
/**
* Returns the current attempt count.
* @return {number} The attempt count.
*/
goog.net.NetworkTester.prototype.getAttemptCount = function() {
return this.attempt_;
};
/**
* 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,22 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2006 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.NetworkTester
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.NetworkTesterTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,237 @@
// 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.
goog.provide('goog.net.NetworkTesterTest');
goog.setTestOnly('goog.net.NetworkTesterTest');
goog.require('goog.Uri');
goog.require('goog.net.NetworkTester');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
var clock;
function setUp() {
clock = new goog.testing.MockClock(true);
}
function tearDown() {
clock.dispose();
}
function testSuccess() {
// set up the tster
var handler = new Handler();
var tester = new goog.net.NetworkTester(handler.callback, handler);
assertFalse(tester.isRunning());
tester.start();
assertTrue(handler.isEmpty());
assertTrue(tester.isRunning());
// simulate the image load and verify
var image = tester.image_;
assertEquals(String(tester.getUri()), image.src);
assertTrue(handler.isEmpty());
image.onload.call(null);
assertTrue(handler.dequeue());
assertFalse(tester.isRunning());
}
function testFailure() {
// set up the tester
var handler = new Handler();
var tester = new goog.net.NetworkTester(handler.callback, handler);
assertFalse(tester.isRunning());
tester.start();
assertTrue(handler.isEmpty());
assertTrue(tester.isRunning());
// simulate the image failure and verify
var image = tester.image_;
assertEquals(String(tester.getUri()), image.src);
assertTrue(handler.isEmpty());
image.onerror.call(null);
assertFalse(handler.dequeue());
assertFalse(tester.isRunning());
}
function testAbort() {
// set up the tester
var handler = new Handler();
var tester = new goog.net.NetworkTester(handler.callback, handler);
assertFalse(tester.isRunning());
tester.start();
assertTrue(handler.isEmpty());
assertTrue(tester.isRunning());
// simulate the image abort and verify
var image = tester.image_;
assertEquals(String(tester.getUri()), image.src);
assertTrue(handler.isEmpty());
image.onabort.call(null);
assertFalse(handler.dequeue());
assertFalse(tester.isRunning());
}
function testTimeout() {
// set up the tester
var handler = new Handler();
var tester = new goog.net.NetworkTester(handler.callback, handler);
assertFalse(tester.isRunning());
tester.start();
assertTrue(handler.isEmpty());
assertTrue(tester.isRunning());
// simulate the image timeout and verify
var image = tester.image_;
assertEquals(String(tester.getUri()), image.src);
assertTrue(handler.isEmpty());
clock.tick(10000);
assertFalse(handler.dequeue());
assertFalse(tester.isRunning());
}
function testRetries() {
// set up the tester
var handler = new Handler();
var tester = new goog.net.NetworkTester(handler.callback, handler);
tester.setNumRetries(1);
assertEquals(tester.getAttemptCount(), 0);
assertFalse(tester.isRunning());
tester.start();
assertTrue(handler.isEmpty());
assertTrue(tester.isRunning());
assertEquals(tester.getAttemptCount(), 1);
// try number 1 fails
var image = tester.image_;
assertEquals(String(tester.getUri()), image.src);
assertTrue(handler.isEmpty());
image.onerror.call(null);
assertTrue(handler.isEmpty());
assertTrue(tester.isRunning());
assertEquals(tester.getAttemptCount(), 2);
// try number 2 succeeds
image = tester.image_;
assertEquals(String(tester.getUri()), image.src);
assertTrue(handler.isEmpty());
image.onload.call(null);
assertTrue(handler.dequeue());
assertFalse(tester.isRunning());
assertEquals(tester.getAttemptCount(), 2);
}
function testPauseBetweenRetries() {
// set up the tester
var handler = new Handler();
var tester = new goog.net.NetworkTester(handler.callback, handler);
tester.setNumRetries(1);
tester.setPauseBetweenRetries(1000);
assertFalse(tester.isRunning());
tester.start();
assertTrue(handler.isEmpty());
assertTrue(tester.isRunning());
// try number 1 fails
var image = tester.image_;
assertEquals(String(tester.getUri()), image.src);
assertTrue(handler.isEmpty());
image.onerror.call(null);
assertTrue(handler.isEmpty());
assertTrue(tester.isRunning());
// need to pause 1000 ms for the second attempt
assertNull(tester.image_);
clock.tick(1000);
// try number 2 succeeds
image = tester.image_;
assertEquals(String(tester.getUri()), image.src);
assertTrue(handler.isEmpty());
image.onload.call(null);
assertTrue(handler.dequeue());
assertFalse(tester.isRunning());
}
function testNonDefaultUri() {
var handler = new Handler();
var newUri = new goog.Uri('//www.google.com/images/cleardot2.gif');
var tester = new goog.net.NetworkTester(handler.callback, handler, newUri);
var testerUri = tester.getUri();
assertTrue(testerUri.toString().indexOf('cleardot2') > -1);
}
function testOffline() {
// set up the tester
var handler = new Handler();
var tester = new goog.net.NetworkTester(handler.callback, handler);
var orgGetNavigatorOffline = goog.net.NetworkTester.getNavigatorOffline_;
goog.net.NetworkTester.getNavigatorOffline_ = function() {
return true;
};
try {
assertFalse(tester.isRunning());
tester.start();
assertTrue(handler.isEmpty());
assertTrue(tester.isRunning());
// the call is done async
clock.tick(1);
assertFalse(handler.dequeue());
assertFalse(tester.isRunning());
} finally {
// Clean up!
goog.net.NetworkTester.getNavigatorOffline_ = orgGetNavigatorOffline;
}
}
// Handler object for verifying callback
function Handler() {
this.events_ = [];
}
function testGetAttemptCount() {
// set up the tester
var handler = new Handler();
var tester = new goog.net.NetworkTester(handler.callback, handler);
assertEquals(tester.getAttemptCount(), 0);
assertTrue(tester.attempt_ === tester.getAttemptCount());
assertFalse(tester.isRunning());
tester.start();
assertTrue(tester.isRunning());
assertTrue(tester.attempt_ === tester.getAttemptCount());
}
Handler.prototype.callback = function(result) {
this.events_.push(result);
};
Handler.prototype.isEmpty = function() {
return this.events_.length == 0;
};
Handler.prototype.dequeue = function() {
if (this.isEmpty()) {
throw Error('Handler is empty');
}
return this.events_.shift();
};
// override image constructor for test - can't use a real image due to
// async load of images - have to simulate it
function Image() {
}
@@ -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,524 @@
// 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.net.WebSocket.base(this, 'constructor');
/**
* 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;
/** @private {?number} */
goog.net.WebSocket.prototype.reconnectTimer_ = null;
/**
* 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|!ArrayBuffer|!ArrayBufferView} 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;
};
/**
* Gets the number of bytes of data that have been queued using calls to send()
* but not yet transmitted to the network.
*
* @return {number} Number of bytes of data that have been queued.
*/
goog.net.WebSocket.prototype.getBufferedAmount = function() {
return this.webSocket_.bufferedAmount;
};
/**
* 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<string>} event The web socket message event.
* @private
*/
goog.net.WebSocket.prototype.onMessage_ = function(event) {
var message = 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.net.WebSocket.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
* @final
*/
goog.net.WebSocket.MessageEvent = function(message) {
goog.net.WebSocket.MessageEvent.base(
this, 'constructor', 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
* @final
*/
goog.net.WebSocket.ErrorEvent = function(data) {
goog.net.WebSocket.ErrorEvent.base(
this, 'constructor', 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,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2011 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.WebSocket
</title>
<script type="text/javascript" src="../base.js">
</script>
<script type="text/javascript">
goog.require('goog.net.WebSocketTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,363 @@
// 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.
goog.provide('goog.net.WebSocketTest');
goog.setTestOnly('goog.net.WebSocketTest');
goog.require('goog.debug.EntryPointMonitor');
goog.require('goog.debug.ErrorHandler');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.events');
goog.require('goog.functions');
goog.require('goog.net.WebSocket');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var webSocket;
var mockClock;
var pr;
var testUrl;
var originalOnOpen = goog.net.WebSocket.prototype.onOpen_;
var originalOnClose = goog.net.WebSocket.prototype.onClose_;
var originalOnMessage = goog.net.WebSocket.prototype.onMessage_;
var originalOnError = goog.net.WebSocket.prototype.onError_;
function setUp() {
pr = new goog.testing.PropertyReplacer();
pr.set(goog.global, 'WebSocket', MockWebSocket);
mockClock = new goog.testing.MockClock(true);
testUrl = 'ws://127.0.0.1:4200';
testProtocol = 'xmpp';
}
function tearDown() {
pr.reset();
goog.net.WebSocket.prototype.onOpen_ = originalOnOpen;
goog.net.WebSocket.prototype.onClose_ = originalOnClose;
goog.net.WebSocket.prototype.onMessage_ = originalOnMessage;
goog.net.WebSocket.prototype.onError_ = originalOnError;
goog.dispose(mockClock);
goog.dispose(webSocket);
}
function testOpenInUnsupportingBrowserThrowsException() {
// Null out WebSocket to simulate lack of support.
if (goog.global.WebSocket) {
goog.global.WebSocket = null;
}
webSocket = new goog.net.WebSocket();
assertThrows('Open should fail if WebSocket is not defined.',
function() {
webSocket.open(testUrl);
});
}
function testOpenTwiceThrowsException() {
webSocket = new goog.net.WebSocket();
webSocket.open(testUrl);
simulateOpenEvent(webSocket.webSocket_);
assertThrows('Attempting to open a second time should fail.',
function() {
webSocket.open(testUrl);
});
}
function testSendWithoutOpeningThrowsException() {
webSocket = new goog.net.WebSocket();
assertThrows('Send should fail if the web socket was not first opened.',
function() {
webSocket.send('test message');
});
}
function testOpenWithProtocol() {
webSocket = new goog.net.WebSocket();
webSocket.open(testUrl, testProtocol);
var ws = webSocket.webSocket_;
simulateOpenEvent(ws);
assertEquals(testUrl, ws.url);
assertEquals(testProtocol, ws.protocol);
}
function testOpenAndClose() {
webSocket = new goog.net.WebSocket();
assertFalse(webSocket.isOpen());
webSocket.open(testUrl);
var ws = webSocket.webSocket_;
simulateOpenEvent(ws);
assertTrue(webSocket.isOpen());
assertEquals(testUrl, ws.url);
webSocket.close();
simulateCloseEvent(ws);
assertFalse(webSocket.isOpen());
}
function testReconnectionDisabled() {
// Construct the web socket and disable reconnection.
webSocket = new goog.net.WebSocket(false);
// Record how many times open is called.
pr.set(webSocket, 'open', goog.testing.recordFunction(webSocket.open));
// Open the web socket.
webSocket.open(testUrl);
assertEquals(0, webSocket.reconnectAttempt_);
assertEquals(1, webSocket.open.getCallCount());
assertFalse(webSocket.isOpen());
// Simulate failure.
var ws = webSocket.webSocket_;
simulateCloseEvent(ws);
assertFalse(webSocket.isOpen());
assertEquals(0, webSocket.reconnectAttempt_);
assertEquals(1, webSocket.open.getCallCount());
// Make sure a reconnection doesn't happen.
mockClock.tick(100000);
assertEquals(0, webSocket.reconnectAttempt_);
assertEquals(1, webSocket.open.getCallCount());
}
function testReconnectionWithFailureOnFirstOpen() {
// Construct the web socket with a linear back-off.
webSocket = new goog.net.WebSocket(true, linearBackOff);
// Record how many times open is called.
pr.set(webSocket, 'open', goog.testing.recordFunction(webSocket.open));
// Open the web socket.
webSocket.open(testUrl, testProtocol);
assertEquals(0, webSocket.reconnectAttempt_);
assertEquals(1, webSocket.open.getCallCount());
assertFalse(webSocket.isOpen());
// Simulate failure.
var ws = webSocket.webSocket_;
simulateCloseEvent(ws);
assertFalse(webSocket.isOpen());
assertEquals(1, webSocket.reconnectAttempt_);
assertEquals(1, webSocket.open.getCallCount());
// Make sure the reconnect doesn't happen before it should.
mockClock.tick(linearBackOff(0) - 1);
assertEquals(1, webSocket.open.getCallCount());
mockClock.tick(1);
assertEquals(2, webSocket.open.getCallCount());
// Simulate another failure.
simulateCloseEvent(ws);
assertFalse(webSocket.isOpen());
assertEquals(2, webSocket.reconnectAttempt_);
assertEquals(2, webSocket.open.getCallCount());
// Make sure the reconnect doesn't happen before it should.
mockClock.tick(linearBackOff(1) - 1);
assertEquals(2, webSocket.open.getCallCount());
mockClock.tick(1);
assertEquals(3, webSocket.open.getCallCount());
// Simulate connection success.
simulateOpenEvent(ws);
assertEquals(0, webSocket.reconnectAttempt_);
assertEquals(3, webSocket.open.getCallCount());
// Make sure the reconnection has the same url and protocol.
assertEquals(testUrl, ws.url);
assertEquals(testProtocol, ws.protocol);
// Ensure no further calls to open are made.
mockClock.tick(linearBackOff(10));
assertEquals(3, webSocket.open.getCallCount());
}
function testReconnectionWithFailureAfterOpen() {
// Construct the web socket with a linear back-off.
webSocket = new goog.net.WebSocket(true, fibonacciBackOff);
// Record how many times open is called.
pr.set(webSocket, 'open', goog.testing.recordFunction(webSocket.open));
// Open the web socket.
webSocket.open(testUrl);
assertEquals(0, webSocket.reconnectAttempt_);
assertEquals(1, webSocket.open.getCallCount());
assertFalse(webSocket.isOpen());
// Simulate connection success.
var ws = webSocket.webSocket_;
simulateOpenEvent(ws);
assertEquals(0, webSocket.reconnectAttempt_);
assertEquals(1, webSocket.open.getCallCount());
// Let some time pass, then fail the connection.
mockClock.tick(100000);
simulateCloseEvent(ws);
assertFalse(webSocket.isOpen());
assertEquals(1, webSocket.reconnectAttempt_);
assertEquals(1, webSocket.open.getCallCount());
// Make sure the reconnect doesn't happen before it should.
mockClock.tick(fibonacciBackOff(0) - 1);
assertEquals(1, webSocket.open.getCallCount());
mockClock.tick(1);
assertEquals(2, webSocket.open.getCallCount());
// Simulate connection success.
ws = webSocket.webSocket_;
simulateOpenEvent(ws);
assertEquals(0, webSocket.reconnectAttempt_);
assertEquals(2, webSocket.open.getCallCount());
// Ensure no further calls to open are made.
mockClock.tick(fibonacciBackOff(10));
assertEquals(2, webSocket.open.getCallCount());
}
function testExponentialBackOff() {
assertEquals(1000, goog.net.WebSocket.EXPONENTIAL_BACKOFF_(0));
assertEquals(2000, goog.net.WebSocket.EXPONENTIAL_BACKOFF_(1));
assertEquals(4000, goog.net.WebSocket.EXPONENTIAL_BACKOFF_(2));
assertEquals(60000, goog.net.WebSocket.EXPONENTIAL_BACKOFF_(6));
assertEquals(60000, goog.net.WebSocket.EXPONENTIAL_BACKOFF_(7));
}
function testEntryPointRegistry() {
var monitor = new goog.debug.EntryPointMonitor();
var replacement = function() {};
monitor.wrap = goog.testing.recordFunction(
goog.functions.constant(replacement));
goog.debug.entryPointRegistry.monitorAll(monitor);
assertTrue(monitor.wrap.getCallCount() >= 1);
assertEquals(replacement, goog.net.WebSocket.prototype.onOpen_);
assertEquals(replacement, goog.net.WebSocket.prototype.onClose_);
assertEquals(replacement, goog.net.WebSocket.prototype.onMessage_);
assertEquals(replacement, goog.net.WebSocket.prototype.onError_);
}
function testErrorHandlerCalled() {
var errorHandlerCalled = false;
var errorHandler = new goog.debug.ErrorHandler(function() {
errorHandlerCalled = true;
});
goog.net.WebSocket.protectEntryPoints(errorHandler);
webSocket = new goog.net.WebSocket();
goog.events.listenOnce(webSocket, goog.net.WebSocket.EventType.OPENED,
function() {
throw Error();
});
webSocket.open(testUrl);
var ws = webSocket.webSocket_;
assertThrows(function() {
simulateOpenEvent(ws);
});
assertTrue('Error handler callback should be called when registered as ' +
'protecting the entry points.', errorHandlerCalled);
}
/**
* Simulates the browser firing the open event for the given web socket.
* @param {MockWebSocket} ws The mock web socket.
*/
function simulateOpenEvent(ws) {
ws.readyState = goog.net.WebSocket.ReadyState_.OPEN;
ws.onopen();
}
/**
* Simulates the browser firing the close event for the given web socket.
* @param {MockWebSocket} ws The mock web socket.
*/
function simulateCloseEvent(ws) {
ws.readyState = goog.net.WebSocket.ReadyState_.CLOSED;
ws.onclose({data: 'mock close event'});
}
/**
* Strategy for reconnection that backs off linearly with a 1 second offset.
* @param {number} attempt The number of reconnects since the last connection.
* @return {number} The amount of time to the next reconnect, in milliseconds.
*/
function linearBackOff(attempt) {
return (attempt * 1000) + 1000;
}
/**
* Strategy for reconnection that backs off with the fibonacci pattern. It is
* offset by 5 seconds so the first attempt will happen after 5 seconds.
* @param {number} attempt The number of reconnects since the last connection.
* @return {number} The amount of time to the next reconnect, in milliseconds.
*/
function fibonacciBackOff(attempt) {
return (fibonacci(attempt) * 1000) + 5000;
}
/**
* Computes the desired fibonacci number.
* @param {number} n The nth desired fibonacci number.
* @return {number} The nth fibonacci number.
*/
function fibonacci(n) {
if (n == 0) {
return 0;
} else if (n == 1) {
return 1;
} else {
return fibonacci(n - 2) + fibonacci(n - 1);
}
}
/**
* Mock WebSocket constructor.
* @param {string} url The url to the web socket server.
* @param {string} protocol The protocol to use.
* @constructor
*/
MockWebSocket = function(url, protocol) {
this.url = url;
this.protocol = protocol;
this.readyState = goog.net.WebSocket.ReadyState_.CONNECTING;
};
/**
* Mocks out the close method of the WebSocket.
*/
MockWebSocket.prototype.close = function() {
this.readyState = goog.net.WebSocket.ReadyState_.CLOSING;
};
/**
* Mocks out the send method of the WebSocket.
*/
MockWebSocket.prototype.send = function() {
// Nothing to do here.
};
@@ -0,0 +1,71 @@
// 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');
/** @suppress {extraRequire} Typedef. */
goog.require('goog.net.XhrLike');
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():!goog.net.XhrLike.OrNative} 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
* @final
*/
goog.net.WrapperXmlHttpFactory = function(xhrFactory, optionsFactory) {
goog.net.XmlHttpFactory.call(this);
/**
* XHR factory method.
* @type {function() : !goog.net.XhrLike.OrNative}
* @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,25 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2007 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
Author: arv@google.com (Erik Arvidsson)
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.XhrIo
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.XhrIoTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,837 @@
// 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.
goog.provide('goog.net.XhrIoTest');
goog.setTestOnly('goog.net.XhrIoTest');
goog.require('goog.Uri');
goog.require('goog.debug.EntryPointMonitor');
goog.require('goog.debug.ErrorHandler');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.events');
goog.require('goog.functions');
goog.require('goog.net.EventType');
goog.require('goog.net.WrapperXmlHttpFactory');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XmlHttp');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.net.XhrIo');
goog.require('goog.testing.recordFunction');
goog.require('goog.userAgent.product');
function MockXmlHttp() {
/**
* The headers for this XmlHttpRequest.
* @type {!Object<string>}
*/
this.headers = {};
}
MockXmlHttp.prototype.readyState = goog.net.XmlHttp.ReadyState.UNINITIALIZED;
MockXmlHttp.prototype.status = 200;
MockXmlHttp.syncSend = false;
MockXmlHttp.prototype.send = function(opt_data) {
this.readyState = goog.net.XmlHttp.ReadyState.UNINITIALIZED;
if (MockXmlHttp.syncSend) {
this.complete();
}
};
MockXmlHttp.prototype.complete = function() {
this.readyState = goog.net.XmlHttp.ReadyState.LOADING;
this.onreadystatechange();
this.readyState = goog.net.XmlHttp.ReadyState.LOADED;
this.onreadystatechange();
this.readyState = goog.net.XmlHttp.ReadyState.INTERACTIVE;
this.onreadystatechange();
this.readyState = goog.net.XmlHttp.ReadyState.COMPLETE;
this.onreadystatechange();
};
MockXmlHttp.prototype.open = function(verb, uri, async) {
};
MockXmlHttp.prototype.abort = function() {};
MockXmlHttp.prototype.setRequestHeader = function(key, value) {
this.headers[key] = value;
};
var lastMockXmlHttp;
goog.net.XmlHttp.setGlobalFactory(new goog.net.WrapperXmlHttpFactory(
function() {
lastMockXmlHttp = new MockXmlHttp();
return lastMockXmlHttp;
},
function() {
return {};
}));
var propertyReplacer = new goog.testing.PropertyReplacer();
var clock;
var originalEntryPoint =
goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_;
function setUp() {
lastMockXmlHttp = null;
clock = new goog.testing.MockClock(true);
}
function tearDown() {
propertyReplacer.reset();
clock.dispose();
goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_ = originalEntryPoint;
}
function testSyncSend() {
if (goog.userAgent.product.SAFARI) {
// TODO(b/20733468): Disabled so we can get the rest of the Closure test
// suite running in a continuous build. Will investigate later.
return;
}
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertTrue('Should be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send('url');
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSyncSendFailure() {
if (goog.userAgent.product.SAFARI) {
// TODO(b/20733468): Disabled so we can get the rest of the Closure test
// suite running in a continuous build. Will investigate later.
return;
}
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertFalse('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send('url');
lastMockXmlHttp.status = 404;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendRelativeZeroStatus() {
if (goog.userAgent.product.SAFARI) {
// TODO(b/20733468): Disabled so we can get the rest of the Closure test
// suite running in a continuous build. Will investigate later.
return;
}
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertEquals('Should be the same as ', e.target.isSuccess(),
window.location.href.toLowerCase().indexOf('file:') == 0);
count++;
});
var inSend = true;
x.send('relative');
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendRelativeUriZeroStatus() {
if (goog.userAgent.product.SAFARI) {
// TODO(b/20733468): Disabled so we can get the rest of the Closure test
// suite running in a continuous build. Will investigate later.
return;
}
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertEquals('Should be the same as ', e.target.isSuccess(),
window.location.href.toLowerCase().indexOf('file:') == 0);
count++;
});
var inSend = true;
x.send(goog.Uri.parse('relative'));
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendHttpZeroStatusFailure() {
if (goog.userAgent.product.SAFARI) {
// TODO(b/20733468): Disabled so we can get the rest of the Closure test
// suite running in a continuous build. Will investigate later.
return;
}
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertFalse('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send('http://foo');
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendHttpUpperZeroStatusFailure() {
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertFalse('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send('HTTP://foo');
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendHttpUpperUriZeroStatusFailure() {
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertFalse('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send(goog.Uri.parse('HTTP://foo'));
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendHttpUriZeroStatusFailure() {
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertFalse('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send(goog.Uri.parse('http://foo'));
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendHttpUriZeroStatusFailure() {
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertFalse('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send(goog.Uri.parse('HTTP://foo'));
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendHttpsZeroStatusFailure() {
if (goog.userAgent.product.SAFARI) {
// TODO(b/20733468): Disabled so we can get the rest of the Closure test
// suite running in a continuous build. Will investigate later.
return;
}
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertFalse('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send('https://foo');
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendFileUpperZeroStatusSuccess() {
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertTrue('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send('FILE:///foo');
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendFileUriZeroStatusSuccess() {
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertTrue('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send(goog.Uri.parse('file:///foo'));
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendDummyUriZeroStatusSuccess() {
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertTrue('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send(goog.Uri.parse('dummy:///foo'));
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendFileUpperUriZeroStatusSuccess() {
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertFalse('Should not fire complete from inside send', inSend);
assertTrue('Should not be succesful', e.target.isSuccess());
count++;
});
var inSend = true;
x.send(goog.Uri.parse('FILE:///foo'));
lastMockXmlHttp.status = 0;
inSend = false;
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testSendFromListener() {
MockXmlHttp.syncSend = true;
var count = 0;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
count++;
var e = assertThrows(function() {
x.send('url2');
});
assertEquals('[goog.net.XhrIo] Object is active with another request=url' +
'; newUri=url2', e.message);
});
x.send('url');
clock.tick(1); // callOnce(f, 0, ...)
assertEquals('Complete should have been called once', 1, count);
}
function testStatesDuringEvents() {
if (goog.userAgent.product.SAFARI) {
// TODO(b/20733468): Disabled so we can get the rest of the Closure test
// suite running in a continuous build. Will investigate later.
return;
}
MockXmlHttp.syncSend = true;
var x = new goog.net.XhrIo;
var readyState = goog.net.XmlHttp.ReadyState.UNINITIALIZED;
goog.events.listen(x, goog.net.EventType.READY_STATE_CHANGE, function(e) {
readyState++;
assertObjectEquals(e.target, x);
assertEquals(x.getReadyState(), readyState);
assertTrue(x.isActive());
});
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
assertObjectEquals(e.target, x);
assertTrue(x.isActive());
});
goog.events.listen(x, goog.net.EventType.SUCCESS, function(e) {
assertObjectEquals(e.target, x);
assertTrue(x.isActive());
});
goog.events.listen(x, goog.net.EventType.READY, function(e) {
assertObjectEquals(e.target, x);
assertFalse(x.isActive());
});
x.send('url');
clock.tick(1); // callOnce(f, 0, ...)
}
function testProtectEntryPointCalledOnAsyncSend() {
MockXmlHttp.syncSend = false;
var errorHandlerCallbackCalled = false;
var errorHandler = new goog.debug.ErrorHandler(function() {
errorHandlerCallbackCalled = true;
});
goog.net.XhrIo.protectEntryPoints(errorHandler);
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.READY_STATE_CHANGE, function(e) {
throw Error();
});
x.send('url');
assertThrows(function() {
lastMockXmlHttp.complete();
});
assertTrue('Error handler callback should be called on async send.',
errorHandlerCallbackCalled);
}
function testXHRIsDiposedEvenIfAListenerThrowsAnExceptionOnComplete() {
MockXmlHttp.syncSend = false;
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
throw Error();
}, false, x);
x.send('url');
assertThrows(function() {
lastMockXmlHttp.complete();
});
// The XHR should have been disposed, even though the listener threw an
// exception.
assertNull(x.xhr_);
}
function testDisposeInternalDoesNotAbortXhrRequestObjectWhenActiveIsFalse() {
MockXmlHttp.syncSend = false;
var xmlHttp = goog.net.XmlHttp;
var abortCalled = false;
var x = new goog.net.XhrIo;
goog.net.XmlHttp.prototype.abort = function() { abortCalled = true; };
goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
this.active_ = false;
this.dispose();
}, false, x);
x.send('url');
lastMockXmlHttp.complete();
goog.net.XmlHttp = xmlHttp;
assertFalse(abortCalled);
}
function testCallingAbortFromWithinAbortCallbackDoesntLoop() {
var x = new goog.net.XhrIo;
goog.events.listen(x, goog.net.EventType.ABORT, function(e) {
x.abort(); // Shouldn't get a stack overflow
});
x.send('url');
x.abort();
}
function testPostSetsContentTypeHeader() {
var x = new goog.net.XhrIo;
x.send('url', 'POST', 'content');
var headers = lastMockXmlHttp.headers;
assertEquals(1, goog.object.getCount(headers));
assertEquals(
headers[goog.net.XhrIo.CONTENT_TYPE_HEADER],
goog.net.XhrIo.FORM_CONTENT_TYPE);
}
function testNonPostSetsContentTypeHeader() {
var x = new goog.net.XhrIo;
x.send('url', 'PUT', 'content');
headers = lastMockXmlHttp.headers;
assertEquals(1, goog.object.getCount(headers));
assertEquals(
headers[goog.net.XhrIo.CONTENT_TYPE_HEADER],
goog.net.XhrIo.FORM_CONTENT_TYPE);
}
function testContentTypeIsTreatedCaseInsensitively() {
var x = new goog.net.XhrIo;
x.send('url', 'POST', 'content', {'content-type': 'testing'});
assertObjectEquals(
'Headers should not be modified since they already contain a ' +
'content type definition',
{'content-type': 'testing'},
lastMockXmlHttp.headers);
}
function testIsContentTypeHeader_() {
assertTrue(goog.net.XhrIo.isContentTypeHeader_('content-type'));
assertTrue(goog.net.XhrIo.isContentTypeHeader_('Content-type'));
assertTrue(goog.net.XhrIo.isContentTypeHeader_('CONTENT-TYPE'));
assertTrue(goog.net.XhrIo.isContentTypeHeader_('Content-Type'));
assertFalse(goog.net.XhrIo.isContentTypeHeader_('Content Type'));
}
function testPostFormDataDoesNotSetContentTypeHeader() {
function FakeFormData() {}
propertyReplacer.set(goog.global, 'FormData', FakeFormData);
var x = new goog.net.XhrIo;
x.send('url', 'POST', new FakeFormData());
var headers = lastMockXmlHttp.headers;
assertTrue(goog.object.isEmpty(headers));
}
function testNonPostFormDataDoesNotSetContentTypeHeader() {
function FakeFormData() {}
propertyReplacer.set(goog.global, 'FormData', FakeFormData);
var x = new goog.net.XhrIo;
x.send('url', 'PUT', new FakeFormData());
headers = lastMockXmlHttp.headers;
assertTrue(goog.object.isEmpty(headers));
}
function testFactoryInjection() {
var xhr = new MockXmlHttp();
var optionsFactoryCalled = 0;
var xhrFactoryCalled = 0;
var wrapperFactory = new goog.net.WrapperXmlHttpFactory(
function() {
xhrFactoryCalled++;
return xhr;
},
function() {
optionsFactoryCalled++;
return {};
});
var xhrIo = new goog.net.XhrIo(wrapperFactory);
xhrIo.send('url');
assertEquals('XHR factory should have been called', 1, xhrFactoryCalled);
assertEquals('Options factory should have been called', 1,
optionsFactoryCalled);
}
function testGoogTestingNetXhrIoIsInSync() {
var xhrIo = new goog.net.XhrIo();
var testingXhrIo = new goog.testing.net.XhrIo();
var propertyComparator = function(value, key, obj) {
if (goog.string.endsWith(key, '_')) {
// Ignore private properties/methods
return true;
} else if (typeof value == 'function' && typeof this[key] != 'function') {
// Only type check is sufficient for functions
fail('Mismatched property:' + key + ': gooo.net.XhrIo has:<' +
value + '>; while goog.testing.net.XhrIo has:<' + this[key] + '>');
return true;
} else {
// Ignore all other type of properties.
return true;
}
};
goog.object.every(xhrIo, propertyComparator, testingXhrIo);
}
function testEntryPointRegistry() {
var monitor = new goog.debug.EntryPointMonitor();
var replacement = function() {};
monitor.wrap = goog.testing.recordFunction(
goog.functions.constant(replacement));
goog.debug.entryPointRegistry.monitorAll(monitor);
assertTrue(monitor.wrap.getCallCount() >= 1);
assertEquals(
replacement,
goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_);
}
function testSetWithCredentials() {
// Test on XHR objects that don't have the withCredentials property (older
// browsers).
var x = new goog.net.XhrIo;
x.setWithCredentials(true);
x.send('url');
assertFalse(
'withCredentials should not be set on an XHR object if the property ' +
'does not exist.',
goog.object.containsKey(lastMockXmlHttp, 'withCredentials'));
// Test on XHR objects that have the withCredentials property.
MockXmlHttp.prototype.withCredentials = false;
x = new goog.net.XhrIo;
x.setWithCredentials(true);
x.send('url');
assertTrue(
'withCredentials should be set on an XHR object if the property exists',
goog.object.containsKey(lastMockXmlHttp, 'withCredentials'));
assertTrue(
'withCredentials value not set on XHR object',
lastMockXmlHttp.withCredentials);
// Reset the prototype so it does not effect other tests.
delete MockXmlHttp.prototype.withCredentials;
}
function testGetResponse() {
var x = new goog.net.XhrIo;
// No XHR yet
assertEquals(null, x.getResponse());
// XHR with no .response and no response type, gets text.
x.xhr_ = {};
x.xhr_.responseText = 'text';
assertEquals('text', x.getResponse());
// Response type of text gets text as well.
x.setResponseType(goog.net.XhrIo.ResponseType.TEXT);
x.xhr_.responseText = '';
assertEquals('', x.getResponse());
// Response type of array buffer gets the array buffer.
x.xhr_.mozResponseArrayBuffer = 'ab';
x.setResponseType(goog.net.XhrIo.ResponseType.ARRAY_BUFFER);
assertEquals('ab', x.getResponse());
// With a response field, it is returned no matter what value it has.
x.xhr_.response = undefined;
assertEquals(undefined, x.getResponse());
x.xhr_.response = null;
assertEquals(null, x.getResponse());
x.xhr_.response = '';
assertEquals('', x.getResponse());
x.xhr_.response = 'resp';
assertEquals('resp', x.getResponse());
}
function testGetResponseHeaders() {
var x = new goog.net.XhrIo();
// No XHR yet
assertEquals(0, goog.object.getCount(x.getResponseHeaders()));
// Simulate an XHR with 2 headers.
var headersRaw = 'test1: foo\r\ntest2: bar';
propertyReplacer.set(x, 'getAllResponseHeaders',
goog.functions.constant(headersRaw));
var headers = x.getResponseHeaders();
assertEquals(2, goog.object.getCount(headers));
assertEquals('foo', headers['test1']);
assertEquals('bar', headers['test2']);
}
function testGetResponseHeadersWithColonInValue() {
var x = new goog.net.XhrIo();
// Simulate an XHR with a colon in the http header value.
var headersRaw = 'test1: f:o:o';
propertyReplacer.set(x, 'getAllResponseHeaders',
goog.functions.constant(headersRaw));
var headers = x.getResponseHeaders();
assertEquals(1, goog.object.getCount(headers));
assertEquals('f:o:o', headers['test1']);
}
function testGetResponseHeadersMultipleValuesForOneKey() {
var x = new goog.net.XhrIo();
// No XHR yet
assertEquals(0, goog.object.getCount(x.getResponseHeaders()));
// Simulate an XHR with 2 headers.
var headersRaw = 'test1: foo\r\ntest1: bar';
propertyReplacer.set(x, 'getAllResponseHeaders',
goog.functions.constant(headersRaw));
var headers = x.getResponseHeaders();
assertEquals(1, goog.object.getCount(headers));
assertEquals('foo, bar', headers['test1']);
}
function testGetResponseHeadersEmptyHeader() {
var x = new goog.net.XhrIo();
// No XHR yet
assertEquals(0, goog.object.getCount(x.getResponseHeaders()));
// Simulate an XHR with 2 headers, the last of which is empty.
var headersRaw = 'test2: bar\r\n';
propertyReplacer.set(x, 'getAllResponseHeaders',
goog.functions.constant(headersRaw));
var headers = x.getResponseHeaders();
assertEquals(1, goog.object.getCount(headers));
assertEquals('bar', headers['test2']);
}
@@ -0,0 +1,79 @@
// 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.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: 0).
* @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) {
headers.forEach(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,124 @@
// Copyright 2013 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.
goog.provide('goog.net.XhrLike');
/**
* Interface for the common parts of XMLHttpRequest.
*
* Mostly copied from externs/w3c_xml.js.
*
* @interface
* @see http://www.w3.org/TR/XMLHttpRequest/
*/
goog.net.XhrLike = function() {};
/**
* Typedef that refers to either native or custom-implemented XHR objects.
* @typedef {!goog.net.XhrLike|!XMLHttpRequest}
*/
goog.net.XhrLike.OrNative;
/**
* @type {function()|null|undefined}
* @see http://www.w3.org/TR/XMLHttpRequest/#handler-xhr-onreadystatechange
*/
goog.net.XhrLike.prototype.onreadystatechange;
/**
* @type {string}
* @see http://www.w3.org/TR/XMLHttpRequest/#the-responsetext-attribute
*/
goog.net.XhrLike.prototype.responseText;
/**
* @type {Document}
* @see http://www.w3.org/TR/XMLHttpRequest/#the-responsexml-attribute
*/
goog.net.XhrLike.prototype.responseXML;
/**
* @type {number}
* @see http://www.w3.org/TR/XMLHttpRequest/#readystate
*/
goog.net.XhrLike.prototype.readyState;
/**
* @type {number}
* @see http://www.w3.org/TR/XMLHttpRequest/#status
*/
goog.net.XhrLike.prototype.status;
/**
* @type {string}
* @see http://www.w3.org/TR/XMLHttpRequest/#statustext
*/
goog.net.XhrLike.prototype.statusText;
/**
* @param {string} method
* @param {string} url
* @param {?boolean=} opt_async
* @param {?string=} opt_user
* @param {?string=} opt_password
* @see http://www.w3.org/TR/XMLHttpRequest/#the-open()-method
*/
goog.net.XhrLike.prototype.open = function(method, url, opt_async, opt_user,
opt_password) {};
/**
* @param {ArrayBuffer|ArrayBufferView|Blob|Document|FormData|string=} opt_data
* @see http://www.w3.org/TR/XMLHttpRequest/#the-send()-method
*/
goog.net.XhrLike.prototype.send = function(opt_data) {};
/**
* @see http://www.w3.org/TR/XMLHttpRequest/#the-abort()-method
*/
goog.net.XhrLike.prototype.abort = function() {};
/**
* @param {string} header
* @param {string} value
* @see http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader()-method
*/
goog.net.XhrLike.prototype.setRequestHeader = function(header, value) {};
/**
* @param {string} header
* @return {string}
* @see http://www.w3.org/TR/XMLHttpRequest/#the-getresponseheader()-method
*/
goog.net.XhrLike.prototype.getResponseHeader = function(header) {};
/**
* @return {string}
* @see http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders()-method
*/
goog.net.XhrLike.prototype.getAllResponseHeaders = function() {};
@@ -0,0 +1,774 @@
// 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.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.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: 0).
* @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.net.XhrManager.base(this, 'constructor');
/**
* 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<string, !goog.net.XhrManager.Request>}
* @private
*/
this.requests_ = new goog.structs.Map();
/**
* The event handler.
* @type {goog.events.EventHandler<!goog.net.XhrManager>}
* @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 buffered and will be sent when an
* XhrIo object becomes available, taking into account the request's
* priority. Note also that requests of equal priority are sent in an
* implementation specific order - to get FIFO queue semantics use a
* monotonically increasing priority for successive requests.
* @param {string} id The id of the request.
* @param {string} url Uri to make the request to.
* @param {string=} opt_method Send method, default: GET.
* @param {ArrayBuffer|ArrayBufferView|Blob|Document|FormData|string=}
* opt_content Post data.
* @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the
* request.
* @param {number=} 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;
this.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}
* @final
*/
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|ArrayBufferView|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
* @final
*/
goog.net.XhrManager.Request = function(url, xhrEventCallback, opt_method,
opt_content, opt_headers, opt_callback, opt_maxRetries, opt_responseType) {
/**
* 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|ArrayBufferView|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}
* @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;
};
/**
* 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|ArrayBufferView|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} 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_;
};
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
Closure Unit Tests - goog.net.XhrManager
</title>
<script src="../base.js">
</script>
<script>
goog.require('goog.net.XhrManagerTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,120 @@
// 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.
goog.provide('goog.net.XhrManagerTest');
goog.setTestOnly('goog.net.XhrManagerTest');
goog.require('goog.events');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XhrManager');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.net.XhrIoPool');
goog.require('goog.testing.recordFunction');
/** @type {goog.net.XhrManager} */
var xhrManager;
/** @type {goog.testing.net.XhrIo} */
var xhrIo;
function setUp() {
xhrManager = new goog.net.XhrManager();
xhrManager.xhrPool_ = new goog.testing.net.XhrIoPool();
xhrIo = xhrManager.xhrPool_.getXhr();
}
function tearDown() {
goog.dispose(xhrManager);
}
function testGetOutstandingRequestIds() {
assertArrayEquals(
'No outstanding requests', [], xhrManager.getOutstandingRequestIds());
xhrManager.send('test1', '/test1');
assertArrayEquals(
'Single outstanding request', ['test1'],
xhrManager.getOutstandingRequestIds());
xhrManager.send('test2', '/test2');
assertArrayEquals(
'Two outstanding requests', ['test1', 'test2'],
xhrManager.getOutstandingRequestIds());
xhrIo.simulateResponse(200, 'data');
assertArrayEquals(
'Single outstanding request', ['test2'],
xhrManager.getOutstandingRequestIds());
xhrIo.simulateResponse(200, 'data');
assertArrayEquals(
'No outstanding requests', [], xhrManager.getOutstandingRequestIds());
}
function testForceAbortQueuedRequest() {
xhrManager.send('test', '/test');
xhrManager.send('queued', '/queued');
assertNotThrows(
'Forced abort of queued request should not throw an error',
goog.bind(xhrManager.abort, xhrManager, 'queued', true));
assertNotThrows(
'Forced abort of normal request should not throw an error',
goog.bind(xhrManager.abort, xhrManager, 'test', true));
}
function testDefaultResponseType() {
var callback = goog.testing.recordFunction(function(e) {
assertEquals('test1', e.id);
assertEquals(
goog.net.XhrIo.ResponseType.DEFAULT, e.xhrIo.getResponseType());
eventCalled = true;
});
goog.events.listenOnce(xhrManager, goog.net.EventType.READY, callback);
xhrManager.send('test1', '/test2');
assertEquals(1, callback.getCallCount());
xhrIo.simulateResponse(200, 'data'); // Do this to make tearDown() happy.
}
function testNonDefaultResponseType() {
var callback = goog.testing.recordFunction(function(e) {
assertEquals('test2', e.id);
assertEquals(
goog.net.XhrIo.ResponseType.ARRAY_BUFFER, e.xhrIo.getResponseType());
eventCalled = true;
});
goog.events.listenOnce(xhrManager, goog.net.EventType.READY, callback);
xhrManager.send('test2', '/test2',
undefined /* opt_method */,
undefined /* opt_content */,
undefined /* opt_headers */,
undefined /* opt_priority */,
undefined /* opt_callback */,
undefined /* opt_maxRetries */,
goog.net.XhrIo.ResponseType.ARRAY_BUFFER);
assertEquals(1, callback.getCallCount());
xhrIo.simulateResponse(200, 'data'); // Do this to make tearDown() happy.
}
@@ -0,0 +1,246 @@
// 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.provide('goog.net.XmlHttpDefines');
goog.require('goog.asserts');
goog.require('goog.net.WrapperXmlHttpFactory');
goog.require('goog.net.XmlHttpFactory');
/**
* Static class for creating XMLHttpRequest objects.
* @return {!goog.net.XhrLike.OrNative} 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 bypasses the ActiveX probing code.
* NOTE(ruilopes): Due to the way JSCompiler works, this define *will not* strip
* out the ActiveX probing code from binaries. To achieve this, use
* {@code goog.net.XmlHttpDefines.ASSUME_NATIVE_XHR} instead.
* TODO(ruilopes): Collapse both defines.
*/
goog.define('goog.net.XmlHttp.ASSUME_NATIVE_XHR', false);
/** @const */
goog.net.XmlHttpDefines = {};
/**
* @define {boolean} Whether to assume XMLHttpRequest exists. Setting this to
* true eliminates the ActiveX probing code.
*/
goog.define('goog.net.XmlHttpDefines.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(
goog.asserts.assert(factory),
goog.asserts.assert(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 ||
goog.net.XmlHttpDefines.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,67 @@
// 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');
/** @suppress {extraRequire} Typedef. */
goog.require('goog.net.XhrLike');
/**
* 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 {!goog.net.XhrLike.OrNative} A new XhrLike 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,855 @@
// 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.Uri');
goog.require('goog.async.Deferred');
goog.require('goog.async.Delay');
goog.require('goog.dispose');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventType');
goog.require('goog.json');
goog.require('goog.log');
goog.require('goog.messaging.AbstractChannel');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.ChannelStates');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.DirectTransport');
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.TransportTypes');
goog.require('goog.net.xpc.UriCfgFields');
goog.require('goog.string');
goog.require('goog.uri.utils');
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.net.xpc.CrossPageChannel.base(this, 'constructor');
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. Please use
* <code>updateChannelNameAndCatalog</code> to change this from the transports
* vs changing the property directly.
* @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<!goog.net.xpc.CrossPageChannel>}
* @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;
if (!goog.events.getListener(window, goog.events.EventType.UNLOAD,
goog.net.xpc.CrossPageChannel.disposeAll_)) {
// Set listener to dispose all registered channels on page unload.
goog.events.listenOnce(window, goog.events.EventType.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.
*
* @return {Object} The window object of the peer.
* @package
*/
goog.net.xpc.CrossPageChannel.prototype.getPeerWindowObject = function() {
return this.peerWindowObject_;
};
/**
* Determines whether the peer window is available (e.g. not closed).
*
* @return {boolean} Whether the peer window is available.
* @package
*/
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;
}
// TODO(user): Use goog.scope.
var CfgFields = goog.net.xpc.CfgFields;
if (!this.cfg_[CfgFields.TRANSPORT]) {
this.cfg_[CfgFields.TRANSPORT] =
this.determineTransportType_();
}
switch (this.cfg_[CfgFields.TRANSPORT]) {
case goog.net.xpc.TransportTypes.NATIVE_MESSAGING:
var protocolVersion = this.cfg_[
CfgFields.NATIVE_TRANSPORT_PROTOCOL_VERSION] || 2;
this.transport_ = new goog.net.xpc.NativeMessagingTransport(
this,
this.cfg_[CfgFields.PEER_HOSTNAME],
this.domHelper_,
!!this.cfg_[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;
case goog.net.xpc.TransportTypes.DIRECT:
if (this.peerWindowObject_ &&
goog.net.xpc.DirectTransport.isSupported(/** @type {!Window} */ (
this.peerWindowObject_))) {
this.transport_ =
new goog.net.xpc.DirectTransport(this, this.domHelper_);
} else {
goog.log.info(
goog.net.xpc.logger,
'DirectTransport not supported for this window, peer window in' +
' different security context or not set yet.');
}
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(
goog.dom.TagName.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_.listenOnceWithScope(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 this channel was previously closed, transition back to the NOT_CONNECTED
// state to ensure that the connection can proceed (xpcDeliver blocks
// transport messages while the connection state is CLOSED).
if (this.state_ == goog.net.xpc.ChannelStates.CLOSED) {
this.state_ = goog.net.xpc.ChannelStates.NOT_CONNECTED;
}
// 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 (goog.isDef(opt_delay)) {
this.connectionDelay_ =
new goog.async.Delay(this.connectCb_, opt_delay);
this.connectionDelay_.start();
} else {
this.connectionDelay_ = null;
this.connectCb_();
}
};
/**
* 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.
*
* @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.
* @package
*/
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 there is another channel still open, the native transport's global
// postMessage listener will still be active. This will mean that messages
// being sent to the now-closed channel will still be received and delivered,
// such as transport service traffic from its previous correspondent in the
// other frame. Ensure these messages don't cause exceptions.
// Example: http://b/12419303
if (this.isDisposed() || this.state_ == goog.net.xpc.ChannelStates.CLOSED) {
goog.log.warning(goog.net.xpc.logger,
'CrossPageChannel::xpcDeliver(): Channel closed.');
} 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 (goog.isNumber(role)) {
return role;
} else {
return window.parent == this.peerWindowObject_ ?
goog.net.xpc.CrossPageChannelRole.INNER :
goog.net.xpc.CrossPageChannelRole.OUTER;
}
};
/**
* Sets the channel name. Note, this doesn't establish a unique channel to
* communicate on.
* @param {string} name The new channel name.
*/
goog.net.xpc.CrossPageChannel.prototype.updateChannelNameAndCatalog = function(
name) {
goog.log.fine(goog.net.xpc.logger, 'changing channel name to ' + name);
delete goog.net.xpc.channels[this.name];
this.name = name;
goog.net.xpc.channels[name] = this;
};
/**
* 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.isEmptyOrWhitespace(goog.string.makeSafe(opt_origin)) ||
goog.string.isEmptyOrWhitespace(goog.string.makeSafe(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.net.xpc.CrossPageChannel.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,36 @@
<!DOCTYPE html>
<!--
Author: dbk@google.com (David Barrett-Kahn)
-->
<html>
<!--
Copyright 2009 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
CrossPageChannel: End-to-End Test
</title>
<script type="text/javascript" src="../../base.js">
</script>
<script type="text/javascript">
goog.require('goog.net.xpc.CrossPageChannelTest');
</script>
</head>
<body>
<!-- Debug box. -->
<div style="position:absolute; float: right; top: 10px; right: 10px; width: 500px">
Debug [
<a href="#" onclick="document.getElementById('debugDiv').innerHTML = '';">
clear
</a>
]:
<br />
<div id="debugDiv" style="border: 1px #000000 solid; font-size:xx-small; background: #fff;">
</div>
</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -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,635 @@
// Copyright 2013 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 an implementation of a transport that can call methods
* directly on a frame. Useful if you want to use XPC for crossdomain messaging
* (using another transport), or same domain messaging (using this transport).
*/
goog.provide('goog.net.xpc.DirectTransport');
goog.require('goog.Timer');
goog.require('goog.async.Deferred');
goog.require('goog.events.EventHandler');
goog.require('goog.log');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
goog.require('goog.net.xpc.TransportTypes');
goog.require('goog.object');
goog.scope(function() {
var CfgFields = goog.net.xpc.CfgFields;
var CrossPageChannelRole = goog.net.xpc.CrossPageChannelRole;
var Deferred = goog.async.Deferred;
var EventHandler = goog.events.EventHandler;
var Timer = goog.Timer;
var Transport = goog.net.xpc.Transport;
/**
* A direct window to window method transport.
*
* If the windows are in the same security context, this transport calls
* directly into the other window without using any additional mechanism. This
* is mainly used in scenarios where you want to optionally use a cross domain
* transport in cross security context situations, or optionally use a direct
* transport in same security context situations.
*
* Note: Global properties are exported by using this transport. One to
* communicate with the other window by, currently crosswindowmessaging.channel,
* and by using goog.getUid on window, currently closure_uid_[0-9]+.
*
* @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/document. If omitted, uses the current
* document.
* @constructor
* @extends {Transport}
*/
goog.net.xpc.DirectTransport = function(channel, opt_domHelper) {
goog.net.xpc.DirectTransport.base(this, 'constructor', opt_domHelper);
/**
* The channel this transport belongs to.
* @private {!goog.net.xpc.CrossPageChannel}
*/
this.channel_ = channel;
/** @private {!EventHandler<!goog.net.xpc.DirectTransport>} */
this.eventHandler_ = new EventHandler(this);
this.registerDisposable(this.eventHandler_);
/**
* Timer for connection reattempts.
* @private {!Timer}
*/
this.maybeAttemptToConnectTimer_ = new Timer(
DirectTransport.CONNECTION_ATTEMPT_INTERVAL_MS_,
this.getWindow());
this.registerDisposable(this.maybeAttemptToConnectTimer_);
/**
* Fires once we've received our SETUP_ACK message.
* @private {!Deferred}
*/
this.setupAckReceived_ = new Deferred();
/**
* Fires once we've sent our SETUP_ACK message.
* @private {!Deferred}
*/
this.setupAckSent_ = new Deferred();
/**
* Fires once we're marked connected.
* @private {!Deferred}
*/
this.connected_ = new Deferred();
/**
* The unique ID of this side of the connection. Used to determine when a peer
* is reloaded.
* @private {string}
*/
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.
* @private {?string}
*/
this.peerEndpointId_ = null;
/**
* The map of sending messages.
* @private {Object}
*/
this.asyncSendsMap_ = {};
/**
* The original channel name.
* @private {string}
*/
this.originalChannelName_ = this.channel_.name;
// We reconfigure the channel name to include the role so that we can
// communicate in the same window between the different roles on the
// same channel.
this.channel_.updateChannelNameAndCatalog(
DirectTransport.getRoledChannelName_(this.channel_.name,
this.channel_.getRole()));
/**
* Flag indicating if this instance of the transport has been initialized.
* @private {boolean}
*/
this.initialized_ = false;
// 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.
// Two sided handshake:
// SETUP_ACK has to have been received, and sent.
this.connected_.awaitDeferred(this.setupAckReceived_);
this.connected_.awaitDeferred(this.setupAckSent_);
this.connected_.addCallback(this.notifyConnected_, this);
this.connected_.callback(true);
this.eventHandler_.
listen(this.maybeAttemptToConnectTimer_, Timer.TICK,
this.maybeAttemptToConnect_);
goog.log.info(
goog.net.xpc.logger,
'DirectTransport created. role=' + this.channel_.getRole());
};
goog.inherits(goog.net.xpc.DirectTransport, Transport);
var DirectTransport = goog.net.xpc.DirectTransport;
/**
* @private {number}
* @const
*/
DirectTransport.CONNECTION_ATTEMPT_INTERVAL_MS_ = 100;
/**
* The delay to notify the xpc of a successful connection. This is used
* to allow both parties to be connected if one party's connection callback
* invokes an immediate send.
* @private {number}
* @const
*/
DirectTransport.CONNECTION_DELAY_INTERVAL_MS_ = 0;
/**
* @param {!Window} peerWindow The peer window to check if DirectTranport is
* supported on.
* @return {boolean} Whether this transport is supported.
*/
DirectTransport.isSupported = function(peerWindow) {
/** @preserveTry */
try {
return window.document.domain == peerWindow.document.domain;
} catch (e) {
return false;
}
};
/**
* Tracks the number of DirectTransport 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.
* @private {!Object<number>}
*/
DirectTransport.activeCount_ = {};
/**
* Path of global message proxy.
* @private {string}
* @const
*/
// TODO(user): Make this configurable using the CfgFields.
DirectTransport.GLOBAL_TRANPORT_PATH_ = 'crosswindowmessaging.channel';
/**
* The delimiter used for transport service messages.
* @private {string}
* @const
*/
DirectTransport.MESSAGE_DELIMITER_ = ',';
/**
* Initializes this transport. Registers a method for 'message'-events in the
* global scope.
* @param {!Window} listenWindow The window to listen to events on.
* @private
*/
DirectTransport.initialize_ = function(listenWindow) {
var uid = goog.getUid(listenWindow);
var value = DirectTransport.activeCount_[uid] || 0;
if (value == 0) {
// Set up a handler on the window to proxy messages to class.
var globalProxy = goog.getObjectByName(
DirectTransport.GLOBAL_TRANPORT_PATH_,
listenWindow);
if (globalProxy == null) {
goog.exportSymbol(
DirectTransport.GLOBAL_TRANPORT_PATH_,
DirectTransport.messageReceivedHandler_,
listenWindow);
}
}
DirectTransport.activeCount_[uid]++;
};
/**
* @param {string} channelName The channel name.
* @param {string|number} role The role.
* @return {string} The formatted channel name including role.
* @private
*/
DirectTransport.getRoledChannelName_ = function(channelName, role) {
return channelName + '_' + role;
};
/**
* @param {!Object} literal The literal unrenamed message.
* @return {boolean} Whether the message was successfully delivered to a
* channel.
* @private
*/
DirectTransport.messageReceivedHandler_ = function(literal) {
var msg = DirectTransport.Message_.fromLiteral(literal);
var channelName = msg.channelName;
var service = msg.service;
var payload = msg.payload;
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);
return true;
}
var transportMessageType = DirectTransport.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() == CrossPageChannelRole.INNER &&
!staleChannel.isConnected() &&
service == goog.net.xpc.TRANSPORT_SERVICE_ &&
transportMessageType == goog.net.xpc.SETUP) {
// 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+).
staleChannel.updateChannelNameAndCatalog(channelName);
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;
};
/**
* The transport type.
* @type {number}
* @override
*/
DirectTransport.prototype.transportType = goog.net.xpc.TransportTypes.DIRECT;
/**
* Handles transport service messages.
* @param {string} payload The message content.
* @override
*/
DirectTransport.prototype.transportServiceHandler = function(payload) {
var transportParts = DirectTransport.parseTransportPayload_(payload);
var transportMessageType = transportParts[0];
var peerEndpointId = transportParts[1];
switch (transportMessageType) {
case goog.net.xpc.SETUP_ACK_:
if (!this.setupAckReceived_.hasFired()) {
this.setupAckReceived_.callback(true);
}
break;
case goog.net.xpc.SETUP:
this.sendSetupAckMessage_();
if ((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.
* @private
*/
DirectTransport.prototype.sendSetupMessage_ = function() {
// Although we could send real objects, since some other transports are
// limited to strings we also keep this requirement.
var payload = goog.net.xpc.SETUP;
payload += DirectTransport.MESSAGE_DELIMITER_;
payload += this.endpointId_;
this.send(goog.net.xpc.TRANSPORT_SERVICE_, payload);
};
/**
* Sends a SETUP_ACK transport service message.
* @private
*/
DirectTransport.prototype.sendSetupAckMessage_ = function() {
this.send(goog.net.xpc.TRANSPORT_SERVICE_, goog.net.xpc.SETUP_ACK_);
if (!this.setupAckSent_.hasFired()) {
this.setupAckSent_.callback(true);
}
};
/** @override */
DirectTransport.prototype.connect = function() {
var win = this.getWindow();
if (win) {
DirectTransport.initialize_(win);
this.initialized_ = true;
this.maybeAttemptToConnect_();
} else {
goog.log.fine(goog.net.xpc.logger, 'connect(): no window to initialize.');
}
};
/**
* 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
*/
DirectTransport.prototype.maybeAttemptToConnect_ = function() {
var outerRole = this.channel_.getRole() == CrossPageChannelRole.OUTER;
if (this.channel_.isConnected()) {
this.maybeAttemptToConnectTimer_.stop();
return;
}
this.maybeAttemptToConnectTimer_.start();
this.sendSetupMessage_();
};
/**
* Prepares to send a message.
* @param {string} service The name of the service the message is to be
* delivered to.
* @param {string} payload The message content.
* @override
*/
DirectTransport.prototype.send = function(service, payload) {
if (!this.channel_.getPeerWindowObject()) {
goog.log.fine(goog.net.xpc.logger, 'send(): window not ready');
return;
}
var channelName = DirectTransport.getRoledChannelName_(
this.originalChannelName_,
this.getPeerRole_());
var message = new DirectTransport.Message_(
channelName,
service,
payload);
if (this.channel_.getConfig()[CfgFields.DIRECT_TRANSPORT_SYNC_MODE]) {
this.executeScheduledSend_(message);
} else {
// Note: goog.async.nextTick doesn't support cancelling or disposal so
// leaving as 0ms timer, though this may have performance implications.
this.asyncSendsMap_[goog.getUid(message)] =
Timer.callOnce(goog.bind(this.executeScheduledSend_, this, message), 0);
}
};
/**
* Sends the message.
* @param {!DirectTransport.Message_} message The message to send.
* @private
*/
DirectTransport.prototype.executeScheduledSend_ = function(message) {
var messageId = goog.getUid(message);
if (this.asyncSendsMap_[messageId]) {
delete this.asyncSendsMap_[messageId];
}
/** @preserveTry */
try {
var peerProxy = goog.getObjectByName(
DirectTransport.GLOBAL_TRANPORT_PATH_,
this.channel_.getPeerWindowObject());
} catch (error) {
goog.log.warning(
goog.net.xpc.logger,
'Can\'t access other window, ignoring.',
error);
return;
}
if (goog.isNull(peerProxy)) {
goog.log.warning(
goog.net.xpc.logger,
'Peer window had no global function.');
return;
}
/** @preserveTry */
try {
peerProxy(message.toLiteral());
goog.log.info(
goog.net.xpc.logger,
'send(): channelName=' + message.channelName +
' service=' + message.service +
' payload=' + message.payload);
} catch (error) {
goog.log.warning(
goog.net.xpc.logger,
'Error performing call, ignoring.',
error);
}
};
/**
* @return {goog.net.xpc.CrossPageChannelRole} The role of peer channel (either
* inner or outer).
* @private
*/
DirectTransport.prototype.getPeerRole_ = function() {
var role = this.channel_.getRole();
return role == goog.net.xpc.CrossPageChannelRole.OUTER ?
goog.net.xpc.CrossPageChannelRole.INNER :
goog.net.xpc.CrossPageChannelRole.OUTER;
};
/**
* Notifies the channel that this transport is connected.
* @private
*/
DirectTransport.prototype.notifyConnected_ = function() {
// Add a delay as the connection callback will break if this transport is
// synchronous and the callback invokes send() immediately.
this.channel_.notifyConnected(
this.channel_.getConfig()[CfgFields.DIRECT_TRANSPORT_SYNC_MODE] ?
DirectTransport.CONNECTION_DELAY_INTERVAL_MS_ : 0);
};
/** @override */
DirectTransport.prototype.disposeInternal = function() {
if (this.initialized_) {
var listenWindow = this.getWindow();
var uid = goog.getUid(listenWindow);
var value = --DirectTransport.activeCount_[uid];
if (value == 1) {
goog.exportSymbol(
DirectTransport.GLOBAL_TRANPORT_PATH_,
null,
listenWindow);
}
}
if (this.asyncSendsMap_) {
goog.object.forEach(this.asyncSendsMap_, function(timerId) {
Timer.clear(timerId);
});
this.asyncSendsMap_ = null;
}
// Deferred's aren't disposables.
if (this.setupAckReceived_) {
this.setupAckReceived_.cancel();
delete this.setupAckReceived_;
}
if (this.setupAckSent_) {
this.setupAckSent_.cancel();
delete this.setupAckSent_;
}
if (this.connected_) {
this.connected_.cancel();
delete this.connected_;
}
DirectTransport.base(this, 'disposeInternal');
};
/**
* Parses a transport service payload message.
* @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
*/
DirectTransport.parseTransportPayload_ = function(payload) {
var transportParts = /** @type {!Array<?string>} */ (payload.split(
DirectTransport.MESSAGE_DELIMITER_));
transportParts[1] = transportParts[1] || null; // Usually endpointId.
return transportParts;
};
/**
* Message container that gets passed back and forth between windows.
* @param {string} channelName The channel name to tranport messages on.
* @param {string} service The service to send the payload to.
* @param {string} payload The payload to send.
* @constructor
* @struct
* @private
*/
DirectTransport.Message_ = function(channelName, service, payload) {
/**
* The name of the channel.
* @type {string}
*/
this.channelName = channelName;
/**
* The service on the channel.
* @type {string}
*/
this.service = service;
/**
* The payload.
* @type {string}
*/
this.payload = payload;
};
/**
* Converts a message to a literal object.
* @return {!Object} The message as a literal object.
*/
DirectTransport.Message_.prototype.toLiteral = function() {
return {
'channelName': this.channelName,
'service': this.service,
'payload': this.payload
};
};
/**
* Creates a Message_ from a literal object.
* @param {!Object} literal The literal to convert to Message.
* @return {!DirectTransport.Message_} The Message.
*/
DirectTransport.Message_.fromLiteral = function(literal) {
return new DirectTransport.Message_(
literal['channelName'],
literal['service'],
literal['payload']);
};
}); // goog.scope
@@ -0,0 +1,275 @@
// Copyright 2013 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 Tests the direct transport.
*/
goog.provide('goog.net.xpc.DirectTransportTest');
goog.require('goog.Promise');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.labs.userAgent.browser');
goog.require('goog.log');
goog.require('goog.log.Level');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.CrossPageChannel');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.TransportTypes');
goog.require('goog.testing.jsunit');
goog.setTestOnly('goog.net.xpc.DirectTransportTest');
/**
* Echo service name.
* @type {string}
* @const
*/
var ECHO_SERVICE_NAME = 'echo';
/**
* Response service name.
* @type {string}
* @const
*/
var RESPONSE_SERVICE_NAME = 'response';
/**
* Test Payload.
* @type {string}
* @const
*/
var MESSAGE_PAYLOAD_1 = 'This is message payload 1.';
/**
* The name id of the peer iframe.
* @type {string}
* @const
*/
var PEER_IFRAME_ID = 'peer-iframe';
// Class aliases.
var CfgFields = goog.net.xpc.CfgFields;
var CrossPageChannel = goog.net.xpc.CrossPageChannel;
var CrossPageChannelRole = goog.net.xpc.CrossPageChannelRole;
var TransportTypes = goog.net.xpc.TransportTypes;
var outerXpc;
var innerXpc;
var peerIframe;
var channelName;
var messageIsSync = false;
var savedHtml;
var debugDiv;
function setUpPage() {
// Show debug log
debugDiv = document.createElement(goog.dom.TagName.DEBUGDIV);
var logger = goog.log.getLogger('goog.net.xpc');
logger.setLevel(goog.log.Level.ALL);
goog.log.addHandler(logger, function(logRecord) {
var msgElm = goog.dom.createDom(goog.dom.TagName.DIV);
msgElm.innerHTML = logRecord.getMessage();
goog.dom.appendChild(debugDiv, msgElm);
});
}
function setUp() {
savedHtml = document.body.innerHTML;
document.body.appendChild(debugDiv);
}
function tearDown() {
if (peerIframe) {
document.body.removeChild(peerIframe);
peerIframe = null;
}
if (outerXpc) {
outerXpc.dispose();
outerXpc = null;
}
if (innerXpc) {
innerXpc.dispose();
innerXpc = null;
}
window.iframeLoadHandler = null;
channelName = null;
messageIsSync = false;
document.body.innerHTML = savedHtml;
}
function createIframe() {
peerIframe = document.createElement(goog.dom.TagName.IFRAME);
peerIframe.id = PEER_IFRAME_ID;
document.body.insertBefore(peerIframe, document.body.firstChild);
}
/**
* Tests 2 same domain frames using direct transport.
*/
function testDirectTransport() {
// This test has been flaky on IE.
// For now, disable.
// Flakiness is tracked in http://b/18595666
if (goog.labs.userAgent.browser.isIE()) {
return;
}
createIframe();
channelName = goog.net.xpc.getRandomString(10);
outerXpc = new CrossPageChannel(
getConfiguration(CrossPageChannelRole.OUTER, PEER_IFRAME_ID));
// Outgoing service.
outerXpc.registerService(ECHO_SERVICE_NAME, goog.nullFunction);
// Incoming service.
var resolver = goog.Promise.withResolver();
outerXpc.registerService(
RESPONSE_SERVICE_NAME,
function(message) {
assertEquals(
'Received payload is equal to sent payload.',
message,
MESSAGE_PAYLOAD_1);
resolver.resolve();
});
outerXpc.connect(function() {
assertTrue('XPC over direct channel is connected', outerXpc.isConnected());
outerXpc.send(ECHO_SERVICE_NAME, MESSAGE_PAYLOAD_1);
});
// inner_peer.html calls this method at end of html.
window.iframeLoadHandler = function() {
peerIframe.contentWindow.instantiateChannel(
getConfiguration(CrossPageChannelRole.INNER));
};
peerIframe.src = 'testdata/inner_peer.html';
return resolver.promise;
}
/**
* Tests 2 xpc's communicating with each other in the same window.
*/
function testSameWindowDirectTransport() {
channelName = goog.net.xpc.getRandomString(10);
outerXpc = new CrossPageChannel(getConfiguration(CrossPageChannelRole.OUTER));
outerXpc.setPeerWindowObject(self);
// Outgoing service.
outerXpc.registerService(ECHO_SERVICE_NAME, goog.nullFunction);
var resolver = goog.Promise.withResolver();
// Incoming service.
outerXpc.registerService(
RESPONSE_SERVICE_NAME,
function(message) {
assertEquals(
'Received payload is equal to sent payload.',
message,
MESSAGE_PAYLOAD_1);
resolver.resolve();
});
outerXpc.connect(function() {
assertTrue(
'XPC over direct channel, same window, is connected',
outerXpc.isConnected());
outerXpc.send(ECHO_SERVICE_NAME, MESSAGE_PAYLOAD_1);
});
innerXpc = new CrossPageChannel(getConfiguration(CrossPageChannelRole.INNER));
innerXpc.setPeerWindowObject(self);
// Incoming service.
innerXpc.registerService(
ECHO_SERVICE_NAME,
function(message) {
innerXpc.send(RESPONSE_SERVICE_NAME, message);
});
// Outgoing service.
innerXpc.registerService(
RESPONSE_SERVICE_NAME,
goog.nullFunction);
innerXpc.connect();
return resolver.promise;
}
function getConfiguration(role, opt_peerFrameId) {
var cfg = {};
cfg[CfgFields.TRANSPORT] = TransportTypes.DIRECT;
if (goog.isDefAndNotNull(opt_peerFrameId)) {
cfg[CfgFields.IFRAME_ID] = opt_peerFrameId;
}
cfg[CfgFields.CHANNEL_NAME] = channelName;
cfg[CfgFields.ROLE] = role;
return cfg;
}
/**
* Tests 2 same domain frames using direct transport using sync mode.
*/
function testSyncMode() {
// This test has been flaky on IE.
// For now, disable.
// Flakiness is tracked in http://b/18595666
if (goog.labs.userAgent.browser.isIE()) {
return;
}
createIframe();
channelName = goog.net.xpc.getRandomString(10);
var cfg = getConfiguration(CrossPageChannelRole.OUTER, PEER_IFRAME_ID);
cfg[CfgFields.DIRECT_TRANSPORT_SYNC_MODE] = true;
outerXpc = new CrossPageChannel(cfg);
// Outgoing service.
outerXpc.registerService(ECHO_SERVICE_NAME, goog.nullFunction);
var resolver = goog.Promise.withResolver();
// Incoming service.
outerXpc.registerService(
RESPONSE_SERVICE_NAME,
function(message) {
assertTrue('The message response was syncronous', messageIsSync);
assertEquals(
'Received payload is equal to sent payload.',
message,
MESSAGE_PAYLOAD_1);
resolver.resolve();
});
outerXpc.connect(function() {
assertTrue('XPC over direct channel is connected', outerXpc.isConnected());
messageIsSync = true;
outerXpc.send(ECHO_SERVICE_NAME, MESSAGE_PAYLOAD_1);
messageIsSync = false;
});
// inner_peer.html calls this method at end of html.
window.iframeLoadHandler = function() {
var cfg = getConfiguration(CrossPageChannelRole.INNER);
cfg[CfgFields.DIRECT_TRANSPORT_SYNC_MODE] = true;
peerIframe.contentWindow.instantiateChannel(cfg);
};
peerIframe.src = 'testdata/inner_peer.html';
return resolver.promise;
}
@@ -0,0 +1,270 @@
// 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.log');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
goog.require('goog.net.xpc.TransportTypes');
/**
* 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}
* @final
*/
goog.net.xpc.FrameElementMethodTransport = function(channel, opt_domHelper) {
goog.net.xpc.FrameElementMethodTransport.base(
this, 'constructor', 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<{serviceName: string, payload: string}>}
* @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;
/** @private */
goog.net.xpc.FrameElementMethodTransport.prototype.attemptSetupCb_;
/** @private */
goog.net.xpc.FrameElementMethodTransport.prototype.outgoing_;
/** @private */
goog.net.xpc.FrameElementMethodTransport.prototype.iframeElm_;
/**
* 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,996 @@
// 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.dom.TagName');
goog.require('goog.log');
goog.require('goog.log.Level');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.Transport');
goog.require('goog.net.xpc.TransportTypes');
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}
* @final
*/
goog.net.xpc.IframePollingTransport = function(channel, opt_domHelper) {
goog.net.xpc.IframePollingTransport.base(this, 'constructor', 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<string>}
* @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;
/** @private {goog.net.xpc.IframePollingTransport.Receiver} */
goog.net.xpc.IframePollingTransport.prototype.ackReceiver_;
/** @private {goog.net.xpc.IframePollingTransport.Sender} */
goog.net.xpc.IframePollingTransport.prototype.ackSender_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.ackIframeElm_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.ackWinObj_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.checkLocalFramesPresentCb_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.deliveryQueue_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.msgIframeElm_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.msgReceiver_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.msgSender_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.msgWinObj_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.rcvdConnectionSetupAck_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.sentConnectionSetupAck_;
/** @private {boolean} */
goog.net.xpc.IframePollingTransport.prototype.sentConnectionSetup_;
/** @private */
goog.net.xpc.IframePollingTransport.prototype.parts_;
/**
* 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(goog.dom.TagName.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_.updateChannelNameAndCatalog(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.net.xpc.IframePollingTransport.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. Must
* be an http:// or https:// URL.
* @param {Object} windowObj The frame used for sending information to.
* @final
*/
goog.net.xpc.IframePollingTransport.Sender = function(url, windowObj) {
// This class is instantiated from goog.net.xpc.IframePollingTransport, which
// takes its URLs from a goog.net.xpc.CrossPageChannel, which in turns
// sanitizes them. However, since this class can be instantiated from
// elsewhere than IframePollingTransport the url needs to be sanitized
// here too.
if (!/^https?:\/\//.test(url)) {
throw Error('URL ' + url + ' is invalid');
}
/**
* The URI used to sending messages.
* @type {string}
* @private
*/
this.sanitizedSendUri_ = 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 {Window}
* @private
*/
this.sendFrame_ = /** @type {Window} */ (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.sanitizedSendUri_ + '#' + 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.
* @final
*/
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,24 @@
<!DOCTYPE html>
<html>
<!--
Copyright 2012 The Closure Library Authors. All Rights Reserved.
Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<!--
-->
<head>
<meta http-equiv="X-IframePollingTransport-Compatible" content="IE=edge" />
<title>
NativeMessagingTransport Unit-Tests
</title>
<script src="../../base.js">
</script>
<script>
goog.require('goog.net.xpc.IframePollingTransportTest');
</script>
</head>
<body>
</body>
</html>
@@ -0,0 +1,289 @@
// 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.
goog.provide('goog.net.xpc.IframePollingTransportTest');
goog.setTestOnly('goog.net.xpc.IframePollingTransportTest');
goog.require('goog.Timer');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.functions');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.CrossPageChannel');
goog.require('goog.net.xpc.CrossPageChannelRole');
goog.require('goog.net.xpc.TransportTypes');
goog.require('goog.object');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');
var mockClock = null;
var outerChannel = null;
var innerChannel = null;
function setUp() {
mockClock = new goog.testing.MockClock(true /* opt_autoInstall */);
// Create the peer windows.
var outerPeerHostName = 'https://www.youtube.com';
var outerPeerWindow = createMockPeerWindow(outerPeerHostName);
var innerPeerHostName = 'https://www.google.com';
var innerPeerWindow = createMockPeerWindow(innerPeerHostName);
// Create the channels.
outerChannel = createChannel(goog.net.xpc.CrossPageChannelRole.OUTER, 'test',
outerPeerHostName, outerPeerWindow, innerPeerHostName, innerPeerWindow);
innerChannel = createChannel(goog.net.xpc.CrossPageChannelRole.INNER, 'test',
innerPeerHostName, innerPeerWindow, outerPeerHostName, outerPeerWindow);
}
function tearDown() {
outerChannel.dispose();
innerChannel.dispose();
mockClock.uninstall();
}
/** Tests that connection happens normally and callbacks are invoked. */
function testConnect() {
var outerConnectCallback = goog.testing.recordFunction();
var innerConnectCallback = goog.testing.recordFunction();
// Connect the two channels.
outerChannel.connect(outerConnectCallback);
innerChannel.connect(innerConnectCallback);
mockClock.tick(1000);
// Check that channels were connected and callbacks invoked.
assertEquals(1, outerConnectCallback.getCallCount());
assertEquals(1, innerConnectCallback.getCallCount());
assertTrue(outerChannel.isConnected());
assertTrue(innerChannel.isConnected());
}
/** Tests that messages are successfully delivered to the inner peer. */
function testSend_outerToInner() {
var serviceCallback = goog.testing.recordFunction();
// Register a service handler in the inner channel.
innerChannel.registerService('svc', function(payload) {
assertEquals('hello', payload);
serviceCallback();
});
// Connect the two channels.
outerChannel.connect();
innerChannel.connect();
mockClock.tick(1000);
// Send a message.
outerChannel.send('svc', 'hello');
mockClock.tick(1000);
// Check that the message was handled.
assertEquals(1, serviceCallback.getCallCount());
}
/** Tests that messages are successfully delivered to the outer peer. */
function testSend_innerToOuter() {
var serviceCallback = goog.testing.recordFunction();
// Register a service handler in the inner channel.
outerChannel.registerService('svc', function(payload) {
assertEquals('hello', payload);
serviceCallback();
});
// Connect the two channels.
outerChannel.connect();
innerChannel.connect();
mockClock.tick(1000);
// Send a message.
innerChannel.send('svc', 'hello');
mockClock.tick(1000);
// Check that the message was handled.
assertEquals(1, serviceCallback.getCallCount());
}
/** Tests that closing the outer peer does not cause an error. */
function testSend_outerPeerClosed() {
// Connect the inner channel.
innerChannel.connect();
mockClock.tick(1000);
// Close the outer peer before it has a chance to connect.
closeWindow(innerChannel.getPeerWindowObject());
// Allow timers to execute (and fail).
mockClock.tick(1000);
}
/** Tests that closing the inner peer does not cause an error. */
function testSend_innerPeerClosed() {
// Connect the outer channel.
outerChannel.connect();
mockClock.tick(1000);
// Close the inner peer before it has a chance to connect.
closeWindow(outerChannel.getPeerWindowObject());
// Allow timers to execute (and fail).
mockClock.tick(1000);
}
/** Tests that partially closing the outer peer does not cause an error. */
function testSend_outerPeerClosing() {
// Connect the inner channel.
innerChannel.connect();
mockClock.tick(1000);
// Close the outer peer before it has a chance to connect, but
// leave closed set to false to simulate a partially closed window.
closeWindow(innerChannel.getPeerWindowObject());
innerChannel.getPeerWindowObject().closed = false;
// Allow timers to execute (and fail).
mockClock.tick(1000);
}
/** Tests that partially closing the inner peer does not cause an error. */
function testSend_innerPeerClosing() {
// Connect the outer channel.
outerChannel.connect();
mockClock.tick(1000);
// Close the inner peer before it has a chance to connect, but
// leave closed set to false to simulate a partially closed window.
closeWindow(outerChannel.getPeerWindowObject());
outerChannel.getPeerWindowObject().closed = false;
// Allow timers to execute (and fail).
mockClock.tick(1000);
}
/**
* Creates a channel with the specified configuration, using frame polling.
* @param {!goog.net.xpc.CrossPageChannelRole} role The channel role.
* @param {string} channelName The channel name.
* @param {string} fromHostName The host name of the window hosting the channel.
* @param {!Object} fromWindow The window hosting the channel.
* @param {string} toHostName The host name of the peer window.
* @param {!Object} toWindow The peer window.
*/
function createChannel(role, channelName, fromHostName, fromWindow, toHostName,
toWindow) {
// Build a channel config using frame polling.
var channelConfig = goog.object.create(
goog.net.xpc.CfgFields.ROLE,
role,
goog.net.xpc.CfgFields.PEER_HOSTNAME,
toHostName,
goog.net.xpc.CfgFields.CHANNEL_NAME,
channelName,
goog.net.xpc.CfgFields.LOCAL_POLL_URI,
fromHostName + '/robots.txt',
goog.net.xpc.CfgFields.PEER_POLL_URI,
toHostName + '/robots.txt',
goog.net.xpc.CfgFields.TRANSPORT,
goog.net.xpc.TransportTypes.IFRAME_POLLING);
// Build the channel.
var channel = new goog.net.xpc.CrossPageChannel(channelConfig);
channel.setPeerWindowObject(toWindow);
// Update the transport's getWindow, to return the correct host window.
channel.createTransport_();
channel.transport_.getWindow = goog.functions.constant(fromWindow);
return channel;
}
/**
* Creates a mock window to use as a peer. The peer window will host the frame
* elements.
* @param {string} url The peer window's initial URL.
*/
function createMockPeerWindow(url) {
var mockPeer = createMockWindow(url);
// Update the appendChild method to use a mock frame window.
mockPeer.document.body.appendChild = function(el) {
assertEquals(goog.dom.TagName.IFRAME, el.tagName);
mockPeer.frames[el.name] = createMockWindow(el.src);
mockPeer.document.body.element.appendChild(el);
};
return mockPeer;
}
/**
* Creates a mock window.
* @param {string} url The window's initial URL.
*/
function createMockWindow(url) {
// Create the mock window, document and body.
var mockWindow = {};
var mockDocument = {};
var mockBody = {};
var mockLocation = {};
// Configure the mock window's document body.
mockBody.element = goog.dom.createDom(goog.dom.TagName.BODY);
// Configure the mock window's document.
mockDocument.body = mockBody;
// Configure the mock window's location.
mockLocation.href = url;
mockLocation.replace = function(value) { mockLocation.href = value; };
// Configure the mock window.
mockWindow.document = mockDocument;
mockWindow.frames = {};
mockWindow.location = mockLocation;
mockWindow.setTimeout = goog.Timer.callOnce;
return mockWindow;
}
/**
* Emulates closing the specified window by clearing frames, document and
* location.
*/
function closeWindow(targetWindow) {
// Close any child frame windows.
for (var frameName in targetWindow.frames) {
closeWindow(targetWindow.frames[frameName]);
}
// Clear the target window, set closed to true.
targetWindow.closed = true;
targetWindow.frames = null;
targetWindow.document = null;
targetWindow.location = null;
}
@@ -0,0 +1,410 @@
// 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.dom.TagName');
goog.require('goog.dom.safe');
goog.require('goog.events');
goog.require('goog.html.SafeHtml');
goog.require('goog.log');
goog.require('goog.log.Level');
goog.require('goog.net.xpc');
goog.require('goog.net.xpc.CfgFields');
goog.require('goog.net.xpc.Transport');
goog.require('goog.net.xpc.TransportTypes');
goog.require('goog.string');
goog.require('goog.string.Const');
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}
* @final
*/
goog.net.xpc.IframeRelayTransport = function(channel, opt_domHelper) {
goog.net.xpc.IframeRelayTransport.base(this, 'constructor', 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(goog.dom.TagName.DIV);
// TODO(user): It might be possible to set the sandbox attribute
// to restrict the privileges of the created iframe.
goog.dom.safe.setInnerHtml(div,
goog.html.SafeHtml.createIframe(null, null, {
'onload': goog.string.Const.from('this.xpcOnload()'),
'sandbox': null
}));
var ifr = div.childNodes[0];
div = null;
ifr['xpcOnload'] = goog.net.xpc.IframeRelayTransport.iframeLoadHandler_;
} else {
var ifr = this.getWindow().document.createElement(goog.dom.TagName.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.net.xpc.IframeRelayTransport.base(this, 'disposeInternal');
if (goog.userAgent.WEBKIT) {
goog.net.xpc.IframeRelayTransport.cleanup_(0);
}
};

Some files were not shown because too many files have changed in this diff Show More