commit b5bc18c7eda7d06a1d03d3ab3765988791f52a33
parent 57603427e386179590f5f457e8e1eb4b3c3c4f15
Author: Dan Stillman <dstillman@zotero.org>
Date: Mon, 5 Dec 2016 02:29:36 -0500
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).
Diffstat:
3 files changed, 229 insertions(+), 39 deletions(-)
diff --git a/chrome/content/zotero/xpcom/server.js b/chrome/content/zotero/xpcom/server.js
@@ -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
diff --git a/chrome/content/zotero/xpcom/server_connector.js b/chrome/content/zotero/xpcom/server_connector.js
@@ -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)];
})
};
diff --git a/test/tests/serverTest.js b/test/tests/serverTest.js
@@ -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");
+ });
+ });
+ });
+ })
+});