Connector attachment saving server changes for 7.0 (#5345)

This commit is contained in:
Adomas Ven 2025-06-19 08:01:52 +03:00 committed by GitHub
parent 1dad1ae0f8
commit cd856efef8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 2991 additions and 10015 deletions

View file

@ -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;

View file

@ -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);
}

View file

@ -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

View file

@ -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;
};
/**

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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');

View 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');
}
}
};

View file

@ -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);

File diff suppressed because it is too large Load diff

View file

@ -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,

View file

@ -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");

View file

@ -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

View file

@ -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);

View file

@ -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',

View file

@ -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;

View file

@ -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}`;

View file

@ -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(

View file

@ -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();

View file

@ -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}`;

View file

@ -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();

View file

@ -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

View file

@ -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();
}
});

View file

@ -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');

View file

@ -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