commit 18349b22322f7fd763777f152a8fe037d1a7c3e8
parent 01fddc9bb9d9070b4d57d20c2f8eff21f2ec5cde
Author: Dan Stillman <dstillman@zotero.org>
Date: Wed, 9 Dec 2015 04:07:48 -0500
Restore certificate checking for API syncing errors
Closes #864
This adds a 'channel' property to Zotero.HTTP.UnexpectedStatusException,
because the 'channel' property of the XHR can be garbage-collected
before handling, and the channel's 'securityInfo' property is necessary
to detect certificate errors.
Diffstat:
5 files changed, 139 insertions(+), 35 deletions(-)
diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js
@@ -12,6 +12,7 @@ Zotero.HTTP = new function() {
this.UnexpectedStatusException = function(xmlhttp, msg) {
this.xmlhttp = xmlhttp;
this.status = xmlhttp.status;
+ this.channel = xmlhttp.channel;
this.message = msg;
// Hide password from debug output
diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js
@@ -754,40 +754,7 @@ Zotero.Sync.Server = new function () {
function _checkResponse(xmlhttp, noReloadOnFailure) {
- if (!xmlhttp.responseText) {
- var channel = xmlhttp.channel;
- // Check SSL cert
- if (channel) {
- var secInfo = channel.securityInfo;
- if (secInfo instanceof Ci.nsITransportSecurityInfo) {
- secInfo.QueryInterface(Ci.nsITransportSecurityInfo);
- if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE) == Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
- var url = channel.name;
- var ios = Components.classes["@mozilla.org/network/io-service;1"].
- getService(Components.interfaces.nsIIOService);
- try {
- var uri = ios.newURI(url, null, null);
- var host = uri.host;
- }
- catch (e) {
- Zotero.debug(e);
- }
- var kbURL = 'https://zotero.org/support/kb/ssl_certificate_error';
- _error(Zotero.getString('sync.storage.error.webdav.sslCertificateError', host) + "\n\n"
- + Zotero.getString('general.seeForMoreInformation', kbURL),
- false, noReloadOnFailure);
- }
- else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) == Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
- _error(Zotero.getString('sync.error.sslConnectionError'), false, noReloadOnFailure);
- }
- }
- }
- if (xmlhttp.status === 0) {
- _error(Zotero.getString('sync.error.checkConnection'), false, noReloadOnFailure);
- }
- _error(Zotero.getString('sync.error.emptyResponseServer') + Zotero.getString('general.tryAgainLater'),
- false, noReloadOnFailure);
- }
+
if (!xmlhttp.responseXML || !xmlhttp.responseXML.childNodes[0] ||
xmlhttp.responseXML.childNodes[0].tagName != 'response' ||
diff --git a/chrome/content/zotero/xpcom/sync/syncAPIClient.js b/chrome/content/zotero/xpcom/sync/syncAPIClient.js
@@ -524,6 +524,7 @@ Zotero.Sync.APIClient.prototype = {
catch (e) {
tries++;
if (e instanceof Zotero.HTTP.UnexpectedStatusException) {
+ this._checkConnection(e.xmlhttp, e.channel);
//this._checkRetry(e.xmlhttp);
if (e.is5xx()) {
@@ -566,6 +567,70 @@ Zotero.Sync.APIClient.prototype = {
},
+ /**
+ * Check connection for certificate errors, interruptions, and empty responses and
+ * throw an appropriate error
+ */
+ _checkConnection: function (xmlhttp, channel) {
+ const Ci = Components.interfaces;
+
+ if (!xmlhttp.responseText) {
+ let msg = null;
+ let dialogButtonText = null;
+ let dialogButtonCallback = null;
+
+ // Check SSL cert
+ if (channel) {
+ let secInfo = channel.securityInfo;
+ if (secInfo instanceof Ci.nsITransportSecurityInfo) {
+ secInfo.QueryInterface(Ci.nsITransportSecurityInfo);
+ if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_INSECURE)
+ == Ci.nsIWebProgressListener.STATE_IS_INSECURE) {
+ let url = channel.name;
+ let ios = Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ try {
+ var uri = ios.newURI(url, null, null);
+ var host = uri.host;
+ }
+ catch (e) {
+ Zotero.debug(e);
+ }
+ let kbURL = 'https://www.zotero.org/support/kb/ssl_certificate_error';
+ msg = Zotero.getString('sync.storage.error.webdav.sslCertificateError', host);
+ dialogButtonText = Zotero.getString('general.moreInformation');
+ dialogButtonCallback = function () {
+ let wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ let win = wm.getMostRecentWindow("navigator:browser");
+ win.ZoteroPane.loadURI(kbURL, { metaKey: true, shiftKey: true });
+ };
+ }
+ else if ((secInfo.securityState & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
+ == Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
+ msg = Zotero.getString('sync.error.sslConnectionError');
+ }
+ }
+ }
+ if (!msg && xmlhttp.status === 0) {
+ msg = Zotero.getString('sync.error.checkConnection');
+ }
+ if (!msg) {
+ msg = Zotero.getString('sync.error.emptyResponseServer')
+ + Zotero.getString('general.tryAgainLater');
+ }
+ throw new Zotero.Error(
+ msg,
+ 0,
+ {
+ dialogButtonText,
+ dialogButtonCallback
+ }
+ );
+ }
+ },
+
+
_checkBackoff: function (xmlhttp) {
var backoff = xmlhttp.getResponseHeader("Backoff");
if (backoff) {
diff --git a/test/content/support.js b/test/content/support.js
@@ -710,7 +710,7 @@ function setHTTPResponse(server, baseURL, response, responses) {
response = responses[topic][key];
}
- var responseArray = [response.status || 200, {}, ""];
+ var responseArray = [response.status !== undefined ? response.status : 200, {}, ""];
if (response.json) {
responseArray[1]["Content-Type"] = "application/json";
responseArray[2] = JSON.stringify(response.json);
diff --git a/test/tests/syncAPIClientTest.js b/test/tests/syncAPIClientTest.js
@@ -0,0 +1,71 @@
+"use strict";
+
+describe("Zotero.Sync.APIClient", function () {
+ Components.utils.import("resource://zotero/config.js");
+
+ var apiKey = Zotero.Utilities.randomString(24);
+ var baseURL = "http://local.zotero/";
+ var server, client;
+
+ function setResponse(response) {
+ setHTTPResponse(server, baseURL, response, {});
+ }
+
+ before(function () {
+ Components.utils.import("resource://zotero/concurrentCaller.js");
+ var caller = new ConcurrentCaller(1);
+ caller.setLogger(msg => Zotero.debug(msg));
+ caller.stopOnError = true;
+ caller.onError = function (e) {
+ Zotero.logError(e);
+ if (e.fatal) {
+ caller.stop();
+ throw e;
+ }
+ };
+
+ Zotero.HTTP.mock = sinon.FakeXMLHttpRequest;
+
+ client = new Zotero.Sync.APIClient({
+ baseURL,
+ apiVersion: ZOTERO_CONFIG.API_VERSION,
+ apiKey,
+ caller
+ })
+ })
+
+ beforeEach(function () {
+ server = sinon.fakeServer.create();
+ server.autoRespond = true;
+ })
+
+ after(function () {
+ Zotero.HTTP.mock = null;
+ })
+
+ describe("#_checkConnection()", function () {
+ it("should catch an error with an empty response", function* () {
+ setResponse({
+ method: "GET",
+ url: "error",
+ status: 500,
+ text: ""
+ })
+ var e = yield getPromiseError(client.makeRequest("GET", baseURL + "error"));
+ assert.ok(e);
+ assert.isTrue(e.message.startsWith(Zotero.getString('sync.error.emptyResponseServer')));
+ })
+
+ it("should catch an interrupted connection", function* () {
+ setResponse({
+ method: "GET",
+ url: "empty",
+ status: 0,
+ text: ""
+ })
+ var e = yield getPromiseError(client.makeRequest("GET", baseURL + "empty"));
+ assert.ok(e);
+ assert.equal(e.message, Zotero.getString('sync.error.checkConnection'));
+ })
+ })
+})