Commit 5b7c9e08 authored by jrw's avatar jrw Committed by Commit bot

Updated remoting.xhr API to use promises.

Removed access to the native XHR object used by the API.

This is a larger change than one might expect for two reasons: First,
because the native XHR object only allows the response content to be
retrieved while the onreadystatechange handler is executing, and
second, because the unit test for dns_blackhole_checker.js relied on
synchronous semantics which cannot be duplicated with promises because
when a promise is resolved, its "then" handlers are not called until
the next event cycle.

Review URL: https://codereview.chromium.org/1003433002

Cr-Commit-Position: refs/heads/master@{#321608}
parent c29919ce
...@@ -105,11 +105,11 @@ remoting.AppRemoting.prototype.start = function(connector, token) { ...@@ -105,11 +105,11 @@ remoting.AppRemoting.prototype.start = function(connector, token) {
/** @type {remoting.AppRemoting} */ /** @type {remoting.AppRemoting} */
var that = this; var that = this;
/** @param {XMLHttpRequest} xhr */ /** @param {!remoting.Xhr.Response} xhrResponse */
var parseAppHostResponse = function(xhr) { var parseAppHostResponse = function(xhrResponse) {
if (xhr.status == 200) { if (xhrResponse.status == 200) {
var response = /** @type {remoting.AppRemoting.AppHostResponse} */ var response = /** @type {remoting.AppRemoting.AppHostResponse} */
(base.jsonParseSafe(xhr.responseText)); (base.jsonParseSafe(xhrResponse.getText()));
if (response && if (response &&
response.status && response.status &&
response.status == 'done' && response.status == 'done' &&
...@@ -157,16 +157,16 @@ remoting.AppRemoting.prototype.start = function(connector, token) { ...@@ -157,16 +157,16 @@ remoting.AppRemoting.prototype.start = function(connector, token) {
// TODO(garykac) Start using remoting.Error.fromHttpStatus once it has // TODO(garykac) Start using remoting.Error.fromHttpStatus once it has
// been updated to properly report 'unknown' errors (rather than // been updated to properly report 'unknown' errors (rather than
// reporting them as AUTHENTICATION_FAILED). // reporting them as AUTHENTICATION_FAILED).
if (xhr.status == 0) { if (xhrResponse.status == 0) {
that.handleError(new remoting.Error( that.handleError(new remoting.Error(
remoting.Error.Tag.NETWORK_FAILURE)); remoting.Error.Tag.NETWORK_FAILURE));
} else if (xhr.status == 401) { } else if (xhrResponse.status == 401) {
that.handleError(new remoting.Error( that.handleError(new remoting.Error(
remoting.Error.Tag.AUTHENTICATION_FAILED)); remoting.Error.Tag.AUTHENTICATION_FAILED));
} else if (xhr.status == 403) { } else if (xhrResponse.status == 403) {
that.handleError(new remoting.Error( that.handleError(new remoting.Error(
remoting.Error.Tag.APP_NOT_AUTHORIZED)); remoting.Error.Tag.APP_NOT_AUTHORIZED));
} else if (xhr.status == 502 || xhr.status == 503) { } else if (xhrResponse.status == 502 || xhrResponse.status == 503) {
that.handleError(new remoting.Error( that.handleError(new remoting.Error(
remoting.Error.Tag.SERVICE_UNAVAILABLE)); remoting.Error.Tag.SERVICE_UNAVAILABLE));
} else { } else {
...@@ -175,12 +175,11 @@ remoting.AppRemoting.prototype.start = function(connector, token) { ...@@ -175,12 +175,11 @@ remoting.AppRemoting.prototype.start = function(connector, token) {
} }
}; };
remoting.xhr.start({ new remoting.Xhr({
method: 'POST', method: 'POST',
url: that.runApplicationUrl(), url: that.runApplicationUrl(),
onDone: parseAppHostResponse,
oauthToken: token oauthToken: token
}); }).start().then(parseAppHostResponse);
}; };
/** /**
......
...@@ -127,21 +127,19 @@ function onToken(token) { ...@@ -127,21 +127,19 @@ function onToken(token) {
'/applications/' + remoting.settings.getAppRemotingApplicationId() + '/applications/' + remoting.settings.getAppRemotingApplicationId() +
'/hosts/' + hostId + '/hosts/' + hostId +
'/reportIssue'; '/reportIssue';
/** @param {XMLHttpRequest} xhr */ var onDone = function(/** !remoting.Xhr.Response */ response) {
var onDone = function(xhr) { if (response.status >= 200 && response.status < 300) {
if (xhr.status >= 200 && xhr.status < 300) {
getUserInfo(); getUserInfo();
} else { } else {
showError(); showError();
} }
}; };
remoting.xhr.start({ new remoting.Xhr({
method: 'POST', method: 'POST',
url: uri, url: uri,
onDone: onDone,
jsonContent: body, jsonContent: body,
oauthToken: token oauthToken: token
}); }).start().then(onDone);
} else { } else {
getUserInfo(); getUserInfo();
} }
......
...@@ -38,7 +38,7 @@ remoting.DnsBlackholeChecker = function(signalStrategy) { ...@@ -38,7 +38,7 @@ remoting.DnsBlackholeChecker = function(signalStrategy) {
/** @private */ /** @private */
this.blackholeState_ = BlackholeState.PENDING; this.blackholeState_ = BlackholeState.PENDING;
/** @private {?XMLHttpRequest} */ /** @private {?remoting.Xhr} */
this.xhr_ = null; this.xhr_ = null;
}; };
...@@ -85,11 +85,11 @@ remoting.DnsBlackholeChecker.prototype.connect = function(server, ...@@ -85,11 +85,11 @@ remoting.DnsBlackholeChecker.prototype.connect = function(server,
this.signalStrategy_.connect(server, username, authToken); this.signalStrategy_.connect(server, username, authToken);
this.xhr_ = remoting.xhr.start({ this.xhr_ = new remoting.Xhr({
method: 'GET', method: 'GET',
url: remoting.DnsBlackholeChecker.URL_TO_REQUEST_, url: remoting.DnsBlackholeChecker.URL_TO_REQUEST_
onDone: this.onHttpRequestDone_.bind(this)
}); });
this.xhr_.start().then(this.onHttpRequestDone_.bind(this));
}; };
remoting.DnsBlackholeChecker.prototype.getState = function() { remoting.DnsBlackholeChecker.prototype.getState = function() {
...@@ -153,12 +153,12 @@ remoting.DnsBlackholeChecker.prototype.onWrappedSignalStrategyStateChanged_ = ...@@ -153,12 +153,12 @@ remoting.DnsBlackholeChecker.prototype.onWrappedSignalStrategyStateChanged_ =
}; };
/** /**
* @param {XMLHttpRequest} xhr * @param {!remoting.Xhr.Response} response
* @private * @private
*/ */
remoting.DnsBlackholeChecker.prototype.onHttpRequestDone_ = function(xhr) { remoting.DnsBlackholeChecker.prototype.onHttpRequestDone_ = function(response) {
this.xhr_ = null; this.xhr_ = null;
if (xhr.status >= 200 && xhr.status <= 299) { if (response.status >= 200 && response.status <= 299) {
console.log("DNS blackhole check succeeded."); console.log("DNS blackhole check succeeded.");
this.blackholeState_ = BlackholeState.OPEN; this.blackholeState_ = BlackholeState.OPEN;
if (this.signalStrategy_.getState() == if (this.signalStrategy_.getState() ==
...@@ -166,14 +166,15 @@ remoting.DnsBlackholeChecker.prototype.onHttpRequestDone_ = function(xhr) { ...@@ -166,14 +166,15 @@ remoting.DnsBlackholeChecker.prototype.onHttpRequestDone_ = function(xhr) {
this.setState_(remoting.SignalStrategy.State.CONNECTED); this.setState_(remoting.SignalStrategy.State.CONNECTED);
} }
} else { } else {
console.error("DNS blackhole check failed: " + xhr.status + " " + console.error("DNS blackhole check failed: " + response.status + " " +
xhr.statusText + ". Response URL: " + xhr.responseURL + response.statusText + ". Response URL: " +
". Response Text: " + xhr.responseText); response.url + ". Response Text: " +
response.getText());
this.blackholeState_ = BlackholeState.BLOCKED; this.blackholeState_ = BlackholeState.BLOCKED;
base.dispose(this.signalStrategy_); base.dispose(this.signalStrategy_);
this.setState_(remoting.SignalStrategy.State.FAILED); this.setState_(remoting.SignalStrategy.State.FAILED);
} }
} };
/** /**
* @param {remoting.SignalStrategy.State} newState * @param {remoting.SignalStrategy.State} newState
......
...@@ -23,13 +23,15 @@ var checker = null; ...@@ -23,13 +23,15 @@ var checker = null;
/** @type {remoting.MockSignalStrategy} */ /** @type {remoting.MockSignalStrategy} */
var signalStrategy = null; var signalStrategy = null;
var fakeXhrs;
/** @type {sinon.FakeXhr} */
var fakeXhr = null;
QUnit.module('dns_blackhole_checker', { QUnit.module('dns_blackhole_checker', {
beforeEach: function(assert) { beforeEach: function(assert) {
fakeXhrs = [];
sinon.useFakeXMLHttpRequest().onCreate = function(xhr) { sinon.useFakeXMLHttpRequest().onCreate = function(xhr) {
fakeXhrs.push(xhr); QUnit.equal(fakeXhr, null, 'exactly one XHR is issued');
fakeXhr = xhr;
}; };
onStateChange = sinon.spy(); onStateChange = sinon.spy();
...@@ -46,9 +48,8 @@ QUnit.module('dns_blackhole_checker', { ...@@ -46,9 +48,8 @@ QUnit.module('dns_blackhole_checker', {
sinon.assert.calledWith(signalStrategy.connect, 'server', 'username', sinon.assert.calledWith(signalStrategy.connect, 'server', 'username',
'authToken'); 'authToken');
assert.equal(fakeXhrs.length, 1, 'exactly one XHR is issued');
assert.equal( assert.equal(
fakeXhrs[0].url, remoting.DnsBlackholeChecker.URL_TO_REQUEST_, fakeXhr.url, remoting.DnsBlackholeChecker.URL_TO_REQUEST_,
'the correct URL is requested'); 'the correct URL is requested');
}, },
afterEach: function() { afterEach: function() {
...@@ -59,112 +60,136 @@ QUnit.module('dns_blackhole_checker', { ...@@ -59,112 +60,136 @@ QUnit.module('dns_blackhole_checker', {
onStateChange = null; onStateChange = null;
onIncomingStanzaCallback = null; onIncomingStanzaCallback = null;
checker = null; checker = null;
}, fakeXhr = null;
}
}); });
QUnit.test('success', QUnit.test('success',
function(assert) { function(assert) {
fakeXhrs[0].respond(200); function checkState(state) {
sinon.assert.notCalled(onStateChange);
[
remoting.SignalStrategy.State.CONNECTING,
remoting.SignalStrategy.State.HANDSHAKE,
remoting.SignalStrategy.State.CONNECTED
].forEach(function(state) {
signalStrategy.setStateForTesting(state); signalStrategy.setStateForTesting(state);
sinon.assert.calledWith(onStateChange, state); sinon.assert.calledWith(onStateChange, state);
assert.equal(checker.getState(), state); assert.equal(checker.getState(), state);
});
} }
);
return base.SpyPromise.run(function() {
fakeXhr.respond(200);
}).then(function() {
sinon.assert.notCalled(onStateChange);
checkState(remoting.SignalStrategy.State.CONNECTING);
checkState(remoting.SignalStrategy.State.HANDSHAKE);
checkState(remoting.SignalStrategy.State.CONNECTED);
});
});
QUnit.test('http response after connected', QUnit.test('http response after connected',
function(assert) { function(assert) {
[ function checkState(state) {
remoting.SignalStrategy.State.CONNECTING,
remoting.SignalStrategy.State.HANDSHAKE,
].forEach(function(state) {
signalStrategy.setStateForTesting(state); signalStrategy.setStateForTesting(state);
sinon.assert.calledWith(onStateChange, state); sinon.assert.calledWith(onStateChange, state);
assert.equal(checker.getState(), state); assert.equal(checker.getState(), state);
}); }
checkState(remoting.SignalStrategy.State.CONNECTING);
checkState(remoting.SignalStrategy.State.HANDSHAKE);
onStateChange.reset(); onStateChange.reset();
// Verify that DnsBlackholeChecker stays in HANDSHAKE state even if the // Verify that DnsBlackholeChecker stays in HANDSHAKE state even if the
// signal strategy has connected. // signal strategy has connected.
signalStrategy.setStateForTesting(remoting.SignalStrategy.State.CONNECTED); return base.SpyPromise.run(function() {
signalStrategy.setStateForTesting(
remoting.SignalStrategy.State.CONNECTED);
}).then(function() {
sinon.assert.notCalled(onStateChange); sinon.assert.notCalled(onStateChange);
assert.equal(checker.getState(), remoting.SignalStrategy.State.HANDSHAKE); assert.equal(checker.getState(), remoting.SignalStrategy.State.HANDSHAKE);
// Verify that DnsBlackholeChecker goes to CONNECTED state after the // Verify that DnsBlackholeChecker goes to CONNECTED state after the
// the HTTP request has succeeded. // the HTTP request has succeeded.
fakeXhrs[0].respond(200); return base.SpyPromise.run(function() {
fakeXhr.respond(200);
});
}).then(function() {
sinon.assert.calledWith(onStateChange, sinon.assert.calledWith(onStateChange,
remoting.SignalStrategy.State.CONNECTED); remoting.SignalStrategy.State.CONNECTED);
} });
); });
QUnit.test('connect failed', QUnit.test('connect failed',
function(assert) { function(assert) {
fakeXhrs[0].respond(200); function checkState(state) {
sinon.assert.notCalled(onStateChange);
[
remoting.SignalStrategy.State.CONNECTING,
remoting.SignalStrategy.State.FAILED
].forEach(function(state) {
signalStrategy.setStateForTesting(state); signalStrategy.setStateForTesting(state);
sinon.assert.calledWith(onStateChange, state); sinon.assert.calledWith(onStateChange, state);
};
return base.SpyPromise.run(function() {
fakeXhr.respond(200);
}).then(function() {
sinon.assert.notCalled(onStateChange);
checkState(remoting.SignalStrategy.State.CONNECTING);
checkState(remoting.SignalStrategy.State.FAILED);
});
}); });
}
);
QUnit.test('blocked', QUnit.test('blocked',
function(assert) { function(assert) {
fakeXhrs[0].respond(400); function checkState(state) {
sinon.assert.calledWith(onStateChange,
remoting.SignalStrategy.State.FAILED);
assert.equal(checker.getError().getTag(), assert.equal(checker.getError().getTag(),
remoting.Error.Tag.NOT_AUTHORIZED); remoting.Error.Tag.NOT_AUTHORIZED);
onStateChange.reset(); onStateChange.reset();
[
remoting.SignalStrategy.State.CONNECTING,
remoting.SignalStrategy.State.HANDSHAKE,
remoting.SignalStrategy.State.CONNECTED
].forEach(function(state) {
signalStrategy.setStateForTesting(state); signalStrategy.setStateForTesting(state);
sinon.assert.notCalled(onStateChange); sinon.assert.notCalled(onStateChange);
assert.equal(checker.getState(), remoting.SignalStrategy.State.FAILED); assert.equal(checker.getState(),
checker.getState(),
remoting.SignalStrategy.State.FAILED,
'checker state is still FAILED');
};
return base.SpyPromise.run(function() {
fakeXhr.respond(400);
}).then(function() {
sinon.assert.calledWith(
onStateChange, remoting.SignalStrategy.State.FAILED);
assert.equal(
checker.getError().getTag(),
remoting.Error.Tag.NOT_AUTHORIZED,
'checker error is NOT_AUTHORIZED');
checkState(remoting.SignalStrategy.State.CONNECTING);
checkState(remoting.SignalStrategy.State.HANDSHAKE);
checkState(remoting.SignalStrategy.State.FAILED);
});
}); });
}
);
QUnit.test('blocked after connected', QUnit.test('blocked after connected',
function(assert) { function(assert) {
[ function checkState(state) {
remoting.SignalStrategy.State.CONNECTING,
remoting.SignalStrategy.State.HANDSHAKE,
].forEach(function(state) {
signalStrategy.setStateForTesting(state); signalStrategy.setStateForTesting(state);
sinon.assert.calledWith(onStateChange, state); sinon.assert.calledWith(onStateChange, state);
assert.equal(checker.getState(), state); assert.equal(checker.getState(), state);
}); };
checkState(remoting.SignalStrategy.State.CONNECTING);
checkState(remoting.SignalStrategy.State.HANDSHAKE);
onStateChange.reset(); onStateChange.reset();
// Verify that DnsBlackholeChecker stays in HANDSHAKE state even if the // Verify that DnsBlackholeChecker stays in HANDSHAKE state even
// signal strategy has connected. // if the signal strategy has connected.
signalStrategy.setStateForTesting(remoting.SignalStrategy.State.CONNECTED); return base.SpyPromise.run(function() {
signalStrategy.setStateForTesting(
remoting.SignalStrategy.State.CONNECTED);
}).then(function() {
sinon.assert.notCalled(onStateChange); sinon.assert.notCalled(onStateChange);
assert.equal(checker.getState(), remoting.SignalStrategy.State.HANDSHAKE); assert.equal(checker.getState(), remoting.SignalStrategy.State.HANDSHAKE);
// Verify that DnsBlackholeChecker goes to FAILED state after it gets the // Verify that DnsBlackholeChecker goes to FAILED state after it
// blocked HTTP response. // gets the blocked HTTP response.
fakeXhrs[0].respond(400); return base.SpyPromise.run(function() {
fakeXhr.respond(400);
});
}).then(function() {
sinon.assert.calledWith(onStateChange, sinon.assert.calledWith(onStateChange,
remoting.SignalStrategy.State.FAILED); remoting.SignalStrategy.State.FAILED);
assert.ok(checker.getError().hasTag(remoting.Error.Tag.NOT_AUTHORIZED)); assert.ok(checker.getError().hasTag(remoting.Error.Tag.NOT_AUTHORIZED));
});
} }
); );
......
...@@ -234,14 +234,14 @@ remoting.HostController.prototype.start = function(hostPin, consent, onDone, ...@@ -234,14 +234,14 @@ remoting.HostController.prototype.start = function(hostPin, consent, onDone,
* @param {string} hostName * @param {string} hostName
* @param {string} publicKey * @param {string} publicKey
* @param {string} privateKey * @param {string} privateKey
* @param {XMLHttpRequest} xhr * @param {!remoting.Xhr.Response} response
*/ */
function onRegistered( function onRegistered(
hostName, publicKey, privateKey, xhr) { hostName, publicKey, privateKey, response) {
var success = (xhr.status == 200); var success = (response.status == 200);
if (success) { if (success) {
var result = base.jsonParseSafe(xhr.responseText); var result = base.jsonParseSafe(response.getText());
if ('data' in result && 'authorizationCode' in result['data']) { if ('data' in result && 'authorizationCode' in result['data']) {
that.hostDaemonFacade_.getCredentialsFromAuthCode( that.hostDaemonFacade_.getCredentialsFromAuthCode(
result['data']['authorizationCode'], result['data']['authorizationCode'],
...@@ -260,8 +260,8 @@ remoting.HostController.prototype.start = function(hostPin, consent, onDone, ...@@ -260,8 +260,8 @@ remoting.HostController.prototype.start = function(hostPin, consent, onDone,
}); });
} }
} else { } else {
console.log('Failed to register the host. Status: ' + xhr.status + console.log('Failed to register the host. Status: ' + response.status +
' response: ' + xhr.responseText); ' response: ' + response.getText());
onError(new remoting.Error(remoting.Error.Tag.REGISTRATION_FAILED)); onError(new remoting.Error(remoting.Error.Tag.REGISTRATION_FAILED));
} }
} }
...@@ -281,16 +281,15 @@ remoting.HostController.prototype.start = function(hostPin, consent, onDone, ...@@ -281,16 +281,15 @@ remoting.HostController.prototype.start = function(hostPin, consent, onDone,
publicKey: publicKey publicKey: publicKey
} }; } };
remoting.xhr.start({ new remoting.Xhr({
method: 'POST', method: 'POST',
url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts', url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts',
urlParams: { urlParams: {
hostClientId: hostClientId hostClientId: hostClientId
}, },
onDone: onRegistered.bind(null, hostName, publicKey, privateKey),
jsonContent: newHostDetails, jsonContent: newHostDetails,
oauthToken: oauthToken oauthToken: oauthToken
}); }).start().then(onRegistered.bind(null, hostName, publicKey, privateKey));
} }
/** /**
......
...@@ -28,17 +28,16 @@ remoting.HostListApiImpl = function() { ...@@ -28,17 +28,16 @@ remoting.HostListApiImpl = function() {
* @param {function(!remoting.Error):void} onError * @param {function(!remoting.Error):void} onError
*/ */
remoting.HostListApiImpl.prototype.get = function(onDone, onError) { remoting.HostListApiImpl.prototype.get = function(onDone, onError) {
/** @type {function(XMLHttpRequest):void} */ /** @type {function(!remoting.Xhr.Response):void} */
var parseHostListResponse = var parseHostListResponse =
this.parseHostListResponse_.bind(this, onDone, onError); this.parseHostListResponse_.bind(this, onDone, onError);
/** @param {string} token */ /** @param {string} token */
var onToken = function(token) { var onToken = function(token) {
remoting.xhr.start({ new remoting.Xhr({
method: 'GET', method: 'GET',
url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts', url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts',
onDone: parseHostListResponse,
oauthToken: token oauthToken: token
}); }).start().then(parseHostListResponse);
}; };
remoting.identity.getToken().then(onToken, remoting.Error.handler(onError)); remoting.identity.getToken().then(onToken, remoting.Error.handler(onError));
}; };
...@@ -63,13 +62,12 @@ remoting.HostListApiImpl.prototype.put = ...@@ -63,13 +62,12 @@ remoting.HostListApiImpl.prototype.put =
'publicKey': hostPublicKey 'publicKey': hostPublicKey
} }
}; };
remoting.xhr.start({ new remoting.Xhr({
method: 'PUT', method: 'PUT',
url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId, url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId,
onDone: remoting.xhr.defaultResponse(onDone, onError),
jsonContent: newHostDetails, jsonContent: newHostDetails,
oauthToken: token oauthToken: token
}); }).start().then(remoting.Xhr.defaultResponse(onDone, onError));
}; };
remoting.identity.getToken().then(onToken, remoting.Error.handler(onError)); remoting.identity.getToken().then(onToken, remoting.Error.handler(onError));
}; };
...@@ -84,13 +82,12 @@ remoting.HostListApiImpl.prototype.put = ...@@ -84,13 +82,12 @@ remoting.HostListApiImpl.prototype.put =
remoting.HostListApiImpl.prototype.remove = function(hostId, onDone, onError) { remoting.HostListApiImpl.prototype.remove = function(hostId, onDone, onError) {
/** @param {string} token */ /** @param {string} token */
var onToken = function(token) { var onToken = function(token) {
remoting.xhr.start({ new remoting.Xhr({
method: 'DELETE', method: 'DELETE',
url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId, url: remoting.settings.DIRECTORY_API_BASE_URL + '/@me/hosts/' + hostId,
onDone: remoting.xhr.defaultResponse(onDone, onError,
[remoting.Error.Tag.NOT_FOUND]),
oauthToken: token oauthToken: token
}); }).start().then(remoting.Xhr.defaultResponse(
onDone, onError, [remoting.Error.Tag.NOT_FOUND]));
}; };
remoting.identity.getToken().then(onToken, remoting.Error.handler(onError)); remoting.identity.getToken().then(onToken, remoting.Error.handler(onError));
}; };
...@@ -102,19 +99,19 @@ remoting.HostListApiImpl.prototype.remove = function(hostId, onDone, onError) { ...@@ -102,19 +99,19 @@ remoting.HostListApiImpl.prototype.remove = function(hostId, onDone, onError) {
* *
* @param {function(Array<remoting.Host>):void} onDone * @param {function(Array<remoting.Host>):void} onDone
* @param {function(!remoting.Error):void} onError * @param {function(!remoting.Error):void} onError
* @param {XMLHttpRequest} xhr * @param {!remoting.Xhr.Response} response
* @private * @private
*/ */
remoting.HostListApiImpl.prototype.parseHostListResponse_ = remoting.HostListApiImpl.prototype.parseHostListResponse_ =
function(onDone, onError, xhr) { function(onDone, onError, response) {
if (xhr.status == 200) { if (response.status == 200) {
var response = /** @type {{data: {items: Array}}} */ var obj = /** @type {{data: {items: Array}}} */
(base.jsonParseSafe(xhr.responseText)); (base.jsonParseSafe(response.getText()));
if (!response || !response.data) { if (!obj || !obj.data) {
console.error('Invalid "hosts" response from server.'); console.error('Invalid "hosts" response from server.');
onError(remoting.Error.unexpected()); onError(remoting.Error.unexpected());
} else { } else {
var items = response.data.items || []; var items = obj.data.items || [];
var hosts = items.map( var hosts = items.map(
function(/** Object */ item) { function(/** Object */ item) {
var host = new remoting.Host(); var host = new remoting.Host();
...@@ -134,7 +131,7 @@ remoting.HostListApiImpl.prototype.parseHostListResponse_ = ...@@ -134,7 +131,7 @@ remoting.HostListApiImpl.prototype.parseHostListResponse_ =
onDone(hosts); onDone(hosts);
} }
} else { } else {
onError(remoting.Error.fromHttpStatus(xhr.status)); onError(remoting.Error.fromHttpStatus(response.status));
} }
}; };
...@@ -142,4 +139,3 @@ remoting.HostListApiImpl.prototype.parseHostListResponse_ = ...@@ -142,4 +139,3 @@ remoting.HostListApiImpl.prototype.parseHostListResponse_ =
remoting.hostListApi = new remoting.HostListApiImpl(); remoting.hostListApi = new remoting.HostListApiImpl();
})(); })();
...@@ -54,8 +54,8 @@ remoting.It2MeConnectFlow.prototype.connect_ = function(accessCode) { ...@@ -54,8 +54,8 @@ remoting.It2MeConnectFlow.prototype.connect_ = function(accessCode) {
return remoting.identity.getToken(); return remoting.identity.getToken();
}).then(function(/** string */ token) { }).then(function(/** string */ token) {
return that.getHostInfo_(token); return that.getHostInfo_(token);
}).then(function(/** XMLHttpRequest */ xhr) { }).then(function(/** !remoting.Xhr.Response */ response) {
return that.onHostInfo_(xhr); return that.onHostInfo_(response);
}).then(function(/** remoting.Host */ host) { }).then(function(/** remoting.Host */ host) {
that.sessionConnector_.connect( that.sessionConnector_.connect(
remoting.DesktopConnectedView.Mode.IT2ME, remoting.DesktopConnectedView.Mode.IT2ME,
...@@ -88,33 +88,31 @@ remoting.It2MeConnectFlow.prototype.verifyAccessCode_ = function(accessCode) { ...@@ -88,33 +88,31 @@ remoting.It2MeConnectFlow.prototype.verifyAccessCode_ = function(accessCode) {
* Continues an IT2Me connection once an access token has been obtained. * Continues an IT2Me connection once an access token has been obtained.
* *
* @param {string} token An OAuth2 access token. * @param {string} token An OAuth2 access token.
* @return {Promise<XMLHttpRequest>} * @return {Promise<!remoting.Xhr.Response>}
* @private * @private
*/ */
remoting.It2MeConnectFlow.prototype.getHostInfo_ = function(token) { remoting.It2MeConnectFlow.prototype.getHostInfo_ = function(token) {
var that = this; var that = this;
return new Promise(function(resolve) { return new remoting.Xhr({
remoting.xhr.start({
method: 'GET', method: 'GET',
url: remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' + url: remoting.settings.DIRECTORY_API_BASE_URL + '/support-hosts/' +
encodeURIComponent(that.hostId_), encodeURIComponent(that.hostId_),
onDone: resolve,
oauthToken: token oauthToken: token
}); }).start();
});
}; };
/** /**
* Continues an IT2Me connection once the host JID has been looked up. * Continues an IT2Me connection once the host JID has been looked up.
* *
* @param {XMLHttpRequest} xhr The server response to the support-hosts query. * @param {!remoting.Xhr.Response} xhrResponse The server response to the
* support-hosts query.
* @return {!Promise<!remoting.Host>} Rejects on error. * @return {!Promise<!remoting.Host>} Rejects on error.
* @private * @private
*/ */
remoting.It2MeConnectFlow.prototype.onHostInfo_ = function(xhr) { remoting.It2MeConnectFlow.prototype.onHostInfo_ = function(xhrResponse) {
if (xhr.status == 200) { if (xhrResponse.status == 200) {
var response = /** @type {{data: {jabberId: string, publicKey: string}}} */ var response = /** @type {{data: {jabberId: string, publicKey: string}}} */
(base.jsonParseSafe(xhr.responseText)); (base.jsonParseSafe(xhrResponse.getText()));
if (response && response.data && if (response && response.data &&
response.data.jabberId && response.data.publicKey) { response.data.jabberId && response.data.publicKey) {
var host = new remoting.Host(); var host = new remoting.Host();
...@@ -128,11 +126,12 @@ remoting.It2MeConnectFlow.prototype.onHostInfo_ = function(xhr) { ...@@ -128,11 +126,12 @@ remoting.It2MeConnectFlow.prototype.onHostInfo_ = function(xhr) {
return Promise.reject(remoting.Error.unexpected()); return Promise.reject(remoting.Error.unexpected());
} }
} else { } else {
return Promise.reject(translateSupportHostsError(xhr.status)); return Promise.reject(translateSupportHostsError(xhrResponse.status));
} }
}; };
/** /**
* TODO(jrw): Replace with remoting.Error.fromHttpStatus.
* @param {number} error An HTTP error code returned by the support-hosts * @param {number} error An HTTP error code returned by the support-hosts
* endpoint. * endpoint.
* @return {remoting.Error} The equivalent remoting.Error code. * @return {remoting.Error} The equivalent remoting.Error code.
......
...@@ -250,7 +250,7 @@ remoting.OAuth2.prototype.onTokens_ = ...@@ -250,7 +250,7 @@ remoting.OAuth2.prototype.onTokens_ =
remoting.OAuth2.prototype.getAuthorizationCode = function(onDone) { remoting.OAuth2.prototype.getAuthorizationCode = function(onDone) {
var xsrf_token = base.generateXsrfToken(); var xsrf_token = base.generateXsrfToken();
var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' + var GET_CODE_URL = this.getOAuth2AuthEndpoint_() + '?' +
remoting.xhr.urlencodeParamHash({ remoting.Xhr.urlencodeParamHash({
'client_id': this.getClientId_(), 'client_id': this.getClientId_(),
'redirect_uri': this.getRedirectUri_(), 'redirect_uri': this.getRedirectUri_(),
'scope': this.SCOPE_, 'scope': this.SCOPE_,
......
...@@ -75,37 +75,36 @@ remoting.OAuth2ApiImpl.prototype.interpretXhrStatus_ = ...@@ -75,37 +75,36 @@ remoting.OAuth2ApiImpl.prototype.interpretXhrStatus_ =
*/ */
remoting.OAuth2ApiImpl.prototype.refreshAccessToken = function( remoting.OAuth2ApiImpl.prototype.refreshAccessToken = function(
onDone, onError, clientId, clientSecret, refreshToken) { onDone, onError, clientId, clientSecret, refreshToken) {
/** @param {XMLHttpRequest} xhr */ /** @param {!remoting.Xhr.Response} response */
var onResponse = function(xhr) { var onResponse = function(response) {
if (xhr.status == 200) { if (response.status == 200) {
try { try {
// Don't use base.jsonParseSafe here unless you also include base.js, // Don't use base.jsonParseSafe here unless you also include base.js,
// otherwise this won't work from the OAuth trampoline. // otherwise this won't work from the OAuth trampoline.
// TODO(jamiewalch): Fix this once we're no longer using the trampoline. // TODO(jamiewalch): Fix this once we're no longer using the trampoline.
var tokens = JSON.parse(xhr.responseText); var tokens = JSON.parse(response.getText());
onDone(tokens['access_token'], tokens['expires_in']); onDone(tokens['access_token'], tokens['expires_in']);
} catch (/** @type {Error} */ err) { } catch (/** @type {Error} */ err) {
console.error('Invalid "token" response from server:', err); console.error('Invalid "token" response from server:', err);
onError(remoting.Error.unexpected()); onError(remoting.Error.unexpected());
} }
} else { } else {
console.error('Failed to refresh token. Status: ' + xhr.status + console.error('Failed to refresh token. Status: ' + response.status +
' response: ' + xhr.responseText); ' response: ' + response.getText());
onError(fromHttpStatus(xhr.status)); onError(remoting.Error.fromHttpStatus(response.status));
} }
}; };
remoting.xhr.start({ new remoting.Xhr({
method: 'POST', method: 'POST',
url: this.getOAuth2TokenEndpoint_(), url: this.getOAuth2TokenEndpoint_(),
onDone: onResponse,
formContent: { formContent: {
'client_id': clientId, 'client_id': clientId,
'client_secret': clientSecret, 'client_secret': clientSecret,
'refresh_token': refreshToken, 'refresh_token': refreshToken,
'grant_type': 'refresh_token' 'grant_type': 'refresh_token'
} }
}); }).start().then(onResponse);
}; };
/** /**
...@@ -124,14 +123,14 @@ remoting.OAuth2ApiImpl.prototype.refreshAccessToken = function( ...@@ -124,14 +123,14 @@ remoting.OAuth2ApiImpl.prototype.refreshAccessToken = function(
*/ */
remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function( remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function(
onDone, onError, clientId, clientSecret, code, redirectUri) { onDone, onError, clientId, clientSecret, code, redirectUri) {
/** @param {XMLHttpRequest} xhr */ /** @param {!remoting.Xhr.Response} response */
var onResponse = function(xhr) { var onResponse = function(response) {
if (xhr.status == 200) { if (response.status == 200) {
try { try {
// Don't use base.jsonParseSafe here unless you also include base.js, // Don't use base.jsonParseSafe here unless you also include base.js,
// otherwise this won't work from the OAuth trampoline. // otherwise this won't work from the OAuth trampoline.
// TODO(jamiewalch): Fix this once we're no longer using the trampoline. // TODO(jamiewalch): Fix this once we're no longer using the trampoline.
var tokens = JSON.parse(xhr.responseText); var tokens = JSON.parse(response.getText());
onDone(tokens['refresh_token'], onDone(tokens['refresh_token'],
tokens['access_token'], tokens['expires_in']); tokens['access_token'], tokens['expires_in']);
} catch (/** @type {Error} */ err) { } catch (/** @type {Error} */ err) {
...@@ -139,16 +138,15 @@ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function( ...@@ -139,16 +138,15 @@ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function(
onError(remoting.Error.unexpected()); onError(remoting.Error.unexpected());
} }
} else { } else {
console.error('Failed to exchange code for token. Status: ' + xhr.status + console.error('Failed to exchange code for token. Status: ' +
' response: ' + xhr.responseText); response.status + ' response: ' + response.getText());
onError(fromHttpStatus(xhr.status)); onError(remoting.Error.fromHttpStatus(response.status));
} }
}; };
remoting.xhr.start({ new remoting.Xhr({
method: 'POST', method: 'POST',
url: this.getOAuth2TokenEndpoint_(), url: this.getOAuth2TokenEndpoint_(),
onDone: onResponse,
formContent: { formContent: {
'client_id': clientId, 'client_id': clientId,
'client_secret': clientSecret, 'client_secret': clientSecret,
...@@ -156,7 +154,7 @@ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function( ...@@ -156,7 +154,7 @@ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function(
'code': code, 'code': code,
'grant_type': 'authorization_code' 'grant_type': 'authorization_code'
} }
}); }).start().then(onResponse);
}; };
/** /**
...@@ -170,28 +168,27 @@ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function( ...@@ -170,28 +168,27 @@ remoting.OAuth2ApiImpl.prototype.exchangeCodeForTokens = function(
* @return {void} Nothing. * @return {void} Nothing.
*/ */
remoting.OAuth2ApiImpl.prototype.getEmail = function(onDone, onError, token) { remoting.OAuth2ApiImpl.prototype.getEmail = function(onDone, onError, token) {
/** @param {XMLHttpRequest} xhr */ /** @param {!remoting.Xhr.Response} response */
var onResponse = function(xhr) { var onResponse = function(response) {
if (xhr.status == 200) { if (response.status == 200) {
try { try {
var result = JSON.parse(xhr.responseText); var result = JSON.parse(response.getText());
onDone(result['email']); onDone(result['email']);
} catch (/** @type {Error} */ err) { } catch (/** @type {Error} */ err) {
console.error('Invalid "userinfo" response from server:', err); console.error('Invalid "userinfo" response from server:', err);
onError(remoting.Error.unexpected()); onError(remoting.Error.unexpected());
} }
} else { } else {
console.error('Failed to get email. Status: ' + xhr.status + console.error('Failed to get email. Status: ' + response.status +
' response: ' + xhr.responseText); ' response: ' + response.getText());
onError(fromHttpStatus(xhr.status)); onError(remoting.Error.fromHttpStatus(response.status));
} }
}; };
remoting.xhr.start({ new remoting.Xhr({
method: 'GET', method: 'GET',
url: this.getOAuth2ApiUserInfoEndpoint_(), url: this.getOAuth2ApiUserInfoEndpoint_(),
onDone: onResponse,
oauthToken: token oauthToken: token
}); }).start().then(onResponse);
}; };
/** /**
...@@ -206,28 +203,27 @@ remoting.OAuth2ApiImpl.prototype.getEmail = function(onDone, onError, token) { ...@@ -206,28 +203,27 @@ remoting.OAuth2ApiImpl.prototype.getEmail = function(onDone, onError, token) {
*/ */
remoting.OAuth2ApiImpl.prototype.getUserInfo = remoting.OAuth2ApiImpl.prototype.getUserInfo =
function(onDone, onError, token) { function(onDone, onError, token) {
/** @param {XMLHttpRequest} xhr */ /** @param {!remoting.Xhr.Response} response */
var onResponse = function(xhr) { var onResponse = function(response) {
if (xhr.status == 200) { if (response.status == 200) {
try { try {
var result = JSON.parse(xhr.responseText); var result = JSON.parse(response.getText());
onDone(result['email'], result['name']); onDone(result['email'], result['name']);
} catch (/** @type {Error} */ err) { } catch (/** @type {Error} */ err) {
console.error('Invalid "userinfo" response from server:', err); console.error('Invalid "userinfo" response from server:', err);
onError(remoting.Error.unexpected()); onError(remoting.Error.unexpected());
} }
} else { } else {
console.error('Failed to get user info. Status: ' + xhr.status + console.error('Failed to get user info. Status: ' + response.status +
' response: ' + xhr.responseText); ' response: ' + response.getText());
onError(fromHttpStatus(xhr.status)); onError(remoting.Error.fromHttpStatus(response.status));
} }
}; };
remoting.xhr.start({ new remoting.Xhr({
method: 'GET', method: 'GET',
url: this.getOAuth2ApiUserInfoEndpoint_(), url: this.getOAuth2ApiUserInfoEndpoint_(),
onDone: onResponse,
oauthToken: token oauthToken: token
}); }).start().then(onResponse);
}; };
/** @returns {!remoting.Error} */ /** @returns {!remoting.Error} */
......
...@@ -75,6 +75,9 @@ remoting.SessionConnectorImpl = function(clientContainer, onConnected, onError, ...@@ -75,6 +75,9 @@ remoting.SessionConnectorImpl = function(clientContainer, onConnected, onError,
remoting.SessionConnectorImpl.prototype.resetConnection_ = function() { remoting.SessionConnectorImpl.prototype.resetConnection_ = function() {
this.closeSession(); this.closeSession();
// It's OK to initialize these member variables here because the
// constructor calls this method.
/** @private {remoting.Host} */ /** @private {remoting.Host} */
this.host_ = null; this.host_ = null;
...@@ -87,8 +90,6 @@ remoting.SessionConnectorImpl.prototype.resetConnection_ = function() { ...@@ -87,8 +90,6 @@ remoting.SessionConnectorImpl.prototype.resetConnection_ = function() {
/** @private {remoting.ClientSession} */ /** @private {remoting.ClientSession} */
this.clientSession_ = null; this.clientSession_ = null;
/** @private {XMLHttpRequest} */
this.pendingXhr_ = null;
/** @private {remoting.CredentialsProvider} */ /** @private {remoting.CredentialsProvider} */
this.credentialsProvider_ = null; this.credentialsProvider_ = null;
......
...@@ -132,7 +132,7 @@ remoting.ThirdPartyTokenFetcher.prototype.parseRedirectUrl_ = ...@@ -132,7 +132,7 @@ remoting.ThirdPartyTokenFetcher.prototype.parseRedirectUrl_ =
* @private * @private
*/ */
remoting.ThirdPartyTokenFetcher.prototype.getFullTokenUrl_ = function() { remoting.ThirdPartyTokenFetcher.prototype.getFullTokenUrl_ = function() {
return this.tokenUrl_ + '?' + remoting.xhr.urlencodeParamHash({ return this.tokenUrl_ + '?' + remoting.Xhr.urlencodeParamHash({
'redirect_uri': this.redirectUri_, 'redirect_uri': this.redirectUri_,
'scope': this.tokenScope_, 'scope': this.tokenScope_,
'client_id': this.hostPublicKey_, 'client_id': this.hostPublicKey_,
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
/** /**
* @fileoverview * @fileoverview
* Simple utilities for making XHRs more pleasant. * Utility class for making XHRs more pleasant.
*/ */
'use strict'; 'use strict';
...@@ -12,26 +12,83 @@ ...@@ -12,26 +12,83 @@
/** @suppress {duplicate} */ /** @suppress {duplicate} */
var remoting = remoting || {}; var remoting = remoting || {};
/** Namespace for XHR functions */
/** @type {Object} */
remoting.xhr = remoting.xhr || {};
/** /**
* Takes an associative array of parameters and urlencodes it. * @constructor
* * @param {remoting.Xhr.Params} params
* @param {Object<string,string>} paramHash The parameter key/value pairs.
* @return {string} URLEncoded version of paramHash.
*/ */
remoting.xhr.urlencodeParamHash = function(paramHash) { remoting.Xhr = function(params) {
var paramArray = []; /** @private @const {!XMLHttpRequest} */
for (var key in paramHash) { this.nativeXhr_ = new XMLHttpRequest();
paramArray.push(encodeURIComponent(key) + this.nativeXhr_.onreadystatechange = this.onReadyStateChange_.bind(this);
'=' + encodeURIComponent(paramHash[key])); this.nativeXhr_.withCredentials = params.withCredentials || false;
/** @private @const */
this.responseType_ = params.responseType || remoting.Xhr.ResponseType.TEXT;
// Apply URL parameters.
var url = params.url;
var parameterString = '';
if (typeof(params.urlParams) === 'string') {
parameterString = params.urlParams;
} else if (typeof(params.urlParams) === 'object') {
parameterString = remoting.Xhr.urlencodeParamHash(
remoting.Xhr.removeNullFields_(params.urlParams));
} }
if (paramArray.length > 0) { if (parameterString) {
return paramArray.join('&'); base.debug.assert(url.indexOf('?') == -1);
url += '?' + parameterString;
} }
return '';
// Check that the content spec is consistent.
if ((Number(params.textContent !== undefined) +
Number(params.formContent !== undefined) +
Number(params.jsonContent !== undefined)) > 1) {
throw new Error(
'may only specify one of textContent, formContent, and jsonContent');
}
// Prepare the build modified headers.
var headers = remoting.Xhr.removeNullFields_(params.headers);
// Convert the content fields to a single text content variable.
/** @private {?string} */
this.content_ = null;
if (params.textContent !== undefined) {
this.content_ = params.textContent;
} else if (params.formContent !== undefined) {
if (!('Content-type' in headers)) {
headers['Content-type'] = 'application/x-www-form-urlencoded';
}
this.content_ = remoting.Xhr.urlencodeParamHash(params.formContent);
} else if (params.jsonContent !== undefined) {
if (!('Content-type' in headers)) {
headers['Content-type'] = 'application/json; charset=UTF-8';
}
this.content_ = JSON.stringify(params.jsonContent);
}
// Apply the oauthToken field.
if (params.oauthToken !== undefined) {
base.debug.assert(!('Authorization' in headers));
headers['Authorization'] = 'Bearer ' + params.oauthToken;
}
this.nativeXhr_.open(params.method, url, true);
for (var key in headers) {
this.nativeXhr_.setRequestHeader(key, headers[key]);
}
/** @private {base.Deferred<!remoting.Xhr.Response>} */
this.deferred_ = null;
};
/**
* @enum {string}
*/
remoting.Xhr.ResponseType = {
TEXT: 'TEXT', // Request a plain text response (default).
JSON: 'JSON', // Request a JSON response.
NONE: 'NONE' // Don't request any response.
}; };
/** /**
...@@ -41,8 +98,6 @@ remoting.xhr.urlencodeParamHash = function(paramHash) { ...@@ -41,8 +98,6 @@ remoting.xhr.urlencodeParamHash = function(paramHash) {
* *
* url: The URL to request. * url: The URL to request.
* *
* onDone: Function to call when the XHR finishes.
* urlParams: (optional) Parameters to be appended to the URL. * urlParams: (optional) Parameters to be appended to the URL.
* Null-valued parameters are omitted. * Null-valued parameters are omitted.
* *
...@@ -64,20 +119,118 @@ remoting.xhr.urlencodeParamHash = function(paramHash) { ...@@ -64,20 +119,118 @@ remoting.xhr.urlencodeParamHash = function(paramHash) {
* oauthToken: (optional) An OAuth2 token used to construct an * oauthToken: (optional) An OAuth2 token used to construct an
* Authentication header. * Authentication header.
* *
* responseType: (optional) Request a response of a specific
* type. Default: TEXT.
*
* @typedef {{ * @typedef {{
* method: string, * method: string,
* url:string, * url:string,
* onDone:(function(XMLHttpRequest):void),
* urlParams:(string|Object<string,?string>|undefined), * urlParams:(string|Object<string,?string>|undefined),
* textContent:(string|undefined), * textContent:(string|undefined),
* formContent:(Object|undefined), * formContent:(Object|undefined),
* jsonContent:(*|undefined), * jsonContent:(*|undefined),
* headers:(Object<string,?string>|undefined), * headers:(Object<string,?string>|undefined),
* withCredentials:(boolean|undefined), * withCredentials:(boolean|undefined),
* oauthToken:(string|undefined) * oauthToken:(string|undefined),
* responseType:(remoting.Xhr.ResponseType|undefined)
* }} * }}
*/ */
remoting.XhrParams; remoting.Xhr.Params;
/**
* Aborts the HTTP request. Does nothing is the request has finished
* already.
*/
remoting.Xhr.prototype.abort = function() {
this.nativeXhr_.abort();
};
/**
* Starts and HTTP request and gets a promise that is resolved when
* the request completes.
*
* Any error that prevents receiving an HTTP status
* code causes this promise to be rejected.
*
* NOTE: Calling this method more than once will return the same
* promise and not start a new request, despite what the name
* suggests.
*
* @return {!Promise<!remoting.Xhr.Response>}
*/
remoting.Xhr.prototype.start = function() {
if (this.deferred_ == null) {
var xhr = this.nativeXhr_;
xhr.send(this.content_);
this.content_ = null; // for gc
this.deferred_ = new base.Deferred();
}
return this.deferred_.promise();
};
/**
* @private
*/
remoting.Xhr.prototype.onReadyStateChange_ = function() {
var xhr = this.nativeXhr_;
if (xhr.readyState == 4) {
// See comments at remoting.Xhr.Response.
this.deferred_.resolve(new remoting.Xhr.Response(xhr, this.responseType_));
}
};
/**
* The response-related parts of an XMLHttpRequest. Note that this
* class is not just a facade for XMLHttpRequest; it saves the value
* of the |responseText| field becuase once onReadyStateChange_
* (above) returns, the value of |responseText| is reset to the empty
* string! This is a documented anti-feature of the XMLHttpRequest
* API.
*
* @constructor
* @param {!XMLHttpRequest} xhr
* @param {remoting.Xhr.ResponseType} type
*/
remoting.Xhr.Response = function(xhr, type) {
/** @private @const */
this.type_ = type;
/**
* The HTTP status code.
* @const {number}
*/
this.status = xhr.status;
/**
* The HTTP status description.
* @const {string}
*/
this.statusText = xhr.statusText;
/**
* The response URL, if any.
* @const {?string}
*/
this.url = xhr.responseURL;
/** @private {string} */
this.text_ = xhr.responseText || '';
};
/**
* @return {string} The text content of the response.
*/
remoting.Xhr.Response.prototype.getText = function() {
return this.text_;
};
/**
* @return {*} The parsed JSON content of the response.
*/
remoting.Xhr.Response.prototype.getJson = function() {
base.debug.assert(this.type_ == remoting.Xhr.ResponseType.JSON);
return JSON.parse(this.text_);
};
/** /**
* Returns a copy of the input object with all null or undefined * Returns a copy of the input object with all null or undefined
...@@ -87,7 +240,7 @@ remoting.XhrParams; ...@@ -87,7 +240,7 @@ remoting.XhrParams;
* @return {!Object<string,string>} * @return {!Object<string,string>}
* @private * @private
*/ */
remoting.xhr.removeNullFields_ = function(input) { remoting.Xhr.removeNullFields_ = function(input) {
/** @type {!Object<string,string>} */ /** @type {!Object<string,string>} */
var result = {}; var result = {};
if (input) { if (input) {
...@@ -102,112 +255,38 @@ remoting.xhr.removeNullFields_ = function(input) { ...@@ -102,112 +255,38 @@ remoting.xhr.removeNullFields_ = function(input) {
}; };
/** /**
* Executes an arbitrary HTTP method asynchronously. * Takes an associative array of parameters and urlencodes it.
* *
* @param {remoting.XhrParams} params * @param {Object<string,string>} paramHash The parameter key/value pairs.
* @return {XMLHttpRequest} The XMLHttpRequest object. * @return {string} URLEncoded version of paramHash.
*/
remoting.xhr.start = function(params) {
// Extract fields that can be used more or less as-is.
var method = params.method;
var url = params.url;
var onDone = params.onDone;
var headers = remoting.xhr.removeNullFields_(params.headers);
var withCredentials = params.withCredentials || false;
// Apply URL parameters.
var parameterString = '';
if (typeof(params.urlParams) === 'string') {
parameterString = params.urlParams;
} else if (typeof(params.urlParams) === 'object') {
parameterString = remoting.xhr.urlencodeParamHash(
remoting.xhr.removeNullFields_(params.urlParams));
}
if (parameterString) {
base.debug.assert(url.indexOf('?') == -1);
url += '?' + parameterString;
}
// Check that the content spec is consistent.
if ((Number(params.textContent !== undefined) +
Number(params.formContent !== undefined) +
Number(params.jsonContent !== undefined)) > 1) {
throw new Error(
'may only specify one of textContent, formContent, and jsonContent');
}
// Convert the content fields to a single text content variable.
/** @type {?string} */
var content = null;
if (params.textContent !== undefined) {
content = params.textContent;
} else if (params.formContent !== undefined) {
if (!('Content-type' in headers)) {
headers['Content-type'] = 'application/x-www-form-urlencoded';
}
content = remoting.xhr.urlencodeParamHash(params.formContent);
} else if (params.jsonContent !== undefined) {
if (!('Content-type' in headers)) {
headers['Content-type'] = 'application/json; charset=UTF-8';
}
content = JSON.stringify(params.jsonContent);
}
// Apply the oauthToken field.
if (params.oauthToken !== undefined) {
base.debug.assert(!('Authorization' in headers));
headers['Authorization'] = 'Bearer ' + params.oauthToken;
}
return remoting.xhr.startInternal_(
method, url, onDone, content, headers, withCredentials);
};
/**
* Executes an arbitrary HTTP method asynchronously.
*
* @param {string} method
* @param {string} url
* @param {function(XMLHttpRequest):void} onDone
* @param {?string} content
* @param {!Object<string,string>} headers
* @param {boolean} withCredentials
* @return {XMLHttpRequest} The XMLHttpRequest object.
* @private
*/ */
remoting.xhr.startInternal_ = function( remoting.Xhr.urlencodeParamHash = function(paramHash) {
method, url, onDone, content, headers, withCredentials) { var paramArray = [];
/** @type {XMLHttpRequest} */ for (var key in paramHash) {
var xhr = new XMLHttpRequest(); paramArray.push(encodeURIComponent(key) +
xhr.onreadystatechange = function() { '=' + encodeURIComponent(paramHash[key]));
if (xhr.readyState != 4) {
return;
} }
onDone(xhr); if (paramArray.length > 0) {
}; return paramArray.join('&');
xhr.open(method, url, true);
for (var key in headers) {
xhr.setRequestHeader(key, headers[key]);
} }
xhr.withCredentials = withCredentials; return '';
xhr.send(content);
return xhr;
}; };
/** /**
* Generic success/failure response proxy. * Generic success/failure response proxy.
* *
* TODO(jrw): Stop using this and move default error handling directly
* into Xhr class.
*
* @param {function():void} onDone * @param {function():void} onDone
* @param {function(!remoting.Error):void} onError * @param {function(!remoting.Error):void} onError
* @param {Array<remoting.Error.Tag>=} opt_ignoreErrors * @param {Array<remoting.Error.Tag>=} opt_ignoreErrors
* @return {function(XMLHttpRequest):void} * @return {function(!remoting.Xhr.Response):void}
*/ */
remoting.xhr.defaultResponse = function(onDone, onError, opt_ignoreErrors) { remoting.Xhr.defaultResponse = function(onDone, onError, opt_ignoreErrors) {
/** @param {XMLHttpRequest} xhr */ /** @param {!remoting.Xhr.Response} response */
var result = function(xhr) { var result = function(response) {
var error = var error = remoting.Error.fromHttpStatus(response.status);
remoting.Error.fromHttpStatus(/** @type {number} */ (xhr.status));
if (error.isNone()) { if (error.isNone()) {
onDone(); onDone();
return; return;
......
...@@ -4,193 +4,185 @@ ...@@ -4,193 +4,185 @@
/** /**
* @fileoverview * @fileoverview
* @suppress {checkTypes|checkVars|reportUnknownTypes}
*/ */
(function() { (function() {
'use strict'; 'use strict';
QUnit.module('xhr');
/** @type {sinon.FakeXhr} */
var fakeXhr;
QUnit.module('xhr', {
beforeEach: function() {
fakeXhr = null;
sinon.useFakeXMLHttpRequest().onCreate =
function(/** sinon.FakeXhr */ xhr) {
fakeXhr = xhr;
};
}
});
QUnit.test('urlencodeParamHash', function(assert) { QUnit.test('urlencodeParamHash', function(assert) {
assert.equal( assert.equal(
remoting.xhr.urlencodeParamHash({}), remoting.Xhr.urlencodeParamHash({}),
''); '');
assert.equal( assert.equal(
remoting.xhr.urlencodeParamHash({'key': 'value'}), remoting.Xhr.urlencodeParamHash({'key': 'value'}),
'key=value'); 'key=value');
assert.equal( assert.equal(
remoting.xhr.urlencodeParamHash({'key /?=&': 'value /?=&'}), remoting.Xhr.urlencodeParamHash({'key /?=&': 'value /?=&'}),
'key%20%2F%3F%3D%26=value%20%2F%3F%3D%26'); 'key%20%2F%3F%3D%26=value%20%2F%3F%3D%26');
assert.equal( assert.equal(
remoting.xhr.urlencodeParamHash({'k1': 'v1', 'k2': 'v2'}), remoting.Xhr.urlencodeParamHash({'k1': 'v1', 'k2': 'v2'}),
'k1=v1&k2=v2'); 'k1=v1&k2=v2');
}); });
QUnit.test('basic GET', function(assert) { QUnit.test('basic GET', function(assert) {
sinon.useFakeXMLHttpRequest(); var promise = new remoting.Xhr({
var done = assert.async();
var request = remoting.xhr.start({
method: 'GET', method: 'GET',
url: 'http://foo.com', url: 'http://foo.com',
onDone: function(xhr) { responseType: remoting.Xhr.ResponseType.TEXT
assert.ok(xhr === request); }).start().then(function(response) {
assert.equal(xhr.status, 200); assert.equal(response.status, 200);
assert.equal(xhr.responseText, 'body'); assert.equal(response.getText(), 'body');
done();
}
}); });
assert.equal(request.method, 'GET'); assert.equal(fakeXhr.method, 'GET');
assert.equal(request.url, 'http://foo.com'); assert.equal(fakeXhr.url, 'http://foo.com');
assert.equal(request.withCredentials, false); assert.equal(fakeXhr.withCredentials, false);
assert.equal(request.requestBody, null); assert.equal(fakeXhr.requestBody, null);
assert.ok(!('Content-type' in request.requestHeaders)); assert.ok(!('Content-type' in fakeXhr.requestHeaders));
request.respond(200, {}, 'body'); fakeXhr.respond(200, {}, 'body');
return promise;
}); });
QUnit.test('GET with param string', function(assert) { QUnit.test('GET with param string', function(assert) {
var done = assert.async(); var promise = new remoting.Xhr({
sinon.useFakeXMLHttpRequest();
var request = remoting.xhr.start({
method: 'GET', method: 'GET',
url: 'http://foo.com', url: 'http://foo.com',
onDone: function(xhr) { responseType: remoting.Xhr.ResponseType.TEXT,
assert.ok(xhr === request);
assert.equal(xhr.status, 200);
assert.equal(xhr.responseText, 'body');
done();
},
urlParams: 'the_param_string' urlParams: 'the_param_string'
}).start().then(function(response) {
assert.equal(response.status, 200);
assert.equal(response.getText(), 'body');
}); });
assert.equal(request.method, 'GET'); assert.equal(fakeXhr.method, 'GET');
assert.equal(request.url, 'http://foo.com?the_param_string'); assert.equal(fakeXhr.url, 'http://foo.com?the_param_string');
assert.equal(request.withCredentials, false); assert.equal(fakeXhr.withCredentials, false);
assert.equal(request.requestBody, null); assert.equal(fakeXhr.requestBody, null);
assert.ok(!('Content-type' in request.requestHeaders)); assert.ok(!('Content-type' in fakeXhr.requestHeaders));
request.respond(200, {}, 'body'); fakeXhr.respond(200, {}, 'body');
return promise;
}); });
QUnit.test('GET with param object', function(assert) { QUnit.test('GET with param object', function(assert) {
var done = assert.async(); var promise = new remoting.Xhr({
sinon.useFakeXMLHttpRequest();
var request = remoting.xhr.start({
method: 'GET', method: 'GET',
url: 'http://foo.com', url: 'http://foo.com',
onDone: function(xhr) { responseType: remoting.Xhr.ResponseType.TEXT,
assert.ok(xhr === request);
assert.equal(xhr.status, 200);
assert.equal(xhr.responseText, 'body');
done();
},
urlParams: {'a': 'b', 'c': 'd'} urlParams: {'a': 'b', 'c': 'd'}
}).start().then(function(response) {
assert.equal(response.status, 200);
assert.equal(response.getText(), 'body');
}); });
assert.equal(request.method, 'GET'); assert.equal(fakeXhr.method, 'GET');
assert.equal(request.url, 'http://foo.com?a=b&c=d'); assert.equal(fakeXhr.url, 'http://foo.com?a=b&c=d');
assert.equal(request.withCredentials, false); assert.equal(fakeXhr.withCredentials, false);
assert.equal(request.requestBody, null); assert.equal(fakeXhr.requestBody, null);
assert.ok(!('Content-type' in request.requestHeaders)); assert.ok(!('Content-type' in fakeXhr.requestHeaders));
request.respond(200, {}, 'body'); fakeXhr.respond(200, {}, 'body');
return promise;
}); });
QUnit.test('GET with headers', function(assert) { QUnit.test('GET with headers', function(assert) {
sinon.useFakeXMLHttpRequest(); var promise = new remoting.Xhr({
var done = assert.async();
var request = remoting.xhr.start({
method: 'GET', method: 'GET',
url: 'http://foo.com', url: 'http://foo.com',
onDone: function(xhr) { responseType: remoting.Xhr.ResponseType.TEXT,
assert.ok(xhr === request);
assert.equal(xhr.status, 200);
assert.equal(xhr.responseText, 'body');
done();
},
headers: {'Header1': 'headerValue1', 'Header2': 'headerValue2'} headers: {'Header1': 'headerValue1', 'Header2': 'headerValue2'}
}).start().then(function(response) {
assert.equal(response.status, 200);
assert.equal(response.getText(), 'body');
}); });
assert.equal(request.method, 'GET'); assert.equal(fakeXhr.method, 'GET');
assert.equal(request.url, 'http://foo.com'); assert.equal(fakeXhr.url, 'http://foo.com');
assert.equal(request.withCredentials, false); assert.equal(fakeXhr.withCredentials, false);
assert.equal(request.requestBody, null); assert.equal(fakeXhr.requestBody, null);
assert.equal( assert.equal(
request.requestHeaders['Header1'], fakeXhr.requestHeaders['Header1'],
'headerValue1'); 'headerValue1');
assert.equal( assert.equal(
request.requestHeaders['Header2'], fakeXhr.requestHeaders['Header2'],
'headerValue2'); 'headerValue2');
assert.ok(!('Content-type' in request.requestHeaders)); assert.ok(!('Content-type' in fakeXhr.requestHeaders));
request.respond(200, {}, 'body'); fakeXhr.respond(200, {}, 'body');
return promise;
}); });
QUnit.test('GET with credentials', function(assert) { QUnit.test('GET with credentials', function(assert) {
sinon.useFakeXMLHttpRequest(); var promise = new remoting.Xhr({
var done = assert.async();
var request = remoting.xhr.start({
method: 'GET', method: 'GET',
url: 'http://foo.com', url: 'http://foo.com',
onDone: function(xhr) { responseType: remoting.Xhr.ResponseType.TEXT,
assert.ok(xhr === request);
assert.equal(xhr.status, 200);
assert.equal(xhr.responseText, 'body');
done();
},
withCredentials: true withCredentials: true
}).start().then(function(response) {
assert.equal(response.status, 200);
assert.equal(response.getText(), 'body');
}); });
assert.equal(request.method, 'GET'); assert.equal(fakeXhr.method, 'GET');
assert.equal(request.url, 'http://foo.com'); assert.equal(fakeXhr.url, 'http://foo.com');
assert.equal(request.withCredentials, true); assert.equal(fakeXhr.withCredentials, true);
assert.equal(request.requestBody, null); assert.equal(fakeXhr.requestBody, null);
assert.ok(!('Content-type' in request.requestHeaders)); assert.ok(!('Content-type' in fakeXhr.requestHeaders));
request.respond(200, {}, 'body'); fakeXhr.respond(200, {}, 'body');
return promise;
}); });
QUnit.test('POST with text content', function(assert) { QUnit.test('POST with text content', function(assert) {
sinon.useFakeXMLHttpRequest();
var done = assert.async(); var done = assert.async();
var request = remoting.xhr.start({
var promise = new remoting.Xhr({
method: 'POST', method: 'POST',
url: 'http://foo.com', url: 'http://foo.com',
onDone: function(xhr) { responseType: remoting.Xhr.ResponseType.TEXT,
assert.ok(xhr === request);
assert.equal(xhr.status, 200);
assert.equal(xhr.responseText, 'body');
done();
},
textContent: 'the_content_string' textContent: 'the_content_string'
}).start().then(function(response) {
assert.equal(response.status, 200);
assert.equal(response.getText(), 'body');
done();
}); });
assert.equal(request.method, 'POST'); assert.equal(fakeXhr.method, 'POST');
assert.equal(request.url, 'http://foo.com'); assert.equal(fakeXhr.url, 'http://foo.com');
assert.equal(request.withCredentials, false); assert.equal(fakeXhr.withCredentials, false);
assert.equal(request.requestBody, 'the_content_string'); assert.equal(fakeXhr.requestBody, 'the_content_string');
assert.ok(!('Content-type' in request.requestHeaders)); assert.ok(!('Content-type' in fakeXhr.requestHeaders));
request.respond(200, {}, 'body'); fakeXhr.respond(200, {}, 'body');
return promise;
}); });
QUnit.test('POST with form content', function(assert) { QUnit.test('POST with form content', function(assert) {
sinon.useFakeXMLHttpRequest(); var promise = new remoting.Xhr({
var done = assert.async();
var request = remoting.xhr.start({
method: 'POST', method: 'POST',
url: 'http://foo.com', url: 'http://foo.com',
onDone: function(xhr) { responseType: remoting.Xhr.ResponseType.TEXT,
assert.ok(xhr === request);
assert.equal(xhr.status, 200);
assert.equal(xhr.responseText, 'body');
done();
},
formContent: {'a': 'b', 'c': 'd'} formContent: {'a': 'b', 'c': 'd'}
}).start().then(function(response) {
assert.equal(response.status, 200);
assert.equal(response.getText(), 'body');
}); });
assert.equal(request.method, 'POST'); assert.equal(fakeXhr.method, 'POST');
assert.equal(request.url, 'http://foo.com'); assert.equal(fakeXhr.url, 'http://foo.com');
assert.equal(request.withCredentials, false); assert.equal(fakeXhr.withCredentials, false);
assert.equal(request.requestBody, 'a=b&c=d'); assert.equal(fakeXhr.requestBody, 'a=b&c=d');
assert.equal( assert.equal(
request.requestHeaders['Content-type'], fakeXhr.requestHeaders['Content-type'],
'application/x-www-form-urlencoded'); 'application/x-www-form-urlencoded');
request.respond(200, {}, 'body'); fakeXhr.respond(200, {}, 'body');
return promise;
}); });
QUnit.test('defaultResponse 200', function(assert) { QUnit.test('defaultResponse 200', function(assert) {
sinon.useFakeXMLHttpRequest();
var done = assert.async(); var done = assert.async();
var onDone = function() { var onDone = function() {
...@@ -203,18 +195,17 @@ QUnit.test('defaultResponse 200', function(assert) { ...@@ -203,18 +195,17 @@ QUnit.test('defaultResponse 200', function(assert) {
done(); done();
}; };
var request = remoting.xhr.start({ new remoting.Xhr({
method: 'POST', method: 'POST',
url: 'http://foo.com', url: 'http://foo.com'
onDone: remoting.xhr.defaultResponse(onDone, onError) }).start().then(remoting.Xhr.defaultResponse(onDone, onError));
}); fakeXhr.respond(200, {}, '');
request.respond(200);
}); });
QUnit.test('defaultResponse 404', function(assert) { QUnit.test('defaultResponse 404', function(assert) {
sinon.useFakeXMLHttpRequest();
var done = assert.async(); var done = assert.async();
var onDone = function() { var onDone = function() {
assert.ok(false); assert.ok(false);
done(); done();
...@@ -225,12 +216,11 @@ QUnit.test('defaultResponse 404', function(assert) { ...@@ -225,12 +216,11 @@ QUnit.test('defaultResponse 404', function(assert) {
done(); done();
}; };
var request = remoting.xhr.start({ new remoting.Xhr({
method: 'POST', method: 'POST',
url: 'http://foo.com', url: 'http://foo.com'
onDone: remoting.xhr.defaultResponse(onDone, onError) }).start().then(remoting.Xhr.defaultResponse(onDone, onError));
}); fakeXhr.respond(404, {}, '');
request.respond(404);
}); });
})(); })();
...@@ -126,3 +126,36 @@ sinon.TestStub.prototype.onFirstCall = function() {}; ...@@ -126,3 +126,36 @@ sinon.TestStub.prototype.onFirstCall = function() {};
/** @returns {Object} */ /** @returns {Object} */
sinon.createStubInstance = function (/** * */ constructor) {}; sinon.createStubInstance = function (/** * */ constructor) {};
/** @return {sinon.FakeXhr} */
sinon.useFakeXMLHttpRequest = function() {};
/** @interface */
sinon.FakeXhr = function() {};
/** @type {string} */
sinon.FakeXhr.prototype.method;
/** @type {string} */
sinon.FakeXhr.prototype.url;
/** @type {boolean} */
sinon.FakeXhr.prototype.withCredentials;
/** @type {?string} */
sinon.FakeXhr.prototype.requestBody;
/** @type {!Object<string,string>} */
sinon.FakeXhr.prototype.requestHeaders;
/**
* @param {number} status
* @param {!Object<string,string>} headers
* @param {?string} content
*/
sinon.FakeXhr.prototype.respond;
/**
* @type {?function(!sinon.FakeXhr)}
*/
sinon.FakeXhr.prototype.onCreate;
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment