Add new init(options) signature for server endpoints

An endpoint can now take a single object containing 'method', 'pathname',
'query', 'headers', and 'data' and return an integer, an array containing
[statusCode, contentType, body], or a promise for either. This allows the
handlers to use the HTTP method and headers and removes the need for callbacks
when some handlers already use coroutine().

If init() returns a promise, it now has to use the new single-parameter
signature (because the check is done with Function.length, and combining
promises and callbacks doesn't make sense anyway).
This commit is contained in:
Dan Stillman 2016-12-05 02:29:36 -05:00
parent 57603427e3
commit b5bc18c7ed
3 changed files with 229 additions and 39 deletions

View file

@ -282,7 +282,7 @@ Zotero.Server.DataListener.prototype._headerFinished = function() {
if(method[1] == "HEAD" || method[1] == "OPTIONS") {
this._requestFinished(this._generateResponse(200));
} else if(method[1] == "GET") {
this._processEndpoint("GET", null);
this._processEndpoint("GET", null); // async
} else if(method[1] == "POST") {
const contentLengthRe = /[\r\n]Content-Length: +([0-9]+)/i;
@ -322,7 +322,7 @@ Zotero.Server.DataListener.prototype._bodyData = function() {
}
// handle envelope
this._processEndpoint("POST", this.body);
this._processEndpoint("POST", this.body); // async
}
}
@ -358,7 +358,7 @@ Zotero.Server.DataListener.prototype._generateResponse = function(status, conten
/**
* Generates a response based on calling the function associated with the endpoint
*/
Zotero.Server.DataListener.prototype._processEndpoint = function(method, postData) {
Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine(function* (method, postData) {
try {
var endpoint = new this.endpoint;
@ -423,10 +423,54 @@ Zotero.Server.DataListener.prototype._processEndpoint = function(method, postDat
me._requestFinished(me._generateResponse(code, contentType, arg));
}
// pass to endpoint
if (endpoint.init.length === 2) {
// Pass to endpoint
//
// Single-parameter endpoint
// - Takes an object with 'method', 'pathname', 'query', 'headers', and 'data'
// - Returns a status code, an array containing [statusCode, contentType, body],
// or a promise for either
if (endpoint.init.length === 1
// Return value from Zotero.Promise.coroutine()
|| endpoint.init.length === 0) {
let headers = {};
let headerLines = this.header.trim().split(/\r\n/);
for (let line of headerLines) {
line = line.trim();
let pos = line.indexOf(':');
if (pos == -1) {
continue;
}
let k = line.substr(0, pos);
let v = line.substr(pos + 1).trim();
headers[k] = v;
}
let maybePromise = endpoint.init({
method,
pathname: this.pathname,
query: this.query ? Zotero.Server.decodeQueryString(this.query.substr(1)) : {},
headers,
data: decodedData
});
let result;
if (maybePromise.then) {
result = yield maybePromise;
}
else {
result = maybePromise;
}
if (Number.isInteger(result)) {
sendResponseCallback(result);
}
else {
sendResponseCallback(...result);
}
}
// Two-parameter endpoint takes data and a callback
else if (endpoint.init.length === 2) {
endpoint.init(decodedData, sendResponseCallback);
}
// Three-parameter endpoint takes a URL, data, and a callback
else {
const uaRe = /[\r\n]User-Agent: +([^\r\n]+)/i;
var m = uaRe.exec(this.header);
@ -442,7 +486,7 @@ Zotero.Server.DataListener.prototype._processEndpoint = function(method, postDat
this._requestFinished(this._generateResponse(500), "text/plain", "An error occurred\n");
throw e;
}
}
});
/*
* returns HTTP data from a request

View file

@ -320,10 +320,10 @@ Zotero.Server.Connector.SaveItem.prototype = {
/**
* Either loads HTML into a hidden browser and initiates translation, or saves items directly
* to the database
* @param {Object} data POST data or GET query string
* @param {Function} sendResponseCallback function to send HTTP response
*/
init: Zotero.Promise.coroutine(function* (url, data, sendResponseCallback) {
init: Zotero.Promise.coroutine(function* (options) {
var data = options.data;
// figure out where to save
var zp = Zotero.getActiveZoteroPane();
try {
@ -351,13 +351,12 @@ Zotero.Server.Connector.SaveItem.prototype = {
}
else {
Zotero.logError("Can't add item to read-only library " + library.name);
sendResponseCallback(500);
return;
return 500;
}
}
var cookieSandbox = data["uri"] ? new Zotero.CookieSandbox(null, data["uri"],
data["detailedCookies"] ? "" : data["cookie"] || "", url.userAgent) : null;
data["detailedCookies"] ? "" : data["cookie"] || "", options.userAgent) : null;
if(cookieSandbox && data.detailedCookies) {
cookieSandbox.addCookiesFromHeader(data.detailedCookies);
}
@ -374,6 +373,7 @@ Zotero.Server.Connector.SaveItem.prototype = {
forceTagType: 1,
cookieSandbox
});
var deferred = Zotero.Promise.defer();
itemSaver.saveItems(data.items, function(returnValue, items) {
if(returnValue) {
try {
@ -387,16 +387,17 @@ Zotero.Server.Connector.SaveItem.prototype = {
}
}
sendResponseCallback(201, "application/json", JSON.stringify({items: data.items}));
deferred.resolve([201, "application/json", JSON.stringify({items: data.items})]);
} catch(e) {
Zotero.logError(e);
sendResponseCallback(500);
deferred.resolve(500);
}
} else {
Zotero.logError(items);
sendResponseCallback(500);
deferred.resolve(500);
}
}, Zotero.Server.Connector.AttachmentProgressManager.onProgress);
return deferred.promise;
})
}
@ -419,10 +420,10 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
/**
* Save snapshot
* @param {String} data POST data or GET query string
* @param {Function} sendResponseCallback function to send HTTP response
*/
init: Zotero.Promise.coroutine(function* (url, data, sendResponseCallback) {
init: Zotero.Promise.coroutine(function* (options) {
var data = options.data;
Zotero.Server.Connector.Data[data["url"]] = "<html>"+data["html"]+"</html>";
var zp = Zotero.getActiveZoteroPane();
@ -451,8 +452,7 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
}
else {
Zotero.logError("Can't add item to read-only library " + library.name);
sendResponseCallback(500);
return;
return 500;
}
}
@ -466,7 +466,7 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
filesEditable = true;
}
var cookieSandbox = new Zotero.CookieSandbox(null, data["url"], data["cookie"], url.userAgent);
var cookieSandbox = new Zotero.CookieSandbox(null, data["url"], data["cookie"], options.userAgent);
if (data.pdf && filesEditable) {
delete Zotero.Server.Connector.Data[data.url];
@ -479,14 +479,15 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
contentType: "application/pdf",
cookieSandbox
});
sendResponseCallback(201)
return 201;
}
catch (e) {
sendResponseCallback(500);
throw e;
Zotero.logError(e);
return 500;
}
}
else {
let deferred = Zotero.Promise.defer();
Zotero.HTTP.processDocuments(
["zotero://connector/" + encodeURIComponent(data.url)],
Zotero.Promise.coroutine(function* (doc) {
@ -512,16 +513,17 @@ Zotero.Server.Connector.SaveSnapshot.prototype = {
});
}
sendResponseCallback(201);
deferred.resolve(201);
} catch(e) {
Zotero.debug("ERROR");
Zotero.debug(e);
sendResponseCallback(500);
deferred.resolve(500);
throw e;
}
}),
null, null, false, cookieSandbox
);
return deferred.promise;
}
})
}
@ -600,16 +602,16 @@ Zotero.Server.Connector.Import.prototype = {
supportedDataTypes: '*',
permitBookmarklet: false,
init: Zotero.Promise.coroutine(function* (url, data, sendResponseCallback){
init: Zotero.Promise.coroutine(function* (options) {
let translate = new Zotero.Translate.Import();
translate.setString(data);
translate.setString(options.data);
let translators = yield translate.getTranslators();
if (!translators || !translators.length) {
return sendResponseCallback(400);
return 400;
}
translate.setTranslator(translators[0]);
let items = yield translate.translate();
return sendResponseCallback(201, "application/json", JSON.stringify(items));
return [201, "application/json", JSON.stringify(items)];
})
}
@ -627,13 +629,13 @@ Zotero.Server.Connector.InstallStyle.prototype = {
supportedDataTypes: '*',
permitBookmarklet: false,
init: Zotero.Promise.coroutine(function* (url, data, sendResponseCallback){
init: Zotero.Promise.coroutine(function* (options) {
try {
var styleName = yield Zotero.Styles.install(data, url.query.origin || null, true);
var styleName = yield Zotero.Styles.install(options.data, options.query.origin || null, true);
} catch (e) {
sendResponseCallback(400, "text/plain", e.message)
return [400, "text/plain", e.message];
}
sendResponseCallback(201, "application/json", JSON.stringify({name: styleName}));
return [201, "application/json", JSON.stringify({name: styleName})];
})
};
@ -741,16 +743,14 @@ Zotero.Server.Connector.GetClientHostnames.prototype = {
/**
* Returns a 200 response to say the server is alive
* @param {String} data POST data or GET query string
* @param {Function} sendResponseCallback function to send HTTP response
*/
init: Zotero.Promise.coroutine(function* (url, postData, sendResponseCallback) {
init: Zotero.Promise.coroutine(function* (options) {
try {
var hostnames = yield Zotero.Proxies.DNS.getHostnames();
} catch(e) {
sendResponseCallback(500);
return 500;
}
sendResponseCallback(200, "application/json", JSON.stringify(hostnames));
return [200, "application/json", JSON.stringify(hostnames)];
})
};

146
test/tests/serverTest.js Normal file
View file

@ -0,0 +1,146 @@
"use strict";
describe("Zotero.Server", function () {
Components.utils.import("resource://zotero-unit/httpd.js");
var serverPath;
before(function* () {
Zotero.Prefs.set("httpServer.enabled", true);
Zotero.Server.init();
serverPath = 'http://127.0.0.1:' + Zotero.Prefs.get('httpServer.port');
});
describe('DataListener', function() {
describe("_processEndpoint()", function () {
describe("1 argument", function () {
it("integer return", function* () {
var called = false;
var endpoint = "/test/" + Zotero.Utilities.randomString();
var handler = function () {};
handler.prototype = {
supportedMethods: ["POST"],
supportedDataTypes: "*",
init: function (options) {
called = true;
assert.isObject(options);
assert.propertyVal(options.headers, "Accept-Charset", "UTF-8");
return 204;
}
};
Zotero.Server.Endpoints[endpoint] = handler;
let req = yield Zotero.HTTP.request(
"POST",
serverPath + endpoint,
{
headers: {
"Accept-Charset": "UTF-8",
"Content-Type": "application/json"
},
responseType: "text",
body: JSON.stringify({
foo: "bar"
})
}
);
assert.ok(called);
assert.equal(req.status, 204);
});
it("array return", function* () {
var called = false;
var endpoint = "/test/" + Zotero.Utilities.randomString();
var handler = function () {};
handler.prototype = {
supportedMethods: ["GET"],
supportedDataTypes: "*",
init: function (options) {
called = true;
assert.isObject(options);
return [201, "text/plain", "Test"];
}
};
Zotero.Server.Endpoints[endpoint] = handler;
let req = yield Zotero.HTTP.request(
"GET",
serverPath + endpoint,
{
responseType: "text"
}
);
assert.ok(called);
assert.equal(req.status, 201);
assert.equal(req.getResponseHeader("Content-Type"), "text/plain");
assert.equal(req.responseText, "Test");
});
it("integer promise return", function* () {
var called = false;
var endpoint = "/test/" + Zotero.Utilities.randomString();
var handler = function () {};
handler.prototype = {
supportedMethods: ["GET"],
supportedDataTypes: "*",
init: Zotero.Promise.coroutine(function* (options) {
called = true;
assert.isObject(options);
return 204;
})
};
Zotero.Server.Endpoints[endpoint] = handler;
let req = yield Zotero.HTTP.request(
"GET",
serverPath + endpoint,
{
responseType: "text"
}
);
assert.ok(called);
assert.equal(req.status, 204);
});
it("array promise return", function* () {
var called = false;
var endpoint = "/test/" + Zotero.Utilities.randomString();
var handler = function () {};
handler.prototype = {
supportedMethods: ["GET"],
supportedDataTypes: "*",
init: Zotero.Promise.coroutine(function* (options) {
called = true;
assert.isObject(options);
return [201, "text/plain", "Test"];
})
};
Zotero.Server.Endpoints[endpoint] = handler;
let req = yield Zotero.HTTP.request(
"GET",
serverPath + endpoint,
{
responseType: "text"
}
);
assert.ok(called);
assert.equal(req.status, 201);
assert.equal(req.getResponseHeader("Content-Type"), "text/plain");
assert.equal(req.responseText, "Test");
});
});
});
})
});