Connector attachment saving server changes for 7.0 (#5345)
This commit is contained in:
parent
1dad1ae0f8
commit
cd856efef8
33 changed files with 2991 additions and 10015 deletions
|
@ -969,99 +969,6 @@ function ZoteroProtocolHandler() {
|
|||
}
|
||||
};
|
||||
|
||||
var ConnectorChannel = function(uri, data) {
|
||||
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
|
||||
.getService(Components.interfaces.nsIScriptSecurityManager);
|
||||
|
||||
this.name = uri;
|
||||
this.URI = ios.newURI(uri, "UTF-8", null);
|
||||
this.owner = (secMan.getCodebasePrincipal || secMan.getSimpleCodebasePrincipal)(this.URI);
|
||||
this._isPending = true;
|
||||
|
||||
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
|
||||
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
|
||||
converter.charset = "UTF-8";
|
||||
this._stream = converter.convertToInputStream(data);
|
||||
this.contentLength = this._stream.available();
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.contentCharset = "UTF-8";
|
||||
ConnectorChannel.prototype.contentType = "text/html";
|
||||
ConnectorChannel.prototype.notificationCallbacks = null;
|
||||
ConnectorChannel.prototype.securityInfo = null;
|
||||
ConnectorChannel.prototype.status = 0;
|
||||
ConnectorChannel.prototype.loadGroup = null;
|
||||
ConnectorChannel.prototype.loadFlags = 393216;
|
||||
|
||||
ConnectorChannel.prototype.__defineGetter__("originalURI", function() { return this.URI });
|
||||
ConnectorChannel.prototype.__defineSetter__("originalURI", function() { });
|
||||
|
||||
ConnectorChannel.prototype.asyncOpen = function(streamListener) {
|
||||
if(this.loadGroup) this.loadGroup.addRequest(this, null);
|
||||
streamListener.onStartRequest(this);
|
||||
streamListener.onDataAvailable(this, this._stream, 0, this.contentLength);
|
||||
streamListener.onStopRequest(this, this.status);
|
||||
this._isPending = false;
|
||||
if(this.loadGroup) this.loadGroup.removeRequest(this, null, 0);
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.isPending = function() {
|
||||
return this._isPending;
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.cancel = function(status) {
|
||||
this.status = status;
|
||||
this._isPending = false;
|
||||
if(this._stream) this._stream.close();
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.suspend = function() {}
|
||||
|
||||
ConnectorChannel.prototype.resume = function() {}
|
||||
|
||||
ConnectorChannel.prototype.open = function() {
|
||||
return this._stream;
|
||||
}
|
||||
|
||||
ConnectorChannel.prototype.QueryInterface = function(iid) {
|
||||
if (!iid.equals(Components.interfaces.nsIChannel) && !iid.equals(Components.interfaces.nsIRequest) &&
|
||||
!iid.equals(Components.interfaces.nsISupports)) {
|
||||
throw Components.results.NS_ERROR_NO_INTERFACE;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* zotero://connector/
|
||||
*
|
||||
* URI spoofing for transferring page data across boundaries
|
||||
*/
|
||||
var ConnectorExtension = new function() {
|
||||
this.loadAsChrome = false;
|
||||
|
||||
this.newChannel = function(uri) {
|
||||
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
|
||||
.getService(Components.interfaces.nsIScriptSecurityManager);
|
||||
var Zotero = Components.classes["@zotero.org/Zotero;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
|
||||
try {
|
||||
var originalURI = uri.pathQueryRef.substr('zotero://connector/'.length);
|
||||
originalURI = decodeURIComponent(originalURI);
|
||||
if(!Zotero.Server.Connector.Data[originalURI]) {
|
||||
return null;
|
||||
} else {
|
||||
return new ConnectorChannel(originalURI, Zotero.Server.Connector.Data[originalURI]);
|
||||
}
|
||||
} catch(e) {
|
||||
Zotero.debug(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
zotero://pdf.js/viewer.html
|
||||
zotero://pdf.js/pdf/1/ABCD5678
|
||||
|
@ -1249,7 +1156,6 @@ function ZoteroProtocolHandler() {
|
|||
this._extensions[ZOTERO_SCHEME + "://timeline"] = TimelineExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://select"] = SelectExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://pdf.js"] = PDFJSExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://open"] = OpenExtension;
|
||||
this._extensions[ZOTERO_SCHEME + "://open-pdf"] = OpenExtension;
|
||||
|
|
|
@ -89,7 +89,6 @@ var ZoteroAdvancedSearch = new function() {
|
|||
isTrash: () => false
|
||||
};
|
||||
|
||||
this.itemsView.changeCollectionTreeRow(collectionTreeRow);
|
||||
// Focus the first field in the window
|
||||
Services.focus.moveFocus(window, null, Services.focus.MOVEFOCUS_FORWARD, 0);
|
||||
}
|
||||
|
|
|
@ -1326,6 +1326,13 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
return this.getRow(this.selection.focused).editable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the underlying view is editable
|
||||
*/
|
||||
get filesEditable() {
|
||||
return this.getRow(this.selection.focused).filesEditable;
|
||||
}
|
||||
|
||||
getRowString(index) {
|
||||
// During filtering, context rows return an empty string to not be selectable
|
||||
// with key-based navigation
|
||||
|
|
|
@ -681,7 +681,7 @@ Zotero.Attachments = new function () {
|
|||
*/
|
||||
this.createURLAttachmentFromTemporaryStorageDirectory = async function (options) {
|
||||
if (!options.directory) throw new Error("'directory' not provided");
|
||||
if (!options.libraryID) throw new Error("'libraryID' not provided");
|
||||
if (!options.libraryID && !options.parentItemID) throw new Error("'libraryID' or 'parentItemID' not provided");
|
||||
if (!options.filename) throw new Error("'filename' not provided");
|
||||
if (!options.url) throw new Error("'directory' not provided");
|
||||
if (!options.contentType) throw new Error("'contentType' not provided");
|
||||
|
@ -977,6 +977,96 @@ Zotero.Attachments = new function () {
|
|||
|
||||
return attachmentItem;
|
||||
});
|
||||
|
||||
/**
|
||||
* Save an attachment from a nsIInputStream
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {String} options.url
|
||||
* @param {nsIStream} options.stream - Stream with data
|
||||
* @param {Integer} options.byteCount - Number of bytes in the stream, usually from the
|
||||
* 'Content-Length' HTTP header.
|
||||
* @param {String} options.contentType - Expected content type
|
||||
* @param {Integer} [options.libraryID] Parent item ID if child attachment
|
||||
* @param {Integer} [options.parentItemID] Parent item ID if child attachment
|
||||
* Either options.libraryID or options.parentItemID are mandatory
|
||||
* @param {Array<String|Integer>} [options.collections] Collection ids or keys
|
||||
* @param {String} [options.title]
|
||||
* @param {Object} [options.saveOptions] - Options to pass to Zotero.Item::save()
|
||||
* @return {Promise<Zotero.Item>} - A promise for the created attachment item
|
||||
*/
|
||||
this.importFromNetworkStream = async (options) => {
|
||||
if (!options.url) throw new Error("'url' not provided");
|
||||
if (!options.stream) throw new Error("'stream' not provided");
|
||||
if (!options.byteCount) throw new Error("'byteCount' not provided");
|
||||
if (!options.contentType) throw new Error("'contentType' not provided");
|
||||
Zotero.debug("Importing attachment item from network stream");
|
||||
|
||||
let url = options.url;
|
||||
let stream = options.stream;
|
||||
let contentType = options.contentType;
|
||||
let libraryID = options.libraryID;
|
||||
let parentItemID = options.parentItemID;
|
||||
let collections = options.collections;
|
||||
let title = options.title;
|
||||
let saveOptions = options.saveOptions;
|
||||
|
||||
if (parentItemID && collections) {
|
||||
throw new Error("parentItemID and collections cannot both be provided");
|
||||
}
|
||||
|
||||
// Create a temporary file
|
||||
let filename;
|
||||
if (parentItemID) {
|
||||
let parentItem = Zotero.Items.get(parentItemID);
|
||||
let fileBaseName = this.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
|
||||
let ext = this._getExtensionFromURL(url, contentType);
|
||||
filename = fileBaseName + (ext != '' ? '.' + ext : '');
|
||||
}
|
||||
else {
|
||||
filename = Zotero.File.truncateFileName(this._getFileNameFromURL(url, contentType), 100);
|
||||
}
|
||||
|
||||
let tmpDirectory = (await this.createTemporaryStorageDirectory()).path;
|
||||
let destDirectory;
|
||||
let attachmentItem;
|
||||
try {
|
||||
let tmpFile = OS.Path.join(tmpDirectory, filename);
|
||||
await Zotero.File.putNetworkStream(tmpFile, stream, options.byteCount);
|
||||
|
||||
attachmentItem = await this.createURLAttachmentFromTemporaryStorageDirectory({
|
||||
directory: tmpDirectory,
|
||||
libraryID,
|
||||
parentItemID,
|
||||
title,
|
||||
filename,
|
||||
url,
|
||||
contentType,
|
||||
collections,
|
||||
saveOptions
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(e, 1);
|
||||
|
||||
// Clean up
|
||||
try {
|
||||
if (tmpDirectory) {
|
||||
await OS.File.removeDir(tmpDirectory, { ignoreAbsent: true });
|
||||
}
|
||||
if (destDirectory) {
|
||||
await OS.File.removeDir(destDirectory, { ignoreAbsent: true });
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.debug(e, 1);
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return attachmentItem;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -208,14 +208,14 @@ Zotero.CollectionTreeRow.prototype.__defineGetter__('filesEditable', function ()
|
|||
}
|
||||
var libraryID = this.ref.libraryID;
|
||||
if (this.isGroup()) {
|
||||
return this.ref.filesEditable;
|
||||
return this.ref.editable && this.ref.filesEditable;
|
||||
}
|
||||
if (this.isCollection() || this.isSearch() || this.isDuplicates() || this.isUnfiled() || this.isRetracted()) {
|
||||
var type = Zotero.Libraries.get(libraryID).libraryType;
|
||||
if (type == 'group') {
|
||||
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
|
||||
var group = Zotero.Groups.get(groupID);
|
||||
return group.filesEditable;
|
||||
return group.editable && group.filesEditable;
|
||||
}
|
||||
throw ("Unknown library type '" + type + "' in Zotero.CollectionTreeRow.filesEditable");
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -446,6 +446,68 @@ Zotero.File = new function(){
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Asynchronously writes data from an nsIAsyncInputStream to a file.
|
||||
*
|
||||
* Designed to handle input streams where data may not be
|
||||
* immediately or fully available, such as network streams.
|
||||
*
|
||||
* @param {nsIInputStream} inputStream - The input stream to read from. This
|
||||
* stream should implement nsIAsyncInputStream.
|
||||
* @param {string} path - The file path where the data will be written.
|
||||
* @param {number} byteCount - The expected number of bytes to write.
|
||||
*
|
||||
* @returns {Promise<number>} A promise that resolves with the number of bytes
|
||||
* written when the operation is complete, or rejects with an error
|
||||
* if any issues occur during reading or writing.
|
||||
*/
|
||||
this.putNetworkStream = async function (path, stream, byteCount) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let bytesRead = 0;
|
||||
var os = FileUtils.openSafeFileOutputStream(new FileUtils.File(path));
|
||||
|
||||
let binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
|
||||
binaryInputStream.setInputStream(stream);
|
||||
|
||||
let readNextChunk = () => {
|
||||
stream.asyncWait({
|
||||
onInputStreamReady: (input) => {
|
||||
try {
|
||||
// Check available data in the stream
|
||||
let available = input.available();
|
||||
if (available > 0) {
|
||||
os.write(binaryInputStream.readBytes(available), available);
|
||||
bytesRead += available;
|
||||
|
||||
if (bytesRead < byteCount) {
|
||||
// Continue reading
|
||||
readNextChunk();
|
||||
}
|
||||
else {
|
||||
// Finished writing all expected bytes
|
||||
FileUtils.closeSafeFileOutputStream(os);
|
||||
resolve(bytesRead);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// No more data, finish the stream
|
||||
FileUtils.closeSafeFileOutputStream(os);
|
||||
resolve(bytesRead);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
os.close();
|
||||
reject(new Components.Exception("File write operation failed", e));
|
||||
}
|
||||
}
|
||||
}, 0, 0, null);
|
||||
};
|
||||
|
||||
// Start reading the first chunk of data
|
||||
readNextChunk();
|
||||
});
|
||||
};
|
||||
|
||||
this.download = async function (uri, path) {
|
||||
var uriStr = uri.spec || uri;
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ Zotero.Prompt = {
|
|||
Zotero.warn("Zotero.Prompt.confirm() option 'delayButtons' is deprecated -- use 'buttonDelay'");
|
||||
buttonDelay = true;
|
||||
}
|
||||
let flags = (buttonDelay && !Zotero.automatedTest) ? Services.prompt.BUTTON_DELAY_ENABLE : 0;
|
||||
let flags = (buttonDelay && !Zotero.test) ? Services.prompt.BUTTON_DELAY_ENABLE : 0;
|
||||
if (typeof button0 == 'number') flags += Services.prompt.BUTTON_POS_0 * button0;
|
||||
else if (typeof button0 == 'string') flags += Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING;
|
||||
if (typeof button1 == 'number') flags += Services.prompt.BUTTON_POS_1 * button1;
|
||||
|
|
|
@ -58,8 +58,8 @@ Zotero.RecognizeDocument = new function () {
|
|||
async function _processQueue() {
|
||||
await Zotero.Schema.schemaUpdatePromise;
|
||||
|
||||
if (_queueProcessing) return;
|
||||
_queueProcessing = true;
|
||||
if (_queueProcessing) return _queueProcessing.promise;
|
||||
_queueProcessing = Zotero.Promise.defer();
|
||||
|
||||
while (1) {
|
||||
// While all current progress queue usages are related with
|
||||
|
@ -99,6 +99,7 @@ Zotero.RecognizeDocument = new function () {
|
|||
}
|
||||
}
|
||||
|
||||
_queueProcessing.resolve();
|
||||
_queueProcessing = false;
|
||||
_processingItemID = null;
|
||||
}
|
||||
|
@ -253,6 +254,7 @@ Zotero.RecognizeDocument = new function () {
|
|||
* @return {Promise} A promise that resolves to a newly created, recognized parent item
|
||||
*/
|
||||
async function _processItem(attachment) {
|
||||
Zotero.debug(`RecognizeDocument: Recognizing attachment ${attachment.getDisplayTitle()}`);
|
||||
// Make sure the attachment still doesn't have a parent
|
||||
if (attachment.parentItemID) {
|
||||
throw new Error('Already has parent');
|
||||
|
@ -268,10 +270,12 @@ Zotero.RecognizeDocument = new function () {
|
|||
}
|
||||
}
|
||||
|
||||
let parentItem = await _recognize(attachment);
|
||||
let parentItem = await Zotero.RecognizeDocument._recognize(attachment);
|
||||
if (!parentItem) {
|
||||
Zotero.debug(`RecognizeDocument: No matches for attachment ${attachment.getDisplayTitle()}`);
|
||||
throw new Zotero.Exception.Alert("recognizePDF.noMatches");
|
||||
}
|
||||
Zotero.debug(`RecognizeDocument: Recognized attachment ${attachment.getDisplayTitle()}`);
|
||||
|
||||
// Put new item in same collections as the old one
|
||||
let collections = attachment.getCollections();
|
||||
|
@ -388,11 +392,7 @@ Zotero.RecognizeDocument = new function () {
|
|||
* @param {Zotero.Item} item
|
||||
* @return {Promise<Zotero.Item>} - New item
|
||||
*/
|
||||
async function _recognize(item) {
|
||||
if (Zotero.RecognizeDocument.recognizeStub) {
|
||||
return Zotero.RecognizeDocument.recognizeStub(item);
|
||||
}
|
||||
|
||||
this._recognize = async function (item) {
|
||||
let filePath = await item.getFilePath();
|
||||
|
||||
if (!filePath || !await OS.File.exists(filePath)) throw new Zotero.Exception.Alert('recognizePDF.fileNotFound');
|
||||
|
|
283
chrome/content/zotero/xpcom/server/saveSession.js
Normal file
283
chrome/content/zotero/xpcom/server/saveSession.js
Normal file
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
***** BEGIN LICENSE BLOCK *****
|
||||
|
||||
Copyright © 2024 Corporation for Digital Scholarship
|
||||
Vienna, 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 *****
|
||||
*/
|
||||
|
||||
Zotero.Server.Connector.SessionManager = {
|
||||
_sessions: new Map(),
|
||||
|
||||
get: function (id) {
|
||||
return this._sessions.get(id);
|
||||
},
|
||||
|
||||
create: function (id, action, requestData) {
|
||||
if (typeof id === 'undefined') {
|
||||
id = Zotero.Utilities.randomString();
|
||||
}
|
||||
if (this._sessions.has(id)) {
|
||||
throw new Error(`Session ID ${id} exists`);
|
||||
}
|
||||
Zotero.debug(`Creating connector save session ${id}`);
|
||||
var session = new Zotero.Server.Connector.SaveSession(id, action, requestData);
|
||||
this._sessions.set(id, session);
|
||||
this.gc();
|
||||
return session;
|
||||
},
|
||||
|
||||
gc: function () {
|
||||
// Delete sessions older than 10 minutes, or older than 1 minute if more than 10 sessions
|
||||
var ttl = this._sessions.size >= 10 ? 60 : 600;
|
||||
var deleteBefore = new Date() - ttl * 1000;
|
||||
|
||||
for (let session of this._sessions) {
|
||||
if (session.created < deleteBefore) {
|
||||
this._session.delete(session.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
Zotero.Server.Connector.SaveSession = class {
|
||||
constructor(id, action, requestData) {
|
||||
this.id = id;
|
||||
this.created = new Date();
|
||||
this._action = action;
|
||||
this._requestData = requestData;
|
||||
this._items = {};
|
||||
|
||||
this._progressItems = {};
|
||||
this._orderedProgressItems = [];
|
||||
}
|
||||
|
||||
async saveItems(target) {
|
||||
var { library, collection } = Zotero.Server.Connector.resolveTarget(target);
|
||||
var data = this._requestData.data;
|
||||
var headers = this._requestData.headers;
|
||||
var cookieSandbox = data.uri
|
||||
? new Zotero.CookieSandbox(
|
||||
null,
|
||||
data.uri,
|
||||
data.detailedCookies ? "" : data.cookie || "",
|
||||
headers["User-Agent"]
|
||||
)
|
||||
: null;
|
||||
if (cookieSandbox && data.detailedCookies) {
|
||||
cookieSandbox.addCookiesFromHeader(data.detailedCookies);
|
||||
}
|
||||
|
||||
var proxy = data.proxy && new Zotero.Proxy(data.proxy);
|
||||
|
||||
this.itemSaver = new Zotero.Translate.ItemSaver({
|
||||
libraryID: library.libraryID,
|
||||
collections: collection ? [collection.id] : undefined,
|
||||
// All attachments come from the Connector
|
||||
attachmentMode: Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE,
|
||||
forceTagType: 1,
|
||||
referrer: data.uri,
|
||||
cookieSandbox,
|
||||
proxy
|
||||
});
|
||||
let items = await this.itemSaver.saveItems(data.items, () => 0, () => 0);
|
||||
// If more itemSaver calls are made, it means we are saving attachments explicitly (like
|
||||
// a snapshot) and we don't want to ignore those.
|
||||
this.itemSaver.attachmentMode = Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD;
|
||||
items.forEach((item, index) => {
|
||||
this.addItem(data.items[index].id, item);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
async saveSnapshot(target) {
|
||||
var { library, collection } = Zotero.Server.Connector.resolveTarget(target);
|
||||
var libraryID = library.libraryID;
|
||||
var data = this._requestData.data;
|
||||
|
||||
let title = data.title || data.url;
|
||||
|
||||
// Create new webpage item
|
||||
let item = new Zotero.Item("webpage");
|
||||
item.libraryID = libraryID;
|
||||
item.setField("title", title);
|
||||
item.setField("url", data.url);
|
||||
item.setField("accessDate", "CURRENT_TIMESTAMP");
|
||||
if (collection) {
|
||||
item.setCollections([collection.id]);
|
||||
}
|
||||
await item.saveTx();
|
||||
|
||||
// SingleFile snapshot may be coming later
|
||||
this.addItem(data.url, item);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
async addItem(key, item) {
|
||||
return this.addItems({ [key]: item });
|
||||
}
|
||||
|
||||
async addItems(items) {
|
||||
this._items = Object.assign(this._items, items);
|
||||
|
||||
// Update the items with the current target data, in case it changed since the save began
|
||||
await this._updateItems(items);
|
||||
}
|
||||
|
||||
getItemByConnectorKey(key) {
|
||||
return this._items[key];
|
||||
}
|
||||
|
||||
// documentRecognizer doesn't return recognized items and it's complicated to make it
|
||||
// do it, so we just retrieve the parent item which is a little hacky but does the job
|
||||
getRecognizedItem() {
|
||||
try {
|
||||
return Object.values(this._items)[0].parentItem;
|
||||
}
|
||||
catch (_) {}
|
||||
}
|
||||
|
||||
remove() {
|
||||
delete Zotero.Server.Connector.SessionManager._sessions[this.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the target data for this session and update any items that have already been saved
|
||||
*/
|
||||
async update(targetID, tags) {
|
||||
var previousTargetID = this._currentTargetID;
|
||||
this._currentTargetID = targetID;
|
||||
this._currentTags = tags || "";
|
||||
|
||||
// Select new destination in collections pane
|
||||
var zp = Zotero.getActiveZoteroPane();
|
||||
if (zp && zp.collectionsView) {
|
||||
await zp.collectionsView.selectByID(targetID);
|
||||
}
|
||||
// If window is closed, select target collection re-open
|
||||
else {
|
||||
Zotero.Prefs.set('lastViewedFolder', targetID);
|
||||
}
|
||||
|
||||
await this._updateItems(this._items);
|
||||
|
||||
// If a single item was saved, select it (or its parent, if it now has one)
|
||||
if (zp && zp.collectionsView && Object.values(this._items).length == 1) {
|
||||
let item = Object.values(this._items)[0];
|
||||
item = item.isTopLevelItem() ? item : item.parentItem;
|
||||
// Don't select if in trash
|
||||
if (!item.deleted) {
|
||||
await zp.selectItem(item.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the passed items with the current target and tags
|
||||
*/
|
||||
_updateItems = Zotero.serial(async function (items) {
|
||||
if (Object.values(items).length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var { library, collection } = Zotero.Server.Connector.resolveTarget(this._currentTargetID);
|
||||
var libraryID = library.libraryID;
|
||||
|
||||
var tags = this._currentTags.trim();
|
||||
tags = tags ? tags.split(/\s*,\s*/).filter(x => x) : [];
|
||||
|
||||
Zotero.debug("Updating items for connector save session " + this.id);
|
||||
|
||||
for (let key in items) {
|
||||
let item = items[key];
|
||||
|
||||
// If the item is now a child item (e.g., from Retrieve Metadata), update the
|
||||
// parent item instead
|
||||
if (!item.isTopLevelItem()) {
|
||||
item = item.parentItem;
|
||||
}
|
||||
|
||||
// Skip deleted items
|
||||
if (!Zotero.Items.exists(item.id)) {
|
||||
Zotero.debug(`Item ${item.id} in save session no longer exists`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.libraryID != libraryID) {
|
||||
let newItem = await item.moveToLibrary(libraryID);
|
||||
this._items[key] = newItem;
|
||||
}
|
||||
|
||||
// Keep automatic tags
|
||||
let originalTags = item.getTags().filter(tag => tag.type == 1);
|
||||
item.setTags(originalTags.concat(tags));
|
||||
item.setCollections(collection ? [collection.id] : []);
|
||||
await item.saveTx();
|
||||
}
|
||||
|
||||
this._updateRecents();
|
||||
});
|
||||
|
||||
|
||||
_updateRecents() {
|
||||
var targetID = this._currentTargetID;
|
||||
try {
|
||||
let numRecents = 7;
|
||||
let recents = Zotero.Prefs.get('recentSaveTargets') || '[]';
|
||||
recents = JSON.parse(recents);
|
||||
// If there's already a target from this session in the list, update it
|
||||
for (let recent of recents) {
|
||||
if (recent.sessionID == this.id) {
|
||||
recent.id = targetID;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If a session is found with the same target, move it to the end without changing
|
||||
// the sessionID. This could be the current session that we updated above or a different
|
||||
// one. (We need to leave the old sessionID for the same target or we'll end up removing
|
||||
// the previous target from the history if it's changed in the current one.)
|
||||
let pos = recents.findIndex(r => r.id == targetID);
|
||||
if (pos != -1) {
|
||||
recents = [
|
||||
...recents.slice(0, pos),
|
||||
...recents.slice(pos + 1),
|
||||
recents[pos]
|
||||
];
|
||||
}
|
||||
// Otherwise just add this one to the end
|
||||
else {
|
||||
recents = recents.concat([{
|
||||
id: targetID,
|
||||
sessionID: this.id
|
||||
}]);
|
||||
}
|
||||
recents = recents.slice(-1 * numRecents);
|
||||
Zotero.Prefs.set('recentSaveTargets', JSON.stringify(recents));
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
Zotero.Prefs.clear('recentSaveTargets');
|
||||
}
|
||||
}
|
||||
};
|
|
@ -23,6 +23,9 @@
|
|||
***** END LICENSE BLOCK *****
|
||||
*/
|
||||
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");
|
||||
Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
Zotero.Server = new function() {
|
||||
var _onlineObserverRegistered, serv;
|
||||
this.responseCodes = {
|
||||
|
@ -42,50 +45,55 @@ Zotero.Server = new function() {
|
|||
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 = function(port, bindAllAddr, maxConcurrentConnections) {
|
||||
if (Zotero.HTTP.browserIsOffline()) {
|
||||
Zotero.debug('Browser is offline -- not initializing HTTP server');
|
||||
_registerOnlineObserver();
|
||||
return;
|
||||
}
|
||||
|
||||
this.init = function (port) {
|
||||
if(serv) {
|
||||
Zotero.debug("Already listening on port " + serv.port);
|
||||
return;
|
||||
}
|
||||
|
||||
// start listening on socket
|
||||
serv = Components.classes["@mozilla.org/network/server-socket;1"]
|
||||
.createInstance(Components.interfaces.nsIServerSocket);
|
||||
port = port || Zotero.Prefs.get('httpServer.port');
|
||||
try {
|
||||
// bind to a random port on loopback only
|
||||
serv.init(port ? port : Zotero.Prefs.get('httpServer.port'), !bindAllAddr, -1);
|
||||
serv.asyncListen(Zotero.Server.SocketListener);
|
||||
|
||||
Zotero.debug("HTTP server listening on "+(bindAllAddr ? "*": " 127.0.0.1")+":"+serv.port);
|
||||
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) {
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
Zotero.debug("Not initializing HTTP server");
|
||||
serv = undefined;
|
||||
}
|
||||
|
||||
_registerOnlineObserver()
|
||||
};
|
||||
|
||||
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.close();
|
||||
this.close = function () {
|
||||
if (!serv) return;
|
||||
serv.stop();
|
||||
serv = undefined;
|
||||
};
|
||||
|
||||
|
@ -102,294 +110,100 @@ Zotero.Server = new function() {
|
|||
}
|
||||
return decodedData;
|
||||
}
|
||||
|
||||
function _registerOnlineObserver() {
|
||||
if (_onlineObserverRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Observer to enable the integration when we go online
|
||||
var observer = {
|
||||
observe: function(subject, topic, data) {
|
||||
if (data == 'online') {
|
||||
Zotero.Server.init();
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
}
|
||||
};
|
||||
|
||||
var observerService =
|
||||
Components.classes["@mozilla.org/observer-service;1"]
|
||||
.getService(Components.interfaces.nsIObserverService);
|
||||
observerService.addObserver(observer, "network:offline-status-changed", false);
|
||||
|
||||
_onlineObserverRegistered = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Zotero.Server.SocketListener = new function() {
|
||||
this.onSocketAccepted = onSocketAccepted;
|
||||
this.onStopListening = onStopListening;
|
||||
|
||||
/*
|
||||
* called when a socket is opened
|
||||
*/
|
||||
function onSocketAccepted(socket, transport) {
|
||||
// get an input stream
|
||||
var iStream = transport.openInputStream(0, 0, 0);
|
||||
var oStream = transport.openOutputStream(Components.interfaces.nsITransport.OPEN_BLOCKING, 0, 0);
|
||||
|
||||
var dataListener = new Zotero.Server.DataListener(iStream, oStream);
|
||||
var pump = Components.classes["@mozilla.org/network/input-stream-pump;1"]
|
||||
.createInstance(Components.interfaces.nsIInputStreamPump);
|
||||
try {
|
||||
pump.init(iStream, 0, 0, false);
|
||||
}
|
||||
catch (e) {
|
||||
pump.init(iStream, -1, -1, 0, 0, false);
|
||||
}
|
||||
pump.asyncRead(dataListener, null);
|
||||
}
|
||||
|
||||
function onStopListening(serverSocket, status) {
|
||||
Zotero.debug("HTTP server going offline");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* handles the actual acquisition of data
|
||||
*/
|
||||
Zotero.Server.DataListener = function(iStream, oStream) {
|
||||
Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
||||
this.header = "";
|
||||
this.headerFinished = false;
|
||||
|
||||
Zotero.Server.networkStreamToString = function (stream, length) {
|
||||
let data = NetUtil.readInputStreamToString(stream, length);
|
||||
return Zotero.Utilities.Internal.decodeUTF8(data);
|
||||
};
|
||||
|
||||
|
||||
Zotero.Server.RequestHandler = function (request, response) {
|
||||
this.body = "";
|
||||
this.bodyLength = 0;
|
||||
|
||||
this.iStream = iStream;
|
||||
this.oStream = oStream;
|
||||
|
||||
this.foundReturn = false;
|
||||
}
|
||||
|
||||
/*
|
||||
* called when a request begins (although the request should have begun before
|
||||
* the DataListener was generated)
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype.onStartRequest = function(request) {}
|
||||
|
||||
/*
|
||||
* called when a request stops
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype.onStopRequest = function(request, status) {
|
||||
this.iStream.close();
|
||||
this.oStream.close();
|
||||
}
|
||||
|
||||
/*
|
||||
* called when new data is available
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype.onDataAvailable = function (request, inputStream, offset, count) {
|
||||
var readData = NetUtil.readInputStreamToString(inputStream, count);
|
||||
|
||||
if(this.headerFinished) { // reading body
|
||||
this.body += readData;
|
||||
// check to see if data is done
|
||||
this._bodyData();
|
||||
} else { // reading header
|
||||
// see if there's a magic double return
|
||||
var lineBreakIndex = readData.indexOf("\r\n\r\n");
|
||||
if(lineBreakIndex != -1) {
|
||||
if(lineBreakIndex != 0) {
|
||||
this.header += readData.substr(0, lineBreakIndex+4);
|
||||
this.body = readData.substr(lineBreakIndex+4);
|
||||
}
|
||||
|
||||
this._headerFinished();
|
||||
return;
|
||||
}
|
||||
var lineBreakIndex = readData.indexOf("\n\n");
|
||||
if(lineBreakIndex != -1) {
|
||||
if(lineBreakIndex != 0) {
|
||||
this.header += readData.substr(0, lineBreakIndex+2);
|
||||
this.body = readData.substr(lineBreakIndex+2);
|
||||
}
|
||||
|
||||
this._headerFinished();
|
||||
return;
|
||||
}
|
||||
if(this.header && this.header[this.header.length-1] == "\n" &&
|
||||
(readData[0] == "\n" || readData[0] == "\r")) {
|
||||
if(readData.length > 1 && readData[1] == "\n") {
|
||||
this.header += readData.substr(0, 2);
|
||||
this.body = readData.substr(2);
|
||||
} else {
|
||||
this.header += readData[0];
|
||||
this.body = readData.substr(1);
|
||||
}
|
||||
|
||||
this._headerFinished();
|
||||
return;
|
||||
}
|
||||
this.header += readData;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* processes an HTTP header and decides what to do
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype._headerFinished = function() {
|
||||
this.headerFinished = true;
|
||||
|
||||
Zotero.debug(this.header, 5);
|
||||
|
||||
// Parse headers into this.headers with lowercase names
|
||||
this.headers = {};
|
||||
var 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).toLowerCase();
|
||||
let v = line.substr(pos + 1).trim();
|
||||
this.headers[k] = v;
|
||||
}
|
||||
|
||||
if (this.headers.origin) {
|
||||
this.origin = this.headers.origin;
|
||||
}
|
||||
else if (this.headers['zotero-bookmarklet']) {
|
||||
this.origin = "https://www.zotero.org";
|
||||
}
|
||||
|
||||
if (!Zotero.isServer) {
|
||||
// Make sure the Host header is set to localhost/127.0.0.1 to prevent DNS rebinding attacks
|
||||
const hostRe = /^(localhost|127\.0\.0\.1)(:[0-9]+)?$/i;
|
||||
if (!hostRe.test(this.headers.host)) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid Host header\n"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// get first line of request
|
||||
const methodRe = /^([A-Z]+) ([^ \r\n?]+)(\?[^ \r\n]+)?/;
|
||||
var method = methodRe.exec(this.header);
|
||||
|
||||
// get content-type
|
||||
var contentType = this.headers['content-type'];
|
||||
if (contentType) {
|
||||
let splitContentType = contentType.split(/\s*;/);
|
||||
this.contentType = splitContentType[0];
|
||||
}
|
||||
|
||||
if(!method) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Invalid method specified\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.pathParams = {};
|
||||
if (Zotero.Server.Endpoints[method[2]]) {
|
||||
this.endpoint = Zotero.Server.Endpoints[method[2]];
|
||||
}
|
||||
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(method[2].split('?')[0])) { // Don't let parser handle query params - we do that already
|
||||
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;
|
||||
}
|
||||
}
|
||||
this.pathname = method[2];
|
||||
this.query = method[3];
|
||||
|
||||
if(method[1] == "HEAD" || method[1] == "OPTIONS") {
|
||||
this._requestFinished(this._generateResponse(200));
|
||||
} else if(method[1] == "GET") {
|
||||
this._processEndpoint("GET", null); // async
|
||||
} else if(method[1] == "POST") {
|
||||
const contentLengthRe = /^([0-9]+)$/;
|
||||
|
||||
// parse content length
|
||||
var m = contentLengthRe.exec(this.headers['content-length']);
|
||||
if(!m) {
|
||||
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"));
|
||||
return;
|
||||
}
|
||||
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.DataListener.prototype._bodyData = function() {
|
||||
if(this.body.length >= this.bodyLength) {
|
||||
let logContentTypes = [
|
||||
'text/plain',
|
||||
'application/json'
|
||||
];
|
||||
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 != '{}'
|
||||
&& logContentTypes.includes(this.contentType)
|
||||
&& 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("POST", this.body); // async
|
||||
}
|
||||
// handle envelope
|
||||
this._processEndpoint("POST", data); // async
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates the response to an HTTP request
|
||||
*/
|
||||
Zotero.Server.DataListener.prototype._generateResponse = function (status, contentTypeOrHeaders, body) {
|
||||
Zotero.Server.RequestHandler.prototype._generateResponse = function (status, contentTypeOrHeaders, body) {
|
||||
var response = "HTTP/1.0 "+status+" "+Zotero.Server.responseCodes[status]+"\r\n";
|
||||
|
||||
// Translation server
|
||||
if (Zotero.isServer) {
|
||||
// Add CORS headers if Origin header matches the allowed origins
|
||||
if (this.origin) {
|
||||
let allowedOrigins = Zotero.Prefs.get('httpServer.allowedOrigins')
|
||||
.split(/, */).filter(x => x);
|
||||
let allAllowed = allowedOrigins.includes('*');
|
||||
if (allAllowed || allowedOrigins.includes(this.origin)) {
|
||||
response += "Access-Control-Allow-Origin: " + (allAllowed ? '*' : this.origin) + "\r\n";
|
||||
response += "Access-Control-Allow-Methods: POST, GET, OPTIONS\r\n";
|
||||
response += "Access-Control-Allow-Headers: Content-Type\r\n";
|
||||
response += "Access-Control-Expose-Headers: Link\r\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
// Client
|
||||
else {
|
||||
response += "X-Zotero-Version: "+Zotero.version+"\r\n";
|
||||
response += "X-Zotero-Connector-API-Version: "+CONNECTOR_API_VERSION+"\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 (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) {
|
||||
|
@ -403,7 +217,7 @@ Zotero.Server.DataListener.prototype._generateResponse = function (status, conte
|
|||
}
|
||||
}
|
||||
|
||||
if(body) {
|
||||
if (body) {
|
||||
response += "\r\n"+body;
|
||||
} else {
|
||||
response += "Content-Length: 0\r\n\r\n";
|
||||
|
@ -412,34 +226,98 @@ Zotero.Server.DataListener.prototype._generateResponse = function (status, conte
|
|||
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) {
|
||||
requestDebug += `${name}: ${request.getHeader(name)}\n`;
|
||||
this.headers[name.toLowerCase()] = request.getHeader(name);
|
||||
}
|
||||
|
||||
Zotero.debug(requestDebug, 5);
|
||||
|
||||
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") {
|
||||
const contentLengthRe = /^([0-9]+)$/;
|
||||
|
||||
// parse content length
|
||||
var m = contentLengthRe.exec(this.headers['content-length']);
|
||||
if(!m) {
|
||||
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.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine(function* (method, postData) {
|
||||
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) {
|
||||
if (endpoint.supportedMethods && endpoint.supportedMethods.indexOf(method) === -1) {
|
||||
this._requestFinished(this._generateResponse(400, "text/plain", "Endpoint does not support method\n"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that endpoint supports bookmarklet
|
||||
if(this.origin) {
|
||||
var isBookmarklet = this.origin === "https://www.zotero.org" || this.origin === "http://www.zotero.org";
|
||||
// Disallow bookmarklet origins to access endpoints without permitBookmarklet
|
||||
// set. We allow other origins to access these endpoints because they have to
|
||||
// be privileged to avoid being blocked by our headers.
|
||||
if(isBookmarklet && !endpoint.permitBookmarklet) {
|
||||
this._requestFinished(this._generateResponse(403, "text/plain", "Access forbidden to bookmarklet\n"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Reject browser-based requests that don't require a CORS preflight request [1] if they
|
||||
// don't come from the connector or include Zotero-Allowed-Request
|
||||
//
|
||||
|
@ -471,53 +349,44 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
|
|||
return;
|
||||
}
|
||||
|
||||
var decodedData = null;
|
||||
if(postData && this.contentType) {
|
||||
var data = null;
|
||||
if (method === 'POST' && this.contentType) {
|
||||
// check that endpoint supports contentType
|
||||
var supportedDataTypes = endpoint.supportedDataTypes;
|
||||
if(supportedDataTypes && 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") {
|
||||
if (this.contentType === "application/json") {
|
||||
try {
|
||||
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
|
||||
decodedData = JSON.parse(postData);
|
||||
} catch(e) {
|
||||
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") {
|
||||
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
|
||||
decodedData = Zotero.Server.decodeQueryString(postData);
|
||||
} else if(this.contentType === "multipart/form-data") {
|
||||
let boundary = /boundary=([^\s]*)/i.exec(this.header);
|
||||
if (!boundary) {
|
||||
Zotero.debug('Invalid boundary: ' + this.header, 1);
|
||||
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
||||
}
|
||||
boundary = '--' + boundary[1];
|
||||
try {
|
||||
decodedData = this._decodeMultipartData(postData, boundary);
|
||||
} catch(e) {
|
||||
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
||||
}
|
||||
} else {
|
||||
postData = Zotero.Utilities.Internal.decodeUTF8(postData);
|
||||
decodedData = postData;
|
||||
}
|
||||
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 = function (code, contentTypeOrHeaders, arg, options) {
|
||||
var sendResponseCallback = (code, contentTypeOrHeaders, arg, options) => {
|
||||
this._requestFinished(
|
||||
this._generateResponse(code, contentTypeOrHeaders, arg),
|
||||
options
|
||||
);
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
// Pass to endpoint
|
||||
//
|
||||
|
@ -528,30 +397,18 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
|
|||
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,
|
||||
pathParams: this.pathParams,
|
||||
searchParams: new URLSearchParams(this.query ? this.query.substring(1) : ''),
|
||||
headers,
|
||||
data: decodedData
|
||||
searchParams: new URLSearchParams(this.query || ''),
|
||||
headers: this.headers,
|
||||
data
|
||||
});
|
||||
let result;
|
||||
if (maybePromise.then) {
|
||||
result = yield maybePromise;
|
||||
result = await maybePromise;
|
||||
}
|
||||
else {
|
||||
result = maybePromise;
|
||||
|
@ -565,71 +422,75 @@ Zotero.Server.DataListener.prototype._processEndpoint = Zotero.Promise.coroutine
|
|||
}
|
||||
// Two-parameter endpoint takes data and a callback
|
||||
else if (endpoint.init.length === 2) {
|
||||
endpoint.init(decodedData, sendResponseCallback);
|
||||
endpoint.init(data, 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);
|
||||
var url = {
|
||||
pathname: this.pathname,
|
||||
searchParams: new URLSearchParams(this.query ? this.query.substring(1) : ''),
|
||||
userAgent: m && m[1]
|
||||
searchParams: new URLSearchParams(this.query || ''),
|
||||
userAgent: this.headers['user-agent']
|
||||
};
|
||||
endpoint.init(url, decodedData, sendResponseCallback);
|
||||
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.DataListener.prototype._requestFinished = function (response, options) {
|
||||
if(this._responseSent) {
|
||||
Zotero.Server.RequestHandler.prototype._requestFinished = function (responseBody, options) {
|
||||
if (this._responseSent) {
|
||||
Zotero.debug("Request already finished; not sending another response");
|
||||
return;
|
||||
}
|
||||
this._responseSent = true;
|
||||
|
||||
// close input stream
|
||||
this.iStream.close();
|
||||
|
||||
// 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.oStream, "UTF-8", 1024, "?".charCodeAt(0));
|
||||
intlStream.init(this.response.bodyOutputStream, "UTF-8", 1024, "?".charCodeAt(0));
|
||||
|
||||
// Filter logged response
|
||||
if (Zotero.Debug.enabled) {
|
||||
let maxLogLength = 2000;
|
||||
let str = response;
|
||||
let str = responseBody;
|
||||
if (options && options.logFilter) {
|
||||
str = options.logFilter(str);
|
||||
}
|
||||
if (str.length > maxLogLength) {
|
||||
str = str.substr(0, maxLogLength) + `\u2026 (${response.length} chars)`;
|
||||
str = str.substr(0, maxLogLength) + `\u2026 (${responseBody.length} chars)`;
|
||||
}
|
||||
Zotero.debug(str, 5);
|
||||
}
|
||||
|
||||
intlStream.writeString(response);
|
||||
} finally {
|
||||
intlStream.close();
|
||||
intlStream.writeString(responseBody);
|
||||
}
|
||||
finally {
|
||||
this.response.finish();
|
||||
}
|
||||
}
|
||||
|
||||
Zotero.Server.DataListener.prototype._decodeMultipartData = function(data, boundary) {
|
||||
var contentDispositionRe = /^Content-Disposition:\s*(.*)$/i;
|
||||
let contentTypeRe = /^Content-Type:\s*(.*)$/i
|
||||
Zotero.Server.RequestHandler.prototype._decodeMultipartData = function(data) {
|
||||
const contentDispositionRe = /^Content-Disposition:\s*(.*)$/i;
|
||||
const contentTypeRe = /^Content-Type:\s*(.*)$/i
|
||||
|
||||
var results = [];
|
||||
let results = [];
|
||||
|
||||
let boundary = /boundary=([^\s]*)/i.exec(this.headers['content-type']);
|
||||
if (!boundary) {
|
||||
Zotero.debug('Invalid boundary: ' + this.headers['content-type'], 1);
|
||||
return this._requestFinished(this._generateResponse(400, "text/plain", "Invalid multipart/form-data provided\n"));
|
||||
}
|
||||
boundary = '--' + boundary[1];
|
||||
|
||||
data = data.split(boundary);
|
||||
// Ignore pre first boundary and post last boundary
|
||||
data = data.slice(1, data.length-1);
|
1240
chrome/content/zotero/xpcom/server/server_connector.js
Normal file
1240
chrome/content/zotero/xpcom/server/server_connector.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -59,6 +59,7 @@ Zotero.Translate.ItemSaver = function(options) {
|
|||
this._referrer = options.referrer;
|
||||
this._cookieSandbox = options.cookieSandbox;
|
||||
this._proxy = options.proxy;
|
||||
this._itemToJSONItem = new Map();
|
||||
|
||||
// the URI to which other URIs are assumed to be relative
|
||||
if(typeof options.baseURI === "object" && options.baseURI instanceof Components.interfaces.nsIURI) {
|
||||
|
@ -82,6 +83,7 @@ Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES = new Set([
|
|||
]);
|
||||
|
||||
Zotero.Translate.ItemSaver.prototype = {
|
||||
|
||||
/**
|
||||
* Saves items to Standalone or the server
|
||||
* @param {Object[]} jsonItems - Items in Zotero.Item.toArray() format
|
||||
|
@ -90,21 +92,17 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
* on failure or attachmentCallback(attachment, progressPercent) periodically during saving.
|
||||
* @param {Function} [itemsDoneCallback] A callback that is called once all top-level items are
|
||||
* done saving with a list of items. Will include saved notes, but exclude attachments.
|
||||
* @param {Function} [pendingAttachmentsCallback] A callback that is called for every
|
||||
* pending attachment to an item. pendingAttachmentsCallback(parentItemID, jsonAttachment)
|
||||
*/
|
||||
saveItems: async function (jsonItems, attachmentCallback, itemsDoneCallback, pendingAttachmentsCallback) {
|
||||
saveItems: async function (jsonItems, attachmentCallback, itemsDoneCallback) {
|
||||
var items = [];
|
||||
var standaloneAttachments = [];
|
||||
var childAttachments = [];
|
||||
var jsonByItem = new Map();
|
||||
|
||||
await Zotero.DB.executeTransaction(async function () {
|
||||
for (let jsonItem of jsonItems) {
|
||||
jsonItem = Object.assign({}, jsonItem);
|
||||
|
||||
let item;
|
||||
let itemID;
|
||||
// Type defaults to "webpage"
|
||||
let type = jsonItem.itemType || "webpage";
|
||||
|
||||
|
@ -121,84 +119,25 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
continue;
|
||||
}
|
||||
else {
|
||||
item = new Zotero.Item(type);
|
||||
item.libraryID = this._libraryID;
|
||||
if (jsonItem.creators) this._cleanCreators(jsonItem.creators);
|
||||
if (jsonItem.tags) jsonItem.tags = this._cleanTags(jsonItem.tags);
|
||||
|
||||
if (jsonItem.accessDate == 'CURRENT_TIMESTAMP') {
|
||||
jsonItem.accessDate = Zotero.Date.dateToISO(new Date());
|
||||
}
|
||||
|
||||
item.fromJSON(this._copyJSONItemForImport(jsonItem));
|
||||
|
||||
// deproxify url
|
||||
if (this._proxy && jsonItem.url) {
|
||||
let url = this._proxy.toProper(jsonItem.url);
|
||||
Zotero.debug(`Deproxifying item url ${jsonItem.url} with scheme ${this._proxy.scheme} to ${url}`, 5);
|
||||
item.setField('url', url);
|
||||
}
|
||||
|
||||
if (this._collections) {
|
||||
item.setCollections(this._collections);
|
||||
}
|
||||
|
||||
// save item
|
||||
itemID = await item.save(this._saveOptions);
|
||||
|
||||
// handle notes
|
||||
if (jsonItem.notes) {
|
||||
for (let note of jsonItem.notes) {
|
||||
await this._saveNote(note, itemID);
|
||||
}
|
||||
}
|
||||
item = await this._saveItem(jsonItem, type);
|
||||
|
||||
// handle attachments
|
||||
if (jsonItem.attachments) {
|
||||
let attachmentsToSave = [];
|
||||
let foundPrimary = false;
|
||||
for (let jsonAttachment of jsonItem.attachments) {
|
||||
if (!this._canSaveAttachment(jsonAttachment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The first PDF/EPUB is the primary one. If that one fails to download,
|
||||
// we might check for an open-access PDF below.
|
||||
if (Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(jsonAttachment.mimeType)
|
||||
&& !foundPrimary) {
|
||||
jsonAttachment.isPrimary = true;
|
||||
foundPrimary = true;
|
||||
}
|
||||
attachmentsToSave.push(jsonAttachment);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
if (jsonAttachment.singleFile) {
|
||||
// SingleFile attachments are saved in 'saveSingleFile'
|
||||
// connector endpoint
|
||||
if (pendingAttachmentsCallback) {
|
||||
pendingAttachmentsCallback(itemID, jsonAttachment);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
childAttachments.push([jsonAttachment, itemID]);
|
||||
}
|
||||
jsonItem.attachments = attachmentsToSave;
|
||||
}
|
||||
|
||||
// handle see also
|
||||
this._handleRelated(jsonItem, item);
|
||||
// process attachments
|
||||
let attachments = this._processChildAttachments(jsonItem, attachmentCallback);
|
||||
attachments.forEach(attachment => childAttachments.push([attachment, item.id]));
|
||||
}
|
||||
|
||||
// Add to new item list
|
||||
items.push(item);
|
||||
jsonByItem.set(item, jsonItem);
|
||||
this._itemToJSONItem.set(item, jsonItem);
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
// Done saving top-level items. Call the callback so that UI code can update
|
||||
if (itemsDoneCallback) {
|
||||
itemsDoneCallback(items.map(item => jsonByItem.get(item)), items);
|
||||
itemsDoneCallback(items.map(item => this._itemToJSONItem.get(item)), items);
|
||||
}
|
||||
|
||||
// Save standalone attachments
|
||||
// Download standalone attachments
|
||||
for (let jsonItem of standaloneAttachments) {
|
||||
let item = await this._saveAttachment(jsonItem, null, attachmentCallback);
|
||||
if (item) {
|
||||
|
@ -210,45 +149,21 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
// open-access PDFs. There's no guarantee that either translated PDFs or OA PDFs will
|
||||
// successfully download, but this lets us update the progress window sooner with
|
||||
// possible downloads.
|
||||
//
|
||||
this._openAccessPDFURLs = new Map();
|
||||
|
||||
// TODO: Separate pref?
|
||||
var shouldDownloadOAPDF = this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD
|
||||
&& Zotero.Prefs.get('downloadAssociatedFiles');
|
||||
var openAccessPDFURLs = new Map();
|
||||
&& Zotero.Prefs.get('downloadAssociatedFiles');
|
||||
if (shouldDownloadOAPDF) {
|
||||
for (let item of items) {
|
||||
let jsonItem = jsonByItem.get(item);
|
||||
|
||||
// Skip items with translated PDF attachments
|
||||
if (jsonItem.attachments
|
||||
&& jsonItem.attachments.some(x => Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(x.mimeType))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
let resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
|
||||
if (!resolvers.length) {
|
||||
openAccessPDFURLs.set(item, []);
|
||||
continue;
|
||||
}
|
||||
let urlObjects = await resolvers[0]();
|
||||
openAccessPDFURLs.set(item, urlObjects);
|
||||
// If there are possible URLs, create a status line for the PDF
|
||||
if (urlObjects.length) {
|
||||
let title = Zotero.getString('findPDF.openAccessPDF');
|
||||
let jsonAttachment = this._makeJSONAttachment(jsonItem.id, title);
|
||||
if (!jsonItem.attachments) jsonItem.attachments = [];
|
||||
jsonItem.attachments.push(jsonAttachment);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
let urlObjects = await this._getOpenAccessPDFURLs(item, attachmentCallback);
|
||||
if (urlObjects) {
|
||||
this._openAccessPDFURLs.set(item, urlObjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save translated child attachments, and keep track of whether the save was successful
|
||||
// Save translated child attachments
|
||||
var itemIDsWithPrimaryAttachments = new Set();
|
||||
for (let [jsonAttachment, parentItemID] of childAttachments) {
|
||||
let attachment = await this._saveAttachment(
|
||||
|
@ -267,93 +182,15 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
}
|
||||
}
|
||||
|
||||
// If a translated PDF attachment wasn't saved successfully, either because there wasn't
|
||||
// one or there was but it failed, look for another PDF (if enabled)
|
||||
if (shouldDownloadOAPDF) {
|
||||
// If a translated PDF attachment wasn't saved successfully, either because there wasn't
|
||||
// one or there was but it failed, look for another PDF (if enabled)
|
||||
for (let item of items) {
|
||||
// Already have a primary attachment from translation
|
||||
if (itemIDsWithPrimaryAttachments.has(item.id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let jsonItem = jsonByItem.get(item);
|
||||
// Reuse the existing status line if there is one. This could be a failed
|
||||
// translator attachment or a possible OA PDF found above.
|
||||
// Explicitly check that the attachment is a PDF, not just any primary type,
|
||||
// since we're reusing it for a PDF attachment.
|
||||
let jsonAttachment = jsonItem.attachments && jsonItem.attachments.find(
|
||||
x => x.mimeType == 'application/pdf' && x.isPrimary
|
||||
);
|
||||
|
||||
// If no translated, no OA, and no custom, don't show a line
|
||||
// If no translated and potential OA, show "Open-Access PDF"
|
||||
// If no translated, no OA, but custom, show custom when it starts
|
||||
// If translated fails and potential OA, show "Open-Access PDF"
|
||||
// If translated fails, no OA, no custom, fail original
|
||||
// If translated fails, no OA, but custom, change to custom when it starts
|
||||
let resolvers = openAccessPDFURLs.get(item);
|
||||
// No translated PDF, so we checked for OA PDFs above
|
||||
if (resolvers) {
|
||||
// Add custom resolvers
|
||||
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
|
||||
|
||||
// No translated, no OA, no custom, no status line
|
||||
if (!resolvers.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// No translated, no OA, just potential custom, so create a status line
|
||||
if (!jsonAttachment) {
|
||||
jsonAttachment = this._makeJSONAttachment(
|
||||
jsonItem.id, Zotero.getString('findPDF.searchingForAvailableFiles')
|
||||
);
|
||||
}
|
||||
}
|
||||
// There was a translated PDF, so we didn't check for OA PDFs yet and didn't
|
||||
// update the status line
|
||||
else {
|
||||
// Look for OA PDFs now
|
||||
resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
|
||||
if (resolvers.length) {
|
||||
resolvers = await resolvers[0]();
|
||||
}
|
||||
|
||||
// Add custom resolvers
|
||||
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
|
||||
|
||||
// Failed translated, no OA, no custom, so fail the existing translator line
|
||||
if (!resolvers.length) {
|
||||
attachmentCallback(jsonAttachment, false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let attachment;
|
||||
try {
|
||||
attachment = await Zotero.Attachments.addFileFromURLs(
|
||||
item,
|
||||
resolvers,
|
||||
{
|
||||
// When a new access method starts, update the status line
|
||||
onAccessMethodStart: (method) => {
|
||||
jsonAttachment.title = this._getPDFTitleForAccessMethod(method);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
attachmentCallback(jsonAttachment, false, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
attachmentCallback(jsonAttachment, 100);
|
||||
}
|
||||
else {
|
||||
attachmentCallback(jsonAttachment, false, "PDF not found");
|
||||
}
|
||||
await this.saveOpenAccessAttachment(item, attachmentCallback);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -364,23 +201,233 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
/**
|
||||
* Save pending snapshot attachments to disk and library
|
||||
*
|
||||
* @param {Array} pendingAttachments - A list of snapshot attachments
|
||||
* @param {Object} content - Snapshot content from SingleFile
|
||||
* @param {Function} attachmentCallback - Callback with progress of attachments
|
||||
* @param {Object} options - A list of snapshot attachments
|
||||
* - title {String}
|
||||
* - url {String}
|
||||
* - parentItemID {Number}
|
||||
* - snapshotContent {String}
|
||||
*/
|
||||
saveSnapshotAttachments: Zotero.Promise.coroutine(function* (pendingAttachments, snapshotContent, attachmentCallback) {
|
||||
for (let [parentItemID, attachment] of pendingAttachments) {
|
||||
Zotero.debug('Saving pending attachment: ' + JSON.stringify(attachment));
|
||||
if (snapshotContent) {
|
||||
attachment.snapshotContent = snapshotContent;
|
||||
}
|
||||
yield this._saveAttachment(
|
||||
saveSnapshotAttachments: async function (options) {
|
||||
let { title, url, parentItemID, snapshotContent } = options;
|
||||
let attachment = { title, url };
|
||||
Zotero.debug('Saving pending attachment: ' + JSON.stringify(attachment));
|
||||
if (snapshotContent) {
|
||||
attachment.snapshotContent = snapshotContent;
|
||||
}
|
||||
await new Promise(async (resolve, reject) => {
|
||||
await this._saveAttachment(
|
||||
attachment,
|
||||
parentItemID,
|
||||
attachmentCallback
|
||||
(attachment, progress, e) => {
|
||||
if (e) reject(e);
|
||||
if (progress === 100) {
|
||||
resolve(progress);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
async _saveItem(jsonItem, type) {
|
||||
let itemID;
|
||||
let item = new Zotero.Item(type);
|
||||
item.libraryID = this._libraryID;
|
||||
if (jsonItem.creators) this._cleanCreators(jsonItem.creators);
|
||||
if (jsonItem.tags) jsonItem.tags = this._cleanTags(jsonItem.tags);
|
||||
|
||||
if (jsonItem.accessDate == 'CURRENT_TIMESTAMP') {
|
||||
jsonItem.accessDate = Zotero.Date.dateToISO(new Date());
|
||||
}
|
||||
|
||||
item.fromJSON(this._copyJSONItemForImport(jsonItem));
|
||||
|
||||
// deproxify url
|
||||
if (this._proxy && jsonItem.url) {
|
||||
let url = this._proxy.toProper(jsonItem.url);
|
||||
Zotero.debug(`Deproxifying item url ${jsonItem.url} with scheme ${this._proxy.scheme} to ${url}`, 5);
|
||||
item.setField('url', url);
|
||||
}
|
||||
|
||||
// save item
|
||||
if (this._collections) {
|
||||
item.setCollections(this._collections);
|
||||
}
|
||||
itemID = await item.save(this._saveOptions);
|
||||
|
||||
// handle notes
|
||||
if (jsonItem.notes) {
|
||||
for (let note of jsonItem.notes) {
|
||||
await this._saveNote(note, itemID);
|
||||
}
|
||||
}
|
||||
|
||||
// handle see also
|
||||
this._handleRelated(jsonItem, item);
|
||||
return item;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Processes attachments to be saved either via Zotero or externally (Connector)
|
||||
*
|
||||
* Calls attachment callbacks for initial attachment progress (0)
|
||||
*/
|
||||
_processChildAttachments(jsonItem, attachmentCallback) {
|
||||
let childAttachments = [];
|
||||
|
||||
let foundPrimary = false;
|
||||
// Attachments to be saved within Zotero
|
||||
if (jsonItem.attachments) {
|
||||
let attachmentsToSave = [];
|
||||
for (let jsonAttachment of jsonItem.attachments) {
|
||||
if (!this._canSaveAttachment(jsonAttachment)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The first PDF/EPUB is the primary one. If that one fails to download,
|
||||
// we might check for an open-access PDF below.
|
||||
if (Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(jsonAttachment.mimeType)
|
||||
&& !foundPrimary) {
|
||||
jsonAttachment.isPrimary = true;
|
||||
foundPrimary = true;
|
||||
}
|
||||
attachmentsToSave.push(jsonAttachment);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
childAttachments.push(jsonAttachment);
|
||||
}
|
||||
|
||||
jsonItem.attachments = attachmentsToSave;
|
||||
}
|
||||
return childAttachments;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets a list of OA PDF URLs for items that did not receive a PDF attachment
|
||||
* from the translator
|
||||
*
|
||||
* Calls attachmentCallback to update UI
|
||||
* @param items
|
||||
* @param attachmentCallback
|
||||
* @returns {Promise<Map<any, any>>}
|
||||
* @private
|
||||
*/
|
||||
async _getOpenAccessPDFURLs(item, attachmentCallback) {
|
||||
let jsonItem = this._itemToJSONItem.get(item);
|
||||
let urlObjects = [];
|
||||
|
||||
// Has a primary attachment or a pending (from Connector) primary attachment
|
||||
if (jsonItem.attachments?.some(x => Zotero.Translate.ItemSaver.PRIMARY_ATTACHMENT_TYPES.has(x.mimeType))
|
||||
|| jsonItem.pendingPrimaryAttachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If no primary attachments available look for an OA one and call attachmentCallback to update UI
|
||||
try {
|
||||
let resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
|
||||
if (!resolvers.length) {
|
||||
return urlObjects;
|
||||
}
|
||||
urlObjects = await resolvers[0]();
|
||||
// If there are possible URLs, create a status line for the PDF
|
||||
if (urlObjects.length) {
|
||||
let title = Zotero.getString('findPDF.openAccessPDF');
|
||||
let jsonAttachment = this._makeJSONAttachment(jsonItem.id, title);
|
||||
if (!jsonItem.attachments) jsonItem.attachments = [];
|
||||
jsonItem.attachments.push(jsonAttachment);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
return urlObjects;
|
||||
},
|
||||
|
||||
async saveOpenAccessAttachment(item, attachmentCallback) {
|
||||
let jsonItem = this._itemToJSONItem.get(item);
|
||||
// Reuse the existing status line if there is one. This could be a failed
|
||||
// translator attachment or a possible OA PDF found above.
|
||||
// Explicitly check that the attachment is a PDF, not just any primary type,
|
||||
// since we're reusing it for a PDF attachment.
|
||||
let jsonAttachment = jsonItem.attachments && jsonItem.attachments.find(
|
||||
x => x.mimeType == 'application/pdf' && x.isPrimary
|
||||
);
|
||||
|
||||
|
||||
// If no translated, no OA, no custom, don't show a line
|
||||
// If translated fails, no OA, no custom, fail original
|
||||
|
||||
// If no translated, potential OA, show "Open-Access PDF" (set in _getOpenAccessPDFURLs())
|
||||
// If translated fails, potential OA, show "Open-Access PDF" (set here)
|
||||
|
||||
// If no translated
|
||||
// or translated fails, no OA, but custom, show custom when it starts
|
||||
|
||||
let resolvers = this._openAccessPDFURLs.get(item);
|
||||
// We checked for OA PDFs in _getOpenAccessPDFURLs() so there was no translated pdf
|
||||
if (resolvers) {
|
||||
// Add custom resolvers
|
||||
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
|
||||
|
||||
// No translated, no OA, no custom, no status line
|
||||
if (!resolvers.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// No translated, no OA, just potential custom, so create a status line
|
||||
if (!jsonAttachment) {
|
||||
jsonAttachment = this._makeJSONAttachment(
|
||||
jsonItem.id, Zotero.getString('findPDF.searchingForAvailableFiles')
|
||||
);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Translated attachment failed, so we didn't check for OA PDFs yet and didn't
|
||||
// update the status line
|
||||
// Look for OA PDFs now
|
||||
resolvers = Zotero.Attachments.getPDFResolvers(item, ['oa']);
|
||||
if (resolvers.length) {
|
||||
resolvers = await resolvers[0]();
|
||||
}
|
||||
|
||||
// Add custom resolvers
|
||||
resolvers.push(...Zotero.Attachments.getPDFResolvers(item, ['custom'], true));
|
||||
|
||||
// Failed translated, no OA, no custom, so fail the existing translator line
|
||||
if (!resolvers.length) {
|
||||
attachmentCallback(jsonAttachment, false);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let attachment;
|
||||
try {
|
||||
attachment = await Zotero.Attachments.addFileFromURLs(
|
||||
item,
|
||||
resolvers,
|
||||
{
|
||||
// When a new access method starts, update the status line
|
||||
onAccessMethodStart: (method) => {
|
||||
jsonAttachment.title = this._getPDFTitleForAccessMethod(method);
|
||||
attachmentCallback(jsonAttachment, 0);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}),
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
attachmentCallback(jsonAttachment, false, e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (attachment) {
|
||||
attachmentCallback(jsonAttachment, 100);
|
||||
}
|
||||
else {
|
||||
attachmentCallback(jsonAttachment, false, "PDF not found");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
_makeJSONAttachment: function (parentID, title) {
|
||||
|
@ -534,16 +581,23 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
try {
|
||||
let newAttachment;
|
||||
|
||||
const isSinglefileSnapshot = !!attachment.snapshotContent;
|
||||
// determine whether to save files and attachments
|
||||
var isLink = Zotero.MIME.isWebPageType(attachment.mimeType)
|
||||
// .snapshot coming from most translators, .linkMode coming from RDF
|
||||
&& (attachment.snapshot === false || attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL);
|
||||
if (isLink || this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) {
|
||||
// .snapshot coming from most translators, .linkMode coming from RDF
|
||||
var isLink = attachment.snapshot === false
|
||||
|| attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL;
|
||||
|
||||
if (isLink || this.attachmentMode === Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE) {
|
||||
newAttachment = yield this._saveAttachmentLink.apply(this, arguments);
|
||||
}
|
||||
else if (isSinglefileSnapshot || this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD) {
|
||||
newAttachment = yield this._saveAttachmentDownload.apply(this, arguments);
|
||||
} else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) {
|
||||
}
|
||||
else if (this.attachmentMode == Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE) {
|
||||
newAttachment = yield this._saveAttachmentFile.apply(this, arguments);
|
||||
} else {
|
||||
Zotero.debug('Translate: Ignoring attachment due to ATTACHMENT_MODE_IGNORE');
|
||||
}
|
||||
else {
|
||||
Zotero.debug(`Translate: Ignoring attachment ${attachment.title} due to ATTACHMENT_MODE_IGNORE`);
|
||||
}
|
||||
|
||||
if (!newAttachment) return false; // attachmentCallback should not have been called in this case
|
||||
|
@ -821,6 +875,50 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
return false;
|
||||
},
|
||||
|
||||
_saveAttachmentLink: async function(attachment, parentItemID, attachmentCallback) {
|
||||
attachment.linkMode = "linked_url";
|
||||
let url, mimeType;
|
||||
if(attachment.document) {
|
||||
url = attachment.document.location.href;
|
||||
mimeType = attachment.mimeType || attachment.document.contentType;
|
||||
} else {
|
||||
url = attachment.url
|
||||
mimeType = attachment.mimeType || undefined;
|
||||
}
|
||||
|
||||
// If no title provided, use "Attachment" as title for progress UI (but not for item)
|
||||
let title = attachment.title || null;
|
||||
if(!attachment.title) {
|
||||
attachment.title = Zotero.getString("itemTypes.attachment");
|
||||
}
|
||||
|
||||
if(!mimeType || !title) {
|
||||
Zotero.debug("Translate: mimeType or title is missing; attaching link to URL will be slower");
|
||||
}
|
||||
|
||||
let cleanURI = Zotero.Attachments.cleanAttachmentURI(url);
|
||||
if (!cleanURI) {
|
||||
throw new Error("Translate: Invalid attachment URL specified <" + url + ">");
|
||||
}
|
||||
url = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService)
|
||||
.newURI(cleanURI, null, null); // This cannot fail, since we check above
|
||||
|
||||
// Only HTTP/HTTPS links are allowed
|
||||
if(url.scheme != "http" && url.scheme != "https") {
|
||||
throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators.");
|
||||
}
|
||||
|
||||
return Zotero.Attachments.linkFromURL({
|
||||
url: cleanURI,
|
||||
parentItemID,
|
||||
contentType: mimeType,
|
||||
title,
|
||||
collections: !parentItemID ? this._collections : undefined,
|
||||
saveOptions: this._saveOptions,
|
||||
});
|
||||
},
|
||||
|
||||
_saveAttachmentDownload: Zotero.Promise.coroutine(function* (attachment, parentItemID, attachmentCallback) {
|
||||
Zotero.debug("Translate: Adding attachment", 4);
|
||||
|
||||
|
@ -839,51 +937,11 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
// Commit to saving
|
||||
attachmentCallback(attachment, 0);
|
||||
|
||||
var isLink = attachment.snapshot === false
|
||||
|| attachment.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL;
|
||||
if (isLink || this.attachmentMode === Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE) {
|
||||
// if snapshot is explicitly set to false, attach as link
|
||||
attachment.linkMode = "linked_url";
|
||||
let url, mimeType;
|
||||
if(attachment.document) {
|
||||
url = attachment.document.location.href;
|
||||
mimeType = attachment.mimeType || attachment.document.contentType;
|
||||
} else {
|
||||
url = attachment.url
|
||||
mimeType = attachment.mimeType || undefined;
|
||||
}
|
||||
|
||||
if(!mimeType || !title) {
|
||||
Zotero.debug("Translate: mimeType or title is missing; attaching link to URL will be slower");
|
||||
}
|
||||
|
||||
let cleanURI = Zotero.Attachments.cleanAttachmentURI(url);
|
||||
if (!cleanURI) {
|
||||
throw new Error("Translate: Invalid attachment URL specified <" + url + ">");
|
||||
}
|
||||
url = Components.classes["@mozilla.org/network/io-service;1"]
|
||||
.getService(Components.interfaces.nsIIOService)
|
||||
.newURI(cleanURI, null, null); // This cannot fail, since we check above
|
||||
|
||||
// Only HTTP/HTTPS links are allowed
|
||||
if(url.scheme != "http" && url.scheme != "https") {
|
||||
throw new Error("Translate: " + url.scheme + " protocol is not allowed for attachments from translators.");
|
||||
}
|
||||
|
||||
return Zotero.Attachments.linkFromURL({
|
||||
url: cleanURI,
|
||||
parentItemID,
|
||||
contentType: mimeType,
|
||||
title,
|
||||
collections: !parentItemID ? this._collections : undefined,
|
||||
saveOptions: this._saveOptions,
|
||||
});
|
||||
}
|
||||
|
||||
// Snapshot is not explicitly set to false, import as file attachment
|
||||
attachment.linkMode = "imported_url";
|
||||
|
||||
// Import from document
|
||||
if(attachment.document) {
|
||||
if (attachment.document) {
|
||||
Zotero.debug('Importing attachment from document');
|
||||
attachment.linkMode = "imported_url";
|
||||
|
||||
|
@ -897,17 +955,6 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
});
|
||||
}
|
||||
|
||||
let mimeType = attachment.mimeType ? attachment.mimeType : null;
|
||||
let fileBaseName;
|
||||
if (parentItemID) {
|
||||
let parentItem = yield Zotero.Items.getAsync(parentItemID);
|
||||
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
|
||||
}
|
||||
|
||||
attachment.linkMode = "imported_url";
|
||||
|
||||
attachmentCallback(attachment, 0);
|
||||
|
||||
// Import from SingleFile content
|
||||
if (attachment.snapshotContent) {
|
||||
Zotero.debug('Importing attachment from SingleFile');
|
||||
|
@ -924,6 +971,13 @@ Zotero.Translate.ItemSaver.prototype = {
|
|||
}
|
||||
|
||||
// Import from URL
|
||||
let mimeType = attachment.mimeType ? attachment.mimeType : null;
|
||||
let fileBaseName;
|
||||
if (parentItemID) {
|
||||
let parentItem = yield Zotero.Items.getAsync(parentItemID);
|
||||
fileBaseName = Zotero.Attachments.getFileBaseNameFromItem(parentItem, { attachmentTitle: title });
|
||||
}
|
||||
|
||||
Zotero.debug('Importing attachment from URL');
|
||||
return Zotero.Attachments.importFromURL({
|
||||
libraryID: this._libraryID,
|
||||
|
|
|
@ -105,6 +105,7 @@ const xpcomFilesLocal = [
|
|||
'feedReader',
|
||||
'fileDragDataProvider',
|
||||
'fulltext',
|
||||
'httpIntegrationClient',
|
||||
'id',
|
||||
'integration',
|
||||
'locale',
|
||||
|
@ -124,8 +125,12 @@ const xpcomFilesLocal = [
|
|||
'retractions',
|
||||
'router',
|
||||
'schema',
|
||||
'server',
|
||||
'server_integration',
|
||||
'server/server',
|
||||
'server/server_integration',
|
||||
'server/server_connector',
|
||||
'server/server_connectorIntegration',
|
||||
'server/server_localAPI',
|
||||
'server/saveSession',
|
||||
'session',
|
||||
'streamer',
|
||||
'style',
|
||||
|
@ -152,10 +157,6 @@ const xpcomFilesLocal = [
|
|||
'users',
|
||||
'translation/translate_item',
|
||||
'translation/translators',
|
||||
'connector/httpIntegrationClient',
|
||||
'connector/server_connector',
|
||||
'connector/server_connectorIntegration',
|
||||
'localAPI/server_localAPI',
|
||||
];
|
||||
|
||||
Components.utils.import("resource://gre/modules/ComponentUtils.jsm");
|
||||
|
|
|
@ -1177,7 +1177,8 @@ async function startHTTPServer(port = null) {
|
|||
if (!port) {
|
||||
port = httpdServerPort;
|
||||
}
|
||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");;
|
||||
var httpd = new HttpServer();
|
||||
while (true) {
|
||||
try {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,5 @@
|
|||
describe("HiddenBrowser", function() {
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");
|
||||
const { HiddenBrowser } = ChromeUtils.import(
|
||||
"chrome://zotero/content/HiddenBrowser.jsm"
|
||||
);
|
||||
|
@ -11,7 +12,7 @@ describe("HiddenBrowser", function() {
|
|||
var pngRequested = false;
|
||||
|
||||
before(function () {
|
||||
Cu.import("resource://zotero-unit/httpd.js");
|
||||
Zotero.debug(HttpServer);
|
||||
httpd = new HttpServer();
|
||||
httpd.start(port);
|
||||
});
|
||||
|
@ -93,7 +94,6 @@ describe("HiddenBrowser", function() {
|
|||
}
|
||||
|
||||
before(function () {
|
||||
Cu.import("resource://zotero-unit/httpd.js");
|
||||
httpd = new HttpServer();
|
||||
httpd.start(port);
|
||||
|
||||
|
@ -190,7 +190,6 @@ describe("HiddenBrowser", function() {
|
|||
var baseURL2 = `http://127.0.0.1:${port2}/`;
|
||||
|
||||
before(function () {
|
||||
Cu.import("resource://zotero-unit/httpd.js");
|
||||
// Create two servers with two separate origins
|
||||
httpd1 = new HttpServer();
|
||||
httpd1.start(port1);
|
||||
|
|
|
@ -1268,7 +1268,7 @@ describe("Zotero.Attachments", function() {
|
|||
var resolvers = [{
|
||||
name: 'Custom',
|
||||
method: 'get',
|
||||
// Registered with httpd.js in beforeEach()
|
||||
// Registered with HTTPD.jsm in beforeEach()
|
||||
url: baseURL + "{doi}",
|
||||
mode: 'html',
|
||||
selector: '#pdf-link',
|
||||
|
|
|
@ -1385,7 +1385,7 @@ describe("Zotero.CollectionTree", function() {
|
|||
|
||||
|
||||
describe("with feed items", function () {
|
||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");;
|
||||
|
||||
const httpdPort = 16214;
|
||||
var httpd;
|
||||
|
|
|
@ -469,7 +469,7 @@ describe("Zotero.File", function () {
|
|||
|
||||
before(async function () {
|
||||
// Real HTTP server
|
||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");;
|
||||
port = 16213;
|
||||
httpd = new HttpServer();
|
||||
baseURL = `http://127.0.0.1:${port}`;
|
||||
|
|
|
@ -13,7 +13,7 @@ describe("Zotero.HTTP", function () {
|
|||
|
||||
before(function* () {
|
||||
// Real HTTP server
|
||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");;
|
||||
httpd = new HttpServer();
|
||||
httpd.start(port);
|
||||
httpd.registerPathHandler(
|
||||
|
|
|
@ -1004,7 +1004,7 @@ describe("Zotero.ItemTree", function() {
|
|||
|
||||
// Serve a PDF to test URL dragging
|
||||
before(function () {
|
||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");;
|
||||
httpd = new HttpServer();
|
||||
httpd.start(port);
|
||||
var file = getTestDataDirectory();
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('Zotero_Import_Mendeley', function () {
|
|||
Components.utils.import('chrome://zotero/content/import/mendeley/mendeleyImport.js');
|
||||
|
||||
// A real HTTP server is used to deliver a Bitcoin PDF so that annotations can be processed during import.
|
||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");;
|
||||
const port = 16213;
|
||||
httpd = new HttpServer();
|
||||
httpdURL = `http://127.0.0.1:${port}`;
|
||||
|
|
|
@ -37,7 +37,7 @@ describe("Document Recognition", function() {
|
|||
}
|
||||
|
||||
queue.cancel();
|
||||
Zotero.RecognizeDocument.recognizeStub = null;
|
||||
Zotero.RecognizeDocument._recognize.restore && Zotero.RecognizeDocument._recognize.restore();
|
||||
Zotero.Prefs.clear('autoRenameFiles.linked');
|
||||
});
|
||||
|
||||
|
@ -223,9 +223,9 @@ describe("Document Recognition", function() {
|
|||
it("should rename a linked file attachment using parent metadata if no existing file attachments and pref enabled", async function () {
|
||||
Zotero.Prefs.set('autoRenameFiles.linked', true);
|
||||
var itemTitle = Zotero.Utilities.randomString();
|
||||
Zotero.RecognizeDocument.recognizeStub = async function () {
|
||||
sinon.stub(Zotero.RecognizeDocument, "_recognize").callsFake(() => {
|
||||
return createDataObject('item', { title: itemTitle });
|
||||
};
|
||||
});
|
||||
|
||||
// Link to the PDF
|
||||
var tempDir = await getTempDirectory();
|
||||
|
@ -263,9 +263,9 @@ describe("Document Recognition", function() {
|
|||
Zotero.Prefs.set('autoRenameFiles.fileTypes', 'x-nonexistent/type');
|
||||
|
||||
var itemTitle = Zotero.Utilities.randomString();
|
||||
Zotero.RecognizeDocument.recognizeStub = async function () {
|
||||
sinon.stub(Zotero.RecognizeDocument, "_recognize").callsFake(() => {
|
||||
return createDataObject('item', { title: itemTitle });
|
||||
};
|
||||
});
|
||||
|
||||
var attachment = await importPDFAttachment();
|
||||
assert.equal(attachment.getField('title'), 'test');
|
||||
|
@ -291,9 +291,9 @@ describe("Document Recognition", function() {
|
|||
it("shouldn't rename a linked file attachment using parent metadata if pref disabled", async function () {
|
||||
Zotero.Prefs.set('autoRenameFiles.linked', false);
|
||||
var itemTitle = Zotero.Utilities.randomString();
|
||||
Zotero.RecognizeDocument.recognizeStub = async function () {
|
||||
sinon.stub(Zotero.RecognizeDocument, "_recognize").callsFake(() => {
|
||||
return createDataObject('item', { title: itemTitle });
|
||||
};
|
||||
});
|
||||
|
||||
// Link to the PDF
|
||||
var tempDir = await getTempDirectory();
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
"use strict";
|
||||
|
||||
Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
describe("Zotero.Server", function () {
|
||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||
var serverPath;
|
||||
|
||||
before(function* () {
|
||||
|
@ -284,10 +285,53 @@ describe("Zotero.Server", function () {
|
|||
}
|
||||
);
|
||||
|
||||
assert.ok(called);
|
||||
assert.equal(req.status, 204);
|
||||
});
|
||||
});
|
||||
describe("application/pdf", function () {
|
||||
it('should provide a stream', async function () {
|
||||
let called = false;
|
||||
let endpoint = "/test/" + Zotero.Utilities.randomString();
|
||||
let file = getTestDataDirectory();
|
||||
file.append('test.pdf');
|
||||
let contents = await Zotero.File.getBinaryContentsAsync(file);
|
||||
|
||||
Zotero.Server.Endpoints[endpoint] = function () {};
|
||||
Zotero.Server.Endpoints[endpoint].prototype = {
|
||||
supportedMethods: ["POST"],
|
||||
supportedDataTypes: ["application/pdf"],
|
||||
|
||||
init: function (options) {
|
||||
called = true;
|
||||
assert.isObject(options);
|
||||
assert.property(options.headers, "Content-Type");
|
||||
assert(options.headers["Content-Type"].startsWith("application/pdf"));
|
||||
assert.isFunction(options.data.available);
|
||||
let data = NetUtil.readInputStreamToString(options.data, options.headers['content-length']);
|
||||
assert.equal(data, contents);
|
||||
|
||||
return 204;
|
||||
}
|
||||
};
|
||||
|
||||
let pdf = await File.createFromFileName(OS.Path.join(getTestDataDirectory().path, 'test.pdf'));
|
||||
|
||||
let req = await Zotero.HTTP.request(
|
||||
"POST",
|
||||
serverPath + endpoint,
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
},
|
||||
body: pdf
|
||||
}
|
||||
);
|
||||
|
||||
assert.ok(called);
|
||||
assert.equal(req.status, 204);
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -25,8 +25,9 @@ describe("MacOS Integration Server", function () {
|
|||
);
|
||||
|
||||
assert.isTrue(stub.calledOnce);
|
||||
assert.isTrue(stub.firstCall.calledWithExactly('httpTest', 'httpTestCommand', 'docName', '-1'));
|
||||
} finally {
|
||||
assert.deepEqual(stub.firstCall.args, ['httpTest', 'httpTestCommand', 'docName', '-1']);
|
||||
}
|
||||
finally {
|
||||
stub.restore();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
new function() {
|
||||
Components.utils.import("resource://zotero-unit/httpd.js");
|
||||
var { HttpServer } = ChromeUtils.import("chrome://remote/content/server/HTTPD.jsm");;
|
||||
|
||||
const { HiddenBrowser } = ChromeUtils.import('chrome://zotero/content/HiddenBrowser.jsm');
|
||||
|
||||
|
|
|
@ -823,7 +823,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
|||
}
|
||||
);
|
||||
|
||||
// Use httpd.js instead of sinon so we get a real nsIURL with a channel
|
||||
// Use HTTPD.jsm instead of sinon so we get a real nsIURL with a channel
|
||||
Zotero.Prefs.set("sync.storage.url", davHostPath);
|
||||
|
||||
// Begin install procedure
|
||||
|
@ -849,7 +849,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
|||
|
||||
|
||||
it("should show an error for a 404 for the parent directory", function* () {
|
||||
// Use httpd.js instead of sinon so we get a real nsIURL with a channel
|
||||
// Use HTTPD.jsm instead of sinon so we get a real nsIURL with a channel
|
||||
Zotero.HTTP.mock = null;
|
||||
Zotero.Prefs.set("sync.storage.url", davHostPath);
|
||||
|
||||
|
@ -937,7 +937,7 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
|
|||
}
|
||||
);
|
||||
|
||||
// Use httpd.js instead of sinon so we get a real nsIURL with a channel
|
||||
// Use HTTPD.jsm instead of sinon so we get a real nsIURL with a channel
|
||||
Zotero.Prefs.set("sync.storage.url", davHostPath);
|
||||
|
||||
// Begin install procedure
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue