diff --git a/chrome/content/zotero/xpcom/http.js b/chrome/content/zotero/xpcom/http.js index 708bc2ac00..28e2c63598 100644 --- a/chrome/content/zotero/xpcom/http.js +++ b/chrome/content/zotero/xpcom/http.js @@ -201,6 +201,8 @@ Zotero.HTTP = new function() { // Send cookie even if "Allow third-party cookies" is disabled (>=Fx3.6 only) var channel = xmlhttp.channel, isFile = channel instanceof Components.interfaces.nsIFileChannel; + var redirectStatus; + var redirectLocation; if(channel instanceof Components.interfaces.nsIHttpChannelInternal) { channel.forceAllowThirdPartyCookie = true; @@ -218,8 +220,22 @@ Zotero.HTTP = new function() { if (options.dontCache) { channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; } + + // Don't follow redirects + if (options.followRedirects === false) { + channel.notificationCallbacks = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor, Ci.nsIChannelEventSync]), + getInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]), + asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) { + redirectStatus = (flags & Ci.nsIChannelEventSink.REDIRECT_PERMANENT) ? 301 : 302; + redirectLocation = newChannel.URI.spec; + oldChannel.cancel(Cr.NS_BINDING_ABORTED); + callback.onRedirectVerifyCallback(Cr.NS_BINDING_ABORTED); + } + }; + } } - + // Set responseType if (options.responseType) { xmlhttp.responseType = options.responseType; @@ -276,17 +292,24 @@ Zotero.HTTP = new function() { deferred.reject(new Zotero.HTTP.TimeoutException(options.timeout)); }; - xmlhttp.onloadend = function() { - var status = xmlhttp.status; + xmlhttp.onloadend = async function() { + var status = xmlhttp.status || redirectStatus; - // If an invalid HTTP response (e.g., NS_ERROR_INVALID_CONTENT_ENCODING) includes a - // 4xx or 5xx HTTP response code, swap it in, since it might be enough info to do - // what we need (e.g., verify a 404 from a WebDAV server) try { - if (!status && xmlhttp.channel.responseStatus >= 400) { - Zotero.warn(`Overriding status for invalid response for ${dispURL} ` - + `(${xmlhttp.channel.status})`); - status = xmlhttp.channel.responseStatus; + if (!status) { + let responseStatus = xmlhttp.channel.responseStatus; + // If we cancelled a redirect, get the 3xx status from the channel + if (responseStatus >= 300 && responseStatus < 400) { + status = responseStatus; + } + // If an invalid HTTP response (e.g., NS_ERROR_INVALID_CONTENT_ENCODING) includes a + // 4xx or 5xx HTTP response code, swap it in, since it might be enough info to do + // what we need (e.g., verify a 404 from a WebDAV server) + else if (responseStatus >= 400) { + Zotero.warn(`Overriding status for invalid response for ${dispURL} ` + + `(${xmlhttp.channel.status})`); + status = responseStatus; + } } } catch (e) {} @@ -301,6 +324,21 @@ Zotero.HTTP = new function() { else if(isFile) { var success = status == 200 || status == 0; } + else if (redirectStatus) { + var success = true; + let channel = xmlhttp.channel; + xmlhttp = { + status, + getResponseHeader: function (header) { + if (header.toLowerCase() == 'location') { + return redirectLocation; + } + Zotero.debug("Warning: Attempt to get response header other than Location " + + "for redirect", 2); + return null; + } + }; + } else { var success = status >= 200 && status < 300; } diff --git a/components/zotero-service.js b/components/zotero-service.js index 41f2fd8bd6..1f25af8ce0 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -29,6 +29,8 @@ const Cc = Components.classes; const Ci = Components.interfaces; +const Cr = Components.results; +const Cu = Components.utils; /** XPCOM files to be loaded for all modes **/ const xpcomFilesAll = [ diff --git a/test/tests/httpTest.js b/test/tests/httpTest.js index f60b81c1b2..0ad5ae505c 100644 --- a/test/tests/httpTest.js +++ b/test/tests/httpTest.js @@ -1,6 +1,9 @@ describe("Zotero.HTTP", function () { var httpd; var port = 16213; + var baseURL = `http://127.0.0.1:${port}/` + var testURL = baseURL + 'test.html'; + var redirectLocation = baseURL + 'test2.html'; before(function* () { Components.utils.import("resource://zotero-unit/httpd.js"); @@ -16,6 +19,16 @@ describe("Zotero.HTTP", function () { } } ); + httpd.registerPathHandler( + '/redirect', + { + handle: function (request, response) { + response.setHeader('Location', redirectLocation); + response.setStatusLine(null, 301, "Moved Permanently"); + response.write(`\n
\nThe document has moved here.
\n`); + } + } + ); }); after(function* () { @@ -24,14 +37,29 @@ describe("Zotero.HTTP", function () { yield defer.promise; }); + + describe("#request()", function () { + it("should succeed with 3xx status if followRedirects is false", async function () { + var req = await Zotero.HTTP.request( + 'GET', + baseURL + 'redirect', + { + followRedirects: false + } + ); + assert.equal(req.status, 301); + assert.equal(req.getResponseHeader('Location'), redirectLocation); + }); + }); + + describe("#processDocuments()", function () { it("should provide a document object", function* () { var called = false; - var url = `http://127.0.0.1:${port}/test.html`; yield Zotero.HTTP.processDocuments( - url, + testURL, function (doc) { - assert.equal(doc.location.href, url); + assert.equal(doc.location.href, testURL); assert.equal(doc.querySelector('p').textContent, 'Test'); var p = doc.evaluate('//p', doc, null, XPathResult.ANY_TYPE, null).iterateNext(); assert.equal(p.textContent, 'Test'); @@ -56,12 +84,11 @@ describe("Zotero.HTTP", function () { it("should provide a document object", function* () { var called = false; - var url = `http://127.0.0.1:${port}/test.html`; yield new Zotero.Promise((resolve) => { Zotero.HTTP.loadDocuments( - url, + testURL, function (doc) { - assert.equal(doc.location.href, url); + assert.equal(doc.location.href, testURL); assert.equal(doc.querySelector('p').textContent, 'Test'); var p = doc.evaluate('//p', doc, null, XPathResult.ANY_TYPE, null).iterateNext(); assert.equal(p.textContent, 'Test');