mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
666 lines
21 KiB
JavaScript
Executable file
666 lines
21 KiB
JavaScript
Executable file
/*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright © 2011 Center for History and New Media
|
|
George Mason University, Fairfax, Virginia, USA
|
|
http://zotero.org
|
|
|
|
This file is part of Zotero.
|
|
|
|
Zotero is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
Zotero is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
*/
|
|
|
|
var { HttpServer } = ChromeUtils.importESModule("chrome://remote/content/server/httpd.sys.mjs");
|
|
var { NetUtil } = ChromeUtils.importESModule("resource://gre/modules/NetUtil.sys.mjs");
|
|
|
|
Zotero.Server = new function () {
|
|
var _onlineObserverRegistered, serv;
|
|
this.responseCodes = {
|
|
200:"OK",
|
|
201:"Created",
|
|
204:"No Content",
|
|
300:"Multiple Choices",
|
|
304:"Not Modified",
|
|
400:"Bad Request",
|
|
403:"Forbidden",
|
|
404:"Not Found",
|
|
409:"Conflict",
|
|
412:"Precondition Failed",
|
|
500:"Internal Server Error",
|
|
501:"Not Implemented",
|
|
503:"Service Unavailable",
|
|
504:"Gateway Timeout"
|
|
};
|
|
|
|
Object.defineProperty(this, 'port', {
|
|
get() {
|
|
if (!serv) {
|
|
throw new Error('Server not initialized');
|
|
}
|
|
return serv.identity.primaryPort;
|
|
}
|
|
});
|
|
|
|
/**
|
|
* initializes a very rudimentary web server
|
|
*/
|
|
this.init = async function (port) {
|
|
if (serv) {
|
|
Zotero.debug("Already listening on port " + serv.port);
|
|
return;
|
|
}
|
|
|
|
// Do any necessary pre-listen initialization first
|
|
await Zotero.Server.LocalAPI.init();
|
|
|
|
port = port || Zotero.Prefs.get('httpServer.port');
|
|
try {
|
|
serv = new HttpServer();
|
|
serv.registerPrefixHandler('/', this.handleRequest)
|
|
serv.start(port);
|
|
|
|
Zotero.debug(`HTTP server listening on 127.0.0.1:${serv.identity.primaryPort}`);
|
|
|
|
// Close port on Zotero shutdown (doesn't apply to translation-server)
|
|
if (Zotero.addShutdownListener) {
|
|
Zotero.addShutdownListener(this.close.bind(this));
|
|
}
|
|
}
|
|
catch (e) {
|
|
Zotero.logError(e);
|
|
Zotero.debug("Not initializing HTTP server");
|
|
serv = undefined;
|
|
}
|
|
};
|
|
|
|
this.handleRequest = function (request, response) {
|
|
let requestHandler = new Zotero.Server.RequestHandler(request, response);
|
|
return requestHandler.handleRequest();
|
|
}
|
|
|
|
/**
|
|
* releases bound port
|
|
*/
|
|
this.close = function () {
|
|
if (!serv) return;
|
|
serv.stop();
|
|
serv = undefined;
|
|
};
|
|
|
|
/**
|
|
* Parses a query string into a key => value object
|
|
* @param {String} queryString Query string
|
|
*/
|
|
this.decodeQueryString = function (queryString) {
|
|
var splitData = queryString.split("&");
|
|
var decodedData = {};
|
|
for (let variable of splitData) {
|
|
var splitIndex = variable.indexOf("=");
|
|
decodedData[decodeURIComponent(variable.substr(0, splitIndex))] = decodeURIComponent(variable.substr(splitIndex+1));
|
|
}
|
|
return decodedData;
|
|
}
|
|
}
|
|
|
|
|
|
// A proxy headers class to make header retrieval case-insensitive
|
|
Zotero.Server.Headers = class {
|
|
constructor() {
|
|
return new Proxy(this, {
|
|
get(target, name, receiver) {
|
|
if (typeof name !== 'string') {
|
|
return Reflect.get(target, name, receiver);
|
|
}
|
|
return Reflect.get(target, name.toLowerCase(), receiver);
|
|
},
|
|
has(target, name, receiver) {
|
|
if (typeof name !== 'string') {
|
|
return Reflect.has(target, name, receiver);
|
|
}
|
|
return Reflect.has(target, name.toLowerCase(), receiver);
|
|
},
|
|
set(target, name, value, receiver) {
|
|
return Reflect.set(target, name.toLowerCase(), value, receiver);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
Zotero.Server.networkStreamToString = function (stream, length) {
|
|
let data = NetUtil.readInputStreamToString(stream, length);
|
|
return Zotero.Utilities.Internal.decodeUTF8(data);
|
|
};
|
|
|
|
/**
|
|
* Decode RFC 2047 encoded headers
|
|
* Supports both Q-encoding (quoted-printable) and B-encoding (base64)
|
|
*
|
|
* @param {String} value - Header value to decode
|
|
* @return {String} Decoded header value
|
|
*/
|
|
Zotero.Server.decodeRFC2047 = function (value) {
|
|
// RFC 2047-decode the result.
|
|
// Process encoded words anywhere in the header value, as per RFC 2047 section 5
|
|
// which allows ordinary ASCII text and encoded words to appear together.
|
|
|
|
// Helper function to convert string to bytes
|
|
function stringToBytes(str) {
|
|
if (typeof str !== "string") {
|
|
throw new Error("Invalid argument for stringToBytes");
|
|
}
|
|
const length = str.length;
|
|
const bytes = new Uint8Array(length);
|
|
for (let i = 0; i < length; ++i) {
|
|
bytes[i] = str.charCodeAt(i) & 0xff;
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
// Helper function to decode text with specified encoding
|
|
function textDecode(encoding, value) {
|
|
if (encoding) {
|
|
if (!/^[\x00-\xFF]+$/.test(value)) {
|
|
return value;
|
|
}
|
|
try {
|
|
const decoder = new TextDecoder(encoding, { fatal: true });
|
|
const buffer = stringToBytes(value);
|
|
value = decoder.decode(buffer);
|
|
}
|
|
// TextDecoder constructor threw - unrecognized encoding.
|
|
catch {
|
|
Zotero.debug(`decodeRFC2047: Unrecognized encoding: ${encoding}`, 1);
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
// RFC 2047, section 2.4
|
|
// encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
|
|
// charset = token (but let's restrict to characters that denote a
|
|
// possibly valid encoding).
|
|
// encoding = q or b
|
|
// encoded-text = any printable ASCII character other than ? or space.
|
|
return value.replace(
|
|
/=\?([\w-]+)\?([QqBb])\?((?:[^? ]|\?(?!=))*)\?=/g,
|
|
(matches, charset, encoding, text) => {
|
|
if (encoding === "q" || encoding === "Q") {
|
|
// RFC 2047 section 4.2.
|
|
text = text.replace(/_/g, " ");
|
|
text = text.replace(/=([0-9a-fA-F]{2})/g, (match, hex) => {
|
|
return String.fromCharCode(parseInt(hex, 16));
|
|
});
|
|
return textDecode(charset, text);
|
|
}
|
|
// else encoding is b or B - base64 (RFC 2047 section 4.1)
|
|
try {
|
|
text = atob(text);
|
|
}
|
|
catch {
|
|
Zotero.debug(`decodeRFC2047: Invalid base64: ${text}`, 1);
|
|
}
|
|
return textDecode(charset, text);
|
|
}
|
|
);
|
|
};
|
|
|
|
Zotero.Server.RequestHandler = function (request, response) {
|
|
this.body = "";
|
|
this.bodyLength = 0;
|
|
|
|
this.foundReturn = false;
|
|
this.request = request;
|
|
this.response = response;
|
|
}
|
|
|
|
/*
|
|
* checks to see if Content-Length bytes of body have been read and, if so, processes the body
|
|
*/
|
|
Zotero.Server.RequestHandler.prototype._bodyData = function () {
|
|
const PLAIN_TEXT_CONTENT_TYPES = new Set([
|
|
'text/plain',
|
|
'application/json',
|
|
'application/x-www-form-urlencoded'
|
|
]);
|
|
|
|
let data = null;
|
|
if (this.bodyLength > 0) {
|
|
if (PLAIN_TEXT_CONTENT_TYPES.has(this.contentType)) {
|
|
this.body = data = Zotero.Server.networkStreamToString(this.request.bodyInputStream, this.bodyLength);
|
|
}
|
|
else if (this.contentType === 'multipart/form-data') {
|
|
data = NetUtil.readInputStreamToString(this.request.bodyInputStream, this.bodyLength);
|
|
try {
|
|
data = this._decodeMultipartData(data);
|
|
}
|
|
catch (e) {
|
|
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
|
}
|
|
}
|
|
}
|
|
if (this.body.length >= this.bodyLength) {
|
|
let noLogEndpoints = [
|
|
'/connector/saveSingleFile'
|
|
];
|
|
if (this.body != '{}'
|
|
&& PLAIN_TEXT_CONTENT_TYPES.has(this.contentType)
|
|
&& !noLogEndpoints.includes(this.pathname)) {
|
|
Zotero.debug(Zotero.Utilities.ellipsize(this.body, 1000, false, true), 5);
|
|
}
|
|
}
|
|
// handle envelope
|
|
this._processEndpoint(this.requestMethod || "POST", data); // async
|
|
}
|
|
|
|
|
|
/**
|
|
* Generates the response to an HTTP request
|
|
*/
|
|
Zotero.Server.RequestHandler.prototype._generateResponse = function (status, contentTypeOrHeaders, body) {
|
|
var response = "HTTP/1.0 "+status+" "+Zotero.Server.responseCodes[status]+"\r\n";
|
|
response += "X-Zotero-Version: "+Zotero.version+"\r\n";
|
|
response += "X-Zotero-Connector-API-Version: "+CONNECTOR_API_VERSION+"\r\n";
|
|
|
|
if (this.origin === ZOTERO_CONFIG.BOOKMARKLET_ORIGIN) {
|
|
response += "Access-Control-Allow-Origin: " + this.origin + "\r\n";
|
|
response += "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n";
|
|
response += "Access-Control-Allow-Headers: Content-Type,X-Zotero-Connector-API-Version,X-Zotero-Version\r\n";
|
|
}
|
|
|
|
if (contentTypeOrHeaders) {
|
|
if (typeof contentTypeOrHeaders == 'string') {
|
|
contentTypeOrHeaders = {
|
|
'Content-Type': contentTypeOrHeaders
|
|
};
|
|
}
|
|
for (let header in contentTypeOrHeaders) {
|
|
response += `${header}: ${contentTypeOrHeaders[header]}\r\n`;
|
|
}
|
|
}
|
|
|
|
if (body) {
|
|
response += "\r\n"+body;
|
|
} else {
|
|
response += "Content-Length: 0\r\n\r\n";
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
Zotero.Server.RequestHandler.prototype.handleRequest = async function () {
|
|
const request = this.request;
|
|
const response = this.response;
|
|
// Tell httpd that we will be constructing our own response
|
|
// without its custom methods, asynchronously
|
|
response.seizePower();
|
|
|
|
let requestDebug = `${request.method} ${request.path} HTTP/${request.httpVersion}\n`
|
|
// Parse headers into this.headers with lowercase names
|
|
this.headers = new Zotero.Server.Headers();
|
|
for (let { data: name } of request.headers) {
|
|
let headerValue = request.getHeader(name);
|
|
requestDebug += `${name}: ${headerValue}\n`;
|
|
// Decode RFC 2047 encoded header values
|
|
this.headers[name.toLowerCase()] = Zotero.Server.decodeRFC2047(headerValue);
|
|
}
|
|
|
|
Zotero.debug(requestDebug, 5);
|
|
|
|
// Make sure the Host header is set to localhost or 127.0.0.1 to prevent DNS rebinding attacks
|
|
let host = this.headers.host;
|
|
if (!host || !/^(?:127\.0\.0\.1|\[::1\]|localhost)(?::[0-9]+)?$/i.test(host)) {
|
|
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid Host header\n"));
|
|
return;
|
|
}
|
|
|
|
if (this.headers.origin) {
|
|
this.origin = this.headers.origin;
|
|
}
|
|
|
|
this.pathname = request.path;
|
|
this.query = request.queryString;
|
|
|
|
// get content-type
|
|
var contentType = this.headers['content-type'];
|
|
if (contentType) {
|
|
let splitContentType = contentType.split(/\s*;/);
|
|
this.contentType = splitContentType[0];
|
|
}
|
|
|
|
this.pathParams = {};
|
|
if (Zotero.Server.Endpoints[this.pathname]) {
|
|
this.endpoint = Zotero.Server.Endpoints[this.pathname];
|
|
}
|
|
else {
|
|
let router = new Zotero.Router(this.pathParams);
|
|
for (let [potentialTemplate, endpoint] of Object.entries(Zotero.Server.Endpoints)) {
|
|
if (!potentialTemplate.includes(':')) continue;
|
|
router.add(potentialTemplate, () => {
|
|
this.pathParams._endpoint = endpoint;
|
|
}, true, /* Do not allow missing params */ false);
|
|
}
|
|
if (router.run(this.pathname)) {
|
|
this.endpoint = this.pathParams._endpoint;
|
|
delete this.pathParams._endpoint;
|
|
delete this.pathParams.url;
|
|
}
|
|
else {
|
|
this._requestFinished(this._generateResponse(404, "text/plain", "No endpoint found\n"));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (request.method == "HEAD" || request.method == "OPTIONS") {
|
|
this._requestFinished(this._generateResponse(200));
|
|
}
|
|
else if (request.method == "GET") {
|
|
this._processEndpoint("GET", null); // async
|
|
}
|
|
else if (request.method == "POST" || request.method == "PUT"
|
|
|| request.method == "PATCH" || request.method == "DELETE") {
|
|
const contentLengthRe = /^([0-9]+)$/;
|
|
this.requestMethod = request.method;
|
|
|
|
// parse content length
|
|
var m = contentLengthRe.exec(this.headers['content-length']);
|
|
if (!m) {
|
|
// DELETE is allowed to have no body
|
|
if (request.method == "DELETE") {
|
|
this._processEndpoint("DELETE", null); // async
|
|
return;
|
|
}
|
|
this._requestFinished(this._generateResponse(400, "text/plain", "Content-length not provided\n"));
|
|
return;
|
|
}
|
|
|
|
this.bodyLength = parseInt(m[1]);
|
|
this._bodyData();
|
|
} else {
|
|
this._requestFinished(this._generateResponse(501, "text/plain", "Method not implemented\n"));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a response based on calling the function associated with the endpoint
|
|
*
|
|
* Note: postData contains raw bytes and should be decoded before use
|
|
*/
|
|
Zotero.Server.RequestHandler.prototype._processEndpoint = async function (method, postData) {
|
|
try {
|
|
var endpoint = new this.endpoint;
|
|
|
|
// Check that endpoint supports method
|
|
if (endpoint.supportedMethods && endpoint.supportedMethods.indexOf(method) === -1) {
|
|
this._requestFinished(this._generateResponse(400, "text/plain", "Endpoint does not support method\n"));
|
|
return;
|
|
}
|
|
|
|
var isBrowser = (this.headers['user-agent'] && this.headers['user-agent'].startsWith('Mozilla/'))
|
|
// Origin isn't sent via fetch() for HEAD/GET, but for crazy UA strings, protecting
|
|
// POST requests is better than nothing
|
|
|| 'origin' in this.headers;
|
|
if (isBrowser
|
|
// Allow endpoints to explicitly opt into allowing browser requests
|
|
// if they really want to
|
|
&& !endpoint.allowRequestsFromUnsafeWebContent
|
|
&& !this.headers['x-zotero-connector-api-version']
|
|
&& !this.headers['zotero-allowed-request']
|
|
// Allow browser requests to test endpoints
|
|
&& !this.pathname.startsWith('/test/')
|
|
// Allow browser requests to /connector/ping as long as they come
|
|
// from navigation, not XHR/fetch()/resource loading
|
|
&& !(this.pathname === '/connector/ping' && this.headers['sec-fetch-mode'] === 'navigate')) {
|
|
Zotero.debug('Preventing request from browser');
|
|
this._cancelResponse();
|
|
return;
|
|
}
|
|
|
|
var data = null;
|
|
if ((method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE')
|
|
&& this.contentType) {
|
|
// check that endpoint supports contentType
|
|
var supportedDataTypes = endpoint.supportedDataTypes;
|
|
if (supportedDataTypes && supportedDataTypes != '*'
|
|
&& supportedDataTypes.indexOf(this.contentType) === -1) {
|
|
this._requestFinished(this._generateResponse(400, "text/plain", "Endpoint does not support content-type\n"));
|
|
return;
|
|
}
|
|
|
|
// decode content-type post data
|
|
if (this.contentType === "application/json") {
|
|
try {
|
|
data = JSON.parse(postData);
|
|
}
|
|
catch(e) {
|
|
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid JSON provided\n"));
|
|
return;
|
|
}
|
|
}
|
|
else if (this.contentType === "application/x-www-form-urlencoded") {
|
|
data = Zotero.Server.decodeQueryString(postData);
|
|
}
|
|
else if (postData) {
|
|
data = postData;
|
|
}
|
|
else {
|
|
data = this.request.bodyInputStream;
|
|
}
|
|
}
|
|
|
|
// set up response callback
|
|
var sendResponseCallback = (code, contentTypeOrHeaders, arg, options) => {
|
|
this._requestFinished(
|
|
this._generateResponse(code, contentTypeOrHeaders, arg),
|
|
options
|
|
);
|
|
};
|
|
|
|
// Pass to endpoint
|
|
//
|
|
// Single-parameter endpoint
|
|
// - Takes an object with 'method', 'pathname', 'pathParams', 'searchParams', 'headers', and 'data'
|
|
// - Returns a status code, an array containing [statusCode, contentType, body],
|
|
// or a promise for either
|
|
if (endpoint.init.length === 1) {
|
|
let maybePromise = endpoint.init({
|
|
method,
|
|
pathname: this.pathname,
|
|
pathParams: this.pathParams,
|
|
searchParams: new URLSearchParams(this.query || ''),
|
|
headers: this.headers,
|
|
data
|
|
});
|
|
let result;
|
|
if (maybePromise.then) {
|
|
result = await 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(data, sendResponseCallback);
|
|
}
|
|
// Three-parameter endpoint takes a URL, data, and a callback
|
|
else {
|
|
var url = {
|
|
pathname: this.pathname,
|
|
searchParams: new URLSearchParams(this.query || ''),
|
|
userAgent: this.headers['user-agent']
|
|
};
|
|
endpoint.init(url, data, sendResponseCallback);
|
|
}
|
|
} catch(e) {
|
|
Zotero.debug(e);
|
|
this._requestFinished(this._generateResponse(500), "text/plain", "An error occurred\n");
|
|
throw e;
|
|
}
|
|
};
|
|
|
|
/*
|
|
* returns HTTP data from a request
|
|
*/
|
|
Zotero.Server.RequestHandler.prototype._requestFinished = function (responseBody, options) {
|
|
if (this._responseSent) {
|
|
Zotero.debug("Request already finished; not sending another response");
|
|
return;
|
|
}
|
|
this._responseSent = true;
|
|
|
|
// open UTF-8 converter for output stream
|
|
var intlStream = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
|
|
.createInstance(Components.interfaces.nsIConverterOutputStream);
|
|
|
|
// write
|
|
try {
|
|
intlStream.init(this.response.bodyOutputStream, "UTF-8", 1024, "?".charCodeAt(0));
|
|
|
|
// Filter logged response
|
|
if (Zotero.Debug.enabled) {
|
|
let maxLogLength = 2000;
|
|
let str = responseBody;
|
|
if (options && options.logFilter) {
|
|
str = options.logFilter(str);
|
|
}
|
|
if (str.length > maxLogLength) {
|
|
str = str.substr(0, maxLogLength) + `\u2026 (${responseBody.length} chars)`;
|
|
}
|
|
Zotero.debug(str, 5);
|
|
}
|
|
|
|
intlStream.writeString(responseBody);
|
|
}
|
|
finally {
|
|
this.response.finish();
|
|
}
|
|
};
|
|
|
|
Zotero.Server.RequestHandler.prototype._cancelResponse = function () {
|
|
// Close the connection without sending anything back, so web content can't
|
|
// get any information about whether Zotero is running.
|
|
//
|
|
// This causes fetch() to throw a TypeError with the message
|
|
// "NetworkError when attempting to fetch resource.", exactly the same as
|
|
// when no server is running on our port.
|
|
if (this._responseSent) {
|
|
Zotero.debug('Request already finished; not cancelling');
|
|
return;
|
|
}
|
|
Zotero.debug('Cancelling without sending a response');
|
|
this._responseSent = true;
|
|
this.response.finish();
|
|
};
|
|
|
|
Zotero.Server.RequestHandler.prototype._decodeMultipartData = function (data) {
|
|
const contentDispositionRe = /^Content-Disposition:\s*(.*)$/i;
|
|
const contentTypeRe = /^Content-Type:\s*(.*)$/i
|
|
|
|
let results = [];
|
|
|
|
let boundary = /boundary=([^\s]*)/i.exec(this.headers['content-type']);
|
|
if (!boundary) {
|
|
Zotero.debug('Invalid boundary: ' + this.headers['content-type'], 1);
|
|
throw new Error('Invalid multipart/form-data boundary');
|
|
}
|
|
boundary = '--' + boundary[1];
|
|
|
|
data = data.split(boundary);
|
|
// Ignore pre first boundary and post last boundary
|
|
data = data.slice(1, data.length-1);
|
|
for (let field of data) {
|
|
let fieldData = {};
|
|
// Split header and body
|
|
let unixHeaderBoundary = field.indexOf("\n\n");
|
|
let windowsHeaderBoundary = field.indexOf("\r\n\r\n");
|
|
if (unixHeaderBoundary < windowsHeaderBoundary && unixHeaderBoundary != -1) {
|
|
fieldData.header = field.slice(0, unixHeaderBoundary).trim();
|
|
// Strip only the trailing delimiter newline before the next boundary, not all
|
|
// surrounding whitespace -- trimming would corrupt binary bodies (e.g., a file
|
|
// ending in a newline byte)
|
|
fieldData.body = field.slice(unixHeaderBoundary+2).replace(/\n$/, '');
|
|
} else if (windowsHeaderBoundary != -1) {
|
|
fieldData.header = field.slice(0, windowsHeaderBoundary).trim();
|
|
fieldData.body = field.slice(windowsHeaderBoundary+4).replace(/\r\n$/, '');
|
|
} else {
|
|
// Only log first 200 characters in case the part is large
|
|
Zotero.debug('Malformed multipart/form-data body: ' + field.substr(0, 200), 1);
|
|
throw new Error('Malformed multipart/form-data body');
|
|
}
|
|
|
|
fieldData.params = {};
|
|
let headers = [];
|
|
if (fieldData.header.indexOf("\r\n") > -1) {
|
|
headers = fieldData.header.split("\r\n");
|
|
}
|
|
else if (fieldData.header.indexOf("\n\n") > -1) {
|
|
headers = fieldData.header.split("\n\n");
|
|
}
|
|
else {
|
|
headers = [fieldData.header];
|
|
}
|
|
for (const header of headers) {
|
|
if (contentDispositionRe.test(header)) {
|
|
// Example:
|
|
// Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
|
|
let contentDisposition = header.split(';');
|
|
if (contentDisposition.length > 1) {
|
|
contentDisposition.shift();
|
|
for (let param of contentDisposition) {
|
|
let nameVal = param.trim().split('=');
|
|
// Apply RFC 2047 decoding to parameter values
|
|
let paramValue = nameVal[1].trim().slice(1, -1);
|
|
fieldData.params[nameVal[0]] = Zotero.Server.decodeRFC2047(paramValue);
|
|
}
|
|
}
|
|
}
|
|
else if (contentTypeRe.test(header)) {
|
|
// Example:
|
|
// Content-Type: image/png
|
|
let contentType = header.split(':');
|
|
if (contentType.length > 1) {
|
|
// Apply RFC 2047 decoding to content type
|
|
fieldData.params.contentType = Zotero.Server.decodeRFC2047(contentType[1].trim());
|
|
}
|
|
}
|
|
}
|
|
results.push(fieldData);
|
|
}
|
|
return results;
|
|
};
|
|
|
|
|
|
/**
|
|
* Endpoints for the HTTP server
|
|
*
|
|
* Each endpoint should take the form of an object. The init() method of this object will be passed:
|
|
* method - the method of the request ("GET" or "POST")
|
|
* data - the query string (for a "GET" request) or POST data (for a "POST" request)
|
|
* sendResponseCallback - a function to send a response to the HTTP request. This can be passed
|
|
* a response code alone (e.g., sendResponseCallback(404)) or a response
|
|
* code, MIME type, and response body
|
|
* (e.g., sendResponseCallback(200, "text/plain", "Hello World!"))
|
|
*
|
|
* See connector/server_connector.js for examples
|
|
*/
|
|
Zotero.Server.Endpoints = {}
|