Add vectortile branch
This commit is contained in:
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>
|
||||
* <body>
|
||||
* <script type="text/javascript"
|
||||
* src="path-to-crossdomainrpc.js"></script>
|
||||
* 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] =
|
||||
* <value of parameter "xdpe:request-id">;
|
||||
* echo[goog.net.CrossDomainRpc.PARAM_ECHO_DUMMY_URI] =
|
||||
* <value of parameter "xdpe:dummy-uri">;
|
||||
*
|
||||
* goog.net.CrossDomainRpc.sendResponse(
|
||||
* '({"result":"<responseInJSON"})',
|
||||
* true, // is JSON
|
||||
* echo, // parameters to echo back
|
||||
* status, // response status code
|
||||
* headers // response headers
|
||||
* );
|
||||
* </script>
|
||||
* </body>
|
||||
* </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, '&') : 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
Reference in New Issue
Block a user