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:
parent
57603427e3
commit
b5bc18c7ed
3 changed files with 229 additions and 39 deletions
|
@ -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
|
||||
|
|
|
@ -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
146
test/tests/serverTest.js
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
Loading…
Reference in a new issue