Add followRedirects: false option to Zotero.HTTP.request()

Currently only .status and .getResponseHeader() (for getting 'Location')
are available in the returned object, but we could make the body
available if necessary.
This commit is contained in:
Dan Stillman 2018-09-11 01:23:15 -04:00
parent b8db83af08
commit b782120840
3 changed files with 83 additions and 16 deletions

View file

@ -201,6 +201,8 @@ Zotero.HTTP = new function() {
// Send cookie even if "Allow third-party cookies" is disabled (>=Fx3.6 only) // Send cookie even if "Allow third-party cookies" is disabled (>=Fx3.6 only)
var channel = xmlhttp.channel, var channel = xmlhttp.channel,
isFile = channel instanceof Components.interfaces.nsIFileChannel; isFile = channel instanceof Components.interfaces.nsIFileChannel;
var redirectStatus;
var redirectLocation;
if(channel instanceof Components.interfaces.nsIHttpChannelInternal) { if(channel instanceof Components.interfaces.nsIHttpChannelInternal) {
channel.forceAllowThirdPartyCookie = true; channel.forceAllowThirdPartyCookie = true;
@ -218,8 +220,22 @@ Zotero.HTTP = new function() {
if (options.dontCache) { if (options.dontCache) {
channel.loadFlags |= Components.interfaces.nsIRequest.LOAD_BYPASS_CACHE; 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 // Set responseType
if (options.responseType) { if (options.responseType) {
xmlhttp.responseType = options.responseType; xmlhttp.responseType = options.responseType;
@ -276,17 +292,24 @@ Zotero.HTTP = new function() {
deferred.reject(new Zotero.HTTP.TimeoutException(options.timeout)); deferred.reject(new Zotero.HTTP.TimeoutException(options.timeout));
}; };
xmlhttp.onloadend = function() { xmlhttp.onloadend = async function() {
var status = xmlhttp.status; 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 { try {
if (!status && xmlhttp.channel.responseStatus >= 400) { if (!status) {
Zotero.warn(`Overriding status for invalid response for ${dispURL} ` let responseStatus = xmlhttp.channel.responseStatus;
+ `(${xmlhttp.channel.status})`); // If we cancelled a redirect, get the 3xx status from the channel
status = xmlhttp.channel.responseStatus; 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) {} catch (e) {}
@ -301,6 +324,21 @@ Zotero.HTTP = new function() {
else if(isFile) { else if(isFile) {
var success = status == 200 || status == 0; 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 { else {
var success = status >= 200 && status < 300; var success = status >= 200 && status < 300;
} }

View file

@ -29,6 +29,8 @@
const Cc = Components.classes; const Cc = Components.classes;
const Ci = Components.interfaces; const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;
/** XPCOM files to be loaded for all modes **/ /** XPCOM files to be loaded for all modes **/
const xpcomFilesAll = [ const xpcomFilesAll = [

View file

@ -1,6 +1,9 @@
describe("Zotero.HTTP", function () { describe("Zotero.HTTP", function () {
var httpd; var httpd;
var port = 16213; var port = 16213;
var baseURL = `http://127.0.0.1:${port}/`
var testURL = baseURL + 'test.html';
var redirectLocation = baseURL + 'test2.html';
before(function* () { before(function* () {
Components.utils.import("resource://zotero-unit/httpd.js"); 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(`<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">\n<html><head>\n<title>301 Moved Permanently</title>\n</head><body>\n<h1>Moved Permanently</h1>\n<p>The document has moved <a href="${redirectLocation}">here</a>.</p>\n</body></html>`);
}
}
);
}); });
after(function* () { after(function* () {
@ -24,14 +37,29 @@ describe("Zotero.HTTP", function () {
yield defer.promise; 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 () { describe("#processDocuments()", function () {
it("should provide a document object", function* () { it("should provide a document object", function* () {
var called = false; var called = false;
var url = `http://127.0.0.1:${port}/test.html`;
yield Zotero.HTTP.processDocuments( yield Zotero.HTTP.processDocuments(
url, testURL,
function (doc) { function (doc) {
assert.equal(doc.location.href, url); assert.equal(doc.location.href, testURL);
assert.equal(doc.querySelector('p').textContent, 'Test'); assert.equal(doc.querySelector('p').textContent, 'Test');
var p = doc.evaluate('//p', doc, null, XPathResult.ANY_TYPE, null).iterateNext(); var p = doc.evaluate('//p', doc, null, XPathResult.ANY_TYPE, null).iterateNext();
assert.equal(p.textContent, 'Test'); assert.equal(p.textContent, 'Test');
@ -56,12 +84,11 @@ describe("Zotero.HTTP", function () {
it("should provide a document object", function* () { it("should provide a document object", function* () {
var called = false; var called = false;
var url = `http://127.0.0.1:${port}/test.html`;
yield new Zotero.Promise((resolve) => { yield new Zotero.Promise((resolve) => {
Zotero.HTTP.loadDocuments( Zotero.HTTP.loadDocuments(
url, testURL,
function (doc) { function (doc) {
assert.equal(doc.location.href, url); assert.equal(doc.location.href, testURL);
assert.equal(doc.querySelector('p').textContent, 'Test'); assert.equal(doc.querySelector('p').textContent, 'Test');
var p = doc.evaluate('//p', doc, null, XPathResult.ANY_TYPE, null).iterateNext(); var p = doc.evaluate('//p', doc, null, XPathResult.ANY_TYPE, null).iterateNext();
assert.equal(p.textContent, 'Test'); assert.equal(p.textContent, 'Test');